Source code for diffBloch.engine.losses
"""Named ``LossFn``/``ScoresFn`` builders: the objective terms ``ExperimentConfig.objective`` picks.
Each adapts a pure :mod:`diffBloch.core.losses` intensity comparison (which reduces over the
reflection axis, yielding a ``(T,)`` per-thickness loss) into two shapes: a ``ScoresFn``
(``AlignedIntensities -> (T,) Tensor``, one score per thickness) and the corresponding ``LossFn``
(``AlignedIntensities -> scalar``, the ``ScoresFn`` summed over thickness). ``RefinementEngine``
uses both off the *same* underlying metric -- ``scores`` for the orientation/thickness
preprocessing search (``score_orientation``/``score_orientation_per_thickness``, which need a
per-thickness vector to argmin over) and ``loss`` for the differentiable gradient-refinement
objective -- so picking one ``ExperimentConfig.objective`` genuinely drives the whole pipeline, not
just the gradient stage. Saves every caller from rewriting
``lambda a: mse(a.calculated, a.observed).sum()`` and gives each objective a named, importable home
-- e.g. ``RefinementEngine(loss=rbragg_loss, scores=robs_scores, ...)``.
"""
from __future__ import annotations
import torch
from torch import Tensor
from diffBloch.core.losses import l1, mse, optimal_scale, rbragg, w_rbragg
from diffBloch.core.products import AlignedIntensities
__all__ = [
"l1_loss",
"mse_loss",
"rbragg_loss",
"robs_scores",
"wr2_loss",
"wr2_scores",
"w_rbragg_loss",
]
[docs]
def mse_loss(aligned: AlignedIntensities) -> Tensor:
"""Per-orientation MSE loss term, summed over thicknesses to a scalar."""
return mse(aligned.calculated, aligned.observed).sum()
[docs]
def l1_loss(aligned: AlignedIntensities) -> Tensor:
"""Per-orientation L1 loss term, summed over thicknesses to a scalar."""
return l1(aligned.calculated, aligned.observed).sum()
[docs]
def w_rbragg_loss(aligned: AlignedIntensities) -> Tensor:
"""Per-orientation weighted-R2 term (default ``mu``), summed over thicknesses to a scalar.
Raw: no calc<->obs scaling. Correct only where the caller has already put calculated on the
observed scale; for the refinement objective use :func:`wr2_loss`, which is the
:func:`~diffBloch.preprocess.scoring.build_engine` default.
"""
return w_rbragg(aligned.calculated, aligned.observed, aligned.sigmas).sum()
[docs]
def wr2_scores(aligned: AlignedIntensities) -> Tensor:
"""Per-thickness scaling-optimised weighted-R2 (shape ``(T,)``).
The calculated intensities come off the dynamical solve on an arbitrary structure-factor scale,
while the observed are PETS intensities on their own scale. Compared raw
(:func:`w_rbragg_loss`), wR2 is denominator-dominated and parks near ~1 with a vanishing
gradient, so a gradient refinement cannot descend it. Every call therefore re-fits the
multiplicative intensity scale independently for every thickness through
:func:`~diffBloch.core.losses.optimal_scale`. The selected grid branch remains differentiable
in its calculated intensities (``torch.min`` routes the gradient through the winning
candidate); only a boundary where the winning grid point changes is piecewise-smooth.
"""
calc, obs = aligned.calculated, aligned.observed
return torch.stack(
[optimal_scale(calc[t], obs[t], aligned.sigmas[t])[1] for t in range(calc.shape[0])]
)
[docs]
def wr2_loss(aligned: AlignedIntensities) -> Tensor:
"""Scaling-optimised weighted-R2 -- the default refinement and orientation-search objective.
Sums :func:`wr2_scores` over the thickness axis to a scalar; see there for the scale-fit.
"""
return wr2_scores(aligned).sum()
[docs]
def robs_scores(aligned: AlignedIntensities) -> Tensor:
"""Per-thickness scaling-optimised Bragg R(obs) (shape ``(T,)``).
Refits the intensity scale per thickness exactly like :func:`wr2_scores`, but against the
:func:`~diffBloch.core.losses.rbragg` metric instead of the default ``w_rbragg`` -- the same
R_obs the app reports elsewhere (:meth:`~diffBloch.engine.forward.RefinementEngine.refinement_metrics`).
"""
calc, obs = aligned.calculated, aligned.observed
return torch.stack(
[
optimal_scale(calc[t], obs[t], aligned.sigmas[t], metric=rbragg)[1]
for t in range(calc.shape[0])
]
)
[docs]
def rbragg_loss(aligned: AlignedIntensities) -> Tensor:
"""Scaling-optimised Bragg R(obs) objective. Sums :func:`robs_scores` over thickness."""
return robs_scores(aligned).sum()