specsr.metrics

Residual and power-spectrum statistics used by the paper figures.

Computation lives here; specsr.plotting draws it. That split is the point: a number that goes in the text should be obtainable without importing matplotlib or rendering anything, and it should be testable.

Extracted from the evaluation notebooks, where several of these were redefined in three separate cells with slightly different names (_nan_moving_average_1d / nan_moving_average_1d) — one of the ways a notebook quietly ends up running a different function than the one you read.

Functions

compute_psd_stats(wavelength, R[, ...])

PSD of a residual map: median, 16-84 band, and high-frequency fraction.

doublet_dip_depth(flux, wavelength, z[, ...])

How resolved a close doublet is: 1 = two clean peaks, 0 = one blended bump.

estimate_median_noise_lambda(X[, ...])

Median per-wavelength noise, estimated from high-frequency scatter.

moving_average_rows(X, win)

NaN-aware moving average along each row.

rank_doublet_examples(wavelength, flux_lr, ...)

Rank spectra by how well they demonstrate the doublet being resolved.

redshift_metrics(z_true, z_pred[, ...])

Redshift-recovery summaries, defined once for the whole codebase.

robust_sigma_lambda(R[, min_count, clip_sigma])

Per-wavelength robust scatter of a residual map, via clipped MAD.

robust_symmetric_vlim(residual_arrays[, ...])

Symmetric colour limits at a percentile of |residual|.

specsr.metrics.redshift_metrics(z_true, z_pred, outlier_thresh=0.15)[source]

Redshift-recovery summaries, defined once for the whole codebase.

Used by the redshift head’s training loop, by the W&B sweeps that rank its hyperparameters, and by the paper figure. That matters: both zhead sweeps previously optimised val_med_abs_dz_over_1pz, a key the training loop never logged, so 75 Bayes trials ran with nothing to rank them by. A metric the paper quotes and a metric the sweep optimises must be the same function.

Two robust scatters are returned and they are not interchangeable:

nmad

1.4826 * median(|dz|) on the raw residual. This is what the published figure’s “NMAD” box reports.

sigma_nmad

the same statistic on dz/(1+z). This is the usual photo-z convention and is a smaller number.

Quote whichever you like, but say which one — they differ by roughly a factor of 1+z, which is a factor of a few at these redshifts.

Parameters:

outlier_thresh (float)

Return type:

dict[str, float]

specsr.metrics.compute_psd_stats(wavelength, R, detrend_win=101, use_hann=True)[source]

PSD of a residual map: median, 16-84 band, and high-frequency fraction.

Parameters:
  • wavelength – Uniformly spaced grid; only the spacing is used, so the frequency axis is cycles per unit of whatever coordinate this is.

  • R(n_spectra, n_lambda) residual map.

  • detrend_win (int) – Remove slow structure per spectrum first. Without it the PSD is dominated by the continuum shape and says nothing about the fine structure the figure is about.

  • use_hann (bool)

Returns:

  • (freq, p50, p16, p84, hf_frac). hf_frac is the median fraction of

  • power above 0.3 x Nyquist – a single number for “how grainy”, which is what

  • lets three curves be compared in a legend.

specsr.metrics.doublet_dip_depth(flux, wavelength, z, lam1=0.4959, lam2=0.5007)[source]

How resolved a close doublet is: 1 = two clean peaks, 0 = one blended bump.

Measures the valley between the two lines relative to the weaker peak, above a local continuum taken from sidebands outside the pair. This is the quantity the paper’s spectrum figure is chosen to display — whether the [O III] doublet, unresolved at prism resolution, is separated after super-resolution.

Returns (depth, peak1, peak2), or None if the pair is off the grid or there is no positive signal to measure.

Parameters:
specsr.metrics.estimate_median_noise_lambda(X, smooth_win=71, local_win=31, min_count_per_lambda=80)[source]

Median per-wavelength noise, estimated from high-frequency scatter.

Smooths each spectrum, takes the residual against its own smooth version as a noise proxy, and reports the median across spectra. This measures the data’s noise without needing the reported uncertainties, which is what makes it a usable reference line on a residual plot.

Parameters:
  • smooth_win (int)

  • local_win (int)

  • min_count_per_lambda (int)

specsr.metrics.moving_average_rows(X, win)[source]

NaN-aware moving average along each row.

Gaps are skipped rather than treated as zero: the denominator counts only finite samples, so a masked detector region does not drag the local mean toward zero and invent a fake absorption trough.

Parameters:

win (int)

specsr.metrics.robust_sigma_lambda(R, min_count=30, clip_sigma=6.0)[source]

Per-wavelength robust scatter of a residual map, via clipped MAD.

MAD rather than standard deviation because emission-line residuals are heavy-tailed: a few galaxies with a strong line at one wavelength would dominate an unclipped estimate and read as an instrumental feature.

Returns NaN at wavelengths with fewer than min_count finite samples, rather than a number computed from too little data.

Parameters:
specsr.metrics.rank_doublet_examples(wavelength, flux_lr, flux_sr, flux_hr, z, *, z_pred=None, hr_min=0.35, lr_max=0.2, sr_min=0.4, amp_percentile=40.0, max_peak_offset=0.12, max_dz_over_1pz=0.02, amp_ratio_range=(0.7, 1.3))[source]

Rank spectra by how well they demonstrate the doublet being resolved.

Selects objects where the doublet is genuinely separated in the HR reference, blended in the LR input, and separated again in the super-resolved output — so the figure shows the model recovering structure that is really there, rather than a case where the reference was already ambiguous.

Position is checked, not just separation. SR2 conditions its line branch on the predicted redshift, so when that prediction is wrong it emits a well-separated doublet at the wrong wavelength. Ranking on dip depth alone selects exactly those cases: two crisp peaks that are not where the lines are. max_peak_offset requires each SR peak to land within a fraction of the doublet separation of its true position, and max_dz_over_1pz optionally rejects objects whose redshift is badly predicted.

Amplitude is checked for the same reason. This function used to score on the SR peak height, which rewards emitting hard: the top-ranked example on the released model over-emitted [O III] 5007 by 18.8x, the 99.7th percentile of the held-out set, where the median galaxy sits at 0.55. It made a figure whose HR reference looked like a flat line beside the model’s output — an unrepresentative worst case presented as the headline illustration. amp_ratio_range bounds the SR/HR peak ratio, and the score now ranks on the HR line brightness, which is what makes an example legible on the page without rewarding the model for overshooting.

Both components are checked, not just the bright one. The ratio bound used to be applied to 5007 alone, so a panel could pass with a faithful 5007 beside a 4959 at half the reference height or displaced most of the way to its neighbour – which is what the previously selected examples showed, and what a reader looking at the doublet notices first. amp_ratio_range and max_peak_offset now bind on 4959 and 5007 alike, and the score is divided by the total amplitude error so that, among examples that pass, the most faithful ranks highest rather than merely the most separated.

Note this selects examples that are faithful in amplitude; it is an illustration, not a measurement. The distribution of SR/HR line flux is the quantity to quote, not these panels.

Returns indices, best first.

Parameters:
specsr.metrics.robust_symmetric_vlim(residual_arrays, vmax_abs_pct=97.0)[source]

Symmetric colour limits at a percentile of |residual|.

Symmetric because these are signed residuals and a diverging colormap must put zero at the centre; percentile-based because a handful of bad pixels would otherwise set the scale and flatten everything else to white.

Parameters:

vmax_abs_pct (float)