Skip to content

Following up on dev->smdc port - #56

Merged
rusholme merged 17 commits into
smdcfrom
smdc-port-followups
Sep 19, 2026
Merged

rusholme merged 17 commits into
smdcfrom
smdc-port-followups

Conversation

@eramey16

@eramey16 eramey16 commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

Follow-ups after the dev→smdc reconcile

Note: these were made largely before today's meeting and can be abandoned / the branch deleted if we prefer. Adding Ben as a reviewer so he can see if any of these are useful.

Seventeen commits on top of smdc @ 6ca495e, made against a written checklist after the diff-level comparison of smdc-devport with the smdc-devport-reconcile / socsim-20260821-rerun line. With the team moving back to dev, this PR is handed over as-is for triage rather than pursued further. The plan site (Stage 3½ "Re-base") has the per-commit ledger and what was checked before each.

Each commit is independent and can be taken or dropped on its own.

Deltas from smdc-devport that the reconcile line lacked

commit what
7fc5091 socsim registrar: INPUTBUCKET check before the heavy imports, so the fail-closed tests pass outside the container
b812e0f injector: close both datamodels after the last WCS use; honest kept-file count
f7fe3a8 light-curve evaluation vectorised through the model functions (not an inlined copy); 4 tests pin vectorised = per-source loop
f6e517a rimtimsim: warn when a frame gets ZPTMAG = 0.0 (six filters)
49feb88 socsims: sip_distortion_degree constant; Quantity guard before float()
36fa77e [sfft] rationale comments in release content (digest unchanged); warn on crossconv without segmentation
f5ae6a5 reference-catalogue timings; psfcat via science_value; ZOGY caveats + config_bool; generator docstring/sorted
8c6128d all five sims sites require INPUTBUCKET/OUTPUTBUCKET (exit 64 when unset), no code defaults

Fixes to defects found in the reconcile line

commit what verified how
016bc32 frozen rapid_db.py, digest re-ratified: numpy floats via psycopg2 Float (was AsIs(repr) → bare nan); execute_sql_queries([]) returns None; ordering docstring adapter rendered on a real connection
7e578cf ramp harness: W9_PROC_DATE defaults to processing_date_for; zero-unit date-scoped gather is named stub tier
1ee6d8d registrar tracebacks into the per-thread log; "migration 100" → 101
1e33689 currency sweeps go through sources.sid → pid → diffimages.pid; current = vbest IN (1,2); source sweep targets the sources child ephemeral PostgreSQL, colliding ids
84f4a08 statistics: DISTINCT ON (aid, pid, isdiffpos) by qfit; explicit status > 0; orphan-object prune ephemeral PostgreSQL
1433a8c [science] refimage_psf_filename bound to its .ini twin round-trip test
1481648 checksum_algorithm required below the bridge (repository, identity helpers, enumerate_source); AST sweep widened repo-wide sweep: zero default-reliant callers
77ba98f PSF registration: --fid vs Filters by wavelength code; 403 ≠ 404; temp dir released; unbounded varchar casts 27 tests; imports with psycopg2/boto3 blocked
bb37ac0 overlapping_fields: report the cap when a pole is inside the image; hemisphere check in the interior test 1,500 pointings identical away from the poles

Deliberately not done

  • The corrected sweeps are still not registered with the operator; re-enabling is a decision.
  • DEFAULT 'sha256' on 051's two checksum_algorithm columns (rapid_systems) is untouched.
  • 101's column comment still says "EXACT" (true away from a pole).
  • The byte-identity check on rapid_db.py (test_deletion_exclusivity) fails on this branch until it merges; the digest test passes.

Gate at tip: stub tier 2709 tests / 102 modules PASS; ruff 383 = baseline at 6ca495e.

🤖 Generated with Claude Code

eramey16 and others added 17 commits September 15, 2026 10:59
… check is reachable everywhere

db_register_socsim_files.py checked INPUTBUCKET (a5108ec) below eleven
imports — boto3, healpy, astropy, psycopg2 through rapid_db — so on any
interpreter without those packages the script died at 'import boto3' with
exit 1 and nothing on stdout, and the two fail-closed regression tests,
which run the script as a subprocess, failed outside the container. Their
docstring claimed the packages were part of the test extra; that extra is
pytest only. The stub tier has been red at the base of this branch for
exactly these two tests.

The check now runs first, before any third-party import: a misconfigured
run fails in milliseconds with the reason on stdout, and the test holds in
every environment. Verified with the astroconda interpreter (2 passed) and
with the bare system python3, which has none of the packages (exit 64,
message on stdout). Test docstring corrected; the pyproject import-mode
comment now names database/sims/test/ alongside database/modules/utils/test/
as the test directories that carry an __init__.py under an unpackaged
parent, which a5108ec made true without saying so.

Stub tier: 2677 tests across 101 modules, PASS (was FAIL at the base).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…the kept-file count honestly

Dev 399d4ed (carried by 287b592) closed dm immediately after reading
dm.meta.wcs, nine lines before gwcs_obj.pixel_to_world used the WCS object
it came from. The close now follows the sky computation, after the last
use, and original_dm — the file-backed model, never closed on either
branch across a whole-bucket loop — is closed with it.

The counter dev added and labelled 'socsims skipped' is incremented on the
append to input_asdf_files, so it counts the files KEPT; printed now as
'to process'. Port-followup item A1; see the reconcile audit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, not an inlined copy

_evaluate_catalogs_at_mjd on this branch carried dev f0630ba verbatim:
the sinusoid and Gaussian formulae inlined and the injectionLightCurveModels
import dropped, which duplicated the photometric model and made
GaussianLightCurve's time_bounds branch unreachable from the injector. It
now calls SinusoidalLightCurve and GaussianLightCurve with arrays, once per
light-curve type per catalogue — the same speed-up, one source of truth.
Also: an empty catalogue is skipped before the WCS call, an unknown type
raises before any evaluation rather than after both blocks have run, and
the coordinate arrays are explicit float64.

New stub-tier test pipeline/test/test_l2_injection_evaluation.py pins the
vectorised result against the per-source loop through the same model
functions on a 400-source synthetic catalogue (all five arrays equal), the
empty-catalogue shape, the unknown-type error, and that the sinusoid is not
inlined. Heavy imports are stubbed only when not installed.
Port-followup item A2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Dev 2c89139 (carried by e6924cc) set per-filter zero points for F213 and
F087 only and left F184, F158, F129, F062, F106 and F146 at a 0.0
placeholder written into the header with no diagnostic. A zero AB zero
point is not a missing value but a wrong one that downstream flux
calibration will use. The placeholder is kept, so behaviour is unchanged,
but it now prints a warning naming the filter. Supplying the six values
(or making the missing case an error) is an open team item.
Port-followup item A3.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…metry Quantities

sip_distortion_degree = 5 is hoisted to module scope (dev 3f22b1b,
e09845b, c58ef15 — three commits whose net effect on the value is none;
only the end state is carried). The derived ZPTMAG path (dev 8428314) did
float(conv) and float(pixarea) directly; roman_datamodels carries
conversion_megajanskys and pixel_area as astropy Quantities (MJy/sr, sr)
in recent versions, and float() on a dimensioned Quantity raises, which
would have sent exactly the files this path was written for to the nominal
table. The bare values are now taken first, in the units the formula
assumes. Port-followup item A4.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…arn on crossconv without segmentation

[sfft] in cdf/science/pipeline.toml carried its six keys with no comment;
the rationale lived only in the master .ini, which is scheduled for
deletion. The comments now state dev 7d85269's finding (crossconv_flag =
true requires sfft_use_segmentation = true: SFFT's decorrelation sigmas come
from the mask's background pixels and it raises only when that set is
empty, so without segmentation ~20% of depth is lost silently), the
per-data-set mask values dev e3c1595 made explicit, and that segmentation
is independent of cross-convolution. Comments only: parsed content and the
release digest are unchanged.

_sfft_argv logs a warning for the crossconv-on / segmentation-off
combination. Loud rather than fatal; whether it should refuse is an open
team item. Port-followup items A5 and A6 (dev 7d85269).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…talogue timings, psfcat reads, ZOGY caveats, generator order

Port-followup items A7-A10, each small and none changing a product:

A7 referenceImageSubs.py — elapsed-time prints around
generateSExtractorReferenceImageCatalog and generatePhotUtilsReferenceImageCatalog
in the existing '=====>' style (dev f07e1ce); the 8/13/26 rimtimsim test
showed reference PhotUtils catalogue generation dominating that pipeline.

A8 stages/science.py — the five DAOStarFinder tuning parameters are read
through context.science_value(psfcat_diffimage, ...) so a missing key is
a named ConfigError, matching 349acf1's rule for the SFFT keys; both call
sites now say the one fact that makes the change reviewable: only
min_separation (1.0 vs the 0.0 default) alters behaviour, and why.

A9 stages/science.py run_zogy — the release toggle is coerced with
sfftCommandSubs.config_bool rather than bool(); the two caveats carried
from dev are recorded beside the call (maps NaN-filled with the clipped
scatter in prepare_zogy_inputs; scalefacref deliberately not applied on
the gain-matched map path).

A10 generateInjectionCatalogsForSims.py — docstring no longer carries the
literal 'injection_catalogs_subdir' text that produced dev's URL bug; the
field union is sorted so catalogues are generated in a deterministic order.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The two converters, the injector and the rimtimsim registrar carried the
2026-07-09 socsim and 2026-04-01 rimtimsim bucket names as code defaults
while the .ini's injection_catalogs_subdir had already moved to the
2026-08-11 catalogues — a mixed state a run would not have noticed. Bucket
names are now REQUIRED from the environment (INPUTBUCKET / OUTPUTBUCKET;
the injector's input stays the fixed public SOC-simulation release), in the
fail-closed shape database/sims/db_register_socsim_files.py already uses
(a5108ec): a forgotten variable on an unattended run exits 64 with the
reason on stdout instead of silently converting or admitting some other
dataset. The current set names (dev 3f22b1b, c3cd546, d3c2317,
f2ce484) are recorded in comments for the run manifest, not as defaults.

Where this is heading (team decision recorded with port-followup item
A11): per-simulation release content will carry the science-affecting
values (gain, read noise, zero points, injection ranges, catalogue
subdirectory) in per-release toml files, per the toml's own placement
rule; bucket names cannot alter a science product and belong with the
data — the sealed generation's _manifest.json that the staging procedure
already writes, which these scripts should eventually read instead of the
environment. The required variables are the bridge until then.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…guard returns None; ordering docstring — digest re-ratified

One ratified edit to the frozen file (rule 17), authorised by Emily for
this branch on the strength of the 2026-09-09 diff-level audit, absorbing
three fixes to existing methods and adding no capability:

* _register_numpy_adapters (d4b7baa) registered numpy.float16/32/64 as
  AsIs(repr(float(value))), so NaN and Infinity became the bare SQL tokens
  nan / inf — 'column nan does not exist' on every row carrying one
  (skymean, zptmag, WCS terms). numpy.float64 had adapted correctly via
  psycopg2's subclass fallback until that exact-type registration overrode
  it. Reproduced: adapt(np.float64('nan')) -> b'nan' after importing this
  module. Floats now go through psycopg2's own Float adapter:
  'NaN'::float / 'Infinity'::float, plain text otherwise. Contract test
  gains the non-finite assertions (4 pass against the real driver).

* execute_sql_queries' empty-list guard (6a2cce9) returned [] with
  exit_code 64; none of the 15 call sites checks exit_code, so a caller
  error read as 'found nothing'. It returns None, like the query-failure
  path. The duplicated inner records = [] is removed (dev 0cad34b moved
  that line; 6a2cce9 added a second copy).

* get_overlapping_l2files' docstring said 'ordered by distance from tile
  center'; the query has ordered by mjdobs then distance since 87156b5.

RAPID_DB_BRANCH_POINT_SHA256 -> 8d7dc0fc…, with the ratification note
listing exactly the above and the previous value. The second digest test's
branch-diff form will report 'modified on this branch' until this lands on
smdc; that is the deliberate friction, accepted for this one merge.
Port-followup items B12, B16, B23.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… name the date when a date-scoped phase gathers nothing

live_w9_ramp.py defaulted W9_PROC_DATE to the OBSERVATION window's start
date (b2335bb). pipeline/operator/gathering.processing_date_for records
that rule as refuted live: the post-DB chain enumerates against
diffimages.created — the day the pipeline did the work — so the old
default matched nothing, gathered zero units and exited 0 with 'nothing
ready in the window'. A ramp step that submitted nothing looked like a
passing ramp step. The default is now processing_date_for(None), the same
function the operator daemon uses; W9_PROC_DATE still overrides it.

A zero-unit gather for a date-scoped phase now prints a WARNING naming the
processing date and the override. Exit stays 0: crossmatch legitimately
gathers nothing until catalog-load completes, and that case must not fail
the ramp. Port-followup item B13.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e overlapfields comments

e578ecc added traceback.print_exc() to the socsim registrar's per-file
handler so a failure names its call site; print_exc() writes to stderr,
and the per-thread log fh is the artifact an operator reads after a
ProcessPoolExecutor run. The formatted traceback is written to fh as well.

The overlapfields comments in all three registrars said 'rapid_systems
migration 100 gives l2files a column'; the migrations were renumbered
(4c5d2d1) after the collision with main's 100 and the column is 101.
e633319 fixed the code references and left these three sentences.
Port-followup items B18, B19.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ce the row records

Both sweeps called catalog_db.delete_superseded_rows with merges.sid as the
join column against an IMAGE identity — diffimages.pid for the merge sweep,
l2files.rid for the source sweep — and the source sweep deleted from
merges_<field>, the merge sweep's own table. sid is a sources sequence
value; comparing it with an image id deletes whichever rows happen to
collide. Verified on PostgreSQL 18.6 with colliding ids (2026-09-15): the
merge sweep deleted a row under a PINNED image (vbest 2) and a current row
and kept the demoted one; the l2files form deleted every row. This is the
defect the 2026-09-12 project ruling unregistered both sweeps from the
operator daemon for.

Two purpose-built deletes replace the generic call at the two sites:

  delete_superseded_merge_rows(cursor, merges_<field>)
    DELETE ... WHERE NOT EXISTS (SELECT 1 FROM sources s JOIN diffimages d
    ON d.pid = s.pid WHERE s.sid = m.sid AND d.vbest IN (1, 2))
  delete_superseded_source_rows(cursor, field)
    DELETE FROM sources s WHERE s.field = %s AND NOT EXISTS (SELECT 1 FROM
    diffimages d WHERE d.pid = s.pid AND d.vbest IN (1, 2))
    — the sources children are per (date, SCA) and the unit is per field,
    so the DELETE targets the inheritance parent; EXPLAIN shows PostgreSQL
    routing it to every child. This is dev's pruneNotBestSources intent
    (0d67e4e), which nothing on smdc performed until now.

On the same check both corrected deletes removed exactly the demoted row,
in either order. vbest IN (1, 2) is 'current' because migration 006
defines 2 as a LOCKED best the update functions refuse to demote; dev's
scripts express the same rule as 'delete where vbest = 0'. The post-write
verification re-runs each corrected predicate as a count; the one-hop
verifier is removed. The generic delete_superseded_rows stays for its
existing tests. Three new stub tests pin the two-hop shape and refuse the
one-hop one.

The operator daemon's registry still leaves both sweeps out; re-registering
them is the team's decision, and the three comments describing the defect
now say it is corrected. Port-followup item B14.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ad images, and prune objects with no current detection

compute_statistics aggregated the raw merges-sources-diffimages join. Bulk
COPY cannot enforce per-row uniqueness, so two sources close enough to
hash to one aid in one difference image both land (dev 3a4cbfc names the
cause), and both were averaged and counted. The aggregate now reads a
DISTINCT ON (aid, pid, isdiffpos) derived table ordered by qfit ASC, sid
ASC: one point per object per image per sign, the best fit kept, positive
and negative detections of one object never collapsed. Dev 39ac549 added
exactly this and dev's own 0feead1 dropped it when rewriting over
enumerated children; smdc is better than dev's tip here and says so.

Two more terms:  — registration's good/bad QA flag on the
difference image (006-core-tables; pipeline/registration/products.py
writes 1 unless release content says otherwise), dev 00afb0d's term
applied to the image the source came from; and, in the same transaction,
astroobjects_<field> rows with no statistics row are deleted (dev's step
5): with the currency join that population grows on every demotion, and
such an object is one nothing can measure. Its merge rows are the merge
sweep's to remove by the same rule.

Verified on PostgreSQL 18.6 (2026-09-15) before the tests were written: a
duplicate pair collapses to the lower-qfit row (nsources 2, meanflux
107.5, not 3), a positive/negative pair on one image counts as two, an
object whose only detection is on a demoted image gets no row and is
pruned. Effect record gains orphan_objects_removed. Three new stub tests
pin the shape. Port-followup item B15.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The round-trip test exempts the whole [science] section as 'authored'
because it was relocated from SSM rather than extracted from the .ini —
true of min_images_to_coadd, not of refimage_psf_filename, which
36e4995 added as the toml twin of [JOB_PARAMS] refimage_psf_filename
(forcedPhotometryForField.py still reads the .ini copy). The two homes
were bound by nothing; b2335bb's message records that they agreed by
luck. A drift changes which PSF every difference is made with. One
targeted assertion pins them, as the [ref_image] twin already is.
Whether [ref_image] should return to dev's north-up template is the
open item recorded on the plan; the value is not changed here.
Port-followup item B17.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6fd9335 made record_l2file_admission refuse to guess a checksum's
algorithm after the 2026-09-09 D6 socsim run mislabelled 444 md5 digests
as sha256. Every layer under it kept the same default: the repository's
add_manifest_entry and admit_l2file, the identity helpers
normalized_checksum / l2file_payload / l2file_identity, and the bridge's
own enumerate_source. The repository is reachable without the bridge
(backfill_g0001_admission.py calls it directly), so a caller there would
have met the identical silent mislabel.

- checksum_algorithm is now a required keyword on both repository methods
  and on l2file_identity; normalized_checksum and l2file_payload take it
  as a required positional; enumerate_source requires algorithm.
- Every production caller already passed it explicitly. The 35 contract
  test call sites that leaned on the default now say "sha256", which is
  what their 64-hex fixtures are.
- The AST sweep in test_admission_bridge_checksum_algorithm now covers
  admit_l2file, add_manifest_entry and enumerate_source as well, and the
  signature check is parametrised over all seven functions.

Not changed: the DEFAULT 'sha256' on the two checksum_algorithm columns
in rapid_systems migration 051. The repository always writes the column,
so the default is never exercised; dropping it is a migration and a team
decision.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…truncating casts

Four findings from the 2026-09-09 audit of db_register_sciimg_psfs.py
and pipeline/repositories/psfs.py, all still present on smdc:

- --filter and --fid were never checked against each other. The filename
  token was matched to --filter, but nothing confirmed --fid names that
  band in Filters, so `--filter f146 --fid 7` would have filed eighteen
  F146 PSFs under Z087. PsfRepository.filter_name reads the Filters row
  and the script compares the three-digit wavelength code (Filters says
  W146, the files say f146) before the first write; a mismatch or an
  unknown fid exits 64 with nothing registered.
- generation_is_sealed answered False to EVERY ClientError, so a missing
  read grant read as "abandoned or still being staged". Only
  404/NoSuchKey/NotFound means unsealed now (the classification
  pipeline/runtime/boundaries.py uses); anything else is re-raised and
  main() reports it as "unknown, not no" with exit 66.
- The mkdtemp download directory was never removed; it is a
  TemporaryDirectory released once the checksums are in hand.
- The repository cast filename and checksum to character varying(255)
  and (32). An explicit bounded cast truncates silently in PostgreSQL;
  the stored functions take unbounded varchar and psfs.filename is text
  since migration 023, so the casts are unbounded and an over-long value
  is refused by the column instead of shortened.

The test module stubbed psycopg2 as a bare module, which cannot satisfy
rapid_db's `import psycopg2.sql`; the script now imports its database
modules and boto3 inside main(), after argument checking and the dry run,
so the pure helpers and their tests import anywhere without stand-ins.
New tests cover the seal classification, the filter/fid agreement, the
Filters read-back and the absence of bounded casts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The declination span was taken from the boundary alone (corners plus
each edge's own extremum). When a pole lies strictly inside the image
that span stops short of 90, so the cap tile and any ring above the
boundary's highest declination were skipped. Measured on a 4088-px SCA
centred on the pole: the cap was reported at |dec| = 89.98 and missed
from 89.985 on. test_over_the_pole sat at 89.98, 0.005 deg from the
failure, and asserted membership only.

- `_contains` tests whether a pole lies inside the spherical
  quadrilateral: same side of all four edge planes AND in the image's
  own hemisphere. The hemisphere check is load-bearing; the same-side
  test alone admits the antipode, and without it a south-pole image took
  the north branch and reported every ring on the sky (caught by the new
  tests before commit).
- When a pole is inside, the result is the cap plus every tile of every
  ring down to the boundary's far edge: whole rings, so a few tiles over
  near the pole and never one under. Away from the poles nothing
  changes: 1,500 random pointings, both insets, both hemispheres up to
  |dec| 89.9, are identical to the previous module, and the pinned
  sliver footprint is unchanged.
- Tests: both caps at 89.98 / 89.985 / 89.99 / 89.999 / 90-1e-9; the
  sampled-subset invariant and the whole of ring 1 for pole-containing
  images; monotonicity across the pole leaving the inset rectangle; an
  image beside the pole unchanged; one pole never claims the other.

Documentation, in the module and the backfill:
- The 101 column comment says "EXACT"; read it as exact away from a pole
  and conservative over one (the comment itself lives in rapid_systems).
- One column, two callers: registrars pass each file's header NAXIS,
  the backfill the release's naxis1/2_sciimage (l2files stores no
  extent). Equal for every data set so far; another detector size must
  be registered, not backfilled.
- The full 101 -> 102 -> deploy rapid -> backfill -> 103 -> 104
  (--acknowledge-coordination) -> 105 sequence, written down once, with
  the note that the smdc registrars need 102 and have no gate.
- `field = ANY(overlapfields)` is true by construction for this writer;
  the falsifiable check is the backfill's preflight on the un-unioned
  geometry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@eramey16
eramey16 requested a review from rusholme September 15, 2026 22:43
@rusholme
rusholme merged commit e566af9 into smdc Sep 19, 2026
1 of 2 checks passed
@rusholme
rusholme deleted the smdc-port-followups branch September 19, 2026 20:45
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.

2 participants