Spaces:
Sleeping
Sleeping
Update pyPDAF/mcp_output/mcp_plugin/mcp_service.py
Browse files- pyPDAF/mcp_output/mcp_plugin/mcp_service.py +551 -1686
pyPDAF/mcp_output/mcp_plugin/mcp_service.py
CHANGED
|
@@ -1,1741 +1,606 @@
|
|
| 1 |
"""
|
| 2 |
-
MCP
|
| 3 |
|
| 4 |
-
This module provides MCP
|
| 5 |
-
|
| 6 |
-
|
| 7 |
"""
|
| 8 |
|
| 9 |
-
import os
|
| 10 |
import sys
|
| 11 |
-
|
|
|
|
|
|
|
| 12 |
|
| 13 |
import numpy as np
|
| 14 |
-
|
| 15 |
-
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 16 |
-
sys.path.insert(0, source_path)
|
| 17 |
-
|
| 18 |
from fastmcp import FastMCP
|
| 19 |
-
from
|
|
|
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
|
| 24 |
# ============================================================================
|
| 25 |
-
#
|
| 26 |
# ============================================================================
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
"""
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
Returns:
|
| 39 |
-
dict: A dictionary containing the full observation dimension.
|
| 40 |
-
"""
|
| 41 |
-
try:
|
| 42 |
-
dim_obs_f = PDAF.gather_dim_obs_f(dim_obs_p)
|
| 43 |
-
return {"success": True, "result": {"dim_obs_f": int(dim_obs_f)}, "error": None}
|
| 44 |
-
except Exception as e:
|
| 45 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
@mcp.tool(name="pdaf_gather_obs_f", description="Gather full observation vector from process-local pieces")
|
| 49 |
-
def pdaf_gather_obs_f(obs_p: List[float], dimobs_f: int) -> dict:
|
| 50 |
-
"""
|
| 51 |
-
Concatenate process-local observation vectors into a full vector (local-filter use).
|
| 52 |
-
|
| 53 |
-
Args:
|
| 54 |
-
obs_p: Process-local observation vector.
|
| 55 |
-
dimobs_f: Full observation dimension.
|
| 56 |
-
|
| 57 |
-
Returns:
|
| 58 |
-
dict: A dictionary containing the gathered observation vector and status.
|
| 59 |
-
"""
|
| 60 |
-
try:
|
| 61 |
-
obs_p_np = np.array(obs_p, dtype=np.float64)
|
| 62 |
-
obs_f, status = PDAF.gather_obs_f(obs_p_np, dimobs_f)
|
| 63 |
-
return {
|
| 64 |
-
"success": True,
|
| 65 |
-
"result": {"obs_f": obs_f.tolist(), "status": int(status)},
|
| 66 |
-
"error": None,
|
| 67 |
-
}
|
| 68 |
-
except Exception as e:
|
| 69 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
@mcp.tool(name="pdaf_gather_obs_f2", description="Gather observation coordinates from local pieces")
|
| 73 |
-
def pdaf_gather_obs_f2(coords_p: List[List[float]], nrows: int, dimobs_f: int) -> dict:
|
| 74 |
-
"""
|
| 75 |
-
Concatenate process-local observation coordinate arrays into a full coordinate array.
|
| 76 |
-
|
| 77 |
-
Args:
|
| 78 |
-
coords_p: Local coordinate array shaped [nrows, dimobs_p].
|
| 79 |
-
nrows: Number of coordinate rows (e.g., spatial dimensions).
|
| 80 |
-
dimobs_f: Full observation dimension.
|
| 81 |
-
|
| 82 |
-
Returns:
|
| 83 |
-
dict: A dictionary containing the gathered coordinates and status.
|
| 84 |
-
"""
|
| 85 |
-
try:
|
| 86 |
-
coords_p_np = np.array(coords_p, dtype=np.float64)
|
| 87 |
-
coords_f, status = PDAF.gather_obs_f2(coords_p_np, nrows, dimobs_f)
|
| 88 |
-
return {
|
| 89 |
-
"success": True,
|
| 90 |
-
"result": {"coords_f": coords_f.tolist(), "status": int(status)},
|
| 91 |
-
"error": None,
|
| 92 |
-
}
|
| 93 |
-
except Exception as e:
|
| 94 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
@mcp.tool(name="pdaf_gather_obs_f_flex", description="Gather full observation vector without PDAF-internal metadata")
|
| 98 |
-
def pdaf_gather_obs_f_flex(dim_obs_p: int, dim_obs_f: int, obs_p: List[float]) -> dict:
|
| 99 |
-
"""
|
| 100 |
-
Flexibly gather observation vectors using explicit local/global sizes.
|
| 101 |
-
|
| 102 |
-
Args:
|
| 103 |
-
dim_obs_p: Process-local observation dimension.
|
| 104 |
-
dim_obs_f: Full observation dimension.
|
| 105 |
-
obs_p: Local observation vector.
|
| 106 |
-
|
| 107 |
-
Returns:
|
| 108 |
-
dict: A dictionary containing the gathered observation vector and status.
|
| 109 |
-
"""
|
| 110 |
-
try:
|
| 111 |
-
obs_p_np = np.array(obs_p, dtype=np.float64)
|
| 112 |
-
obs_f, status = PDAF.gather_obs_f_flex(dim_obs_p, dim_obs_f, obs_p_np)
|
| 113 |
-
return {
|
| 114 |
-
"success": True,
|
| 115 |
-
"result": {"obs_f": obs_f.tolist(), "status": int(status)},
|
| 116 |
-
"error": None,
|
| 117 |
-
}
|
| 118 |
-
except Exception as e:
|
| 119 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
@mcp.tool(name="pdaf_gather_obs_f2_flex", description="Gather observation coordinates without PDAF-internal metadata")
|
| 123 |
-
def pdaf_gather_obs_f2_flex(dim_obs_p: int, dim_obs_f: int, coords_p: List[List[float]], nrows: int) -> dict:
|
| 124 |
-
"""
|
| 125 |
-
Flexibly gather 2D observation coordinate arrays using explicit local/global sizes.
|
| 126 |
-
|
| 127 |
-
Args:
|
| 128 |
-
dim_obs_p: Process-local observation dimension.
|
| 129 |
-
dim_obs_f: Full observation dimension.
|
| 130 |
-
coords_p: Local coordinate array shaped [nrows, dim_obs_p].
|
| 131 |
-
nrows: Number of coordinate rows (e.g., spatial dimensions).
|
| 132 |
-
|
| 133 |
-
Returns:
|
| 134 |
-
dict: A dictionary containing the gathered coordinates and status.
|
| 135 |
-
"""
|
| 136 |
-
try:
|
| 137 |
-
coords_p_np = np.array(coords_p, dtype=np.float64)
|
| 138 |
-
coords_f, status = PDAF.gather_obs_f2_flex(dim_obs_p, dim_obs_f, coords_p_np, nrows)
|
| 139 |
-
return {
|
| 140 |
-
"success": True,
|
| 141 |
-
"result": {"coords_f": coords_f.tolist(), "status": int(status)},
|
| 142 |
-
"error": None,
|
| 143 |
-
}
|
| 144 |
-
except Exception as e:
|
| 145 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 146 |
-
|
| 147 |
-
@mcp.tool(name="pdaf_correlation_function", description="Calculate the value of a correlation function at a given distance")
|
| 148 |
-
def pdaf_correlation_function(ctype: int, length: float, distance: float) -> dict:
|
| 149 |
-
"""
|
| 150 |
-
Calculate the value of the chosen correlation function according to the specified length scale.
|
| 151 |
-
|
| 152 |
-
Args:
|
| 153 |
-
ctype: Type of correlation function
|
| 154 |
-
1: Gaussian with f(0)=1.0
|
| 155 |
-
2: 5th-order polynomial (Gaspari/Cohn, 1999)
|
| 156 |
-
length: Length scale of function
|
| 157 |
-
ctype=1: standard deviation
|
| 158 |
-
ctype=2: support length (f=0 for distance>length)
|
| 159 |
-
distance: Distance at which the function is evaluated
|
| 160 |
-
|
| 161 |
-
Returns:
|
| 162 |
-
dict: A dictionary containing the success status and the correlation value.
|
| 163 |
-
"""
|
| 164 |
-
try:
|
| 165 |
-
value = PDAF.correlation_function(ctype, length, distance)
|
| 166 |
-
return {"success": True, "result": {"value": float(value)}, "error": None}
|
| 167 |
-
except Exception as e:
|
| 168 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
@mcp.tool(name="pdaf_deallocate", description="Finalize the PDAF system and free allocated memory")
|
| 172 |
-
def pdaf_deallocate() -> dict:
|
| 173 |
-
"""
|
| 174 |
-
Finalise the PDAF system including freeing some of the memory used by PDAF.
|
| 175 |
-
|
| 176 |
-
Note: This function cannot free all allocated PDAF memory.
|
| 177 |
-
Therefore, one should not use PDAF.init afterwards.
|
| 178 |
-
|
| 179 |
-
Returns:
|
| 180 |
-
dict: A dictionary containing the success status.
|
| 181 |
-
"""
|
| 182 |
-
try:
|
| 183 |
-
PDAF.deallocate()
|
| 184 |
-
return {"success": True, "result": "PDAF memory deallocated successfully", "error": None}
|
| 185 |
-
except Exception as e:
|
| 186 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
@mcp.tool(name="pdaf_eofcovar", description="Perform EOF analysis of an ensemble of state vectors by SVD")
|
| 190 |
-
def pdaf_eofcovar(
|
| 191 |
-
dim: int,
|
| 192 |
-
nstates: int,
|
| 193 |
-
nfields: int,
|
| 194 |
-
dim_fields: List[int],
|
| 195 |
-
offsets: List[int],
|
| 196 |
-
remove_mstate: int,
|
| 197 |
-
do_mv: int,
|
| 198 |
-
states: List[List[float]],
|
| 199 |
-
meanstate: List[float],
|
| 200 |
-
verbose: int
|
| 201 |
-
) -> dict:
|
| 202 |
-
"""
|
| 203 |
-
EOF analysis of an ensemble of state vectors by singular value decomposition.
|
| 204 |
-
|
| 205 |
-
This function performs a singular value decomposition of the ensemble anomaly.
|
| 206 |
-
The singular values and corresponding singular vectors can be used to
|
| 207 |
-
construct a covariance matrix for the initial ensemble.
|
| 208 |
-
|
| 209 |
-
Args:
|
| 210 |
-
dim: Dimension of state vector
|
| 211 |
-
nstates: Number of state vectors
|
| 212 |
-
nfields: Number of fields in state vector
|
| 213 |
-
dim_fields: Size of each field (list of length nfields)
|
| 214 |
-
offsets: Start position of each field (list of length nfields)
|
| 215 |
-
remove_mstate: 1 to subtract mean state from states
|
| 216 |
-
do_mv: 1 for multivariate scaling; 0 for no scaling
|
| 217 |
-
states: State perturbations (2D list of shape [dim, nstates])
|
| 218 |
-
meanstate: Mean state (list of length dim)
|
| 219 |
-
verbose: Verbosity flag
|
| 220 |
-
|
| 221 |
-
Returns:
|
| 222 |
-
dict: A dictionary containing:
|
| 223 |
-
- states: Updated state perturbations
|
| 224 |
-
- stddev: Standard deviation of field variability
|
| 225 |
-
- svals: Singular values divided by sqrt(nstates-1)
|
| 226 |
-
- svec: Singular vectors
|
| 227 |
-
- meanstate: Updated mean state
|
| 228 |
-
- status: Status flag
|
| 229 |
-
"""
|
| 230 |
-
try:
|
| 231 |
-
states_np = np.array(states, dtype=np.float64)
|
| 232 |
-
meanstate_np = np.array(meanstate, dtype=np.float64)
|
| 233 |
-
dim_fields_np = np.array(dim_fields, dtype=np.int32)
|
| 234 |
-
offsets_np = np.array(offsets, dtype=np.int32)
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
def pdaf_get_fcst_info(steps: int = 0, time: float = 0.0, doexit: int = 0) -> dict:
|
| 279 |
-
"""
|
| 280 |
-
Return the number of time steps, current model time, and exit flag.
|
| 281 |
-
|
| 282 |
-
This is used when the flexible parallelization mode is used with
|
| 283 |
-
PDAF3.assimilate. This is also relevant for legacy assimilation functions.
|
| 284 |
-
|
| 285 |
-
Args:
|
| 286 |
-
steps: Number of forecast time steps (input can be arbitrary)
|
| 287 |
-
time: Current model time
|
| 288 |
-
doexit: Whether to exit from forecasts
|
| 289 |
-
|
| 290 |
-
Returns:
|
| 291 |
-
dict: A dictionary containing:
|
| 292 |
-
- steps: Number of forecast time steps for next assimilation
|
| 293 |
-
- time: Current model time
|
| 294 |
-
- doexit: Whether to exit from forecasts
|
| 295 |
-
"""
|
| 296 |
-
try:
|
| 297 |
-
steps_out, time_out, doexit_out = PDAF.get_fcst_info(steps, time, doexit)
|
| 298 |
-
return {
|
| 299 |
-
"success": True,
|
| 300 |
-
"result": {
|
| 301 |
-
"steps": int(steps_out),
|
| 302 |
-
"time": float(time_out),
|
| 303 |
-
"doexit": int(doexit_out)
|
| 304 |
-
},
|
| 305 |
-
"error": None
|
| 306 |
-
}
|
| 307 |
-
except Exception as e:
|
| 308 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
@mcp.tool(name="pdaf_print_filter_types", description="Print available filter types in PDAF")
|
| 312 |
-
def pdaf_print_filter_types(verbose: int = 1) -> dict:
|
| 313 |
-
"""
|
| 314 |
-
Print all available filter types in PDAF to the console.
|
| 315 |
-
|
| 316 |
-
Args:
|
| 317 |
-
verbose: Verbosity flag. If 0, no output; if > 0, prints list to stdout.
|
| 318 |
-
|
| 319 |
-
Returns:
|
| 320 |
-
dict: A dictionary containing the success status.
|
| 321 |
-
"""
|
| 322 |
-
try:
|
| 323 |
-
PDAF.print_filter_types(verbose)
|
| 324 |
-
return {"success": True, "result": "Filter types printed to console", "error": None}
|
| 325 |
-
except Exception as e:
|
| 326 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 327 |
|
| 328 |
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
dict: A dictionary containing the success status.
|
| 339 |
-
"""
|
| 340 |
-
try:
|
| 341 |
-
PDAF.print_da_types(verbose)
|
| 342 |
-
return {"success": True, "result": "DA types printed to console", "error": None}
|
| 343 |
-
except Exception as e:
|
| 344 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 345 |
|
| 346 |
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
|
| 358 |
-
|
| 359 |
-
dict: A dictionary containing the success status.
|
| 360 |
-
"""
|
| 361 |
-
try:
|
| 362 |
-
PDAF.print_info(printtype)
|
| 363 |
-
return {"success": True, "result": f"Info type {printtype} printed to console", "error": None}
|
| 364 |
-
except Exception as e:
|
| 365 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 366 |
|
| 367 |
|
| 368 |
-
|
| 369 |
-
|
|
|
|
| 370 |
"""
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
The forgetting factor is used for covariance inflation.
|
| 374 |
-
A value less than 1.0 inflates the ensemble spread.
|
| 375 |
-
|
| 376 |
-
For local ensemble Kalman filters, the forgetting factor can be set
|
| 377 |
-
either globally (outside the loop over local domains) or differently
|
| 378 |
-
for each local analysis domain (within the loop).
|
| 379 |
-
|
| 380 |
-
Args:
|
| 381 |
-
forget_in: New forgetting factor value
|
| 382 |
-
|
| 383 |
-
Returns:
|
| 384 |
-
dict: A dictionary containing the success status.
|
| 385 |
"""
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
|
|
|
|
|
|
|
|
|
| 391 |
|
| 392 |
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
dict: A dictionary containing the success status.
|
| 406 |
-
"""
|
| 407 |
-
try:
|
| 408 |
-
PDAF.set_debug_flag(debugval)
|
| 409 |
-
return {"success": True, "result": f"Debug flag set to {debugval}", "error": None}
|
| 410 |
-
except Exception as e:
|
| 411 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 412 |
|
| 413 |
|
| 414 |
-
|
| 415 |
-
def pdaf_set_offline_mode(screen: int) -> dict:
|
| 416 |
"""
|
| 417 |
-
|
| 418 |
|
| 419 |
Args:
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
Returns:
|
| 423 |
-
dict: A dictionary containing the success status.
|
| 424 |
-
"""
|
| 425 |
-
try:
|
| 426 |
-
PDAF.set_offline_mode(screen)
|
| 427 |
-
return {"success": True, "result": f"Offline mode set with screen={screen}", "error": None}
|
| 428 |
-
except Exception as e:
|
| 429 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
@mcp.tool(name="pdaf_get_assim_flag", description="Get the flag indicating if DA was performed in last time step")
|
| 433 |
-
def pdaf_get_assim_flag() -> dict:
|
| 434 |
-
"""
|
| 435 |
-
Return the flag that indicates if the DA is performed in the last time step.
|
| 436 |
-
This only works for online DA systems.
|
| 437 |
|
| 438 |
Returns:
|
| 439 |
-
|
| 440 |
-
- did_assim: 1 for assimilation performed, 0 otherwise
|
| 441 |
"""
|
| 442 |
-
|
| 443 |
-
did_assim = PDAF.get_assim_flag()
|
| 444 |
-
return {"success": True, "result": {"did_assim": int(did_assim)}, "error": None}
|
| 445 |
-
except Exception as e:
|
| 446 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
@mcp.tool(name="pdaf_get_localfilter", description="Check whether a local filter is used")
|
| 450 |
-
def pdaf_get_localfilter() -> dict:
|
| 451 |
-
"""
|
| 452 |
-
Return whether a local filter is used.
|
| 453 |
-
|
| 454 |
-
Returns:
|
| 455 |
-
dict: A dictionary containing:
|
| 456 |
-
- lfilter: 1 for local filters (domain-localized), 0 for global filters
|
| 457 |
-
"""
|
| 458 |
-
try:
|
| 459 |
-
lfilter = PDAF.get_localfilter()
|
| 460 |
-
return {"success": True, "result": {"lfilter": int(lfilter)}, "error": None}
|
| 461 |
-
except Exception as e:
|
| 462 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
@mcp.tool(name="pdaf_get_memberid", description="Get the ensemble member ID on the current process")
|
| 466 |
-
def pdaf_get_memberid(memberid: int = 0) -> dict:
|
| 467 |
-
"""
|
| 468 |
-
Return the ensemble member ID on the current process.
|
| 469 |
-
|
| 470 |
-
This can be called during ensemble integration if ensemble-specific
|
| 471 |
-
forcing is read. It can also be used in user-supplied functions.
|
| 472 |
-
|
| 473 |
-
Args:
|
| 474 |
-
memberid: Input member ID (can be any value)
|
| 475 |
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
|
| 486 |
|
| 487 |
# ============================================================================
|
| 488 |
-
#
|
| 489 |
# ============================================================================
|
| 490 |
|
| 491 |
-
@mcp.tool(
|
| 492 |
-
|
| 493 |
-
""
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
|
| 553 |
|
| 554 |
# ============================================================================
|
| 555 |
-
#
|
| 556 |
# ============================================================================
|
| 557 |
|
| 558 |
-
@mcp.tool(
|
| 559 |
-
|
| 560 |
-
""
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
return {"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 594 |
except Exception as e:
|
| 595 |
-
return {"
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
@mcp.tool(name="pdafomi_check_error", description="Check PDAFomi internal error flag")
|
| 599 |
-
def pdafomi_check_error(flag: int = 0) -> dict:
|
| 600 |
-
"""
|
| 601 |
-
Check the value of the PDAF-OMI internal error flag.
|
| 602 |
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
|
| 607 |
-
|
| 608 |
-
|
| 609 |
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 620 |
|
| 621 |
-
@mcp.tool(name="pdafomi_set_debug_flag", description="Activate or deactivate PDAFomi debug output")
|
| 622 |
-
def pdafomi_set_debug_flag(debugval: int) -> dict:
|
| 623 |
-
"""
|
| 624 |
-
Activate or deactivate debug output for PDAFomi.
|
| 625 |
-
|
| 626 |
-
Args:
|
| 627 |
-
debugval: Value for debugging flag (0 to disable, non-zero to enable)
|
| 628 |
-
|
| 629 |
-
Returns:
|
| 630 |
-
dict: A dictionary containing the success status.
|
| 631 |
-
"""
|
| 632 |
-
try:
|
| 633 |
-
PDAFomi.set_debug_flag(debugval)
|
| 634 |
-
return {"success": True, "result": f"PDAFomi debug flag set to {debugval}", "error": None}
|
| 635 |
-
except Exception as e:
|
| 636 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 637 |
|
|
|
|
|
|
|
|
|
|
| 638 |
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
Set the doassim attribute for a given observation type.
|
| 643 |
-
|
| 644 |
-
Args:
|
| 645 |
-
i_obs: Index of observation type
|
| 646 |
-
doassim: 0) do not assimilate; 1) assimilate the observation type
|
| 647 |
-
|
| 648 |
-
Returns:
|
| 649 |
-
dict: A dictionary containing the success status.
|
| 650 |
-
"""
|
| 651 |
-
try:
|
| 652 |
-
PDAFomi.set_doassim(i_obs, doassim)
|
| 653 |
-
return {"success": True, "result": f"Observation type {i_obs} doassim set to {doassim}", "error": None}
|
| 654 |
-
except Exception as e:
|
| 655 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
@mcp.tool(name="pdafomi_set_disttype", description="Set distance calculation method for observation localization")
|
| 659 |
-
def pdafomi_set_disttype(i_obs: int, disttype: int) -> dict:
|
| 660 |
-
"""
|
| 661 |
-
Set the observation localization distance calculation method.
|
| 662 |
-
|
| 663 |
-
Args:
|
| 664 |
-
i_obs: Index of observation type
|
| 665 |
-
disttype: Type of distance calculation:
|
| 666 |
-
0) Cartesian (any units)
|
| 667 |
-
1) Cartesian periodic (any units)
|
| 668 |
-
2) Geographic distance in metres (lat/lon in radians)
|
| 669 |
-
3) Haversine formula for distance on sphere
|
| 670 |
-
10) 3D Cartesian with separate horizontal/vertical
|
| 671 |
-
11) 3D Cartesian periodic with separate horizontal/vertical
|
| 672 |
-
12) Geographic horizontal + user vertical
|
| 673 |
-
13) Haversine horizontal + user vertical
|
| 674 |
-
|
| 675 |
-
Returns:
|
| 676 |
-
dict: A dictionary containing the success status.
|
| 677 |
-
"""
|
| 678 |
-
try:
|
| 679 |
-
PDAFomi.set_disttype(i_obs, disttype)
|
| 680 |
-
return {"success": True, "result": f"Observation type {i_obs} disttype set to {disttype}", "error": None}
|
| 681 |
-
except Exception as e:
|
| 682 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
@mcp.tool(name="pdafomi_set_ncoord", description="Set number of spatial dimensions for observations")
|
| 686 |
-
def pdafomi_set_ncoord(i_obs: int, ncoord: int) -> dict:
|
| 687 |
-
"""
|
| 688 |
-
Set the number of spatial dimensions of observations.
|
| 689 |
-
|
| 690 |
-
Args:
|
| 691 |
-
i_obs: Index of observation type
|
| 692 |
-
ncoord: Dimension of the observation coordinate (e.g., 2 for 2D)
|
| 693 |
-
|
| 694 |
-
Returns:
|
| 695 |
-
dict: A dictionary containing the success status.
|
| 696 |
-
"""
|
| 697 |
-
try:
|
| 698 |
-
PDAFomi.set_ncoord(i_obs, ncoord)
|
| 699 |
-
return {"success": True, "result": f"Observation type {i_obs} ncoord set to {ncoord}", "error": None}
|
| 700 |
-
except Exception as e:
|
| 701 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
@mcp.tool(name="pdafomi_diag_nobstypes", description="Get number of active observation types")
|
| 705 |
-
def pdafomi_diag_nobstypes(nobs: int = 0) -> dict:
|
| 706 |
-
"""
|
| 707 |
-
Get the number of observation types that are active in an assimilation run.
|
| 708 |
-
|
| 709 |
-
Args:
|
| 710 |
-
nobs: Number of observation types (input can be arbitrary)
|
| 711 |
-
|
| 712 |
-
Returns:
|
| 713 |
-
dict: A dictionary containing:
|
| 714 |
-
- nobs: Number of active observation types
|
| 715 |
-
"""
|
| 716 |
-
try:
|
| 717 |
-
result = PDAFomi.diag_nobstypes(nobs)
|
| 718 |
-
return {"success": True, "result": {"nobs": int(result)}, "error": None}
|
| 719 |
-
except Exception as e:
|
| 720 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
@mcp.tool(name="pdafomi_diag_dimobs", description="Get observation dimensions for each observation type")
|
| 724 |
-
def pdafomi_diag_dimobs() -> dict:
|
| 725 |
-
"""
|
| 726 |
-
Get observation dimension for each observation type.
|
| 727 |
-
|
| 728 |
-
Returns:
|
| 729 |
-
dict: A dictionary containing:
|
| 730 |
-
- dim_obs: Observation dimension for each type (list)
|
| 731 |
-
"""
|
| 732 |
-
try:
|
| 733 |
-
result = PDAFomi.diag_dimobs()
|
| 734 |
-
return {"success": True, "result": {"dim_obs": result.tolist()}, "error": None}
|
| 735 |
-
except Exception as e:
|
| 736 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
# ============================================================================
|
| 740 |
-
# PDAFlocal Module Tools
|
| 741 |
-
# ============================================================================
|
| 742 |
-
|
| 743 |
-
@mcp.tool(name="pdaflocal_set_indices", description="Set index mapping from local to global state vector")
|
| 744 |
-
def pdaflocal_set_indices(dim_l: int, map_indices: List[int]) -> dict:
|
| 745 |
-
"""
|
| 746 |
-
Set index vector to map local state vector to global state vectors.
|
| 747 |
-
|
| 748 |
-
This is called in the user-supplied function py__init_dim_l_pdaf.
|
| 749 |
-
Each element of map is an index of the global state vector (1-based).
|
| 750 |
-
|
| 751 |
-
E.g., map[0] = 2 means that the first element of local state vector
|
| 752 |
-
is the 2nd element of the global state vector.
|
| 753 |
-
|
| 754 |
-
Args:
|
| 755 |
-
dim_l: Dimension of local state vector
|
| 756 |
-
map_indices: Index array for mapping between local and global state vector
|
| 757 |
-
|
| 758 |
-
Returns:
|
| 759 |
-
dict: A dictionary containing the success status.
|
| 760 |
-
"""
|
| 761 |
-
try:
|
| 762 |
-
map_np = np.array(map_indices, dtype=np.int32)
|
| 763 |
-
PDAFlocal.set_indices(dim_l, map_np)
|
| 764 |
-
return {"success": True, "result": f"Local indices set for dim_l={dim_l}", "error": None}
|
| 765 |
-
except Exception as e:
|
| 766 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
@mcp.tool(name="pdaflocal_set_increment_weights", description="Set local increment weights for vertical localization")
|
| 770 |
-
def pdaflocal_set_increment_weights(dim_l: int, weights: List[float]) -> dict:
|
| 771 |
-
"""
|
| 772 |
-
Initialize a PDAF-internal local array of increment weights.
|
| 773 |
-
|
| 774 |
-
The weights are applied where the local state vector is weighted.
|
| 775 |
-
These can be used to apply vertical localization or implement
|
| 776 |
-
weakly-coupled assimilation.
|
| 777 |
-
|
| 778 |
-
Args:
|
| 779 |
-
dim_l: Dimension of local state vector
|
| 780 |
-
weights: Weights array (list of length dim_l)
|
| 781 |
-
|
| 782 |
-
Returns:
|
| 783 |
-
dict: A dictionary containing the success status.
|
| 784 |
-
"""
|
| 785 |
-
try:
|
| 786 |
-
weights_np = np.array(weights, dtype=np.float64)
|
| 787 |
-
PDAFlocal.set_increment_weights(dim_l, weights_np)
|
| 788 |
-
return {"success": True, "result": f"Increment weights set for dim_l={dim_l}", "error": None}
|
| 789 |
-
except Exception as e:
|
| 790 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
@mcp.tool(name="pdaflocal_clear_increment_weights", description="Deallocate local increment weight vector")
|
| 794 |
-
def pdaflocal_clear_increment_weights() -> dict:
|
| 795 |
-
"""
|
| 796 |
-
Deallocate the local increment weight vector set by set_increment_weights.
|
| 797 |
-
|
| 798 |
-
Returns:
|
| 799 |
-
dict: A dictionary containing the success status.
|
| 800 |
-
"""
|
| 801 |
-
try:
|
| 802 |
-
PDAFlocal.clear_increment_weights()
|
| 803 |
-
return {"success": True, "result": "Increment weights cleared", "error": None}
|
| 804 |
-
except Exception as e:
|
| 805 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 806 |
-
|
| 807 |
-
|
| 808 |
-
# ============================================================================
|
| 809 |
-
# Additional PDAF Diagnostic Tools
|
| 810 |
-
# ============================================================================
|
| 811 |
-
|
| 812 |
-
@mcp.tool(name="pdaf_diag_stddev_nompi", description="Compute ensemble standard deviation without MPI")
|
| 813 |
-
def pdaf_diag_stddev_nompi(
|
| 814 |
-
dim: int,
|
| 815 |
-
dim_ens: int,
|
| 816 |
-
ens: List[List[float]],
|
| 817 |
-
do_mean: int
|
| 818 |
-
) -> dict:
|
| 819 |
-
"""
|
| 820 |
-
Compute ensemble standard deviation and ensemble mean without MPI.
|
| 821 |
-
|
| 822 |
-
Args:
|
| 823 |
-
dim: State dimension
|
| 824 |
-
dim_ens: Ensemble size
|
| 825 |
-
ens: State ensemble (2D list of shape [dim, dim_ens])
|
| 826 |
-
do_mean: Whether to compute ensemble mean (1=yes, 0=no)
|
| 827 |
-
|
| 828 |
-
Returns:
|
| 829 |
-
dict: A dictionary containing:
|
| 830 |
-
- state: State vector (ensemble mean if do_mean=1)
|
| 831 |
-
- stddev: Standard deviation of ensemble
|
| 832 |
-
- status: Status flag (0=success)
|
| 833 |
-
"""
|
| 834 |
-
try:
|
| 835 |
-
ens_np = np.array(ens, dtype=np.float64)
|
| 836 |
-
state = np.zeros(dim, dtype=np.float64)
|
| 837 |
-
|
| 838 |
-
state_out, stddev, status = PDAF.diag_stddev_nompi(dim, dim_ens, state, ens_np, do_mean)
|
| 839 |
-
return {
|
| 840 |
-
"success": True,
|
| 841 |
-
"result": {
|
| 842 |
-
"state": state_out.tolist(),
|
| 843 |
-
"stddev": float(stddev),
|
| 844 |
-
"status": int(status)
|
| 845 |
-
},
|
| 846 |
-
"error": None
|
| 847 |
-
}
|
| 848 |
-
except Exception as e:
|
| 849 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
@mcp.tool(name="pdaf_diag_variance_nompi", description="Compute ensemble variance without MPI")
|
| 853 |
-
def pdaf_diag_variance_nompi(
|
| 854 |
-
dim: int,
|
| 855 |
-
dim_ens: int,
|
| 856 |
-
ens: List[List[float]],
|
| 857 |
-
do_mean: int,
|
| 858 |
-
do_stddev: int
|
| 859 |
-
) -> dict:
|
| 860 |
-
"""
|
| 861 |
-
Compute ensemble variance/standard deviation and mean without MPI.
|
| 862 |
-
|
| 863 |
-
Args:
|
| 864 |
-
dim: State dimension
|
| 865 |
-
dim_ens: Ensemble size
|
| 866 |
-
ens: State ensemble (2D list of shape [dim, dim_ens])
|
| 867 |
-
do_mean: Whether to compute ensemble mean (1=yes, 0=no)
|
| 868 |
-
do_stddev: Whether to compute the ensemble mean standard deviation (1=yes, 0=no)
|
| 869 |
-
|
| 870 |
-
Returns:
|
| 871 |
-
dict: A dictionary containing:
|
| 872 |
-
- state: State vector (ensemble mean if do_mean=1)
|
| 873 |
-
- variance: Variance state vector
|
| 874 |
-
- stddev: Standard deviation of ensemble
|
| 875 |
-
- status: Status flag (0=success)
|
| 876 |
-
"""
|
| 877 |
-
try:
|
| 878 |
-
ens_np = np.array(ens, dtype=np.float64)
|
| 879 |
-
state = np.zeros(dim, dtype=np.float64)
|
| 880 |
-
|
| 881 |
-
state_out, variance, stddev, status = PDAF.diag_variance_nompi(
|
| 882 |
-
dim, dim_ens, state, ens_np, do_mean, do_stddev
|
| 883 |
-
)
|
| 884 |
-
return {
|
| 885 |
-
"success": True,
|
| 886 |
-
"result": {
|
| 887 |
-
"state": state_out.tolist(),
|
| 888 |
-
"variance": variance.tolist(),
|
| 889 |
-
"stddev": float(stddev),
|
| 890 |
-
"status": int(status)
|
| 891 |
-
},
|
| 892 |
-
"error": None
|
| 893 |
-
}
|
| 894 |
-
except Exception as e:
|
| 895 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 896 |
-
|
| 897 |
-
|
| 898 |
-
@mcp.tool(name="pdaf_diag_rmsd_nompi", description="Compute RMSD between two vectors without MPI")
|
| 899 |
-
def pdaf_diag_rmsd_nompi(
|
| 900 |
-
dim_p: int,
|
| 901 |
-
statea_p: List[float],
|
| 902 |
-
stateb_p: List[float]
|
| 903 |
-
) -> dict:
|
| 904 |
-
"""
|
| 905 |
-
Compute the root mean squared distance between two vectors without MPI.
|
| 906 |
-
|
| 907 |
-
Args:
|
| 908 |
-
dim_p: State dimension
|
| 909 |
-
statea_p: State vector A (list of length dim_p)
|
| 910 |
-
stateb_p: State vector B (list of length dim_p)
|
| 911 |
-
|
| 912 |
-
Returns:
|
| 913 |
-
dict: A dictionary containing:
|
| 914 |
-
- rmsd_p: Root mean squared distance
|
| 915 |
-
- status: Status flag (0=success)
|
| 916 |
-
"""
|
| 917 |
-
try:
|
| 918 |
-
statea_np = np.array(statea_p, dtype=np.float64)
|
| 919 |
-
stateb_np = np.array(stateb_p, dtype=np.float64)
|
| 920 |
-
|
| 921 |
-
rmsd_p, status = PDAF.diag_rmsd_nompi(dim_p, statea_np, stateb_np)
|
| 922 |
-
return {
|
| 923 |
-
"success": True,
|
| 924 |
-
"result": {
|
| 925 |
-
"rmsd_p": float(rmsd_p),
|
| 926 |
-
"status": int(status)
|
| 927 |
-
},
|
| 928 |
-
"error": None
|
| 929 |
-
}
|
| 930 |
-
except Exception as e:
|
| 931 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 932 |
-
|
| 933 |
-
|
| 934 |
-
@mcp.tool(name="pdaf_diag_ensstats", description="Compute ensemble skewness and kurtosis")
|
| 935 |
-
def pdaf_diag_ensstats(
|
| 936 |
-
dim: int,
|
| 937 |
-
dim_ens: int,
|
| 938 |
-
element: int,
|
| 939 |
-
ens: List[List[float]]
|
| 940 |
-
) -> dict:
|
| 941 |
-
"""
|
| 942 |
-
Compute the skewness and kurtosis of the ensemble for a given state vector element.
|
| 943 |
-
|
| 944 |
-
The definition used for kurtosis follows Lawson & Hansen (2004).
|
| 945 |
-
|
| 946 |
-
Args:
|
| 947 |
-
dim: PE-local state dimension
|
| 948 |
-
dim_ens: Ensemble size
|
| 949 |
-
element: ID of element to be used
|
| 950 |
-
ens: State ensemble (2D list of shape [dim, dim_ens])
|
| 951 |
-
|
| 952 |
-
Returns:
|
| 953 |
-
dict: A dictionary containing:
|
| 954 |
-
- skewness: Skewness of ensemble
|
| 955 |
-
- kurtosis: Kurtosis of ensemble
|
| 956 |
-
- status: Status flag (0=success)
|
| 957 |
-
"""
|
| 958 |
-
try:
|
| 959 |
-
ens_np = np.array(ens, dtype=np.float64)
|
| 960 |
-
state = np.zeros(dim, dtype=np.float64)
|
| 961 |
-
|
| 962 |
-
skewness, kurtosis, status = PDAF.diag_ensstats(dim, dim_ens, element, state, ens_np)
|
| 963 |
-
return {
|
| 964 |
-
"success": True,
|
| 965 |
-
"result": {
|
| 966 |
-
"skewness": float(skewness),
|
| 967 |
-
"kurtosis": float(kurtosis),
|
| 968 |
-
"status": int(status)
|
| 969 |
-
},
|
| 970 |
-
"error": None
|
| 971 |
-
}
|
| 972 |
-
except Exception as e:
|
| 973 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 974 |
-
|
| 975 |
-
|
| 976 |
-
@mcp.tool(name="pdaf_sample_ens", description="Generate ensemble from EOF modes and singular values")
|
| 977 |
-
def pdaf_sample_ens(
|
| 978 |
-
dim: int,
|
| 979 |
-
dim_ens: int,
|
| 980 |
-
modes: List[List[float]],
|
| 981 |
-
svals: List[float],
|
| 982 |
-
state: List[float],
|
| 983 |
-
verbose: int,
|
| 984 |
-
flag: int = 0
|
| 985 |
-
) -> dict:
|
| 986 |
-
"""
|
| 987 |
-
Generate an ensemble from singular values and their vectors (EOF modes).
|
| 988 |
-
|
| 989 |
-
The singular values and vectors are derived from ensemble anomalies.
|
| 990 |
-
This ensemble anomaly can be obtained from a time anomaly of a model
|
| 991 |
-
trajectory using PDAF.eofcovar.
|
| 992 |
-
|
| 993 |
-
Args:
|
| 994 |
-
dim: Size of the state vector
|
| 995 |
-
dim_ens: Ensemble size
|
| 996 |
-
modes: Array of EOF modes/matrix of singular vectors (shape [dim, dim_ens-1])
|
| 997 |
-
svals: Singular values (list of length dim_ens-1)
|
| 998 |
-
state: PE-local model mean state (list of length dim)
|
| 999 |
-
verbose: Verbosity flag
|
| 1000 |
-
flag: Status flag input
|
| 1001 |
-
|
| 1002 |
-
Returns:
|
| 1003 |
-
dict: A dictionary containing:
|
| 1004 |
-
- modes: Updated EOF modes
|
| 1005 |
-
- state: Updated mean state
|
| 1006 |
-
- ens: Generated state ensemble (shape [dim, dim_ens])
|
| 1007 |
-
- flag: Status flag
|
| 1008 |
-
"""
|
| 1009 |
-
try:
|
| 1010 |
-
modes_np = np.array(modes, dtype=np.float64)
|
| 1011 |
-
svals_np = np.array(svals, dtype=np.float64)
|
| 1012 |
-
state_np = np.array(state, dtype=np.float64)
|
| 1013 |
-
|
| 1014 |
-
modes_out, state_out, ens, flag_out = PDAF.sample_ens(
|
| 1015 |
-
dim, dim_ens, modes_np, svals_np, state_np, verbose, flag
|
| 1016 |
-
)
|
| 1017 |
-
return {
|
| 1018 |
-
"success": True,
|
| 1019 |
-
"result": {
|
| 1020 |
-
"modes": modes_out.tolist(),
|
| 1021 |
-
"state": state_out.tolist(),
|
| 1022 |
-
"ens": ens.tolist(),
|
| 1023 |
-
"flag": int(flag_out)
|
| 1024 |
-
},
|
| 1025 |
-
"error": None
|
| 1026 |
-
}
|
| 1027 |
-
except Exception as e:
|
| 1028 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1029 |
-
|
| 1030 |
-
|
| 1031 |
-
@mcp.tool(name="pdaf_local_weight", description="Compute localization weight for a given distance")
|
| 1032 |
-
def pdaf_local_weight(
|
| 1033 |
-
wtype: int,
|
| 1034 |
-
rtype: int,
|
| 1035 |
-
cradius: float,
|
| 1036 |
-
sradius: float,
|
| 1037 |
-
distance: float,
|
| 1038 |
-
nrows: int,
|
| 1039 |
-
ncols: int,
|
| 1040 |
-
a: List[List[float]],
|
| 1041 |
-
var_obs: float,
|
| 1042 |
-
verbose: int
|
| 1043 |
-
) -> dict:
|
| 1044 |
-
"""
|
| 1045 |
-
Get localization weight for given distance, cut-off radius, support radius,
|
| 1046 |
-
weighting type, and weighting function.
|
| 1047 |
-
|
| 1048 |
-
Args:
|
| 1049 |
-
wtype: Type of weight function:
|
| 1050 |
-
0: unit weight (weight=1 up to distance=cradius)
|
| 1051 |
-
1: exponential decrease (weight=1/e at distance=sradius)
|
| 1052 |
-
2: 5th order polynomial (Gaspari and Cohn 1999)
|
| 1053 |
-
rtype: Type of regulated weighting:
|
| 1054 |
-
!=1: no regulation
|
| 1055 |
-
1: regulated by variance of matrix A and observation variance
|
| 1056 |
-
cradius: Cut-off radius where weight=0 beyond it
|
| 1057 |
-
sradius: Support radius of localization function
|
| 1058 |
-
distance: Distance to observation
|
| 1059 |
-
nrows: Number of rows in matrix A
|
| 1060 |
-
ncols: Number of columns in matrix A
|
| 1061 |
-
a: Ensemble perturbation/anomaly matrix (shape [nrows, ncols])
|
| 1062 |
-
var_obs: Observation variance
|
| 1063 |
-
verbose: Verbosity flag
|
| 1064 |
-
|
| 1065 |
-
Returns:
|
| 1066 |
-
dict: A dictionary containing:
|
| 1067 |
-
- weight: Localization weight
|
| 1068 |
-
"""
|
| 1069 |
-
try:
|
| 1070 |
-
a_np = np.array(a, dtype=np.float64)
|
| 1071 |
-
|
| 1072 |
-
weight = PDAF.local_weight(
|
| 1073 |
-
wtype, rtype, cradius, sradius, distance,
|
| 1074 |
-
nrows, ncols, a_np, var_obs, verbose
|
| 1075 |
-
)
|
| 1076 |
-
return {"success": True, "result": {"weight": float(weight)}, "error": None}
|
| 1077 |
-
except Exception as e:
|
| 1078 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
@mcp.tool(name="pdaf_get_local_type", description="Get the localization type of the selected filter")
|
| 1082 |
-
def pdaf_get_local_type() -> dict:
|
| 1083 |
-
"""
|
| 1084 |
-
Return the information on the localization type of the selected filter.
|
| 1085 |
-
|
| 1086 |
-
Returns:
|
| 1087 |
-
dict: A dictionary containing:
|
| 1088 |
-
- localtype: Localization type
|
| 1089 |
-
0: no localization; global filter
|
| 1090 |
-
1: domain localization (LESTKF, LETKF, LNETF, LSEIK)
|
| 1091 |
-
2: covariance localization (LEnKF)
|
| 1092 |
-
3: covariance loc. but observation handling like domain localization (ENSRF)
|
| 1093 |
-
"""
|
| 1094 |
-
try:
|
| 1095 |
-
localtype = PDAF.get_local_type()
|
| 1096 |
-
return {"success": True, "result": {"localtype": int(localtype)}, "error": None}
|
| 1097 |
-
except Exception as e:
|
| 1098 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
@mcp.tool(name="pdaf_set_ens_pointer", description="Get a numpy array view of the internal ensemble array")
|
| 1102 |
-
def pdaf_set_ens_pointer() -> dict:
|
| 1103 |
-
"""
|
| 1104 |
-
Return the ensemble in a numpy array with the same memory address as
|
| 1105 |
-
PDAF's internal ensemble array, allowing for manual ensemble modification.
|
| 1106 |
-
|
| 1107 |
-
Returns:
|
| 1108 |
-
dict: A dictionary containing:
|
| 1109 |
-
- ens_shape: Shape of the ensemble array [dim, dim_ens]
|
| 1110 |
-
- status: Status flag
|
| 1111 |
-
"""
|
| 1112 |
-
try:
|
| 1113 |
-
ens_ptr, status = PDAF.set_ens_pointer()
|
| 1114 |
-
return {
|
| 1115 |
-
"success": True,
|
| 1116 |
-
"result": {
|
| 1117 |
-
"ens_shape": list(ens_ptr.shape),
|
| 1118 |
-
"ens": ens_ptr.tolist(),
|
| 1119 |
-
"status": int(status)
|
| 1120 |
-
},
|
| 1121 |
-
"error": None
|
| 1122 |
-
}
|
| 1123 |
-
except Exception as e:
|
| 1124 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
# ============================================================================
|
| 1128 |
-
# Additional PDAFomi Tools
|
| 1129 |
-
# ============================================================================
|
| 1130 |
-
|
| 1131 |
-
@mcp.tool(name="pdafomi_set_obs_err_type", description="Set observation error distribution type")
|
| 1132 |
-
def pdafomi_set_obs_err_type(i_obs: int, obs_err_type: int) -> dict:
|
| 1133 |
-
"""
|
| 1134 |
-
Set the type of observation error distribution for a given observation type.
|
| 1135 |
-
|
| 1136 |
-
Args:
|
| 1137 |
-
i_obs: Index of observation type
|
| 1138 |
-
obs_err_type: Type of observation error distribution:
|
| 1139 |
-
0: Gaussian (default)
|
| 1140 |
-
1: double exponential (Laplacian)
|
| 1141 |
-
|
| 1142 |
-
Returns:
|
| 1143 |
-
dict: A dictionary containing the success status.
|
| 1144 |
-
"""
|
| 1145 |
-
try:
|
| 1146 |
-
PDAFomi.set_obs_err_type(i_obs, obs_err_type)
|
| 1147 |
-
return {"success": True, "result": f"Observation type {i_obs} error type set to {obs_err_type}", "error": None}
|
| 1148 |
-
except Exception as e:
|
| 1149 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1150 |
-
|
| 1151 |
-
|
| 1152 |
-
@mcp.tool(name="pdafomi_set_use_global_obs", description="Set whether to use global or process-local observations")
|
| 1153 |
-
def pdafomi_set_use_global_obs(i_obs: int, use_global_obs: int) -> dict:
|
| 1154 |
-
"""
|
| 1155 |
-
Set switch for using process-local or global observations.
|
| 1156 |
-
|
| 1157 |
-
By default (use_global_obs=1), PDAF-OMI gathers the entire observation
|
| 1158 |
-
vector for all processes. Setting use_global_obs=0 uses only process-local
|
| 1159 |
-
observations, which can save computational cost.
|
| 1160 |
-
|
| 1161 |
-
Args:
|
| 1162 |
-
i_obs: Index of observation type
|
| 1163 |
-
use_global_obs: 0: Using process-local observations
|
| 1164 |
-
1: Using cross-process observations (default)
|
| 1165 |
-
|
| 1166 |
-
Returns:
|
| 1167 |
-
dict: A dictionary containing the success status.
|
| 1168 |
-
"""
|
| 1169 |
-
try:
|
| 1170 |
-
PDAFomi.set_use_global_obs(i_obs, use_global_obs)
|
| 1171 |
-
return {"success": True, "result": f"Observation type {i_obs} use_global_obs set to {use_global_obs}", "error": None}
|
| 1172 |
-
except Exception as e:
|
| 1173 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
@mcp.tool(name="pdafomi_diag_obs_rmsd", description="Compute RMSD between observations and observed model state")
|
| 1177 |
-
def pdafomi_diag_obs_rmsd(nobs: int, verbose: int) -> dict:
|
| 1178 |
-
"""
|
| 1179 |
-
Compute root mean squared distance between observations and observed
|
| 1180 |
-
model state for each observation type.
|
| 1181 |
-
|
| 1182 |
-
Args:
|
| 1183 |
-
nobs: Number of observation types
|
| 1184 |
-
verbose: Verbosity flag
|
| 1185 |
-
|
| 1186 |
-
Returns:
|
| 1187 |
-
dict: A dictionary containing:
|
| 1188 |
-
- nobs: Number of observation types
|
| 1189 |
-
- rmsd: Vector of RMSD values for each observation type
|
| 1190 |
-
"""
|
| 1191 |
-
try:
|
| 1192 |
-
nobs_out, rmsd = PDAFomi.diag_obs_rmsd(nobs, verbose)
|
| 1193 |
-
return {
|
| 1194 |
-
"success": True,
|
| 1195 |
-
"result": {
|
| 1196 |
-
"nobs": int(nobs_out),
|
| 1197 |
-
"rmsd": rmsd.tolist()
|
| 1198 |
-
},
|
| 1199 |
-
"error": None
|
| 1200 |
-
}
|
| 1201 |
-
except Exception as e:
|
| 1202 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1203 |
-
|
| 1204 |
-
|
| 1205 |
-
@mcp.tool(name="pdafomi_diag_stats", description="Compute statistics comparing observations and observed ensemble mean")
|
| 1206 |
-
def pdafomi_diag_stats(nobs: int, verbose: int) -> dict:
|
| 1207 |
-
"""
|
| 1208 |
-
Compute a selection of 6 statistics comparing observations and
|
| 1209 |
-
observed ensemble mean for each observation type.
|
| 1210 |
-
|
| 1211 |
-
Statistics include:
|
| 1212 |
-
- (1,:) correlations between observation and observed ensemble mean
|
| 1213 |
-
- (2,:) centered RMS difference
|
| 1214 |
-
- (3,:) mean bias (observation minus observed ensemble mean)
|
| 1215 |
-
- (4,:) mean absolute difference
|
| 1216 |
-
- (5,:) variance of observations
|
| 1217 |
-
- (6,:) variance of observed ensemble mean
|
| 1218 |
-
|
| 1219 |
-
Args:
|
| 1220 |
-
nobs: Number of observation types
|
| 1221 |
-
verbose: Verbosity flag
|
| 1222 |
-
|
| 1223 |
-
Returns:
|
| 1224 |
-
dict: A dictionary containing:
|
| 1225 |
-
- nobs: Number of observation types
|
| 1226 |
-
- obsstats: Array of observation statistics (shape [6, nobs])
|
| 1227 |
-
"""
|
| 1228 |
-
try:
|
| 1229 |
-
nobs_out, obsstats = PDAFomi.diag_stats(nobs, verbose)
|
| 1230 |
-
return {
|
| 1231 |
-
"success": True,
|
| 1232 |
-
"result": {
|
| 1233 |
-
"nobs": int(nobs_out),
|
| 1234 |
-
"obsstats": obsstats.tolist()
|
| 1235 |
-
},
|
| 1236 |
-
"error": None
|
| 1237 |
-
}
|
| 1238 |
-
except Exception as e:
|
| 1239 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1240 |
-
|
| 1241 |
-
|
| 1242 |
-
# ============================================================================
|
| 1243 |
-
# Additional PDAF Setter Functions (不需要回调和MPI)
|
| 1244 |
-
# ============================================================================
|
| 1245 |
-
|
| 1246 |
-
@mcp.tool(name="pdaf_set_comm_pdaf", description="Set the MPI communicator used by PDAF")
|
| 1247 |
-
def pdaf_set_comm_pdaf(in_comm_pdaf: int) -> dict:
|
| 1248 |
-
"""
|
| 1249 |
-
Set the MPI communicator used by PDAF.
|
| 1250 |
-
|
| 1251 |
-
By default, PDAF assumes it can use all available processes (MPI_COMM_WORLD).
|
| 1252 |
-
By using this function, we limit the number of processes that can be used
|
| 1253 |
-
by PDAF to the given MPI communicator.
|
| 1254 |
-
|
| 1255 |
-
Args:
|
| 1256 |
-
in_comm_pdaf: MPI communicator for PDAF (integer handle)
|
| 1257 |
-
|
| 1258 |
-
Returns:
|
| 1259 |
-
dict: A dictionary containing the success status.
|
| 1260 |
-
"""
|
| 1261 |
-
try:
|
| 1262 |
-
PDAF.set_comm_pdaf(in_comm_pdaf)
|
| 1263 |
-
return {"success": True, "result": f"PDAF communicator set to {in_comm_pdaf}", "error": None}
|
| 1264 |
-
except Exception as e:
|
| 1265 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1266 |
-
|
| 1267 |
-
|
| 1268 |
-
@mcp.tool(name="pdaf_set_iparam", description="Set integer parameters for PDAF")
|
| 1269 |
-
def pdaf_set_iparam(idval: int, value: int, flag: int = 0) -> dict:
|
| 1270 |
-
"""
|
| 1271 |
-
Set integer parameters for PDAF.
|
| 1272 |
-
|
| 1273 |
-
This function provides an alternative way to set integer parameters
|
| 1274 |
-
instead of providing all parameters in the call to PDAF.init.
|
| 1275 |
-
|
| 1276 |
-
Args:
|
| 1277 |
-
idval: Index of parameter
|
| 1278 |
-
value: Parameter value
|
| 1279 |
-
flag: Status flag input (default 0)
|
| 1280 |
-
|
| 1281 |
-
Returns:
|
| 1282 |
-
dict: A dictionary containing:
|
| 1283 |
-
- flag: Status flag (0 for no error)
|
| 1284 |
-
"""
|
| 1285 |
-
try:
|
| 1286 |
-
result = PDAF.set_iparam(idval, value, flag)
|
| 1287 |
-
return {"success": True, "result": {"flag": int(result)}, "error": None}
|
| 1288 |
-
except Exception as e:
|
| 1289 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1290 |
-
|
| 1291 |
-
|
| 1292 |
-
@mcp.tool(name="pdaf_set_rparam", description="Set floating-point parameters for PDAF")
|
| 1293 |
-
def pdaf_set_rparam(idval: int, value: float, flag: int = 0) -> dict:
|
| 1294 |
-
"""
|
| 1295 |
-
Set floating-point parameters for PDAF.
|
| 1296 |
-
|
| 1297 |
-
This function provides an alternative way to set real parameters
|
| 1298 |
-
instead of providing all parameters in the call to PDAF.init.
|
| 1299 |
-
|
| 1300 |
-
Args:
|
| 1301 |
-
idval: Index of parameter
|
| 1302 |
-
value: Parameter value (float)
|
| 1303 |
-
flag: Status flag input (default 0)
|
| 1304 |
-
|
| 1305 |
-
Returns:
|
| 1306 |
-
dict: A dictionary containing:
|
| 1307 |
-
- flag: Status flag (0 for no error)
|
| 1308 |
-
"""
|
| 1309 |
-
try:
|
| 1310 |
-
result = PDAF.set_rparam(idval, value, flag)
|
| 1311 |
-
return {"success": True, "result": {"flag": int(result)}, "error": None}
|
| 1312 |
-
except Exception as e:
|
| 1313 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1314 |
-
|
| 1315 |
-
|
| 1316 |
-
@mcp.tool(name="pdaf_set_memberid", description="Set the ensemble member index to a given value")
|
| 1317 |
-
def pdaf_set_memberid(memberid: int) -> dict:
|
| 1318 |
-
"""
|
| 1319 |
-
Set the ensemble member index to a given value.
|
| 1320 |
-
|
| 1321 |
-
Args:
|
| 1322 |
-
memberid: Index in the local ensemble
|
| 1323 |
-
|
| 1324 |
-
Returns:
|
| 1325 |
-
dict: A dictionary containing:
|
| 1326 |
-
- memberid: The set member index
|
| 1327 |
-
"""
|
| 1328 |
-
try:
|
| 1329 |
-
result = PDAF.set_memberid(memberid)
|
| 1330 |
-
return {"success": True, "result": {"memberid": int(result)}, "error": None}
|
| 1331 |
-
except Exception as e:
|
| 1332 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1333 |
-
|
| 1334 |
-
|
| 1335 |
-
@mcp.tool(name="pdaf_set_seedset", description="Choose a seedset for the random number generator")
|
| 1336 |
-
def pdaf_set_seedset(seedset_in: int) -> dict:
|
| 1337 |
-
"""
|
| 1338 |
-
Choose a seedset for the random number generator used in PDAF.
|
| 1339 |
-
|
| 1340 |
-
Args:
|
| 1341 |
-
seedset_in: Seedset index (1-20)
|
| 1342 |
-
|
| 1343 |
-
Returns:
|
| 1344 |
-
dict: A dictionary containing the success status.
|
| 1345 |
-
"""
|
| 1346 |
-
try:
|
| 1347 |
-
PDAF.set_seedset(seedset_in)
|
| 1348 |
-
return {"success": True, "result": f"Seedset set to {seedset_in}", "error": None}
|
| 1349 |
-
except Exception as e:
|
| 1350 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1351 |
-
|
| 1352 |
-
|
| 1353 |
-
@mcp.tool(name="pdaf_get_obsmemberid", description="Get ensemble member ID when observation operator is applied")
|
| 1354 |
-
def pdaf_get_obsmemberid(memberid: int = 0) -> dict:
|
| 1355 |
-
"""
|
| 1356 |
-
Return the ensemble member ID when observation operator is being applied.
|
| 1357 |
-
|
| 1358 |
-
This function is used specifically for user-supplied function py__obs_op_pdaf.
|
| 1359 |
-
|
| 1360 |
-
Args:
|
| 1361 |
-
memberid: Input member ID (can be any value)
|
| 1362 |
-
|
| 1363 |
-
Returns:
|
| 1364 |
-
dict: A dictionary containing:
|
| 1365 |
-
- memberid: Index in the local ensemble
|
| 1366 |
-
"""
|
| 1367 |
-
try:
|
| 1368 |
-
result = PDAF.get_obsmemberid(memberid)
|
| 1369 |
-
return {"success": True, "result": {"memberid": int(result)}, "error": None}
|
| 1370 |
-
except Exception as e:
|
| 1371 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1372 |
-
|
| 1373 |
-
|
| 1374 |
-
@mcp.tool(name="pdaf_local_weights", description="Get a vector of localization weights for given distances")
|
| 1375 |
-
def pdaf_local_weights(
|
| 1376 |
-
wtype: int,
|
| 1377 |
-
cradius: float,
|
| 1378 |
-
sradius: float,
|
| 1379 |
-
dim: int,
|
| 1380 |
-
distance: List[float],
|
| 1381 |
-
verbose: int
|
| 1382 |
-
) -> dict:
|
| 1383 |
-
"""
|
| 1384 |
-
Get a vector of localization weights for given distances.
|
| 1385 |
-
|
| 1386 |
-
This is a vectorized version of pdaf_local_weight without regulation.
|
| 1387 |
-
|
| 1388 |
-
Args:
|
| 1389 |
-
wtype: Type of weight function:
|
| 1390 |
-
0: unit weight (weight=1 up to distance=cradius)
|
| 1391 |
-
1: exponential decrease (weight=1/e at distance=sradius)
|
| 1392 |
-
2: 5th order polynomial (Gaspari and Cohn 1999)
|
| 1393 |
-
cradius: Cut-off radius where weight=0 beyond it
|
| 1394 |
-
sradius: Support radius of localization function
|
| 1395 |
-
dim: Size of distance and weight arrays
|
| 1396 |
-
distance: Array of distances to observations (list of length dim)
|
| 1397 |
-
verbose: Verbosity flag
|
| 1398 |
-
|
| 1399 |
-
Returns:
|
| 1400 |
-
dict: A dictionary containing:
|
| 1401 |
-
- weights: Array of localization weights (list of length dim)
|
| 1402 |
-
"""
|
| 1403 |
-
try:
|
| 1404 |
-
distance_np = np.array(distance, dtype=np.float64)
|
| 1405 |
-
weights = PDAF.local_weights(wtype, cradius, sradius, dim, distance_np, verbose)
|
| 1406 |
-
return {"success": True, "result": {"weights": weights.tolist()}, "error": None}
|
| 1407 |
-
except Exception as e:
|
| 1408 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1409 |
-
|
| 1410 |
-
|
| 1411 |
-
@mcp.tool(name="pdaf_diag_crps_nompi", description="Compute Continuous Ranked Probability Score without MPI")
|
| 1412 |
-
def pdaf_diag_crps_nompi(
|
| 1413 |
-
dim: int,
|
| 1414 |
-
dim_ens: int,
|
| 1415 |
-
element: int,
|
| 1416 |
-
oens: List[List[float]],
|
| 1417 |
-
obs: List[float]
|
| 1418 |
-
) -> dict:
|
| 1419 |
-
"""
|
| 1420 |
-
Obtain a Continuous Ranked Probability Score (CRPS) for an ensemble without MPI.
|
| 1421 |
-
|
| 1422 |
-
Based on Hersbach (2000) decomposition of CRPS.
|
| 1423 |
-
|
| 1424 |
-
Args:
|
| 1425 |
-
dim: Dimension of state vector
|
| 1426 |
-
dim_ens: Ensemble size
|
| 1427 |
-
element: ID of element to be used (0 for mean over all elements)
|
| 1428 |
-
oens: State ensemble (2D list of shape [dim, dim_ens])
|
| 1429 |
-
obs: Observation/true state (list of length dim)
|
| 1430 |
-
|
| 1431 |
-
Returns:
|
| 1432 |
-
dict: A dictionary containing:
|
| 1433 |
-
- CRPS: Continuous Ranked Probability Score
|
| 1434 |
-
- reli: Reliability
|
| 1435 |
-
- resol: Resolution
|
| 1436 |
-
- uncert: Uncertainty
|
| 1437 |
-
- status: Status flag (0=success)
|
| 1438 |
-
"""
|
| 1439 |
-
try:
|
| 1440 |
-
oens_np = np.array(oens, dtype=np.float64)
|
| 1441 |
-
obs_np = np.array(obs, dtype=np.float64)
|
| 1442 |
-
|
| 1443 |
-
crps, reli, resol, uncert, status = PDAF.diag_crps_nompi(
|
| 1444 |
-
dim, dim_ens, element, oens_np, obs_np
|
| 1445 |
-
)
|
| 1446 |
-
return {
|
| 1447 |
-
"success": True,
|
| 1448 |
-
"result": {
|
| 1449 |
-
"CRPS": float(crps),
|
| 1450 |
-
"reli": float(reli),
|
| 1451 |
-
"resol": float(resol),
|
| 1452 |
-
"uncert": float(uncert),
|
| 1453 |
-
"status": int(status)
|
| 1454 |
-
},
|
| 1455 |
-
"error": None
|
| 1456 |
-
}
|
| 1457 |
-
except Exception as e:
|
| 1458 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1459 |
-
|
| 1460 |
-
|
| 1461 |
-
@mcp.tool(name="pdaf_diag_compute_moments", description="Compute statistical moments from an ensemble")
|
| 1462 |
-
def pdaf_diag_compute_moments(
|
| 1463 |
-
dim_p: int,
|
| 1464 |
-
dim_ens: int,
|
| 1465 |
-
ens: List[List[float]],
|
| 1466 |
-
kmax: int,
|
| 1467 |
-
bias: int
|
| 1468 |
-
) -> dict:
|
| 1469 |
-
"""
|
| 1470 |
-
Compute the mean, variance, skewness, and excess kurtosis from an ensemble.
|
| 1471 |
-
|
| 1472 |
-
Args:
|
| 1473 |
-
dim_p: PE-local state dimension
|
| 1474 |
-
dim_ens: Ensemble size
|
| 1475 |
-
ens: State ensemble (2D list of shape [dim_p, dim_ens])
|
| 1476 |
-
kmax: Maximum moment to compute (1=mean, 2=variance, 3=skewness, 4=kurtosis)
|
| 1477 |
-
bias: 0 for unbiased estimates, 1 for biased estimates
|
| 1478 |
-
|
| 1479 |
-
Returns:
|
| 1480 |
-
dict: A dictionary containing:
|
| 1481 |
-
- moments: Array of moments (shape [kmax, dim_p])
|
| 1482 |
-
"""
|
| 1483 |
-
try:
|
| 1484 |
-
ens_np = np.array(ens, dtype=np.float64)
|
| 1485 |
-
moments = PDAF.diag_compute_moments(dim_p, dim_ens, ens_np, kmax, bias)
|
| 1486 |
-
return {"success": True, "result": {"moments": moments.tolist()}, "error": None}
|
| 1487 |
-
except Exception as e:
|
| 1488 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1489 |
-
|
| 1490 |
-
|
| 1491 |
-
# ============================================================================
|
| 1492 |
-
# Additional PDAFomi Setter Functions
|
| 1493 |
-
# ============================================================================
|
| 1494 |
-
|
| 1495 |
-
@mcp.tool(name="pdafomi_set_inno_omit", description="Set innovation threshold for removing observation outliers")
|
| 1496 |
-
def pdafomi_set_inno_omit(i_obs: int, inno_omit: float) -> dict:
|
| 1497 |
-
"""
|
| 1498 |
-
Set innovation threshold for removing observation outliers.
|
| 1499 |
-
|
| 1500 |
-
By default, no observations are omitted. Observation omission is only
|
| 1501 |
-
activated when inno_omit > 0.0. PDAF will omit observations where
|
| 1502 |
-
the squared innovation of the ensemble mean is larger than the product
|
| 1503 |
-
of inno_omit and observation error variance.
|
| 1504 |
-
|
| 1505 |
-
Args:
|
| 1506 |
-
i_obs: Index of observation type
|
| 1507 |
-
inno_omit: Threshold of innovation to be omitted
|
| 1508 |
-
|
| 1509 |
-
Returns:
|
| 1510 |
-
dict: A dictionary containing the success status.
|
| 1511 |
-
"""
|
| 1512 |
-
try:
|
| 1513 |
-
PDAFomi.set_inno_omit(i_obs, inno_omit)
|
| 1514 |
-
return {"success": True, "result": f"Observation type {i_obs} inno_omit set to {inno_omit}", "error": None}
|
| 1515 |
-
except Exception as e:
|
| 1516 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1517 |
-
|
| 1518 |
-
|
| 1519 |
-
@mcp.tool(name="pdafomi_set_inno_omit_ivar", description="Set inverse variance for omitted observations")
|
| 1520 |
-
def pdafomi_set_inno_omit_ivar(i_obs: int, inno_omit_ivar: float) -> dict:
|
| 1521 |
-
"""
|
| 1522 |
-
Set the inverse of observation error variance for omitted observations.
|
| 1523 |
-
|
| 1524 |
-
This should be set to a very small value relative to assimilated observations.
|
| 1525 |
-
By default, it is set to 1e-12.
|
| 1526 |
-
|
| 1527 |
-
Args:
|
| 1528 |
-
i_obs: Index of observation type
|
| 1529 |
-
inno_omit_ivar: Inverse of observation variance for omitted observations
|
| 1530 |
-
|
| 1531 |
-
Returns:
|
| 1532 |
-
dict: A dictionary containing the success status.
|
| 1533 |
-
"""
|
| 1534 |
-
try:
|
| 1535 |
-
PDAFomi.set_inno_omit_ivar(i_obs, inno_omit_ivar)
|
| 1536 |
-
return {"success": True, "result": f"Observation type {i_obs} inno_omit_ivar set to {inno_omit_ivar}", "error": None}
|
| 1537 |
-
except Exception as e:
|
| 1538 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1539 |
-
|
| 1540 |
-
|
| 1541 |
-
@mcp.tool(name="pdafomi_set_domainsize", description="Set domain size for observation type")
|
| 1542 |
-
def pdafomi_set_domainsize(i_obs: int, domainsize: List[float]) -> dict:
|
| 1543 |
-
"""
|
| 1544 |
-
Set the domain size for periodic boundary conditions.
|
| 1545 |
-
|
| 1546 |
-
This is used when disttype is set to periodic distance calculation.
|
| 1547 |
-
|
| 1548 |
-
Args:
|
| 1549 |
-
i_obs: Index of observation type
|
| 1550 |
-
domainsize: Domain size array (list of floats)
|
| 1551 |
-
|
| 1552 |
-
Returns:
|
| 1553 |
-
dict: A dictionary containing the success status.
|
| 1554 |
-
"""
|
| 1555 |
-
try:
|
| 1556 |
-
domainsize_np = np.array(domainsize, dtype=np.float64)
|
| 1557 |
-
PDAFomi.set_domainsize(i_obs, domainsize_np)
|
| 1558 |
-
return {"success": True, "result": f"Observation type {i_obs} domainsize set", "error": None}
|
| 1559 |
-
except Exception as e:
|
| 1560 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1561 |
-
|
| 1562 |
-
|
| 1563 |
-
@mcp.tool(name="pdafomi_set_name", description="Set the name of an observation type")
|
| 1564 |
-
def pdafomi_set_name(i_obs: int, name: str) -> dict:
|
| 1565 |
-
"""
|
| 1566 |
-
Set the name identifier for an observation type.
|
| 1567 |
-
|
| 1568 |
-
This name is used in diagnostic output to identify the observation type.
|
| 1569 |
-
|
| 1570 |
-
Args:
|
| 1571 |
-
i_obs: Index of observation type
|
| 1572 |
-
name: Name string for the observation type
|
| 1573 |
-
|
| 1574 |
-
Returns:
|
| 1575 |
-
dict: A dictionary containing the success status.
|
| 1576 |
-
"""
|
| 1577 |
-
try:
|
| 1578 |
-
PDAFomi.set_name(i_obs, name)
|
| 1579 |
-
return {"success": True, "result": f"Observation type {i_obs} name set to '{name}'", "error": None}
|
| 1580 |
-
except Exception as e:
|
| 1581 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1582 |
-
|
| 1583 |
-
|
| 1584 |
-
@mcp.tool(name="pdafomi_diag_get_obs", description="Get observation vector and coordinates for an observation type")
|
| 1585 |
-
def pdafomi_diag_get_obs(id_obs: int) -> dict:
|
| 1586 |
-
"""
|
| 1587 |
-
Get observation vector and corresponding coordinates for specified observation type.
|
| 1588 |
-
|
| 1589 |
-
Args:
|
| 1590 |
-
id_obs: Index of observation type to return
|
| 1591 |
-
|
| 1592 |
-
Returns:
|
| 1593 |
-
dict: A dictionary containing:
|
| 1594 |
-
- dim_obs_diag: Observation dimension
|
| 1595 |
-
- ncoord: Number of observation dimensions
|
| 1596 |
-
- obs_p: Observation vector
|
| 1597 |
-
- ocoord_p: Coordinate array
|
| 1598 |
-
"""
|
| 1599 |
-
try:
|
| 1600 |
-
dim_obs_diag, ncoord, obs_p, ocoord_p = PDAFomi.diag_get_obs(id_obs)
|
| 1601 |
-
return {
|
| 1602 |
-
"success": True,
|
| 1603 |
-
"result": {
|
| 1604 |
-
"dim_obs_diag": int(dim_obs_diag),
|
| 1605 |
-
"ncoord": int(ncoord),
|
| 1606 |
-
"obs_p": obs_p.tolist(),
|
| 1607 |
-
"ocoord_p": ocoord_p.tolist()
|
| 1608 |
-
},
|
| 1609 |
-
"error": None
|
| 1610 |
-
}
|
| 1611 |
-
except Exception as e:
|
| 1612 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1613 |
-
|
| 1614 |
-
|
| 1615 |
-
@mcp.tool(name="pdafomi_diag_get_hxmean", description="Get observed ensemble mean for an observation type")
|
| 1616 |
-
def pdafomi_diag_get_hxmean(id_obs: int) -> dict:
|
| 1617 |
-
"""
|
| 1618 |
-
Get observed ensemble mean for a given observation type.
|
| 1619 |
-
|
| 1620 |
-
Args:
|
| 1621 |
-
id_obs: Index of observation type to return
|
| 1622 |
-
|
| 1623 |
-
Returns:
|
| 1624 |
-
dict: A dictionary containing:
|
| 1625 |
-
- dim_obs_diag: Observation dimension
|
| 1626 |
-
- hxmean_p: Observed ensemble mean
|
| 1627 |
-
"""
|
| 1628 |
-
try:
|
| 1629 |
-
dim_obs_diag, hxmean_p = PDAFomi.diag_get_hxmean(id_obs)
|
| 1630 |
-
return {
|
| 1631 |
-
"success": True,
|
| 1632 |
-
"result": {
|
| 1633 |
-
"dim_obs_diag": int(dim_obs_diag),
|
| 1634 |
-
"hxmean_p": hxmean_p.tolist()
|
| 1635 |
-
},
|
| 1636 |
-
"error": None
|
| 1637 |
-
}
|
| 1638 |
-
except Exception as e:
|
| 1639 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1640 |
-
|
| 1641 |
-
|
| 1642 |
-
@mcp.tool(name="pdafomi_diag_get_ivar", description="Get inverse observation error variance for an observation type")
|
| 1643 |
-
def pdafomi_diag_get_ivar(id_obs: int) -> dict:
|
| 1644 |
-
"""
|
| 1645 |
-
Get inverse of observation error variance for a given observation type.
|
| 1646 |
-
|
| 1647 |
-
Args:
|
| 1648 |
-
id_obs: Index of observation type to return
|
| 1649 |
-
|
| 1650 |
-
Returns:
|
| 1651 |
-
dict: A dictionary containing:
|
| 1652 |
-
- dim_obs_diag: Observation dimension
|
| 1653 |
-
- ivar: Inverse observation error variances
|
| 1654 |
-
"""
|
| 1655 |
-
try:
|
| 1656 |
-
dim_obs_diag, ivar = PDAFomi.diag_get_ivar(id_obs)
|
| 1657 |
-
return {
|
| 1658 |
-
"success": True,
|
| 1659 |
-
"result": {
|
| 1660 |
-
"dim_obs_diag": int(dim_obs_diag),
|
| 1661 |
-
"ivar": ivar.tolist()
|
| 1662 |
-
},
|
| 1663 |
-
"error": None
|
| 1664 |
-
}
|
| 1665 |
-
except Exception as e:
|
| 1666 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1667 |
-
|
| 1668 |
-
|
| 1669 |
-
# ============================================================================
|
| 1670 |
-
# Utility Functions
|
| 1671 |
-
# ============================================================================
|
| 1672 |
-
|
| 1673 |
-
@mcp.tool(name="get_pdaf_module_info", description="Get information about available pyPDAF modules and their functions")
|
| 1674 |
-
def get_pdaf_module_info() -> dict:
|
| 1675 |
-
"""
|
| 1676 |
-
Get information about available pyPDAF modules and their main functions.
|
| 1677 |
-
|
| 1678 |
-
Returns:
|
| 1679 |
-
dict: A dictionary containing module information.
|
| 1680 |
-
"""
|
| 1681 |
-
try:
|
| 1682 |
-
info = {
|
| 1683 |
-
"PDAF": {
|
| 1684 |
-
"description": "Core PDAF module with filter functions and utilities",
|
| 1685 |
-
"main_functions": [
|
| 1686 |
-
"correlation_function", "deallocate", "eofcovar", "force_analysis",
|
| 1687 |
-
"get_fcst_info", "print_filter_types", "print_da_types", "print_info",
|
| 1688 |
-
"reset_forget", "set_debug_flag", "set_offline_mode", "sample_ens",
|
| 1689 |
-
"local_weight", "local_weights", "set_ens_pointer",
|
| 1690 |
-
"get_assim_flag", "get_localfilter", "get_local_type", "get_memberid",
|
| 1691 |
-
"diag_ensmean", "diag_stddev_nompi", "diag_stddev",
|
| 1692 |
-
"diag_variance_nompi", "diag_variance",
|
| 1693 |
-
"diag_rmsd_nompi", "diag_rmsd",
|
| 1694 |
-
"diag_effsample", "diag_ensstats", "diag_compute_moments"
|
| 1695 |
-
]
|
| 1696 |
-
},
|
| 1697 |
-
"PDAF3": {
|
| 1698 |
-
"description": "PDAF3 module with initialization and assimilation functions",
|
| 1699 |
-
"main_functions": [
|
| 1700 |
-
"init", "init_forecast", "set_parallel",
|
| 1701 |
-
"assimilate", "assim_offline",
|
| 1702 |
-
"assimilate_3dvar_all", "assim_offline_3dvar_all",
|
| 1703 |
-
"assimilate_local_nondiagr", "assimilate_global_nondiagr",
|
| 1704 |
-
"generate_obs", "generate_obs_offline"
|
| 1705 |
-
]
|
| 1706 |
-
},
|
| 1707 |
-
"PDAFomi": {
|
| 1708 |
-
"description": "PDAF Observation Module Interface for flexible observation handling",
|
| 1709 |
-
"main_functions": [
|
| 1710 |
-
"init", "init_local", "check_error", "gather_obs",
|
| 1711 |
-
"set_debug_flag", "set_doassim", "set_disttype", "set_ncoord",
|
| 1712 |
-
"set_obs_err_type", "set_use_global_obs",
|
| 1713 |
-
"set_inno_omit", "set_inno_omit_ivar",
|
| 1714 |
-
"diag_nobstypes", "diag_dimobs", "diag_obs_rmsd", "diag_stats",
|
| 1715 |
-
"diag_get_hx", "diag_get_hxmean", "diag_get_obs"
|
| 1716 |
-
]
|
| 1717 |
-
},
|
| 1718 |
-
"PDAFlocal": {
|
| 1719 |
-
"description": "PDAF local analysis module for domain localization",
|
| 1720 |
-
"main_functions": [
|
| 1721 |
-
"set_indices", "set_increment_weights", "clear_increment_weights"
|
| 1722 |
-
]
|
| 1723 |
-
},
|
| 1724 |
-
"PDAFlocalomi": {
|
| 1725 |
-
"description": "PDAF local analysis with OMI observation handling",
|
| 1726 |
-
"main_functions": []
|
| 1727 |
-
}
|
| 1728 |
-
}
|
| 1729 |
-
return {"success": True, "result": info, "error": None}
|
| 1730 |
-
except Exception as e:
|
| 1731 |
-
return {"success": False, "result": None, "error": str(e)}
|
| 1732 |
-
|
| 1733 |
-
|
| 1734 |
-
def create_app() -> FastMCP:
|
| 1735 |
-
"""
|
| 1736 |
-
Creates and returns the FastMCP application instance.
|
| 1737 |
-
|
| 1738 |
-
Returns:
|
| 1739 |
-
FastMCP: The FastMCP application instance.
|
| 1740 |
-
"""
|
| 1741 |
-
return mcp
|
|
|
|
| 1 |
"""
|
| 2 |
+
MCP Server for Ocean Data Assimilation using pyPDAF EnOI
|
| 3 |
|
| 4 |
+
This module provides two MCP tools:
|
| 5 |
+
1. generate_demo_ocean_data: Generate synthetic ocean data (restart, ensemble, observations)
|
| 6 |
+
2. run_enoi_pipeline: Run EnOI data assimilation with optional localization
|
| 7 |
"""
|
| 8 |
|
|
|
|
| 9 |
import sys
|
| 10 |
+
import os
|
| 11 |
+
# Add source path to import pyPDAF
|
| 12 |
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../../source/src'))
|
| 13 |
|
| 14 |
import numpy as np
|
| 15 |
+
import netCDF4 as nc
|
|
|
|
|
|
|
|
|
|
| 16 |
from fastmcp import FastMCP
|
| 17 |
+
from typing import Dict, Any, Tuple
|
| 18 |
+
import glob
|
| 19 |
|
| 20 |
+
try:
|
| 21 |
+
import pyPDAF
|
| 22 |
+
import pyPDAF.PDAF as PDAF
|
| 23 |
+
import pyPDAF.PDAFomi as PDAFomi
|
| 24 |
+
PYPDAF_AVAILABLE = True
|
| 25 |
+
except ImportError:
|
| 26 |
+
PYPDAF_AVAILABLE = False
|
| 27 |
+
print("Warning: pyPDAF not available, run_enoi_pipeline will not work")
|
| 28 |
+
|
| 29 |
+
# Initialize FastMCP server
|
| 30 |
+
mcp = FastMCP("pyPDAF MCP Server")
|
| 31 |
|
| 32 |
|
| 33 |
# ============================================================================
|
| 34 |
+
# Helper Functions
|
| 35 |
# ============================================================================
|
| 36 |
|
| 37 |
+
def _write_model_nc(filepath: str, lon: np.ndarray, lat: np.ndarray, depth: np.ndarray,
|
| 38 |
+
temp: np.ndarray, salt: np.ndarray, u: np.ndarray, v: np.ndarray, eta_t: np.ndarray):
|
| 39 |
+
"""Write ocean model data to NetCDF file."""
|
| 40 |
+
with nc.Dataset(filepath, 'w') as ds:
|
| 41 |
+
# Create dimensions
|
| 42 |
+
ds.createDimension('lon', len(lon))
|
| 43 |
+
ds.createDimension('lat', len(lat))
|
| 44 |
+
ds.createDimension('depth', len(depth))
|
| 45 |
+
ds.createDimension('time', None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
+
# Create coordinate variables
|
| 48 |
+
lon_var = ds.createVariable('lon', 'f4', ('lon',))
|
| 49 |
+
lon_var[:] = lon
|
| 50 |
+
lon_var.units = 'degrees_east'
|
| 51 |
|
| 52 |
+
lat_var = ds.createVariable('lat', 'f4', ('lat',))
|
| 53 |
+
lat_var[:] = lat
|
| 54 |
+
lat_var.units = 'degrees_north'
|
| 55 |
+
|
| 56 |
+
depth_var = ds.createVariable('depth', 'f4', ('depth',))
|
| 57 |
+
depth_var[:] = depth
|
| 58 |
+
depth_var.units = 'meters'
|
| 59 |
+
|
| 60 |
+
time_var = ds.createVariable('time', 'f4', ('time',))
|
| 61 |
+
time_var.units = 'days since 2000-01-01'
|
| 62 |
+
time_var[:] = [0]
|
| 63 |
+
|
| 64 |
+
# Create data variables
|
| 65 |
+
temp_var = ds.createVariable('temp', 'f4', ('time', 'depth', 'lat', 'lon'))
|
| 66 |
+
temp_var[:] = temp[np.newaxis, :, :, :]
|
| 67 |
+
temp_var.long_name = 'Temperature'
|
| 68 |
+
temp_var.units = 'degC'
|
| 69 |
+
|
| 70 |
+
salt_var = ds.createVariable('salt', 'f4', ('time', 'depth', 'lat', 'lon'))
|
| 71 |
+
salt_var[:] = salt[np.newaxis, :, :, :]
|
| 72 |
+
salt_var.long_name = 'Salinity'
|
| 73 |
+
salt_var.units = 'psu'
|
| 74 |
+
|
| 75 |
+
u_var = ds.createVariable('u', 'f4', ('time', 'depth', 'lat', 'lon'))
|
| 76 |
+
u_var[:] = u[np.newaxis, :, :, :]
|
| 77 |
+
u_var.long_name = 'Zonal velocity'
|
| 78 |
+
u_var.units = 'm/s'
|
| 79 |
+
|
| 80 |
+
v_var = ds.createVariable('v', 'f4', ('time', 'depth', 'lat', 'lon'))
|
| 81 |
+
v_var[:] = v[np.newaxis, :, :, :]
|
| 82 |
+
v_var.long_name = 'Meridional velocity'
|
| 83 |
+
v_var.units = 'm/s'
|
| 84 |
+
|
| 85 |
+
eta_var = ds.createVariable('eta_t', 'f4', ('time', 'lat', 'lon'))
|
| 86 |
+
eta_var[:] = eta_t[np.newaxis, :, :]
|
| 87 |
+
eta_var.long_name = 'Sea surface height'
|
| 88 |
+
eta_var.units = 'meters'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
|
| 91 |
+
def _flatten_state(temp: np.ndarray, salt: np.ndarray, u: np.ndarray, v: np.ndarray, eta_t: np.ndarray) -> np.ndarray:
|
| 92 |
+
"""Flatten 3D/2D ocean fields into 1D state vector."""
|
| 93 |
+
return np.concatenate([
|
| 94 |
+
temp.ravel(),
|
| 95 |
+
salt.ravel(),
|
| 96 |
+
u.ravel(),
|
| 97 |
+
v.ravel(),
|
| 98 |
+
eta_t.ravel()
|
| 99 |
+
])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
|
| 102 |
+
def _unflatten_state(state: np.ndarray, nz: int, ny: int, nx: int) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
| 103 |
+
"""Unflatten 1D state vector back to 3D/2D ocean fields."""
|
| 104 |
+
n_3d = nz * ny * nx
|
| 105 |
+
n_2d = ny * nx
|
| 106 |
|
| 107 |
+
temp = state[0:n_3d].reshape((nz, ny, nx))
|
| 108 |
+
salt = state[n_3d:2*n_3d].reshape((nz, ny, nx))
|
| 109 |
+
u = state[2*n_3d:3*n_3d].reshape((nz, ny, nx))
|
| 110 |
+
v = state[3*n_3d:4*n_3d].reshape((nz, ny, nx))
|
| 111 |
+
eta_t = state[4*n_3d:4*n_3d+n_2d].reshape((ny, nx))
|
| 112 |
|
| 113 |
+
return temp, salt, u, v, eta_t
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
|
| 116 |
+
def _state_indices_for_temp(obs_lon: np.ndarray, obs_lat: np.ndarray,
|
| 117 |
+
lon: np.ndarray, lat: np.ndarray,
|
| 118 |
+
nz: int, ny: int, nx: int) -> np.ndarray:
|
| 119 |
"""
|
| 120 |
+
Map observation lon/lat to state vector indices for temperature at k=0 layer.
|
| 121 |
+
Uses nearest-neighbor approach.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
"""
|
| 123 |
+
indices = []
|
| 124 |
+
for olon, olat in zip(obs_lon, obs_lat):
|
| 125 |
+
i = np.argmin(np.abs(lon - olon))
|
| 126 |
+
j = np.argmin(np.abs(lat - olat))
|
| 127 |
+
k = 0
|
| 128 |
+
idx = k * ny * nx + j * nx + i
|
| 129 |
+
indices.append(idx)
|
| 130 |
+
return np.array(indices)
|
| 131 |
|
| 132 |
|
| 133 |
+
def _read_state_nc(filepath: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
|
| 134 |
+
"""Read ocean state from NetCDF file."""
|
| 135 |
+
with nc.Dataset(filepath, 'r') as ds:
|
| 136 |
+
lon = ds.variables['lon'][:]
|
| 137 |
+
lat = ds.variables['lat'][:]
|
| 138 |
+
depth = ds.variables['depth'][:]
|
| 139 |
+
temp = ds.variables['temp'][0, :, :, :]
|
| 140 |
+
salt = ds.variables['salt'][0, :, :, :]
|
| 141 |
+
u = ds.variables['u'][0, :, :, :]
|
| 142 |
+
v = ds.variables['v'][0, :, :, :]
|
| 143 |
+
eta_t = ds.variables['eta_t'][0, :, :]
|
| 144 |
+
return lon, lat, depth, temp, salt, u, v, eta_t
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
|
| 147 |
+
def _gaspari_cohn(r: float, c: float) -> float:
|
|
|
|
| 148 |
"""
|
| 149 |
+
Gaspari-Cohn correlation function for localization.
|
| 150 |
|
| 151 |
Args:
|
| 152 |
+
r: Distance between two points
|
| 153 |
+
c: Cutoff radius (localization radius)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
Returns:
|
| 156 |
+
Correlation value between 0 and 1
|
|
|
|
| 157 |
"""
|
| 158 |
+
z = r / c
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
if z >= 2:
|
| 161 |
+
return 0.0
|
| 162 |
+
elif z >= 1:
|
| 163 |
+
term1 = z**5 / 12.0
|
| 164 |
+
term2 = z**4 / 2.0
|
| 165 |
+
term3 = 5.0 * z**3 / 8.0
|
| 166 |
+
term4 = 5.0 * z**2 / 3.0
|
| 167 |
+
result = -term1 + term2 + term3 - term4 + 5.0 * z - 4.0 + 2.0 / (3.0 * z)
|
| 168 |
+
return result
|
| 169 |
+
else: # z < 1
|
| 170 |
+
term1 = z**5 / 4.0
|
| 171 |
+
term2 = z**4 / 2.0
|
| 172 |
+
term3 = 5.0 * z**3 / 8.0
|
| 173 |
+
result = -term1 + term2 + term3 + 1.0
|
| 174 |
+
return result
|
| 175 |
|
| 176 |
|
| 177 |
# ============================================================================
|
| 178 |
+
# MCP Tool 1: Generate Demo Ocean Data
|
| 179 |
# ============================================================================
|
| 180 |
|
| 181 |
+
@mcp.tool(
|
| 182 |
+
name="generate_demo_ocean_data",
|
| 183 |
+
description="Generate synthetic ocean data for EnOI testing. Creates restart.nc (background state), ensemble members, and temperature observations at surface."
|
| 184 |
+
)
|
| 185 |
+
def generate_demo_ocean_data(
|
| 186 |
+
nx: int = 20,
|
| 187 |
+
ny: int = 20,
|
| 188 |
+
nz: int = 5,
|
| 189 |
+
n_ensemble: int = 10,
|
| 190 |
+
n_obs: int = 50,
|
| 191 |
+
lon_min: float = 0.0,
|
| 192 |
+
lon_max: float = 20.0,
|
| 193 |
+
lat_min: float = 0.0,
|
| 194 |
+
lat_max: float = 20.0,
|
| 195 |
+
depth_max: float = 100.0,
|
| 196 |
+
restart_path: str = "restart.nc",
|
| 197 |
+
ensemble_dir: str = "ensemble_data",
|
| 198 |
+
obs_path: str = "obs.nc",
|
| 199 |
+
ensemble_noise_std: float = 0.5,
|
| 200 |
+
obs_noise_std: float = 0.3,
|
| 201 |
+
seed: int = 42
|
| 202 |
+
) -> Dict[str, Any]:
|
| 203 |
+
"""
|
| 204 |
+
Generate synthetic ocean data for EnOI testing.
|
| 205 |
+
|
| 206 |
+
Creates three types of files:
|
| 207 |
+
1. Background state (restart.nc): Initial ocean state
|
| 208 |
+
2. Static ensemble (ensemble_data/member_*.nc): Ensemble members for error covariance
|
| 209 |
+
3. Observations (obs.nc): Temperature observations at surface (k=0)
|
| 210 |
+
|
| 211 |
+
Args:
|
| 212 |
+
nx, ny, nz: Grid dimensions (longitude, latitude, depth)
|
| 213 |
+
n_ensemble: Number of ensemble members
|
| 214 |
+
n_obs: Number of observations
|
| 215 |
+
lon_min, lon_max, lat_min, lat_max, depth_max: Coordinate ranges
|
| 216 |
+
restart_path: Path to background state file
|
| 217 |
+
ensemble_dir: Directory for ensemble members
|
| 218 |
+
obs_path: Path to observation file
|
| 219 |
+
ensemble_noise_std: Noise level for ensemble perturbations
|
| 220 |
+
obs_noise_std: Observation error standard deviation
|
| 221 |
+
seed: Random seed for reproducibility
|
| 222 |
+
|
| 223 |
+
Returns:
|
| 224 |
+
Status dictionary with file paths and metadata
|
| 225 |
+
"""
|
| 226 |
+
np.random.seed(seed)
|
| 227 |
+
|
| 228 |
+
# Create coordinate grids
|
| 229 |
+
lon = np.linspace(lon_min, lon_max, nx)
|
| 230 |
+
lat = np.linspace(lat_min, lat_max, ny)
|
| 231 |
+
depth = np.linspace(0, depth_max, nz)
|
| 232 |
+
|
| 233 |
+
LON, LAT, DEPTH = np.meshgrid(lon, lat, depth, indexing='ij')
|
| 234 |
+
LON = LON.T
|
| 235 |
+
LAT = LAT.T
|
| 236 |
+
DEPTH = DEPTH.T
|
| 237 |
+
|
| 238 |
+
# Generate background state (restart.nc)
|
| 239 |
+
temp = 25.0 - 0.5 * (LAT - 20.0) - 2.0 * DEPTH
|
| 240 |
+
salt = 35.0 + np.random.randn(nz, ny, nx) * 0.1
|
| 241 |
+
u = np.sin(LON / 5.0) * 0.5
|
| 242 |
+
v = np.cos(LAT / 5.0) * 0.5
|
| 243 |
+
eta_t = np.random.randn(ny, nx) * 0.1
|
| 244 |
+
|
| 245 |
+
_write_model_nc(restart_path, lon, lat, depth, temp, salt, u, v, eta_t)
|
| 246 |
+
|
| 247 |
+
# Generate ensemble members
|
| 248 |
+
os.makedirs(ensemble_dir, exist_ok=True)
|
| 249 |
+
ensemble_paths = []
|
| 250 |
+
|
| 251 |
+
for i in range(n_ensemble):
|
| 252 |
+
member_path = os.path.join(ensemble_dir, f"member_{i+1:03d}.nc")
|
| 253 |
|
| 254 |
+
temp_m = temp + np.random.randn(nz, ny, nx) * ensemble_noise_std
|
| 255 |
+
salt_m = salt + np.random.randn(nz, ny, nx) * ensemble_noise_std * 0.5
|
| 256 |
+
u_m = u + np.random.randn(nz, ny, nx) * ensemble_noise_std * 0.2
|
| 257 |
+
v_m = v + np.random.randn(nz, ny, nx) * ensemble_noise_std * 0.2
|
| 258 |
+
eta_m = eta_t + np.random.randn(ny, nx) * ensemble_noise_std * 0.1
|
| 259 |
+
|
| 260 |
+
_write_model_nc(member_path, lon, lat, depth, temp_m, salt_m, u_m, v_m, eta_m)
|
| 261 |
+
ensemble_paths.append(member_path)
|
| 262 |
+
|
| 263 |
+
# Generate observations (temperature at surface k=0)
|
| 264 |
+
obs_lon = np.random.uniform(lon_min, lon_max, n_obs)
|
| 265 |
+
obs_lat = np.random.uniform(lat_min, lat_max, n_obs)
|
| 266 |
+
|
| 267 |
+
# Create "true" observations from background + noise
|
| 268 |
+
obs_temp = []
|
| 269 |
+
for olon, olat in zip(obs_lon, obs_lat):
|
| 270 |
+
i = np.argmin(np.abs(lon - olon))
|
| 271 |
+
j = np.argmin(np.abs(lat - olat))
|
| 272 |
+
true_val = temp[0, j, i] + np.random.randn() * obs_noise_std
|
| 273 |
+
obs_temp.append(true_val)
|
| 274 |
+
obs_temp = np.array(obs_temp)
|
| 275 |
+
|
| 276 |
+
# Write observations to NetCDF
|
| 277 |
+
with nc.Dataset(obs_path, 'w') as ds:
|
| 278 |
+
ds.createDimension('obs', n_obs)
|
| 279 |
+
|
| 280 |
+
lon_var = ds.createVariable('lon', 'f4', ('obs',))
|
| 281 |
+
lon_var[:] = obs_lon
|
| 282 |
+
lon_var.units = 'degrees_east'
|
| 283 |
+
|
| 284 |
+
lat_var = ds.createVariable('lat', 'f4', ('obs',))
|
| 285 |
+
lat_var[:] = obs_lat
|
| 286 |
+
lat_var.units = 'degrees_north'
|
| 287 |
+
|
| 288 |
+
temp_var = ds.createVariable('temp', 'f4', ('obs',))
|
| 289 |
+
temp_var[:] = obs_temp
|
| 290 |
+
temp_var.long_name = 'Temperature'
|
| 291 |
+
temp_var.units = 'degC'
|
| 292 |
+
|
| 293 |
+
error_var = ds.createVariable('error', 'f4', ('obs',))
|
| 294 |
+
error_var[:] = np.full(n_obs, obs_noise_std)
|
| 295 |
+
error_var.long_name = 'Observation error std'
|
| 296 |
+
|
| 297 |
+
return {
|
| 298 |
+
"status": "success",
|
| 299 |
+
"restart_path": os.path.abspath(restart_path),
|
| 300 |
+
"ensemble_dir": os.path.abspath(ensemble_dir),
|
| 301 |
+
"n_ensemble": n_ensemble,
|
| 302 |
+
"obs_path": os.path.abspath(obs_path),
|
| 303 |
+
"n_obs": n_obs,
|
| 304 |
+
"grid_shape": {"nx": nx, "ny": ny, "nz": nz},
|
| 305 |
+
"state_vector_size": nx * ny * nz * 4 + nx * ny
|
| 306 |
+
}
|
| 307 |
|
| 308 |
|
| 309 |
# ============================================================================
|
| 310 |
+
# MCP Tool 2: Run EnOI Pipeline with Localization
|
| 311 |
# ============================================================================
|
| 312 |
|
| 313 |
+
@mcp.tool(
|
| 314 |
+
name="run_enoi_pipeline",
|
| 315 |
+
description="Run Ensemble Optimal Interpolation (EnOI) data assimilation pipeline with optional Gaspari-Cohn localization. Computes analysis state from background, ensemble, and observations."
|
| 316 |
+
)
|
| 317 |
+
def run_enoi_pipeline(
|
| 318 |
+
restart_path: str = "restart.nc",
|
| 319 |
+
ensemble_dir: str = "ensemble_data",
|
| 320 |
+
obs_path: str = "obs.nc",
|
| 321 |
+
analysis_path: str = "restart_analysis.nc",
|
| 322 |
+
inflation_factor: float = 1.0,
|
| 323 |
+
localization_radius: float = 500.0
|
| 324 |
+
) -> Dict[str, Any]:
|
| 325 |
+
"""
|
| 326 |
+
Run Ensemble Optimal Interpolation (EnOI) data assimilation pipeline using pyPDAF.
|
| 327 |
+
|
| 328 |
+
This implements the EnOI algorithm by:
|
| 329 |
+
1. Initialize PDAF with EnKF filtertype=200 (EnOI with static ensemble)
|
| 330 |
+
2. Load background state, static ensemble, and observations
|
| 331 |
+
3. Use PDAFomi to handle observation operator
|
| 332 |
+
4. Apply optional Gaspari-Cohn localization
|
| 333 |
+
5. Run offline assimilation
|
| 334 |
+
6. Write analysis state
|
| 335 |
+
|
| 336 |
+
Args:
|
| 337 |
+
restart_path: Path to background state file
|
| 338 |
+
ensemble_dir: Directory containing ensemble member files
|
| 339 |
+
obs_path: Path to observation file
|
| 340 |
+
analysis_path: Output path for analysis state
|
| 341 |
+
inflation_factor: Covariance inflation factor (default 1.0)
|
| 342 |
+
localization_radius: Localization radius in km (default 500, 0 = no localization)
|
| 343 |
+
|
| 344 |
+
Returns:
|
| 345 |
+
Status dictionary with analysis statistics
|
| 346 |
+
"""
|
| 347 |
+
if not PYPDAF_AVAILABLE:
|
| 348 |
+
return {"status": "error", "message": "pyPDAF library is not available"}
|
| 349 |
+
|
| 350 |
+
# Step 1: Load background state
|
| 351 |
+
lon, lat, depth, temp_b, salt_b, u_b, v_b, eta_b = _read_state_nc(restart_path)
|
| 352 |
+
nz, ny, nx = temp_b.shape
|
| 353 |
+
|
| 354 |
+
state_b = _flatten_state(temp_b, salt_b, u_b, v_b, eta_b)
|
| 355 |
+
n_state = len(state_b)
|
| 356 |
+
|
| 357 |
+
# Step 1b: Load ensemble members
|
| 358 |
+
member_files = sorted(glob.glob(os.path.join(ensemble_dir, "member_*.nc")))
|
| 359 |
+
n_ensemble = len(member_files)
|
| 360 |
+
|
| 361 |
+
if n_ensemble == 0:
|
| 362 |
+
return {"status": "error", "message": f"No ensemble members found in {ensemble_dir}"}
|
| 363 |
+
|
| 364 |
+
ensemble_states = np.zeros((n_state, n_ensemble), order='F')
|
| 365 |
+
for i, mfile in enumerate(member_files):
|
| 366 |
+
_, _, _, temp_m, salt_m, u_m, v_m, eta_m = _read_state_nc(mfile)
|
| 367 |
+
ensemble_states[:, i] = _flatten_state(temp_m, salt_m, u_m, v_m, eta_m)
|
| 368 |
+
|
| 369 |
+
# Step 2: Load observations
|
| 370 |
+
with nc.Dataset(obs_path, 'r') as ds:
|
| 371 |
+
obs_lon = ds.variables['lon'][:]
|
| 372 |
+
obs_lat = ds.variables['lat'][:]
|
| 373 |
+
obs_temp = ds.variables['temp'][:]
|
| 374 |
+
obs_error = ds.variables['error'][:]
|
| 375 |
+
|
| 376 |
+
n_obs = len(obs_temp)
|
| 377 |
+
|
| 378 |
+
# Build observation indices (for temp at k=0)
|
| 379 |
+
obs_indices = _state_indices_for_temp(obs_lon, obs_lat, lon, lat, nz, ny, nx)
|
| 380 |
+
|
| 381 |
+
# ========================================================================
|
| 382 |
+
# Step 3: Initialize PDAF for EnOI (offline mode)
|
| 383 |
+
# ========================================================================
|
| 384 |
+
|
| 385 |
+
# FilterType: For EnOI, we use filtertype=200 (SEEK with fixed covariance)
|
| 386 |
+
# or filtertype=2 (EnKF) which works for static ensemble
|
| 387 |
+
filtertype = 2 # EnKF
|
| 388 |
+
subtype = 0 # Standard form
|
| 389 |
+
|
| 390 |
+
# PDAF parameters
|
| 391 |
+
filter_param_i = np.array([n_state, n_ensemble], dtype=np.intc)
|
| 392 |
+
filter_param_r = np.array([inflation_factor], dtype=np.float64)
|
| 393 |
+
|
| 394 |
+
# User-supplied function to initialize ensemble
|
| 395 |
+
def init_ens_pdaf(filtertype_in, dim_p, dim_ens, state_p, uinv, ens_p, status):
|
| 396 |
+
"""Initialize ensemble in PDAF"""
|
| 397 |
+
ens_p[:, :] = ensemble_states
|
| 398 |
+
state_p[:] = state_b
|
| 399 |
+
return state_p, uinv, ens_p, status
|
| 400 |
+
|
| 401 |
+
# Initialize PDAF
|
| 402 |
+
try:
|
| 403 |
+
_, _, status = pyPDAF.init(
|
| 404 |
+
filtertype, subtype, 0, # step=0 for offline
|
| 405 |
+
filter_param_i, len(filter_param_i),
|
| 406 |
+
filter_param_r, len(filter_param_r),
|
| 407 |
+
py__init_ens_pdaf=init_ens_pdaf,
|
| 408 |
+
in_screen=1 # verbose level
|
| 409 |
+
)
|
| 410 |
+
if status != 0:
|
| 411 |
+
return {"status": "error", "message": f"PDAF init failed with status {status}"}
|
| 412 |
except Exception as e:
|
| 413 |
+
return {"status": "error", "message": f"PDAF init exception: {str(e)}"}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
|
| 415 |
+
# ========================================================================
|
| 416 |
+
# Step 4: Setup PDAFomi for observations
|
| 417 |
+
# ========================================================================
|
| 418 |
|
| 419 |
+
# Initialize PDAFomi with 1 observation type
|
| 420 |
+
PDAFomi.init(1)
|
| 421 |
|
| 422 |
+
# Setup localization if requested
|
| 423 |
+
if localization_radius > 0:
|
| 424 |
+
# Convert radius from km to degrees (approximate)
|
| 425 |
+
radius_deg = localization_radius / 111.0
|
| 426 |
+
|
| 427 |
+
# Initialize local analysis
|
| 428 |
+
PDAFomi.init_local()
|
| 429 |
+
|
| 430 |
+
# Set localization
|
| 431 |
+
PDAF.set_localfilter(1) # Enable local filter
|
| 432 |
+
|
| 433 |
+
# Observation class to handle PDAFomi callbacks
|
| 434 |
+
class ObsHandler:
|
| 435 |
+
def __init__(self):
|
| 436 |
+
self.i_obs = 1
|
| 437 |
+
|
| 438 |
+
def init_dim_obs_pdafomi(self, step, dim_obs):
|
| 439 |
+
"""Initialize observation dimension"""
|
| 440 |
+
# Set OMI parameters
|
| 441 |
+
PDAFomi.set_doassim(self.i_obs, 1) # Assimilate this obs type
|
| 442 |
+
PDAFomi.set_disttype(self.i_obs, 0) # Cartesian distance
|
| 443 |
+
PDAFomi.set_ncoord(self.i_obs, 2) # 2D coordinates
|
| 444 |
+
|
| 445 |
+
# Create coordinate array for observations
|
| 446 |
+
ocoord_p = np.zeros((2, n_obs), order='F')
|
| 447 |
+
ocoord_p[0, :] = obs_lon
|
| 448 |
+
ocoord_p[1, :] = obs_lat
|
| 449 |
+
|
| 450 |
+
# Set observation error
|
| 451 |
+
ivar_obs = 1.0 / (obs_error ** 2)
|
| 452 |
+
|
| 453 |
+
# Set id_obs_p (state vector indices, 1-indexed for Fortran)
|
| 454 |
+
id_obs_p = np.zeros((1, n_obs), dtype=np.intc, order='F')
|
| 455 |
+
id_obs_p[0, :] = obs_indices + 1 # Fortran 1-indexing
|
| 456 |
+
|
| 457 |
+
# Gather observation information
|
| 458 |
+
PDAFomi.set_id_obs_p(self.i_obs, 1, n_obs, id_obs_p)
|
| 459 |
+
PDAFomi.set_ivar_obs_p(self.i_obs, n_obs, ivar_obs)
|
| 460 |
+
PDAFomi.set_ocoord_p(self.i_obs, 2, n_obs, ocoord_p)
|
| 461 |
+
|
| 462 |
+
# Set observation values
|
| 463 |
+
PDAFomi.set_obs_p(self.i_obs, n_obs, obs_temp)
|
| 464 |
+
|
| 465 |
+
return n_obs
|
| 466 |
+
|
| 467 |
+
def obs_op_pdafomi(self, step, dim_p, dim_obs_p, state_p, ostate):
|
| 468 |
+
"""Observation operator"""
|
| 469 |
+
# Extract observed values from state vector
|
| 470 |
+
ostate[:] = state_p[obs_indices]
|
| 471 |
+
return ostate
|
| 472 |
+
|
| 473 |
+
obs_handler = ObsHandler()
|
| 474 |
+
|
| 475 |
+
# Collector class to handle state collection
|
| 476 |
+
class StateCollector:
|
| 477 |
+
def __init__(self):
|
| 478 |
+
self.analysis_state = np.copy(state_b)
|
| 479 |
+
|
| 480 |
+
def collect_state_pdaf(self, dim_p, state_p):
|
| 481 |
+
"""Collect state from model (not used in offline mode)"""
|
| 482 |
+
state_p[:] = self.analysis_state
|
| 483 |
+
return state_p
|
| 484 |
+
|
| 485 |
+
def distribute_state_pdaf(self, dim_p, state_p):
|
| 486 |
+
"""Distribute state to model"""
|
| 487 |
+
self.analysis_state[:] = state_p
|
| 488 |
+
return state_p
|
| 489 |
+
|
| 490 |
+
def prepoststep_pdaf(self, step, dim_p, dim_ens, dim_ens_p, dim_obs_p,
|
| 491 |
+
state_p, uinv, ens_p, flag):
|
| 492 |
+
"""Pre/post processing"""
|
| 493 |
+
if flag > 0: # After analysis
|
| 494 |
+
self.analysis_state[:] = state_p
|
| 495 |
+
return state_p, uinv, ens_p
|
| 496 |
+
|
| 497 |
+
collector = StateCollector()
|
| 498 |
+
|
| 499 |
+
# Localization handler (if needed)
|
| 500 |
+
if localization_radius > 0:
|
| 501 |
+
# Create grid coordinates
|
| 502 |
+
lon_grid, lat_grid = np.meshgrid(lon, lat, indexing='ij')
|
| 503 |
+
|
| 504 |
+
class LocalizationHandler:
|
| 505 |
+
def init_n_domains_pdaf(self, step, n_domains_p):
|
| 506 |
+
"""Number of local analysis domains"""
|
| 507 |
+
n_domains_p = n_state
|
| 508 |
+
return n_domains_p
|
| 509 |
+
|
| 510 |
+
def init_dim_l_pdaf(self, step, domain_p, dim_l):
|
| 511 |
+
"""Dimension of local state vector"""
|
| 512 |
+
dim_l = n_state # Full state for each domain
|
| 513 |
+
return dim_l
|
| 514 |
+
|
| 515 |
+
def init_dim_obs_l_pdafomi(self, domain_p, step, dim_obs_f, dim_obs_l):
|
| 516 |
+
"""Local observation dimension"""
|
| 517 |
+
# For each domain point, find observations within localization radius
|
| 518 |
+
i_domain = domain_p - 1 # Convert to 0-index
|
| 519 |
+
|
| 520 |
+
# Get domain coordinates
|
| 521 |
+
state_lon = lon_grid.ravel()[i_domain % (nx*ny)]
|
| 522 |
+
state_lat = lat_grid.ravel()[i_domain % (nx*ny)]
|
| 523 |
+
|
| 524 |
+
# Calculate distances to all observations
|
| 525 |
+
dx = obs_lon - state_lon
|
| 526 |
+
dy = obs_lat - state_lat
|
| 527 |
+
dist = np.sqrt(dx**2 + dy**2)
|
| 528 |
+
|
| 529 |
+
# Count observations within localization radius
|
| 530 |
+
dim_obs_l = np.sum(dist <= radius_deg)
|
| 531 |
+
return dim_obs_l
|
| 532 |
+
|
| 533 |
+
loc_handler = LocalizationHandler()
|
| 534 |
+
|
| 535 |
+
# Run offline assimilation with localization
|
| 536 |
+
try:
|
| 537 |
+
status = 0
|
| 538 |
+
pyPDAF.assim_offline_local_nondiagr(
|
| 539 |
+
collector.collect_state_pdaf,
|
| 540 |
+
collector.distribute_state_pdaf,
|
| 541 |
+
obs_handler.init_dim_obs_pdafomi,
|
| 542 |
+
obs_handler.obs_op_pdafomi,
|
| 543 |
+
collector.prepoststep_pdaf,
|
| 544 |
+
loc_handler.init_n_domains_pdaf,
|
| 545 |
+
loc_handler.init_dim_l_pdaf,
|
| 546 |
+
loc_handler.init_dim_obs_l_pdafomi,
|
| 547 |
+
status
|
| 548 |
+
)
|
| 549 |
+
except Exception as e:
|
| 550 |
+
return {"status": "error", "message": f"Assimilation failed: {str(e)}"}
|
| 551 |
+
else:
|
| 552 |
+
# Run offline assimilation without localization (global)
|
| 553 |
+
try:
|
| 554 |
+
status = 0
|
| 555 |
+
pyPDAF.assim_offline_global_nondiagr(
|
| 556 |
+
collector.collect_state_pdaf,
|
| 557 |
+
collector.distribute_state_pdaf,
|
| 558 |
+
obs_handler.init_dim_obs_pdafomi,
|
| 559 |
+
obs_handler.obs_op_pdafomi,
|
| 560 |
+
collector.prepoststep_pdaf,
|
| 561 |
+
status
|
| 562 |
+
)
|
| 563 |
+
except Exception as e:
|
| 564 |
+
return {"status": "error", "message": f"Assimilation failed: {str(e)}"}
|
| 565 |
+
|
| 566 |
+
# ========================================================================
|
| 567 |
+
# Step 6: Write analysis state
|
| 568 |
+
# ========================================================================
|
| 569 |
+
|
| 570 |
+
# Unflatten analysis state
|
| 571 |
+
state_a = collector.analysis_state
|
| 572 |
+
temp_a, salt_a, u_a, v_a, eta_a = _unflatten_state(state_a, nz, ny, nx)
|
| 573 |
+
_write_model_nc(analysis_path, lon, lat, depth, temp_a, salt_a, u_a, v_a, eta_a)
|
| 574 |
+
|
| 575 |
+
# Compute statistics
|
| 576 |
+
increment = state_a - state_b
|
| 577 |
+
innovation = obs_temp - state_b[obs_indices]
|
| 578 |
+
|
| 579 |
+
rms_increment = float(np.sqrt(np.mean(increment**2)))
|
| 580 |
+
max_increment = float(np.max(np.abs(increment)))
|
| 581 |
+
rms_innovation = float(np.sqrt(np.mean(innovation**2)))
|
| 582 |
+
|
| 583 |
+
# Finalize PDAF
|
| 584 |
+
PDAF.deallocate()
|
| 585 |
+
|
| 586 |
+
return {
|
| 587 |
+
"status": "success",
|
| 588 |
+
"analysis_path": os.path.abspath(analysis_path),
|
| 589 |
+
"n_ensemble": n_ensemble,
|
| 590 |
+
"n_obs": n_obs,
|
| 591 |
+
"n_state": n_state,
|
| 592 |
+
"rms_innovation": rms_innovation,
|
| 593 |
+
"rms_increment": rms_increment,
|
| 594 |
+
"max_increment": max_increment,
|
| 595 |
+
"inflation_factor": inflation_factor,
|
| 596 |
+
"localization_radius_km": localization_radius if localization_radius > 0 else "none"
|
| 597 |
+
}
|
| 598 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 599 |
|
| 600 |
+
# ============================================================================
|
| 601 |
+
# Create MCP Server App
|
| 602 |
+
# ============================================================================
|
| 603 |
|
| 604 |
+
def create_app():
|
| 605 |
+
"""Create and return the FastMCP application."""
|
| 606 |
+
return mcp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|