Source code for aimmd.network.rescalable
"""
aimmd.network.rescalable
=======================
Rescaling mixin for network modules.
This module defines :class:`Rescalable`, a mixin that augments a `torch.nn.Module`
by applying an optional *output rescaling* step after the normal forward call.
The rescaling is represented by two 1D buffers:
- ``rescale_knots``: knot locations in the original output coordinate,
- ``rescale_values``: corresponding target values in the rescaled coordinate.
The actual transformation is performed by :func:`aimmd.network.rescale_utils.rescale`
and is applied **in-place** to the network output under ``torch.no_grad()``.
Typical usage
-------------
Subclass `Rescalable` together with a standard `nn.Module` implementation:
- define `forward(...)` as usual,
- call :meth:`set_knots_and_values` to enable rescaling.
Rescaling is disabled by default: buffers are filled with NaNs and only entries
up to the first NaN are considered active.
"""
# external
import torch
from abc import ABC
from math import nan
from torch import nn
from collections.abc import Iterable
# internal imports
from .rescale_utils import rescale
# rescalable network module
[docs]
class Rescalable(ABC, nn.Module):
"""
Mixin that applies output rescaling at the end of `__call__`.
This class is intended to be used with multiple inheritance alongside an
`nn.Module` that implements `forward(...)`. By overriding :meth:`__call__`,
it ensures:
1) the usual PyTorch module call path runs first (hooks, autocast, etc.),
2) the resulting output `q` is optionally rescaled based on stored knots
and values.
Rescaling parameters are stored as **buffers** so they are:
- moved with the module across devices (`.to(...)`),
- included in module state (unless explicitly filtered),
- not treated as trainable parameters.
Notes
-----
- The rescaling is applied under ``torch.no_grad()`` and mutates the output
tensor in-place (as implemented by :func:`rescale`).
- Rescaling is considered active when `rescale_knots` contains at least one
non-NaN entry.
"""
[docs]
def __init__(self, max_knots=100):
"""
Parameters
----------
max_knots : int, optional
Maximum number of knots/values stored in the internal buffers.
Two buffers of shape ``(max_knots,)`` are registered:
``rescale_knots`` and ``rescale_values``. Entries are initialized to
NaN and only non-NaN entries are used.
"""
super().__init__()
# fixed-size buffers; NaN means "inactive slot"
self.register_buffer("rescale_knots", torch.full((max_knots,), nan))
self.register_buffer("rescale_values", torch.full((max_knots,), nan))
def __call__(self, *args, **kwargs):
"""Invoke the module and apply optional output rescaling.
Parameters
----------
*args, **kwargs
Forward arguments passed to the next class in the MRO (typically
`nn.Module.__call__`, which dispatches to `forward` and handles hooks).
Returns
-------
q : torch.Tensor
The module output, potentially rescaled. If rescaling is active,
the returned tensor is the same object as produced by the forward
call but modified in-place by :func:`rescale`.
"""
# let nn.Module handle forward hooks, autocast, etc.
q = super().__call__(*args, **kwargs)
# identify active knot/value entries (non-NaN)
keepers = ~torch.isnan(self.rescale_knots)
if keepers.any():
# slice active portion
knots = self.rescale_knots[keepers]
values = self.rescale_values[keepers]
# apply transformation without tracking gradients
with torch.no_grad():
q = rescale(q, knots, values)
return q
[docs]
def set_knots_and_values(self, knots, values):
"""Set the active rescaling definition.
Parameters
----------
knots : Iterable[float]
Knot positions in the original coordinate. Must be iterable.
values : Iterable[float]
Target values at each knot. Must be iterable and have the same
length as `knots`.
Raises
------
TypeError
If `knots` or `values` are not iterable, or if they do not have the
same length.
Notes
-----
- Values are copied into the buffers starting at index 0.
- Only the first ``len(knots)`` entries are overwritten; remaining buffer
entries are left unchanged. If you need to clear previous knots, call
:meth:`reset_parameters` first (or set fewer knots after a reset).
"""
# basic validation (iterability and matching lengths)
if not isinstance(knots, Iterable):
raise TypeError(f'knots must be iterable, got {knots!r}')
if not isinstance(values, Iterable):
raise TypeError(f'values must be iterable, got {values!r}')
if len(knots) != len(values):
raise TypeError(f'knots and values must have the same length, '
f'got {len(knots)} != {len(values)}')
# store as tensors (default dtype/device follow torch.tensor behavior)
self.rescale_knots[:len(knots)] = torch.tensor(knots)
self.rescale_values[:len(knots)] = torch.tensor(values)
[docs]
def reset_parameters(self):
"""Reset module parameters and clear rescaling buffers.
This cooperates with multiple inheritance by calling `reset_parameters`
on the next class in the MRO if it exists, then clears the rescaling
buffers by filling them with NaNs.
Notes
-----
- Clearing the buffers disables rescaling until new knots/values are set.
"""
# cooperate with multiple inheritance
if hasattr(super(), "reset_parameters"):
super().reset_parameters()
# clear rescaling buffers
with torch.no_grad():
self.rescale_knots.fill_(nan)
self.rescale_values.fill_(nan)