NeuralGCM / model /reference_code /linear_transforms.py
yzt15806542928's picture
Upload folder using huggingface_hub
f4a39ee verified
Raw
History Blame Contribute Delete
11.6 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.
"""LinearTransforms for use in Metrics."""
import dataclasses
import functools
from typing import Callable, Mapping, Optional, Sequence
from dinosaur import coordinate_systems
from dinosaur import filtering
from dinosaur import horizontal_interpolation
from dinosaur import pytree_utils
from dinosaur import spherical_harmonic
from dinosaur import typing
import gin
import jax
import jax.numpy as jnp
import numpy as np
import model.reference_code.metrics_util as metrics_util
Pytree = typing.Pytree
TrajectoryRepresentations = typing.TrajectoryRepresentations
tree_leaves = jax.tree_util.tree_leaves
tree_map = jax.tree_util.tree_map
@dataclasses.dataclass
class LinearTransform:
"""A linear transformation, for TransformedL2Loss."""
trajectory_spec: metrics_util.TrajectorySpec
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
raise NotImplementedError
LinearTransformConstructor = Callable[
[metrics_util.TrajectorySpec], LinearTransform
]
@dataclasses.dataclass
class ComposedTransformForLoss(LinearTransform):
"""Composition of multiple linear transformations for computation of loss.
Attributes:
components: components[i](self.trajectory_spec) initializes the i + 1 member
of self.transforms.
transforms: errors are transformed as error --> transforms[0](error) -->
transforms[1](error) --> ⋯. The 0th transform is inserted by this class as
TruncateToTrajectoryLength.
"""
components: Sequence[LinearTransformConstructor]
transforms: Sequence[LinearTransform] = dataclasses.field(init=False)
def __post_init__(self):
# Insert TruncateToTrajectoryLength first in all cases. It's okay if it was
# already inserted... it is idempotent. This ensures that
# len(self.transforms) = len(self.components) + 1
# in all cases.
components = [TruncateToTrajectoryLength] + list(self.components)
self.transforms = [
constructor(self.trajectory_spec) for constructor in components
]
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
for transform in self.transforms:
errors = transform(errors, targets)
return errors
@gin.register
@dataclasses.dataclass
class LegacyTimeRescaling(LinearTransform):
"""Time scaling from WeightedL2CumulativeLoss."""
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
n = self.trajectory_spec.trajectory_length
steps_per_save = self.trajectory_spec.steps_per_save
scale = 1 if n == 1 else 1 / np.sqrt((n - 1) * steps_per_save)
return tree_map(lambda x: x * scale, errors)
@gin.register
@dataclasses.dataclass
class TimeRescaling(LinearTransform):
"""Time scaling that assumes error grows like a random walk.
This rescales errors like
errors --> errors / σ(T),
σ(T) := sqrt( sum(variance) / variance(T) )
where variance(T) is the assumed variance. A random walk has variance ∝ T.
This function uses similar scaling.
See also:
* Climatology vs. ENS CRPS values indicate skill difficult after 240 hrs
http://screen/8sVodqThEk6o693
* Plotting this function for various parameter values
http://screen/AubXNomsgm7g92o and http://gpaste/6727081386835968
Attributes:
base_squared_error_in_hours: Number of hours before assumed variance starts
growing (almost) linearly.
asymptotic_squared_error_in_hours: Number of hours before assumed variance
slows its growth. Set to None (the default) if variance grows indefinitely
"""
base_squared_error_in_hours: float
asymptotic_squared_error_in_hours: Optional[float] = None
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
time_sizes = np.unique([x.shape[0] for x in tree_leaves(errors)])
if time_sizes.size != 1:
raise ValueError(f'Expected unique time dimension size. {time_sizes=}')
time_size = time_sizes[0]
if self.trajectory_spec.trajectory_length != time_size:
logging.info(
f'errors has {time_size=} !='
f' {self.trajectory_spec.trajectory_length=}. This is probably due to'
' the Loss slicing via the time_step kwarg. Will use {time_size=}'
' to compute scaling.'
)
steps_per_save = self.trajectory_spec.steps_per_save
t = np.arange(time_size) * steps_per_save
if self.asymptotic_squared_error_in_hours is not None:
# Rescale "time" `t`, so it stops growing when
# t >> asymptotic_squared_error_in_hours.
t = t / (1 + t / self.asymptotic_squared_error_in_hours)
inv_variance = 1 / (1 + t / self.base_squared_error_in_hours)
scale = np.sqrt(inv_variance / inv_variance.sum())
scale = scale.reshape(-1, 1, 1, 1)
return tree_map(lambda x: x * scale, errors)
@gin.register
@dataclasses.dataclass
class CustomTimeRescaling(LinearTransform):
"""Custom time scaling that uses pre-specified values."""
scaling_weights: Sequence[float]
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
n = self.trajectory_spec.trajectory_length
scale = np.asarray(self.scaling_weights)[:n].reshape(-1, 1, 1, 1)
return tree_map(lambda x: x * scale, errors)
@gin.register
@dataclasses.dataclass
class DelayedTimeRescaling(LinearTransform):
"""Time scaling with smooth delay that transitions into hyperbolic decay."""
base_squared_error_in_hours: float
delay_power: float = 1.0
decay_power: float = 1.0
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
n = self.trajectory_spec.trajectory_length
steps_per_save = self.trajectory_spec.steps_per_save
t = np.arange(n) * steps_per_save
a = 1 / self.base_squared_error_in_hours
inv_variance = 1 / (
(1 + (a * t) ** self.delay_power) ** (1/self.decay_power))
scale = np.sqrt(inv_variance / inv_variance.sum())
scale = scale.reshape(-1, 1, 1, 1)
return tree_map(lambda x: x * scale, errors)
@gin.register
@dataclasses.dataclass
class TruncateToTrajectoryLength(LinearTransform):
"""Truncate errors to self.trajectory_spec.trajectory_length.
To ensure loss is computed over the correct trajectory length, this transform
should be used as the first step in any ComposedTransformForLoss.
"""
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
n = self.trajectory_spec.trajectory_length
return metrics_util.extract_time_slice(errors, slice(0, n))
@gin.register
@dataclasses.dataclass
class TotalWavenumberMasking(LinearTransform):
"""Transform that masks out wavenumbers greater than `max_wavenumber`."""
max_wavenumber: int
is_encoded: bool = False
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
if self.is_encoded:
grid = self.trajectory_spec.coords.horizontal
else:
grid = self.trajectory_spec.data_coords.horizontal
modal_shape = grid.modal_shape
mask = np.arange(modal_shape[-1]) < self.max_wavenumber
mask = mask.astype(float)
return tree_map(lambda x: x * mask, errors)
@gin.register
@dataclasses.dataclass
class ConservativeRegridder(LinearTransform):
"""Linear transform that regrids."""
def __init__(
self,
trajectory_spec: metrics_util.TrajectorySpec,
target_grid: spherical_harmonic.Grid,
):
super().__init__(trajectory_spec=trajectory_spec)
self.regridder = horizontal_interpolation.ConservativeRegridder(
source_grid=trajectory_spec.coords.horizontal, target_grid=target_grid
)
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # Unused
return tree_map(self.regridder, errors)
@gin.register
@dataclasses.dataclass
class PerVariableRescaling(LinearTransform):
"""Transform that reweights contribution per variable."""
weights: Pytree
scale: float = 1.0
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
weights = self.weights
if weights is None:
weights = tree_map(lambda x: 1.0, errors)
else:
weights = pytree_utils.replace_with_matching_or_default(
errors, weights, default=None,
check_used_all_replace_keys=True,
)
root_weights = tree_map(lambda w: np.sqrt(w * self.scale), weights)
return tree_map(jnp.multiply, errors, root_weights)
@gin.register
class ExponentialFilteringByLeadtime(LinearTransform):
"""Applied leadtime dependent exponential filters to errors."""
def __init__(
self,
trajectory_spec: metrics_util.TrajectorySpec,
filter_attenuations: typing.Pytree,
filter_orders: typing.Pytree,
is_encoded: bool = False,
):
super().__init__(trajectory_spec=trajectory_spec)
n = trajectory_spec.trajectory_length
if is_encoded:
grid = trajectory_spec.coords.horizontal
else:
grid = trajectory_spec.data_coords.horizontal
# expand dims for `level, lon, total wavenumbers` so that filter parameters
# are applied to different time values.
to_array_fn = lambda x: np.expand_dims(np.array(x)[:n], axis=(1, 2, 3))
is_leaf = lambda x: isinstance(x, Sequence)
attenuations = tree_map(to_array_fn, filter_attenuations, is_leaf=is_leaf)
orders = tree_map(to_array_fn, filter_orders, is_leaf=is_leaf)
self.filter_fns = tree_map(
lambda a, p: filtering.exponential_filter(grid, a, p),
attenuations,
orders,
)
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
filter_fns = pytree_utils.replace_with_matching_or_default(
errors, self.filter_fns, default=None, check_used_all_replace_keys=True)
return tree_map(lambda fn, err: fn(err), filter_fns, errors)
@gin.register
class LevelRescaling(LinearTransform):
"""Linear transform that scales values with vertical levels."""
def __init__(
self,
trajectory_spec: metrics_util.TrajectorySpec,
scale: Sequence[float],
keys_to_scale: Sequence[str] = tuple(),
):
super().__init__(trajectory_spec)
self.scale_fn = functools.partial(
coordinate_systems.scale_levels_for_matching_keys,
scales=np.asarray(scale),
keys_to_scale=keys_to_scale,
)
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
return self.scale_fn(errors)
@gin.register
class LevelRemoval(LinearTransform):
"""Linear transform that removes vertical levels."""
def __init__(
self,
trajectory_spec: metrics_util.TrajectorySpec,
keep_levels: Sequence[float],
):
super().__init__(trajectory_spec)
n_levels = trajectory_spec.data_coords.vertical.layers
indices = jnp.array([i for i in range(n_levels) if keep_levels[i]])
self.take_arr = lambda x: jnp.take(x, indices, axis=metrics_util.LEVEL_AXIS)
def __call__(self, errors: Pytree, targets: Pytree) -> Pytree:
del targets # unused.
return tree_map(self.take_arr, errors)