roms-tools 2.6.0__py3-none-any.whl → 2.6.1__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 (21) hide show
  1. roms_tools/analysis/roms_output.py +218 -10
  2. roms_tools/analysis/utils.py +41 -9
  3. roms_tools/regrid.py +9 -2
  4. roms_tools/setup/river_forcing.py +5 -5
  5. roms_tools/tests/test_analysis/test_roms_output.py +55 -0
  6. roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata +2 -2
  7. roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata +2 -2
  8. roms_tools/tests/test_setup/test_river_forcing.py +3 -3
  9. roms_tools/tests/test_vertical_coordinate.py +73 -0
  10. roms_tools/vertical_coordinate.py +6 -0
  11. {roms_tools-2.6.0.dist-info → roms_tools-2.6.1.dist-info}/METADATA +1 -1
  12. {roms_tools-2.6.0.dist-info → roms_tools-2.6.1.dist-info}/RECORD +21 -20
  13. /roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/{river_location → river_flux}/.zarray +0 -0
  14. /roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/{river_location → river_flux}/.zattrs +0 -0
  15. /roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/{river_location → river_flux}/0.0 +0 -0
  16. /roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/{river_location → river_flux}/.zarray +0 -0
  17. /roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/{river_location → river_flux}/.zattrs +0 -0
  18. /roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/{river_location → river_flux}/0.0 +0 -0
  19. {roms_tools-2.6.0.dist-info → roms_tools-2.6.1.dist-info}/WHEEL +0 -0
  20. {roms_tools-2.6.0.dist-info → roms_tools-2.6.1.dist-info}/licenses/LICENSE +0 -0
  21. {roms_tools-2.6.0.dist-info → roms_tools-2.6.1.dist-info}/top_level.txt +0 -0
@@ -9,11 +9,13 @@ from typing import Union, Optional
9
9
  from pathlib import Path
10
10
  import re
11
11
  import logging
12
+ import warnings
12
13
  from datetime import datetime, timedelta
13
14
  from roms_tools import Grid
14
15
  from roms_tools.vertical_coordinate import (
15
16
  compute_depth_coordinates,
16
17
  )
18
+ from roms_tools.utils import interpolate_from_rho_to_u, interpolate_from_rho_to_v
17
19
  from roms_tools.analysis.utils import _validate_plot_inputs, _generate_coordinate_range
18
20
 
19
21
 
@@ -415,6 +417,145 @@ class ROMSOutput:
415
417
  if save_path:
416
418
  plt.savefig(save_path, dpi=300, bbox_inches="tight")
417
419
 
420
+ def regrid(self, var_names=None, horizontal_resolution=None, depth_levels=None):
421
+ """Regrid the dataset both horizontally and vertically.
422
+
423
+ This method selects the specified variables, interpolates them onto a lat-lon-z horizontal grid. The horizontal target resolution and vertical target depth levels are either specified or inferred dynamically.
424
+
425
+ Parameters
426
+ ----------
427
+ var_names : list of str, optional
428
+ List of variable names to be regridded. If None, all variables in the dataset
429
+ are used.
430
+ horizontal_resolution : float, optional
431
+ Target horizontal resolution in degrees. If None, the nominal horizontal resolution is inferred from the grid.
432
+ depth_levels : xarray.DataArray, numpy.ndarray, list, optional
433
+ Target depth levels. If None, depth levels are determined dynamically.
434
+ If provided as a list or numpy array, it is safely converted to an `xarray.DataArray`.
435
+
436
+ Returns
437
+ -------
438
+ xarray.Dataset
439
+ The regridded dataset.
440
+ """
441
+
442
+ if var_names is None:
443
+ var_names = list(self.ds.data_vars)
444
+
445
+ # Check that all var_names exist in self.ds
446
+ missing_vars = [var for var in var_names if var not in self.ds.data_vars]
447
+ if missing_vars:
448
+ raise ValueError(
449
+ f"The following variables are not found in the dataset: {', '.join(missing_vars)}"
450
+ )
451
+
452
+ # Retain only the variables in var_names and drop others
453
+ ds = self.ds[var_names]
454
+
455
+ # Prepare lateral regrid
456
+ lat_deg = self.grid.ds["lat_rho"]
457
+ lon_deg = self.grid.ds["lon_rho"]
458
+ if self.grid.straddle:
459
+ lon_deg = xr.where(lon_deg > 180, lon_deg - 360, lon_deg)
460
+
461
+ if horizontal_resolution is None:
462
+ horizontal_resolution = self._infer_nominal_horizontal_resolution()
463
+ lons = _generate_coordinate_range(
464
+ lon_deg.min().values, lon_deg.max().values, horizontal_resolution
465
+ )
466
+ lons = xr.DataArray(lons, dims=["lon"], attrs={"units": "°E"})
467
+ lats = _generate_coordinate_range(
468
+ lat_deg.min().values, lat_deg.max().values, horizontal_resolution
469
+ )
470
+ lats = xr.DataArray(lats, dims=["lat"], attrs={"units": "°N"})
471
+ target_coords = {"lat": lats, "lon": lons}
472
+
473
+ # Prepare vertical regrid
474
+ if depth_levels is None:
475
+ depth_levels, _ = self._compute_exponential_depth_levels()
476
+
477
+ # Ensure depth_levels is an xarray.DataArray
478
+ if not isinstance(depth_levels, xr.DataArray):
479
+ depth_levels = xr.DataArray(
480
+ np.asarray(depth_levels),
481
+ dims=["depth"],
482
+ attrs={"long_name": "Depth", "units": "m"},
483
+ )
484
+
485
+ depth_levels = depth_levels.astype(np.float32)
486
+
487
+ # Initialize list to hold regridded datasets
488
+ regridded_datasets = []
489
+
490
+ for loc, dims in [
491
+ ("rho", ("eta_rho", "xi_rho")),
492
+ ("u", ("eta_rho", "xi_u")),
493
+ ("v", ("eta_v", "xi_rho")),
494
+ ]:
495
+ var_names_loc = [
496
+ var_name
497
+ for var_name in var_names
498
+ if all(dim in ds[var_name].dims for dim in dims)
499
+ ]
500
+ if var_names_loc:
501
+ ds_loc = (
502
+ ds[var_names_loc]
503
+ .rename({f"lat_{loc}": "lat", f"lon_{loc}": "lon"})
504
+ .where(self.grid.ds[f"mask_{loc}"])
505
+ )
506
+ self._get_depth_coordinates(depth_type="layer", locations=[loc])
507
+ layer_depth_loc = self.ds_depth_coords[f"layer_depth_{loc}"]
508
+ h_loc = self.grid.ds.h
509
+ if loc == "u":
510
+ h_loc = interpolate_from_rho_to_u(h_loc)
511
+ elif loc == "v":
512
+ h_loc = interpolate_from_rho_to_v(h_loc)
513
+
514
+ # Exclude the horizontal boundary cells since diagnostic variables may contain zeros there
515
+ ds_loc = ds_loc.isel({dims[0]: slice(1, -1), dims[1]: slice(1, -1)})
516
+ layer_depth_loc = layer_depth_loc.isel(
517
+ {dims[0]: slice(1, -1), dims[1]: slice(1, -1)}
518
+ )
519
+ h_loc = h_loc.isel({dims[0]: slice(1, -1), dims[1]: slice(1, -1)})
520
+
521
+ # Lateral regridding
522
+ lateral_regrid = LateralRegridFromROMS(ds_loc, target_coords)
523
+ ds_loc = lateral_regrid.apply(ds_loc)
524
+ layer_depth_loc = lateral_regrid.apply(layer_depth_loc)
525
+ h_loc = lateral_regrid.apply(h_loc)
526
+ # Vertical regridding
527
+ vertical_regrid = VerticalRegridFromROMS(ds_loc)
528
+ for var_name in var_names_loc:
529
+ if "s_rho" in ds_loc[var_name].dims:
530
+ attrs = ds_loc[var_name].attrs
531
+ regridded = vertical_regrid.apply(
532
+ ds_loc[var_name],
533
+ layer_depth_loc,
534
+ depth_levels,
535
+ mask_edges=False,
536
+ )
537
+ regridded = regridded.where(regridded.depth < h_loc)
538
+ ds_loc[var_name] = regridded
539
+ ds_loc[var_name].attrs = attrs
540
+
541
+ ds_loc = ds_loc.assign_coords({"depth": depth_levels})
542
+
543
+ # Collect regridded dataset for merging
544
+ regridded_datasets.append(ds_loc)
545
+
546
+ # Merge all regridded datasets
547
+ if regridded_datasets:
548
+ ds = xr.merge(regridded_datasets)
549
+
550
+ with warnings.catch_warnings():
551
+ warnings.filterwarnings("ignore", category=UserWarning)
552
+ ds = ds.rename({"abs_time": "time"}).set_index(time="time")
553
+ ds["time"].attrs = {"long_name": "Time"}
554
+ ds["lon"].attrs = {"long_name": "Longitude", "units": "Degrees East"}
555
+ ds["lat"].attrs = {"long_name": "Latitude", "units": "Degrees North"}
556
+
557
+ return ds
558
+
418
559
  def _get_depth_coordinates(self, depth_type="layer", locations=["rho"]):
419
560
  """Ensure depth coordinates are stored for a given location and depth type.
420
561
 
@@ -460,9 +601,11 @@ class ROMSOutput:
460
601
  zeta = 0
461
602
 
462
603
  for location in locations:
463
- self.ds_depth_coords[
464
- f"{depth_type}_depth_{location}"
465
- ] = compute_depth_coordinates(self.grid.ds, zeta, depth_type, location)
604
+ var_name = f"{depth_type}_depth_{location}"
605
+ if var_name not in self.ds_depth_coords:
606
+ self.ds_depth_coords[var_name] = compute_depth_coordinates(
607
+ self.grid.ds, zeta, depth_type, location
608
+ )
466
609
 
467
610
  def _load_model_output(self) -> xr.Dataset:
468
611
  """Load the model output."""
@@ -630,24 +773,40 @@ class ROMSOutput:
630
773
  return ds
631
774
 
632
775
  def _add_lat_lon_coords(self, ds: xr.Dataset) -> xr.Dataset:
633
- """Add latitude and longitude coordinates to the dataset.
776
+ """Add latitude and longitude coordinates to the dataset based on the grid.
634
777
 
635
- Adds "lat_rho" and "lon_rho" from the grid object to the dataset.
778
+ This method assigns latitude and longitude coordinates from the grid to the dataset.
779
+ It always adds the "lat_rho" and "lon_rho" coordinates. If the dataset contains the
780
+ "xi_u" or "eta_v" dimensions, it also adds the corresponding "lat_u", "lon_u",
781
+ "lat_v", and "lon_v" coordinates.
636
782
 
637
783
  Parameters
638
784
  ----------
639
785
  ds : xarray.Dataset
640
- Dataset to update.
786
+ Input dataset to which latitude and longitude coordinates will be added.
641
787
 
642
788
  Returns
643
789
  -------
644
790
  xarray.Dataset
645
- Dataset with "lat_rho" and "lon_rho" coordinates added.
791
+ Updated dataset with the appropriate latitude and longitude coordinates
792
+ assigned to "rho", "u", and "v" points if applicable.
646
793
  """
647
- ds = ds.assign_coords(
648
- {"lat_rho": self.grid.ds["lat_rho"], "lon_rho": self.grid.ds["lon_rho"]}
649
- )
794
+ coords_to_add = {
795
+ "lat_rho": self.grid.ds["lat_rho"],
796
+ "lon_rho": self.grid.ds["lon_rho"],
797
+ }
650
798
 
799
+ if "xi_u" in ds.dims:
800
+ coords_to_add.update(
801
+ {"lat_u": self.grid.ds["lat_u"], "lon_u": self.grid.ds["lon_u"]}
802
+ )
803
+ if "eta_v" in ds.dims:
804
+ coords_to_add.update(
805
+ {"lat_v": self.grid.ds["lat_v"], "lon_v": self.grid.ds["lon_v"]}
806
+ )
807
+
808
+ # Add all necessary coordinates in one go
809
+ ds = ds.assign_coords(coords_to_add)
651
810
  return ds
652
811
 
653
812
  def _infer_nominal_horizontal_resolution(self, lat=None):
@@ -693,3 +852,52 @@ class ROMSOutput:
693
852
  resolution_in_degrees = resolution_in_m / (meters_per_degree * np.cos(lat_rad))
694
853
 
695
854
  return resolution_in_degrees
855
+
856
+ def _compute_exponential_depth_levels(self, Nz=None, depth=None, h=None):
857
+ """Compute vertical grid center and face depths using an exponential profile.
858
+
859
+ Parameters
860
+ ----------
861
+ Nz : int, optional
862
+ Number of vertical levels. Defaults to `len(self.ds.s_rho)`.
863
+
864
+ depth : float, optional
865
+ Total depth of the domain. Defaults to `grid.ds.h.max().values`.
866
+
867
+ h : float, optional
868
+ Scaling parameter for the exponential profile. Defaults to `Nz / 4.5`.
869
+
870
+ Returns
871
+ -------
872
+ tuple of numpy.ndarray
873
+ Depth values at the vertical grid centers (`z_centers`) and grid faces (`z_faces`),
874
+ both rounded to two decimal places.
875
+ """
876
+ if Nz is None:
877
+ Nz = len(self.ds.s_rho)
878
+ if depth is None:
879
+ depth = self.grid.ds.h.max().values
880
+ if h is None:
881
+ h = Nz / 4.5
882
+
883
+ k = np.arange(1, Nz + 2)
884
+
885
+ # Define the exponential profile function
886
+ def exponential_profile(k, Nz, h):
887
+ return np.exp(k / h)
888
+
889
+ z_faces = np.vectorize(exponential_profile)(k, Nz, h)
890
+
891
+ # Normalize
892
+ z_faces -= z_faces[0]
893
+ z_faces *= depth / z_faces[-1]
894
+ z_faces[0] = 0.0
895
+
896
+ # Calculate center depths (average between adjacent face depths)
897
+ z_centers = (z_faces[:-1] + z_faces[1:]) / 2
898
+
899
+ # Round both z_faces and z_centers to two decimal places
900
+ z_faces = np.round(z_faces, 2)
901
+ z_centers = np.round(z_centers, 2)
902
+
903
+ return z_centers, z_faces
@@ -107,12 +107,13 @@ def _validate_plot_inputs(field, s, eta, xi, depth, lat, lon, include_boundary):
107
107
 
108
108
 
109
109
  def _generate_coordinate_range(min, max, resolution):
110
- """Generate an array of target latitude or longitude coordinates within a specified
111
- range.
110
+ """Generate an array of target coordinates (e.g., latitude or longitude) within a
111
+ specified range, with a resolution that is rounded to the nearest value of the form
112
+ `1/n` (or integer).
112
113
 
113
114
  This method generates an array of target coordinates between the provided `min` and `max`
114
- values, with a specified resolution (spacing) between each coordinate. Both `min` and `max`
115
- are included in the generated range.
115
+ values, ensuring that both `min` and `max` are included in the resulting range. The resolution
116
+ is rounded to the nearest fraction of the form `1/n` or an integer, based on the input.
116
117
 
117
118
  Parameters
118
119
  ----------
@@ -123,15 +124,46 @@ def _generate_coordinate_range(min, max, resolution):
123
124
  The maximum value (in degrees) of the coordinate range (inclusive).
124
125
 
125
126
  resolution : float
126
- The spacing (in degrees) between each coordinate in the array.
127
+ The spacing (in degrees) between each coordinate in the array. The resolution will
128
+ be rounded to the nearest value of the form `1/n` or an integer, depending on the size
129
+ of the resolution value.
127
130
 
128
131
  Returns
129
132
  -------
130
133
  numpy.ndarray
131
- An array of target coordinates generated from the specified range and resolution.
134
+ An array of target coordinates generated from the specified range, with the resolution
135
+ rounded to a suitable fraction (e.g., `1/n`) or integer, depending on the input resolution.
132
136
  """
133
137
 
134
- # Generate the array including both min and max values
135
- target = np.arange(min, max + resolution, resolution)
138
+ # Find the closest fraction of the form 1/n or integer to match the resolution
139
+ resolution_rounded = None
140
+ min_diff = float("inf") # Initialize the minimum difference as infinity
136
141
 
137
- return target
142
+ # Search for the best fraction or integer approximation to the resolution
143
+ for n in range(1, 1000): # Try fractions 1/n, where n ranges from 1 to 999
144
+ if resolution <= 1:
145
+ fraction = (
146
+ 1 / n
147
+ ) # For small resolutions (<= 1), consider fractions of the form 1/n
148
+ else:
149
+ fraction = n # For larger resolutions (>1), consider integers (n)
150
+
151
+ diff = abs(
152
+ fraction - resolution
153
+ ) # Calculate the difference between the fraction and the resolution
154
+
155
+ if diff < min_diff: # If the current fraction is a better approximation
156
+ min_diff = diff
157
+ resolution_rounded = fraction # Update the best fraction (or integer) found
158
+
159
+ # Adjust the start and end of the range to include integer values
160
+ start_int = np.floor(min) # Round the minimum value down to the nearest integer
161
+ end_int = np.ceil(max) # Round the maximum value up to the nearest integer
162
+
163
+ # Generate the array of target coordinates, including both the min and max values
164
+ target = np.arange(start_int, end_int + resolution_rounded, resolution_rounded)
165
+
166
+ # Truncate any values that exceed max (including small floating point errors)
167
+ target = target[target <= end_int + 1e-10]
168
+
169
+ return target.astype(np.float32)
roms_tools/regrid.py CHANGED
@@ -257,7 +257,7 @@ class VerticalRegridFromROMS:
257
257
  """
258
258
  self.grid = xgcm.Grid(ds, coords={"s_rho": {"center": "s_rho"}}, periodic=False)
259
259
 
260
- def apply(self, da, depth_coords, target_depth_levels):
260
+ def apply(self, da, depth_coords, target_depth_levels, mask_edges=True):
261
261
  """Applies vertical regridding from ROMS to the specified target depth levels.
262
262
 
263
263
  This method transforms the input data array `da` from the ROMS vertical coordinate (`s_rho`)
@@ -275,6 +275,9 @@ class VerticalRegridFromROMS:
275
275
  target_depth_levels : array-like
276
276
  The target depth levels to which the input data `da` will be regridded.
277
277
 
278
+ mask_edges: bool, optional
279
+ If activated, target values outside the range of depth_coords are masked with nan. Defaults to True.
280
+
278
281
  Returns
279
282
  -------
280
283
  xarray.DataArray
@@ -284,7 +287,11 @@ class VerticalRegridFromROMS:
284
287
  with warnings.catch_warnings():
285
288
  warnings.filterwarnings("ignore", category=FutureWarning, module="xgcm")
286
289
  transformed = self.grid.transform(
287
- da, "s_rho", target_depth_levels, target_data=depth_coords
290
+ da,
291
+ "s_rho",
292
+ target_depth_levels,
293
+ target_data=depth_coords,
294
+ mask_edges=mask_edges,
288
295
  )
289
296
 
290
297
  return transformed
@@ -441,21 +441,21 @@ class RiverForcing:
441
441
  return river_indices
442
442
 
443
443
  def _write_indices_into_dataset(self, ds):
444
- """Adds river location indices to the dataset as the "river_location" variable.
444
+ """Adds river location indices to the dataset as the "river_flux" variable.
445
445
 
446
- This method creates a new "river_location" variable
446
+ This method creates a new "river_flux" variable
447
447
  using river station indices from `self.indices` and assigns it to the dataset.
448
448
  The indices specify the river station locations in terms of eta_rho and xi_rho grid cell indices.
449
449
 
450
450
  Parameters
451
451
  ----------
452
452
  ds : xarray.Dataset
453
- The dataset to which the "river_location" variable will be added.
453
+ The dataset to which the "river_flux" variable will be added.
454
454
 
455
455
  Returns
456
456
  -------
457
457
  xarray.Dataset
458
- The modified dataset with the "river_location" variable added.
458
+ The modified dataset with the "river_flux" variable added.
459
459
  """
460
460
 
461
461
  river_locations = xr.zeros_like(self.grid.ds.h)
@@ -474,7 +474,7 @@ class RiverForcing:
474
474
 
475
475
  river_locations.attrs["long_name"] = "River ID plus local volume fraction"
476
476
  river_locations.attrs["units"] = "none"
477
- ds["river_location"] = river_locations
477
+ ds["river_flux"] = river_locations
478
478
 
479
479
  ds = ds.drop_vars(["lat_rho", "lon_rho"])
480
480
 
@@ -1,6 +1,7 @@
1
1
  import pytest
2
2
  from pathlib import Path
3
3
  import xarray as xr
4
+ import numpy as np
4
5
  import os
5
6
  import logging
6
7
  from datetime import datetime
@@ -486,3 +487,57 @@ def test_figure_gets_saved(roms_output_from_restart_file, tmp_path):
486
487
 
487
488
  assert filename.exists()
488
489
  filename.unlink()
490
+
491
+
492
+ @pytest.mark.skipif(xesmf is None, reason="xesmf required")
493
+ def test_regrid_all_variables(roms_output_from_restart_file):
494
+ ds_regridded = roms_output_from_restart_file.regrid()
495
+ assert isinstance(ds_regridded, xr.Dataset)
496
+ assert set(ds_regridded.data_vars).issubset(
497
+ set(roms_output_from_restart_file.ds.data_vars)
498
+ )
499
+ assert "lon" in ds_regridded.coords
500
+ assert "lat" in ds_regridded.coords
501
+ assert "depth" in ds_regridded.coords
502
+ assert "time" in ds_regridded.coords
503
+
504
+
505
+ @pytest.mark.skipif(xesmf is None, reason="xesmf required")
506
+ def test_regrid_specific_variables(roms_output_from_restart_file):
507
+ var_names = ["temp", "salt"]
508
+ ds_regridded = roms_output_from_restart_file.regrid(var_names=var_names)
509
+ assert isinstance(ds_regridded, xr.Dataset)
510
+ assert set(ds_regridded.data_vars) == set(var_names)
511
+
512
+ ds = roms_output_from_restart_file.regrid(var_names=[])
513
+ assert ds is None
514
+
515
+
516
+ @pytest.mark.skipif(xesmf is None, reason="xesmf required")
517
+ def test_regrid_missing_variable_raises_error(roms_output_from_restart_file):
518
+ with pytest.raises(
519
+ ValueError, match="The following variables are not found in the dataset"
520
+ ):
521
+ roms_output_from_restart_file.regrid(var_names=["fake_variable"])
522
+
523
+
524
+ @pytest.mark.skipif(xesmf is None, reason="xesmf required")
525
+ def test_regrid_with_custom_horizontal_resolution(roms_output_from_restart_file):
526
+ ds_regridded = roms_output_from_restart_file.regrid(horizontal_resolution=0.1)
527
+ assert isinstance(ds_regridded, xr.Dataset)
528
+ assert "lon" in ds_regridded.coords
529
+ assert "lat" in ds_regridded.coords
530
+
531
+ assert np.allclose(ds_regridded.lon.diff(dim="lon"), 0.1, atol=1e-4)
532
+ assert np.allclose(ds_regridded.lat.diff(dim="lat"), 0.1, atol=1e-4)
533
+
534
+
535
+ @pytest.mark.skipif(xesmf is None, reason="xesmf required")
536
+ def test_regrid_with_custom_depth_levels(roms_output_from_restart_file):
537
+ depth_levels = xr.DataArray(
538
+ np.linspace(0, 500, 51), dims=["depth"], attrs={"units": "m"}
539
+ )
540
+ ds_regridded = roms_output_from_restart_file.regrid(depth_levels=depth_levels)
541
+ assert isinstance(ds_regridded, xr.Dataset)
542
+ assert "depth" in ds_regridded.coords
543
+ np.allclose(ds_regridded.depth, depth_levels, atol=0.0)
@@ -58,7 +58,7 @@
58
58
  ],
59
59
  "long_name": "River ID (1-based Fortran indexing)"
60
60
  },
61
- "river_location/.zarray": {
61
+ "river_flux/.zarray": {
62
62
  "chunks": [
63
63
  20,
64
64
  20
@@ -80,7 +80,7 @@
80
80
  ],
81
81
  "zarr_format": 2
82
82
  },
83
- "river_location/.zattrs": {
83
+ "river_flux/.zattrs": {
84
84
  "_ARRAY_DIMENSIONS": [
85
85
  "eta_rho",
86
86
  "xi_rho"
@@ -86,7 +86,7 @@
86
86
  ],
87
87
  "long_name": "River ID (1-based Fortran indexing)"
88
88
  },
89
- "river_location/.zarray": {
89
+ "river_flux/.zarray": {
90
90
  "chunks": [
91
91
  20,
92
92
  20
@@ -108,7 +108,7 @@
108
108
  ],
109
109
  "zarr_format": 2
110
110
  },
111
- "river_location/.zattrs": {
111
+ "river_flux/.zattrs": {
112
112
  "_ARRAY_DIMENSIONS": [
113
113
  "eta_rho",
114
114
  "xi_rho"
@@ -236,7 +236,7 @@ class TestRiverForcingGeneral:
236
236
  for name in indices.keys():
237
237
  for (eta_rho, xi_rho) in indices[name]:
238
238
  assert coast[eta_rho, xi_rho]
239
- assert river_forcing.ds["river_location"][eta_rho, xi_rho] > 0
239
+ assert river_forcing.ds["river_flux"][eta_rho, xi_rho] > 0
240
240
 
241
241
  def test_missing_source_name(self, iceland_test_grid):
242
242
  with pytest.raises(ValueError, match="`source` must include a 'name'."):
@@ -475,14 +475,14 @@ class TestRiverForcingWithPrescribedIndices:
475
475
 
476
476
  # check that all values are integers for single cell rivers
477
477
  non_zero_values = river_forcing_with_prescribed_single_cell_indices.ds[
478
- "river_location"
478
+ "river_flux"
479
479
  ]
480
480
  is_integer = non_zero_values == np.floor(non_zero_values)
481
481
  assert (is_integer).all()
482
482
 
483
483
  # check that not all values are integers for multi cell rivers
484
484
  non_zero_values = river_forcing_with_prescribed_multi_cell_indices.ds[
485
- "river_location"
485
+ "river_flux"
486
486
  ]
487
487
  is_integer = non_zero_values == np.floor(non_zero_values)
488
488
  assert not (is_integer).all()
@@ -0,0 +1,73 @@
1
+ import pytest
2
+ import numpy as np
3
+ import xarray as xr
4
+
5
+ from roms_tools.vertical_coordinate import (
6
+ compute_cs,
7
+ sigma_stretch,
8
+ compute_depth,
9
+ compute_depth_coordinates,
10
+ )
11
+
12
+
13
+ def test_compute_cs():
14
+ sigma = np.linspace(-1, 0, 10)
15
+ theta_s, theta_b = 5, 2
16
+ cs = compute_cs(sigma, theta_s, theta_b)
17
+ assert cs.shape == sigma.shape
18
+ assert np.all(cs <= 0) and np.all(cs >= -1)
19
+
20
+ with pytest.raises(ValueError, match="theta_s must be between 0 and 10"):
21
+ compute_cs(sigma, 15, 2)
22
+
23
+ with pytest.raises(ValueError, match="theta_b must be between 0 and 4"):
24
+ compute_cs(sigma, 5, 5)
25
+
26
+
27
+ def test_sigma_stretch():
28
+ theta_s, theta_b, N = 5, 2, 10
29
+ cs, sigma = sigma_stretch(theta_s, theta_b, N, "r")
30
+ assert cs.shape == sigma.shape
31
+ assert isinstance(cs, xr.DataArray)
32
+ assert isinstance(sigma, xr.DataArray)
33
+
34
+ with pytest.raises(
35
+ ValueError,
36
+ match="Type must be either 'w' for vertical velocity points or 'r' for rho-points.",
37
+ ):
38
+ sigma_stretch(theta_s, theta_b, N, "invalid")
39
+
40
+
41
+ def test_compute_depth():
42
+ zeta = xr.DataArray(0.5)
43
+ h = xr.DataArray(10.0)
44
+ hc = 5.0
45
+ cs = xr.DataArray(np.linspace(-1, 0, 10), dims="s_rho")
46
+ sigma = xr.DataArray(np.linspace(-1, 0, 10), dims="s_rho")
47
+
48
+ depth = compute_depth(zeta, h, hc, cs, sigma)
49
+ assert depth.shape == sigma.shape
50
+ assert isinstance(depth, xr.DataArray)
51
+
52
+
53
+ def test_compute_depth_coordinates():
54
+ grid_ds = xr.Dataset(
55
+ {
56
+ "h": xr.DataArray([[10, 20], [30, 40]], dims=("eta_rho", "xi_rho")),
57
+ "Cs_r": xr.DataArray(np.linspace(-1, 0, 10), dims="s_rho"),
58
+ "sigma_r": xr.DataArray(np.linspace(-1, 0, 10), dims="s_rho"),
59
+ "Cs_w": xr.DataArray(np.linspace(-1, 0, 11), dims="s_w"),
60
+ "sigma_w": xr.DataArray(np.linspace(-1, 0, 11), dims="s_w"),
61
+ },
62
+ attrs={"hc": 5.0},
63
+ )
64
+
65
+ depth = compute_depth_coordinates(grid_ds, depth_type="layer", location="rho")
66
+ assert isinstance(depth, xr.DataArray)
67
+ assert "eta_rho" in depth.dims and "xi_rho" in depth.dims
68
+
69
+ with pytest.raises(ValueError, match="Invalid depth_type"):
70
+ compute_depth_coordinates(grid_ds, depth_type="invalid")
71
+
72
+ with pytest.raises(ValueError, match="Invalid location"):
73
+ compute_depth_coordinates(grid_ds, location="invalid")
@@ -171,6 +171,12 @@ def compute_depth_coordinates(
171
171
  - Spatial slicing (`eta`, `xi`) is performed before depth computation to optimize efficiency.
172
172
  - Depth calculations rely on the ROMS vertical stretching curves (`Cs`) and sigma-layers.
173
173
  """
174
+ # Validate location
175
+ valid_locations = {"rho", "u", "v"}
176
+ if location not in valid_locations:
177
+ raise ValueError(
178
+ f"Invalid location: {location}. Must be one of {valid_locations}."
179
+ )
174
180
 
175
181
  # Select the appropriate depth computation parameters
176
182
  if depth_type == "layer":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: roms-tools
3
- Version: 2.6.0
3
+ Version: 2.6.1
4
4
  Summary: Tools for running and analysing UCLA-ROMS simulations
5
5
  Author-email: Nora Loose <nora.loose@gmail.com>, Thomas Nicholas <tom@cworthy.org>
6
6
  License: Apache-2
@@ -3,11 +3,11 @@ ci/environment.yml,sha256=Ehxy6nYiVQXoS7EGlmNm2G0ZPHg6VFBGY1IflApIhIY,207
3
3
  roms_tools/__init__.py,sha256=jRghiteCoPjJvJjkFI36ocGyqzcTN5m-5eCa_DNQ9Dw,988
4
4
  roms_tools/download.py,sha256=W6S_DFVJDXat2w9MfyyHyushrswbpUI2hxegSuua1XE,6248
5
5
  roms_tools/plot.py,sha256=33ft1wN0kc_vIvyy_sIoY-nc0k4THXWLc_k7wEavNq8,17578
6
- roms_tools/regrid.py,sha256=BNtsJS5Oxmg0ivDQ-MLgVumeeLX15LZ7cp_RHAhUIOI,10222
6
+ roms_tools/regrid.py,sha256=av9fROSNxlDeczOB544zUjFRUTiUKO42wbfZ90mpuD0,10476
7
7
  roms_tools/utils.py,sha256=eveBkWuDsXNJADFMWFgRMHdbXkZlTyVK9dN2YAnjYJ4,13323
8
- roms_tools/vertical_coordinate.py,sha256=txObO4Duf-8Ef2r4JgK3C1PsDmaF7KeRngDTKx7MPHg,7034
9
- roms_tools/analysis/roms_output.py,sha256=Wm91jUICcmKFOaayt7ADWUopowBcfuh2dyk6Id65ICo,27915
10
- roms_tools/analysis/utils.py,sha256=RwV7SDBjx-pzJOJ7fG3q0qwfbwXThkcI6wAgeIe1bzY,5431
8
+ roms_tools/vertical_coordinate.py,sha256=uIxZl7rwY-fSCqXWhm6TXrOsLK4pMOMXOZB8VuP9xwg,7253
9
+ roms_tools/analysis/roms_output.py,sha256=DUSmT2YRoqd1fowuPnoCxdukuO_NC3ONphMWsu43_nE,36560
10
+ roms_tools/analysis/utils.py,sha256=K1Z1VyZUWKth1GMT8dumE1uUVcxrcO6rAm0Yfh8DtIg,7207
11
11
  roms_tools/setup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
12
  roms_tools/setup/boundary_forcing.py,sha256=LpNBurvcUhiemmubglI5aEgbOuPpjCDBj3C8XmRV60k,43609
13
13
  roms_tools/setup/datasets.py,sha256=WDEkMbLrzmdHxU5G9x_ECJASYVpmPa2i3oXWg9Rvxc4,84822
@@ -16,14 +16,15 @@ roms_tools/setup/grid.py,sha256=DWNPXoG2D76u6QMjOf_f5byvzGXT8gFOfiantUvnVjg,4940
16
16
  roms_tools/setup/initial_conditions.py,sha256=uVLkqpo1l1uw_LNjugypNVX2Tio5Hmi4txJoGC7P8G8,36012
17
17
  roms_tools/setup/mask.py,sha256=IK2SrVnMJwZjE4jNFtzMQhp1c5c8SUO45OIpCIrNSis,3413
18
18
  roms_tools/setup/nesting.py,sha256=j8l2zVCfhxNNtN4ZiSpipSlPXYmaoZWIPw6zImafsuk,26511
19
- roms_tools/setup/river_forcing.py,sha256=vH8sGGZ4fmcb8gDFfnwd1s9DflYksNI8e61WQyP1MxY,32824
19
+ roms_tools/setup/river_forcing.py,sha256=G-WoajtMloqS9gzt3aERSC8NObvC5ODi8XD_Q0jhYFU,32804
20
20
  roms_tools/setup/surface_forcing.py,sha256=HwIiA0qS64gBcq2ktqZx8ZhTIFRjweRZZe3haMTjCOg,23980
21
21
  roms_tools/setup/tides.py,sha256=2eFzQMVnlcHsG83AEhTUfGgQbO0CqI_pDb8YeZ5APTU,27088
22
22
  roms_tools/setup/topography.py,sha256=s1dSF0ZWCNdrZ25yv-pUcCgufxRmGgy3yr_VhdRM7LU,14126
23
23
  roms_tools/setup/utils.py,sha256=S9_alsNEORLjEA74LVbgaxhWyVKa-PqFSPida9MQNxw,40911
24
24
  roms_tools/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
25
25
  roms_tools/tests/test_regrid.py,sha256=-wzZ31BkUdSn81jq5NF1wnuaBRfd0aiYgQZEv5E_h9w,4682
26
- roms_tools/tests/test_analysis/test_roms_output.py,sha256=bckL5onorFl7tx9Mu5gZ1SyPmJya0j5IaiF4TcNjj8s,16556
26
+ roms_tools/tests/test_vertical_coordinate.py,sha256=4D2jruuxBwUYk1fSeaJKICgSjzpixhjd03Y4lyivMkQ,2348
27
+ roms_tools/tests/test_analysis/test_roms_output.py,sha256=EtxS9b5JHoREch99jbhs4QDg_3zmXDmVzXplbVrFocA,18811
27
28
  roms_tools/tests/test_setup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
29
  roms_tools/tests/test_setup/test_boundary_forcing.py,sha256=gET86PiGnMBejTBe69-inXpaEZL8YH4FMqyT_b8V-Zk,21183
29
30
  roms_tools/tests/test_setup/test_datasets.py,sha256=EInjmCLFNWJoO8nIQJhDtmFmMFmjcrr5L_5vJYm-qno,16435
@@ -31,7 +32,7 @@ roms_tools/tests/test_setup/test_fill.py,sha256=gDHuM58d3ECQE317ZntChYt4hWCfo4eV
31
32
  roms_tools/tests/test_setup/test_grid.py,sha256=e1S8TYt21TVczaEfyeYoAU-qxUhAgiugkHA3EDAGtIQ,14095
32
33
  roms_tools/tests/test_setup/test_initial_conditions.py,sha256=4LiULWmuktHoOty94asiFXgGNysag2okiosxG00om9M,15697
33
34
  roms_tools/tests/test_setup/test_nesting.py,sha256=WUhyP9mlMYwAJQgbgqjEU1zOyb8QD3XMgSZvHR_9LE8,18764
34
- roms_tools/tests/test_setup/test_river_forcing.py,sha256=gtqq67gdo4M0r5NgZT2C6gwYRikbjMuMwfE29VyLoIM,23839
35
+ roms_tools/tests/test_setup/test_river_forcing.py,sha256=on3j7pSMESVlFHKXNVBL4H7qqMc_DmMupAtDcUmZ014,23827
35
36
  roms_tools/tests/test_setup/test_surface_forcing.py,sha256=uZAZLbX6zK_e5CYOVZvZLp_i28A1o372naeaOeF83vE,27626
36
37
  roms_tools/tests/test_setup/test_tides.py,sha256=ACFXytda3Am984QMKGxtML00KPX1LvLWlSL0FpZTqyc,8085
37
38
  roms_tools/tests/test_setup/test_topography.py,sha256=EAF-zCHfo6XnXQfBTrSZLuZDVzegWHHlrf9oBmvY0hM,4963
@@ -948,16 +949,16 @@ roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatolo
948
949
  roms_tools/tests/test_setup/test_data/initial_conditions_with_bgc_from_climatology.zarr/zooC/0.0.0.0,sha256=t8ItMb1eQSD9ZijnctYDpcZSvDigD2-5YYGHMEMmnHM,194
949
950
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zattrs,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
950
951
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zgroup,sha256=I4N0bme0vMJ2Kz8QDwbD-i1fFJq1qOXaXTNSFGSgGVk,24
951
- roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata,sha256=f-y-IgLl25pRtOCGZI_F7Sol9wUwZiI7iXGohzo9whs,6141
952
+ roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/.zmetadata,sha256=yUONMD7prhzTzs4Haer6nX06txMntOJilUf6iaBMEJU,6133
952
953
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/abs_time/.zarray,sha256=AVrYaEeCVeJWUi2FbvisrHwjFyifr4Vt3Yag7Q-n33g,312
953
954
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/abs_time/.zattrs,sha256=0hG3RGr8ZCkuViKEvgrkYvnehkAk3wndb4vesT1jYeI,177
954
955
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/abs_time/0,sha256=PvO30V_gimagXglEZjQSDgwcOrEIoWmt9JQbY4toiic,48
955
956
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/nriver/.zarray,sha256=zti2U0p_i_zbPjPqTErqhxNnmnUtoQPZ_4r1-9yDH7Y,312
956
957
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/nriver/.zattrs,sha256=Ng-_DbPNix2DMxTo-GGKyyBnH6mo3teb2SuqtQWmokE,109
957
958
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/nriver/0,sha256=irfTfL897AhvCrDL98Vo7ZX6zJZhKFiktdZNWOdDlSU,64
958
- roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zarray,sha256=jYVuViCHVLNS6VB-K_4O32zkhyPcFnoUdLM8pA1nGXk,339
959
- roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/.zattrs,sha256=Ulaw9zPnPcNer1cdhavwQtQ_tUPPTDzTrQGcHP8u9y4,149
960
- roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_location/0.0,sha256=_gdp3GhMsDlFGIuL7bmmlXcm1aD6AFF1fAmIjanpwWI,196
959
+ roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_flux/.zarray,sha256=jYVuViCHVLNS6VB-K_4O32zkhyPcFnoUdLM8pA1nGXk,339
960
+ roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_flux/.zattrs,sha256=Ulaw9zPnPcNer1cdhavwQtQ_tUPPTDzTrQGcHP8u9y4,149
961
+ roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_flux/0.0,sha256=_gdp3GhMsDlFGIuL7bmmlXcm1aD6AFF1fAmIjanpwWI,196
961
962
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_name/.zarray,sha256=IiztAgS5VBT0bsRnDObags8I7F_tyQbMajEszHXfHb8,364
962
963
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_name/.zattrs,sha256=EPrut8rW6fC_MGEIOeqFacMyRlxf36MZsFCgP-nZ9d4,84
963
964
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/river_name/0,sha256=fuD1GoUrOX7tb_9WSj42rwDVUExgniEIJ9XrblUos0E,95
@@ -975,7 +976,7 @@ roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_n
975
976
  roms_tools/tests/test_setup/test_data/river_forcing_no_climatology.zarr/tracer_name/0,sha256=m5RTy6Ka6ScsKdL_pma6qpevroqCf8LJzuYXCFl9Mds,48
976
977
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zattrs,sha256=jFoQJqWC46fw5uLFNg8B7WYcGSGnc9XJAWTk3ZKejOY,29
977
978
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zgroup,sha256=I4N0bme0vMJ2Kz8QDwbD-i1fFJq1qOXaXTNSFGSgGVk,24
978
- roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata,sha256=FZTi-ZnRzmPH_XNilKJIiDIbJ9clP29eBHwKFgMxiEY,6901
979
+ roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/.zmetadata,sha256=zKm4eTj9Anuc0qm_Ip8PXeBofwHA5cnl9J1CQyVlT7Q,6893
979
980
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/abs_time/.zarray,sha256=bRSAng1vaRGTtFKNWhv1GcxwVKYhRsJnjbtncEbdWNA,314
980
981
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/abs_time/.zattrs,sha256=gDDHVjAFysUhH72DUxR7UwPLMOXFxalOA1fCWa6ImCQ,177
981
982
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/abs_time/0,sha256=SZIHK8deA_aE9wtgz1iLQ8ybjbVQK4Ym1FEsVryztu0,112
@@ -985,9 +986,9 @@ roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/month/0,sha256
985
986
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/nriver/.zarray,sha256=zti2U0p_i_zbPjPqTErqhxNnmnUtoQPZ_4r1-9yDH7Y,312
986
987
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/nriver/.zattrs,sha256=Ng-_DbPNix2DMxTo-GGKyyBnH6mo3teb2SuqtQWmokE,109
987
988
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/nriver/0,sha256=irfTfL897AhvCrDL98Vo7ZX6zJZhKFiktdZNWOdDlSU,64
988
- roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zarray,sha256=jYVuViCHVLNS6VB-K_4O32zkhyPcFnoUdLM8pA1nGXk,339
989
- roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/.zattrs,sha256=Ulaw9zPnPcNer1cdhavwQtQ_tUPPTDzTrQGcHP8u9y4,149
990
- roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_location/0.0,sha256=_gdp3GhMsDlFGIuL7bmmlXcm1aD6AFF1fAmIjanpwWI,196
989
+ roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_flux/.zarray,sha256=jYVuViCHVLNS6VB-K_4O32zkhyPcFnoUdLM8pA1nGXk,339
990
+ roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_flux/.zattrs,sha256=Ulaw9zPnPcNer1cdhavwQtQ_tUPPTDzTrQGcHP8u9y4,149
991
+ roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_flux/0.0,sha256=_gdp3GhMsDlFGIuL7bmmlXcm1aD6AFF1fAmIjanpwWI,196
991
992
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_name/.zarray,sha256=IiztAgS5VBT0bsRnDObags8I7F_tyQbMajEszHXfHb8,364
992
993
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_name/.zattrs,sha256=EPrut8rW6fC_MGEIOeqFacMyRlxf36MZsFCgP-nZ9d4,84
993
994
  roms_tools/tests/test_setup/test_data/river_forcing_with_bgc.zarr/river_name/0,sha256=fuD1GoUrOX7tb_9WSj42rwDVUExgniEIJ9XrblUos0E,95
@@ -1065,8 +1066,8 @@ roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Re/.zattrs,sha256=2z7
1065
1066
  roms_tools/tests/test_setup/test_data/tidal_forcing.zarr/v_Re/0.0.0,sha256=33Gl8otBmgqVarmAnZuEqTYS2_hVJUJh-iN1HzvaDuo,96
1066
1067
  roms_tools/tests/test_tiling/test_partition.py,sha256=b6EepZndVDv1B6Qt5_MbDfrFF2LtR0BF7i1t30xHEvA,7977
1067
1068
  roms_tools/tiling/partition.py,sha256=ZxDNGIKXZf_7eEzw9cxGP2XR_WBhZ4WCeIMl7_IdskA,12302
1068
- roms_tools-2.6.0.dist-info/licenses/LICENSE,sha256=yiff76E4xRioW2bHhlPpyYpstmePQBx2bF8HhgQhSsg,11318
1069
- roms_tools-2.6.0.dist-info/METADATA,sha256=QehIrzcMOtZk3NgvS1nFrp2ZvQfMrI9IgQUFiErw1b4,4698
1070
- roms_tools-2.6.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
1071
- roms_tools-2.6.0.dist-info/top_level.txt,sha256=aAf4T4nYQSkay5iKJ9kmTjlDgd4ETdp9OSlB4sJdt8Y,19
1072
- roms_tools-2.6.0.dist-info/RECORD,,
1069
+ roms_tools-2.6.1.dist-info/licenses/LICENSE,sha256=yiff76E4xRioW2bHhlPpyYpstmePQBx2bF8HhgQhSsg,11318
1070
+ roms_tools-2.6.1.dist-info/METADATA,sha256=-nmxtLvdMwgledQl2Fuf1ir4Se9NsYEi5wTWx8rMQGo,4698
1071
+ roms_tools-2.6.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
1072
+ roms_tools-2.6.1.dist-info/top_level.txt,sha256=aAf4T4nYQSkay5iKJ9kmTjlDgd4ETdp9OSlB4sJdt8Y,19
1073
+ roms_tools-2.6.1.dist-info/RECORD,,