Skip to content

chuchichaestli.data.transforms

N-dimensional data transforms built on torchvision transforms v2.

Modules:

Name Description
basis

Separable projection onto truncated tensor-product bases.

channel

Channel dimension expansion/collapse transforms.

complex

Complex/real view transforms.

compose

Composition of v2 transforms with a single shared per-batch param draw.

crop

N-dimensional cropping transforms.

geometric

N-dimensional geometric augmentations (exact, interpolation-free).

intensity

Value transforms acting on pixel/voxel intensities (stretches and scalers).

resize

N-dimensional resize and pad transforms.

wavelet

Discrete wavelet transforms over the trailing spatial dimensions.

Classes:

Name Description
Affine

Affine value transform $(x - b) / a$ (location-scale form).

BasisProjection

Project selected axes onto truncated bases, and reconstruct back.

CenterCropND

Deterministic centered crop over the trailing N spatial dims.

ChannelCollapse

Inverse of ChannelExpand.

ChannelExpand

Insert (and optionally replicate) a channel dimension.

Clamp

Clamp values to [min, max] (lossy, no inverse).

ComplexCollapse

Inverse of ComplexExpand: collapse to complex, expand on revert.

ComplexExpand

Expand a complex tensor into a real one with an axis of 2.

HEPTransform

High-energy-physics stretch for a more Gaussian-like value distribution.

HounsfieldClamp

Clamp Hounsfield units to a window (lossy, no inverse).

HounsfieldScale

Rescale raw CT stored values to Hounsfield units: slope * x + intercept.

InvBasisProjection

Inverse of BasisProjection: reconstruct forward, project on revert.

InvHEPTransform

Inverse of HEPTransform.

InvHounsfieldScale

Inverse of HounsfieldScale (Hounsfield units -> stored values).

InvLogP1Transform

Inverse of LogP1Transform.

InvLogTransform

Inverse of LogTransform.

InvMinMaxScale

Inverse of MinMaxScale (requires fixed vmin/vmax).

InvWaveletDecompose

Inverse of WaveletDecompose: reconstruct forward, decompose on revert.

InvWaveletPacket

Inverse of WaveletPacket: reconstruct forward, decompose on revert.

LogP1Transform

Log1p-style stretch $\hat{x} = \log(x + 1)$ (handles zeros).

LogTransform

Logarithmic stretch $\hat{x} = \log(x)$ (invertible by exponentiation).

MinMaxScale

Affine rescale from [vmin, vmax] into feature_range.

PadND

Pad the trailing N spatial dims via torch.nn.functional.pad.

RandomCropND

Random crop over the trailing N spatial dims (leading dims pass through).

RandomFlipND

Randomly flip each eligible trailing spatial axis with probability p.

RandomRot90ND

Random k*90 degree rotation in a random trailing spatial axis-plane.

ResizeND

Resize the trailing N spatial dims via torch.nn.functional.interpolate.

SequentialTransform

Chain v2 transforms, sampling each child's params once per batch.

WaveletDecompose

Replace the trailing ndim axes by the subbands of a wavelet transform.

WaveletPacket

WaveletDecompose that keeps splitting every subband, not only the coarsest.

ZScale

Adaptive IRAF zscale normalization: clip to zscale limits, then rescale.

ZScaleInterval

Compute IRAF zscale display limits (z1, z2) from a tensor.

Affine

Affine(a: float = 1.0, b: float = 0.0)

Bases: Transform

Affine value transform $(x - b) / a$ (location-scale form).

Constructor.

Parameters:

Name Type Description Default
a float

The scale, i.e. the divisor (e.g. a data range).

1.0
b float

The loc, i.e. the offset subtracted (e.g. a data minimum).

0.0

Methods:

Name Description
get_inverse

Return the inverse affine map.

revert

Invert the affine map.

transform

Apply the affine map.

Source code in src/chuchichaestli/data/transforms/intensity.py
41
42
43
44
45
46
47
48
49
50
def __init__(self, a: float = 1.0, b: float = 0.0):
    """Constructor.

    Args:
        a: The scale, i.e. the divisor (e.g. a data range).
        b: The loc, i.e. the offset subtracted (e.g. a data minimum).
    """
    super().__init__()
    self.a = _as_tensor(a)
    self.b = _as_tensor(b)

get_inverse

get_inverse() -> Transform

Return the inverse affine map.

Source code in src/chuchichaestli/data/transforms/intensity.py
60
61
62
def get_inverse(self) -> Transform:
    """Return the inverse affine map."""
    return Affine(a=1.0 / self.a, b=-self.b / self.a)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Invert the affine map.

Source code in src/chuchichaestli/data/transforms/intensity.py
56
57
58
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Invert the affine map."""
    return x * self.a + self.b

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the affine map.

Source code in src/chuchichaestli/data/transforms/intensity.py
52
53
54
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the affine map."""
    return (x - self.b) / self.a

BasisProjection

BasisProjection(
    bases: dict[int, tuple[str, int] | int | torch.Tensor],
    basis: str = "chebyshev",
    weights: dict[int, torch.Tensor] | None = None,
    rcond: float | None = None,
    lengths: dict[int, int] | None = None,
    cond_warn: float | None = 1000000.0,
)

Bases: Transform

Project selected axes onto truncated bases, and reconstruct back.

The forward pass replaces each selected axis of length N by M coefficients of a truncated expansion, revert reconstructs the original length. With a design matrix B_k per axis this is a decomposition with fixed (non-learned) factor matrices:

C = X x_1 B_1^+ x_2 B_2^+ ...    (project)
X = C x_1 B_1   x_2 B_2   ...    (reconstruct)

Complex input is supported: the real design matrices are promoted to the input dtype, so real and imaginary parts project independently.

Constructor.

Parameters:

Name Type Description Default
bases dict[int, tuple[str, int] | int | torch.Tensor]

Maps an axis of the input to its basis. Each value is either an (name, order) pair, a bare order using the default basis family, or an explicit (N, M) design matrix (e.g. fitted PCA components). Axes may be negative.

required
basis str

Default family for entries given as a bare order. One of BASIS_REGISTRY: chebyshev, legendre, monomial, fourier, dct.

'chebyshev'
weights dict[int, torch.Tensor] | None

Optional per-axis 1-D non-negative weights of length N for a weighted least-squares fit, e.g. for measurement noise or masked samples. Axes as in bases, sign included.

None
rcond float | None

Relative cutoff for small singular values in the pseudoinverse, forwarded to torch.linalg.pinv as rtol.

None
lengths dict[int, int] | None

Original length N per axis, which coefficients do not carry. Projecting records it, but pass it explicitly to revert standalone or across processes. Axes as in bases, sign included.

None
cond_warn float | None

Warn when the fitted matrix's condition number exceeds this. None disables the check.

1000000.0

Methods:

Name Description
design_matrix

Return the (n, M) design matrix for axis, in float64.

revert

Reconstruct from the coefficients (ignores params).

transform

Project onto the truncated bases (ignores params).

Source code in src/chuchichaestli/data/transforms/basis.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def __init__(
    self,
    bases: dict[int, tuple[str, int] | int | torch.Tensor],
    basis: str = "chebyshev",
    weights: dict[int, torch.Tensor] | None = None,
    rcond: float | None = None,
    lengths: dict[int, int] | None = None,
    cond_warn: float | None = 1e6,
):
    """Constructor.

    Args:
        bases: Maps an axis of the input to its basis. Each value is either
            an `(name, order)` pair, a bare `order` using the default
            `basis` family, or an explicit `(N, M)` design matrix (e.g.
            fitted PCA components). Axes may be negative.
        basis: Default family for entries given as a bare order. One of
            `BASIS_REGISTRY`: chebyshev, legendre, monomial, fourier, dct.
        weights: Optional per-axis 1-D non-negative weights of length
            `N` for a weighted least-squares fit, e.g. for measurement
            noise or masked samples. Axes as in `bases`, sign included.
        rcond: Relative cutoff for small singular values in the
            pseudoinverse, forwarded to `torch.linalg.pinv` as `rtol`.
        lengths: Original length `N` per axis, which coefficients do not
            carry. Projecting records it, but pass it explicitly to revert
            standalone or across processes. Axes as in `bases`, sign
            included.
        cond_warn: Warn when the fitted matrix's condition number exceeds
            this. `None` disables the check.
    """
    super().__init__()
    if not bases:
        raise ValueError("bases must name at least one axis")
    normalised: dict[int, tuple[str, int] | torch.Tensor] = {}
    for axis, spec in bases.items():
        if isinstance(spec, torch.Tensor):
            if spec.ndim != 2:
                raise ValueError(
                    f"design matrix for axis {axis} must be 2-D (N, M), "
                    f"got shape {tuple(spec.shape)}"
                )
            normalised[axis] = spec
            continue
        name, order = (basis, spec) if isinstance(spec, int) else spec
        if name not in BASIS_REGISTRY:
            raise ValueError(
                f"unknown basis {name!r}; choose from {sorted(BASIS_REGISTRY)}"
            )
        if order < 1:
            raise ValueError(f"order for axis {axis} must be >= 1, got {order}")
        normalised[axis] = (name, order)
    for name, mapping in (("weights", weights), ("lengths", lengths)):
        stray = sorted(set(mapping or ()) - set(normalised))
        if stray:
            raise ValueError(
                f"{name} names axes {stray} that are absent from bases "
                f"{sorted(normalised)}; write them with the same sign"
            )
    self.bases = normalised
    self.basis = basis
    self.weights = {a: w.detach().clone() for a, w in (weights or {}).items()}
    self.rcond = rcond
    self.cond_warn = cond_warn
    self.lengths = dict(lengths) if lengths else {}  # original axis lengths
    self._declared = set(self.lengths)
    for axis, spec in normalised.items():
        if isinstance(spec, torch.Tensor):
            self.lengths.setdefault(axis, spec.shape[0])
            self._declared.add(axis)
    self._cache: dict[Any, tuple[torch.Tensor, torch.Tensor]] = {}

design_matrix

design_matrix(axis: int, n: int) -> torch.Tensor

Return the (n, M) design matrix for axis, in float64.

Built-in families are built on the CPU; an explicit design matrix is returned on whichever device the caller supplied it on.

Source code in src/chuchichaestli/data/transforms/basis.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def design_matrix(self, axis: int, n: int) -> torch.Tensor:
    """Return the `(n, M)` design matrix for `axis`, in float64.

    Built-in families are built on the CPU; an explicit design matrix is
    returned on whichever device the caller supplied it on.
    """
    spec = self.bases[axis]
    if isinstance(spec, torch.Tensor):
        if spec.shape[0] != n:
            raise ValueError(
                f"design matrix for axis {axis} has {spec.shape[0]} rows but "
                f"the input has length {n} along that axis"
            )
        return spec.to(torch.float64)
    name, order = spec
    if order > n:
        raise ValueError(
            f"order {order} exceeds the length {n} of axis {axis}; the fit "
            "would be underdetermined"
        )
    return BASIS_REGISTRY[name](n, order)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Reconstruct from the coefficients (ignores params).

Source code in src/chuchichaestli/data/transforms/basis.py
330
331
332
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Reconstruct from the coefficients (ignores `params`)."""
    return self._reconstruct(x)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Project onto the truncated bases (ignores params).

Source code in src/chuchichaestli/data/transforms/basis.py
326
327
328
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Project onto the truncated bases (ignores `params`)."""
    return self._project(x)

CenterCropND

CenterCropND(
    size: int | Sequence[int], ndim: int | None = None
)

Bases: Transform

Deterministic centered crop over the trailing N spatial dims.

Symmetrically zero-pads any axis whose target size exceeds the input.

Constructor.

Parameters:

Name Type Description Default
size int | Sequence[int]

Per-axis crop size (e.g. (128, 128, 128)), or an int with ndim.

required
ndim int | None

Number of trailing spatial dims when size is an int.

None

Methods:

Name Description
transform

Crop (and pad if needed) the trailing spatial dims about the center.

Source code in src/chuchichaestli/data/transforms/crop.py
75
76
77
78
79
80
81
82
83
84
85
def __init__(self, size: int | Sequence[int], ndim: int | None = None):
    """Constructor.

    Args:
        size: Per-axis crop size (e.g. `(128, 128, 128)`), or an int with
            `ndim`.
        ndim: Number of trailing spatial dims when `size` is an int.
    """
    super().__init__()
    self.size = _as_size(size, ndim)
    self.ndim = len(self.size)

transform

transform(x: Any, params: dict[str, Any]) -> Any

Crop (and pad if needed) the trailing spatial dims about the center.

Source code in src/chuchichaestli/data/transforms/crop.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
def transform(self, x: Any, params: dict[str, Any]) -> Any:
    """Crop (and pad if needed) the trailing spatial dims about the center."""
    spatial = x.shape[-self.ndim :]
    slices, pads = [], []
    for dim, crop in zip(spatial, self.size):
        if crop <= dim:
            start = (dim - crop) // 2
            slices.append(slice(start, start + crop))
            pads.append((0, 0))
        else:
            slices.append(slice(0, dim))
            total = crop - dim
            pads.append((total // 2, total - total // 2))
    out = x[(..., *tuple(slices))]
    if any(lo or hi for lo, hi in pads):
        pad_arg = [p for lohi in reversed(pads) for p in lohi]
        out = F.pad(out, pad_arg)
    return out

ChannelCollapse

ChannelCollapse(
    channel_first: bool = True, replicate: int = 0
)

Bases: ChannelExpand

Inverse of ChannelExpand.

Methods:

Name Description
revert

Expand the channel dimension.

transform

Collapse the channel dimension.

Source code in src/chuchichaestli/data/transforms/channel.py
22
23
24
25
26
27
28
29
30
31
def __init__(self, channel_first: bool = True, replicate: int = 0):
    """Constructor.

    Args:
        channel_first: If `True`, insert the channel dim first, else last.
        replicate: If `>= 2`, replicate the new channel this many times.
    """
    super().__init__()
    self.channel_first = channel_first
    self.replicate = replicate

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Expand the channel dimension.

Source code in src/chuchichaestli/data/transforms/channel.py
63
64
65
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Expand the channel dimension."""
    return super().transform(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Collapse the channel dimension.

Source code in src/chuchichaestli/data/transforms/channel.py
59
60
61
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Collapse the channel dimension."""
    return super().revert(x, params)

ChannelExpand

ChannelExpand(
    channel_first: bool = True, replicate: int = 0
)

Bases: Transform

Insert (and optionally replicate) a channel dimension.

Maps (W, H)/(B, W, H) tensors to a channel-first (B, C, W, H) (or channel-last) layout.

Constructor.

Parameters:

Name Type Description Default
channel_first bool

If True, insert the channel dim first, else last.

True
replicate int

If >= 2, replicate the new channel this many times.

0

Methods:

Name Description
revert

Collapse the channel dimension.

transform

Expand the channel dimension.

Source code in src/chuchichaestli/data/transforms/channel.py
22
23
24
25
26
27
28
29
30
31
def __init__(self, channel_first: bool = True, replicate: int = 0):
    """Constructor.

    Args:
        channel_first: If `True`, insert the channel dim first, else last.
        replicate: If `>= 2`, replicate the new channel this many times.
    """
    super().__init__()
    self.channel_first = channel_first
    self.replicate = replicate

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Collapse the channel dimension.

Source code in src/chuchichaestli/data/transforms/channel.py
44
45
46
47
48
49
50
51
52
53
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Collapse the channel dimension."""
    if self.replicate > 1:
        x = x[:, 0, :, :] if self.channel_first else x[:, :, :, 0]
    if x.ndim == 4:
        if self.channel_first and x.shape[1] == 1:
            x = x.squeeze(1)
        elif not self.channel_first and x.shape[-1] == 1:
            x = x.squeeze(-1)
    return x

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Expand the channel dimension.

Source code in src/chuchichaestli/data/transforms/channel.py
33
34
35
36
37
38
39
40
41
42
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Expand the channel dimension."""
    if x.ndim == 2:
        x = x.unsqueeze(0).unsqueeze(self.channel_first - 1)
    elif x.ndim == 3:
        x = x.unsqueeze(1) if self.channel_first else x.unsqueeze(-1)
    if self.replicate > 1:
        dim = 1 if self.channel_first else 3
        x = torch.cat((x,) * self.replicate, dim=dim)
    return x

Clamp

Clamp(min: float | None = None, max: float | None = None)

Bases: Transform

Clamp values to [min, max] (lossy, no inverse).

Constructor.

Parameters:

Name Type Description Default
min float | None

Lower bound (None leaves the lower side unbounded).

None
max float | None

Upper bound (None leaves the upper side unbounded).

None

Methods:

Name Description
transform

Clamp the input.

Source code in src/chuchichaestli/data/transforms/intensity.py
269
270
271
272
273
274
275
276
277
278
def __init__(self, min: float | None = None, max: float | None = None):
    """Constructor.

    Args:
        min: Lower bound (`None` leaves the lower side unbounded).
        max: Upper bound (`None` leaves the upper side unbounded).
    """
    super().__init__()
    self.min = min
    self.max = max

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Clamp the input.

Source code in src/chuchichaestli/data/transforms/intensity.py
280
281
282
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Clamp the input."""
    return torch.clamp(x, min=self.min, max=self.max)

ComplexCollapse

ComplexCollapse(dim: int = -1, strict: bool = True)

Bases: ComplexExpand

Inverse of ComplexExpand: collapse to complex, expand on revert.

Methods:

Name Description
revert

Expand complex to real.

transform

Collapse real to complex.

Source code in src/chuchichaestli/data/transforms/complex.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, dim: int = -1, strict: bool = True):
    """Constructor.

    Args:
        dim: Position of the real/imaginary axis in the expanded tensor.
            The default `-1` is the natural layout of `torch.view_as_real`;
            any other position adds a `movedim` (still a view). Use `0` or
            `1` to feed real and imaginary parts to a network as channels.
        strict: If `True`, a non-complex input to `transform` (or an input
            to `revert` whose `dim` axis is not of size 2) raises. Set
            `False` to pass such tensors through unchanged, which is what
            you want when the transform is applied across a nested sample
            holding both complex and real entries -- at the cost of the
            round trip no longer being checked.
    """
    super().__init__()
    self.dim = dim
    self.strict = strict

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Expand complex to real.

Source code in src/chuchichaestli/data/transforms/complex.py
105
106
107
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Expand complex to real."""
    return self._expand(x)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Collapse real to complex.

Source code in src/chuchichaestli/data/transforms/complex.py
101
102
103
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Collapse real to complex."""
    return self._collapse(x)

ComplexExpand

ComplexExpand(dim: int = -1, strict: bool = True)

Bases: Transform

Expand a complex tensor into a real one with an axis of 2.

The real/imaginary axis is appended by torch.view_as_real and then moved to dim, so it is trailing only for the default dim=-1.

Use this if complex data should work with SharedArray. Both directions are zero-copy views wherever the strides allow it; only revert on a non-contiguous input falls back to a copy.

Note that the real dtype follows the complex one
  • complex128 expands to float64
  • complex64 expands to float32
  • complex32 expands to float16 (incompatible with SharedArray)

Constructor.

Parameters:

Name Type Description Default
dim int

Position of the real/imaginary axis in the expanded tensor. The default -1 is the natural layout of torch.view_as_real; any other position adds a movedim (still a view). Use 0 or 1 to feed real and imaginary parts to a network as channels.

-1
strict bool

If True, a non-complex input to transform (or an input to revert whose dim axis is not of size 2) raises. Set False to pass such tensors through unchanged, which is what you want when the transform is applied across a nested sample holding both complex and real entries -- at the cost of the round trip no longer being checked.

True

Methods:

Name Description
revert

Collapse real back to complex (ignores params).

transform

Expand complex to real (ignores params).

Source code in src/chuchichaestli/data/transforms/complex.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, dim: int = -1, strict: bool = True):
    """Constructor.

    Args:
        dim: Position of the real/imaginary axis in the expanded tensor.
            The default `-1` is the natural layout of `torch.view_as_real`;
            any other position adds a `movedim` (still a view). Use `0` or
            `1` to feed real and imaginary parts to a network as channels.
        strict: If `True`, a non-complex input to `transform` (or an input
            to `revert` whose `dim` axis is not of size 2) raises. Set
            `False` to pass such tensors through unchanged, which is what
            you want when the transform is applied across a nested sample
            holding both complex and real entries -- at the cost of the
            round trip no longer being checked.
    """
    super().__init__()
    self.dim = dim
    self.strict = strict

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Collapse real back to complex (ignores params).

Source code in src/chuchichaestli/data/transforms/complex.py
93
94
95
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Collapse real back to complex (ignores `params`)."""
    return self._collapse(x)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Expand complex to real (ignores params).

Source code in src/chuchichaestli/data/transforms/complex.py
89
90
91
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Expand complex to real (ignores `params`)."""
    return self._expand(x)

HEPTransform

HEPTransform(
    gamma: float = 1.0, b: float = 0.0, C: float = 1.0
)

Bases: Transform

High-energy-physics stretch for a more Gaussian-like value distribution.

Computes $\hat{x} = ((x - b) / C)^{1/\gamma}$, where $C$ is roughly the data range and $b$ the bias (data minimum).

Constructor.

Parameters:

Name Type Description Default
gamma float

Inverse-power exponent of the stretch.

1.0
b float

Bias, i.e. the global data minimum.

0.0
C float

A number around the global data range.

1.0

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Invert the stretch.

transform

Apply the stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(self, gamma: float = 1.0, b: float = 0.0, C: float = 1.0):
    """Constructor.

    Args:
        gamma: Inverse-power exponent of the stretch.
        b: Bias, i.e. the global data minimum.
        C: A number around the global data range.
    """
    super().__init__()
    self.gamma = _as_tensor(gamma)
    self.b = _as_tensor(b)
    self.C = _as_tensor(C)
    self.inv_gamma = 1.0 / self.gamma
    self.affine = Affine(a=self.C, b=self.b)

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
97
98
99
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return InvHEPTransform(gamma=self.gamma, b=self.b, C=self.C)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Invert the stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
93
94
95
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Invert the stretch."""
    return self.affine.revert(torch.pow(x, self.gamma))

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
89
90
91
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the stretch."""
    return torch.pow(self.affine.transform(x), self.inv_gamma)

HounsfieldClamp

HounsfieldClamp(center: float, width: float)

Bases: Clamp

Clamp Hounsfield units to a window (lossy, no inverse).

Constructor.

Parameters:

Name Type Description Default
center float

Window center (level) in HU, e.g. 40 for soft tissue.

required
width float

Window width in HU, e.g. 400 for soft tissue.

required
Source code in src/chuchichaestli/data/transforms/intensity.py
334
335
336
337
338
339
340
341
342
343
def __init__(self, center: float, width: float):
    """Constructor.

    Args:
        center: Window center (level) in HU, e.g. 40 for soft tissue.
        width: Window width in HU, e.g. 400 for soft tissue.
    """
    super().__init__(min=center - width / 2, max=center + width / 2)
    self.center = center
    self.width = width

HounsfieldScale

HounsfieldScale(
    slope: float = 1.0, intercept: float = -1024.0
)

Bases: Transform

Rescale raw CT stored values to Hounsfield units: slope * x + intercept.

Constructor.

Parameters:

Name Type Description Default
slope float

DICOM RescaleSlope (tag 0028,1053).

1.0
intercept float

DICOM RescaleIntercept (tag 0028,1052).

-1024.0

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Map Hounsfield units back to stored values.

transform

Map stored values to Hounsfield units.

Source code in src/chuchichaestli/data/transforms/intensity.py
290
291
292
293
294
295
296
297
298
299
300
def __init__(self, slope: float = 1.0, intercept: float = -1024.0):
    """Constructor.

    Args:
        slope: DICOM `RescaleSlope` (tag 0028,1053).
        intercept: DICOM `RescaleIntercept` (tag 0028,1052).
    """
    super().__init__()
    self.slope = _as_tensor(slope)
    self.intercept = _as_tensor(intercept)
    self.affine = Affine(a=1.0 / self.slope, b=-self.intercept / self.slope)

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
310
311
312
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return InvHounsfieldScale(slope=self.slope, intercept=self.intercept)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Map Hounsfield units back to stored values.

Source code in src/chuchichaestli/data/transforms/intensity.py
306
307
308
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Map Hounsfield units back to stored values."""
    return self.affine.revert(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Map stored values to Hounsfield units.

Source code in src/chuchichaestli/data/transforms/intensity.py
302
303
304
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Map stored values to Hounsfield units."""
    return self.affine.transform(x, params)

InvBasisProjection

InvBasisProjection(
    bases: dict[int, tuple[str, int] | int | torch.Tensor],
    basis: str = "chebyshev",
    weights: dict[int, torch.Tensor] | None = None,
    rcond: float | None = None,
    lengths: dict[int, int] | None = None,
    cond_warn: float | None = 1000000.0,
)

Bases: BasisProjection

Inverse of BasisProjection: reconstruct forward, project on revert.

The original axis lengths cannot be inferred from coefficients alone, so every axis needs one of: an entry in lengths, an explicit design matrix (whose row count supplies it), or a prior projection through this instance.

Methods:

Name Description
revert

Project onto the truncated bases.

transform

Reconstruct from the coefficients.

Source code in src/chuchichaestli/data/transforms/basis.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
def __init__(
    self,
    bases: dict[int, tuple[str, int] | int | torch.Tensor],
    basis: str = "chebyshev",
    weights: dict[int, torch.Tensor] | None = None,
    rcond: float | None = None,
    lengths: dict[int, int] | None = None,
    cond_warn: float | None = 1e6,
):
    """Constructor.

    Args:
        bases: Maps an axis of the input to its basis. Each value is either
            an `(name, order)` pair, a bare `order` using the default
            `basis` family, or an explicit `(N, M)` design matrix (e.g.
            fitted PCA components). Axes may be negative.
        basis: Default family for entries given as a bare order. One of
            `BASIS_REGISTRY`: chebyshev, legendre, monomial, fourier, dct.
        weights: Optional per-axis 1-D non-negative weights of length
            `N` for a weighted least-squares fit, e.g. for measurement
            noise or masked samples. Axes as in `bases`, sign included.
        rcond: Relative cutoff for small singular values in the
            pseudoinverse, forwarded to `torch.linalg.pinv` as `rtol`.
        lengths: Original length `N` per axis, which coefficients do not
            carry. Projecting records it, but pass it explicitly to revert
            standalone or across processes. Axes as in `bases`, sign
            included.
        cond_warn: Warn when the fitted matrix's condition number exceeds
            this. `None` disables the check.
    """
    super().__init__()
    if not bases:
        raise ValueError("bases must name at least one axis")
    normalised: dict[int, tuple[str, int] | torch.Tensor] = {}
    for axis, spec in bases.items():
        if isinstance(spec, torch.Tensor):
            if spec.ndim != 2:
                raise ValueError(
                    f"design matrix for axis {axis} must be 2-D (N, M), "
                    f"got shape {tuple(spec.shape)}"
                )
            normalised[axis] = spec
            continue
        name, order = (basis, spec) if isinstance(spec, int) else spec
        if name not in BASIS_REGISTRY:
            raise ValueError(
                f"unknown basis {name!r}; choose from {sorted(BASIS_REGISTRY)}"
            )
        if order < 1:
            raise ValueError(f"order for axis {axis} must be >= 1, got {order}")
        normalised[axis] = (name, order)
    for name, mapping in (("weights", weights), ("lengths", lengths)):
        stray = sorted(set(mapping or ()) - set(normalised))
        if stray:
            raise ValueError(
                f"{name} names axes {stray} that are absent from bases "
                f"{sorted(normalised)}; write them with the same sign"
            )
    self.bases = normalised
    self.basis = basis
    self.weights = {a: w.detach().clone() for a, w in (weights or {}).items()}
    self.rcond = rcond
    self.cond_warn = cond_warn
    self.lengths = dict(lengths) if lengths else {}  # original axis lengths
    self._declared = set(self.lengths)
    for axis, spec in normalised.items():
        if isinstance(spec, torch.Tensor):
            self.lengths.setdefault(axis, spec.shape[0])
            self._declared.add(axis)
    self._cache: dict[Any, tuple[torch.Tensor, torch.Tensor]] = {}

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Project onto the truncated bases.

Source code in src/chuchichaestli/data/transforms/basis.py
347
348
349
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Project onto the truncated bases."""
    return super().transform(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Reconstruct from the coefficients.

Source code in src/chuchichaestli/data/transforms/basis.py
343
344
345
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Reconstruct from the coefficients."""
    return super().revert(x, params)

InvHEPTransform

InvHEPTransform(
    gamma: float = 1.0, b: float = 0.0, C: float = 1.0
)

Bases: HEPTransform

Inverse of HEPTransform.

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Apply the forward stretch.

transform

Apply the inverse stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
74
75
76
77
78
79
80
81
82
83
84
85
86
87
def __init__(self, gamma: float = 1.0, b: float = 0.0, C: float = 1.0):
    """Constructor.

    Args:
        gamma: Inverse-power exponent of the stretch.
        b: Bias, i.e. the global data minimum.
        C: A number around the global data range.
    """
    super().__init__()
    self.gamma = _as_tensor(gamma)
    self.b = _as_tensor(b)
    self.C = _as_tensor(C)
    self.inv_gamma = 1.0 / self.gamma
    self.affine = Affine(a=self.C, b=self.b)

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
113
114
115
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return HEPTransform(gamma=self.gamma, b=self.b, C=self.C)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Apply the forward stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
109
110
111
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the forward stretch."""
    return super().transform(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the inverse stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
105
106
107
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the inverse stretch."""
    return super().revert(x, params)

InvHounsfieldScale

InvHounsfieldScale(
    slope: float = 1.0, intercept: float = -1024.0
)

Bases: HounsfieldScale

Inverse of HounsfieldScale (Hounsfield units -> stored values).

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Map stored values to Hounsfield units.

transform

Map Hounsfield units to stored values.

Source code in src/chuchichaestli/data/transforms/intensity.py
290
291
292
293
294
295
296
297
298
299
300
def __init__(self, slope: float = 1.0, intercept: float = -1024.0):
    """Constructor.

    Args:
        slope: DICOM `RescaleSlope` (tag 0028,1053).
        intercept: DICOM `RescaleIntercept` (tag 0028,1052).
    """
    super().__init__()
    self.slope = _as_tensor(slope)
    self.intercept = _as_tensor(intercept)
    self.affine = Affine(a=1.0 / self.slope, b=-self.intercept / self.slope)

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
326
327
328
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return HounsfieldScale(slope=self.slope, intercept=self.intercept)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Map stored values to Hounsfield units.

Source code in src/chuchichaestli/data/transforms/intensity.py
322
323
324
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Map stored values to Hounsfield units."""
    return super().transform(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Map Hounsfield units to stored values.

Source code in src/chuchichaestli/data/transforms/intensity.py
318
319
320
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Map Hounsfield units to stored values."""
    return super().revert(x, params)

InvLogP1Transform

InvLogP1Transform(log_fn: Callable = torch.log)

Bases: LogP1Transform

Inverse of LogP1Transform.

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Apply the forward log1p stretch.

transform

Apply the inverse log1p stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
127
128
129
130
131
132
133
134
135
136
137
def __init__(self, log_fn: Callable = torch.log):
    """Constructor.

    Args:
        log_fn: Logarithm function fixing the base (default: natural log).
    """
    super().__init__()
    self.fn = log_fn
    self.base = torch.exp(
        torch.log(torch.tensor(10.0)) / log_fn(torch.tensor(10.0))
    )

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
195
196
197
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return LogP1Transform(log_fn=self.fn)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Apply the forward log1p stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
191
192
193
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the forward log1p stretch."""
    return super().transform(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the inverse log1p stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
187
188
189
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the inverse log1p stretch."""
    return super().revert(x, params)

InvLogTransform

InvLogTransform(log_fn: Callable = torch.log)

Bases: LogTransform

Inverse of LogTransform.

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Apply the log.

transform

Apply the exponentiation.

Source code in src/chuchichaestli/data/transforms/intensity.py
127
128
129
130
131
132
133
134
135
136
137
def __init__(self, log_fn: Callable = torch.log):
    """Constructor.

    Args:
        log_fn: Logarithm function fixing the base (default: natural log).
    """
    super().__init__()
    self.fn = log_fn
    self.base = torch.exp(
        torch.log(torch.tensor(10.0)) / log_fn(torch.tensor(10.0))
    )

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
163
164
165
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return LogTransform(log_fn=self.fn)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Apply the log.

Source code in src/chuchichaestli/data/transforms/intensity.py
159
160
161
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the log."""
    return super().transform(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the exponentiation.

Source code in src/chuchichaestli/data/transforms/intensity.py
155
156
157
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the exponentiation."""
    return super().revert(x, params)

InvMinMaxScale

InvMinMaxScale(
    vmin: float | None = None,
    vmax: float | None = None,
    feature_range: tuple[float, float] = (0.0, 1.0),
)

Bases: MinMaxScale

Inverse of MinMaxScale (requires fixed vmin/vmax).

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Apply the forward rescale.

transform

Apply the inverse rescale.

Source code in src/chuchichaestli/data/transforms/intensity.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def __init__(
    self,
    vmin: float | None = None,
    vmax: float | None = None,
    feature_range: tuple[float, float] = (0.0, 1.0),
):
    """Constructor.

    Args:
        vmin: Input minimum; `None` uses each tensor's own `min()`.
        vmax: Input maximum; `None` uses each tensor's own `max()`.
        feature_range: Target `(low, high)` range.
    """
    super().__init__()
    self.vmin = None if vmin is None else _as_tensor(vmin)
    self.vmax = None if vmax is None else _as_tensor(vmax)
    self.lo, self.hi = feature_range

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
257
258
259
260
261
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return MinMaxScale(
        vmin=self.vmin, vmax=self.vmax, feature_range=(self.lo, self.hi)
    )

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Apply the forward rescale.

Source code in src/chuchichaestli/data/transforms/intensity.py
253
254
255
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the forward rescale."""
    return super().transform(x, params)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the inverse rescale.

Source code in src/chuchichaestli/data/transforms/intensity.py
249
250
251
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the inverse rescale."""
    return super().revert(x, params)

InvWaveletDecompose

InvWaveletDecompose(
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
)

Bases: WaveletDecompose

Inverse of WaveletDecompose: reconstruct forward, decompose on revert.

Methods:

Name Description
get_inverse

Return the transform that decomposes what this one reconstructs.

make_params

Report the recorded spatial shape; the forward pass reads, not writes.

revert

Decompose the trailing spatial axes.

transform

Reconstruct the trailing spatial axes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
):
    """Constructor.

    Args:
        wavelet: Wavelet to decompose with, by name or as a `Wavelet`.
        ndim: Number of trailing spatial dims to transform.
        mode: Signal extension mode; `'periodization'` is the only one that
            halves each axis exactly, and so the only one that does not grow
            the data.
        stack_dim: Position of the subband axis in the result; immediately
            before the spatial axes if omitted.
        sizes: Spatial shape the coefficients revert to, if it is known.

    Raises:
        ValueError: If `ndim` is out of range.
    """
    super().__init__()
    if not 1 <= ndim <= 3:
        raise ValueError(f"The transform runs over one to three axes; got {ndim}.")
    levels = 1
    self.wavelet = wavelet
    self.ndim = ndim
    self.mode = mode
    self.levels = levels
    self.stack_dim = -ndim - 1 if stack_dim is None else stack_dim
    self.sizes = None if sizes is None else tuple(int(n) for n in sizes)
    self._declared = sizes is not None
    self._coerced: Wavelet | None = None

get_inverse

get_inverse() -> WaveletDecompose

Return the transform that decomposes what this one reconstructs.

Source code in src/chuchichaestli/data/transforms/wavelet.py
234
235
236
237
238
def get_inverse(self) -> "WaveletDecompose":
    """Return the transform that decomposes what this one reconstructs."""
    return WaveletDecompose(
        self.wavelet, self.ndim, self.mode, self.stack_dim, self.sizes
    )

make_params

make_params(flat_inputs: list[Any]) -> dict[str, Any]

Report the recorded spatial shape; the forward pass reads, not writes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
230
231
232
def make_params(self, flat_inputs: list[Any]) -> dict[str, Any]:
    """Report the recorded spatial shape; the forward pass reads, not writes."""
    return {"sizes": self.sizes}

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Decompose the trailing spatial axes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
225
226
227
228
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Decompose the trailing spatial axes."""
    self._record(tuple(int(n) for n in x.shape[-self.ndim :]))
    return self._decompose(x)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Reconstruct the trailing spatial axes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
221
222
223
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Reconstruct the trailing spatial axes."""
    return self._reconstruct(x)

InvWaveletPacket

InvWaveletPacket(
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    levels: int = 2,
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
)

Bases: WaveletPacket, InvWaveletDecompose

Inverse of WaveletPacket: reconstruct forward, decompose on revert.

Takes the level count from WaveletPacket and the swapped direction from InvWaveletDecompose, in that order.

Methods:

Name Description
get_inverse

Return the transform that decomposes what this one reconstructs.

Source code in src/chuchichaestli/data/transforms/wavelet.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def __init__(
    self,
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    levels: int = 2,
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
):
    """Constructor.

    Args:
        wavelet: Wavelet to decompose with, by name or as a `Wavelet`.
        ndim: Number of trailing spatial dims to transform.
        mode: Signal extension mode.
        levels: Number of levels to decompose.
        stack_dim: Position of the subband axis in the result.
        sizes: Spatial shape the coefficients revert to, if it is known.

    Raises:
        ValueError: If `levels` is not positive.
    """
    super().__init__(wavelet, ndim, mode, stack_dim, sizes)
    if levels < 1:
        raise ValueError(f"A decomposition needs at least one level; got {levels}.")
    self.levels = levels

get_inverse

get_inverse() -> WaveletPacket

Return the transform that decomposes what this one reconstructs.

Source code in src/chuchichaestli/data/transforms/wavelet.py
294
295
296
297
298
299
300
301
302
303
def get_inverse(self) -> "WaveletPacket":
    """Return the transform that decomposes what this one reconstructs."""
    return WaveletPacket(
        self.wavelet,
        self.ndim,
        self.mode,
        self.levels,
        self.stack_dim,
        self.sizes,
    )

LogP1Transform

LogP1Transform(log_fn: Callable = torch.log)

Bases: LogTransform

Log1p-style stretch $\hat{x} = \log(x + 1)$ (handles zeros).

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Invert the log1p stretch.

transform

Apply the log1p stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
127
128
129
130
131
132
133
134
135
136
137
def __init__(self, log_fn: Callable = torch.log):
    """Constructor.

    Args:
        log_fn: Logarithm function fixing the base (default: natural log).
    """
    super().__init__()
    self.fn = log_fn
    self.base = torch.exp(
        torch.log(torch.tensor(10.0)) / log_fn(torch.tensor(10.0))
    )

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
179
180
181
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return InvLogP1Transform(log_fn=self.fn)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Invert the log1p stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
175
176
177
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Invert the log1p stretch."""
    return super().revert(x, params) - 1

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the log1p stretch.

Source code in src/chuchichaestli/data/transforms/intensity.py
171
172
173
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the log1p stretch."""
    return super().transform(x + 1, params)

LogTransform

LogTransform(log_fn: Callable = torch.log)

Bases: Transform

Logarithmic stretch $\hat{x} = \log(x)$ (invertible by exponentiation).

Pure nonlinearity; compose with Affine (e.g. via SequentialTransform) to normalize the log-space range.

Constructor.

Parameters:

Name Type Description Default
log_fn Callable

Logarithm function fixing the base (default: natural log).

torch.log

Methods:

Name Description
get_inverse

Return the inverse transform.

revert

Invert the log by exponentiation.

transform

Apply the log.

Source code in src/chuchichaestli/data/transforms/intensity.py
127
128
129
130
131
132
133
134
135
136
137
def __init__(self, log_fn: Callable = torch.log):
    """Constructor.

    Args:
        log_fn: Logarithm function fixing the base (default: natural log).
    """
    super().__init__()
    self.fn = log_fn
    self.base = torch.exp(
        torch.log(torch.tensor(10.0)) / log_fn(torch.tensor(10.0))
    )

get_inverse

get_inverse() -> Transform

Return the inverse transform.

Source code in src/chuchichaestli/data/transforms/intensity.py
147
148
149
def get_inverse(self) -> Transform:
    """Return the inverse transform."""
    return InvLogTransform(log_fn=self.fn)

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Invert the log by exponentiation.

Source code in src/chuchichaestli/data/transforms/intensity.py
143
144
145
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Invert the log by exponentiation."""
    return torch.pow(self.base, x)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Apply the log.

Source code in src/chuchichaestli/data/transforms/intensity.py
139
140
141
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Apply the log."""
    return self.fn(x)

MinMaxScale

MinMaxScale(
    vmin: float | None = None,
    vmax: float | None = None,
    feature_range: tuple[float, float] = (0.0, 1.0),
)

Bases: Transform

Affine rescale from [vmin, vmax] into feature_range.

Constructor.

Parameters:

Name Type Description Default
vmin float | None

Input minimum; None uses each tensor's own min().

None
vmax float | None

Input maximum; None uses each tensor's own max().

None
feature_range tuple[float, float]

Target (low, high) range.

(0.0, 1.0)

Methods:

Name Description
get_inverse

Return the inverse rescale (requires fixed vmin/vmax).

revert

Invert the rescale (requires fixed vmin/vmax).

transform

Rescale the input into feature_range.

Source code in src/chuchichaestli/data/transforms/intensity.py
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def __init__(
    self,
    vmin: float | None = None,
    vmax: float | None = None,
    feature_range: tuple[float, float] = (0.0, 1.0),
):
    """Constructor.

    Args:
        vmin: Input minimum; `None` uses each tensor's own `min()`.
        vmax: Input maximum; `None` uses each tensor's own `max()`.
        feature_range: Target `(low, high)` range.
    """
    super().__init__()
    self.vmin = None if vmin is None else _as_tensor(vmin)
    self.vmax = None if vmax is None else _as_tensor(vmax)
    self.lo, self.hi = feature_range

get_inverse

get_inverse() -> Transform

Return the inverse rescale (requires fixed vmin/vmax).

Source code in src/chuchichaestli/data/transforms/intensity.py
237
238
239
240
241
242
243
def get_inverse(self) -> Transform:
    """Return the inverse rescale (requires fixed `vmin`/`vmax`)."""
    if self.vmin is None or self.vmax is None:
        raise RuntimeError("MinMaxScale.get_inverse requires fixed vmin/vmax.")
    return InvMinMaxScale(
        vmin=self.vmin, vmax=self.vmax, feature_range=(self.lo, self.hi)
    )

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Invert the rescale (requires fixed vmin/vmax).

Source code in src/chuchichaestli/data/transforms/intensity.py
230
231
232
233
234
235
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Invert the rescale (requires fixed `vmin`/`vmax`)."""
    if self.vmin is None or self.vmax is None:
        raise RuntimeError("MinMaxScale.revert requires fixed vmin/vmax.")
    x = (x - self.lo) / (self.hi - self.lo)
    return x * (self.vmax - self.vmin) + self.vmin

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Rescale the input into feature_range.

Source code in src/chuchichaestli/data/transforms/intensity.py
223
224
225
226
227
228
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Rescale the input into `feature_range`."""
    vmin = x.min() if self.vmin is None else self.vmin
    vmax = x.max() if self.vmax is None else self.vmax
    x = (x - vmin) / (vmax - vmin)
    return x * (self.hi - self.lo) + self.lo

PadND

PadND(
    padding: Sequence[Sequence[int]],
    mode: str = "constant",
    value: float = 0.0,
)

Bases: Transform

Pad the trailing N spatial dims via torch.nn.functional.pad.

Constructor.

Parameters:

Name Type Description Default
padding Sequence[Sequence[int]]

One (left, right) pair per trailing spatial axis, in axis order (outermost first).

required
mode str

Padding mode (constant, reflect, replicate, circular).

'constant'
value float

Fill value for constant padding.

0.0

Methods:

Name Description
transform

Apply the padding to the trailing spatial dims.

Source code in src/chuchichaestli/data/transforms/resize.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
def __init__(
    self,
    padding: Sequence[Sequence[int]],
    mode: str = "constant",
    value: float = 0.0,
):
    """Constructor.

    Args:
        padding: One `(left, right)` pair per trailing spatial axis, in axis
            order (outermost first).
        mode: Padding mode (`constant`, `reflect`, `replicate`, `circular`).
        value: Fill value for `constant` padding.
    """
    super().__init__()
    self.padding = tuple(tuple(p) for p in padding)
    self.mode = mode
    self.value = value

transform

transform(inpt: Any, params: dict[str, Any]) -> Any

Apply the padding to the trailing spatial dims.

Source code in src/chuchichaestli/data/transforms/resize.py
90
91
92
93
94
95
def transform(self, inpt: Any, params: dict[str, Any]) -> Any:
    """Apply the padding to the trailing spatial dims."""
    pad_arg = [p for lohi in reversed(self.padding) for p in lohi]
    if self.mode == "constant":
        return F.pad(inpt, pad_arg, mode="constant", value=self.value)
    return F.pad(inpt, pad_arg, mode=self.mode)

RandomCropND

RandomCropND(
    size: int | Sequence[int], ndim: int | None = None
)

Bases: Transform

Random crop over the trailing N spatial dims (leading dims pass through).

Randomness lives in make_params, so a SequenceCollate draws one crop box per batch and applies it identically to every paired field/step.

Constructor.

Parameters:

Name Type Description Default
size int | Sequence[int]

Per-axis crop size (e.g. (128, 128, 128)), or an ndim int.

required
ndim int | None

Number of trailing spatial dims when size is an int.

None

Methods:

Name Description
make_params

Sample one in-bounds crop origin from the first tensor's shape.

transform

Slice the shared crop box on the trailing spatial dims.

Source code in src/chuchichaestli/data/transforms/crop.py
38
39
40
41
42
43
44
45
46
47
def __init__(self, size: int | Sequence[int], ndim: int | None = None):
    """Constructor.

    Args:
        size: Per-axis crop size (e.g. `(128, 128, 128)`), or an `ndim` int.
        ndim: Number of trailing spatial dims when `size` is an int.
    """
    super().__init__()
    self.size = _as_size(size, ndim)
    self.ndim = len(self.size)

make_params

make_params(flat_inputs: list[Any]) -> dict[str, Any]

Sample one in-bounds crop origin from the first tensor's shape.

Source code in src/chuchichaestli/data/transforms/crop.py
49
50
51
52
53
54
55
56
57
def make_params(self, flat_inputs: list[Any]) -> dict[str, Any]:
    """Sample one in-bounds crop origin from the first tensor's shape."""
    x = next(i for i in flat_inputs if isinstance(i, torch.Tensor))
    spatial = x.shape[-self.ndim :]
    starts = tuple(
        int(torch.randint(0, max(1, dim - crop + 1), size=()))
        for dim, crop in zip(spatial, self.size)
    )
    return {"starts": starts, "size": self.size}

transform

transform(x: Any, params: dict[str, Any]) -> Any

Slice the shared crop box on the trailing spatial dims.

Source code in src/chuchichaestli/data/transforms/crop.py
59
60
61
62
63
64
def transform(self, x: Any, params: dict[str, Any]) -> Any:
    """Slice the shared crop box on the trailing spatial dims."""
    slices = tuple(
        slice(s, s + c) for s, c in zip(params["starts"], params["size"])
    )
    return x[(..., *slices)]

RandomFlipND

RandomFlipND(
    ndim: int,
    p: float = 0.5,
    axes: Sequence[int] | None = None,
)

Bases: Transform

Randomly flip each eligible trailing spatial axis with probability p.

The per-axis draws happen in make_params, so a SequenceCollate flips every paired field/step identically.

Constructor.

Parameters:

Name Type Description Default
ndim int

Number of trailing spatial dims.

required
p float

Per-axis flip probability.

0.5
axes Sequence[int] | None

Spatial axes eligible for flipping (default: all ndim).

None

Methods:

Name Description
make_params

Sample which axes to flip.

transform

Flip the chosen axes (identity when none were sampled).

Source code in src/chuchichaestli/data/transforms/geometric.py
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self, ndim: int, p: float = 0.5, axes: Sequence[int] | None = None
):
    """Constructor.

    Args:
        ndim: Number of trailing spatial dims.
        p: Per-axis flip probability.
        axes: Spatial axes eligible for flipping (default: all `ndim`).
    """
    super().__init__()
    self.ndim = ndim
    self.p = p
    self.axes = _neg_axes(axes, ndim)

make_params

make_params(flat_inputs: list[Any]) -> dict[str, Any]

Sample which axes to flip.

Source code in src/chuchichaestli/data/transforms/geometric.py
50
51
52
53
def make_params(self, flat_inputs: list[Any]) -> dict[str, Any]:
    """Sample which axes to flip."""
    dims = tuple(a for a in self.axes if float(torch.rand(())) < self.p)
    return {"dims": dims}

transform

transform(x: Any, params: dict[str, Any]) -> Any

Flip the chosen axes (identity when none were sampled).

Source code in src/chuchichaestli/data/transforms/geometric.py
55
56
57
def transform(self, x: Any, params: dict[str, Any]) -> Any:
    """Flip the chosen axes (identity when none were sampled)."""
    return torch.flip(x, dims=params["dims"]) if params["dims"] else x

RandomRot90ND

RandomRot90ND(ndim: int, axes: Sequence[int] | None = None)

Bases: Transform

Random k*90 degree rotation in a random trailing spatial axis-plane.

Exact (no interpolation). k and the plane are drawn in make_params, so a SequenceCollate rotates every paired field/step identically.

Constructor.

Parameters:

Name Type Description Default
ndim int

Number of trailing spatial dims (must be >= 2).

required
axes Sequence[int] | None

Spatial axes eligible to form rotation planes (default: all).

None

Methods:

Name Description
make_params

Sample the rotation count k and the axis-plane.

transform

Rotate by k*90 degrees in the chosen plane (identity when k%4==0).

Source code in src/chuchichaestli/data/transforms/geometric.py
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def __init__(self, ndim: int, axes: Sequence[int] | None = None):
    """Constructor.

    Args:
        ndim: Number of trailing spatial dims (must be >= 2).
        axes: Spatial axes eligible to form rotation planes (default: all).
    """
    super().__init__()
    if ndim < 2:
        raise ValueError("RandomRot90ND requires ndim >= 2.")
    self.ndim = ndim
    spatial = _neg_axes(axes, ndim)
    self.planes = [
        (spatial[i], spatial[j])
        for i in range(len(spatial))
        for j in range(i + 1, len(spatial))
    ]

make_params

make_params(flat_inputs: list[Any]) -> dict[str, Any]

Sample the rotation count k and the axis-plane.

Source code in src/chuchichaestli/data/transforms/geometric.py
87
88
89
90
91
def make_params(self, flat_inputs: list[Any]) -> dict[str, Any]:
    """Sample the rotation count `k` and the axis-plane."""
    k = int(torch.randint(0, 4, size=()))
    plane = self.planes[int(torch.randint(0, len(self.planes), size=()))]
    return {"k": k, "plane": plane}

transform

transform(x: Any, params: dict[str, Any]) -> Any

Rotate by k*90 degrees in the chosen plane (identity when k%4==0).

Source code in src/chuchichaestli/data/transforms/geometric.py
93
94
95
96
97
def transform(self, x: Any, params: dict[str, Any]) -> Any:
    """Rotate by `k*90` degrees in the chosen plane (identity when k%4==0)."""
    if params["k"] % 4 == 0:
        return x
    return torch.rot90(x, params["k"], dims=params["plane"])

ResizeND

ResizeND(
    size: int | Sequence[int],
    ndim: int | None = None,
    mode: str = "nearest",
)

Bases: Transform

Resize the trailing N spatial dims via torch.nn.functional.interpolate.

Bare (*spatial) and (C, *spatial) leaves are supported by temporarily unsqueezing to the (N, C, *spatial) layout interpolate expects.

Constructor.

Parameters:

Name Type Description Default
size int | Sequence[int]

Target per-axis size (e.g. (64, 64, 64)), or an int with ndim.

required
ndim int | None

Number of trailing spatial dims when size is an int.

None
mode str

Interpolation mode passed to interpolate (e.g. nearest, trilinear).

'nearest'

Methods:

Name Description
transform

Interpolate to size, restoring the input's rank afterwards.

Source code in src/chuchichaestli/data/transforms/resize.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(
    self,
    size: int | Sequence[int],
    ndim: int | None = None,
    mode: str = "nearest",
):
    """Constructor.

    Args:
        size: Target per-axis size (e.g. `(64, 64, 64)`), or an int with
            `ndim`.
        ndim: Number of trailing spatial dims when `size` is an int.
        mode: Interpolation mode passed to `interpolate` (e.g. `nearest`,
            `trilinear`).
    """
    super().__init__()
    if isinstance(size, int):
        if ndim is None:
            raise ValueError("`ndim` is required when `size` is an int.")
        size = (size,) * ndim
    self.size = tuple(size)
    self.ndim = len(self.size)
    self.mode = mode

transform

transform(inpt: Any, params: dict[str, Any]) -> Any

Interpolate to size, restoring the input's rank afterwards.

Source code in src/chuchichaestli/data/transforms/resize.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def transform(self, inpt: Any, params: dict[str, Any]) -> Any:
    """Interpolate to `size`, restoring the input's rank afterwards."""
    x = inpt
    squeezed = 0
    while x.ndim < self.ndim + 2:
        x = x.unsqueeze(0)
        squeezed += 1
    if self.mode in _ALIGN_MODES:
        x = F.interpolate(x, size=self.size, mode=self.mode, align_corners=False)
    else:
        x = F.interpolate(x, size=self.size, mode=self.mode)
    for _ in range(squeezed):
        x = x.squeeze(0)
    return x

SequentialTransform

SequentialTransform(
    *transforms: Transform | Sequence[Transform],
)

Bases: Transform

Chain v2 transforms, sampling each child's params once per batch.

Unlike torchvision's Compose (which re-samples per call and exposes no make_params/transform), this drives the child make_params/transform hooks directly, so it plugs into SequenceCollate and applies the same draw to every paired field/step. It is invertible when all children are.

Constructor.

Parameters:

Name Type Description Default
transforms Transform | Sequence[Transform]

The child transforms to apply in order, given either as positional arguments or a single sequence.

()

Methods:

Name Description
get_inverse

Return the reversed chain of child inverses.

make_params

Sample each child's params once, threading the running leaves through.

revert

Invert every child in reverse order (all children must be invertible).

transform

Apply every child in order with its shared params.

Source code in src/chuchichaestli/data/transforms/compose.py
24
25
26
27
28
29
30
31
32
33
34
def __init__(self, *transforms: Transform | Sequence[Transform]):
    """Constructor.

    Args:
        transforms: The child transforms to apply in order, given either as
            positional arguments or a single sequence.
    """
    super().__init__()
    if len(transforms) == 1 and isinstance(transforms[0], list | tuple):
        transforms = tuple(transforms[0])
    self.transforms = list(transforms)

get_inverse

get_inverse() -> SequentialTransform

Return the reversed chain of child inverses.

Source code in src/chuchichaestli/data/transforms/compose.py
61
62
63
def get_inverse(self) -> "SequentialTransform":
    """Return the reversed chain of child inverses."""
    return SequentialTransform(*[t.get_inverse() for t in reversed(self.transforms)])

make_params

make_params(flat_inputs: list[Any]) -> dict[str, Any]

Sample each child's params once, threading the running leaves through.

Later children see the (intermediate) shape produced by earlier ones, so e.g. a crop before a flip is accounted for.

Source code in src/chuchichaestli/data/transforms/compose.py
36
37
38
39
40
41
42
43
44
45
46
47
def make_params(self, flat_inputs: list[Any]) -> dict[str, Any]:
    """Sample each child's params once, threading the running leaves through.

    Later children see the (intermediate) shape produced by earlier ones, so
    e.g. a crop before a flip is accounted for.
    """
    params, cur = [], list(flat_inputs)
    for t in self.transforms:
        p = t.make_params(cur)
        params.append(p)
        cur = [t.transform(x, p) for x in cur]
    return {"params": params}

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Invert every child in reverse order (all children must be invertible).

Source code in src/chuchichaestli/data/transforms/compose.py
55
56
57
58
59
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Invert every child in reverse order (all children must be invertible)."""
    for t in reversed(self.transforms):
        x = t.revert(x)
    return x

transform

transform(inpt: Any, params: dict[str, Any]) -> Any

Apply every child in order with its shared params.

Source code in src/chuchichaestli/data/transforms/compose.py
49
50
51
52
53
def transform(self, inpt: Any, params: dict[str, Any]) -> Any:
    """Apply every child in order with its shared params."""
    for t, p in zip(self.transforms, params["params"]):
        inpt = t.transform(inpt, p)
    return inpt

WaveletDecompose

WaveletDecompose(
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
)

Bases: Transform

Replace the trailing ndim axes by the subbands of a wavelet transform.

The subbands are stacked on one new axis, so the result stays a single tensor and collates like any other field. Leading axes pass through. Only one level is taken: recursing on the coarsest subband alone would leave the levels at different resolutions, which no single tensor can hold. See WaveletPacket for the deeper decomposition that does keep one resolution.

Reverting needs the spatial shape the coefficients came from, which the transform records as it runs; pass sizes to fix it up front instead.

Constructor.

Parameters:

Name Type Description Default
wavelet str | Wavelet

Wavelet to decompose with, by name or as a Wavelet.

'haar'
ndim int

Number of trailing spatial dims to transform.

2
mode ExtensionModeTypes

Signal extension mode; 'periodization' is the only one that halves each axis exactly, and so the only one that does not grow the data.

'zero'
stack_dim int | None

Position of the subband axis in the result; immediately before the spatial axes if omitted.

None
sizes Sequence[int] | None

Spatial shape the coefficients revert to, if it is known.

None

Raises:

Type Description
ValueError

If ndim is out of range.

Methods:

Name Description
get_inverse

Return the transform that reconstructs what this one decomposes.

make_params

Record the spatial shape of the first tensor, shared by every leaf.

revert

Reconstruct the trailing spatial axes.

transform

Decompose the trailing spatial axes.

Attributes:

Name Type Description
axes tuple[int, ...]

Axes the transform runs over, as trailing indices.

subbands int

Number of subbands the transform produces.

Source code in src/chuchichaestli/data/transforms/wavelet.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
def __init__(
    self,
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
):
    """Constructor.

    Args:
        wavelet: Wavelet to decompose with, by name or as a `Wavelet`.
        ndim: Number of trailing spatial dims to transform.
        mode: Signal extension mode; `'periodization'` is the only one that
            halves each axis exactly, and so the only one that does not grow
            the data.
        stack_dim: Position of the subband axis in the result; immediately
            before the spatial axes if omitted.
        sizes: Spatial shape the coefficients revert to, if it is known.

    Raises:
        ValueError: If `ndim` is out of range.
    """
    super().__init__()
    if not 1 <= ndim <= 3:
        raise ValueError(f"The transform runs over one to three axes; got {ndim}.")
    levels = 1
    self.wavelet = wavelet
    self.ndim = ndim
    self.mode = mode
    self.levels = levels
    self.stack_dim = -ndim - 1 if stack_dim is None else stack_dim
    self.sizes = None if sizes is None else tuple(int(n) for n in sizes)
    self._declared = sizes is not None
    self._coerced: Wavelet | None = None

axes property

axes: tuple[int, ...]

Axes the transform runs over, as trailing indices.

subbands property

subbands: int

Number of subbands the transform produces.

get_inverse

get_inverse() -> InvWaveletDecompose

Return the transform that reconstructs what this one decomposes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
211
212
213
214
215
def get_inverse(self) -> "InvWaveletDecompose":
    """Return the transform that reconstructs what this one decomposes."""
    return InvWaveletDecompose(
        self.wavelet, self.ndim, self.mode, self.stack_dim, self.sizes
    )

make_params

make_params(flat_inputs: list[Any]) -> dict[str, Any]

Record the spatial shape of the first tensor, shared by every leaf.

Source code in src/chuchichaestli/data/transforms/wavelet.py
195
196
197
198
199
200
def make_params(self, flat_inputs: list[Any]) -> dict[str, Any]:
    """Record the spatial shape of the first tensor, shared by every leaf."""
    x = next(i for i in flat_inputs if isinstance(i, torch.Tensor))
    shape = tuple(int(n) for n in x.shape[-self.ndim :])
    self._record(shape)
    return {"sizes": shape}

revert

revert(x: Any, params: dict[str, Any] | None = None) -> Any

Reconstruct the trailing spatial axes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
207
208
209
def revert(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Reconstruct the trailing spatial axes."""
    return self._reconstruct(x)

transform

transform(
    x: Any, params: dict[str, Any] | None = None
) -> Any

Decompose the trailing spatial axes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
202
203
204
205
def transform(self, x: Any, params: dict[str, Any] | None = None) -> Any:
    """Decompose the trailing spatial axes."""
    self._record(tuple(int(n) for n in x.shape[-self.ndim :]))
    return self._decompose(x)

WaveletPacket

WaveletPacket(
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    levels: int = 2,
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
)

Bases: WaveletDecompose

WaveletDecompose that keeps splitting every subband, not only the coarsest.

A packet decomposition of l levels leaves 2 ** (ndim * l) subbands, all at the same resolution, which is what makes the result a single tensor.

Constructor.

Parameters:

Name Type Description Default
wavelet str | Wavelet

Wavelet to decompose with, by name or as a Wavelet.

'haar'
ndim int

Number of trailing spatial dims to transform.

2
mode ExtensionModeTypes

Signal extension mode.

'zero'
levels int

Number of levels to decompose.

2
stack_dim int | None

Position of the subband axis in the result.

None
sizes Sequence[int] | None

Spatial shape the coefficients revert to, if it is known.

None

Raises:

Type Description
ValueError

If levels is not positive.

Methods:

Name Description
get_inverse

Return the transform that reconstructs what this one decomposes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def __init__(
    self,
    wavelet: str | Wavelet = "haar",
    ndim: int = 2,
    mode: ExtensionModeTypes = "zero",
    levels: int = 2,
    stack_dim: int | None = None,
    sizes: Sequence[int] | None = None,
):
    """Constructor.

    Args:
        wavelet: Wavelet to decompose with, by name or as a `Wavelet`.
        ndim: Number of trailing spatial dims to transform.
        mode: Signal extension mode.
        levels: Number of levels to decompose.
        stack_dim: Position of the subband axis in the result.
        sizes: Spatial shape the coefficients revert to, if it is known.

    Raises:
        ValueError: If `levels` is not positive.
    """
    super().__init__(wavelet, ndim, mode, stack_dim, sizes)
    if levels < 1:
        raise ValueError(f"A decomposition needs at least one level; got {levels}.")
    self.levels = levels

get_inverse

get_inverse() -> InvWaveletPacket

Return the transform that reconstructs what this one decomposes.

Source code in src/chuchichaestli/data/transforms/wavelet.py
275
276
277
278
279
280
281
282
283
284
def get_inverse(self) -> "InvWaveletPacket":
    """Return the transform that reconstructs what this one decomposes."""
    return InvWaveletPacket(
        self.wavelet,
        self.ndim,
        self.mode,
        self.levels,
        self.stack_dim,
        self.sizes,
    )

ZScale

ZScale(
    interval: ZScaleInterval | None = None,
    feature_range: tuple[float, float] = (0.0, 1.0),
)

Bases: Transform

Adaptive IRAF zscale normalization: clip to zscale limits, then rescale.

Constructor.

Parameters:

Name Type Description Default
interval ZScaleInterval | None

The ZScaleInterval computing (z1, z2) (default: one with IRAF defaults).

None
feature_range tuple[float, float]

Target (low, high) range after clipping.

(0.0, 1.0)

Methods:

Name Description
make_params

Compute the shared (z1, z2) limits from the first tensor.

transform

Clip to [z1, z2] and rescale into feature_range.

Source code in src/chuchichaestli/data/transforms/intensity.py
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
def __init__(
    self,
    interval: ZScaleInterval | None = None,
    feature_range: tuple[float, float] = (0.0, 1.0),
):
    """Constructor.

    Args:
        interval: The `ZScaleInterval` computing `(z1, z2)` (default: one with
            IRAF defaults).
        feature_range: Target `(low, high)` range after clipping.
    """
    super().__init__()
    self.interval = interval or ZScaleInterval()
    self.feature_range = feature_range

make_params

make_params(flat_inputs: list[Any]) -> dict[str, Any]

Compute the shared (z1, z2) limits from the first tensor.

Source code in src/chuchichaestli/data/transforms/intensity.py
460
461
462
463
464
def make_params(self, flat_inputs: list[Any]) -> dict[str, Any]:
    """Compute the shared `(z1, z2)` limits from the first tensor."""
    x = next(i for i in flat_inputs if isinstance(i, torch.Tensor))
    z1, z2 = self.interval.get_limits(x)
    return {"z1": z1, "z2": z2}

transform

transform(x: Any, params: dict[str, Any]) -> Any

Clip to [z1, z2] and rescale into feature_range.

Source code in src/chuchichaestli/data/transforms/intensity.py
466
467
468
469
470
def transform(self, x: Any, params: dict[str, Any]) -> Any:
    """Clip to `[z1, z2]` and rescale into `feature_range`."""
    z1, z2 = params["z1"], params["z2"]
    x = Clamp(z1, z2).transform(x)
    return MinMaxScale(z1, z2, self.feature_range).transform(x)

ZScaleInterval

ZScaleInterval(
    nsamples: int = 1000,
    contrast: float = 0.25,
    max_reject: float = 0.5,
    min_npixels: int = 5,
    krej: float = 2.5,
    max_iterations: int = 5,
)

Compute IRAF zscale display limits (z1, z2) from a tensor.

Samples the data, fits a line to the sorted samples with iterative k-sigma rejection, and derives cut limits about the median (see IRAF/astropy ZScaleInterval).

Constructor.

Parameters:

Name Type Description Default
nsamples int

Number of pixels to sample from the input.

1000
contrast float

Scales the fitted slope; smaller means higher contrast.

0.25
max_reject float

Minimum fraction of samples that must survive rejection for the fit to be used (IRAF convention; despite the name).

0.5
min_npixels int

Minimum number of surviving samples for a valid fit.

5
krej float

k-sigma rejection threshold on the fit residuals.

2.5
max_iterations int

Maximum number of rejection iterations.

5

Methods:

Name Description
get_limits

Return the (z1, z2) cut limits for x.

Source code in src/chuchichaestli/data/transforms/intensity.py
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
def __init__(
    self,
    nsamples: int = 1000,
    contrast: float = 0.25,
    max_reject: float = 0.5,
    min_npixels: int = 5,
    krej: float = 2.5,
    max_iterations: int = 5,
):
    """Constructor.

    Args:
        nsamples: Number of pixels to sample from the input.
        contrast: Scales the fitted slope; smaller means higher contrast.
        max_reject: Minimum fraction of samples that must survive
            rejection for the fit to be used (IRAF convention;
            despite the name).
        min_npixels: Minimum number of surviving samples for a valid fit.
        krej: k-sigma rejection threshold on the fit residuals.
        max_iterations: Maximum number of rejection iterations.
    """
    self.nsamples = nsamples
    self.contrast = contrast
    self.max_reject = max_reject
    self.min_npixels = min_npixels
    self.krej = krej
    self.max_iterations = max_iterations

get_limits

get_limits(
    x: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]

Return the (z1, z2) cut limits for x.

Source code in src/chuchichaestli/data/transforms/intensity.py
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
def get_limits(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
    """Return the `(z1, z2)` cut limits for `x`."""
    values = x.flatten()
    values = values[torch.isfinite(values)]
    stride = int(max(1.0, values.numel() / self.nsamples))
    samples = torch.sort(values[::stride][: self.nsamples])[0].float()
    npix = samples.numel()
    vmin, vmax = samples[0], samples[-1]
    if npix < self.min_npixels:
        return vmin, vmax

    minpix = max(self.min_npixels, int(npix * self.max_reject))
    ngrow = max(1, int(npix * 0.01))
    idx = torch.arange(npix, dtype=samples.dtype, device=samples.device)
    badpix = torch.zeros(npix, dtype=torch.bool, device=samples.device)
    slope = torch.zeros((), dtype=samples.dtype, device=samples.device)
    ngoodpix, last_ngoodpix = npix, npix + 1
    for _ in range(self.max_iterations):
        if ngoodpix >= last_ngoodpix or ngoodpix < minpix:
            break
        good = ~badpix
        slope, intercept = _lstsq_line(idx[good], samples[good])
        resid = samples - (slope * idx + intercept)
        # population std (ddof=0), as in IRAF/`astropy`; torch would
        # otherwise default to the sample std (correction=1)
        threshold = self.krej * resid[good].std(correction=0)
        badpix = _grow(badpix | (resid.abs() > threshold), ngrow)
        last_ngoodpix, ngoodpix = ngoodpix, int((~badpix).sum())

    if ngoodpix < minpix:
        return vmin, vmax
    if self.contrast > 0:
        slope = slope / self.contrast
    center = (npix - 1) // 2
    median = samples.median()
    z1 = torch.maximum(vmin, median - center * slope)
    z2 = torch.minimum(vmax, median + (npix - 1 - center) * slope)
    return z1, z2