Skip to content

sed: make_bc can extend the Av axis, and BULGE_AV_PTS says how far and how finely (2.9.16) - #301

Merged
jdeast merged 11 commits into
masterfrom
sed-bulge-av-axis
Sep 17, 2026
Merged

jdeast merged 11 commits into
masterfrom
sed-bulge-av-axis

Conversation

@jdeast

@jdeast jdeast commented Sep 17, 2026

Copy link
Copy Markdown
Owner

Serves review 2.9.16. Makes the Av axis expressible; does not regenerate the shipped tables.

Why

The shipped Av axis stops at 6.0 mag, and SED._inject_grid_bounds makes the grid extents the sampled parameter's exact support through the logit transform — so av cannot exceed 6, and a bulge fit is truncated rather than warned. make_bc_tables did av_pts = axes["av_pts"], inheriting the axis from the tables already on disk, so there was no way to lift the ceiling. It now takes an explicit axis; passing nothing reproduces the shipped one exactly.

Range — set by what's observable, not what's tabulated

Inverting DC2018's own red-clump extinctions through models/extinction_law.ascii, its 293 sightlines need A_V = 1.69 to 19.04 (median 2.62, p95 10.06) — 11% past 6.0.

But the largest fittable requirement is 15.19 ± 1.33 (event 100: the faintest sightline with a released light curve, source fraction 0.434, baseline S/N 23 → source S/N ≈ 10). A_V ≈ 19 appears only in the extinction table with no light curve, and at that depth a source like these sits at S/N 2–3 and carries no SED information.

So 20.0 clears the largest fittable value by 3.6σ — the margin that keeps the posterior off the bound. The top few magnitudes exist to prevent truncation, not because anything is measured there.

Spacing — measured, not assumed

|d²BC/dAv²| at high Av, p95 over all 660 (Teff, logg) cells of the shipped tables:

band curvature h=2 h=1 h=0.5
Gaia_G 0.0389 0.019 0.0049 0.0012
TESS 0.0166 0.0083 0.0021 0.0005
WFI_F146 0.0083 0.0041 0.0010 0.0003
2MASS_J 0.0007 0.0003 0.0001 0.0000

Gaia_G is sixty times more curved than J, because a wide blue passband reweights as the spectrum reddens. So the shipped 2-mag steps above Av=1 already cost ~0.019 mag in Gaia_G — indistinguishable from today's 0.02 systematic floor, and 4× over the 0.005 that Landolt-era calibration targets. h=1 lands exactly on 0.005, which is no margin; h=0.5 is 4× under it. Below Av=1 the curvature is higher, so the shipped fine sampling there is kept.

Cost

48 points against 13 — ~3.7× the table size, 841 KB → ~3.1 MB per feh file. Irrelevant beside the R=150 spectra's own ~0.02 mag error, which dominates interpolation by 16×. Regenerating on the full-resolution grids is what actually buys 0.005; this spacing is chosen so the axis won't be the limiting term when that happens.

Note on the test

validate_av_pts is module-level deliberately: a test of the axis contract must not need the 313 MB model tree or the Zenodo fetch that ensure_model_data triggers before the generator's body is reached. The first version of the test called make_bc_tables and only passed quickly because the tree happened to be cached.

56 passed with tests/test_sed.py, on a compute node.

🤖 Generated with Claude Code

jdeast and others added 2 commits September 17, 2026 09:31
…d how finely

The shipped Av axis stops at 6.0 mag and `SED._inject_grid_bounds` makes the
grid extents the sampled parameter's EXACT SUPPORT through the logit
transform -- so `av` cannot exceed 6 and a bulge fit is truncated rather than
warned (review 2.9.16).  make_bc_tables took `av_pts = axes["av_pts"]`, i.e.
it INHERITED the axis from the tables already on disk, so there was no way to
lift the ceiling.  It now accepts an explicit axis; passing nothing
reproduces the shipped one exactly.

RANGE, measured rather than assumed.  Inverting the DC2018 challenge's own
red-clump extinctions through models/extinction_law.ascii, its 293 lines of
sight need A_V from 1.69 to 19.04 (median 2.62, p95 10.06): 11% are past 6.0.
But the range is set by what is OBSERVABLE, not by what is tabulated.  The
largest FITTABLE requirement is 15.19 +/- 1.33 -- event 100, the faintest
sightline with a released light curve, source fraction 0.434 and baseline S/N
23, so source S/N ~10.  A_V ~ 19 appears only in the extinction table with no
light curve, and at that depth a source like these sits at S/N 2-3 and
carries no SED information.  20.0 then clears the largest fittable value by
3.6 sigma, which is the margin that keeps the posterior off the bound; the
top few magnitudes exist to prevent truncation, not because anything is
measured there.

SPACING, also measured.  |d2BC/dAv2| at high Av, p95 over all 660
(Teff, logg) cells of the shipped tables, is 0.0007 for 2MASS_J and 0.0389
for Gaia_G -- sixty times larger, because a wide blue passband reweights as
the spectrum reddens.  Linear interpolation costs h^2/8 of that, so in the
worst band h=2 is 0.019 mag, h=1 is 0.0049 and h=0.5 is 0.0012.  The shipped
2-mag steps above Av=1 therefore already cost ~0.019 mag in Gaia_G, which is
indistinguishable from today's 0.02 systematic floor and 4x over the 0.005
that Landolt-era calibration targets; h=1 lands exactly ON 0.005, which is no
margin.  h=0.5 is 4x under it.  Below Av=1 the curvature is HIGHER, so the
shipped fine sampling there is kept.

COST: 48 points against 13, ~3.7x the table size (841 KB -> ~3.1 MB per feh
file).  Irrelevant beside the R=150 spectra's own ~0.02 mag error, which
dominates interpolation by 16x -- regenerating on the full-resolution grids
is what buys the 0.005 target, and this spacing is chosen so the axis is not
the limiting term when that happens.

validate_av_pts is module-level on purpose: a test of the axis contract must
not need the 313 MB model tree or the Zenodo fetch that ensure_model_data
would trigger before the generator's body was reached.  56 passed with
tests/test_sed.py on a compute node.

NOT REGENERATED HERE.  This only makes the axis expressible; the tables on
disk are unchanged, and regenerating them is a separate decision since they
are shipped data.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…bservable up there

JDE: "we won't have gaia (optical) data at high extinction ... it doesn't pay
to be conservative here -- a resolution of 1 mag up to av=15 will be a
substantial bloat to the grid for everyone in order to apply it to a handful
of microlensing systems."

Correct, and it inverts my spacing conclusion.  I had sized the axis on the
most curved band in the table, Gaia_G at |d2BC/dAv2| = 0.0389, which demanded
h=0.5 and would have made the grid 3.7x bigger for every user.  But at
A_V = 15 there is no Gaia measurement to interpolate: A_G is 11.6 mag, so a
bulge clump giant (M_G ~ 0, m ~ 14.5 unreddened) sits at m = 26, five
magnitudes past Gaia's limit.  The same arithmetic removes Bessell B and V,
TESS (limit ~16 against m = 23.8) and all of 2MASS (limits J 15.8, H 15.1,
K 14.3 against 19.1, 17.3, 16.4).

What survives A_V = 15 is Roman's own two bands, deep ground-based IR of VVV
class, and WISE.  The worst curvature among those is WFI_F146 at 0.00825,
needing only h < 2.20 mag for 0.005 -- so the SHIPPED 2-mag step already
costs 0.0041 mag where it is used, inside the Landolt-era target, and
refining it would buy precision in bands that cannot be observed at that
extinction.

So BULGE_AV_PTS is now the shipped 13 points verbatim plus 8, 10, 12, 14, 16,
18, 20 at the same cadence: 20 points, 1.5x rather than 3.7x, 841 KB ->
~1.3 MB per feh file, and the growth is confined to a range that was
previously unreachable.  The test asserts both halves -- that the shipped
range is untouched, and that the spacing is sized by the SURVIVING bands.

Filed rather than fixed: for a user WITH optical data at MODERATE extinction,
the shipped 2-mag steps between Av = 2, 4 and 6 already cost ~0.019 mag in
Gaia_G.  That is a pre-existing limitation of the shipped grid, not of this
extension, and refining 1-6 would bloat the grid for everyone -- so it is a
separate decision, noted in the constant.

56 passed with tests/test_sed.py on a compute node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdeast

jdeast commented Sep 17, 2026

Copy link
Copy Markdown
Owner Author

Update after review

The spacing argument in the first version was wrong, and the correction shrinks this change considerably.

I had sized the axis on the most curved band in the table — Gaia_G, |d²BC/dAv²| = 0.0389 — which demanded h = 0.5 and would have made the grid 3.7× bigger for every user. But at A_V = 15 there is no Gaia measurement to interpolate: A_G = 11.6 mag, so a bulge clump giant (M_G ≈ 0, m ≈ 14.5 unreddened) sits at m = 26, five magnitudes past Gaia's limit. The same arithmetic removes Bessell B/V, TESS (limit ~16 against m = 23.8), and all of 2MASS (limits J 15.8, H 15.1, K 14.3 against 19.1, 17.3, 16.4).

What survives A_V = 15 is Roman's own two bands, deep ground IR of VVV class, and WISE — and the worst curvature among those is WFI_F146 at 0.00825, needing only h < 2.20 mag for 0.005. So the shipped 2-mag step already costs 0.0041 mag where it is used, inside the Landolt-era target. Refining it would buy precision in bands that cannot be observed at that extinction.

So: extend, don't refine.

shipped this PR first version
points 13 20 48
growth — 1.5× 3.7×
range 0–6 0–20 0–20
spacing above 1 1, 2, 2 unchanged, continued at 2 0.5
per-feh file 841 KB ~1.3 MB ~3.1 MB

The shipped 13 points are kept verbatim — the curvature is highest below Av=1, so their fine sampling is exactly where it's needed — and the growth is confined to a range that was previously unreachable. The test asserts both halves: that the shipped range is untouched, and that the spacing is sized by the surviving bands.

Range (unchanged from the first version)

A_V = 20 clears the largest fittable requirement (15.19 ± 1.33, event 100) by 3.6σ. A_V ≈ 19 appears only in DC2018's extinction table with no light curve attached, and at that depth a source like these sits at S/N 2–3 — so the ceiling exists to prevent posterior truncation, not because anything is measured up there.

Filed rather than fixed

For a user with optical data at moderate extinction, the shipped 2-mag steps between Av = 2, 4 and 6 already cost ~0.019 mag in Gaia_G. That's a pre-existing limitation of the shipped grid, not of this extension, and refining 1–6 would bloat the grid for everyone to serve it. Noted in the constant as a separate decision.

56 passed with tests/test_sed.py, on a compute node. The tables on disk are unchanged — this only makes the axis expressible.

jdeast and others added 9 commits September 17, 2026 09:59
.pre-commit-config.yaml pins ruff at v0.16.0 and says so emphatically -- it is
"THE ONLY AUTHORITY ON THE RUFF VERSION", ruff is deliberately absent from
pyproject.toml so there is no second pin to drift, and CI runs THOSE hooks
rather than its own ruff so the two cannot disagree.  I had been formatting
this whole session with the conda environment's 0.16.2, which is exactly the
drift the comment warns about (it even names a case of 0.16.1 against 0.16.0).

The two versions disagree on one list literal here: 0.16.0 wraps it, 0.16.2
leaves it inline.  Formatted with 0.16.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…p all 8 Roman bands

Runs make_bc.py over all seven facilities with BULGE_AV_PTS, which is the
axis the previous two commits added.  77 files, 2m25s on 4 cores of a
compute node (NOT the login node).

THE OLD ROWS DO NOT MOVE.  All 8,580 pre-existing (teff, logg, feh, Av)
rows reproduce bit-identically -- max |dBC| = 0.000e+00 across every band
of every facility -- because BULGE_AV_PTS extends rather than refines: it
is the shipped 13 points verbatim plus 8, 10, 12, 14, 16, 18, 20 at the
same 2-mag cadence the old axis already used above Av = 2.  So the grid
grows 1.54x (8,580 -> 13,200 rows) and no shipped example's SED can shift.
Extend-not-refine is also why the spacing argument came out this way: at
A_V = 15 an optical band is several magnitudes past any real detection
limit, so refining up there would have bloated the grid for everyone to
serve data that cannot exist.

ROMAN GOES FROM 2 BANDS TO 8 (F062, F087, F106, F129, F146, F158, F184,
F213 -- the WFI imaging set; Prism and Grism are spectroscopic and have no
BC).  Roman was the one facility never tracked here, because
make_bc.generate_missing_facility builds a missing facility on demand --
but it builds only the bands the config happens to request, so a fit using
Z087+W149 silently got a 2-band table and paid a one-time generation cost
for it.  Shipping the full set removes both.  The tables are what ships;
the SVO transmission profiles still download to the machine-level cache at
fit time, which is the designed split (filters/filter.py deliberately
treats the package directory as read-only).

Cost: the tracked BC tables go 59.5 MB -> 91.8 MB for the axis, plus 29 MB
for Roman.  Worth knowing before the R~1e6 regeneration, which rewrites
all of it again; if that lands in git too, these tables are the obvious
candidates to move behind utilities/zenodo.py like the NextGen spectra
already are.

Verified: axis is 20 points 0.0-20.0 and IDENTICAL across all seven
facilities; teff/logg/feh axes unchanged (60/11/11); no NaN or inf; per-band
BC ranges order correctly by wavelength.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…01 not 9.88

With the Av axis extended to 20 mag the event-194 prior could finally be
measured rather than clipped, and measuring it turned up the last piece of
7.7.3: OUR MODEL AND THE SIMULATION USE DIFFERENT EXTINCTION CONVENTIONS,
so no single av satisfies both truth bands.

WHAT THE SIMULATION DID, demonstrated rather than assumed.  Its
A_Z087/A_W149 = 1.9236 is reproduced by the shipped law at the filters'
WavelengthEff (0.86510, 1.30496 um) to 0.1%, and by NO other wavelength
definition SVO publishes: pivot/ref is 13.3% off, phot 6.0%, cen 23.6%,
mean 24.7%.  Inverting the data's ratio for the W149 wavelength that would
produce it gives 1.3130 um -- WavelengthEff to 0.6%.  So the challenge
reddened MONOCHROMATICALLY at each band's effective wavelength, which is
the provenance of the 9.88 that has been quoted as event 194's A_V.

WHAT WE DO IS THE PHYSICALLY CORRECT THING: make_bc integrates a reddened
spectrum through the passband.  For F146/W149 (0.896-2.073 um, WidthEff
0.789) that is a different quantity.  As Av rises the band's blue wing is
extinguished away, its own effective wavelength drifts red, and
dA_band/dAv FALLS -- 0.272 near Av=0 to 0.212 at Av=10 -- so our
A_Z087/A_W149 GROWS with Av (1.978 at 0.1, 2.219 at 11.75, 2.317 at 18)
while the data's is constant by construction.

HENCE THE ANCHOR CHOICE IS WORTH 15% AND THE COLOUR IS THE RIGHT ONE.
Through our integrated grid: A_W149 -> av 11.75, A_Z087 -> 10.14,
E(Z087-W149) -> 9.01.  At 9.01 both bands come out -0.58 mag, THE SAME
OFFSET, and a common-mode grey offset is degenerate with distance and
radius, which the SED already fits.  Every other anchor dumps the residual
into COLOUR where only teffsed can absorb it (+0.81 mag at the A_W149
anchor, +0.26 even at the simulation's own 9.88).  sigma is 0.3, a model
uncertainty: the colour-matched av has std 0.10 across 36 (feh, Teff, logg)
cells, is independent of logg and feh to +/-0.01, and depends on Teff alone
at 0.28 per 1000 K.  The arm's old N(9.88, 1.19) was the wrong centre and a
width propagated from the wrong quantity.

This is neither a law error nor a grid error, and the two earlier readings
of the same 26% spread -- "our law disagrees with the simulation" and then
"my own convention artefact" -- were both wrong in the same way.  It is the
monochromatic-vs-integrated difference, irreducible while the data are one
and the model the other.  For real Roman data the integrated treatment is
simply right and none of this arithmetic arises; mkticsed's clump path
still inverts monochromatically and so carries the same ~15% error for
W149-width filters, which is fine for a prior a magnitude wide and is filed
with 2.9.16 rather than fixed here.

ALSO IN HERE:
* C29 in conventions.md AND convention.tex, since a C-number must mean the
  same thing in both and ride one commit.  The paper repo's third copy is
  not reachable from this machine, so the tex header now records C29 as
  OWED to the paper rather than claiming all three are in step.
* NextGen.grid.yaml still declared the OLD 13-point Av axis.  The fit path
  never reads it -- sed.py::_inject_grid_bounds calls peek_grid_axes, which
  derives the axes from the tables -- but slice_bc and sed/plot.py do, so it
  described a grid that no longer existed and nothing complained.  Synced,
  and tests/test_sed.py now pins yaml == tables on all four axes, because
  that failure is silent by construction.
* dc18_set_cores.py said "'NoneType' object has no attribute 'get'" when
  handed an empty config, naming neither the file nor the problem.  It was
  a 0-byte DC2018_194.yaml -- ab194's av_true arm, truncated when the arm
  was set up -- and the array task spent a 64-slot allocation printing that
  traceback.  Now one line that also names the three paths an arm config
  has to change.
* The four av arms are tracked (configs only, as the other arms are), with
  av_true rebuilt from its sibling: prefix, parameter_file AND sed.file all
  renamed, verified by grepping for the donor's name.  Leaving one of those
  three behind is how the tight arm's trace was destroyed.
* av_free's completed run is archived in av_free_ceiling6/ rather than left
  to be overwritten: av = 5.26 +0.33 -0.40 against a hard [0, 6], i.e. the
  grid running out 2.3 sigma from a bound it cannot cross, with no warning.
  Its siblings av_clump (8.61) and av_band (11.65) were REFUSED at startup
  for a start outside those bounds -- the guard working as designed.

Tests: 127 passed (test_sed, test_make_bc, test_extinction_law_inversion,
test_sed_grid_barrier, test_mkticsed) on a compute node.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…engineering around it

JDE 2026-09-17: "I don't want to go to great effort (e.g., ship new grids)
to support a problem in DC2018.  we should raise our zero point error
and/or our av prior error to accommodate their error, compute the impact,
and list the systematic disagreement as a caveat alongside the discussion
of how well we recover 'truth'."

So no new grid work.  Three changes to the sweep's priors, all measured.

(1) `av` IS ANCHORED ON THE COLOUR, THROUGH OUR OWN BC GRID.  AV_COLS put
A_W149 straight into `av`, ~5x too small, and it survived only because the
grid's Av axis stopped at 6.0 so a correct prior would have hit a wall
instead (7.7.3).  We invert the clump's (A_W149, A_Z087) for A_V through the
INTEGRATED band extinction the model actually uses, deliberately NOT through
components.sed.extinction's monochromatic law -- using the law would
reproduce the simulation's own convention and hide the disagreement rather
than budget for it.  Per event: 008 2.26, 152 3.70, 223 5.46, 128 6.44,
062 7.26, 194 9.01.  All six are now inside the axis; under the old ceiling
three of them (062, 128, 194) would have been refused at startup, as
ab194's av_clump and av_band arms actually were.

(2) THE TWO QUOTED DISPERSIONS ARE ONE FRACTIONAL ERROR, FULLY CORRELATED,
and treating them as independent overstates the colour error by ~2.4x.
Measured on all six events: sigma_Z/sigma_W = 1.885-2.000 against
A_Z/A_W = 1.918-1.924, and sigma_W/A_W equals sigma_Z/A_Z to three digits
(event 194: 0.1200 vs 0.1191).  event_info.txt is quoting one fractional
uncertainty on the line-of-sight extinction, so the colour excess carries
that same fraction and `av` scales with it: sigma_av = av * hypot(frac,
0.06), the 0.06 being the shipped law's own ~6% NIR shape error (2.9.16).
That term also keeps event 008 off a ZERO-width prior -- both its
dispersions are quoted as 0.01, which rounds the correlated colour error to
exactly 0.

(3) THE ZEROPOINT PRIOR ABSORBS THE LEFTOVER GREY TERM.  N(22.0, 0.02) was
right about the instrument and wrong as an error budget.  Anchoring the
colour leaves both bands off by the SAME amount -- -0.07 mag on 008 rising
to -0.58 on 194 -- and with the zeropoint pinned that hard the offset had
nowhere to go but theta_star.

THE IMPACT, WHICH IS THE POINT OF COMPUTING IT.  A grey error leaves the
colour alone, so in the challenge's own CSB relation
(log theta = 0.986 + 0.366*(Z087-W149)_0 - 0.2*W149_0) only the magnitude
term moves, and theta_star is biased LOW by 10**(-0.2*grey):

    event   grey (mag)   theta_star bias
    008       -0.07          0.970x
    152       -0.15          0.935x
    223       -0.26          0.886x
    128       -0.34          0.856x
    062       -0.41          0.827x
    194       -0.58          0.766x

Against the 0.62x deficit measured on event 194 against truth, the
convention accounts for 0.766 of it -- more than half the discrepancy in
log space (-0.116 dex of -0.208).  It is not the whole story and this does
not claim to fix it; widening the prior does not remove a systematic, it
stops the systematic masquerading as a measurement.  The residual
disagreement is a CAVEAT to report beside the truth-recovery table, not
something the sweep should be tuned against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n path

JDE 2026-09-17: "I don't think on the fly generation of the BC grids (as we
once envisioned) is practical in production (with the full resolution
atmosphere grids). it would require the user to download 250 GB of high
resolution models."  It is affordable now only because the shipped spectra
are plot-resolution.  The machinery stays -- it builds the shipped tables --
but the expected path for a new filter is to request it and have it generated
centrally and shipped, and generate_missing_facility now says so at the
function a reader lands on.

Also records the option JDE raised and nobody has costed: a service hosted
beside the models that computes and serves a requested BC table without the
caller holding the 250 GB grid.  That would turn extending the Av axis -- and
adding the Rv axis (2.9.16) -- from a repository-wide regeneration into a
query.  Whether Harvard IT would host it is an open question and not a
software one; written down so it is not rediscovered as novel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The priors the sweep has been running are ~5x too small (AV_COLS wrote the
clump's A_W149 into `av`), and the correct ones would have been REFUSED by
the old 6-mag grid ceiling on three of these six events.  Both are fixed, so
regenerate:

    event   was              now              zeropoint sigma
    008     0.66 +/- 0.01    2.26 +/- 0.14    0.07  (was 0.02)
    062     2.20 +/- 0.20    7.26 +/- 0.79    0.41
    128     1.94 +/- 0.26    6.44 +/- 0.94    0.34
    152     1.10 +/- 0.02    3.70 +/- 0.23    0.15
    194     2.75 +/- 0.33    9.01 +/- 1.20    0.58
    223     1.64 +/- 0.15    5.46 +/- 0.61    0.26

All six are now inside the axis; the largest is 9.01 against a ceiling of 20.

THE RE-RUN CHANGES TWO THINGS, NOT ONE, and the archive README says so
rather than claiming a clean single-variable comparison.  Regenerating also
picks up `err_scale: [0.5, 2.0]` on both instruments, which postdates these
configs -- JDE's call of 2026-09-15 (8.2.2), added because on event 226 a
point-lens basin inflated both bands 300-460x and turned the anomaly into
noise.  Re-running without it would deliberately reinstate a known defect,
so it stays and the second variable is declared.  Everything else is
byte-identical: only the six params.yaml files changed, and the sampler
block (ptde_async, 64 cores, n_temps auto, T_max 200, tune 5000, draws
50000) is untouched.

The previous sweep's traces and result tables are MOVED to
sweep_v7_avW149/ -- 25 GB, the only copy, and the scored baseline the truth
table was built from (152 5/7, 194 6/7; 008, 062, 128, 223 salvaged from
traces after the h_rss kills).  Any claim that the corrected priors helped
or hurt is measured against it.  Configs are copied there too, with a
warning not to rerun from that directory since their internal paths still
point at ../sweep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Self-correction to the previous two commits, found before the sweep
launched.  Widening both zeropoints to the grey residual was the obvious
move and it is the wrong one.

WHY IT IS WRONG.  These configs run `filters: []`, so the SED carries no
photometry of its own and the two mulensing zeropoints are the ONLY colour
information in the entire fit.  Two INDEPENDENT priors of width 0.58 admit
sqrt(2)*0.58 = 0.82 mag of COLOUR slack -- an order of magnitude more than
the colour signal separating plausible source temperatures.  It would have
bought an honest error bar on theta_star by throwing teffsed away, and it
would have slackened precisely the constraint the colour anchor exists to
exploit.  The prior that WOULD be right -- one grey term shared between the
two bands -- is not expressible in the config format, and building it is the
engineering JDE ruled out for a DC2018 problem.

WHAT REPLACES IT.  `av` is the correctly CORRELATED nuisance: moving it
reddens both bands together in the ratio the law dictates, which is mostly
grey with exactly the colour term the physics implies, so it buys freedom
along the reddening direction without opening the differential.  Half the
anchor spread (colour 9.01 vs A_W149 11.75 on event 194) now goes into the
av prior width alongside the clump and law terms:

    event   A_V prior          zeropoint
    008     2.26 +/- 0.19      22.0 +/- 0.02   (unchanged)
    152     3.70 +/- 0.38      22.0 +/- 0.02
    223     5.46 +/- 0.83      22.0 +/- 0.02
    128     6.44 +/- 1.20      22.0 +/- 0.02
    062     7.26 +/- 1.22      22.0 +/- 0.02
    194     9.01 +/- 1.83      22.0 +/- 0.02

SO THE BIAS IS REPORTED, NOT ABSORBED, and dc18_truth_table.py now prints
it beside the recovery table -- the per-event grey residual and the
theta_star bias it implies (0.970x on 008 to 0.766x on 194), with the note
that on 194 it is 56% of the measured deficit and the rest is ours.  JDE
asked for the disagreement "as a caveat alongside the discussion of how
well we recover 'truth'"; this table IS that discussion for anyone reading
a scoring run, and a caveat that lives only in the paper is a caveat nobody
applies.

The rejected approach is kept in the comments at both sites rather than
deleted, because "widen the zeropoint, it is a grey term" is exactly the
reasoning a later reader will re-derive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JDE 2026-09-17: "update the sweep to follow all currently known best
practices -- ptde, reparameterizations, data derived tight bounds, and the
correct av prior."  All four were measured on event 194 and all four were
measured SEPARATELY; this is the first configuration that carries them
together, which is the one thing about it that is NOT yet validated.

(1) ptde, NOT ptde_async.  Seven scored runs of event 194: ptde put ALL 78
chains in the good-likelihood region 3 times out of 3, under three
different configurations; every ptde_async run of the same event put at
most 40 there and usually fewer than three.  ESS followed -- u0te_sync
reached ~30,000 from 3,595 draws at 36 GB where its async twin needed
26,984 draws and 385 GB.

(2) fitu0te ON THE SOURCE.  Best of the three ptde arms by ESS(core):
30-32k against 23-25k (tight bounds) and 9.9k (control), at Rhat 1.00 with
78/78 chains.  It samples u_0*t_E, which attacks the u_0/t_E/f_blend
degeneracy that makes the blended events hard.

(3) DATA-DERIVED TIGHT BOUNDS (8.2.2 path 1), reproducing the `tight` arm's
rule exactly rather than approximately -- its committed numbers come back
bit-for-bit from this generator (log_f_total -2.1222092/1.8777908 and
-3.3546268/0.6453732 on 194, t_0 2458346.505461/2460059.241444):
  * source.t_0    -> the observing span of the light curves themselves.  A
                     peak outside the data is not a detection of anything.
  * source.u_0    -> [-3, 3], i.e. "the source was magnified".
  * log_f_total   -> log10(median flux of that curve) +/- 2 dex.
  * err_scale     -> NOT the tight arm's [0.1, 10] but the later, tighter
                     [0.5, 2.0] (JDE 2026-09-15, 8.2.2), which the sweep
                     already had and which supersedes it.
Worth 9.9k -> 23-25k ESS on their own.

(4) The colour-anchored av prior, from the previous commits.

AND ONE THING NO ARM HAS EVER HAD: all seven predate #293, so `ptde`
ignored store_hot_chains in every sync arm -- no hot-chain mode detector in
any run that had good chains.  That is fixed, so this sweep is the first to
get the good chains AND mode discovery together.

VERIFIED BEFORE LAUNCH, on a compute node, not assumed:
  * The config builds: system.prepare() + build_model() succeed with a
    finite start logp (88318.23 on event 194).
  * fitu0te is actually active -- the `source.fitu0te_jacobian` potential is
    in the model.
  * THE u_0 BOUND STILL BINDS, as a SOFT barrier: fitu0te makes u_0 a
    derived parameter ("from_u0te") with u0te sampled on [-5000, 5000], so
    the |u_0| <= 3 bound arrives as the `low_bound.source.u_0` /
    `up_bound.source.u_0` potentials rather than as hard support.  That is
    the right strength for a statement this weak, and it is documented at
    the site so nobody "fixes" it into a hard bound.
  * Every seed value sits INSIDE its new bound on all six events (t_0
    2458554-2459665 against a span of 2458346-2460059; |u_0| 0.012-1.472
    against 3), so none of them can be refused at startup the way the
    av_clump and av_band arms were.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s move with it

CI caught both of these and I should have: `av`'s bounds ARE the grid's
extent (sed.py::_inject_grid_bounds), so extending the axis to 20 mag is not
only a data change.  Running the five SED-adjacent test files was the wrong
check -- 127 passes there and the suite still red -- which is the heuristic
docs/testing.md already warns against.  CI's complete failure set was 6
tests in 2 files, both of them here.

(1) test_component_override_channel.py declared the grid it expects as a
constant, `"av": (0.0, 6.0)`.  Now (0.0, 20.0).  Three tests read it
(the override-channel check, the match-the-live-axes check, and the
looser-user-bound clipping check); all three pass.  The constant is
deliberately kept rather than derived from the grid -- it is asserted
against the live axes in the same file, so it only has to agree with the
tables on disk, and a silent axis change should break it.

(2) The three KMT-2019-BLG-1806 fixtures had to be re-recorded, and the
delta is fully explained -- ONE term, on the reference machine:

    POT:logit_uniform_prior.star.av   -12.797195 -> -15.202805   (-2.405610)

    start values moved:  0 of them, bit-identical
    terms changed:       2-3 of 87, of which exactly ONE is substantive
                         (the others are 1-ulp on imf_prior and
                         mulensinstrument.model)

That is the logit-uniform prior's normalization following the support from
[0, 6] to [0, 20]; log_jac carries log(hi - lo), and the raw coordinate for
the same physical av moves with it.  The stored start is unchanged, so this
is the PRIOR moving and not the likelihood -- which is what the fixtures
exist to separate.

RE-RECORDED WITH --only, NOT WHOLESALE.  A full `--check` also reports
ob08092 and ob09020 as DIFFERS, but purely at the last ulp
(RV:mulensinstrument.model -2.3e-10, imf_prior -2.2e-16), which the strict
same-machine recorder flags and the replay tests' tolerance tier correctly
absorbs -- neither fails in CI.  Re-recording everything would have swept
that unrelated drift into this commit and hidden it.  It also reports five
`MISSING fixture` lines for ob09020_accept/_diag/_polish/_tune/_tuneonly,
which are UNTRACKED local configs in this checkout, not shipped examples.

Replay tests after: 7 passed, largest residual 6.4e-15 relative.

WHAT THIS DOES NOT SETTLE, and it is worth a decision rather than a commit:
the same mechanism means every SED fit now carries a uniform av prior over
[0, 20] instead of [0, 6], because the grid extent IS the sampled support.
Harmless where av is well constrained (a uniform prior's widening is a
constant), a real 3.3x loosening where it is not -- and it lands on every
user to serve bulge sightlines, which is the objection JDE already raised
about grid spacing, reappearing as prior width.  Decoupling the
interpolation extent from the sampled support is the fix and is a design
change (it collides with "bounds may only be TIGHTENED"), so it is filed
rather than done: see 2.9.16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jdeast
jdeast merged commit 155b17c into master Sep 17, 2026
19 of 20 checks passed
@jdeast
jdeast deleted the sed-bulge-av-axis branch September 17, 2026 17:36
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