"""
Predefined, picklable functions for use with :class:`MetadataFunctionTransformer`.
Each function follows the calling convention expected by
``MetadataFunctionTransformer``: ``X`` is the first positional argument and
any additional inputs are named keyword arguments declared in the transformer's
``metadata`` parameter.
These module-level named functions can be pickled by the standard library,
which is important when serialising a pipeline with tools such as
``joblib.dump``.
Examples
--------
>>> import numpy as np
>>> from chemotools.adaptation import MetadataFunctionTransformer
>>> from chemotools.adaptation.functions import subtract_reference
>>>
>>> rng = np.random.default_rng(0)
>>> X = rng.normal(size=(10, 50))
>>> reference = rng.normal(size=(1, 50))
>>>
>>> mft = MetadataFunctionTransformer(
... func=subtract_reference, metadata=("reference",)
... )
>>> X_corrected = mft.fit_transform(X, reference=reference)
"""
import numpy as np
from sklearn.utils.validation import check_array
def _check_metadata(arr, name: str) -> np.ndarray:
"""Validate *arr* as a numeric input for a metadata argument.
Scalars are returned as-is. Array-like inputs are passed through
:func:`sklearn.utils.validation.check_array` with ``ensure_2d=True``,
which raises a ``ValueError`` for 1-D inputs and guides the user to
reshape with ``.reshape(-1, 1)`` (per-sample) or ``.reshape(1, -1)``
(shared).
"""
arr = np.asarray(arr)
if arr.ndim == 0:
return arr # scalar — valid for any function
try:
return check_array(arr, ensure_2d=True, dtype="numeric", input_name=name)
except ValueError as exc:
raise ValueError(f"Invalid metadata argument `{name}`: {exc}") from exc
[docs]
def subtract_reference(X: np.ndarray, reference: float | np.ndarray) -> np.ndarray:
"""Subtract a reference spectrum from every row of ``X``.
A common operation in spectroscopy for blank or solvent subtraction,
background removal, or single-beam to double-beam correction.
Parameters
----------
X : np.ndarray of shape (n_samples, n_features)
Input spectra.
reference : float or np.ndarray
Reference to subtract. Accepted shapes:
* scalar ``float`` — subtracted from every element.
* ``(1, n_features)`` — shared reference broadcast across all samples.
* ``(n_samples, 1)`` — per-sample scalar subtracted from every feature.
* ``(n_samples, n_features)`` — per-sample full spectrum.
1-D inputs are rejected; use ``.reshape(1, -1)`` for a shared
spectrum or ``.reshape(-1, 1)`` for a per-sample scalar.
Returns
-------
X_corrected : np.ndarray of shape (n_samples, n_features)
``X - reference``.
Examples
--------
>>> import numpy as np
>>> from chemotools.adaptation.functions import subtract_reference
>>> X = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
>>> ref = np.array([[0.5, 0.5, 0.5]])
>>> subtract_reference(X, ref)
array([[0.5, 1.5, 2.5],
[3.5, 4.5, 5.5]])
"""
return X - _check_metadata(reference, "reference")
[docs]
def divide_by_reference(X: np.ndarray, reference: float | np.ndarray) -> np.ndarray:
"""Divide every row of ``X`` by a reference spectrum.
Useful for single-beam transmission corrections or ratiometric
normalisation where each sample is divided by a simultaneously
measured reference channel.
Parameters
----------
X : np.ndarray of shape (n_samples, n_features)
Input spectra.
reference : float or np.ndarray
Reference to divide by. Accepted shapes:
* scalar ``float`` — divides every element.
* ``(1, n_features)`` — shared reference broadcast across all samples.
* ``(n_samples, 1)`` — per-sample scalar divisor.
* ``(n_samples, n_features)`` — per-sample full spectrum.
1-D inputs are rejected; use ``.reshape(1, -1)`` for a shared
spectrum or ``.reshape(-1, 1)`` for a per-sample scalar. Zero values
are rejected because division by zero produces non-finite output.
Returns
-------
X_corrected : np.ndarray of shape (n_samples, n_features)
``X / reference``.
Raises
------
ValueError
If ``reference`` contains zero values.
Examples
--------
>>> import numpy as np
>>> from chemotools.adaptation.functions import divide_by_reference
>>> X = np.array([[2.0, 4.0, 6.0], [8.0, 10.0, 12.0]])
>>> ref = np.array([[2.0, 2.0, 2.0]])
>>> divide_by_reference(X, ref)
array([[1., 2., 3.],
[4., 5., 6.]])
"""
reference_checked = _check_metadata(reference, "reference")
if np.any(reference_checked == 0):
raise ValueError("`reference` must not contain zero values.")
return X / reference_checked
[docs]
def scale_by_factor(X: np.ndarray, factor: float | np.ndarray) -> np.ndarray:
"""Multiply every row of ``X`` by a scalar or array factor.
Useful for per-batch or per-sample intensity rescaling, e.g. correcting
for integration time, path-length, or dilution differences.
Parameters
----------
X : np.ndarray of shape (n_samples, n_features)
Input spectra.
factor : float or np.ndarray
Scaling factor. Accepted shapes:
* scalar ``float`` — multiplies every element.
* ``(n_samples, 1)`` — per-sample scalar factor.
* ``(1, n_features)`` — per-feature factor shared across samples.
1-D inputs are rejected; use ``.reshape(-1, 1)`` for per-sample
or ``.reshape(1, -1)`` for per-feature.
Returns
-------
X_scaled : np.ndarray of shape (n_samples, n_features)
``X * factor``.
Examples
--------
>>> import numpy as np
>>> from chemotools.adaptation.functions import scale_by_factor
>>> X = np.array([[1.0, 2.0], [3.0, 4.0]])
>>> scale_by_factor(X, np.array([[2.0], [0.5]]))
array([[2. , 4. ],
[1.5, 2. ]])
"""
return X * _check_metadata(factor, "factor")
[docs]
def add_offset(X: np.ndarray, offset: float | np.ndarray) -> np.ndarray:
"""Add a baseline offset to every row of ``X``.
Useful for correcting instrument drift, dark-current baselines, or
additive scatter effects that have been measured externally.
Parameters
----------
X : np.ndarray of shape (n_samples, n_features)
Input spectra.
offset : float or np.ndarray
Offset to add. Accepted shapes:
* scalar ``float`` — added to every element.
* ``(n_samples, 1)`` — per-sample scalar offset.
* ``(1, n_features)`` — per-feature offset shared across samples.
* ``(n_samples, n_features)`` — per-sample full spectrum.
1-D inputs are rejected; use ``.reshape(-1, 1)`` for per-sample
or ``.reshape(1, -1)`` for per-feature.
Returns
-------
X_shifted : np.ndarray of shape (n_samples, n_features)
``X + offset``.
Examples
--------
>>> import numpy as np
>>> from chemotools.adaptation.functions import add_offset
>>> X = np.array([[1.0, 2.0, 3.0]])
>>> add_offset(X, np.array([[0.1, 0.2, 0.3]]))
array([[1.1, 2.2, 3.3]])
"""
return X + _check_metadata(offset, "offset")