Skip to content

Dev compare - #6

Merged
brentmantooth merged 14 commits into
mainfrom
dev_compare
Jul 19, 2026
Merged

Dev compare#6
brentmantooth merged 14 commits into
mainfrom
dev_compare

Conversation

@brentmantooth

Copy link
Copy Markdown
Owner

significant changes to spatial detail calculation
Update of Unit Test suite
GUI enhancements

brentmantooth and others added 14 commits July 17, 2026 10:24
… dedupe Moffat fitting

background_subtracted() could silently upcast to float64 if photutils ever
returned a float64 background model; Background2D relied on photutils'
internal SigmaClip default and used the now-deprecated bkgrms_estimator
keyword. Camera gain resolution (EGAIN > GAIN > CCDGAIN > GAINDB) existed
in two places that could disagree for the same file - the GUI metadata
panel and the SNR analyzer - now unified behind one shared helper.

The 2D Moffat PSF fit was duplicated between psf_analyzer.py and
halo_dialog.py with drifted plausibility bounds; extracted into
analysis/moffat_fit.py and migrated off the deprecated LevMarLSQFitter to
TRFLSQFitter with explicit bounds. PSF_FWHM_CLIP_NSIGMA was being applied
to a raw (unscaled) MAD, clipping at ~2sigma despite its name; switched to
mad_std so the multiplier means what it says.

Also: guarded unguarded float() header parses, deduped the pixel-scale
keyword table, removed dead code, and pinned astropy>=6.0/photutils>=3.0
in requirements files.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nfigurable

The nebula/background mask used throughout Section 8 (Spatial Detail Analysis)
was a hardcoded 2sigma cut with no way to capture dim/dark transition regions
at nebula edges. Adds three GUI-configurable parameters (default 1.7sigma
threshold, 3px dilation, 5px hole-fill) that flow from control_panel.py through
analysis_thread.py into SpatialDetailAnalyzer, plus fixes the two-image shared
mask combination (nebula: union instead of intersection, so either image's
classification counts; background stays an intersection for a clean noise
floor).

Also fixes a bug surfaced while adding dilation: growing the mask with
binary_dilation was amplifying scattered single-pixel noise-driven false
positives (expected at a loose sigma cut) into large blobs, inflating the
mask ~4x and diluting the noise-corrected contrast metrics. Small isolated
objects are now stripped before dilation, matching the existing small-hole-fill
treatment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…Section 8

Every Section 8 map figure already had a per-pixel correlation scatter except
one: the raw "Original" panel had map/diff/correlation data computed but no
figure was ever generated for it, so the reported "some correlation plots are
missing" traces to this single gap. Adds a full Original Image family
(A/B map-pair, log-ratio, cross-section, histogram, correlation scatter) using
the same _plot_side_by_side/_plot_metric_correlation machinery as every other
family.

When a cross-section line is set in the viewer, its position is now overlaid
directly on the Image A/B panels (semi-transparent line, XS_LINE_ALPHA) in
every family's map figure, not just as a separate profile chart panel. New
SpatialDetailAnalyzer._crosshair_to_cropped_px() converts the line into each
map's already border-cropped pixel frame; the view is explicitly locked before
plotting so a line near an image edge doesn't add blank autoscale padding.

Reorganized Section 8 to open with the raw image (8b), then group detail-based
families (LoG, Wavelet, Gradient) ahead of contrast-based families (Local
sigma, Weber), with the cross-method overview last. This also fixed a
pre-existing duplicate "8a" heading and required sweeping ~28 literal
subsection cross-references scattered through captions/info-boxes to the new
lettering (a documented CLAUDE.md pitfall).

Along the way, fixed a real bug behind "kernel sizes appear out of order":
figs_for()'s sorted(figs) was a lexicographic string sort, rendering
nrm_std_10px before nrm_std_3px/5px. Replaced with _family_nrm_figs(), which
iterates the already numerically-ordered _SPATIAL_CORR_ROWS list.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…od error bars

Section 8 previously only compared Image A vs B over two coarse regions
(Nebula/Background) per metric/scale, with no way to see whether their
differences concentrate near sharp local features (stars, edges, bright
knots) rather than being spread uniformly across the nebula. New Section 8j
detects a scale-adaptive local-maxima mask independently for each of the 14
metric/scale combinations (std, LoG, gradient, wavelet, Weber) and reports,
per row: mean +/- SD magnitude for each image, the geometric-mean A/B ratio,
a Mann-Whitney U + Cliff's delta significance test, and a violin+box
distribution figure of the underlying masked A/B pixel populations.

The mask itself uses maximum_filter non-max suppression + a percentile
prominence threshold + pre-smoothing (suppresses noise-driven false peaks)
+ binary_dilation region growth, with every parameter expressed relative to
that row's own characteristic scale rather than a fixed pixel count. The
previous single-scale (|LoG| sigma=3px) illustrative mask figure is replaced
with a full grid — one row per metric family, columns = kernel/scale
smallest to largest — showing every row's actual mask, not just an example.

Both cross-method overview plots (8i, 8j) gained error bars: exact for 8j
(std of the pixel-paired per-pixel log-ratio population within each mask),
approximate coefficient-of-variation propagation for 8i (no pixel-paired
population exists there — explicitly captioned as an approximation, not a
formal confidence interval).

Removed Section 8c's Nebula-vs-Background log-ratio violin figure, whose
visual technique is now reused for 8j's A/B pixel-value distribution figure;
fixed a latent bug where 8c's mask illustration was accidentally gated on
that removed figure's own output and would have disappeared right along
with it.

Extracted core/stats_utils.py's Mann-Whitney + Cliff's delta helper using
the exact O(n log n) identity (delta = 2U/(n1*n2) - 1) in place of an
O(n*m) pairwise sign matrix, which would have been a multi-gigabyte memory
blowup on Section 8j's much larger per-pixel populations (Section 4's PSF
table, with only per-star counts, never hit this — but shares the fix).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…j ratio in log space

Section 8's five metric families (8d-8h) and Section 4's PSF test-chart
image sequence each embedded 6-9 composite figures with no way to collapse
them, making the report long and hard to navigate. Reused the existing
_info_box <details>/<summary> pattern (previously text-only) to wrap each
family's map/correlation/noise-normalised figures and the test-chart
image sequence in closed-by-default collapsible boxes, keeping headings,
methodology, and tables visible outside.

Section 8j's local-maxima mask overlay changed from darkorange (poor
contrast against bright nebula regions) to magenta. The mask itself was
undersampling broad bright plateaus that never register as a sharp local
maximum, so a new top-N%-brightness mask (SECTION8_LOCALMAX_TOP_PERCENT,
default 5%, GUI-configurable) is unioned into the peak mask via a shared
_combined_localmax_mask helper -- used identically by the table stats and
the mask-grid figure so both always agree on what's masked.

8j's "Ratio A/B" column was already sampled directly from the log-ratio
image within the mask, but displayed as a linear ratio with no visible
spread, and the cross-method overview plot converted the log-space error
into linear units via a delta-method approximation. Both now work
natively in log10 space: the table shows "log ratio A/B (geo. mean +/-
SD)" shaded a neutral blue (not a red/green comparison), a new per-scale
log-ratio distribution figure sits alongside the existing Image A vs B
one, and the overview plot's y-axis and error bars are exact by
construction, no conversion needed.

Removed a duplicate Contrast Retention Detail table from Section 9 --
_psf_simulation_html called _psf_retention_table(sim) twice with
identical arguments purely to populate a cache attribute re-spliced into
the summary section later.

CLAUDE.md gained a new "Collapsible figure blocks" convention, extended
the local-maxima and ratio-uncertainty conventions for the OR-mask and
log-space presentation patterns, and two new pitfalls (percentile
threshold on a flat array, cached render-to-attribute duplication).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…auto-reset

The Parameters group had grown to 13 rows in a single QFormLayout, forcing
Metrics and Output & Run to carry a lot of dead space since none of the three
control-panel boxes had a stretch factor. Split Parameters into two
semantically-grouped columns (General/PSF | Nebula & Local-Maxima), number
the three group boxes to read as workflow steps, and retune the panel's
height cap and default window width from measured QGroupBox.sizeHint()s
instead of the old per-row guess.

Add a slim toolbar above the image panels (Open A/B | Select ROI/Line | Run
Analysis) so the load -> select-region -> run sequence has a visible
left-to-right order. Toolbar actions share QActions with the existing menu
items and proxy clicks to the real control-panel buttons rather than
duplicating their toggle state.

Also close a gap noted in CLAUDE.md's stale-ROI pitfall: loading a new image
into either panel now resets the ROI and cross-section line (and their
visual overlays) instead of only catching an out-of-bounds ROI reactively
at Run time.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Section 4: drop redundant per-star FWHM/eccentricity histograms, relabel
the PSF-simulation test-chart dropdown, and switch the contrast-retention
figure from a 3-row stack to a 2x2 grid.

Section 6: add EDGE_N_TOP_EDGES to core/models.py so the number of
auto-detected edges is configurable (default 3), fix the gradient-map
overview to show only the edges actually analyzed instead of every
searched candidate (was silently showing 9 boxes against a "three
selected" caption), and add a directional start marker to the edge ROI
and ESF/LSF profile figures.

Section 8: remove the per-family plain tables (SNR, contrast ratio,
Weber 99th-pct) and consolidate the five noise-corrected tables
previously duplicated under 8d-8h into one combined table in 8j.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tion 6 edge width bug

Section 8 (Spatial Detail): remove the Weber fraction contrast metric and
add a Local Entropy family (map, contrast ratio, noise-corrected score)
reusing the existing shared NC/contrast helpers. Reuses Weber's former 8h
letter slot so no cross-reference renumbering is needed.

Fix a matplotlib mathtext ParseException race condition: pyparsing's
packrat cache (enabled globally by matplotlib's mathtext grammar) is not
thread-safe, and this app renders figures from multiple analyzer threads
concurrently. Serialize all savefig() calls through a lock in
core/fig_utils.py and remove report_builder.py's duplicate, unprotected
copy of fig_to_b64.

Vectorize the new local entropy map (per-bin uniform_filter box sums
instead of a per-pixel generic_filter callback) after it was found to
regress the test suite runtime ~2x.

Section 6 (Edge Analysis): fix two bugs in EdgeAnalyzer._extract_esf.
The "Scan start" marker was drawn through the Sobel-detected gradient
peak instead of the ROI's own array center, which is the actual pivot
rotate() uses -- the marker could land off the scan-direction line.
Separately, the rotation angle formula aligned edges horizontally
instead of vertically, so the ESF's column-wise averaging integrated
across the transition instead of along it, inflating measured edge
widths by 7-14x since the feature's introduction. Add a ground-truth
width-accuracy test (checking measured width against the analytically
known erf-profile width of a Gaussian-blurred synthetic edge), since the
existing tests only checked monotonicity/shape and never caught this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Splash screen now loads resources/AstroImageLabSplash.png instead of a
procedurally-painted QPixmap; the whole resources/ dir is already bundled
by AstroImageLab.spec, so no packaging changes were needed.

The prior mathtext ParseException fix only locked fig_to_b64()'s
savefig() call, but fig.tight_layout() triggers the same draw pass and
was left unprotected -- PowerSpectrumAnalyzer runs image A/B concurrently
in a ThreadPoolExecutor, so an unlocked tight_layout() in one thread
could still corrupt the shared pyparsing packrat cache mid-parse in
another, reproducing the exact "ParseException: ... (at char 0)" failure
on Section 7. Added finalize_layout() to core/fig_utils.py to route
tight_layout() through the same lock as savefig(), and switched every
call site that can run concurrently with other figure-building code.
report_builder.py's tight_layout() calls were left as-is since report
generation runs strictly serially after all analyzer threads join.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…t-xdist

The "not slow" CI job was hitting its 20-minute timeout. Root cause was
dozens of tests across 7 files re-running the same expensive analyze()/
build() call with identical inputs instead of sharing a fixture (worst
case: one parametrized test in test_spatial_detail.py ran a full
two-image SpatialDetailAnalyzer.analyze() 15x to check one absent key).
Collapsed same-input calls into class/module-scoped fixtures and added
pytest-xdist (-n auto) for parallelism. Documented the pattern and the
incident in CLAUDE.md so it doesn't regress as the suite grows.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@brentmantooth
brentmantooth merged commit 4ffb922 into main Jul 19, 2026
6 checks passed
@brentmantooth
brentmantooth deleted the dev_compare branch July 20, 2026 18:29
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