roms-tools 3.4.0__py3-none-any.whl → 3.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. roms_tools/datasets/lat_lon_datasets.py +12 -0
  2. roms_tools/datasets/roms_dataset.py +140 -53
  3. roms_tools/datasets/utils.py +14 -2
  4. roms_tools/regrid.py +76 -0
  5. roms_tools/setup/boundary_forcing.py +2 -2
  6. roms_tools/setup/grid.py +17 -3
  7. roms_tools/setup/initial_conditions.py +314 -55
  8. roms_tools/setup/mask.py +2 -5
  9. roms_tools/setup/nesting.py +6 -3
  10. roms_tools/setup/surface_forcing.py +1 -2
  11. roms_tools/setup/tides.py +6 -5
  12. roms_tools/setup/utils.py +220 -142
  13. roms_tools/tests/test_datasets/test_roms_dataset.py +225 -21
  14. roms_tools/tests/test_regrid.py +120 -1
  15. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK/c/0/0/0/0 +0 -0
  16. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK/zarr.json +57 -0
  17. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK_ALT_CO2/c/0/0/0/0 +0 -0
  18. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ALK_ALT_CO2/zarr.json +57 -0
  19. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_r/c/0 +0 -0
  20. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_r/zarr.json +47 -0
  21. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_w/c/0 +0 -0
  22. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Cs_w/zarr.json +47 -0
  23. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC/c/0/0/0/0 +0 -0
  24. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC/zarr.json +57 -0
  25. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/c/0/0/0/0 +0 -0
  26. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DIC_ALT_CO2/zarr.json +57 -0
  27. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/c/0/0/0/0 +0 -0
  28. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOC/zarr.json +57 -0
  29. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOCr/c/0/0/0/0 +0 -0
  30. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOCr/zarr.json +57 -0
  31. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/c/0/0/0/0 +0 -0
  32. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DON/zarr.json +57 -0
  33. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/c/0/0/0/0 +0 -0
  34. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DONr/zarr.json +57 -0
  35. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/c/0/0/0/0 +0 -0
  36. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOP/zarr.json +57 -0
  37. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/c/0/0/0/0 +0 -0
  38. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/DOPr/zarr.json +57 -0
  39. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/c/0/0/0/0 +0 -0
  40. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Fe/zarr.json +57 -0
  41. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Lig/c/0/0/0/0 +0 -0
  42. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/Lig/zarr.json +57 -0
  43. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NH4/c/0/0/0/0 +0 -0
  44. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NH4/zarr.json +57 -0
  45. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/c/0/0/0/0 +0 -0
  46. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/NO3/zarr.json +57 -0
  47. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/O2/c/0/0/0/0 +0 -0
  48. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/O2/zarr.json +57 -0
  49. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/c/0/0/0/0 +0 -0
  50. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/PO4/zarr.json +57 -0
  51. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/c/0/0/0/0 +0 -0
  52. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/SiO3/zarr.json +57 -0
  53. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/abs_time/zarr.json +47 -0
  54. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatC/c/0/0/0/0 +0 -0
  55. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatC/zarr.json +57 -0
  56. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatChl/c/0/0/0/0 +0 -0
  57. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatChl/zarr.json +57 -0
  58. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatFe/c/0/0/0/0 +0 -0
  59. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatFe/zarr.json +57 -0
  60. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatP/c/0/0/0/0 +0 -0
  61. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatP/zarr.json +57 -0
  62. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatSi/c/0/0/0/0 +0 -0
  63. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diatSi/zarr.json +57 -0
  64. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/c/0/0/0/0 +0 -0
  65. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazC/zarr.json +57 -0
  66. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazChl/c/0/0/0/0 +0 -0
  67. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazChl/zarr.json +57 -0
  68. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazFe/c/0/0/0/0 +0 -0
  69. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazFe/zarr.json +57 -0
  70. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/c/0/0/0/0 +0 -0
  71. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/diazP/zarr.json +57 -0
  72. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/c/0 +0 -0
  73. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ocean_time/zarr.json +47 -0
  74. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/salt/c/0/0/0/0 +0 -0
  75. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/salt/zarr.json +57 -0
  76. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/c/0/0/0/0 +0 -0
  77. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spC/zarr.json +57 -0
  78. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/c/0/0/0/0 +0 -0
  79. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spCaCO3/zarr.json +57 -0
  80. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spChl/c/0/0/0/0 +0 -0
  81. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spChl/zarr.json +57 -0
  82. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/c/0/0/0/0 +0 -0
  83. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spFe/zarr.json +57 -0
  84. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/c/0/0/0/0 +0 -0
  85. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/spP/zarr.json +57 -0
  86. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/c/0/0/0/0 +0 -0
  87. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/temp/zarr.json +57 -0
  88. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/u/c/0/0/0/0 +0 -0
  89. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/u/zarr.json +57 -0
  90. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/c/0/0/0 +0 -0
  91. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/ubar/zarr.json +54 -0
  92. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/c/0/0/0/0 +0 -0
  93. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/v/zarr.json +57 -0
  94. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/c/0/0/0 +0 -0
  95. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/vbar/zarr.json +54 -0
  96. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/w/zarr.json +57 -0
  97. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zarr.json +2481 -0
  98. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/c/0/0/0 +0 -0
  99. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zeta/zarr.json +54 -0
  100. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/c/0/0/0/0 +0 -0
  101. roms_tools/tests/test_setup/test_data/initial_conditions_from_roms.zarr/zooC/zarr.json +57 -0
  102. roms_tools/tests/test_setup/test_grid.py +24 -0
  103. roms_tools/tests/test_setup/test_initial_conditions.py +128 -11
  104. roms_tools/tests/test_setup/test_validation.py +15 -0
  105. roms_tools/tests/test_utils.py +287 -0
  106. roms_tools/utils.py +177 -72
  107. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/METADATA +2 -3
  108. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/RECORD +111 -24
  109. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/WHEEL +1 -1
  110. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/licenses/LICENSE +0 -0
  111. {roms_tools-3.4.0.dist-info → roms_tools-3.5.0.dist-info}/top_level.txt +0 -0
roms_tools/setup/utils.py CHANGED
@@ -3,6 +3,7 @@ import logging
3
3
  import time
4
4
  import typing
5
5
  from collections.abc import Sequence
6
+ from copy import deepcopy
6
7
  from dataclasses import asdict, fields, is_dataclass
7
8
  from datetime import datetime
8
9
  from enum import StrEnum
@@ -17,7 +18,6 @@ import yaml
17
18
  from pydantic import BaseModel
18
19
 
19
20
  from roms_tools.constants import R_EARTH
20
- from roms_tools.utils import interpolate_from_rho_to_u, interpolate_from_rho_to_v
21
21
 
22
22
  if typing.TYPE_CHECKING:
23
23
  from roms_tools.setup.grid import Grid
@@ -966,46 +966,6 @@ def get_target_coords(
966
966
  return target_coords
967
967
 
968
968
 
969
- def rotate_velocities(
970
- u: xr.DataArray, v: xr.DataArray, angle: xr.DataArray, interpolate: bool = True
971
- ) -> tuple[xr.DataArray, xr.DataArray]:
972
- """Rotate and optionally interpolate velocity components to align with grid
973
- orientation.
974
-
975
- Parameters
976
- ----------
977
- u : xarray.DataArray
978
- Zonal (east-west) velocity component at u-points.
979
- v : xarray.DataArray
980
- Meridional (north-south) velocity component at v-points.
981
- angle : xarray.DataArray
982
- Grid angle values for rotation.
983
- interpolate : bool, optional
984
- If True, interpolates rotated velocities to grid points (default is True).
985
-
986
- Returns
987
- -------
988
- tuple of xarray.DataArray
989
- Rotated velocity components (u_rot, v_rot).
990
-
991
- Notes
992
- -----
993
- - Rotation formulas:
994
- - u_rot = u * cos(angle) + v * sin(angle)
995
- - v_rot = v * cos(angle) - u * sin(angle)
996
- """
997
- # Rotate velocities to grid orientation
998
- u_rot = u * np.cos(angle) + v * np.sin(angle)
999
- v_rot = v * np.cos(angle) - u * np.sin(angle)
1000
-
1001
- # Interpolate to u- and v-points
1002
- if interpolate:
1003
- u_rot = interpolate_from_rho_to_u(u_rot)
1004
- v_rot = interpolate_from_rho_to_v(v_rot)
1005
-
1006
- return u_rot, v_rot
1007
-
1008
-
1009
969
  def compute_barotropic_velocity(
1010
970
  vel: xr.DataArray, interface_depth: xr.DataArray
1011
971
  ) -> xr.DataArray:
@@ -1365,142 +1325,262 @@ def write_to_yaml(yaml_data, filepath: str | Path) -> None:
1365
1325
  )
1366
1326
 
1367
1327
 
1328
+ def serialize_paths(value: Any) -> Any:
1329
+ """Recursively convert Path objects to strings."""
1330
+ if isinstance(value, Path):
1331
+ return str(value)
1332
+ if isinstance(value, list):
1333
+ return [serialize_paths(v) for v in value]
1334
+ if isinstance(value, dict):
1335
+ return {k: serialize_paths(v) for k, v in value.items()}
1336
+ return value
1337
+
1338
+
1339
+ def normalize_paths(value: Any) -> Any:
1340
+ """Recursively convert path-like strings back to Path objects.
1341
+
1342
+ Heuristic: strings containing '/' or ending with '.nc' are treated as paths.
1343
+ """
1344
+ if isinstance(value, str):
1345
+ return Path(value) if "/" in value or value.endswith(".nc") else value
1346
+ if isinstance(value, list):
1347
+ return [normalize_paths(v) for v in value]
1348
+ if isinstance(value, dict):
1349
+ return {k: normalize_paths(v) for k, v in value.items()}
1350
+ return value
1351
+
1352
+
1353
+ def serialize_datetime(value: datetime | list[datetime] | Any) -> Any:
1354
+ """Convert datetime or list of datetimes to ISO 8601 strings."""
1355
+ if isinstance(value, datetime):
1356
+ return value.isoformat()
1357
+ if isinstance(value, list) and all(isinstance(v, datetime) for v in value):
1358
+ return [v.isoformat() for v in value]
1359
+ return value
1360
+
1361
+
1362
+ def deserialize_datetime(
1363
+ value: str | list[str] | datetime | Any,
1364
+ ) -> datetime | list[datetime] | Any:
1365
+ """Convert ISO 8601 string(s) to datetime object(s).
1366
+
1367
+ Returns:
1368
+ datetime if input is string,
1369
+ list of datetime if input is list of strings,
1370
+ original value if parsing fails or input is already datetime.
1371
+ """
1372
+ if isinstance(value, list):
1373
+ result: list[datetime | Any] = []
1374
+ for v in value:
1375
+ try:
1376
+ result.append(datetime.fromisoformat(str(v)))
1377
+ except ValueError:
1378
+ result.append(v)
1379
+ return result
1380
+
1381
+ if isinstance(value, str):
1382
+ try:
1383
+ return datetime.fromisoformat(value)
1384
+ except ValueError:
1385
+ return value
1386
+
1387
+ return value
1388
+
1389
+
1390
+ def serialize_source_dict(src: dict[str, Any] | None) -> dict[str, Any] | None:
1391
+ """Serialize a source or BGC source dictionary for YAML or JSON output.
1392
+
1393
+ This function performs the following transformations:
1394
+ - Converts any `Path` objects (including nested lists or dicts) to strings.
1395
+ - Serializes any nested `Grid` objects using `serialize_grid`.
1396
+ - Creates a deep copy of the input dictionary to avoid modifying the original.
1397
+
1398
+ Parameters
1399
+ ----------
1400
+ src : dict[str, Any] | None
1401
+ The source or BGC source dictionary to serialize. Keys typically include:
1402
+ - "path": path(s) to files
1403
+ - "grid": a Grid object
1404
+
1405
+ Returns
1406
+ -------
1407
+ dict[str, Any] | None
1408
+ A serialized dictionary suitable for saving to YAML or JSON, with:
1409
+ - Paths converted to strings
1410
+ - Nested Grid objects serialized
1411
+ Returns `None` if input `src` is `None`.
1412
+ """
1413
+ if src is None:
1414
+ return None
1415
+
1416
+ src = deepcopy(src)
1417
+
1418
+ # Serialize paths
1419
+ if "path" in src:
1420
+ src["path"] = serialize_paths(src["path"])
1421
+
1422
+ # Serialize nested grid
1423
+ if "grid" in src and src["grid"] is not None:
1424
+ src["grid"] = serialize_grid(src["grid"])
1425
+
1426
+ return src
1427
+
1428
+
1429
+ def deserialize_source_dict(src: dict[str, Any] | None) -> dict[str, Any] | None:
1430
+ """Deserialize a source / bgc_source dictionary.
1431
+
1432
+ Converts string paths back to Path objects.
1433
+
1434
+ Parameters
1435
+ ----------
1436
+ src : dict[str, Any] | None
1437
+ Serialized source or bgc_source dictionary.
1438
+
1439
+ Returns
1440
+ -------
1441
+ dict[str, Any] | None
1442
+ Dictionary with paths converted to Path objects.
1443
+ """
1444
+ if src is None:
1445
+ return None
1446
+
1447
+ src = deepcopy(src)
1448
+
1449
+ # Deserialize paths
1450
+ if "path" in src:
1451
+ src["path"] = normalize_paths(src["path"])
1452
+
1453
+ return src
1454
+
1455
+
1456
+ def serialize_grid(grid_obj: Any) -> dict[str, Any]:
1457
+ """Serialize a Grid object to a dictionary, excluding non-serializable attributes."""
1458
+ return pop_grid_data(asdict(grid_obj))
1459
+
1460
+
1461
+ def pop_grid_data(grid_data: dict[str, Any]) -> dict[str, Any]:
1462
+ """Remove non-serializable or unnecessary keys from a Grid dictionary.
1463
+
1464
+ Removes 'ds', 'straddle', and 'verbose' keys if present.
1465
+
1466
+ Parameters
1467
+ ----------
1468
+ grid_data : dict
1469
+ Dictionary representation of a Grid object.
1470
+
1471
+ Returns
1472
+ -------
1473
+ dict
1474
+ Cleaned dictionary suitable for serialization.
1475
+ """
1476
+ for key in ("ds", "straddle", "verbose"):
1477
+ grid_data.pop(key, None)
1478
+ return grid_data
1479
+
1480
+
1368
1481
  def to_dict(forcing_object, exclude: list[str] | None = None) -> dict:
1369
1482
  """Serialize a forcing object (including its grid) into a dictionary.
1370
1483
 
1371
- This function serializes a dataclass object (forcing_object) and its associated
1372
- `grid` attribute into a dictionary. It omits fields like `grid` and `ds`
1373
- that are not serializable or meant to be excluded.
1484
+ This function serializes a forcing object (dataclass or pydantic model),
1485
+ including its associated grid(s), into a dictionary suitable for YAML output.
1374
1486
 
1375
- The function also converts datetime fields to ISO format strings for proper
1376
- serialization.
1487
+ - Top-level grids (`grid`, `parent_grid`) are serialized consistently
1488
+ - Nested grids inside `source` and `bgc_source` are also serialized
1489
+ - Datetime objects are converted to ISO strings
1490
+ - Path objects are converted to strings
1377
1491
 
1378
1492
  Parameters
1379
1493
  ----------
1380
1494
  forcing_object : object
1381
- The object that contains the forcing data, typically a dataclass with attributes
1382
- such as `grid`, `start_time`, `end_time`, etc.
1495
+ A dataclass or pydantic model representing a forcing configuration.
1383
1496
  exclude : list[str], optional
1384
- List of keys to exclude from the serialized output. Defaults to empty list. The field "ds" is always excluded by default.
1497
+ List of field names to exclude from serialization. The fields
1498
+ "grid", "parent_grid", and "ds" are always excluded.
1385
1499
 
1386
1500
  Returns
1387
1501
  -------
1388
1502
  dict
1503
+ Serialized representation of the forcing object.
1389
1504
  """
1390
- # Serialize Grid data
1505
+ exclude_list = exclude or []
1506
+ exclude_set: set[str] = {"grid", "parent_grid", "ds", *exclude_list}
1507
+
1508
+ # --- Serialize top-level grid(s) ---
1509
+ yaml_data = {}
1510
+
1391
1511
  if hasattr(forcing_object, "grid") and forcing_object.grid is not None:
1392
- grid_data = asdict(forcing_object.grid)
1393
- grid_yaml_data = {"Grid": pop_grid_data(grid_data)}
1394
- elif hasattr(forcing_object, "parent_grid"):
1395
- grid_data = asdict(forcing_object.parent_grid)
1396
- grid_yaml_data = {"ParentGrid": pop_grid_data(grid_data)}
1397
-
1398
- # Ensure Paths are Strings
1399
- def ensure_paths_are_strings(obj, key):
1400
- attr = getattr(obj, key, None)
1401
- if attr is not None and "path" in attr:
1402
- paths = attr["path"]
1403
- if isinstance(paths, list):
1404
- attr["path"] = [str(p) if isinstance(p, Path) else p for p in paths]
1405
- elif isinstance(paths, Path):
1406
- attr["path"] = str(paths)
1407
- elif isinstance(paths, dict):
1408
- for key, path in paths.items():
1409
- attr["path"][key] = str(path)
1410
-
1411
- ensure_paths_are_strings(forcing_object, "source")
1412
- ensure_paths_are_strings(forcing_object, "bgc_source")
1413
-
1414
- # Prepare Forcing Data
1415
- forcing_data = {}
1512
+ yaml_data["Grid"] = serialize_grid(forcing_object.grid)
1513
+
1514
+ if (
1515
+ hasattr(forcing_object, "parent_grid")
1516
+ and forcing_object.parent_grid is not None
1517
+ ):
1518
+ yaml_data["ParentGrid"] = serialize_grid(forcing_object.parent_grid)
1519
+
1520
+ # --- Collect forcing fields ---
1416
1521
  if isinstance(forcing_object, BaseModel):
1417
- field_names = forcing_object.model_fields
1522
+ field_names = forcing_object.model_fields.keys()
1418
1523
  elif is_dataclass(forcing_object):
1419
- field_names = [field.name for field in fields(forcing_object)]
1524
+ field_names = [f.name for f in fields(forcing_object)]
1420
1525
  else:
1421
- raise TypeError("Forcing object is not a dataclass or pydantic model")
1422
-
1423
- if exclude is None:
1424
- exclude = []
1425
- exclude = ["grid", "parent_grid", "ds", *exclude]
1526
+ raise TypeError("Forcing object must be a dataclass or pydantic model")
1426
1527
 
1427
- filtered_field_names = [param for param in field_names if param not in exclude]
1528
+ forcing_data = {}
1428
1529
 
1429
- for field_name in filtered_field_names:
1430
- # Retrieve the value of each field using getattr
1431
- value = getattr(forcing_object, field_name)
1530
+ for name in field_names:
1531
+ if name in exclude_set:
1532
+ continue
1432
1533
 
1433
- # If the field is a datetime object, convert it to ISO format
1434
- if isinstance(value, datetime):
1435
- value = value.isoformat()
1436
- # Convert list of datetimes to list of ISO strings
1437
- elif isinstance(value, list) and all(isinstance(v, datetime) for v in value):
1438
- value = [v.isoformat() for v in value]
1534
+ value = getattr(forcing_object, name)
1439
1535
 
1440
- # Add the field and its value to the forcing_data dictionary
1441
- forcing_data[field_name] = value
1536
+ if name in {"source", "bgc_source"}:
1537
+ forcing_data[name] = serialize_source_dict(value)
1538
+ continue
1442
1539
 
1443
- # Combine Grid and Forcing Data into a single dictionary for the final YAML content
1444
- yaml_data = {
1445
- **grid_yaml_data, # Add the grid data to the final YAML structure
1446
- forcing_object.__class__.__name__: forcing_data, # Include the serialized forcing object data
1447
- }
1540
+ value = serialize_datetime(value)
1541
+ value = serialize_paths(value)
1448
1542
 
1449
- return yaml_data
1543
+ forcing_data[name] = value
1450
1544
 
1545
+ # --- Final YAML structure ---
1546
+ yaml_data[forcing_object.__class__.__name__] = forcing_data
1451
1547
 
1452
- def pop_grid_data(grid_data):
1453
- grid_data.pop("ds", None) # Remove 'ds' attribute (non-serializable)
1454
- grid_data.pop("straddle", None)
1455
- grid_data.pop("verbose", None)
1456
-
1457
- return grid_data
1548
+ return yaml_data
1458
1549
 
1459
1550
 
1460
1551
  def from_yaml(forcing_object: type, filepath: str | Path) -> dict[str, Any]:
1461
- """Extract the configuration data for a given forcing object from a YAML file.
1552
+ """Load configuration for a forcing object from a YAML file.
1462
1553
 
1463
- This function reads a YAML file, searches for the configuration data associated
1464
- with the class name of the forcing object, and returns the configuration data
1465
- as a dictionary. The dictionary contains the forcing parameters extracted from
1466
- the YAML file, with any date fields converted from ISO format.
1554
+ Searches for a dictionary keyed by the class name of `forcing_object` and
1555
+ returns it, converting:
1556
+ - ISO-format date strings to `datetime` objects
1557
+ - Path-like strings back to `Path` objects
1558
+ - `source` and `bgc_source` nested dictionaries back to proper Grid objects
1467
1559
 
1468
1560
  Parameters
1469
1561
  ----------
1470
- filepath : Union[str, Path]
1471
- The path to the YAML file from which the parameters will be read.
1472
- forcing_object : Type
1473
- The class type (e.g., TidalForcing) whose configuration data is to be loaded
1474
- from the YAML file. The class name is used to locate the relevant data in
1475
- the YAML structure.
1562
+ forcing_object : type
1563
+ The class type whose configuration to load (e.g., `TidalForcing`).
1564
+ filepath : str | Path
1565
+ Path to the YAML file containing the configuration.
1476
1566
 
1477
1567
  Returns
1478
1568
  -------
1479
- dict
1480
- A dictionary containing the forcing parameters extracted from the YAML file.
1481
- This dictionary contains key-value pairs where the keys are the parameter
1482
- names, and the values are the corresponding values from the YAML file.
1483
- Any date fields are converted from ISO format if necessary.
1569
+ dict[str, Any]
1570
+ Dictionary of configuration parameters with dates, paths, and nested grids restored.
1484
1571
 
1485
1572
  Raises
1486
1573
  ------
1487
1574
  ValueError
1488
- If no configuration for the specified class name is found in the YAML file.
1575
+ If no configuration for the specified class is found in the YAML file.
1489
1576
  """
1490
- # Ensure filepath is a Path object
1491
1577
  filepath = Path(filepath)
1492
-
1493
- # Read the entire file content
1494
- with filepath.open("r") as file:
1495
- file_content = file.read()
1496
-
1497
- # Split the content into YAML documents
1498
- documents = list(yaml.safe_load_all(file_content))
1578
+ with filepath.open("r") as f:
1579
+ documents = list(yaml.safe_load_all(f))
1499
1580
 
1500
1581
  forcing_data = None
1501
1582
  forcing_object_name = forcing_object.__name__
1502
1583
 
1503
- # Process the YAML documents to find the forcing data for the given object
1504
1584
  for doc in documents:
1505
1585
  if doc is None:
1506
1586
  continue
@@ -1513,21 +1593,19 @@ def from_yaml(forcing_object: type, filepath: str | Path) -> dict[str, Any]:
1513
1593
  f"No {forcing_object_name} configuration found in the YAML file."
1514
1594
  )
1515
1595
 
1516
- # Convert any date fields from ISO format if necessary
1596
+ # Convert ISO date strings to datetime objects
1517
1597
  for key, value in forcing_data.items():
1518
- forcing_data[key] = _convert_from_iso_format(value)
1598
+ forcing_data[key] = deserialize_datetime(value)
1519
1599
 
1520
- # Return the forcing data as a dictionary
1521
- return forcing_data
1600
+ # Convert path-like strings back to Path objects
1601
+ forcing_data = normalize_paths(forcing_data)
1522
1602
 
1603
+ # Deserialize source and bgc_source nested dictionaries
1604
+ for key in ["source", "bgc_source"]:
1605
+ if key in forcing_data:
1606
+ forcing_data[key] = deserialize_source_dict(forcing_data[key])
1523
1607
 
1524
- def _convert_from_iso_format(value):
1525
- try:
1526
- # Return the parsed datetime object if successful
1527
- return datetime.fromisoformat(str(value))
1528
- except ValueError:
1529
- # Return None or raise an exception if parsing fails
1530
- return value
1608
+ return forcing_data
1531
1609
 
1532
1610
 
1533
1611
  def handle_boundaries(field):