guohanghui commited on
Commit
ce55036
·
verified ·
1 Parent(s): fc17224

Update pyPDAF/mcp_output/mcp_plugin/mcp_service.py

Browse files
pyPDAF/mcp_output/mcp_plugin/mcp_service.py CHANGED
@@ -151,36 +151,6 @@ def _read_state_nc(filepath: str) -> Tuple[np.ndarray, np.ndarray, np.ndarray, n
151
  return lon, lat, depth, temp, salt, u, v, eta_t
152
 
153
 
154
- def _gaspari_cohn(r: float, c: float) -> float:
155
- """
156
- Gaspari-Cohn correlation function for localization.
157
-
158
- Args:
159
- r: Distance between two points
160
- c: Cutoff radius (localization radius)
161
-
162
- Returns:
163
- Correlation value between 0 and 1
164
- """
165
- z = r / c
166
-
167
- if z >= 2:
168
- return 0.0
169
- elif z >= 1:
170
- term1 = z**5 / 12.0
171
- term2 = z**4 / 2.0
172
- term3 = 5.0 * z**3 / 8.0
173
- term4 = 5.0 * z**2 / 3.0
174
- result = -term1 + term2 + term3 - term4 + 5.0 * z - 4.0 + 2.0 / (3.0 * z)
175
- return result
176
- else: # z < 1
177
- term1 = z**5 / 4.0
178
- term2 = z**4 / 2.0
179
- term3 = 5.0 * z**3 / 8.0
180
- result = -term1 + term2 + term3 + 1.0
181
- return result
182
-
183
-
184
  # ============================================================================
185
  # MCP Tool 1: Generate Demo Ocean Data
186
  # ============================================================================
@@ -321,16 +291,16 @@ def run_enoi_pipeline(
321
  obs_path: str = "obs.nc",
322
  analysis_path: str = "restart_analysis.nc",
323
  inflation_factor: float = 1.0,
324
- localization_radius: float = 500.0
325
  ) -> Dict[str, Any]:
326
  """
327
  Run Ensemble Optimal Interpolation (EnOI) data assimilation pipeline using pyPDAF.
328
 
329
  This implements the EnOI algorithm by:
330
- 1. Initialize PDAF with EnKF filtertype=200 (EnOI with static ensemble)
331
  2. Load background state, static ensemble, and observations
332
  3. Use PDAFomi to handle observation operator
333
- 4. Apply optional Gaspari-Cohn localization
334
  5. Run offline assimilation
335
  6. Write analysis state
336
 
@@ -340,7 +310,7 @@ def run_enoi_pipeline(
340
  obs_path: Path to observation file
341
  analysis_path: Output path for analysis state
342
  inflation_factor: Covariance inflation factor (default 1.0)
343
- localization_radius: Localization radius in km (default 500, 0 = no localization)
344
 
345
  Returns:
346
  Status dictionary with analysis statistics
@@ -383,8 +353,7 @@ def run_enoi_pipeline(
383
  # Step 3: Initialize PDAF for EnOI (offline mode)
384
  # ========================================================================
385
 
386
- # FilterType: For EnOI, we use filtertype=200 (SEEK with fixed covariance)
387
- # or filtertype=2 (EnKF) which works for static ensemble
388
  filtertype = 2 # EnKF
389
  subtype = 0 # Standard form
390
 
@@ -420,17 +389,6 @@ def run_enoi_pipeline(
420
  # Initialize PDAFomi with 1 observation type
421
  pyPDAF.PDAFomi.init(1)
422
 
423
- # Setup localization if requested
424
- if localization_radius > 0:
425
- # Convert radius from km to degrees (approximate)
426
- radius_deg = localization_radius / 111.0
427
-
428
- # Initialize local analysis
429
- pyPDAF.PDAFomi.init_local()
430
-
431
- # Set localization
432
- pyPDAF.PDAF.set_localfilter(1) # Enable local filter
433
-
434
  # Observation class to handle PDAFomi callbacks
435
  class ObsHandler:
436
  def __init__(self):
@@ -497,12 +455,23 @@ def run_enoi_pipeline(
497
 
498
  collector = StateCollector()
499
 
500
- # Localization handler (if needed)
 
 
 
501
  if localization_radius > 0:
502
- # Create grid coordinates
 
 
 
 
503
  lon_grid, lat_grid = np.meshgrid(lon, lat, indexing='ij')
504
 
505
  class LocalizationHandler:
 
 
 
 
506
  def init_n_domains_pdaf(self, step, n_domains_p):
507
  """Number of local analysis domains"""
508
  n_domains_p = n_state
@@ -514,21 +483,39 @@ def run_enoi_pipeline(
514
  return dim_l
515
 
516
  def init_dim_obs_l_pdafomi(self, domain_p, step, dim_obs_f, dim_obs_l):
517
- """Local observation dimension"""
518
- # For each domain point, find observations within localization radius
519
- i_domain = domain_p - 1 # Convert to 0-index
520
-
521
  # Get domain coordinates
 
522
  state_lon = lon_grid.ravel()[i_domain % (nx*ny)]
523
  state_lat = lat_grid.ravel()[i_domain % (nx*ny)]
524
 
525
- # Calculate distances to all observations
526
- dx = obs_lon - state_lon
527
- dy = obs_lat - state_lat
528
- dist = np.sqrt(dx**2 + dy**2)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
529
 
530
- # Count observations within localization radius
531
- dim_obs_l = np.sum(dist <= radius_deg)
532
  return dim_obs_l
533
 
534
  loc_handler = LocalizationHandler()
@@ -548,9 +535,9 @@ def run_enoi_pipeline(
548
  status
549
  )
550
  except Exception as e:
551
- return {"status": "error", "message": f"Assimilation failed: {str(e)}"}
552
  else:
553
- # Run offline assimilation without localization (global)
554
  try:
555
  status = 0
556
  pyPDAF.assim_offline_global_nondiagr(
@@ -562,7 +549,7 @@ def run_enoi_pipeline(
562
  status
563
  )
564
  except Exception as e:
565
- return {"status": "error", "message": f"Assimilation failed: {str(e)}"}
566
 
567
  # ========================================================================
568
  # Step 6: Write analysis state
@@ -604,4 +591,4 @@ def run_enoi_pipeline(
604
 
605
  def create_app():
606
  """Create and return the FastMCP application."""
607
- return mcp
 
151
  return lon, lat, depth, temp, salt, u, v, eta_t
152
 
153
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
154
  # ============================================================================
155
  # MCP Tool 1: Generate Demo Ocean Data
156
  # ============================================================================
 
291
  obs_path: str = "obs.nc",
292
  analysis_path: str = "restart_analysis.nc",
293
  inflation_factor: float = 1.0,
294
+ localization_radius: float = 0.0
295
  ) -> Dict[str, Any]:
296
  """
297
  Run Ensemble Optimal Interpolation (EnOI) data assimilation pipeline using pyPDAF.
298
 
299
  This implements the EnOI algorithm by:
300
+ 1. Initialize PDAF with EnKF filtertype=2 (EnKF with static ensemble for EnOI)
301
  2. Load background state, static ensemble, and observations
302
  3. Use PDAFomi to handle observation operator
303
+ 4. Apply optional Gaspari-Cohn localization using omi_init_dim_obs_l_iso
304
  5. Run offline assimilation
305
  6. Write analysis state
306
 
 
310
  obs_path: Path to observation file
311
  analysis_path: Output path for analysis state
312
  inflation_factor: Covariance inflation factor (default 1.0)
313
+ localization_radius: Localization radius in km (default 0 = no localization, global filter)
314
 
315
  Returns:
316
  Status dictionary with analysis statistics
 
353
  # Step 3: Initialize PDAF for EnOI (offline mode)
354
  # ========================================================================
355
 
356
+ # FilterType: For EnOI, we use filtertype=2 (EnKF with static ensemble)
 
357
  filtertype = 2 # EnKF
358
  subtype = 0 # Standard form
359
 
 
389
  # Initialize PDAFomi with 1 observation type
390
  pyPDAF.PDAFomi.init(1)
391
 
 
 
 
 
 
 
 
 
 
 
 
392
  # Observation class to handle PDAFomi callbacks
393
  class ObsHandler:
394
  def __init__(self):
 
455
 
456
  collector = StateCollector()
457
 
458
+ # ========================================================================
459
+ # Step 5: Run assimilation (global or local)
460
+ # ========================================================================
461
+
462
  if localization_radius > 0:
463
+ # Run with localization (domain-localized filter)
464
+ # Convert radius from km to degrees (approximate)
465
+ radius_deg = localization_radius / 111.0
466
+
467
+ # Create grid coordinates for domain points
468
  lon_grid, lat_grid = np.meshgrid(lon, lat, indexing='ij')
469
 
470
  class LocalizationHandler:
471
+ def __init__(self):
472
+ # Initialize local analysis
473
+ pyPDAF.PDAFomi.init_local()
474
+
475
  def init_n_domains_pdaf(self, step, n_domains_p):
476
  """Number of local analysis domains"""
477
  n_domains_p = n_state
 
483
  return dim_l
484
 
485
  def init_dim_obs_l_pdafomi(self, domain_p, step, dim_obs_f, dim_obs_l):
486
+ """Local observation dimension using PDAFomi localization"""
 
 
 
487
  # Get domain coordinates
488
+ i_domain = domain_p - 1 # Convert to 0-index
489
  state_lon = lon_grid.ravel()[i_domain % (nx*ny)]
490
  state_lat = lat_grid.ravel()[i_domain % (nx*ny)]
491
 
492
+ # Create coordinate for this domain
493
+ coords_l = np.array([state_lon, state_lat], order='F')
494
+
495
+ # Use PDAFomi isotropic localization
496
+ # Parameters: thisobs_l, thisobs, coords_l, locweight, cradius, sradius, dim_obs_l
497
+ # locweight: 0=unit weight, 2=Gaspari-Cohn
498
+ locweight = 2 # Gaspari-Cohn
499
+ cradius = radius_deg # Cut-off radius
500
+ sradius = radius_deg # Support radius (same as cut-off for Gaspari-Cohn)
501
+
502
+ # Call omi_init_dim_obs_l_iso for isotropic localization
503
+ try:
504
+ dim_obs_l = pyPDAF.PDAF.omi_init_dim_obs_l_iso(
505
+ obs_handler.i_obs, # observation type
506
+ coords_l, # local domain coordinates
507
+ locweight, # localization weight function
508
+ cradius, # cut-off radius
509
+ sradius # support radius
510
+ )
511
+ except Exception as e:
512
+ print(f"Warning: omi_init_dim_obs_l_iso failed: {e}")
513
+ # Fallback: count observations manually
514
+ dx = obs_lon - state_lon
515
+ dy = obs_lat - state_lat
516
+ dist = np.sqrt(dx**2 + dy**2)
517
+ dim_obs_l = np.sum(dist <= radius_deg)
518
 
 
 
519
  return dim_obs_l
520
 
521
  loc_handler = LocalizationHandler()
 
535
  status
536
  )
537
  except Exception as e:
538
+ return {"status": "error", "message": f"Localized assimilation failed: {str(e)}"}
539
  else:
540
+ # Run without localization (global filter)
541
  try:
542
  status = 0
543
  pyPDAF.assim_offline_global_nondiagr(
 
549
  status
550
  )
551
  except Exception as e:
552
+ return {"status": "error", "message": f"Global assimilation failed: {str(e)}"}
553
 
554
  # ========================================================================
555
  # Step 6: Write analysis state
 
591
 
592
  def create_app():
593
  """Create and return the FastMCP application."""
594
+ return mcp