NeuralGCM / model /legacy /decoders.py
yzt15806542928's picture
Upload folder using huggingface_hub
f4a39ee verified
Raw
History Blame Contribute Delete
26.9 kB
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Defines `decoder` modules that map model state to output data format."""
import functools
from typing import Any, Callable, Dict, Optional, Tuple, TypeVar
import zlib
from dinosaur import coordinate_systems
from dinosaur import primitive_equations
from dinosaur import pytree_utils
from dinosaur import scales
from dinosaur import spherical_harmonic
from dinosaur import typing
from dinosaur import vertical_interpolation
from dinosaur import weatherbench_utils
from dinosaur import xarray_utils
import gin
import haiku as hk
import jax
import jax.numpy as jnp
from model.legacy import diagnostics
from model.legacy import features
from model.legacy import filters
from model.legacy import mappings
from model.legacy import orographies
from model.legacy import perturbations
from model.legacy import stochastic
from model.legacy import transforms
import numpy as np
# long lines are better than splitting argument definitions onto two lines
# pylint: disable=line-too-long
# We ♥ λ's
# pylint: disable=g-long-lambda
DataState = typing.DataState
DiagnosticModule = diagnostics.DiagnosticModule
FeaturesModule = features.FeaturesModule
FilterModule = Callable[..., typing.PyTreeFilterFn]
Forcing = typing.Forcing
MappingModule = mappings.MappingModule
PyTreeState = typing.PyTreeState
ModelState = typing.ModelState
TransformModule = typing.TransformModule
OrographyModule = orographies.OrographyModule
PerturbationModule = perturbations.PerturbationModule
RandomnessModule = stochastic.RandomnessModule
@gin.register
class DecoderIdentityTransform(hk.Module):
"""Transformation that returns inputs without modification."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
name: Optional[str] = None,
):
super().__init__(name=name)
del coords, dt, physics_specs, aux_features, output_coords
def __call__(self, inputs: PyTreeState) -> PyTreeState:
return inputs
@gin.register
class DecoderFilterTransform(hk.Module):
"""Transformation that returns truncated and filtered modal inputs."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
filter_module: FilterModule = filters.DataNoFilter,
return_nodal: bool = True,
name: Optional[str] = None,
):
super().__init__(name=name)
self.output_coords = output_coords
self.filter_fn = filter_module(coords, dt, physics_specs, aux_features)
self.return_nodal = return_nodal
def __call__(self, inputs: PyTreeState) -> PyTreeState:
modal_inputs = coordinate_systems.maybe_to_modal(inputs, self.output_coords)
filtered_inputs = self.filter_fn(modal_inputs)
if self.return_nodal:
return self.output_coords.horizontal.to_nodal(filtered_inputs)
return filtered_inputs
@gin.register
class OutputModalToModalTransform(hk.Module):
"""Transformation that truncates modal state to output coords."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
name: Optional[str] = None,
):
super().__init__(name=name)
self.coords = coords
self.output_coords = output_coords
def __call__(self, inputs: PyTreeState) -> PyTreeState:
downsample_fn = coordinate_systems.get_spectral_downsample_fn(
self.coords, self.output_coords
)
return downsample_fn(inputs)
@gin.register
class OutputModalToNodalTransform(hk.Module):
"""Transformation that converts modal state to nodal representation."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
name: Optional[str] = None,
):
super().__init__(name=name)
self.coords = coords
self.output_coords = output_coords
def __call__(self, inputs: PyTreeState) -> PyTreeState:
to_nodal_fn = self.output_coords.horizontal.to_nodal
downsample_fn = coordinate_systems.get_spectral_downsample_fn(
self.coords, self.output_coords
)
return jax.tree_util.tree_map(
lambda x: to_nodal_fn(downsample_fn(x)), inputs
)
@gin.register
class OutputNodalToModalTransform(hk.Module):
"""Transformation that converts nodal state to modal representation."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
name: Optional[str] = None,
):
super().__init__(name=name)
self.output_coords = output_coords
def __call__(self, inputs: PyTreeState) -> PyTreeState:
return self.output_coords.horizontal.to_modal(inputs)
@gin.register
class ModalOutputLearnedAdaptorTransform(hk.Module):
"""Transformation using a tower to adapt modal outputs to the data domain."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
modal_to_nodal_features_module: FeaturesModule,
nodal_mapping_module: MappingModule,
output_transform_module: TransformModule,
name: Optional[str] = None,
):
del output_coords # unused.
super().__init__(name=name)
self.coords = coords
self.modal_to_nodal_features_fn = modal_to_nodal_features_module(
coords, dt, physics_specs, aux_features
)
self.nodal_mapping_module = nodal_mapping_module
self.output_transform_fn = output_transform_module(
coords, dt, physics_specs, aux_features
)
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
x, coords
)
def __call__(self, inputs: PyTreeState) -> PyTreeState:
"""Applies transform to modal inputs, returns modal outputs."""
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
prediction_shapes = jax.tree_util.tree_map(self.get_nodal_shape_fn, inputs)
# if `inputs` contain `sim_time` - remove it from corrections.
sim_time_shape = prediction_shapes.pop('sim_time', None)
net = self.nodal_mapping_module(prediction_shapes)
nodal_input_features = self.modal_to_nodal_features_fn(inputs, None)
nodal_corrections = self.output_transform_fn(net(nodal_input_features))
corrections = self.coords.horizontal.to_modal(nodal_corrections)
if sim_time_shape is not None:
corrections['sim_time'] = 0.0
outputs = jax.tree_util.tree_map(lambda x, y: x + y, inputs, corrections)
return from_dict_fn(outputs)
@gin.register
class NodalOutputLearnedAdaptorTransform(hk.Module):
"""Transformation using a tower to adapt nodal outputs to the data domain."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
nodal_to_nodal_features_module: FeaturesModule,
nodal_mapping_module: MappingModule,
output_transform_module: TransformModule,
name: Optional[str] = None,
):
del output_coords # unused.
super().__init__(name=name)
self.coords = coords
self.nodal_to_nodal_features_fn = nodal_to_nodal_features_module(
coords, dt, physics_specs, aux_features
)
self.nodal_mapping_module = nodal_mapping_module
self.output_transform_fn = output_transform_module(
coords, dt, physics_specs, aux_features
)
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
x, coords
)
def __call__(self, inputs: PyTreeState) -> PyTreeState:
"""Applies transform to nodal inputs, returns nodal outputs."""
inputs, from_dict_fn = pytree_utils.as_dict(inputs)
prediction_shapes = jax.tree_util.tree_map(self.get_nodal_shape_fn, inputs)
# if `inputs` contain `sim_time` - remove it from corrections.
sim_time_shape = prediction_shapes.pop('sim_time', None)
net = self.nodal_mapping_module(prediction_shapes)
input_features = self.nodal_to_nodal_features_fn(inputs, None)
corrections = self.output_transform_fn(net(input_features))
if sim_time_shape is not None:
corrections['sim_time'] = 0.0
outputs = jax.tree_util.tree_map(lambda x, y: x + y, inputs, corrections)
return from_dict_fn(outputs)
@gin.register
class DecoderCombinedTransform(hk.Module):
"""Module that applies multiple transformations sequentially."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: typing.AuxFeatures,
output_coords: coordinate_systems.CoordinateSystem,
transforms: Tuple[TransformModule, ...], # pylint: disable=redefined-outer-name
name: Optional[str] = None,
):
super().__init__(name=name)
self.transform_fns = [
module(coords, dt, physics_specs, aux_features, output_coords)
for module in transforms
]
def __call__(self, inputs: PyTreeState) -> PyTreeState:
for transform_fn in self.transform_fns:
inputs = transform_fn(inputs)
return inputs
@gin.register
class IdentityDecoder(hk.Module):
"""Decoder that returns model state unaltered."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
name: Optional[str] = None,
):
del coords, dt, physics_specs, aux_features, output_coords
super().__init__(name=name)
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
del forcing
return x.state
@gin.register
class StateToDictDecoder(hk.Module):
"""Decoder that returns a dict representation of a model state."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
transform_module: TransformModule = DecoderIdentityTransform,
name: Optional[str] = None,
):
super().__init__(name=name)
self.transform_fn = transform_module(
coords, dt, physics_specs, aux_features, output_coords
)
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
del forcing
state_dict, _ = pytree_utils.as_dict(x.state)
return self.transform_fn(state_dict)
@gin.register
class LeapfrogSliceDecoder(hk.Module):
"""Decoder that returns one slice out of a leapfrog pair."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
slice_id: int = 0,
transform_module: TransformModule = DecoderIdentityTransform,
name: Optional[str] = None,
):
super().__init__(name=name)
self.slice_id = slice_id
self.transform_fn = transform_module(
coords, dt, physics_specs, aux_features, output_coords
)
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
del forcing
return self.transform_fn(x.state[self.slice_id])
@gin.register
class LeapfrogSliceDictDecoder(hk.Module):
"""Decoder that returns one slice out of a leapfrog pair as dictionary."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
slice_id: int = 0,
transform_module: TransformModule = DecoderIdentityTransform,
name: Optional[str] = None,
):
super().__init__(name=name)
self.slice_id = slice_id
self.transform_fn = transform_module(
coords, dt, physics_specs, aux_features, output_coords
)
def __call__(self, x: ModelState, forcing: Forcing) -> DataState:
del forcing
state_dict, _ = pytree_utils.as_dict(x.state[self.slice_id])
return self.transform_fn(state_dict)
@gin.configurable
class PrimitiveToWeatherbenchDecoder(hk.Module):
"""Decoder that converts `StateWithTime` to `weatherbench.State`."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
time_axis: int = 0,
orography_module: OrographyModule = orographies.ClippedOrography,
transform_module: TransformModule = DecoderIdentityTransform,
name: Optional[str] = None,
):
super().__init__(name=name)
ref_temps = aux_features[xarray_utils.REF_TEMP_KEY]
self.ref_temps = ref_temps[..., np.newaxis, np.newaxis]
self.output_coords = output_coords
self.coords = coords
self.physics_specs = physics_specs
self.velocity_fn = functools.partial(
spherical_harmonic.vor_div_to_uv_nodal,
output_coords.horizontal,
)
modal_orography_init_fn = orography_module(
coords, dt, physics_specs, aux_features
)
orography = modal_orography_init_fn() # pytype: disable=not-callable # jax-ndarray
self.nodal_orography = coords.horizontal.to_nodal(orography)
self.geopotential_fn = functools.partial(
primitive_equations.get_geopotential_with_moisture,
nodal_orography=self.nodal_orography,
coordinates=coords.vertical,
gravity_acceleration=physics_specs.gravity_acceleration,
ideal_gas_constant=physics_specs.ideal_gas_constant,
water_vapor_gas_constant=physics_specs.water_vapor_gas_constant,
)
self.transform_fn = transform_module(
coords, dt, physics_specs, aux_features, output_coords
)
def primitive_to_weatherbench(
self,
inputs: primitive_equations.StateWithTime,
) -> weatherbench_utils.State:
"""Converts pe_state to weatherbench state on pressure levels."""
# output state is computed on output_coords.
to_nodal_fn = self.output_coords.horizontal.to_nodal
u, v = self.velocity_fn( # returned in nodal space.
vorticity=inputs.vorticity, divergence=inputs.divergence
)
t = self.ref_temps + to_nodal_fn(inputs.temperature_variation)
tracers = to_nodal_fn(inputs.tracers)
z = self.geopotential_fn(t, tracers['specific_humidity'])
surface_pressure = jnp.exp(to_nodal_fn(inputs.log_surface_pressure))
u, v, t, z, tracers, surface_pressure = (
self.coords.dycore_to_physics_sharding(
(u, v, t, z, tracers, surface_pressure)
)
)
interpolate_with_linear_extrap_fn = (
vertical_interpolation.vectorize_vertical_interpolation(
vertical_interpolation.linear_interp_with_linear_extrap
)
)
interpolate_with_constant_extrap_fn = (
vertical_interpolation.vectorize_vertical_interpolation(
vertical_interpolation.vertical_interpolation
)
)
regrid_with_linear_fn = functools.partial(
vertical_interpolation.interp_sigma_to_pressure,
pressure_coords=self.output_coords.vertical,
sigma_coords=self.coords.vertical,
surface_pressure=surface_pressure,
interpolate_fn=interpolate_with_linear_extrap_fn,
)
regrid_with_constant_fn = functools.partial(
vertical_interpolation.interp_sigma_to_pressure,
pressure_coords=self.output_coords.vertical,
sigma_coords=self.coords.vertical,
surface_pressure=surface_pressure,
interpolate_fn=interpolate_with_constant_extrap_fn,
)
# closes regridding options based on http://shortn/_X09ZAU1jsx.
# use constant extrapolation for `u, v, tracers`.
# use linear extrapolation for `z, t`.
return weatherbench_utils.State(
u=regrid_with_constant_fn(u), # pyrefly: ignore[unexpected-keyword]
v=regrid_with_constant_fn(v), # pyrefly: ignore[unexpected-keyword]
t=regrid_with_linear_fn(t), # pyrefly: ignore[unexpected-keyword]
z=regrid_with_linear_fn(z), # pyrefly: ignore[unexpected-keyword]
sim_time=inputs.sim_time, # pyrefly: ignore[unexpected-keyword]
tracers=regrid_with_constant_fn(tracers), # pyrefly: ignore[unexpected-keyword]
)
def __call__(
self, inputs: ModelState, forcing: Forcing
) -> DataState:
del forcing
wb_on_sigma = self.primitive_to_weatherbench(inputs.state)
return self.transform_fn(wb_on_sigma.asdict()) # pyrefly: ignore[missing-attribute]
_DECODER_SALT = zlib.crc32(b'decoder') # arbitrary uint32 value
def _decoder_prng_key(
randomness: typing.RandomnessState,
) -> typing.PRNGKeyArray | None:
"""Get a PRNG Key suitable for decoder randomness."""
if randomness.prng_key is None:
return None
salt = jnp.uint32(_DECODER_SALT) + jnp.uint32(randomness.prng_step)
return jax.random.fold_in(randomness.prng_key, salt)
@gin.register
class LearnedPrimitiveToWeatherbenchDecoder(PrimitiveToWeatherbenchDecoder):
"""Similar to `PrimitiveToWeatherbenchDecoder` with learned interpolation."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
modal_to_nodal_model_features_module: FeaturesModule,
modal_to_nodal_data_features_module: FeaturesModule,
correction_transform_module: TransformModule,
nodal_mapping_module: MappingModule,
prediction_mask: typing.Pytree,
time_axis: int = 0,
orography_module: OrographyModule = orographies.ClippedOrography,
transform_module: TransformModule = DecoderIdentityTransform,
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
name: Optional[str] = None,
):
super().__init__(
coords=coords,
dt=dt,
physics_specs=physics_specs,
aux_features=aux_features,
output_coords=output_coords,
time_axis=time_axis,
orography_module=orography_module,
name=name,
) # don't pass the transform, as we apply it at the end.
self.prediction_mask = prediction_mask
# features are computed on both coordinate systems.
self.model_features_fn = modal_to_nodal_model_features_module(
coords, dt, physics_specs, aux_features
)
self.data_features_fn = modal_to_nodal_data_features_module(
output_coords, dt, physics_specs, aux_features
)
self.corrections_transform_fn = correction_transform_module(
coords, dt, physics_specs, aux_features
)
# corrections are computed in real space on output coordinates.
self.nodal_mapping_module = nodal_mapping_module
self.get_nodal_shape_fn = lambda x: coordinate_systems.get_nodal_shapes(
x, output_coords
)
self.transform_fn = transform_module(
coords, dt, physics_specs, aux_features, output_coords
)
self.randomness_fn = randomness_module(
coords, dt, physics_specs, aux_features
)
self.perturbation_fn = perturbation_module(
coords, dt, physics_specs, aux_features
)
self.diagnostic_fn = diagnostics_module(
coords, dt, physics_specs, aux_features
)
def __call__(
self, inputs: ModelState, forcing: Forcing
) -> DataState:
randomness = self.randomness_fn.unconditional_sample(
_decoder_prng_key(inputs.randomness)
)
prognostics = self.perturbation_fn(
inputs=self.coords.with_dycore_sharding(inputs.state),
state=None,
randomness=self.coords.with_dycore_sharding(randomness.nodal_value),
)
inputs.state = prognostics # compute diagnostics from the perturbed state.
# TODO(dkochkov) Could we pass physics_tendencies here?
# TODO(janniyuval) Consider using evaporation diagnostics for training.
decoder_diagnostics = self.diagnostic_fn(inputs, None)
wb_on_pressure_dict = self.primitive_to_weatherbench(prognostics).asdict() # pyrefly: ignore[missing-attribute]
wb_on_pressure_modal = coordinate_systems.maybe_to_modal(
self.coords.physics_to_dycore_sharding(wb_on_pressure_dict),
self.output_coords,
)
wb_on_pressure_dict['diagnostics'] = decoder_diagnostics
prediction_mask = pytree_utils.replace_with_matching_or_default(
wb_on_pressure_dict, self.prediction_mask, default=False)
prediction_shapes = jax.tree_util.tree_map(
lambda x, y: self.get_nodal_shape_fn(x) if y else None,
wb_on_pressure_dict,
prediction_mask,
)
net = self.nodal_mapping_module(prediction_shapes)
model_features = self.model_features_fn(
prognostics.asdict(), forcing=forcing,
randomness=randomness.nodal_value
)
data_features = self.data_features_fn(wb_on_pressure_modal, forcing=forcing)
data_features = transforms.add_prefix(data_features, 'data_')
model_features = transforms.add_prefix(model_features, 'model_')
all_features = self.coords.dycore_to_physics_sharding(
data_features | model_features
)
nodal_outputs = self.corrections_transform_fn(net(all_features))
add_fn = lambda x, y: x + y if y is not None else x
wb_on_pressure_dict = jax.tree_util.tree_map(
add_fn, wb_on_pressure_dict, nodal_outputs
)
return self.transform_fn(wb_on_pressure_dict)
@gin.register
class DimensionalPrimitiveToWeatherbenchDecoder(PrimitiveToWeatherbenchDecoder):
"""Same as PrimitiveToWeatherbenchDecoder, but with dimensional output."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
inputs_to_units_mapping: Dict[str, str],
time_axis: int = 0,
orography_module: OrographyModule = orographies.ClippedOrography,
transform_module: TransformModule = DecoderIdentityTransform,
name: Optional[str] = None,
):
nondim_pressure_centers = physics_specs.nondimensionalize(
output_coords.vertical.centers * scales.units.millibar
)
nondim_output_coords = coordinate_systems.CoordinateSystem(
output_coords.horizontal,
vertical_interpolation.PressureCoordinates(nondim_pressure_centers),
spmd_mesh=output_coords.spmd_mesh,
)
super().__init__(
coords,
dt,
physics_specs,
aux_features,
output_coords=nondim_output_coords,
time_axis=time_axis,
orography_module=orography_module,
transform_module=transform_module,
name=name,
)
self.redimensionalize_fn = transforms.RedimensionalizeTransform(
coords,
dt,
physics_specs,
aux_features,
output_coords=output_coords,
inputs_to_units_mapping=inputs_to_units_mapping,
)
def __call__(
self, inputs: ModelState, forcing: Forcing
) -> DataState:
return self.redimensionalize_fn(super().__call__(inputs, forcing))
@gin.configurable
class DimensionalLearnedPrimitiveToWeatherbenchDecoder(
LearnedPrimitiveToWeatherbenchDecoder
):
"""Same as LearnedPrimitiveToWeatherbenchDecoder, but with dimensional output."""
def __init__(
self,
coords: coordinate_systems.CoordinateSystem,
dt: float,
physics_specs: Any,
aux_features: Dict[str, Any],
output_coords: coordinate_systems.CoordinateSystem,
modal_to_nodal_model_features_module: FeaturesModule,
modal_to_nodal_data_features_module: FeaturesModule,
nodal_mapping_module: MappingModule,
correction_transform_module: TransformModule,
prediction_mask: typing.Pytree,
inputs_to_units_mapping: Dict[str, str],
time_axis: int = 0,
orography_module: OrographyModule = orographies.ClippedOrography,
transform_module: TransformModule = DecoderIdentityTransform,
randomness_module: RandomnessModule = stochastic.ZerosRandomField,
perturbation_module: PerturbationModule = perturbations.NoPerturbation,
diagnostics_module: DiagnosticModule = diagnostics.NoDiagnostics,
name: Optional[str] = None,
):
nondim_pressure_centers = physics_specs.nondimensionalize(
output_coords.vertical.centers * scales.units.millibar
)
nondim_output_coords = coordinate_systems.CoordinateSystem(
output_coords.horizontal,
vertical_interpolation.PressureCoordinates(nondim_pressure_centers),
spmd_mesh=output_coords.spmd_mesh,
)
super().__init__(
coords,
dt,
physics_specs,
aux_features,
output_coords=nondim_output_coords,
modal_to_nodal_model_features_module=(
modal_to_nodal_model_features_module
),
modal_to_nodal_data_features_module=modal_to_nodal_data_features_module,
nodal_mapping_module=nodal_mapping_module,
correction_transform_module=correction_transform_module,
prediction_mask=prediction_mask,
time_axis=time_axis,
orography_module=orography_module,
transform_module=transform_module,
randomness_module=randomness_module,
perturbation_module=perturbation_module,
diagnostics_module=diagnostics_module,
name=name,
)
self.redimensionalize_fn = transforms.RedimensionalizeTransform(
coords,
dt,
physics_specs,
aux_features,
output_coords=output_coords,
inputs_to_units_mapping=inputs_to_units_mapping,
)
def __call__(
self, inputs: ModelState, forcing: Forcing
) -> DataState:
return self.redimensionalize_fn(super().__call__(inputs, forcing))