jwst-vmpt 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,638 @@
1
+ # Changelog
2
+
3
+ All notable changes to vMPT are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
5
+ project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [1.2.2] — 2026-06-03
8
+
9
+ Packaging release: vMPT is now **pip-installable** as `jwst-vmpt`
10
+ on TestPyPI / PyPI.
11
+
12
+ ### `pip install`
13
+
14
+ ```bash
15
+ pip install jwst-vmpt # PyPI (when promoted)
16
+ pip install -i https://test.pypi.org/simple/ jwst-vmpt # TestPyPI
17
+ vmpt # opens at http://localhost:5006/app
18
+ ```
19
+
20
+ The console script `vmpt` accepts the same flags as `run.sh`:
21
+ `--port`, `--fits`, `--jpg`, `--wcs`, `--catalog` (repeatable). A
22
+ new `vmpt examples download [DIR]` subcommand pulls the two example
23
+ datasets (`example_a370`, `example_r0600` — together ~64 MB) from a
24
+ GitHub release asset on demand, so the pip wheel itself stays at
25
+ ~20 MB (only the required MSA grid + per-shutter dispersion table
26
+ are bundled).
27
+
28
+ ### Repo restructuring
29
+
30
+ - The Bokeh app directory renamed from `app/` to `vmpt/` so it
31
+ doubles as the Python import package. All `from app.X` imports
32
+ rewritten to `from vmpt.X` (17 files, ~56 references).
33
+ - `data/*.npz` moved to `vmpt/data/*.npz` so the wheel ships them
34
+ alongside the modules. Path lookups in `vmpt/msa.py` and
35
+ `vmpt/wavelengths.py` adjusted (one fewer `parent`).
36
+ - `run.sh` updated to `bokeh serve vmpt/` for source-tree users; no
37
+ behavioural change.
38
+ - New top-level files: `pyproject.toml` (PEP 517/518 metadata + the
39
+ `vmpt` console script), `MANIFEST.in` (sdist completeness),
40
+ `vmpt/cli.py` (entry point).
41
+ - `.gitignore` gains `build/`, `dist/`, `.eggs/` for the pip build
42
+ flow.
43
+
44
+ ### No behavioural changes
45
+
46
+ Tests still pass at **139 / 4 skipped**. The Bokeh app behaves
47
+ identically to v1.2.1; only the install path / directory name
48
+ changed.
49
+
50
+ ## [1.2.1] — 2026-06-03
51
+
52
+ Patch release. Two real bug fixes on top of v1.2.0's collision-
53
+ protection feature, plus a substantial UI cleanup that came out of
54
+ a hands-on review.
55
+
56
+ ### Collision-protection fixes
57
+
58
+ - **Row tolerance is now slitlet-aware.** v1.2.0 hard-coded
59
+ ``|Δs| ≤ 1`` between source centres, but a 3-shutter slitlet at
60
+ row ``s_p`` already occupies rows ``{s_p−1, s_p, s_p+1}`` and the
61
+ user-requested rule is "no other shutter at s_p±2 either." The
62
+ evaluator now computes two tolerances at construction time:
63
+ - Protected slitlet ↔ stuck-open (single shutter):
64
+ ``|Δs| ≤ half + 1``
65
+ - Protected ↔ another slitlet (same slit_length):
66
+ ``|Δs| ≤ 2·half + 1``
67
+ where ``half = slit_length // 2``. So the default ``slit_length=3``
68
+ now correctly forbids stuck-open or other slitlets at rows
69
+ ``s_p±2``. For ``slit_length=5`` the buffer scales up to
70
+ ``s_p±3`` (stuck-open) or ``s_p±5`` (other slitlets).
71
+ ``SHVAL_S_TOLERANCE = 1`` is preserved as the per-individual-
72
+ shutter constant for the live-canvas orange overlap glyph (each
73
+ opened shutter contributes its own ±1 zone, so the visualization
74
+ already paints the correct envelope around a multi-shutter slitlet).
75
+ - **Advanced settings modal sits above the new config modal.**
76
+ When the optimizer config dialog was added in this release the
77
+ Advanced settings card stayed at the same z-index, so opening
78
+ Advanced from inside Configure showed nothing — the config card
79
+ drew on top. Bumped Advanced backdrop / card to ``z-index`` 1001 /
80
+ 1002.
81
+
82
+ ### Pointing-tab UI moved into a dialog
83
+
84
+ The Pointing tab used to stack 10+ optimizer-config widgets, and the
85
+ ``Run optimization`` button slid below the fold on any window under
86
+ ~1200 px tall. The whole block now lives in a centered modal:
87
+
88
+ - The Pointing tab shows a single primary ``Open optimizer…`` button.
89
+ - The modal (``opt_config_modal_card``) contains every optimizer-
90
+ config widget plus ``Run`` / ``Cancel`` and the live status line.
91
+ - The existing progress + results modal flow (``opt_modal_card``) is
92
+ unchanged after ``Run`` is clicked — the config card just dismisses
93
+ itself first.
94
+ - Both the optimizer config and the catalog editor modals gained a
95
+ top-right ``×`` dismiss button.
96
+
97
+ ### Help / status text — context-aware
98
+
99
+ - The help panel on the right side of the canvas is now **collapsed
100
+ by default**. The toggle button stays in place; one click on
101
+ ``Show help`` restores width to its v1.2.0 size with the Quick
102
+ guide + rotating tip. The figure uses fixed
103
+ ``frame_width``/``frame_height`` so the canvas pixel aspect doesn't
104
+ change when the panel collapses / expands.
105
+ - The Method dropdown's three-line Democracy / Meritocracy /
106
+ Hierarchy blurb is hidden by default; an ``ⓘ What do these mean?``
107
+ toggle reveals it on demand. The dropdown's own option labels
108
+ already carry the one-line summary.
109
+ - The status line under ``Run optimization`` was always reading
110
+ *"Load a catalog with priorities, then click Run."* even when a
111
+ catalog with priorities was loaded. ``_refresh_opt_status_div()``
112
+ now updates it based on (catalog presence, method, priority /
113
+ weight column availability):
114
+ - no catalog → ``Load a catalog (Input tab) before running.``
115
+ - catalog + Democracy → ``Ready · N sources.``
116
+ - catalog + Meritocracy without ``weight`` →
117
+ ``⚠ Meritocracy needs a weight column.``
118
+ - catalog + Hierarchy without ``priority`` →
119
+ ``⚠ Hierarchy needs a priority column.``
120
+
121
+ ### Input / MPT tabs
122
+
123
+ - All path inputs across the Input + MPT tabs now use a unified
124
+ ``_wrap_path_picker`` helper: the path ``TextInput`` is hidden
125
+ behind an ``Edit path`` toggle when empty, and Browse buttons are
126
+ promoted to primary blue. The path **auto-reveals** as soon as
127
+ it's populated (by Browse, by autoload, or by typing), so users
128
+ always see what's loaded — only the empty default is hidden.
129
+ - The MPT tab is grouped into four sections (Import / Save / Load /
130
+ Export) separated by dashed and solid hr dividers, so the 10+
131
+ widgets feel like coherent blocks instead of one long column.
132
+ - Renamed the ``Setting`` tab title to ``Settings`` (singular →
133
+ plural).
134
+
135
+ ### Tests
136
+
137
+ - ``tests/test_optimizer_protection.py`` gains 4 new tests:
138
+ parametrize over ``N ∈ {1, 3, 5}`` to pin the cached tolerances,
139
+ and a regression that an N=3 slitlet drops at least as many
140
+ unprotected sources as N=1 under an H grating. **139 passed, 4
141
+ skipped** in total (up from 135 / 4 in v1.2.0).
142
+
143
+ ## [1.2.0] — 2026-06-03
144
+
145
+ Feature release: **shutter collision protection in the optimizer**.
146
+
147
+ Same-row sources on the same NIRSpec detector half (Q1/Q3 → NRS1,
148
+ Q2/Q4 → NRS2) disperse onto overlapping detector pixels when their
149
+ V2 separation is smaller than the spectrum's V2 half-extent
150
+ (`app.wavelengths.v2_overlap_distance` — 35″ for PRISM, ~500″ for
151
+ the H gratings). Until now the optimizer counted both members of
152
+ every such pair as observable; the live canvas already painted the
153
+ loser orange, but the score didn't reflect that downstream
154
+ penalty. v1.2.0 wires the same collision check into the optimizer's
155
+ per-pointing scoring so the user can mark high-priority targets as
156
+ **protected** and have the optimizer steer them into rows free of
157
+ collisions.
158
+
159
+ ### Optimizer core (`app/optimizer.py`)
160
+
161
+ - `PointingEvaluator` accepts new keyword args: `protect_mask`,
162
+ `priorities`, `weights`, `disperser`, `filt`, `reason`. When
163
+ `protect_mask` is None (the default), behaviour is identical to
164
+ v1.1.1 — the existing 16 optimizer tests are unchanged.
165
+ - A new method `evaluate_with_stats(...)` returns the existing
166
+ 3-tuple plus the count of sources dropped by the collision rules
167
+ at this pointing. `evaluate(...)` still returns the 3-tuple but
168
+ its `detected` mask is now the **kept** mask (post-drop) when
169
+ protection is configured, so callers that score via `det.sum()`
170
+ pick up collision filtering for free.
171
+ - Three rules, applied in order at every pointing:
172
+ 1. **Protected ↔ stuck-open** — a protected source landing on a
173
+ row colliding with any shutter flagged as stuck-open (REASON
174
+ == 2 in the CRDS `msaoper` file) is dropped. Stuck-opens
175
+ always disperse light onto the detector regardless of which
176
+ slitlets the user opens, so the protected target's spectrum is
177
+ unavoidably contaminated.
178
+ 2. **Protected ↔ protected** — within each colliding cluster the
179
+ lowest-priority-number source wins. Ties on priority break on
180
+ higher weight; ties on weight break on lower index (stable).
181
+ Losers are dropped; winners continue to provide collision
182
+ pressure on the next rule.
183
+ 3. **Protected ↔ unprotected** — every unprotected source whose
184
+ row collides with any still-kept protected source is dropped.
185
+ - Dropped protected sources do **not** propagate collision pressure
186
+ to rules 2/3 — if a high-priority spectrum is already
187
+ contaminated we won't compound the loss by also blocking
188
+ unprotected sources in the same row.
189
+
190
+ ### Pointing-tab UI (`app/main.py`)
191
+
192
+ - New **"Protect spectra from collision"** group in the optimizer
193
+ sidebar (just below the existing Priority cutoff input):
194
+ - Checkbox: **Enable collision protection**.
195
+ - Radio: **By priority ≤** | **By weight ≥** (mutually exclusive).
196
+ - **Threshold** text input.
197
+ - Live status line: e.g. *"12 protected · 240 other (G140H /
198
+ F100LP · V2 overlap ≈ 500″)"* — updates as you toggle the
199
+ checkbox, switch the radio, type a threshold, or change the
200
+ current Disperser/Filter.
201
+ - `_rebuild_merged_catalog` now propagates `weight` when multiple
202
+ catalogs are stacked — previously single-catalog mode kept weight
203
+ (it pointed at the original `Catalog` object) but merged mode
204
+ dropped it, so the multi-catalog "By weight ≥" rule would have
205
+ silently selected zero sources.
206
+
207
+ ### Results modal
208
+
209
+ - When protection is enabled the Score cell gains a **`−K`**
210
+ suffix where K = number of collision-dropped sources at this
211
+ pointing. The Score column is widened by ~36 px so the suffix
212
+ doesn't ellipsis-truncate.
213
+ - The hover top-10 prefixes protected sources with **🛡** so the
214
+ user can verify which sources are providing collision pressure.
215
+ A trailing line in the tooltip explains the −K count.
216
+ - Header summary line picks up a **"🛡 collision protection ON"**
217
+ badge with a one-line explainer.
218
+
219
+ ### Tests
220
+
221
+ - New `tests/test_optimizer_protection.py` — 11 tests covering
222
+ backwards compatibility, input validation, all three drop rules,
223
+ cross-detector-half non-collision, stuck-open handling, and the
224
+ invariant that protection can only reduce a pointing's score
225
+ (never increase it). 3 tests skip gracefully when synthetic
226
+ sources don't happen to land in the geometry the test exercises.
227
+ - Existing test suite unchanged: **135 passed, 4 skipped** total
228
+ (up from 124/1).
229
+
230
+ ### Notes / known limitations
231
+
232
+ - For the H gratings the V2 overlap distance is ~500″ — comparable
233
+ to the full MSA — so even one protected target rules out a large
234
+ fraction of co-observable sources. The result is physically
235
+ truthful, not a bug; the modal shows the lower kept count so
236
+ expectations match reality.
237
+ - Unprotected sources whose rows collide with a stuck-open shutter
238
+ are NOT dropped from scoring (only protected sources have the
239
+ contamination penalty applied to them). This matches the
240
+ user-requested semantic: protection is a high-priority-only
241
+ feature, not a universal contamination filter.
242
+
243
+ ## [1.1.1] — 2026-06-03
244
+
245
+ Patch release. Polish + several real bugs in the v1.1.0 optimizer
246
+ and catalog editor. The big change is that **Hierarchy mode actually
247
+ optimises lower tiers now**, plus a much richer results table.
248
+
249
+ ### Optimizer
250
+
251
+ - **Slitlet centre is now right under the target.** The optimizer's
252
+ `axy_to_shutter` returns 0-based fractional indices, but
253
+ `_add_slitlet` expects 1-based — the missing `+1` was opening every
254
+ slitlet one row up and one column to the left of the target. Now
255
+ centred correctly.
256
+ - **Confirm dialog before Apply.** Clicking **Apply #N** opens a
257
+ browser confirm: "This will CLEAR all previously open shutters and
258
+ replace them with the optimizer's slitlets." OK → clears + applies
259
+ (single Undo step); Cancel → no-op. Wired via `Button.js_on_click`
260
+ → hidden trigger `TextInput` → Python handler. The trigger pattern
261
+ is needed because `CustomJS.args` only accepts Bokeh Model
262
+ instances, not floats — that's why the Apply button silently did
263
+ nothing in v1.1.0; embed per-button scalars via Python f-string
264
+ interpolation into the JS body.
265
+ - **Hierarchy mode now genuinely optimises every priority tier.**
266
+ Previously DE refinement used `weights = 1 at top tier, 0
267
+ elsewhere`, so DE happily slid to any pointing that kept the
268
+ top-tier count even if it lost lower-tier sources in the process.
269
+ DE now uses **auto-derived lex weights** (smallest int weights
270
+ such that any higher tier strictly outweighs the sum of all lower
271
+ tiers); their sum is a lex-equivalent scalar that DE maximises
272
+ without violating priority ordering. The grid + multi-stage filter
273
+ phase is unchanged.
274
+ - **Results table shows tier breakdown.** For Hierarchy, the Score
275
+ column reads e.g. `P0:4 · P1:12 · P2:30 (46)` — per-tier source
276
+ count + total in parens. For Meritocracy, `Σw 287.0 (46)`. For
277
+ Democracy, just the count `46`.
278
+ - **Hover any Score cell** to see the top 10 placed sources at that
279
+ pointing, sorted by priority ascending then weight descending —
280
+ IDs + P + W per line.
281
+ - **Modal widened** to 740 px to fit the new columns; Score column
282
+ width is method-specific; cells now `overflow: hidden +
283
+ white-space: nowrap + text-overflow: ellipsis` so a label that
284
+ overruns its column truncates instead of wrapping under the row.
285
+
286
+ ### Catalog editor
287
+
288
+ - **Numeric sort on Priority + Weight.** Both columns are now stored
289
+ as floats with NaN for missing (was strings → `"10"` < `"2"`
290
+ lexicographically). An HTMLTemplateFormatter renders the cell as
291
+ a rounded integer or blank; cell edits via StringEditor are
292
+ coerced back to float in `_on_cat_edit_data_change` so the column
293
+ stays a sortable numeric.
294
+ - **After a header click, the table scrolls to row 1.** Document-
295
+ level click delegate on `.slick-header-column` resets the table's
296
+ `.slick-viewport.scrollTop` to 0 (with an 80 ms delay so the
297
+ re-render finishes first).
298
+ - **CSV save** uses `_fmt_int_or_blank` for Priority + Weight so
299
+ the output is `5` not `5.0` and blanks stay blank.
300
+ - **`Compute w from p` / `Compute p from w`** write floats to the
301
+ source (was strings) so the new column stays numerically sortable.
302
+
303
+ ### Misc
304
+
305
+ - The `compute_weights_from_priorities` helper now correctly
306
+ satisfies BOTH `w(p) > w(p+1)` AND `N(p)·w(p) > N(p+1)·w(p+1)`
307
+ using `max(w_prev + 1, n_prev * w_prev // n_q + 1)` as the
308
+ smallest integer that dominates the prior class (regression-tested
309
+ in `tests/test_catalog_ops.py`).
310
+ - Loader: empty cells in numeric columns now properly become NaN
311
+ even when the source column was masked-int (previously came
312
+ through as 0).
313
+ - A couple of additional patterns added to `.gitignore` so stray
314
+ personal files in the repo root can't accidentally be staged.
315
+
316
+ ### Tests
317
+
318
+ - 124 passing, 1 skipped (same as v1.1.0; no test regressions).
319
+
320
+ [1.1.1]: https://github.com/fengwusun/vMPT/releases/tag/v1.1.1
321
+
322
+ ## [1.1.0] — 2026-06-02
323
+
324
+ Headline feature: a complete **MSA pointing optimizer** with three
325
+ methods (Democracy / Meritocracy / Hierarchy), plus an editable,
326
+ sortable catalog editor. Several quality-of-life improvements
327
+ elsewhere.
328
+
329
+ ### MSA pointing optimizer
330
+
331
+ - New panel at the bottom of the **Pointing** tab. Searches over
332
+ (ΔRA, ΔDec, ΔPA) for an (RA, Dec, V3 PA) that maximises a
333
+ user-selectable objective. Re-implemented in vMPT style from
334
+ [hMPT](https://github.com/zihaowu-astro/hMPT) (Eisenstein, McCarty,
335
+ Wu; CfA / Harvard); see `app/optimizer.py` for attribution +
336
+ algorithm notes.
337
+ - **Three methods**:
338
+ - **Democracy** — raw source count; ignores priority and weight.
339
+ - **Meritocracy** — sum of `weight` of placed sources (MPT-style).
340
+ Requires a populated weight column.
341
+ - **Hierarchy** — strict priority-tier lex ordering (eMPT-style).
342
+ Multi-stage filter: a higher-priority source is never traded for
343
+ any number of lower-priority sources.
344
+ - **Pop-up modal** with an animated striped progress bar, a spinning
345
+ ring, and a status line showing the current phase
346
+ (`Grid: 5,200 / 20,000 · 4.2s elapsed · ~12s left`,
347
+ `Hierarchy filter: tier 2 / 4 (p=1) — survivors: 18`,
348
+ `Refining top 10: 3 / 10 · 7.4s elapsed`).
349
+ - **Results table** with the top-10 distinct solutions (near-
350
+ duplicates collapsed). Each row pairs score + (ΔRA, ΔDec, ΔPA)
351
+ with an **Apply #N** button.
352
+ - **Apply #N** sets the pointing AND opens an N-shutter slitlet
353
+ (N from the Setting tab) at every observable target's shutter,
354
+ auto-tagged with the catalog source ID. One Undo step reverts
355
+ the whole apply.
356
+ - **ΔX = 0 freezes the axis** — set ΔPA = 0 to search RA/Dec only
357
+ at the current roll, etc. Both the grid sweep and the DE
358
+ refinement honour the freeze.
359
+ - **Advanced settings…** modal exposes grid resolution (n_RA, n_Dec,
360
+ n_PA), DE max iterations, objective (count/flux), source σ, and
361
+ the APT DVA θ.
362
+
363
+ ### Catalog editor
364
+
365
+ - New **Edit catalog…** button in the Input tab opens a sortable,
366
+ in-cell-editable spreadsheet pop-up.
367
+ - Single-click any cell to edit. Tab / Enter commits; Esc cancels.
368
+ - Drag inside a cell to highlight text; Cmd/Ctrl-C / Cmd/Ctrl-V
369
+ copy / paste — a custom capture-phase keydown handler bypasses
370
+ SlickGrid's column-copy default so only the selected text is
371
+ copied.
372
+ - 🗑️ icon at the end of each row deletes that row.
373
+ - ↶ Undo / ↷ Redo for every edit, delete, derivation, and
374
+ column add (100-step history).
375
+ - **Column picker** — toggle which columns are visible. Extras
376
+ columns from the source CSV/FITS (the loader now preserves every
377
+ column it didn't claim) live alongside the standard set and can
378
+ be turned on or off.
379
+ - **Add a custom column** via a text input + button. Empty by
380
+ default; useful for `weight`, `reference`, etc. Round-trips
381
+ through Apply changes and Save as CSV.
382
+ - **Compute w from p** and **Compute p from w** buttons derive
383
+ one column from the other:
384
+ - `w(lowest p) = 1`; for each higher-priority class, the smallest
385
+ integer `w(p)` satisfying `w(p) > w(p+1)` AND
386
+ `N(p) * w(p) > N(p+1) * w(p+1)`. Guarantees strict-dominance:
387
+ one source at any tier outweighs every source at all lower
388
+ tiers combined.
389
+ - `p` from `w` groups unique weights descending and assigns
390
+ priorities 1, 2, 3, …
391
+ - **Save as CSV** with a Browse… file picker.
392
+ - **Apply changes & close** commits the working copy to the
393
+ in-memory catalog so the eMPT bundle export reflects edits.
394
+
395
+ ### Catalog model
396
+
397
+ - `Catalog.weight` is now a first-class field (sibling of
398
+ `priority`). Loader detects `weight` / `w` / `wt` / `weights`
399
+ aliases. Empty cells in numeric columns properly become NaN
400
+ (previously masked integer columns silently became 0).
401
+ - Extras columns the loader didn't claim are preserved on the
402
+ `Catalog.extras` dict (object arrays, original column name as
403
+ key) and surfaced through the editor's column picker.
404
+
405
+ ### UI polish
406
+
407
+ - **`run.sh`** gained `--port N`, `--fits PATH`, `--jpg PATH`,
408
+ `--wcs PATH`, `--catalog PATH` (repeatable) flags. Mutual-
409
+ exclusion rules: `--jpg` and `--wcs` come as a pair; `--fits` is
410
+ exclusive with them.
411
+ - **Tabs renamed**: Image → Input, Aim → Pointing, Pick → Setting.
412
+ - **Pointing tab** now also hosts Disperser/Filter (was on the
413
+ former Pick tab). RA/Dec inputs share a row; V3 PA/APA share a
414
+ row; Visibility date + button share a row.
415
+ - **Canvas pixel aspect locked** — `frame_width` / `frame_height`
416
+ match the loaded image's pixel W:H exactly. Window resizes
417
+ letterbox around the canvas; the image is never stretched.
418
+ - **Sequenced autoload**: `run.sh --jpg ... --wcs ... --catalog ...`
419
+ loads the image first and the catalogs strictly after, via an
420
+ `on_complete` callback chain so the catalog overlay never races
421
+ the image's `_set_image_and_recenter`.
422
+ - **Status bar** moved out of the scrollable sidebar column and
423
+ pinned to the bottom-left of the viewport (position:fixed) so
424
+ it can't render on top of tab content.
425
+ - **Optimizer Advanced settings** moved into a pop-up modal.
426
+ - **6 new tips** in the help-panel carousel — `run.sh` args,
427
+ optimizer, catalog editor, multi-catalog, pixel aspect, big-ID
428
+ mod.
429
+
430
+ ### Tests
431
+
432
+ - 124 passing (was 96 at v1.0.1). New coverage: catalog `weight`
433
+ column, mod-1e7 + empty-cell NaN handling, optimizer correctness
434
+ (radec→Axy, quadrant inverse, centration monotonicity,
435
+ Hierarchy-vs-Democracy divergence, dΔ=0 freezes, dedup), the two
436
+ weight↔priority compute helpers in `app/catalog_ops.py`.
437
+
438
+ [1.1.0]: https://github.com/fengwusun/vMPT/releases/tag/v1.1.0
439
+
440
+ ## [1.0.1] — 2026-05-21
441
+
442
+ Patch release. Two large quality-of-life corrections — accurate
443
+ per-shutter wavelength values, and a much friendlier catalog
444
+ loader — plus polish on the catalog UI and overlay defaults.
445
+
446
+ ### Wavelength accuracy
447
+
448
+ - **Per-shutter dispersion table for every (disperser, filter)
449
+ combo**, derived from numerical integration of the pipeline
450
+ reference files via [`spacetelescope/msaviz`](https://github.com/spacetelescope/msaviz).
451
+ Lives at `data/dispersion_cutoffs.npz` (19 MB compressed) and is
452
+ regenerated by `scripts/precompute_dispersion_cutoffs.py`. Replaces
453
+ the old linear V2-shift approximation that was wrong in two ways
454
+ for PRISM:
455
+ - Previous PRISM gap was held at 2.7–3.2 μm everywhere. Real
456
+ gap location varies dramatically across the MSA (5–95 %
457
+ spread: gap_lo 0.65–3.59 μm, gap_hi 3.03–5.02 μm) because
458
+ PRISM dispersion is highly non-linear. The new lookup gives
459
+ msaviz-accurate values per shutter.
460
+ - PRISM endpoints used to drift with V2; in reality they're
461
+ essentially constant (msaviz spread is ~0.01 μm).
462
+ - **Q3 / Q4 PRISM shutters** correctly report "no gap on this
463
+ spectrum" — their spectra fall entirely on one detector.
464
+ - **Grating endpoints** updated to match the pipeline-reference
465
+ `sci_range` instead of the slightly narrower JDox "useful range":
466
+ - G140M/F100LP: 0.97–**1.89** (was 0.97–1.84)
467
+ - G140H/F100LP: 0.97–**1.89** (was 0.97–1.84)
468
+ - G235M/F170LP: 1.66–**3.17** (was 1.66–3.07)
469
+ - G235H/F170LP: 1.66–**3.17** (was 1.66–3.07)
470
+ - G395M/F290LP: 2.87–**5.27** (was 2.87–5.14)
471
+ - G395H/F290LP: 2.87–**5.27** (was 2.87–5.14)
472
+ - G140H/F070LP: **0.70**–1.27 (was 0.81–1.27)
473
+ - **vMPT does NOT depend on msaviz at runtime** — only the
474
+ precompute script does. The shipped npz is everything the app
475
+ needs.
476
+
477
+ ### Catalog loader: looser column matching
478
+
479
+ - **`_norm()`** lowercases, strips bracketed/parenthesised unit
480
+ annotations (`[deg]`, `(deg)`), collapses non-alphanumerics, and
481
+ peels trailing unit/epoch tokens (`deg`, `degrees`, `rad`,
482
+ `arcsec`, `J2000`, `ICRS`, `FK5`). All of these now match RA:
483
+ `RA`, `ra`, `RA[deg]`, `RA(deg)`, `RA_deg`, `RAJ2000`,
484
+ `Right Ascension`, `ALPHA_J2000`, `R.A.[deg]`. Same for Dec
485
+ (including Vizier's `DEJ2000`).
486
+ - **ID resolution** accepts the usual aliases (`id`, `no`,
487
+ `source_id`, `objid`, `srcid`, …) plus permissive fallbacks
488
+ (`name`, `label`, `tag`, `target`, `#`) — fallbacks honoured only
489
+ when values coerce to integer.
490
+ - **Missing ID column → synthesised** sequential IDs 1..N so the
491
+ catalog still loads.
492
+ - **Numeric IDs ≥ 10⁷ are taken mod 10⁷** (`ID_MOD = 10_000_000`)
493
+ so JADES-style 8–9-digit IDs collapse to APT's compact space.
494
+ - **Priority class strings** (`P0`, `P1`, …) and masked numeric
495
+ cells now flow through cleanly — the old loader threw
496
+ `ValueError` on a `P0` priority cell and produced `0.0` for
497
+ masked `mag` / `z` instead of `NaN`.
498
+
499
+ ### Multi-catalog
500
+
501
+ - **Load multiple catalogs at once**. Each gets a colour chip in
502
+ the sidebar list. Toggle visibility per-catalog with a checkbox;
503
+ × to remove; ▲ / ▼ to reorder the visual stack.
504
+ - **Per-catalog marker colours** cycle through an 8-entry palette
505
+ (yellow / magenta / pale green / coral / lavender / sky-blue /
506
+ white / salmon), picked to read clearly on dark fields and avoid
507
+ the other overlay colours.
508
+ - **Z-order by list order** (earlier-loaded catalogs draw on top)
509
+ with **alpha decay** by depth (1.0 → 0.35 floor). Matched-shutter
510
+ targets always render fully opaque so a "picked" marker is never
511
+ visually demoted.
512
+ - Sessions serialise the list (`catalog_paths`) — workspace JSON
513
+ remembers each path and its enabled flag.
514
+
515
+ ### MPT-importable catalog (export)
516
+
517
+ - **Output is now a superset of the input catalog** — every input
518
+ source is included, plus any synthesised entries for slitlets
519
+ without a real match. The `Label` column carries `real` or
520
+ `vMPT_synth` so downstream tools can tell which is which.
521
+ - **Integer IDs only**, extracted as the largest digit run from
522
+ the original token (so `RJ0600-10274-P0` → 10274). The original
523
+ string token is preserved in the `Label` column for traceability.
524
+
525
+ ### Overlay defaults
526
+
527
+ - Operable-shutter stroke: 0.75 px → **1.0 px**.
528
+ - Spectral-overlap fill alpha: 0.10 → **0.20**.
529
+ - Spectral-overlap edge colour now explicitly **orange (#d97a00)**
530
+ — when you reveal the edge via the stroke slider it now matches
531
+ the orange fill instead of Bokeh's default blue-grey.
532
+
533
+ ### Tests
534
+
535
+ - 96 passing (was 63 at v1.0.0). Coverage growth concentrated on
536
+ the catalog loader (column aliases, ID synth, mod-10⁷, string
537
+ IDs, name-as-numeric-ID) and the wavelength model (every
538
+ disperser × filter combo verified against the msaviz table).
539
+
540
+ [1.0.1]: https://github.com/fengwusun/vMPT/releases/tag/v1.0.1
541
+
542
+ ## [1.0.0] — 2026-05-20
543
+
544
+ First public release. The tool is feature-complete for hand-picking
545
+ JWST/NIRSpec MSA shutter configurations on a target field and
546
+ exporting a bundle that loads into APT MPT and the eMPT pipeline.
547
+
548
+ ### Highlights
549
+
550
+ - **Interactive shutter picker** with N-shutter slitlets (N ∈ {1, 2, 3, 5}),
551
+ snap-to-nearest-operable, undo / clear, double-click highlights,
552
+ shift-click to move the pointing, wheel-zoom and pan.
553
+ - **Live overlays** — MSA outline, operable shutters (silver edge),
554
+ stuck-open (dark-red outline), user picks (red fill), spectral
555
+ conflicts (orange fill, stackable), 5 fixed slits (gold), catalog
556
+ targets (yellow / green when matched), lime pointing cross.
557
+ - **APT-ready bundle export** — 6 files per export, with role-prefixed
558
+ filenames (`MPT_*`, `vMPT_*`, `eMPT_*`). The MPT plan JSON matches
559
+ APT's reference schema field-for-field; the `<catalog>.cat` uses
560
+ JDox-recognized column names (ID, RA, DEC, Weight, Primary, Label).
561
+ Labels distinguish `real` catalog rows from `vMPT_synth` synthesised
562
+ entries.
563
+ - **APT plan importer** — load any `MPT_plan.json`, shutter mask CSV,
564
+ local `.aptx` archive, or fetch by JWST program ID directly from
565
+ STScI. Reads multi-plan archives (e.g. program 1208 with 40+ plans).
566
+ - **Bundle round-trip** — Save session → load session restores
567
+ pointing, V3 PA, disperser/filter, every open shutter with its
568
+ `target_id` + `role`, the highlighted set, and the image + sidecar
569
+ paths. Point at either `MPT_plan.json` OR `vMPT_workspace.json` —
570
+ the sibling auto-loads.
571
+ - **Responsive layout** — canvas stretches to fill the browser
572
+ window; sidebar / help panel scroll on overflow; left-sidebar
573
+ fixed at 340 px, right help panel at 340 px.
574
+ - **Rotating tip card** in the help panel (13 hand-written tips,
575
+ 15-second rotation with CSS fade-in).
576
+ - **GitHub version-check on startup** — non-blocking background
577
+ thread compares the local HEAD to `origin/main`; shows a
578
+ dismissible amber notification if the local copy is behind.
579
+ - **Custom favicon** (4 MSA quadrants + lime pointing cross).
580
+ - **One-page summary slide** generator (`build_vmpt_slide.js`,
581
+ pptxgenjs-based).
582
+
583
+ ### Science correctness
584
+
585
+ - **MSA geometry** sourced from `pysiaf` (`NRS_FULL_MSA`); 138.575°
586
+ intra-MSA rotation, V2/V3 reference at (378.563, −428.403).
587
+ - **APA = V3 PA + V3IdlYAngle (mod 360)**; both quantities are
588
+ surfaced in the status bar and editable from the Aim tab.
589
+ - **Operability** read from CRDS `jwst_nirspec_msaoper_*.json` —
590
+ failed-open shutters always disperse and contribute to the
591
+ spec-overlap calculation.
592
+ - **Spectral overlap** — `|Δs| ≤ 1` cross-quadrant via NRS1 (Q1↔Q3)
593
+ and NRS2 (Q2↔Q4) detector pairing; per-grating V2 half-extent
594
+ (PRISM 35″, M-gratings 200″, H-gratings 500″).
595
+ - **Wavelength endpoints** per disperser+filter, clamped to the
596
+ grating's intrinsic range (no spurious PRISM > 5.3 µm tooltips).
597
+ - **Source matching** uses APT's *Unconstrained* Source Centering
598
+ rule (full shutter pitch including bars).
599
+ - **WCS Jacobian** uses `astropy.SkyCoord.spherical_offsets_to` —
600
+ cos(Dec) factor handled correctly at non-equatorial fields.
601
+
602
+ ### Example data shipped
603
+
604
+ - `example_a370/` (43 MB) — JWST NIRCam F182M+F200W+F210M FITS of
605
+ Abell 370, target catalog, GTO-1208 APT MPT plan, shutter-mask CSV.
606
+ - `example_r0600/` (21 MB) — JWST NIRCam F090W+F200W+F444W JPG of
607
+ RXCJ0600 + WCS sidecar + 28k-source target catalog. JPG re-encoded
608
+ at quality 85 (was 251 MB) without changing WCS.
609
+
610
+ ### Tests
611
+
612
+ - 63 tests, ~5 s. Run with `pytest tests/`.
613
+ - Coverage: session bundle round-trip, MPT plan parser (incl. .aptx
614
+ archives), eMPT format byte-compatibility, MPT catalog writer
615
+ format guard, wavelength model, image loaders, end-to-end export.
616
+
617
+ ### Known limitations
618
+
619
+ - `plannerSpecification` block in `MPT_plan.json` carries sensible
620
+ defaults (matching APT's reference schema) but its dither /
621
+ search-grid parameters don't reflect any vMPT internal state —
622
+ APT uses them only as starting values for re-planning.
623
+ - Bokeh single-session state: opening the same server in two
624
+ browser tabs lets picks bleed across them. Use one tab per user.
625
+ - Older `pysiaf` PRD (PRDOPSSOC-068) lags the online version by
626
+ ~0.05″ for some apertures; safe to ignore unless you need
627
+ milli-arcsec geometry.
628
+
629
+ ### Acknowledgements
630
+
631
+ Export-bundle format calibrated against [eMPT](https://github.com/esdc-esac-esa-int/eMPT_v1)
632
+ (Bonaventura et al. 2023, A&A 672, A40). Coordinate plumbing builds
633
+ on `pysiaf` (NIRSpec apertures) and `astropy.wcs`. Visibility
634
+ windows queried via [`jwst_gtvt`](https://github.com/spacetelescope/jwst_gtvt).
635
+ MPT catalog and plan JSON schemas follow the
636
+ [JDox MPT documentation](https://jwst-docs.stsci.edu/jwst-near-infrared-spectrograph/nirspec-apt-templates/nirspec-multi-object-spectroscopy-apt-template).
637
+
638
+ [1.0.0]: https://github.com/fengwusun/vMPT/releases/tag/v1.0.0