Skip to content

Commit 845e20a

Browse files
"Backport PR #9672 on branch release/1.19.x (JP-4067: Fix white_light crash on repeated time stamps)" (#9675)
Co-authored-by: Melanie Clarke <[email protected]>
1 parent bfb5006 commit 845e20a

File tree

3 files changed

+165
-14
lines changed

3 files changed

+165
-14
lines changed

changes/9672.white_light.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a crash caused by repeated time stamps in the spectral table. If found, warn and keep only the first one.

jwst/white_light/tests/test_white_light.py

Lines changed: 153 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010
from jwst.tests.helpers import LogWatcher
1111
from jwst.white_light.white_light import white_light
1212

13+
TIME_KEYS = [
14+
"MJD-BEG",
15+
"MJD-AVG",
16+
"MJD-END",
17+
"TDB-BEG",
18+
"TDB-MID",
19+
"TDB-END",
20+
]
21+
1322

1423
@pytest.fixture(scope="module")
1524
def make_datamodel():
@@ -129,19 +138,12 @@ def make_datamodel():
129138

130139
# set blank times for one integration in order 1 to test warning raise
131140
model.spec[0].spec_table["INT_NUM"][2] = 0
132-
time_keys = [
133-
"MJD-BEG",
134-
"MJD-AVG",
135-
"MJD-END",
136-
"TDB-BEG",
137-
"TDB-MID",
138-
"TDB-END",
139-
]
140-
for key in time_keys:
141+
142+
for key in TIME_KEYS:
141143
model.spec[0].spec_table[key][2] = np.nan
142144

143145
# set one time to a different value for order 2 to test table integration
144-
for key in time_keys:
146+
for key in TIME_KEYS:
145147
model.spec[1].spec_table[key][2] += 0.01
146148

147149
return model
@@ -151,7 +153,7 @@ def test_white_light(make_datamodel, monkeypatch):
151153
"""Test white light step"""
152154
data = make_datamodel
153155

154-
watcher = LogWatcher("There were 1 spectra in order 1 with no mid time (20")
156+
watcher = LogWatcher("1 spectra in order 1 with no mid time or duplicate mid time (20")
155157
monkeypatch.setattr(logging.getLogger("jwst.white_light.white_light"), "warning", watcher)
156158
result = white_light(data)
157159
watcher.assert_seen()
@@ -190,7 +192,7 @@ def test_white_light(make_datamodel, monkeypatch):
190192

191193

192194
def test_white_light_multi_detector(make_datamodel):
193-
"""Test white light step"""
195+
"""Test white light step on data with multiple detectors."""
194196
# Set the detectors in the two spec tables to different values
195197
data = make_datamodel.copy()
196198
data.spec[0].detector = "NRS1"
@@ -237,3 +239,142 @@ def test_white_light_multi_detector(make_datamodel):
237239

238240
assert_allclose(result["whitelight_flux_order_1_NRS1"], expected_flux_order_1, equal_nan=True)
239241
assert_allclose(result["whitelight_flux_order_2_NRS2"], expected_flux_order_2, equal_nan=True)
242+
243+
244+
def test_white_light_duplicate_times(monkeypatch, make_datamodel):
245+
"""Test white light step on data with duplicate time stamps."""
246+
# Set all times to the same value for order 1
247+
data = make_datamodel.copy()
248+
for key in TIME_KEYS:
249+
data.spec[0].spec_table[key][:] = data.spec[0].spec_table[key][0]
250+
251+
watcher = LogWatcher("4 spectra in order 1 with no mid time or duplicate mid time")
252+
monkeypatch.setattr(logging.getLogger("jwst.white_light.white_light"), "warning", watcher)
253+
result = white_light(data)
254+
watcher.assert_seen()
255+
256+
# Expected times match input for the order 2 spectrum
257+
n_spec = len(data.spec[1].spec_table)
258+
mid_times = data.spec[1].spec_table["MJD-AVG"]
259+
expected_tdb = data.spec[1].spec_table["TDB-MID"]
260+
261+
np.testing.assert_allclose(result["MJD_UTC"], mid_times)
262+
np.testing.assert_allclose(result["BJD_TDB"], expected_tdb, equal_nan=True)
263+
assert result["whitelight_flux_order_1"].shape == (len(mid_times),)
264+
assert result["whitelight_flux_order_2"].shape == (len(mid_times),)
265+
266+
# For order 1, duplicate timestamps so only the first flux is kept
267+
assert np.sum(np.isnan(result["whitelight_flux_order_1"])) == n_spec - 1
268+
269+
# check fluxes are summed appropriately
270+
expected_flux_per_spec = np.sum(np.arange(1, 21, dtype=np.float32))
271+
expected_flux = np.array(
272+
[
273+
expected_flux_per_spec,
274+
]
275+
* len(mid_times)
276+
)
277+
expected_flux_order_1 = expected_flux.copy()
278+
expected_flux_order_1[1:] = np.nan
279+
expected_flux_order_2 = expected_flux.copy()
280+
281+
assert_allclose(result["whitelight_flux_order_1"], expected_flux_order_1, equal_nan=True)
282+
assert_allclose(result["whitelight_flux_order_2"], expected_flux_order_2, equal_nan=True)
283+
284+
285+
@pytest.fixture
286+
def wavelengthrange():
287+
"""Mock the wavelengthrange info from reference files."""
288+
orders = [1, 1, 2, 3]
289+
filters = ["CLEAR", "F277W", "CLEAR", "CLEAR"]
290+
wl_min = [0.93, 2.41, 0.64, 0.64]
291+
wl_max = [2.82, 2.82, 0.83, 0.95]
292+
return Table(
293+
data=[orders, filters, wl_min, wl_max],
294+
names=["order", "filter", "min_wave", "max_wave"],
295+
dtype=[int, str, float, float],
296+
)
297+
298+
299+
def test_determine_wavelength_range(wavelengthrange):
300+
"""Test that the wavelength range is determined correctly."""
301+
# retrieve from reference file
302+
wl_min, wl_max = _determine_wavelength_range(1, "F277W", waverange_table=wavelengthrange)
303+
assert wl_min == 2.41
304+
assert wl_max == 2.82
305+
306+
# user-specified values override reference file values
307+
wl_min, wl_max = _determine_wavelength_range(
308+
1, "F277W", waverange_table=wavelengthrange, min_wave=2.0, max_wave=3.0
309+
)
310+
assert wl_min == 2.0
311+
assert wl_max == 3.0
312+
313+
# use default values when no reference file is provided
314+
wl_min, wl_max = _determine_wavelength_range(4, "CLEAR")
315+
assert wl_min == -1.0
316+
assert wl_max == 1.0e10
317+
318+
319+
def test_determine_wavelength_range_no_match(wavelengthrange):
320+
"""Test that an error is raised if no match is found."""
321+
with pytest.raises(
322+
ValueError, match="No reference wavelength range found for order 4 and filter CLEAR"
323+
):
324+
_determine_wavelength_range(4, "CLEAR", waverange_table=wavelengthrange)
325+
326+
327+
def test_determine_wavelength_range_multiple_matches(wavelengthrange):
328+
"""Test that an error is raised if more than one match is found."""
329+
wavelengthrange["order"][2] = 1
330+
with pytest.raises(
331+
ValueError, match="Multiple reference wavelength ranges found for order 1 and filter CLEAR"
332+
):
333+
_determine_wavelength_range(1, "CLEAR", waverange_table=wavelengthrange)
334+
335+
336+
def test_get_reference_wavelength_range(make_datamodel):
337+
"""Test reading of wavelength range reference file."""
338+
wr = WhiteLightStep()._get_reference_wavelength_range(make_datamodel)
339+
assert isinstance(wr, Table)
340+
assert len(wr) > 0
341+
assert "order" in wr.columns
342+
assert "filter" in wr.columns
343+
assert "min_wave" in wr.columns
344+
assert "max_wave" in wr.columns
345+
346+
347+
def test_get_reference_wavelength_range_other_exptype(make_datamodel):
348+
"""Test that non-SOSS exposure types return None."""
349+
model = make_datamodel.copy()
350+
model.meta.exposure.type = "NRC_TSIMAGE"
351+
wr = WhiteLightStep()._get_reference_wavelength_range(model)
352+
assert wr is None
353+
354+
355+
def test_get_reference_wavelength_range_no_file(make_datamodel, monkeypatch, log_watcher):
356+
"""Test that missing wavelength range reference files are handled."""
357+
model = make_datamodel.copy()
358+
monkeypatch.setattr(WhiteLightStep, "get_reference_file", lambda *args: "N/A")
359+
360+
watcher = log_watcher(
361+
"jwst.white_light.white_light_step",
362+
message="No wavelength range reference file found",
363+
level="warning",
364+
)
365+
wr = WhiteLightStep()._get_reference_wavelength_range(model)
366+
watcher.assert_seen()
367+
assert wr is None
368+
369+
370+
def test_call_step(make_datamodel, tmp_cwd, log_watcher):
371+
"""Smoke test to ensure the step at least runs without error."""
372+
watcher = log_watcher(
373+
"jwst.white_light.white_light_step",
374+
message="Using wavelength range reference file",
375+
level="info",
376+
)
377+
result = WhiteLightStep().call(make_datamodel, save_results=True)
378+
watcher.assert_seen()
379+
assert isinstance(result, Table)
380+
assert Path("step_WhiteLightStep_whtlt.ecsv").exists()

jwst/white_light/white_light.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,14 @@ def white_light(input_model, min_wave=None, max_wave=None):
6464
# Get mid times for all integrations in this order
6565
mid_time = spec.spec_table["MJD-AVG"]
6666
mid_tdb = spec.spec_table["TDB-MID"]
67+
68+
# Check for unique time stamps: keep only the first
69+
_, unq_idx = np.unique(mid_time, return_index=True)
70+
is_unique = np.full(mid_time.shape, False)
71+
is_unique[unq_idx] = True
72+
mid_time[~is_unique] = np.nan
73+
74+
# Store time arrays
6775
good = ~np.isnan(mid_time)
6876
if len(mid_times) == 0:
6977
mid_times = mid_time
@@ -88,9 +96,10 @@ def white_light(input_model, min_wave=None, max_wave=None):
8896
if problems > 0:
8997
log.warning(
9098
f"There were {problems} spectra in order {spectral_order} "
91-
f"with no mid time ({100.0 * problems / n_spec} percent of spectra). "
92-
"These spectra will be ignored in the output table."
99+
"with no mid time or duplicate mid time "
100+
f"({100.0 * problems / n_spec} percent of spectra). "
93101
)
102+
log.warning("These spectra will be ignored in the output table.")
94103

95104
# Set up output table, removing problems
96105
tbl = _make_empty_output_table(input_model)

0 commit comments

Comments
 (0)