-
Notifications
You must be signed in to change notification settings - Fork 3
Added class MaskWithRivers #88
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,6 +10,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import numpy as np | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import xarray as xr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from numpy.typing import ArrayLike | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from scipy.sparse import dok_array | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from bitsea.commons.bathymetry import Bathymetry | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from bitsea.commons.geodistances import extend_from_average | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -185,7 +186,7 @@ def convert_lon_lat_wetpoint_indices( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Remove all the points whose distance is greater than the max_radius | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cut_mask[distances > max_radius] = False | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If there is no water in the slice, we return (jp, ip) but we | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # If there is no water in the slice, we return (jp, ip), but we | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # also raise a warning | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if not np.any(cut_mask): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| warn( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -200,7 +201,7 @@ def convert_lon_lat_wetpoint_indices( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| distances[~cut_mask] = max_radius * max_radius + 1 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # We get the index of the minimum value. We need to unravel because | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # argmin works on the flatten array | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # argmin works on the flattened array | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| local_min = np.unravel_index( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| np.argmin(distances, axis=None), distances.shape | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -236,7 +237,7 @@ def mask_at_level(self, z: float) -> np.ndarray: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def bathymetry_in_cells(self) -> np.ndarray: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns a 2d map that for each columns associates the number of water | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns a 2d map that associates for each column the number of water | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| cells that are present on that column. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -247,7 +248,7 @@ def bathymetry_in_cells(self) -> np.ndarray: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def rough_bathymetry(self) -> np.ndarray: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Calculates the bathymetry used by the model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| It does not takes in account e3t | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| It does not take into account e3t | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| np.ndarray: a 2d numpy array of floats | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -259,7 +260,7 @@ def rough_bathymetry(self) -> np.ndarray: | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def bathymetry(self) -> np.ndarray: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Calculates the bathymetry used by the model | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Best evaluation, since it takes in account e3t. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Best evaluation, since it takes into account e3t. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Returns: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| a 2d numpy array of floats | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -670,7 +671,7 @@ def save_as_netcdf(self, file_path: Union[PathLike, str]): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MaskBathymetry(Bathymetry): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This class is a bathymetry, generated starting from a mask, i.e., it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| This class is a bathymetry generated starting from a mask, i.e., it | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| returns the z-coordinate of the bottom face of the deepest cell of the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| column that contains the point (lon, lat). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| """ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -680,7 +681,7 @@ def __init__(self, mask: Mask): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._bathymetry_data = mask.bathymetry() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Fix the bathymetry of the land cells to 0 (to be coherent with the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # behaviour of the bathymetry classes). Otherwise, if we let the land | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # behavior of the bathymetry classes). Otherwise, if we let the land | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # points to have bathymetry = 1e20, they will be in every | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # BathymetricBasin | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self._bathymetry_data[np.logical_not(self._mask[0, :, :])] = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -712,3 +713,140 @@ def __call__(self, lon, lat): | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return output | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return float(output.squeeze().item()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class MaskWithRivers(Mask): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| self, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| grid: Grid, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| zlevels: ArrayLike, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| mask_array: ArrayLike, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| river_positions: ArrayLike, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allow_broadcast: bool = False, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| e3t: Optional[np.ndarray] = None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): | |
| ): | |
| """ | |
| Initialize a MaskWithRivers object. | |
| Parameters | |
| ---------- | |
| grid : Grid | |
| The grid object defining the spatial domain. | |
| zlevels : ArrayLike | |
| The vertical levels of the mask (e.g., depth layers). | |
| mask_array : ArrayLike | |
| A boolean array indicating the presence of water (True) or land (False) | |
| at each grid cell and depth level. Shape should be (len(zlevels), grid.shape[0], grid.shape[1]). | |
| river_positions : ArrayLike | |
| An array or sparse matrix indicating the positions of river cells in the grid. | |
| Typically, this should be a 2D array (grid.shape[0], grid.shape[1]) where nonzero entries | |
| indicate river cells and their values may represent river indices or IDs. | |
| allow_broadcast : bool, optional | |
| If True, allows broadcasting of mask_array to match the required shape. Default is False. | |
| e3t : Optional[np.ndarray], optional | |
| Optional array specifying the thickness of each vertical layer. | |
| Notes | |
| ----- | |
| River cells are removed from the mask and stored separately for further processing. | |
| """ |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The early break when no river cells are found at a depth level assumes that rivers don't exist at deeper levels if they don't exist at a shallower level. This assumption may not hold if different rivers have different depths or if there are gaps in the vertical distribution of river cells. Consider processing all depth levels instead of breaking early, or document this assumption clearly if it's intentional.
| if not found_cells: | |
| break |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The allow_broadcast parameter is passed to the parent's init method, but at this point the mask_data has already been processed and potentially broadcasted. This could lead to inconsistent behavior because the parent class won't perform any broadcasting (since mask_data already has the correct shape). Consider setting allow_broadcast=False when calling super().init() to make the intent clearer.
| allow_broadcast=allow_broadcast, | |
| allow_broadcast=False, |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MaskWithRivers class should override the copy() method to properly handle the _river_cells attribute. The parent class's copy() method (which uses self.class) will fail because it doesn't pass the required river_positions parameter. This will cause a TypeError when attempting to copy a MaskWithRivers instance.
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The get_water_cells method lacks a docstring. This is particularly important because this method's behavior differs from the parent class's implementation - it returns the combined mask of both sea and river cells, which is critical for users to understand.
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -1,12 +1,14 @@ | ||||||||
| from itertools import product as cart_prod | ||||||||
|
|
||||||||
| import netCDF4 | ||||||||
| import numpy as np | ||||||||
| import pytest | ||||||||
|
|
||||||||
| from bitsea.commons.grid import RegularGrid | ||||||||
| from bitsea.commons.mask import FILL_VALUE | ||||||||
| from bitsea.commons.mask import Mask | ||||||||
| from bitsea.commons.mask import MaskBathymetry | ||||||||
| from bitsea.commons.mask import MaskWithRivers | ||||||||
| from bitsea.commons.mesh import Mesh | ||||||||
|
|
||||||||
|
|
||||||||
|
|
@@ -409,3 +411,28 @@ def test_mask_to_xarray(mask): | |||||||
| assert np.allclose(xarray_mask.longitude, mask.xlevels) | ||||||||
| assert np.allclose(xarray_mask.depth, mask.zlevels) | ||||||||
| assert np.allclose(xarray_mask.tmask, mask) | ||||||||
|
|
||||||||
|
|
||||||||
|
||||||||
| @pytest.mark.uses_test_data |
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no test coverage for the copy() method with MaskWithRivers instances. Since the base Mask class has a test_mask_copy test, and given that MaskWithRivers has additional state (_river_cells) that needs to be properly copied, a test should be added to verify that copying a MaskWithRivers instance works correctly and preserves the river information.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The MaskWithRivers class is missing a docstring. As a public class that extends Mask, it should have documentation explaining its purpose, parameters, and how it differs from the base Mask class (specifically how it handles river cells separately from sea cells).