roms-tools 2.6.2__py3-none-any.whl → 2.7.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 (51) hide show
  1. roms_tools/__init__.py +1 -0
  2. roms_tools/analysis/roms_output.py +11 -77
  3. roms_tools/analysis/utils.py +0 -66
  4. roms_tools/constants.py +2 -0
  5. roms_tools/download.py +46 -3
  6. roms_tools/plot.py +22 -5
  7. roms_tools/setup/cdr_forcing.py +1126 -0
  8. roms_tools/setup/datasets.py +742 -87
  9. roms_tools/setup/grid.py +42 -4
  10. roms_tools/setup/river_forcing.py +11 -84
  11. roms_tools/setup/tides.py +81 -411
  12. roms_tools/setup/utils.py +241 -37
  13. roms_tools/tests/test_setup/test_cdr_forcing.py +772 -0
  14. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +53 -1
  15. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_tracer/.zattrs +1 -1
  16. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/.zarray +20 -0
  17. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/.zattrs +6 -0
  18. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_long_name/0 +0 -0
  19. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/.zarray +20 -0
  20. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/.zattrs +6 -0
  21. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_unit/0 +0 -0
  22. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +53 -1
  23. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_tracer/.zattrs +1 -1
  24. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/.zarray +20 -0
  25. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/.zattrs +6 -0
  26. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_long_name/0 +0 -0
  27. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/.zarray +20 -0
  28. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/.zattrs +6 -0
  29. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/tracer_unit/0 +0 -0
  30. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/.zattrs +1 -2
  31. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/.zmetadata +27 -5
  32. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/.zarray +20 -0
  33. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/.zattrs +5 -0
  34. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ntides/0 +0 -0
  35. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/omega/.zattrs +1 -3
  36. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/pot_Im/0.0.0 +0 -0
  37. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/pot_Re/0.0.0 +0 -0
  38. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ssh_Im/0.0.0 +0 -0
  39. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/ssh_Re/0.0.0 +0 -0
  40. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Im/0.0.0 +0 -0
  41. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/u_Re/0.0.0 +0 -0
  42. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Im/0.0.0 +0 -0
  43. roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Re/0.0.0 +0 -0
  44. roms_tools/tests/test_setup/test_datasets.py +103 -1
  45. roms_tools/tests/test_setup/test_tides.py +112 -47
  46. roms_tools/utils.py +115 -1
  47. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/METADATA +1 -1
  48. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/RECORD +51 -33
  49. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/WHEEL +1 -1
  50. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/licenses/LICENSE +0 -0
  51. {roms_tools-2.6.2.dist-info → roms_tools-2.7.0.dist-info}/top_level.txt +0 -0
roms_tools/setup/utils.py CHANGED
@@ -8,6 +8,7 @@ from datetime import datetime
8
8
  from dataclasses import fields, asdict
9
9
  import importlib.metadata
10
10
  import yaml
11
+ from roms_tools.constants import R_EARTH
11
12
  from roms_tools.utils import interpolate_from_rho_to_u, interpolate_from_rho_to_v
12
13
 
13
14
 
@@ -594,8 +595,8 @@ def compute_missing_surface_bgc_variables(bgc_data):
594
595
  # Define the relationships for missing variables
595
596
  variable_relations = {
596
597
  "pco2_air_alt": ("pco2_air", 1.0),
597
- "nox": (None, 1e-13), # has incorrect unit nmol/cm2/s, we need kg/m2/s
598
- "nhy": (None, 5e-12), # has incorrect unit nmol/cm2/s, we need kg/m2/s
598
+ "nox": (None, 1e-12), # kg/m2/s
599
+ "nhy": (None, 5e-12), # kg/m2/s
599
600
  }
600
601
 
601
602
  # Fill in missing variables using the defined relationships
@@ -609,6 +610,161 @@ def compute_missing_surface_bgc_variables(bgc_data):
609
610
  return bgc_data
610
611
 
611
612
 
613
+ def get_tracer_metadata_dict(include_bgc=True):
614
+ """Returns a dictionary of tracer metadata.
615
+
616
+ This function constructs a dictionary containing the names, units, and long names
617
+ of the tracers, based on whether biogeochemical tracers should be included or not.
618
+
619
+ Parameters
620
+ ----------
621
+ include_bgc : bool, optional
622
+ Whether to include biogeochemical tracers. Defaults to True.
623
+
624
+ Returns
625
+ -------
626
+ dict
627
+ A dictionary containing tracer names, units, and long names.
628
+ """
629
+ if include_bgc:
630
+ tracer_names = [
631
+ "temp",
632
+ "salt",
633
+ "PO4",
634
+ "NO3",
635
+ "SiO3",
636
+ "NH4",
637
+ "Fe",
638
+ "Lig",
639
+ "O2",
640
+ "DIC",
641
+ "DIC_ALT_CO2",
642
+ "ALK",
643
+ "ALK_ALT_CO2",
644
+ "DOC",
645
+ "DON",
646
+ "DOP",
647
+ "DOPr",
648
+ "DONr",
649
+ "DOCr",
650
+ "zooC",
651
+ "spChl",
652
+ "spC",
653
+ "spP",
654
+ "spFe",
655
+ "spCaCO3",
656
+ "diatChl",
657
+ "diatC",
658
+ "diatP",
659
+ "diatFe",
660
+ "diatSi",
661
+ "diazChl",
662
+ "diazC",
663
+ "diazP",
664
+ "diazFe",
665
+ ]
666
+ else:
667
+ tracer_names = ["temp", "salt"]
668
+
669
+ metadata = get_variable_metadata()
670
+
671
+ tracer_dict = {
672
+ tracer: {
673
+ "units": metadata[tracer]["units"],
674
+ "long_name": metadata[tracer]["long_name"],
675
+ }
676
+ for tracer in tracer_names
677
+ }
678
+
679
+ return tracer_dict
680
+
681
+
682
+ def add_tracer_metadata_to_ds(ds, include_bgc=True):
683
+ """Adds tracer metadata to a dataset.
684
+
685
+ This function adds tracer metadata (name, unit, long name) as coordinates to
686
+ the provided dataset.
687
+
688
+ Parameters
689
+ ----------
690
+ ds : xarray.Dataset
691
+ Dataset to which tracer metadata will be added.
692
+ include_bgc : bool, optional
693
+ Whether to include biogeochemical tracers. Defaults to True.
694
+
695
+ Returns
696
+ -------
697
+ xarray.Dataset
698
+ The dataset with added tracer metadata.
699
+ """
700
+ tracer_dict = get_tracer_metadata_dict(include_bgc)
701
+
702
+ tracer_names = list(tracer_dict.keys())
703
+ tracer_units = [tracer_dict[tracer]["units"] for tracer in tracer_names]
704
+ tracer_long_names = [tracer_dict[tracer]["long_name"] for tracer in tracer_names]
705
+
706
+ ds = ds.assign_coords(
707
+ tracer_name=("ntracers", tracer_names, {"long_name": "Tracer name"}),
708
+ tracer_unit=("ntracers", tracer_units, {"long_name": "Tracer unit"}),
709
+ tracer_long_name=(
710
+ "ntracers",
711
+ tracer_long_names,
712
+ {"long_name": "Tracer long name"},
713
+ ),
714
+ )
715
+
716
+ return ds
717
+
718
+
719
+ def get_tracer_defaults():
720
+ """Returns constant default tracer concentrations for ROMS-MARBL.
721
+
722
+ These values represent typical physical and biogeochemical tracer levels
723
+ (e.g., temperature, salinity, nutrients, carbon) in freshwater.
724
+
725
+ Returns
726
+ -------
727
+ dict
728
+ Dictionary of tracer names and their default concentrations
729
+ """
730
+ return {
731
+ "temp": 17.0, # degrees C
732
+ "salt": 1.0, # psu
733
+ "PO4": 2.7, # mmol m-3
734
+ "NO3": 24.2, # mmol m-3
735
+ "SiO3": 13.2, # mmol m-3
736
+ "NH4": 2.2, # mmol m-3
737
+ "Fe": 1.79, # mmol m-3
738
+ "Lig": 3 * 1.79, # mmol m-3, inferred from Fe
739
+ "O2": 187.5, # mmol m-3
740
+ "DIC": 2370.0, # mmol m-3
741
+ "DIC_ALT_CO2": 2370.0, # mmol m-3
742
+ "ALK": 2310.0, # meq m-3
743
+ "ALK_ALT_CO2": 2310.0, # meq m-3
744
+ "DOC": 1e-4, # mmol m-3
745
+ "DON": 1.0, # mmol m-3
746
+ "DOP": 0.1, # mmol m-3
747
+ "DOPr": 0.003, # mmol m-3
748
+ "DONr": 0.8, # mmol m-3
749
+ "DOCr": 1e-6, # mmol m-3
750
+ "zooC": 2.7, # mmol m-3
751
+ "spChl": 1.35, # mg m-3
752
+ "spC": 6.75, # mmol m-3
753
+ "spP": 1.5 * 0.03, # mmol m-3, inferred from ?
754
+ "spFe": 2.7e-5, # mmol m-3
755
+ "spCaCO3": 0.135, # mmol m-3
756
+ "diatChl": 0.135, # mg m-3
757
+ "diatC": 0.405, # mmol m-3
758
+ "diatP": 1.5 * 0.02, # mmol m-3, inferred from ?
759
+ "diatFe": 2.7e-6, # mmol m-3
760
+ "diatSi": 0.135, # mmol m-3
761
+ "diazChl": 0.015, # mg m-3
762
+ "diazC": 0.075, # mmol m-3
763
+ "diazP": 1.5 * 0.01, # mmol m-3, inferred from ?
764
+ "diazFe": 1.5e-6, # mmol m-3
765
+ }
766
+
767
+
612
768
  def extract_single_value(data):
613
769
  """Extracts a single value from an xarray.DataArray or numpy array.
614
770
 
@@ -982,11 +1138,8 @@ def gc_dist(lon1, lat1, lon2, lat2, input_in_degrees=True):
982
1138
  )
983
1139
  )
984
1140
 
985
- # Radius of the Earth in meters
986
- r_earth = 6371315.0
987
-
988
1141
  # Distance in meters
989
- dis = r_earth * dang
1142
+ dis = R_EARTH * dang
990
1143
 
991
1144
  return dis
992
1145
 
@@ -1041,16 +1194,17 @@ def convert_to_roms_time(ds, model_reference_date, climatology, time_name="time"
1041
1194
  return ds, time
1042
1195
 
1043
1196
 
1197
+ class NoAliasDumper(yaml.SafeDumper):
1198
+ def ignore_aliases(self, data):
1199
+ return True
1200
+
1201
+
1044
1202
  def _to_yaml(forcing_object, filepath: Union[str, Path]) -> None:
1045
1203
  """Serialize a forcing object (including its grid) into a YAML file.
1046
1204
 
1047
1205
  This function serializes a dataclass object (forcing_object) and its associated
1048
1206
  `grid` attribute into a YAML file. It includes additional metadata, such as
1049
- the version of the `roms-tools` package, and omits fields like `grid` and `ds`
1050
- that are not serializable or meant to be excluded.
1051
-
1052
- The function also converts datetime fields to ISO format strings for proper
1053
- serialization.
1207
+ the version of the `roms-tools` package.
1054
1208
 
1055
1209
  Parameters
1056
1210
  ----------
@@ -1069,8 +1223,53 @@ def _to_yaml(forcing_object, filepath: Union[str, Path]) -> None:
1069
1223
  # Convert the filepath to a Path object
1070
1224
  filepath = Path(filepath)
1071
1225
 
1072
- # Step 1: Serialize Grid data
1073
- # Check if the forcing_object has a grid attribute
1226
+ # Serialize object into dictionary
1227
+ yaml_data = _to_dict(forcing_object)
1228
+
1229
+ # Create YAML header with version information
1230
+ try:
1231
+ roms_tools_version = importlib.metadata.version("roms-tools")
1232
+ except importlib.metadata.PackageNotFoundError:
1233
+ roms_tools_version = "unknown"
1234
+
1235
+ header = f"---\nroms_tools_version: {roms_tools_version}\n---\n"
1236
+
1237
+ # Write to YAML file
1238
+ with filepath.open("w") as file:
1239
+ # Write the header first
1240
+ file.write(header)
1241
+ # Write the serialized YAML data
1242
+ yaml.dump(
1243
+ yaml_data,
1244
+ file,
1245
+ Dumper=NoAliasDumper,
1246
+ default_flow_style=False,
1247
+ sort_keys=False,
1248
+ )
1249
+
1250
+
1251
+ def _to_dict(forcing_object) -> None:
1252
+ """Serialize a forcing object (including its grid) into a dictionary.
1253
+
1254
+ This function serializes a dataclass object (forcing_object) and its associated
1255
+ `grid` attribute into a dictionary. It omits fields like `grid` and `ds`
1256
+ that are not serializable or meant to be excluded.
1257
+
1258
+ The function also converts datetime fields to ISO format strings for proper
1259
+ serialization.
1260
+
1261
+ Parameters
1262
+ ----------
1263
+ forcing_object : object
1264
+ The object that contains the forcing data, typically a dataclass with attributes
1265
+ such as `grid`, `start_time`, `end_time`, etc.
1266
+
1267
+ Returns
1268
+ -------
1269
+ dict
1270
+ """
1271
+
1272
+ # Serialize Grid data
1074
1273
  if hasattr(forcing_object, "grid") and forcing_object.grid is not None:
1075
1274
  grid_data = asdict(forcing_object.grid)
1076
1275
  grid_yaml_data = {"Grid": _pop_grid_data(grid_data)}
@@ -1078,7 +1277,7 @@ def _to_yaml(forcing_object, filepath: Union[str, Path]) -> None:
1078
1277
  grid_data = asdict(forcing_object.parent_grid)
1079
1278
  grid_yaml_data = {"ParentGrid": _pop_grid_data(grid_data)}
1080
1279
 
1081
- # Step 2: Ensure Paths are Strings
1280
+ # Ensure Paths are Strings
1082
1281
  def ensure_paths_are_strings(obj, key):
1083
1282
  attr = getattr(obj, key, None)
1084
1283
  if attr is not None and "path" in attr:
@@ -1087,20 +1286,14 @@ def _to_yaml(forcing_object, filepath: Union[str, Path]) -> None:
1087
1286
  attr["path"] = [str(p) if isinstance(p, Path) else p for p in paths]
1088
1287
  elif isinstance(paths, Path):
1089
1288
  attr["path"] = str(paths)
1289
+ elif isinstance(paths, dict):
1290
+ for key, path in paths.items():
1291
+ attr["path"][key] = str(path)
1090
1292
 
1091
1293
  ensure_paths_are_strings(forcing_object, "source")
1092
1294
  ensure_paths_are_strings(forcing_object, "bgc_source")
1093
1295
 
1094
- # Step 3: Get ROMS Tools version
1095
- try:
1096
- roms_tools_version = importlib.metadata.version("roms-tools")
1097
- except importlib.metadata.PackageNotFoundError:
1098
- roms_tools_version = "unknown"
1099
-
1100
- # Create YAML header with version information
1101
- header = f"---\nroms_tools_version: {roms_tools_version}\n---\n"
1102
-
1103
- # Step 4: Prepare Forcing Data
1296
+ # Prepare Forcing Data
1104
1297
  forcing_data = {}
1105
1298
  field_names = [field.name for field in fields(forcing_object)]
1106
1299
  filtered_field_names = [
@@ -1126,11 +1319,14 @@ def _to_yaml(forcing_object, filepath: Union[str, Path]) -> None:
1126
1319
  # If the field is a datetime object, convert it to ISO format
1127
1320
  if isinstance(value, datetime):
1128
1321
  value = value.isoformat()
1322
+ # Convert list of datetimes to list of ISO strings
1323
+ elif isinstance(value, list) and all(isinstance(v, datetime) for v in value):
1324
+ value = [v.isoformat() for v in value]
1129
1325
 
1130
1326
  # Add the field and its value to the forcing_data dictionary
1131
1327
  forcing_data[field_name] = value
1132
1328
 
1133
- # Step 5: Serialize `indices` data (conditionally)
1329
+ # Serialize `indices` data (conditionally)
1134
1330
  indices_data = getattr(forcing_object, "indices", None)
1135
1331
  if indices_data is not None:
1136
1332
  serialized_indices = {
@@ -1144,23 +1340,13 @@ def _to_yaml(forcing_object, filepath: Union[str, Path]) -> None:
1144
1340
 
1145
1341
  forcing_data["indices"] = serialized_indices
1146
1342
 
1147
- # Step 6: Combine Grid and Forcing Data into a single dictionary for the final YAML content
1343
+ # Combine Grid and Forcing Data into a single dictionary for the final YAML content
1148
1344
  yaml_data = {
1149
1345
  **grid_yaml_data, # Add the grid data to the final YAML structure
1150
1346
  forcing_object.__class__.__name__: forcing_data, # Include the serialized forcing object data
1151
1347
  }
1152
1348
 
1153
- # Step 7: Write to YAML file
1154
- with filepath.open("w") as file:
1155
- # Write the header first
1156
- file.write(header)
1157
- # Write the serialized YAML data
1158
- yaml.dump(
1159
- yaml_data,
1160
- file,
1161
- default_flow_style=False,
1162
- sort_keys=False,
1163
- )
1349
+ return yaml_data
1164
1350
 
1165
1351
 
1166
1352
  def _pop_grid_data(grid_data):
@@ -1346,3 +1532,21 @@ def wrap_longitudes(grid_ds, straddle):
1346
1532
  )
1347
1533
 
1348
1534
  return grid_ds
1535
+
1536
+
1537
+ def to_float(val):
1538
+ """Convert a value or list of values to float.
1539
+
1540
+ Parameters
1541
+ ----------
1542
+ val : float, int, or list of float/int
1543
+ A numeric value or a list of numeric values.
1544
+
1545
+ Returns
1546
+ -------
1547
+ float or list of float
1548
+ The input value(s) converted to float type.
1549
+ """
1550
+ if isinstance(val, list):
1551
+ return [float(v) for v in val]
1552
+ return float(val)