Skip to content

Commit d4f8a11

Browse files
JP-3784: Fix off-nominal pixel_replace and cube_build output names (#9019)
2 parents ec33049 + d65a12d commit d4f8a11

File tree

10 files changed

+148
-48
lines changed

10 files changed

+148
-48
lines changed

changes/9019.pixel_replace.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Base output filenames on input filenames when the step is called outside the pipeline, in order to create sensible names when the input is a list of models.

jwst/cube_build/data_types.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ def __init__(self, input, single, output_file, output_dir):
9898
# Suffixes will be added to this name later, to designate the
9999
# channel+subchannel (MIRI MRS) or grating+filter (NRS IFU) the output cube covers.
100100

101-
102101
if output_file is not None:
103102
basename, ext = os.path.splitext(os.path.basename(output_file))
104103
self.output_name = basename

jwst/cube_build/ifu_cube.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def __init__(self,
5353

5454
self.input_models = input_models # needed when building single mode IFU cubes
5555
self.output_name_base = output_name_base
56+
5657
self.num_files = None
5758

5859
self.instrument = instrument
@@ -168,7 +169,7 @@ def define_cubename(self):
168169
""" Define the base output name
169170
"""
170171
if self.pipeline == 2:
171-
newname = self.output_name_base + self.suffix + '.fits'
172+
newname = self.output_name_base + '_' + self.suffix + '.fits'
172173
else:
173174
if self.instrument == 'MIRI':
174175

jwst/cube_build/tests/test_cube_build_step.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import numpy as np
6+
import os
67
import pytest
78
from astropy.io import fits
89

@@ -13,6 +14,7 @@
1314
from jwst.cube_build import CubeBuildStep
1415
from jwst.cube_build.file_table import ErrorNoAssignWCS
1516
from jwst.cube_build.cube_build import ErrorNoChannels
17+
from jwst.datamodels import ModelContainer
1618

1719

1820
@pytest.fixture(scope='module')
@@ -158,7 +160,7 @@ def nirspec_data():
158160
image.meta.instrument.name = 'NIRSPEC'
159161
image.meta.instrument.detector = 'NRS1'
160162
image.meta.exposure.type = 'NRS_IFU'
161-
image.meta.filename = 'test_nirspec.fits'
163+
image.meta.filename = 'test_nirspec_cal.fits'
162164
image.meta.observation.date = '2023-10-06'
163165
image.meta.observation.time = '00:00:00.000'
164166
# below values taken from regtest using file
@@ -182,12 +184,42 @@ def nirspec_data():
182184
@pytest.mark.parametrize("as_filename", [True, False])
183185
def test_call_cube_build_nirspec(tmp_cwd, nirspec_data, tmp_path, as_filename):
184186
if as_filename:
185-
fn = tmp_path / 'test_nirspec.fits'
187+
fn = tmp_path / 'test_nirspec_cal.fits'
186188
nirspec_data.save(fn)
187189
step_input = fn
188190
else:
189191
step_input = nirspec_data
190192
step = CubeBuildStep()
191193
step.channel = '1'
192194
step.coord_system = 'internal_cal'
193-
step.run(step_input)
195+
step.save_results = True
196+
result = step.run(step_input)
197+
198+
assert isinstance(result, ModelContainer)
199+
assert len(result) == 1
200+
model = result[0]
201+
assert model.meta.cal_step.cube_build == 'COMPLETE'
202+
assert model.meta.filename == 'test_nirspec_g395h-f290lp_internal_s3d.fits'
203+
assert os.path.isfile(model.meta.filename)
204+
205+
206+
@pytest.mark.parametrize("as_filename", [True, False])
207+
def test_call_cube_build_nirspec_multi(tmp_cwd, nirspec_data, tmp_path, as_filename):
208+
if as_filename:
209+
fn = tmp_path / 'test_nirspec_cal.fits'
210+
nirspec_data.save(fn)
211+
step_input = fn
212+
else:
213+
step_input = nirspec_data
214+
step = CubeBuildStep()
215+
step.channel = '1'
216+
step.coord_system = 'internal_cal'
217+
step.save_results = True
218+
step.output_type = 'multi'
219+
result = step.run(step_input)
220+
221+
assert isinstance(result, ModelContainer)
222+
assert len(result) == 1
223+
model = result[0]
224+
assert model.meta.cal_step.cube_build == 'COMPLETE'
225+
assert model.meta.filename == 'test_nirspec_s3d.fits'

jwst/pipeline/calwebb_spec3.py

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
#!/usr/bin/env python
22
from collections import defaultdict
3-
from functools import wraps
43
import os.path as op
54

65
from stdatamodels.jwst import datamodels
76

87
from jwst.datamodels import SourceModelContainer
98
from jwst.stpipe import query_step_status
10-
9+
from jwst.stpipe.utilities import invariant_filename
1110
from ..associations.lib.rules_level3_base import format_product
1211
from ..exp_to_source import multislit_to_container
1312
from ..master_background.master_background_step import split_container
@@ -350,25 +349,3 @@ def _create_nrsmos_source_id(self, source_models):
350349
srcid = f's{str(source_id):>09s}'
351350

352351
return srcid
353-
354-
# #########
355-
# Utilities
356-
# #########
357-
def invariant_filename(save_model_func):
358-
"""Restore meta.filename after save_model"""
359-
360-
@wraps(save_model_func)
361-
def save_model(model, **kwargs):
362-
try:
363-
filename = model.meta.filename
364-
except AttributeError:
365-
filename = None
366-
367-
result = save_model_func(model, **kwargs)
368-
369-
if filename:
370-
model.meta.filename = filename
371-
372-
return result
373-
374-
return save_model

jwst/pixel_replace/pixel_replace.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ class PixelReplacement:
2929
VERTICAL = 2
3030
LOG_SLICE = ['column', 'row']
3131

32-
default_suffix = 'pixrep'
33-
3432
def __init__(self, input_model, **pars):
3533
"""
3634
Initialize the class with input data model.

jwst/pixel_replace/pixel_replace_step.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
#! /usr/bin/env python
21
from functools import partial
32

4-
from ..stpipe import Step
5-
from jwst.stpipe import record_step_status
63
from jwst import datamodels
7-
from .pixel_replace import PixelReplacement
4+
from jwst.pixel_replace.pixel_replace import PixelReplacement
5+
from jwst.stpipe import record_step_status, Step
86

97
__all__ = ["PixelReplaceStep"]
108

@@ -33,6 +31,7 @@ class PixelReplaceStep(Step):
3331
algorithm = option("fit_profile", "mingrad", "N/A", default="fit_profile")
3432
n_adjacent_cols = integer(default=3) # Number of adjacent columns to use in creation of profile
3533
skip = boolean(default=True) # Step must be turned on by parameter reference or user
34+
output_use_model = boolean(default=True) # Use input filenames in the output models
3635
"""
3736

3837
def process(self, input):
@@ -54,7 +53,7 @@ def process(self, input):
5453

5554
if isinstance(input_model, (datamodels.MultiSlitModel,
5655
datamodels.SlitModel,
57-
datamodels.ImageModel,
56+
datamodels.ImageModel,
5857
datamodels.IFUImageModel,
5958
datamodels.CubeModel)):
6059
self.log.debug(f'Input is a {input_model.meta.model_type}.')
@@ -72,12 +71,12 @@ def process(self, input):
7271
'n_adjacent_cols': self.n_adjacent_cols,
7372
}
7473

75-
# ___________________
76-
# calewbb_spec3 case
77-
# ___________________
74+
# calwebb_spec3 case / ModelContainer
7875
if isinstance(input_model, datamodels.ModelContainer):
7976
output_model = input_model
80-
# Setup output path naming if associations are involved.
77+
78+
# Set up output path name to include the ASN ID
79+
# if associations are involved
8180
asn_id = None
8281
try:
8382
asn_id = input_model.asn_table["asn_id"]
@@ -93,6 +92,7 @@ def process(self, input):
9392
_make_output_path,
9493
asn_id=asn_id
9594
)
95+
9696
# Check models to confirm they are the correct type
9797
for i, model in enumerate(output_model):
9898
run_pixel_replace = True
@@ -112,10 +112,10 @@ def process(self, input):
112112
replacement.replace()
113113
output_model[i] = replacement.output
114114
record_step_status(output_model[i], 'pixel_replace', success=True)
115+
115116
return output_model
116-
# ________________________________________
117-
# calewbb_spec2 case - single input model
118-
# ________________________________________
117+
118+
# calwebb_spec2 case / single input model
119119
else:
120120
# Make copy of input to prevent overwriting
121121
result = input_model.copy()

jwst/pixel_replace/tests/test_pixel_replace.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1+
import os
12
import numpy as np
23
import pytest
34

45
from stdatamodels.jwst import datamodels
6+
from jwst.datamodels import ModelContainer
57
from stdatamodels.jwst.datamodels.dqflags import pixel as flags
68

79
from jwst.assign_wcs import AssignWcsStep
810
from jwst.assign_wcs.tests.test_nirspec import create_nirspec_ifu_file
911
from jwst.pixel_replace.pixel_replace_step import PixelReplaceStep
12+
from glob import glob
1013

1114

1215
def cal_data(shape, bad_idx, dispaxis=1, model='slit'):
@@ -99,6 +102,7 @@ def nirspec_ifu():
99102
model.var_poisson = test_data.var_poisson
100103
model.var_rnoise = test_data.var_rnoise
101104
model.var_flat = test_data.var_flat
105+
102106
test_data.close()
103107

104108
return model, bad_idx
@@ -199,10 +203,9 @@ def test_pixel_replace_multislit(input_model_function, algorithm):
199203

200204

201205
@pytest.mark.slow
202-
@pytest.mark.parametrize('input_model_function',
203-
[nirspec_ifu])
206+
@pytest.mark.parametrize('input_model_function', [nirspec_ifu])
204207
@pytest.mark.parametrize('algorithm', ['fit_profile', 'mingrad'])
205-
def test_pixel_replace_nirspec_ifu(input_model_function, algorithm):
208+
def test_pixel_replace_nirspec_ifu(tmp_cwd, input_model_function, algorithm):
206209
"""
207210
Test pixel replacement for NIRSpec IFU.
208211
@@ -212,10 +215,16 @@ def test_pixel_replace_nirspec_ifu(input_model_function, algorithm):
212215
The test is otherwise the same as for other modes.
213216
"""
214217
input_model, bad_idx = input_model_function()
218+
input_model.meta.filename = 'jwst_nirspec_cal.fits'
215219

216220
# for this simple case, the results from either algorithm should
217221
# be the same
218-
result = PixelReplaceStep.call(input_model, skip=False, algorithm=algorithm)
222+
result = PixelReplaceStep.call(input_model, skip=False,
223+
algorithm=algorithm, save_results=True)
224+
225+
assert result.meta.filename == 'jwst_nirspec_pixelreplacestep.fits'
226+
assert result.meta.cal_step.pixel_replace == 'COMPLETE'
227+
assert os.path.isfile(result.meta.filename)
219228

220229
for ext in ['data', 'err', 'var_poisson', 'var_rnoise', 'var_flat']:
221230
# non-science edges are uncorrected
@@ -238,3 +247,33 @@ def test_pixel_replace_nirspec_ifu(input_model_function, algorithm):
238247

239248
result.close()
240249
input_model.close()
250+
251+
252+
@pytest.mark.parametrize('input_model_function', [nirspec_fs_slitmodel])
253+
def test_pixel_replace_container_names(tmp_cwd, input_model_function):
254+
"""Test pixel replace output names for input container."""
255+
input_model, _ = input_model_function()
256+
input_model.meta.filename = 'jwst_nirspec_1_cal.fits'
257+
input_model2, _ = input_model_function()
258+
input_model2.meta.filename = 'jwst_nirspec_2_cal.fits'
259+
cfiles = [input_model, input_model2]
260+
container = ModelContainer(cfiles)
261+
262+
expected_name = ['jwst_nirspec_1_pixelreplacestep.fits',
263+
'jwst_nirspec_2_pixelreplacestep.fits']
264+
265+
result = PixelReplaceStep.call(container, skip=False, save_results=True)
266+
for i, model in enumerate(result):
267+
assert model.meta.filename == expected_name[i]
268+
assert model.meta.cal_step.pixel_replace == 'COMPLETE'
269+
270+
result_files = glob(os.path.join(tmp_cwd, '*pixelreplacestep.fits'))
271+
for i, file in enumerate(sorted(result_files)):
272+
basename = os.path.basename(file)
273+
assert expected_name[i] == basename
274+
with datamodels.open(file) as model:
275+
assert model.meta.cal_step.pixel_replace == 'COMPLETE'
276+
assert model.meta.filename == expected_name[i]
277+
278+
result.close()
279+
input_model.close()

jwst/stpipe/tests/test_utilities.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
from jwst.stpipe.utilities import all_steps, NOT_SET
66
import jwst.pipeline
77
import jwst.step
8+
from stdatamodels.jwst import datamodels
9+
from jwst.stpipe.utilities import invariant_filename
10+
811
from jwst import datamodels as dm
912

1013

@@ -54,3 +57,31 @@ def test_record_query_step_status():
5457
# test query not set
5558
model3 = dm.MultiSpecModel()
5659
assert query_step_status(model3, 'test_step') == NOT_SET
60+
61+
62+
def change_name_func(model):
63+
model.meta.filename = "changed"
64+
model.meta.cal_step.pixel_replace = "COMPLETE"
65+
return model
66+
67+
68+
def test_invariant_filename():
69+
# Make sure the change_name_func changes the name and has side effects
70+
# (here, setting a status variable, but normally, actually saving the file)
71+
input_model = datamodels.IFUImageModel()
72+
input_model.meta.filename = 'test1.fits'
73+
change_name_func(input_model)
74+
assert input_model.meta.filename == 'changed'
75+
assert input_model.meta.cal_step.pixel_replace == 'COMPLETE'
76+
77+
# When the function is wrapped with invariant_filename,
78+
# the filename is not changed, but the side effect still happens
79+
input_model = datamodels.IFUImageModel()
80+
input_model.meta.filename = 'test2.fits'
81+
invariant_save_func = invariant_filename(change_name_func)
82+
output_model = invariant_save_func(input_model)
83+
assert output_model.meta.filename == "test2.fits"
84+
assert output_model.meta.cal_step.pixel_replace == 'COMPLETE'
85+
86+
# The output model is not a copy - the name is reset in place
87+
assert output_model is input_model

jwst/stpipe/utilities.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,14 @@
3030
Utilities
3131
"""
3232
import importlib.util
33-
from importlib import import_module
3433
import inspect
3534
import logging
3635
import os
3736
import re
3837
from collections.abc import Sequence
38+
from functools import wraps
39+
from importlib import import_module
40+
3941
from jwst import datamodels
4042

4143
# Configure logging
@@ -211,3 +213,23 @@ def query_step_status(datamodel, cal_step):
211213
return getattr(datamodel[0].meta.cal_step, cal_step, NOT_SET)
212214
else:
213215
return getattr(datamodel.meta.cal_step, cal_step, NOT_SET)
216+
217+
218+
def invariant_filename(save_model_func):
219+
"""Restore meta.filename after save_model"""
220+
221+
@wraps(save_model_func)
222+
def save_model(model, **kwargs):
223+
try:
224+
filename = model.meta.filename
225+
except AttributeError:
226+
filename = None
227+
228+
result = save_model_func(model, **kwargs)
229+
230+
if filename:
231+
model.meta.filename = filename
232+
233+
return result
234+
235+
return save_model

0 commit comments

Comments
 (0)