Skip to content

Commit 9972ca6

Browse files
Ben Mazinclaude
andcommitted
detection_space: one curve and legend entry per unique legend label
Sweeps over parameters the legend label does not encode (the HWO config sweeps scene.planet.contrast, which the 5-sigma contrast curve does not depend on) stacked N visually identical curves with N repeated legend entries. Curves and legend entries now dedupe on the legend label, with a logged notice; byte-exact curve comparison was not usable because the per-cell HOSTS exozodi draw perturbs the curves invisibly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 4dec675 commit 9972ca6

2 files changed

Lines changed: 72 additions & 3 deletions

File tree

nullsim/outputs/plots/detection_space.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,12 +224,27 @@ def _style_axes_atmosphere(axes: plt.Axes) -> None:
224224

225225

226226
def _plot_detection_curves(axes: plt.Axes, runs: RunCollection) -> None:
227-
"""One paper-style detection-mode contrast curve per sweep cell."""
227+
"""One paper-style detection-mode contrast curve per sweep cell.
228+
229+
Cells whose legend label duplicates an already-drawn one are skipped
230+
(with a logged notice): sweeps over parameters the label does not encode
231+
(e.g. scene.planet.contrast) otherwise stack visually identical lines
232+
and repeated legend entries. Only the first such cell is drawn, so
233+
per-cell Monte Carlo scatter (e.g. the HOSTS exozodi draw) is not
234+
averaged, just represented by one cell.
235+
"""
228236

237+
seen: set[str] = set()
238+
n_duplicates = 0
229239
for record in _paper_curve_records(runs):
230240
curve = record.result.state.results.contrast_curve
231241
if curve is None or curve.size == 0:
232242
continue
243+
key = _curve_legend_label(record, mode="det")
244+
if key in seen:
245+
n_duplicates += 1
246+
continue
247+
seen.add(key)
233248
separations_mas = np.asarray(curve["separation_mas"], dtype=np.float64)
234249
contrasts = np.asarray(curve["contrast"], dtype=np.float64)
235250
order = np.argsort(separations_mas)
@@ -272,6 +287,12 @@ def _plot_detection_curves(axes: plt.Axes, runs: RunCollection) -> None:
272287
linewidth=0,
273288
zorder=1,
274289
)
290+
if n_duplicates:
291+
logging.info(
292+
"detection_space: skipped %d sweep cell(s) whose legend label "
293+
"duplicates an already-plotted cell.",
294+
n_duplicates,
295+
)
275296

276297

277298
# ----------------------------------------------------------------------
@@ -508,12 +529,17 @@ def _bin_widths_um(wavelengths_um: NDArrayFloat) -> NDArrayFloat:
508529
def _plot_characterization_curves(axes: plt.Axes, runs: RunCollection) -> None:
509530
"""Overlay char-mode 5σ contrast curves as dashed lines matched in colour."""
510531

532+
seen: set[str] = set()
511533
for record in _paper_curve_records(runs):
512534
payload = _characterization_curve(record)
513535
if payload is None:
514536
continue
515537
separations_mas = np.asarray(payload["separations_mas"], dtype=np.float64)
516538
contrasts = np.asarray(payload["contrast_5sigma"], dtype=np.float64)
539+
key = _curve_legend_label(record, mode="char")
540+
if key in seen:
541+
continue
542+
seen.add(key)
517543
mask = (
518544
np.isfinite(separations_mas)
519545
& np.isfinite(contrasts)
@@ -876,17 +902,31 @@ def _attach_legends(
876902

877903
handles: list[Line2D] = []
878904
labels: list[str] = []
905+
seen_det: set[str] = set()
879906
for record in _paper_curve_records(runs):
880-
if record.result.state.results.contrast_curve is None:
907+
curve = record.result.state.results.contrast_curve
908+
if curve is None:
909+
continue
910+
# Mirror the label dedupe in _plot_detection_curves: one legend
911+
# entry per drawn line.
912+
key = _curve_legend_label(record, mode="det")
913+
if key in seen_det:
881914
continue
915+
seen_det.add(key)
882916
handles.append(
883917
Line2D([], [], color=_DETECTION_COLOR, **_line_style_for_record(record))
884918
)
885919
labels.append(_curve_legend_label(record, mode="det"))
886920

921+
seen_char: set[str] = set()
887922
for record in _paper_curve_records(runs):
888-
if _characterization_curve(record) is None:
923+
payload = _characterization_curve(record)
924+
if payload is None:
925+
continue
926+
key = _curve_legend_label(record, mode="char")
927+
if key in seen_char:
889928
continue
929+
seen_char.add(key)
890930
handles.append(
891931
Line2D(
892932
[],

tests/outputs/plots/test_detection_space_ideal_coronagraph.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,32 @@ def test_inputs_assemble_perfect_optics_rate() -> None:
161161
# I=1.5 solar analog through a clear 8 m primary and QE 0.9 lands at
162162
# a few 1e10 cps; pin the order of magnitude, not the zero point.
163163
assert 1.0e9 < inputs["stellar_rate_hz"] < 1.0e12
164+
165+
166+
def test_degenerate_sweep_draws_one_curve_and_one_legend_entry() -> None:
167+
"""Cells identical in label and curve collapse to a single line/entry.
168+
169+
The HWO config sweeps scene.planet.contrast, which the 5-sigma contrast
170+
curve does not depend on: pre-fix the figure stacked five identical
171+
curves and legend entries.
172+
"""
173+
174+
from nullsim.outputs.plots.detection_space import (
175+
_attach_legends,
176+
_plot_detection_curves,
177+
)
178+
179+
records = tuple(_record(orders=()) for _ in range(5))
180+
collection = RunCollection(
181+
config=records[0].config, records=records, output_dir=Path(".")
182+
)
183+
figure, axes = plt.subplots()
184+
try:
185+
_plot_detection_curves(axes, collection)
186+
assert len(axes.lines) == 1
187+
_attach_legends(axes, (), collection)
188+
legend = axes.get_legend()
189+
labels = [text.get_text() for text in legend.get_texts()]
190+
assert len([lab for lab in labels if "HWO" in lab]) == 1
191+
finally:
192+
plt.close(figure)

0 commit comments

Comments
 (0)