Source code for fridom.framework.field_metadata
"""dataclass for the metadata of a ScalarField."""
from __future__ import annotations
from copy import deepcopy
from dataclasses import dataclass, field
from functools import partial
import fridom.framework as fr
[docs]
@partial(fr.utils.jaxify, dynamic=("position", ))
@dataclass
class FieldMetadata:
"""
Metadata for the ScalarField.
Description
-----------
The FieldMetadata class contains all metadata for the ScalarField. This
includes the name, long name, units, and additional attributes for the
NetCDF file or xarray. The metadata also contains information about the
position of the ScalarField on the grid, the topology, and the boundary
conditions.
Parameters
----------
name : str (default "unnamed")
Name of the ScalarField
long_name : str (default "Unnamed")
Long name of the ScalarField
units : str (default "n/a")
Units of the ScalarField
nc_attrs : dict | None (default None)
Additional attributes for the NetCDF file or xarray
is_spectral : bool (default False)
True if the ScalarField should be initialized in spectral space
topo : list[bool] | None (default None)
Topology of the ScalarField. If None, the ScalarField is
assumed to be fully extended in all directions. If a list of booleans
is given, the ScalarField has no extend in the directions where the
corresponding entry is False.
position : fr.grid.Position | None (default None)
Position of the ScalarField on the grid
bc_types : tuple[BCType] | None (default None)
Tuple of BCType objects that specify the type of boundary condition
in each direction. If None, the default boundary conditions is Neumann.
_flags : dict (default {"NO_ADV": False,
"ENABLE_MIXING": False,
"ENABLE_FRICTION": False})
Dictionary with flag options for the ScalarField
"""
name: str = "unnamed"
long_name: str = "Unnamed"
units: str = "n/a"
nc_attrs: dict | None = None
is_spectral: bool = False
_topo: tuple[bool] | None = None
position: fr.grid.Position | None = None
_bc_types: tuple[fr.grid.BCType] | None = None
_flags: dict = field(
default_factory=lambda: {"NO_ADV": False,
"ENABLE_MIXING": False,
"ENABLE_FRICTION": False})
[docs]
def set_default(self, mset: fr.ModelSettingsBase) -> None:
"""Set None values to default values."""
if self.nc_attrs is None:
self.nc_attrs = {}
if self.position is None:
self.position = mset.grid.cell_center
if self.topo is None:
self.topo = tuple([True] * mset.grid.n_dims)
if self.bc_types is None:
self.bc_types = [fr.grid.BCType.NEUMANN] * mset.grid.n_dims
def __copy__(self) -> FieldMetadata:
return deepcopy(self)
[docs]
def copy(self) -> FieldMetadata:
"""Create a deep copy of the FieldMetadata."""
return deepcopy(self)
# ================================================================
# Serialization
# ================================================================
[docs]
def to_serializable(self) -> dict:
"""Convert the FieldMetadata to a serializable dictionary."""
res = {
"name": self.name,
"long_name": self.long_name,
"units": self.units,
"nc_attrs": [str(key) for key in self.nc_attrs],
"is_spectral": int(self.is_spectral),
"topo": tuple(int(x) for x in self.topo),
"position": [x.value for x in self.position.positions],
"bc_types": [x.value for x in self.bc_types],
"flags": [str(key) for key in self.flags],
}
res.update(self.nc_attrs)
for flag, value in self.flags.items():
res[flag] = int(value)
return res
[docs]
@classmethod
def from_serializable(cls, data: dict) -> FieldMetadata:
"""Create a FieldMetadata object from a serializable dictionary."""
return cls(name=data["name"],
long_name=data["long_name"],
units=data["units"],
nc_attrs={key: data[key] for key in data["nc_attrs"]},
is_spectral=bool(data["is_spectral"]),
_topo=tuple(bool(x) for x in data["topo"]),
position=fr.grid.Position(
[fr.grid.AxisPosition(x) for x in data["position"]]),
_bc_types=tuple(fr.grid.BCType(x) for x in data["bc_types"]),
_flags={key: bool(data[key]) for key in data["flags"]})
# ================================================================
# Properties
# ================================================================
@property
def topo(self) -> tuple[bool]:
"""
The topology of the Scalar Field.
Description
-----------
The topology of a scalar field determines whether the field lives in
a certain direction. If the topology is True in a direction, the field
is fully extended in this direction, if it is False, the field has no
extend in this direction.
"""
return self._topo
@topo.setter
def topo(self, topo: tuple[bool] | list[bool]) -> None:
self._topo = tuple(topo)
@property
def bc_types(self) -> tuple[fr.grid.BCType] | None:
"""The boundary condition types for the ScalarField."""
return self._bc_types
@bc_types.setter
def bc_types(self, bc_types: tuple[fr.grid.BCType] | None) -> None:
if bc_types is not None and len(bc_types) != len(self.position.positions):
msg = "Number of BCType objects must match the number of positions"
raise ValueError(msg)
self._bc_types = tuple(bc_types)
@property
def flags(self) -> dict:
"""Dictionary with flag options for the ScalarField."""
return self._flags
@flags.setter
def flags(self, update: dict) -> None:
for key, value in update.items():
if key not in self._flags:
msg = f"Flag {key} not available. "
msg += f"Available flags: {self._flags}"
raise KeyError(msg)
if not isinstance(value, bool):
msg = f"Flag {key} must be a boolean, "
msg += f"but is of type {type(value)}"
raise TypeError(msg)
self._flags[key] = value