Skip to content

Diagnostic Normalisation

MIF-016 bounds dirty diagnostic channels before they enter AER encoding. Each calibrated channel declares its physical unit interval, affine offset, scale, clip policy, provenance, and optional AER address. Samples are mapped into [-1, 1] by

\[ x_\mathrm{norm} = 2 \frac{x - x_\min}{x_\max - x_\min} - 1. \]

The runtime never leaves out-of-range handling implicit. clip saturates at the nearest endpoint and records the channel in the clip mask; reject fails closed with a deterministic error. Every output vector is a read-only float64 array bounded in [-1, 1].

Calibration validation also checks the derived affine coefficients, not only the raw endpoint values. A finite endpoint pair is rejected if the physical span or scale would become non-finite. The midpoint is computed as x_min + 0.5 * (x_max - x_min) so finite endpoints with a finite positive span keep a finite offset instead of overflowing through x_min + x_max.

Python API

normalisation

Bound physical diagnostic channels before AER encoding (MIF-016).

Each channel is mapped from its calibrated physical interval [physical_min, physical_max] into [-1, 1] using the affine map

.. math::

x_\mathrm{norm} = 2 \frac{x - x_\min}{x_\max - x_\min} - 1.

Out-of-range behaviour is explicit per channel: clip saturates deterministically at the endpoint and records a clip mask, while reject raises. The resulting feature vectors are read-only float64 NumPy arrays so downstream AER front-ends cannot observe overflow beyond [-1, 1]. Finite endpoint pairs are also rejected when the derived affine span or scale would be non-finite. The stable midpoint offset remains finite whenever both endpoints and the span are finite.

DiagnosticChannelCalibration(name, unit, physical_min, physical_max, clip_policy, provenance, aer_address=None) dataclass

Calibration record for one physical diagnostic channel.

Parameters are deliberately stored in physical units. The derived offset and scale properties are included in manifests so an AER ingestion chain can reproduce the exact affine mapping without inferring it from opaque data.

offset property

Physical midpoint subtracted before applying scale.

scale property

Multiplicative factor from physical units into the normalized interval.

__post_init__()

Validate channel labels, physical range, clip policy, and AER address.

normalise_value(value)

Return (normalised_value, clipped) for a single channel sample.

to_manifest_row()

Return the durable manifest row for this channel.

NormalisedDiagnosticSample(channel_names, features, clip_mask, out_of_range_channels, sample_period_ns=None) dataclass

Read-only normalised diagnostic vector plus clipping metadata.

__post_init__()

Freeze the normalised features and validate masks and channel metadata.

to_aer_features()

Return the bounded feature vector consumed by the AER front-end.

NormalisedDiagnosticMatrix(channel_names, features, clip_mask, clipped_counts, sample_period_ns=None) dataclass

Read-only samples x channels normalisation result with aggregate clipping.

The throughput counterpart of :class:NormalisedDiagnosticSample: one result for a positional row-major batch, carrying per-channel clipped-sample counts instead of per-row channel-name lists.

Attributes

channel_names : tuple of str Ordered channel names matching each feature row. features : FloatArray Read-only (samples, channels) bounded feature matrix. clip_mask : numpy.ndarray Read-only (samples, channels) boolean clip mask. clipped_counts : tuple of int Per-channel count of clipped samples across the batch. sample_period_ns : int or None Optional nominal sample period carried from the state.

samples property

Number of samples (rows) in the batch.

__post_init__()

Freeze the matrices and validate shapes against the channel metadata.

DiagnosticNormalisationState(calibrations, *, sample_period_ns=None)

Deterministic ordered normalisation state for a diagnostic vector.

calibrations property

Ordered immutable channel calibrations.

channel_names property

Ordered channel names matching feature-vector order.

sample_period_ns property

Nominal sample period associated with the diagnostic frame.

normalise_sample(sample)

Normalise a mapping of physical channel samples into [-1, 1].

normalise_vector(values)

Normalise a positional vector in calibration order.

normalise_batch(samples)

Return a read-only (n_samples, n_channels) matrix of bounded features.

normalise_matrix(values)

Normalise a positional row-major samples x channels matrix in one call.

The positional counterpart of :meth:normalise_batch and the batch counterpart of :meth:normalise_vector: each row is normalised with exactly the per-sample arithmetic, so a matrix row is bit-identical to the corresponding :meth:normalise_vector features, while clipping is aggregated into per-channel counts.

Parameters

values : ArrayLike Two-dimensional (samples, channels) matrix of raw physical values, at least one row, column count equal to the calibrated channel count.

Returns

NormalisedDiagnosticMatrix The bounded feature matrix, the clip mask, and per-channel clipped-sample counts.

calibration_manifest()

Return the explicit calibration manifest required by MIF-016.

fit_diagnostic_calibrations(observations, *, units, provenance, clip_policy='clip', aer_addresses=None)

Fit min/max calibrations from observed physical samples.

Channel order follows the order of units. Every observation must contain every declared channel, and each channel must span a non-zero physical range.

Manifest Contract

DiagnosticNormalisationState.calibration_manifest() records:

  • schema_version, kernel, sample_period_ns, and output range;
  • one row per channel with physical range, offset, scale, clip policy, provenance, and AER address;
  • deterministic_mapping = true so downstream AER replay can reproduce the exact transform.

Dispatch

Use scpn_mif_core.diagnostics.dispatched_normalisation_state(...) for the fastest available measured backend:

"diagnostics.normalisation" = ["rust", "python", "julia"]

The Python reference remains the canonical manifest surface. Rust mirrors the affine kernel through PyO3, and Julia mirrors the reference behaviour for calibration/scaling audit scripts.

Matrix batch surface

normalise_matrix(values) is the positional row-major batch counterpart of normalise_vector: one call normalises a (samples, channels) matrix and returns a NormalisedDiagnosticMatrix — the bounded feature matrix, the clip mask, and per-channel clipped-sample counts (aggregates, not per-row channel-name lists). Each matrix row is bit-identical to the corresponding normalise_vector features. On the Rust backend the whole matrix crosses the FFI boundary once as a zero-copy 2-D NumPy view and the outputs come back as NumPy arrays, so the per-call bridge overhead of the 4 096-call loop disappears: median 128 µs for the 4 096×4 batch versus 3.18 ms through the per-call loop and 37.5 ms for the Python matrix reference (non-isolated local comparison, 2026-07-04).

Validation

The committed tests verify:

  • exact affine mapping and manifest fields;
  • deterministic clipping and bounded AER features;
  • reject-policy failure semantics;
  • invalid range, non-finite endpoint, non-finite affine-span, subnormal-scale, missing-channel, and zero-span fit guards;
  • Python/Rust parity across 16 seeded random vectors;
  • matrix rows bit-identical to single-vector calls on both backends, matrix Python↔Rust bit-exact parity with agreeing clipped counts, read-only outputs, and shape/reject failure semantics;
  • Julia reference behaviour in julia/SCPNMIFCore/test/runtests.jl.

End-to-end ControlObservation cosimulation remains downstream of MIF-015. This surface supplies the bounded feature vector and manifest required by that later integration.

Benchmarks

The benchmark harness ships at bench/kernels/bench_diagnostic_normalisation.py. It measures one four-channel frame and a 4 096-frame batch for the allocated Python, Rust, and Julia surfaces. The committed benchmark is local comparison evidence, not CPU-isolated production latency evidence; host load, governor, and runtime versions are recorded in bench/results/diagnostic_normalisation.json.