Adapters¶
Use metrics from other libraries as SR-Forge metrics.
A metric from torchmetrics (and, later, other libraries) can be named
directly in a config::
_target: torchmetrics.image.PeakSignalNoiseRatio
params:
data_range: 1.0
and it arrives in the training loop as an ordinary :class:~srforge.metrics.Metric.
Nothing in the YAML mentions an adapter; :func:adapt is applied by the
config resolver to whatever comes out of _target, and returns anything it
does not recognise untouched.
The reasoning, the measurements behind the reduction policy, and the rejected
alternatives are in docs/design/external-metric-adapters-rfc.md.
ExternalMetric
¶
Bases: Metric
A :class:Metric that delegates to a metric object from another library.
The foreign metric is held, never inherited from. Metric is already
nn.Module + IOModule + ABC and torchmetrics' Metric is itself an
nn.Module with its own state machinery; inheriting from both is how
you get an object whose .to(device) half-works. Assigning it to
self.inner still registers it as a submodule, so device movement and
state_dict propagate — composition keeps the two lifecycles separate
without losing that.
Subclasses provide :meth:detects, :meth:_direction, :meth:_arity
and :meth:_call. Weight, name, IO binding and MetricScores
packaging all come from Metric unchanged.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inner
|
Any
|
The foreign metric object. |
required |
weight
|
float
|
Defaults to |
1.0
|
arity: str
property
¶
"FR" or "NR" — what the wrapped metric declared.
detects(obj: Any) -> bool
staticmethod
¶
Whether this adapter handles obj. Imports inside the body.
available() -> bool
classmethod
¶
Whether this adapter's library is loaded.
Checks sys.modules rather than importing: if the library has not
been imported, no object from it can exist, so it cannot be the thing
being adapted. This keeps :func:adapt free of imports on the common
path and costs nothing when the library is absent.
calculate_score(x: torch.Tensor, y: torch.Tensor = None) -> torch.Tensor
¶
Score a batch, returning [B]: one value per image.
Libraries differ in what they return for a batch. pyiqa, and a
torchmetrics metric built with reduction='none', return one value
per image: used as is, from one call. Most torchmetrics metrics pool
the batch into a single number, which is no image's score, so each
image is scored in a call of its own. Which kind a metric is shows on
its first batch of more than one image and is remembered — that batch
is the only one scored twice.
Nothing here is configurable, because nothing is a choice: a score
per image is the Metric contract, and the pooled number is what
aggregate="micro" gives.
TorchMetricAdapter
¶
Bases: ExternalMetric
Adapts a torchmetrics.Metric to the SR-Forge Metric interface.
torchmetrics usually pools a whole batch into one number. aggregate
decides what the epoch value means:
========================== =============================================
aggregate="macro" per image, averaged — every image gets its
(default) own score, exactly as a native SR-Forge metric
reports. A metric that pools is called once
per image for it; one built with
reduction='none' already returns a value
per image and is called once per batch.
aggregate="micro" pooled — the metric's own counts are
totalled (over the batch for training, over
every batch and process for the epoch), then
its own compute() runs once. The number
torchmetrics would give had it seen the whole
epoch at once. One call per batch.
========================== =============================================
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
**kwargs
|
Any
|
Passed to :class: |
{}
|
How micro works, and why not the obvious way. The obvious way is
to let torchmetrics keep its state across batches and call compute()
at the end. That fails three ways: nothing tells a metric that an epoch
started, so epoch 2 would include epoch 1; under DDP the counts would be
summed across ranks by SR-Forge and then synced again by torchmetrics;
and until now the option did exactly that — kept state nobody read.
Instead each batch resets the metric, updates it, and hands its state
(tp/fp/fn/tn, a confusion matrix, a sum of squared errors)
to SR-Forge as the parts of this metric, declared with the reductions
torchmetrics itself registered for them. SR-Forge totals them the usual
way — correctly across ranks — and :meth:finalize loads the totals back
and calls the metric's own compute. The wrapped metric is stateless
between calls.
PyIQAAdapter
¶
Bases: ExternalMetric
Adapts a pyiqa metric to the SR-Forge Metric interface.
pyiqa is where SR-Forge's no-reference metrics come from — it ships NIQE, PIQE and around sixty others, and torchmetrics has none at all. Reach one through its factory::
_target: pyiqa.create_metric
params:
metric_name: niqe
Unlike torchmetrics, pyiqa declares whether each metric needs a reference
(metric_mode), so a no-reference metric binds only x and is never
asked for a reference image that does not exist. It also returns one
value per image, so every batch is scored in a single call.
A metric's prior is not SR-Forge's judgement. NIQE scores an image by its distance from statistics fitted to roughly 125 natural photographs, and pyiqa's pipeline converts RGB to luma before measuring. Neither assumption holds for multispectral, hyperspectral, SAR or Bayer data — on SAR especially, speckle reads as maximally unnatural, so the score behaves as a speckle meter and a denoiser that destroys real texture will appear to improve it. Adapting rather than reimplementing is what keeps that judgement with the user who picks the metric. See §7 of docs/design/external-metric-adapters-rfc.md.
register_adapter(*, library: str, module: str, install: str)
¶
Register an adapter class for a foreign metric library.
One registry drives both dispatch and the "you need to install X" message, so the set of libraries SR-Forge claims to support and the set it can actually adapt cannot drift apart.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
library
|
str
|
Human-readable name used in messages. |
required |
module
|
str
|
Top-level importable module name, used for availability. |
required |
install
|
str
|
The command that installs it. |
required |
known_adapters() -> Iterable[type['ExternalMetric']]
¶
Every registered adapter, in registration order.
library_for(obj: Any) -> str | None
¶
Name the library whose adapter would handle obj, or None.
Asks the same detectors :func:adapt dispatches on, without building
anything. Used to turn "that is not a Metric" into "that is a torchmetrics
metric, here is the one call that fixes it".
adapt(obj: Any, **options: Any) -> Any
¶
Return obj as a :class:Metric, wrapping it if it comes from a
library SR-Forge knows how to adapt.
Anything that is already a Metric — and anything no adapter claims —
is returned unchanged and identical, so this is safe to call on
every object the resolver builds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
obj
|
Any
|
The object to adapt. |
required |
**options
|
Any
|
Forwarded to the adapter's constructor ( |
{}
|
missing_library_report(module: str, target: str) -> str | None
¶
Explain a failed _target import when it names an adaptable library.
Returns None when module is nothing SR-Forge knows how to adapt, so
the caller can fall back to its ordinary error. Built from the same
registry that dispatches, so the list of supported libraries cannot claim
something the code does not actually do.