Skip to content

Commit

Permalink
Merge pull request #266 from Deltares/numpy-constructors
Browse files Browse the repository at this point in the history
From data constructors for UgridDataArray
  • Loading branch information
Huite authored Jul 24, 2024
2 parents 6ad513a + 46ef73e commit bec2674
Show file tree
Hide file tree
Showing 13 changed files with 5,588 additions and 4,081 deletions.
5 changes: 5 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ UgridDataArray
UgridDataArray
UgridDataArray.ugrid
UgridDataArray.from_structured
UgridDataArray.from_data

UgridDataset
------------
Expand Down Expand Up @@ -232,6 +233,8 @@ UGRID1D Topology
Ugrid1d.from_geodataframe
Ugrid1d.from_shapely
Ugrid1d.to_shapely

Ugrid1d.create_data_array

Ugrid1d.plot

Expand Down Expand Up @@ -338,6 +341,8 @@ UGRID2D Topology
Ugrid2d.to_shapely
Ugrid2d.earcut_triangulate_polygons

Ugrid2d.create_data_array

Ugrid2d.plot

UGRID Roles Accessor
Expand Down
4 changes: 4 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Added

- Included ``edge_node_connectivity`` in :meth:`xugrid.Ugrid2d.from_meshkernel`,
so the ordering of edges is consistent with ``meshkernel``.
- Added :meth:`xugrid.Ugrid1d.create_data_array`,
:meth:`xugrid.Ugrid2d.create_data_array`, and
:meth:`xugrid.UgridDataArray.from_data` to more easily instantiate a
UgridDataArray from a grid topology and an array of values.

Changed
~~~~~~~
Expand Down
9,388 changes: 5,356 additions & 4,032 deletions pixi.lock

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ profile = "black"
exclude_lines = [
"pragma: no cover",
"@abc.abstractmethod",
"@abc.abstractproperty",
]

[tool.pixi.project]
Expand Down Expand Up @@ -100,7 +99,7 @@ sphinx = "*"
sphinx-gallery = "*"
xarray = "*"
zarr = "*"
ipykernel = "6.26.0.*" # So we can run examples
ipykernel = "*" # So we can run examples
twine = "*"
build = "*"

Expand Down
22 changes: 22 additions & 0 deletions tests/test_ugrid1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,25 @@ def test_equals():
grid_copy.attrs["attr"] = "something_else"
# Dataset.identical is called so returns False
assert not grid.equals(grid_copy)


def test_ugrid1d_create_data_array():
grid = grid1d()

uda = grid.create_data_array(np.zeros(grid.n_node), facet="node")
assert isinstance(uda, xugrid.UgridDataArray)

uda = grid.create_data_array(np.zeros(grid.n_edge), facet="edge")
assert isinstance(uda, xugrid.UgridDataArray)

# Error on facet
with pytest.raises(ValueError, match="Invalid facet"):
grid.create_data_array([1, 2, 3], facet="face")

# Error on on dimensions
with pytest.raises(ValueError, match="Can only create DataArrays from 1D arrays"):
grid.create_data_array([[1, 2, 3]], facet="node")

# Error on size
with pytest.raises(ValueError, match="Conflicting sizes"):
grid.create_data_array([1, 2, 3, 4], facet="node")
29 changes: 27 additions & 2 deletions tests/test_ugrid2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -1464,11 +1464,36 @@ def test_earcut_triangulate_polygons():

grid = xugrid.Ugrid2d.earcut_triangulate_polygons(polygons=gdf)
assert isinstance(grid, xugrid.Ugrid2d)
assert grid.n_face == 7
assert np.allclose(polygon.area, grid.area.sum())

grid, index = xugrid.Ugrid2d.earcut_triangulate_polygons(
polygons=gdf, return_index=True
)
assert isinstance(grid, xugrid.Ugrid2d)
assert isinstance(index, np.ndarray)
assert np.array_equal(index, np.zeros(7, dtype=int))
assert (index == 0).all()


def test_ugrid2d_create_data_array():
grid = grid2d()

uda = grid.create_data_array(np.zeros(grid.n_node), facet="node")
assert isinstance(uda, xugrid.UgridDataArray)

uda = grid.create_data_array(np.zeros(grid.n_edge), facet="edge")
assert isinstance(uda, xugrid.UgridDataArray)

uda = grid.create_data_array(np.zeros(grid.n_face), facet="face")
assert isinstance(uda, xugrid.UgridDataArray)

# Error on facet
with pytest.raises(ValueError, match="Invalid facet"):
grid.create_data_array([1, 2, 3, 4], facet="volume")

# Error on on dimensions
with pytest.raises(ValueError, match="Can only create DataArrays from 1D arrays"):
grid.create_data_array([[1, 2, 3, 4]], facet="face")

# Error on size
with pytest.raises(ValueError, match="Conflicting sizes"):
grid.create_data_array([1, 2, 3, 4, 5], facet="face")
5 changes: 5 additions & 0 deletions tests/test_ugrid_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ def test_init(self):
assert isinstance(self.uda.ugrid.grid, xugrid.Ugrid2d)
assert self.uda.grid.face_dimension in self.uda.coords

def test_from_data(self):
grid = self.uda.ugrid.grid
uda = xugrid.UgridDataArray.from_data(np.zeros(grid.n_node), grid, facet="node")
assert isinstance(uda, xugrid.UgridDataArray)

def test_reinit_error(self):
# Should not be able to initialize using a UgridDataArray.
with pytest.raises(TypeError, match="obj must be xarray.DataArray"):
Expand Down
48 changes: 27 additions & 21 deletions xugrid/core/accessorbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,63 +9,69 @@

class AbstractUgridAccessor(abc.ABC):
@abc.abstractmethod
def to_dataset():
def to_dataset(self):
pass

@abc.abstractmethod
def assign_node_coords():
def assign_node_coords(self):
pass

@abc.abstractmethod
def set_node_coords():
def set_node_coords(self):
pass

@abc.abstractproperty
def crs():
@property
@abc.abstractmethod
def crs(self):
pass

@abc.abstractmethod
def set_crs():
def set_crs(self):
pass

@abc.abstractmethod
def to_crs():
def to_crs(self):
pass

@abc.abstractmethod
def sel():
def sel(self):
pass

@abc.abstractmethod
def sel_points():
def sel_points(self):
pass

@abc.abstractmethod
def intersect_line():
def intersect_line(self):
pass

@abc.abstractmethod
def intersect_linestring():
def intersect_linestring(self):
pass

@abc.abstractproperty
def bounds():
@property
@abc.abstractmethod
def bounds(self):
pass

@abc.abstractproperty
def total_bounds():
@property
@abc.abstractmethod
def total_bounds(self):
pass

@abc.abstractproperty
def name():
@property
@abc.abstractmethod
def name(self):
pass

@abc.abstractproperty
def names():
@property
@abc.abstractmethod
def names(self):
pass

@abc.abstractproperty
def topology():
@property
@abc.abstractmethod
def topology(self):
pass

@staticmethod
Expand Down
23 changes: 23 additions & 0 deletions xugrid/core/wrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from typing import List, Sequence, Union

import xarray as xr
from numpy.typing import ArrayLike
from pandas import RangeIndex

import xugrid
Expand Down Expand Up @@ -285,6 +286,28 @@ def from_structured(
)
return UgridDataArray(face_da, grid)

@staticmethod
def from_data(data: ArrayLike, grid: UgridType, facet: str) -> UgridDataArray:
"""
Create a UgridDataArray from a grid and a 1D array of values.
Parameters
----------
data: array like
Values for this array. Must be a ``numpy.ndarray`` or castable to
it.
grid: Ugrid1d, Ugrid2d
facet: str
With which facet to associate the data. Options for Ugrid1d are,
``"node"`` or ``"edge"``. Options for Ugrid2d are ``"node"``,
``"edge"``, or ``"face"``.
Returns
-------
uda: UgridDataArray
"""
return grid.create_data_array(data=data, facet=facet)


class UgridDataset(DatasetForwardMixin):
def __init__(
Expand Down
2 changes: 1 addition & 1 deletion xugrid/ugrid/burn.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def _locate_polygon(
rings = np.cumsum([len(exterior)] + [len(interior) for interior in interiors])
vertices = np.vstack([exterior] + interiors).astype(np.float64)
triangles = mapbox_earcut.triangulate_float64(vertices, rings).reshape((-1, 3))
triangle_indices, grid_indices = grid.celltree._locate_faces(vertices, triangles)
triangle_indices, grid_indices = grid.celltree.locate_faces(vertices, triangles)
if all_touched:
return grid_indices
else:
Expand Down
29 changes: 29 additions & 0 deletions xugrid/ugrid/ugrid1d.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import numpy as np
import pandas as pd
import xarray as xr
from numpy.typing import ArrayLike

import xugrid
from xugrid import conversion
from xugrid.constants import (
BoolArray,
Expand Down Expand Up @@ -709,3 +711,30 @@ def reindex_like(
),
}
return obj.isel(indexers, missing_dims="ignore")

def create_data_array(self, data: ArrayLike, facet: str) -> "xugrid.UgridDataArray":
"""
Create a UgridDataArray from this grid and a 1D array of values.
Parameters
----------
data: array like
Values for this array. Must be a ``numpy.ndarray`` or castable to
it.
grid: Ugrid1d, Ugrid2d
facet: str
With which facet to associate the data. Options for Ugrid1d are,
``"node"`` or ``"edge"``. Options for Ugrid2d are ``"node"``,
``"edge"``, or ``"face"``.
Returns
-------
uda: UgridDataArray
"""
if facet == "node":
dimension = self.node_dimension
elif facet == "edge":
dimension = self.edge_dimension
else:
raise ValueError(f"Invalid facet: {facet}. Must be one of: node, edge.")
return self._create_data_array(data, dimension)
30 changes: 30 additions & 0 deletions xugrid/ugrid/ugrid2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pandas as pd
import xarray as xr
from numba_celltree import CellTree2d
from numpy.typing import ArrayLike
from scipy.sparse import coo_matrix, csr_matrix
from scipy.sparse.csgraph import reverse_cuthill_mckee

Expand Down Expand Up @@ -2243,3 +2244,32 @@ def _bbox_area(bounds):
collection = shapely.polygonize(shapely.linestrings(edges))
polygon = max(collection.geoms, key=lambda x: _bbox_area(x.bounds))
return polygon

def create_data_array(self, data: ArrayLike, facet: str) -> "xugrid.UgridDataArray":
"""
Create a UgridDataArray from this grid and a 1D array of values.
Parameters
----------
data: array like
Values for this array. Must be a ``numpy.ndarray`` or castable to
it.
grid: Ugrid1d, Ugrid2d
facet: str
With which facet to associate the data. Options for Ugrid1d are,
``"node"`` or ``"edge"``. Options for Ugrid2d are ``"node"``,
``"edge"``, or ``"face"``.
Returns
-------
uda: UgridDataArray
"""
if facet == "node":
dimension = self.node_dimension
elif facet == "edge":
dimension = self.edge_dimension
elif facet == "face":
dimension = self.face_dimension
else:
raise ValueError(f"Invalid facet: {facet}. Must be one of: node, edge.")
return self._create_data_array(data, dimension)
Loading

0 comments on commit bec2674

Please sign in to comment.