Skip to content

Commit

Permalink
Add support for XBEACH output (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
priscavdsluis authored Feb 20, 2024
1 parent 204bb48 commit 8463b27
Show file tree
Hide file tree
Showing 21 changed files with 256 additions and 150 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ These steps will ensure that the converter is installed within a virtual environ

- `file_version`: Specifies the version of the configuration file format.

- `model_type`: A string value indicating the model type used for the generation of the netCDF file. Supported options: `D-HYDRO`, `XBEACH`.

- `time_index_start`: An integer value specifying the starting index of the time steps to be converted. This allows you to specify a subset of time steps from the netCDF file.

- `time_index_end` (optional): An integer value specifying the end index of the time steps to be converted. When this option is not specified, the converter will include all time steps from the time step with index `time_index_start` to the last time step in the original dataset.
Expand Down Expand Up @@ -106,6 +108,7 @@ These steps will ensure that the converter is installed within a virtual environ
```json
{
"file_version":"0.1.0",
"model_type": "D-HYDRO",
"time_index_start":50,
"time_index_end":100,
"times_per_frame":3,
Expand Down Expand Up @@ -133,7 +136,7 @@ These steps will ensure that the converter is installed within a virtual environ
}
```

In the above example, we render two variables from the netCDF file: `Mesh2d_waterdepth` and `Mesh2d_s1`. For the animation we take a subset of the time steps. The animation will start at time step with index 50 and will end at time step with index 100. The animation will have a resolution of 3 time steps. Additionally, we apply a coordinate shift to ensure that the meshes have an origin at (0,0). Furthermore, we set the scale for both the horizontal and vertical directions to 0.5, resulting in a reduction of size in all directions by a factor of two.
In the above example, we render two variables from a D-HYDRO output map netCDF file: `Mesh2d_waterdepth` and `Mesh2d_s1`. For the animation we take a subset of the time steps. The animation will start at time step with index 50 and will end at time step with index 100. The animation will have a resolution of 3 time steps. Additionally, we apply a coordinate shift to ensure that the meshes have an origin at (0,0). Furthermore, we set the scale for both the horizontal and vertical directions to 0.5, resulting in a reduction of size in all directions by a factor of two.

For the `Mesh2d_waterdepth` variable, an additional threshold mesh is rendered at a height of 0.01. Each mesh is assigned its own color, specified by the normalized red, green, blue and alpha (RGBA) values.

Expand Down
13 changes: 12 additions & 1 deletion netcdf_to_gltf_converter/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
from packaging.version import Version
from pydantic import BaseModel as PydanticBaseModel
from pydantic import Extra, root_validator, validator
from strenum import StrEnum

from netcdf_to_gltf_converter.utils.validation import in_range

Color = List[float]


class BaseModel(PydanticBaseModel):
"""BaseModel defines the base for Pydantic model classes."""

Expand Down Expand Up @@ -90,6 +90,14 @@ def from_file(cls: Type[TJsonConfigFile], path: Path) -> TJsonConfigFile:

return cls.parse_file(path)

class ModelType(StrEnum):
"""The model type of the input data."""

DHYDRO = "D-HYDRO"
"""Output from a D-HYDRO model (UGRID)."""

XBEACH = "XBEACH"
"""Output from an XBEACH model (regular grid)."""

class Variable(BaseModel):
"""Configuration properties of a variable."""
Expand Down Expand Up @@ -160,6 +168,9 @@ class Config(AbstractJsonConfigFile, AbstractFileVersionFile):

_expected_file_version = Version("0.1.0")

model_type: ModelType
"""ModelType: The model type that is used for the input."""

time_index_start: int
"""int: The time index the animation should start with."""

Expand Down
2 changes: 1 addition & 1 deletion netcdf_to_gltf_converter/data/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import numpy as np

from netcdf_to_gltf_converter.custom_types import Color
from netcdf_to_gltf_converter.typing.custom_types import Color
from netcdf_to_gltf_converter.utils.arrays import float32_array, validate_2d_array


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_coordinate_variables(data, standard_name: str) -> List[xr.DataArray]:
return coord_vars


class GridWrapper(ABC):
class GridBase(ABC):
"""Class that serves as a wrapper object for a grid object.
The wrapper allows for easier retrieval of relevant data.
"""
Expand Down Expand Up @@ -62,19 +62,20 @@ def fill_value(self) -> int:
pass


class VariableWrapper(ABC):
class VariableBase(ABC):
"""Class that serves as a wrapper object for an xarray.DataArray.
The wrapper allows for easier retrieval of relevant data.
"""

def __init__(self, data: xr.DataArray) -> None:
"""Initialize a VariableWrapper with the specified data.
"""Initialize a VariableBase with the specified data.
Args:
data (xr.DataArray): The variable data.
"""
self._data = data
self._coordinates = self._get_coordinates()
self._time_var_name = get_coordinate_variables(data, "time")[0].name

@property
def coordinates(self) -> np.ndarray:
Expand All @@ -86,16 +87,14 @@ def coordinates(self) -> np.ndarray:
return self._coordinates

@property
@abstractmethod
def time_index_max(self) -> int:
"""Get the maximum time step index for this data variable.
Returns:
int: An integer specifying the maximum time step index.
"""
pass
return self._data.sizes[self._time_var_name] - 1

@abstractmethod
def get_data_at_time(self, time_index: int) -> np.ndarray:
"""Get the variable values at the specified time index.
Expand All @@ -105,24 +104,26 @@ def get_data_at_time(self, time_index: int) -> np.ndarray:
Returns:
np.ndarray: A 1D np.ndarray of floats.
"""
pass
time_filter = {self._time_var_name : time_index}
return self._data.isel(**time_filter).values.flatten()


def _get_coordinates(self) -> np.ndarray:
def get_coordinates(standard_name: str):
return get_coordinate_variables(self._data, standard_name)[0].values
return get_coordinate_variables(self._data, standard_name)[0].values.flatten()

x_coords = get_coordinates("projection_x_coordinate")
y_coords = get_coordinates("projection_y_coordinate")
return np.column_stack([x_coords, y_coords])


class DatasetWrapper(ABC):
class DatasetBase(ABC):
"""Class that serves as a wrapper object for an xarray.Dataset.
The wrapper allows for easier retrieval of relevant data.
"""

def __init__(self, dataset: xr.Dataset) -> None:
"""Initialize a DatasetWrapper with the specified arguments.
"""Initialize a DatasetBase with the specified arguments.
Args:
dataset (xr.Dataset): The xarray Dataset.
Expand All @@ -131,11 +132,11 @@ def __init__(self, dataset: xr.Dataset) -> None:

@property
@abstractmethod
def grid(self) -> GridWrapper:
def grid(self) -> GridBase:
"""Get the grid definition from the data set.
Returns:
GridWrapper: A GridWrapper object created from the data set.
GridBase: A GridBase object created from the data set.
"""
pass

Expand Down Expand Up @@ -205,14 +206,14 @@ def get_array(self, variable_name: str) -> xr.DataArray:
return self._dataset[variable_name]

@abstractmethod
def get_variable(self, variable_name: str) -> VariableWrapper:
def get_variable(self, variable_name: str) -> VariableBase:
"""Get the variable with the specified name from the data set.
Args:
variable_name (str): The variable name.
Returns:
VariableWrapper: The wrapper object for the variable.
VariableBase: The wrapper object for the variable.
Raises:
ValueError: When the dataset does not contain a variable with the name.
Expand Down
39 changes: 22 additions & 17 deletions netcdf_to_gltf_converter/netcdf/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
import numpy as np
import xarray as xr

from netcdf_to_gltf_converter.config import Config, Variable
from netcdf_to_gltf_converter.config import Config, ModelType, Variable
from netcdf_to_gltf_converter.data.mesh import MeshAttributes, TriangularMesh
from netcdf_to_gltf_converter.netcdf.ugrid.wrapper import UgridDataset
from netcdf_to_gltf_converter.netcdf.wrapper import (
DatasetWrapper,
GridWrapper,
VariableWrapper,
from netcdf_to_gltf_converter.netcdf.ugrid.ugrid_data import UgridDataset
from netcdf_to_gltf_converter.netcdf.xbeach.xbeach_data import XBeachDataset
from netcdf_to_gltf_converter.netcdf.netcdf_data import (
DatasetBase,
GridBase,
VariableBase,
)
from netcdf_to_gltf_converter.preprocessing.interpolation import (
NearestPointInterpolator,
Expand All @@ -35,16 +36,20 @@ def parse(self, dataset: xr.Dataset, config: Config) -> List[TriangularMesh]:
dataset (xr.Dataset): The NetCDF dataset.
config (Config): The converter configuration.
"""
ugrid_dataset = UgridDataset(dataset)
Parser._transform_grid(config, ugrid_dataset)

grid = ugrid_dataset.grid
if config.model_type == ModelType.DHYDRO:
dataset = UgridDataset(dataset)
elif config.model_type == ModelType.XBEACH:
dataset = XBeachDataset(dataset)

Parser._transform_grid(config, dataset)

grid = dataset.grid
triangulate(grid)

triangular_meshes = []

for variable in config.variables:
data_mesh = self._parse_variable(variable, grid, ugrid_dataset, config)
data_mesh = self._parse_variable(variable, grid, dataset, config)
triangular_meshes.append(data_mesh)

if variable.use_threshold:
Expand All @@ -59,8 +64,8 @@ def parse(self, dataset: xr.Dataset, config: Config) -> List[TriangularMesh]:
def _parse_variable(
self,
variable: Variable,
grid: GridWrapper,
ugrid_dataset: DatasetWrapper,
grid: GridBase,
ugrid_dataset: DatasetBase,
config: Config,
):
data = ugrid_dataset.get_variable(variable.name)
Expand Down Expand Up @@ -98,14 +103,14 @@ def _get_time_indices(time_index_max: int, config: Config):
return inclusive_range(start, end, config.times_per_frame)

@staticmethod
def _transform_grid(config: Config, ugrid_dataset):
def _transform_grid(config: Config, dataset: DatasetBase):
if config.shift_coordinates:
shift(ugrid_dataset)
shift(dataset)

variables = [var.name for var in config.variables]
scale(ugrid_dataset, variables, config.scale_horizontal, config.scale_vertical)
scale(dataset, variables, config.scale_horizontal, config.scale_vertical)

def _interpolate(self, data: VariableWrapper, time_index: int, grid: GridWrapper):
def _interpolate(self, data: VariableBase, time_index: int, grid: GridBase):
return self._interpolator.interpolate(
data.coordinates, data.get_data_at_time(time_index), grid
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import xugrid as xu

from netcdf_to_gltf_converter.netcdf.ugrid.conventions import AttrKey, CfRoleAttrValue
from netcdf_to_gltf_converter.netcdf.wrapper import (
DatasetWrapper,
GridWrapper,
VariableWrapper,
from netcdf_to_gltf_converter.netcdf.netcdf_data import (
DatasetBase,
GridBase,
VariableBase,
)


Expand All @@ -20,7 +20,7 @@ class Topology(str, Enum):
faces = "face_coordinates"


class Ugrid(GridWrapper):
class Ugrid(GridBase):
"""Class that serves as a wrapper object for a xu.Ugrid2d object.
The wrapper allows for easier retrieval of relevant data.
"""
Expand Down Expand Up @@ -69,33 +69,13 @@ def fill_value(self) -> int:
return self._ugrid2d.fill_value


class UgridVariable(VariableWrapper):
class UgridVariable(VariableBase):
"""Class that serves as a wrapper object for an xarray.DataArray with UGrid conventions.
The wrapper allows for easier retrieval of relevant data.
"""

@property
def time_index_max(self) -> int:
"""Get the maximum time step index for this data variable.
Returns:
int: An integer specifying the maximum time step index.
"""
return self._data.sizes["time"] - 1

def get_data_at_time(self, time_index: int) -> np.ndarray:
"""Get the variable values at the specified time index.
Args:
time_index (int): The time index.
Returns:
np.ndarray: A 1D np.ndarray of floats.
"""
return self._data.isel(time=time_index).to_numpy()


class UgridDataset(DatasetWrapper):
class UgridDataset(DatasetBase):
"""Class that serves as a wrapper object for an xarray.Dataset with UGrid conventions.
The wrapper allows for easier retrieval of relevant data.
"""
Expand All @@ -105,7 +85,6 @@ def __init__(self, dataset: xr.Dataset) -> None:
Args:
dataset (xr.Dataset): The xarray Dataset.
config (Config): The converter configuration.
"""
super().__init__(dataset)

Expand Down
Empty file.
Loading

0 comments on commit 8463b27

Please sign in to comment.