Download model/reference_code/linear_transforms.py from OneScience-Group/NeuralGCM: direct link, hf CLI and curl.
- Browser
- Download file 11.6 kB
-
https://huggingface.co/OneScience-Group/NeuralGCM/resolve/main/model/reference_code/linear_transforms.py
- Command line
-
hf download hf://OneScience-Group/NeuralGCM/model/reference_code/linear_transforms.py
-
curl -L -o linear_transforms.py https://huggingface.co/OneScience-Group/NeuralGCM/resolve/main/model/reference_code/linear_transforms.py
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 | |
| 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 | |
| ] | |
| 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 | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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)) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |
| 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) | |