| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| """Modules that computes relevant state features to be used by ML components.""" |
|
|
| from typing import Any, Callable, Mapping, Optional, Protocol, Sequence |
| from dinosaur import coordinate_systems |
| from dinosaur import primitive_equations |
| from dinosaur import pytree_utils |
| from dinosaur import radiation |
| from dinosaur import scales |
| from dinosaur import spherical_harmonic |
| from dinosaur import typing |
| from dinosaur import xarray_utils |
| import gin |
| import haiku as hk |
| import jax |
| import jax.numpy as jnp |
| from model.legacy import transforms |
| import numpy as np |
|
|
|
|
| Array = typing.Array |
| Pytree = typing.Pytree |
| TransformModule = typing.TransformModule |
| KeyWithCosLatFactor = typing.KeyWithCosLatFactor |
|
|
|
|
| class FeaturesFn(Protocol): |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| ... |
|
|
|
|
| FeaturesModule = Callable[..., FeaturesFn] |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class PrimitiveEquationsDiagnosticState(hk.Module): |
| """Features modules that returns processed DiagnosticState for PE.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
| self.coords = coords |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> primitive_equations.DiagnosticState: |
| del memory, diagnostics, randomness, forcing |
| if not isinstance(inputs, primitive_equations.State): |
| inputs = primitive_equations.State(**inputs) |
| d_state = primitive_equations.compute_diagnostic_state(inputs, self.coords) |
| return self.features_transform_fn(d_state.asdict()) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class VelocityAndPrognostics(hk.Module): |
| """Features module that returns prognostics + u,v and optionally gradients.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| fields_to_include: Optional[Sequence[str]] = None, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| compute_gradients_module: TransformModule = transforms.EmptyTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
| self.coords = coords |
| self.fields_to_include = fields_to_include |
| self.compute_gradients_fn = compute_gradients_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def _extract_features( |
| self, |
| inputs: typing.Pytree, |
| prefix: str = '', |
| ) -> typing.Pytree: |
| """Returns a nodal velocity and prognostic features.""" |
| |
| |
|
|
| |
| if set(['vorticity', 'divergence']).issubset(inputs.keys()) and not set( |
| ['u', 'v'] |
| ).intersection(inputs.keys()): |
| cos_lat_u, cos_lat_v = spherical_harmonic.get_cos_lat_vector( |
| inputs['vorticity'], inputs['divergence'], self.coords.horizontal |
| ) |
| modal_features = { |
| KeyWithCosLatFactor(prefix + 'u', 1): cos_lat_u, |
| KeyWithCosLatFactor(prefix + 'v', 1): cos_lat_v, |
| } |
| else: |
| modal_features = {} |
| prognostics_keys = list(inputs.keys()) |
| prognostics_keys.remove('tracers') |
| prognostics_keys.remove('sim_time') |
| for k in prognostics_keys: |
| if self.fields_to_include is None or k in self.fields_to_include: |
| modal_features[KeyWithCosLatFactor(prefix + k, 0)] = inputs[k] |
|
|
| for k, v in inputs['tracers'].items(): |
| if self.fields_to_include is None or k in self.fields_to_include: |
| modal_features[KeyWithCosLatFactor(prefix + k, 0)] = v |
| |
| modal_features = self.coords.with_dycore_sharding(modal_features) |
| diff_operator_features = self.compute_gradients_fn(modal_features) |
| sec_lat = 1 / self.coords.horizontal.cos_lat |
| sec2_lat = self.coords.horizontal.sec2_lat |
| sec_lat_scales = {0: 1, 1: sec_lat, 2: sec2_lat} |
| |
| features = {} |
| for k, v in (diff_operator_features | modal_features).items(): |
| sec_lat_scale = sec_lat_scales[k.factor_order] |
| features[k.name] = self.coords.horizontal.to_nodal(v) * sec_lat_scale |
| features = self.coords.with_dycore_sharding(features) |
| return features |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del memory, diagnostics, randomness, forcing |
| nodal_features = self._extract_features(inputs) |
| return self.features_transform_fn(nodal_features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class MemoryVelocityAndValues(VelocityAndPrognostics): |
| """Similar to `VelocityAndPrognostics`, but operates on memory.""" |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del inputs, diagnostics, randomness, forcing |
| nodal_features = self._extract_features(memory, 'memory_') |
| return self.features_transform_fn(nodal_features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class NodalInputVelocityAndPrognostics(VelocityAndPrognostics): |
| """Features modules that returns velocities, temperature, and optionally gradients.""" |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| to_modal_fn = self.coords.horizontal.to_modal |
| inputs = to_modal_fn(inputs) |
| memory = to_modal_fn(memory) |
| return super().__call__(inputs, memory, randomness, forcing) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class RadiationFeatures(hk.Module): |
| """Feature module that computes incident radiation flux.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
| ref_datetime_str = str(aux_features[xarray_utils.REFERENCE_DATETIME_KEY]) |
| self.solar_radiation = radiation.SolarRadiation.normalized( |
| coords=coords, |
| physics_specs=physics_specs, |
| reference_datetime=np.datetime64(ref_datetime_str), |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del memory, diagnostics, randomness, forcing |
| features = {} |
| features['radiation'] = self.solar_radiation.radiation_flux( |
| inputs['sim_time'] |
| ) |
| |
| |
| features = jax.tree_util.tree_map(lambda x: jnp.expand_dims(x, 0), features) |
| return self.features_transform_fn(features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class OrbitalTimeFeatures(hk.Module): |
| """Feature module that computes orbital time features.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
| ref_datetime_str = str(aux_features[xarray_utils.REFERENCE_DATETIME_KEY]) |
| self.solar_radiation = radiation.SolarRadiation.normalized( |
| coords=coords, |
| physics_specs=physics_specs, |
| reference_datetime=np.datetime64(ref_datetime_str), |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del memory, diagnostics, randomness, forcing |
| features = {} |
| |
| orbital_time = self.solar_radiation.time_to_orbital_time(inputs['sim_time']) |
| |
| |
| orbital_phase = orbital_time.orbital_phase - radiation.PERIHELION |
| |
| ones = jnp.ones(self.solar_radiation.coords.surface_nodal_shape) |
| features['cos_orbital_phase'] = jnp.cos(orbital_phase) * ones |
| features['sin_orbital_phase'] = jnp.sin(orbital_phase) * ones |
| |
| solar_hour_angle = self.solar_radiation.solar_hour_angle(inputs['sim_time']) |
| solar_hour_angle = jnp.expand_dims(solar_hour_angle, 0) |
| features['cos_solar_hour'] = jnp.cos(solar_hour_angle) |
| features['sin_solar_hour'] = jnp.sin(solar_hour_angle) |
| |
| return self.features_transform_fn(features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class ForcingFeatures(hk.Module): |
| """Feature module that provides forcing values as features.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| forcing_to_include: Sequence[str] = tuple(), |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.forcing_to_include = forcing_to_include |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def __call__( |
| self, |
| inputs: Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Forcing] = None, |
| ) -> Pytree: |
| del inputs, memory, diagnostics, randomness |
| features = {} |
| for key in self.forcing_to_include: |
| value = forcing[key] |
| |
| if value.ndim > 3: |
| raise ValueError( |
| f'Expected forcing "{key}" to have ndim <= 3, got {value.ndim}' |
| ) |
| if value.ndim == 2: |
| value = jnp.expand_dims(value, axis=0) |
| if value.shape[0] != 1: |
| raise ValueError( |
| f'Expected forcing "{key}" to have leading dimension 1' |
| f'for level, got {value.shape}' |
| ) |
| features[key] = value |
|
|
| return self.features_transform_fn(features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class LatitudeFeatures(hk.Module): |
| """Feature module that creates cos and sin of latitude as features.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
| self.coords = coords |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del inputs, memory, diagnostics, randomness, forcing |
| _, sin_lat = self.coords.horizontal.nodal_mesh |
| sin_features = sin_lat[np.newaxis, ...] |
| cos_features = jnp.cos(jnp.arcsin(sin_features)) |
| features = { |
| 'cos_latitude': cos_features, |
| 'sin_latitude': sin_features, |
| } |
| return self.features_transform_fn(features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class RandomnessFeatures(hk.Module): |
| """Feature module that returns fields from `randomness` as features.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del inputs, memory, diagnostics, forcing |
| if randomness is None: |
| random_features = {} |
| elif isinstance(randomness, dict): |
| random_features, _ = pytree_utils.flatten_dict(randomness) |
| elif isinstance(randomness, jax.Array): |
| random_features = {'randomness': randomness} |
| else: |
| raise ValueError(f'randomness has unsupported {type(randomness)=}.') |
| |
| if randomness is not None: |
| ndims = set(x.ndim for x in jax.tree_util.tree_leaves(random_features)) |
| if not ndims.issubset({2, 3}): |
| raise ValueError( |
| f'Random fields expected to be 2D and/or 3D. Found {ndims=}' |
| ) |
|
|
| def make_3d(x): |
| if x.ndim == 3: |
| return x |
| if x.ndim == 2: |
| return x[np.newaxis, ...] |
|
|
| random_features = jax.tree_util.tree_map(make_3d, random_features) |
| return self.features_transform_fn(random_features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class OrographyFeatures(hk.Module): |
| """Feature module that computes orographic features.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| if xarray_utils.OROGRAPHY not in aux_features: |
| raise ValueError('OrographyFeatures requires orography in aux_features.') |
| self.nodal_orography = aux_features[xarray_utils.OROGRAPHY] |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del inputs, memory, diagnostics, randomness, forcing |
| features = { |
| xarray_utils.OROGRAPHY: jnp.expand_dims(self.nodal_orography, 0), |
| } |
| return self.features_transform_fn(features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class OneHotAuxFeatures(hk.Module): |
| """Feature module that produces one-hot encodings from binary covariates.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| covariate_keys: Sequence[str] = gin.REQUIRED, |
| convert_float_to_int: bool = False, |
| name: Optional[str] = None, |
| ): |
| del coords, dt, physics_specs |
| super().__init__(name=name) |
| covariates = {} |
| num_classes = {} |
| for key in covariate_keys: |
| if key not in aux_features: |
| raise ValueError(f'Covariate {key} not found in aux_features.') |
| if not np.issubdtype(aux_features[key].dtype, np.integer): |
| if convert_float_to_int: |
| aux_features[key] = np.round(aux_features[key]).astype(int) |
| else: |
| raise ValueError( |
| f'Covariate {key} is expected to be integer dtype, ' |
| f'but is: {aux_features[key].dtype}' |
| ) |
| covariates[key] = aux_features[key] |
| num_classes[key] = np.unique(aux_features[key]).size |
| self.covariates = covariates |
| self.num_classes = num_classes |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> dict[str, jnp.ndarray]: |
| del inputs, memory, diagnostics, randomness, forcing |
| features = { |
| k: jax.nn.one_hot(v, self.num_classes[k], axis=0) |
| for k, v in self.covariates.items() |
| } |
| return features |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class LearnedPositionalFeatures(hk.Module): |
| """Feature module with learned params at surface nodal locations.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| latent_size: int, |
| scale: float = 1.0, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.scale = scale |
| self.padding = coords.horizontal.nodal_padding |
| unpadded_nodal_shape = tuple( |
| x - y for x, y in zip(coords.horizontal.nodal_shape, self.padding) |
| ) |
| self.positional_features = hk.get_parameter( |
| 'learned_positional_features', |
| (latent_size,) + unpadded_nodal_shape, |
| jnp.float32, |
| init=hk.initializers.Constant(0.0), |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> dict[str, jnp.ndarray]: |
| """Returns scaled parameter values at surface nodal locations.""" |
| del inputs, memory, diagnostics, randomness, forcing |
| pad_x, pad_y = self.padding |
| positional_features = self.scale * jnp.pad( |
| self.positional_features, [(0, 0), (0, pad_x), (0, pad_y)] |
| ) |
| return {'learned_positional_features': positional_features} |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class EmbeddingSurfaceFeatures(hk.Module): |
| """Feature module that specifies embedding surface outputs as features. |
| |
| Returns {feature_name: nn_output} |
| where nn_output.shape = (output_size, lon, lat). |
| """ |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| feature_name: str, |
| output_size: int, |
| embedding_module: typing.EmbeddingModule, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| |
| output_shapes = { |
| feature_name: np.asarray((output_size,) + coords.horizontal.nodal_shape) |
| } |
| self.embedding_fn = embedding_module( |
| coords, dt, physics_specs, aux_features, output_shapes=output_shapes |
| ) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| features = self.embedding_fn( |
| inputs, memory, diagnostics, randomness, forcing |
| ) |
| return self.features_transform_fn(features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class EmbeddingVolumeFeatures(hk.Module): |
| """Feature module that specifies embedding volume outputs as features. |
| |
| Returns {feature_name_0: nn_output_0, |
| feature_name_1: nn_output_1, |
| ... |
| } |
| where the NN output array has shape (output_size, level, lon, lat), which is |
| unpacked over output_size such that nn_output_{i}.shape = (level, lon, lat) |
| for each i in range(output_size). |
| """ |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| feature_name: str, |
| output_size: int, |
| embedding_module: typing.EmbeddingModule, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| |
| output_shapes = { |
| f'{feature_name}_{i}': np.asarray(coords.nodal_shape) |
| for i in range(output_size) |
| } |
| self.embedding_fn = embedding_module( |
| coords, dt, physics_specs, aux_features, output_shapes=output_shapes |
| ) |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| features = self.embedding_fn( |
| inputs, memory, diagnostics, randomness, forcing |
| ) |
| return self.features_transform_fn(features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class FloatDataFeatures(hk.Module): |
| """Feature module that supplies floating point covariates from data.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| covariate_data_path: str = gin.REQUIRED, |
| covariate_keys: Sequence[str] = gin.REQUIRED, |
| renaming_dict: Optional[Mapping[str, str]] = None, |
| compute_gradients_module: TransformModule = transforms.EmptyTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.covariates = {} |
| self.compute_gradients_fn = compute_gradients_module( |
| coords, dt, physics_specs, aux_features |
| ) |
| self.coords = coords |
| ds = xarray_utils.ds_from_path_or_aux(covariate_data_path, aux_features) |
| if renaming_dict is not None: |
| ds = ds.rename(renaming_dict) |
| lon, lat = (ds[xarray_utils.XR_LON_NAME], ds[xarray_utils.XR_LAT_NAME]) |
| xarray_utils.verify_grid_consistency(lon, lat, coords.horizontal) |
| lon_lat_order = (xarray_utils.XR_LON_NAME, xarray_utils.XR_LAT_NAME) |
| for key in covariate_keys: |
| data = ds[key].transpose(*lon_lat_order) |
| data_units = scales.parse_units(data.attrs['units']) |
| data = physics_specs.nondimensionalize(data.values * data_units) |
| if data.ndim != 3: |
| data = data[np.newaxis, ...] |
| self.covariates[key] = data |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> dict[str, jnp.ndarray]: |
| del inputs, memory, diagnostics, forcing, randomness |
| features = {k: v for k, v in self.covariates.items()} |
| modal_features = self.coords.horizontal.to_modal(features) |
| modal_features = { |
| KeyWithCosLatFactor(k, 0): v for k, v in modal_features.items() |
| } |
| modal_gradient_features = self.compute_gradients_fn(modal_features) |
| sec_lat = 1 / self.coords.horizontal.cos_lat |
| sec2_lat = self.coords.horizontal.sec2_lat |
| sec_lat_scales = {0: 1, 1: sec_lat, 2: sec2_lat} |
| for k, v in modal_gradient_features.items(): |
| sec_lat_scale = sec_lat_scales[k.factor_order] |
| features[k.name] = self.coords.horizontal.to_nodal(v) * sec_lat_scale |
| return features |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class CombinedFeatures(hk.Module): |
| """Feature module that combines multiple feature modules together.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| feature_modules: Sequence[FeaturesModule] = gin.REQUIRED, |
| feature_module_names_to_exclude: Sequence[str] = tuple(), |
| features_to_exclude: Sequence[str] = tuple(), |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.feature_fns = [ |
| module(coords, dt, physics_specs, aux_features) |
| for module in feature_modules |
| ] |
| self.feature_module_names_to_exclude = feature_module_names_to_exclude |
| self.features_to_exclude = features_to_exclude |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Forcing] = None, |
| ) -> dict[str, jnp.ndarray]: |
| all_features = {} |
| for feature_fn in self.feature_fns: |
| if type(feature_fn).__name__ not in self.feature_module_names_to_exclude: |
| features = feature_fn(inputs, memory, diagnostics, randomness, forcing) |
| for k, v in features.items(): |
| if k in all_features: |
| raise ValueError(f'Encountered duplicate feature {k}') |
| all_features[k] = v |
| all_features = self.features_transform_fn(all_features) |
| for k in self.features_to_exclude: |
| all_features.pop(k, None) |
| return all_features |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class NullFeatures(hk.Module): |
| """Placeholder features module that returns an empty dict.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| name: Optional[str] = None, |
| ): |
| del coords, dt, physics_specs, aux_features |
| super().__init__(name=name) |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> dict[str, jnp.ndarray]: |
| del inputs, memory, diagnostics, randomness, forcing |
| return {} |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class PressureFeatures(hk.Module): |
| """Feature module that computes pressure.""" |
|
|
| def __init__( |
| self, |
| coords: coordinate_systems.CoordinateSystem, |
| dt: float, |
| physics_specs: Any, |
| aux_features: typing.AuxFeatures, |
| features_transform_module: TransformModule = transforms.IdentityTransform, |
| name: Optional[str] = None, |
| ): |
| super().__init__(name=name) |
| self.coords = coords |
| self.features_transform_fn = features_transform_module( |
| coords, dt, physics_specs, aux_features |
| ) |
|
|
| def _nodal_pressure( |
| self, |
| inputs: typing.Pytree, |
| prefix: str = '', |
| ) -> Mapping[str, Array]: |
| """Computes nodal pressure from model inputs.""" |
| |
| to_nodal_fn = self.coords.horizontal.to_nodal |
| sigma = self.coords.vertical.centers |
| surface_pressure = jnp.exp(to_nodal_fn(inputs['log_surface_pressure'])) |
| pressure = surface_pressure * sigma[:, jnp.newaxis, jnp.newaxis] |
| nodal_features = {prefix + 'pressure': pressure} |
| return nodal_features |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del memory, diagnostics, randomness, forcing |
| nodal_features = self._nodal_pressure(inputs) |
| return self.features_transform_fn(nodal_features) |
|
|
|
|
| @gin.register(denylist=['coords', 'dt', 'physics_specs', 'aux_features']) |
| class MemoryPressureFeatures(PressureFeatures): |
| """Feature module that computes pressure from memory values.""" |
|
|
| def __call__( |
| self, |
| inputs: typing.Pytree, |
| memory: Optional[typing.PyTreeState] = None, |
| diagnostics: Optional[typing.Pytree] = None, |
| randomness: Optional[typing.PyTreeState] = None, |
| forcing: Optional[typing.Pytree] = None, |
| ) -> typing.Pytree: |
| del inputs, diagnostics, randomness, forcing |
| nodal_features = self._nodal_pressure(memory, 'memory_') |
| return self.features_transform_fn(nodal_features) |
|
|