Skip to content

Distortion-fit machinery: scan engine, time-dependent pointing, and committed fit results - #68

Open
jacobdparker wants to merge 22 commits into
mainfrom
feature/distortion-fit-machinery
Open

Distortion-fit machinery: scan engine, time-dependent pointing, and committed fit results#68
jacobdparker wants to merge 22 commits into
mainfrom
feature/distortion-fit-machinery

Conversation

@jacobdparker

@jacobdparker jacobdparker commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Distortion-fit machinery: scan engine, time-dependent pointing, and committed fit results

This PR turns the notebook-era distortion fit into library machinery, adds the fitted time-dependent payload pointing of the 2019 flight, and replaces hardcoded fit numbers with committed, human-readable data files that anyone can regenerate from the flight data.

The fitted model

  • distortion_fit() now loads its per-channel parameters from _data/distortion_reference.ecsv. The reference pointing was re-polished against a deterministic PSF-smoothed correlation merit (401² scene), correcting constant per-channel misregistrations of up to ~2.5 detector pixels left by the original stochastic 201² optimization.
  • distortion_fit(axis_time="time") adds the fitted per-frame payload pointing (one element per Level-1 frame, from _data/distortion_pointing.ecsv). During the flight the payload yaw sweeps monotonically from +3.3″ to −4.4″; pitch and roll drift less. Common-mode registration residuals after this model are ≤0.5 detector pixels across all 30 frames.

model/data blink

The fitting engine

  • esis.optics.fit_distortion_scan() — a derivative-free coordinate search: coarse-to-fine scans of each parameter through the correlation merit, refined with a parabola. Per-channel separable readout (axis_channel) fits all four channels in one pass of coherent scans; coherent=True implements a rigid-payload model; tuple-keyed rounds scan strongly-coupled pairs jointly (grating pitch × instrument pitch, etc.), which independent 1-D scans provably need. Every fit writes its scan curves (scan.json) as an audit trail.
  • fit_distortion_series() now drives the scan engine, fitting every frame independently from a shared reference (parallel over frames with workers).
  • fit_distortion_lsq is removed. scipy.optimize.least_squares under-converges on this merit even with smoothing and tuned steps (at t=0 of the June series it moved 2×10⁻⁵ arcsec before terminating while the true peak was ~4″ away). DistortionResidual remains as the deterministic objective with a note recording the finding; the DE path (fit_distortion) is unchanged.
  • Determinism: system.image() is stochastic only through the global numpy RNG (cell_centers(random=True)); seeding before each evaluation plus a σ=1 px Gaussian PSF on the modeled image only (sigma_psf) makes the merit deterministic and smooth enough to read at the 0.07″ level.

Results as data, not hardcode

  • DistortionParameters.to_file()/from_file() round-trip fit results through plain-text ECSV (units preserved, provenance in the header), so results diff cleanly in review.
  • Regeneration entry points: esis.flights.f1.optics.fit_distortion_reference() (all 9 per-channel dof from the as-built model, from scratch) and fit_distortion_pointing() (the 30-frame series) reproduce the committed files for verification. The series takes ~17 CPU-hours (≈35 min on a large workstation); merits reproduce to ~10⁻⁴ across machines (threaded-reduction floating point).

Bug fix

  • as_built() had a sign error in the measured grating radii of curvature (+597 mm vs the −597.83 mm design convention), making the as-built grating diverge the beam (96 mm RMS spot). Fixed, with a focus regression test. This is also why the fit machinery previously started from design() rather than the measured model.

Docs

  • The distortion-fit tutorial (docs/reports/distortion-fit.ipynb) replaces its least-squares section with the scan engine and ends with the committed-results/blink summary.

🤖 Generated with Claude Code

https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j

jacobdparker and others added 18 commits June 11, 2026 17:41
…`ConvergenceLogger`, porting the distortion-fit machinery out of the notebook.

The hand-built guess/units/bounds lists and positional parameter unpacking
from the distortion notebook are replaced by a named parameter prototype
flattened with `na.pack`/`na.unpack`. Also added
`esis.flights.f1.optics.distortion_fit_bounds()`, recording the bounds of
the ESISI_distortion_optimization_20260213_151715 run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wraps `scipy.optimize.differential_evolution` around `DistortionObjective`
with packed bounds and initial guess, optional `ConvergenceLogger` output,
and returns the best solution as a `DistortionParameters`. Replaces the
per-channel optimization loop in the distortion notebook.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…el`.

When fitting several channels at once with shared parameters, the
correlation and off-target penalty are now computed independently for each
channel and averaged, so per-channel brightness differences do not suppress
the merit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he`.

Avoids repeating the JSOC queries on every fresh session.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Loads the Level-1 frame and the cached AIA scene, starts from the
hard-coded per-channel `distortion_fit()`, couples the physically-shared
parameters (field-stop roll, primary displacement, and pointing; 36 -> 21
degrees of freedom since there is only one field stop, one primary, and one
payload), and polishes all four channels at once with `fit_distortion()`.
`quick = True` runs a minutes-long demonstration fit; set it to `False`
for a production fit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stortion_lsq`, and `fit_distortion_series`.

Deterministic derivative-based alternative to the differential-evolution
polish, with coarse-to-fine smoothing schedules and warm-started
time-series fitting. Not yet validated as an improvement over the DE
fit on real data (see distortion_lsq_run); needs a robustness review
of parameter normalization and finite-difference step sizes before PR.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ith the modeled image only.

Per the 2026-07-06 merit-smoothness scan, a 1-pixel Gaussian on the
model image at a 401-pixel scene resolution reduces the minimum
readable finite-difference step from 0.21 to 0.07 arcsec, and is the
physical choice: the observation already carries the real PSF, so only
the raytraced image needs the blur. Passed through `fit_distortion_lsq`
and `fit_distortion_series`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ing the fitted per-frame payload pointing.

The pointing offsets (one per Level-1 frame, common to all channels)
were measured 2026-07-06 by independent per-frame coherent
pitch/yaw/roll merit scans against the imaged AIA scene (401-pixel
scene, 1-pixel Gaussian PSF on the modeled image, parabola-refined).
Yaw dominates, sweeping monotonically from +3.3 to -4.4 arcsec across
the flight. Full fit artifacts in pointing_run/ (untracked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rtion_fit()`.

Phase cross-correlation of the modeled and observed images revealed
constant per-channel misregistrations (up to ~2.5 detector pixels in y
for channels 0 and 2) frozen into the original stochastic 201-pixel
optimization. Per-channel peaks read from deterministic PSF-smoothed
correlation scans at the time=15 reference frame raise the per-channel
correlation to [0.572, 0.604, 0.564, 0.585] from
[0.561, 0.603, 0.546, 0.584]. The per-frame time offsets will be refit
against this corrected reference in the following commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…)` against the re-polished reference.

The time=15 offsets are now consistent with zero (pitch +0.07",
yaw -0.005"), confirming the corrected per-channel reference and the
time series agree. Mean PSF-smoothed correlation across the flight
rises to 0.569 from 0.566. Run 2026-07-07 on rci-parkerlab, 30 frames
x 88 evaluations, artifacts in pointing_run/ (untracked).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The vendor measurements report the magnitude of the radius of
curvature, but the sag convention for these concave gratings is
negative (the design radius is -597.83 mm).  As committed, the
as-built grating diverged the beam, producing a ~96 mm RMS spot;
with the correct sign the model focuses (~0.07 mm RMS).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
…istortionParameters` file I/O.

`fit_distortion_scan` is the derivative-free engine proven on the
2026-07 per-frame pointing fits: coarse-to-fine axis scans refined
with a parabola, with an optional per-channel separable readout
(one coherent scan pass yields every channel's peak).
`fit_distortion_series` now drives it, fitting every frame
independently from a shared reference (optionally in parallel).

`fit_distortion_lsq` is removed: scipy.optimize.least_squares
under-converges on the locally-flat smoothed-correlation surface
(t=0 of the June series moved 2e-5 arcsec before terminating while
the true peak was ~4 arcsec away). `DistortionResidual` remains as
the deterministic objective, with a note recording the finding.

`DistortionParameters.to_file()`/`from_file()` round-trip fit
results through plain-text ECSV tables (units preserved, provenance
in the header) so they can be committed and reviewed as text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
The hardcoded per-channel reference parameters and per-frame pointing
offsets now live in `_data/distortion_reference.ecsv` and
`_data/distortion_pointing.ecsv`, written by
`DistortionParameters.to_file()` with the provenance of each fit in
the table headers, and are loaded on demand. Tests pin spot values so
the files and their loader cannot drift silently.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
A grids entry keyed by a tuple of field names scans the outer product
of the given offset grids and reads the joint peak, which is required
for strongly-coupled pairs (grating pitch x instrument pitch, grating
yaw x instrument yaw): independent 1-D scans of such pairs converge
into a compensating local optimum instead of the true solution, as
found when fitting the as-built ESIS-I model from scratch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
…he regeneration entry points for the committed fits.

Anyone can now re-derive (or verify) the committed data files from the
flight data: `fit_distortion_reference()` fits all nine per-channel
degrees of freedom from the as-built model with the joint-scan capture
schedule, and `fit_distortion_pointing()` refits the per-frame payload
pointing series, each optionally writing the ECSV file it reproduces.
`fit_distortion_scan` gained a `coherent` option (per-channel merit,
one shared peak) implementing the rigid-payload model of the series.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
The least-squares polish section is replaced with
`fit_distortion_scan` (coherent rigid-payload pointing polish), and a
closing section documents the committed ECSV fit results, their
regeneration entry points, and the model/data blink comparison of the
whole flight (docs/_static/blink.gif).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.72752% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
esis/optics/_distortions/_distortions.py 99.49% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

jacobdparker and others added 4 commits July 9, 2026 21:49
`fit_distortion_series(workers=...)` now uses the spawn start method
explicitly: the fork default on Linux is unsafe once numpy/numba
thread pools exist in the parent and duplicates its full memory into
every worker, which broke the process pool on the CI runners.

The flight-data-dependent `_fits` entry points are excluded from
coverage (they require the JSOC scene and hours of raytracing), and
small tests now exercise the extra-axis reduction, the file-format
and joint-grid error branches, the polish-tolerance convergence path,
and the default-parameter paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
The docs notebooks pay the full Level-1 pipeline (peaks of tens of
GiB) inside whichever nbsphinx kernel first calls level_1(), on top
of Sphinx's own accumulated memory — enough to OOM the runner now
that the distortion-fit tutorial joins the build. Computing the
Level-1 and AIA-scene caches in a clean process before Sphinx runs
keeps that peak isolated, and persisting ~/.esis/cache with
actions/cache makes it a no-op on subsequent builds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
The warm step exhausts the runner where a local measurement of the
same call peaks at 24.7 GiB; the ticker records the actual memory
trajectory so the failure is diagnosable from the log. The warm-up is
also split per level so the failing stage is unambiguous.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
The generic `esis.data.synth.scene_aia` gained a `crop` parameter,
applied after the normalization is computed from the full frame but
before the radiometric arithmetic, so the kept pixels are identical
to cropping the result. The ESIS-I wrapper now passes its central-
third crop this way and builds the scene one spectral line at a time
instead of opening all three AIA channels at once. The result is
bit-identical to the previous implementation (verified against the
cached scene); the remaining peak is internal to `sdo.aia.open` for
a single channel.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013gaMo9vwybK8qWZzCR5c3j
@github-actions

Copy link
Copy Markdown

Documentation preview available at https://esis-mission.github.io/esis-pr-previews/68-feature/distortion-fit-machinery
Note that it might take a couple seconds for the update to show up after the preview_build workflow has completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant