Follow-up from the review of #544. Not a defect in that PR — it is correct as written — but it leaves a shared invariant maintained in two places, and the seam it pulls on is worth tidying separately.
The duplicated invariant
#544 makes SQM photometry geometry scale-aware: radii are defined in solve-image (512px) pixels and converted to the raw photometry image's own pitch by the centroid scale. That conversion is now implemented twice, independently:
PiFinder/solver.py — _scaled_photometry_radii():
aperture = max(1, round(aperture_radius * scale))
PiFinder/sqm/wings.py — WingEstimator.set_scale():
aperture = max(1, round(self._base_aperture_radius * scale))
The wing correction is only meaningful if the WingEstimator's aperture is the same radius as the aperture the photometry actually integrates over — it measures the enclosed-flux fraction f for that specific aperture, and mzero is corrected by -2.5*log10(f). That equality is currently held by two copies of one expression living in two modules, with no test asserting they agree. Today they do agree. A future change to the rounding or clamping in one (say, a floor bump for very small scales) silently desyncs the correction from the measurement, and nothing fails loudly — the SQM just drifts.
The widening private seam
Related, and the reason this is worth doing as one change rather than a one-line dedupe: PiFinder/ui/sqm_calibration.py reaches into solver for underscore-prefixed helpers, and #544 adds a fourth:
from PiFinder.solver import (
_derotate_centroids,
_extract_raw_photometry_image,
_scale_solution_centroids,
_scaled_photometry_radii, # added by #544
)
These four are a coherent unit — "given a raw frame and a solution, produce a derotated raw photometry image plus the centroids and radii to measure on it." That is SQM geometry, not solver internals, and both the solver process and the calibration wizard are legitimate consumers. The leading underscores are now telling readers the opposite of the truth.
Suggested shape
Move the geometry unit into a public module, e.g. PiFinder/sqm/geometry.py:
photometry_image(raw, profile)
centroid_scale(...) / derotate_centroids(...) / scale_solution_centroids(...)
scaled_photometry_radii(scale, ...) — with type hints; the current helper has none while the rest of sqm/ is typed
Then have WingEstimator.set_scale() derive its aperture from scaled_photometry_radii() rather than recomputing it, so the equality is structural instead of coincidental. solver.py and ui/sqm_calibration.py both import from the public module.
Minimum viable version if the full move is more churn than wanted: keep the helpers where they are, but have set_scale() call the solver helper for its aperture, and add a test pinning
WingEstimator(...).aperture_radius == scaled_photometry_radii(scale)[0]
across the real per-profile scales (imx462 0.957, hq 1.484, imx296 2.125). That closes the actual risk; the module move is the cleanup.
While in here: glossary drift
docs/ax/sqm/CONTEXT.md still defines these as fixed pixel counts:
Aperture: Circular radius-5-pixel region whose sky-subtracted sum is stellar flux.
Annulus: The local sky ring from radius 10 through 18 pixels.
After #544 those numbers are solve-image pixels scaled by the centroid scale — on the imx296 the real values are 11 / 21 / 38. The glossary should say the radii are expressed in solve-image pixels and converted per sensor, so the vocabulary keeps matching the code.
Not urgent
No user-visible symptom today, and no behaviour change intended by this issue — it is a consolidation. Best done after #544 merges, since it builds directly on the helper that PR introduces.
Follow-up from the review of #544. Not a defect in that PR — it is correct as written — but it leaves a shared invariant maintained in two places, and the seam it pulls on is worth tidying separately.
The duplicated invariant
#544 makes SQM photometry geometry scale-aware: radii are defined in solve-image (512px) pixels and converted to the raw photometry image's own pitch by the centroid scale. That conversion is now implemented twice, independently:
PiFinder/solver.py—_scaled_photometry_radii():PiFinder/sqm/wings.py—WingEstimator.set_scale():The wing correction is only meaningful if the
WingEstimator's aperture is the same radius as the aperture the photometry actually integrates over — it measures the enclosed-flux fractionffor that specific aperture, andmzerois corrected by-2.5*log10(f). That equality is currently held by two copies of one expression living in two modules, with no test asserting they agree. Today they do agree. A future change to the rounding or clamping in one (say, a floor bump for very small scales) silently desyncs the correction from the measurement, and nothing fails loudly — the SQM just drifts.The widening private seam
Related, and the reason this is worth doing as one change rather than a one-line dedupe:
PiFinder/ui/sqm_calibration.pyreaches intosolverfor underscore-prefixed helpers, and #544 adds a fourth:These four are a coherent unit — "given a raw frame and a solution, produce a derotated raw photometry image plus the centroids and radii to measure on it." That is SQM geometry, not solver internals, and both the solver process and the calibration wizard are legitimate consumers. The leading underscores are now telling readers the opposite of the truth.
Suggested shape
Move the geometry unit into a public module, e.g.
PiFinder/sqm/geometry.py:photometry_image(raw, profile)centroid_scale(...)/derotate_centroids(...)/scale_solution_centroids(...)scaled_photometry_radii(scale, ...)— with type hints; the current helper has none while the rest ofsqm/is typedThen have
WingEstimator.set_scale()derive its aperture fromscaled_photometry_radii()rather than recomputing it, so the equality is structural instead of coincidental.solver.pyandui/sqm_calibration.pyboth import from the public module.Minimum viable version if the full move is more churn than wanted: keep the helpers where they are, but have
set_scale()call the solver helper for its aperture, and add a test pinningacross the real per-profile scales (imx462 0.957, hq 1.484, imx296 2.125). That closes the actual risk; the module move is the cleanup.
While in here: glossary drift
docs/ax/sqm/CONTEXT.mdstill defines these as fixed pixel counts:After #544 those numbers are solve-image pixels scaled by the centroid scale — on the imx296 the real values are 11 / 21 / 38. The glossary should say the radii are expressed in solve-image pixels and converted per sensor, so the vocabulary keeps matching the code.
Not urgent
No user-visible symptom today, and no behaviour change intended by this issue — it is a consolidation. Best done after #544 merges, since it builds directly on the helper that PR introduces.