Skip to content

Commit c935ae9

Browse files
authored
Merge pull request spacetelescope#69 from rgcosentino/B21_integral_nonlin_update_rgc
B21 integral nonlin update rgc
2 parents c58b8a8 + a348020 commit c935ae9

File tree

5 files changed

+203
-68
lines changed

5 files changed

+203
-68
lines changed

src/wfi_reference_pipeline/reference_types/integral_non_linearity/integral_non_linearity.py

Lines changed: 89 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
2-
31
import numpy as np
42
import roman_datamodels.stnode as rds
53

@@ -17,30 +15,24 @@ class IntegralNonLinearity(ReferenceType):
1715
method creates the asdf reference file.
1816
1917
Example creation
20-
from wfi_reference_pipeline.reference_types.integral_non_linearity.integral_non_linearity import simulate_inl_correction_array
2118
from wfi_reference_pipeline.resources.make_dev_meta import MakeDevMeta
2219
from wfi_reference_pipeline.reference_types.integral_non_linearity.integral_non_linearity import IntegralNonLinearity
2320
24-
for det in range(1, 19):
25-
arr = simulate_inl_correction_array()
26-
# Create meta data object for INL ref file
27-
tmp = MakeDevMeta(ref_type='INTEGRALNONLINEARITY')
28-
# Update meta per detector to get the right values from the form and update description
29-
tmp.meta_integral_non_linearity.instrument_detector = f"WFI{det:02d}"
30-
tmp.meta_integral_non_linearity.description = (
31-
"To support new integral non-linearity correction development for B21. "
32-
"The integral non-linearity correction is applied to each channel, "
33-
"numbered left to right from 1 to 32, with each channel containing 128 "
34-
"pixels in length."
35-
)
36-
# Update the file name to match the detector
37-
fl_name = 'new_roman_inl_' + tmp.meta_integral_non_linearity.instrument_detector
38-
# Instantiate an object and write the file out
39-
rfp_inl = IntegralNonLinearity(meta_data=tmp.meta_integral_non_linearity,
40-
ref_type_data=arr,
41-
clobber=True,
42-
outfile=fl_name+'.asdf')
43-
rfp_inl.generate_outfile()
21+
arr - assumed here to be a numpy array that is derived in the instrument coordinate reference frame and properly
22+
transformed into the science coordinate reference frame for ingestion into the class.
23+
24+
tmp = MakeDevMeta(ref_type='INTEGRALNONLINEARITY')
25+
rfp_inl = IntegralNonLinearity(meta_data=tmp.meta_integral_non_linearity,
26+
ref_type_data=arr)
27+
rfp_inl.generate_outfile()
28+
29+
Documentation and important notes. This effect comes from the Analog to Digital converters that read out the detector
30+
array. The dimensions of the detector are 4096x4096 with 32 A/D converters and amplifiers reading out every 128 pixels
31+
together at the same time. The correction implemented adjust the measured value to the actual value which varies across
32+
all possible values in UNIT16 from 0 to 65535. This module expects that the transformation from instrument to science
33+
coordinate reference system has already taken place when the input data is passed into the class as ref_type_data.
34+
35+
See https://roman-docs.stsci.edu/data-handbook/wfi-data-levels-and-products/coordinate-systems
4436
"""
4537

4638
def __init__(
@@ -96,10 +88,52 @@ def __init__(
9688
if len(self.meta_data.description) == 0:
9789
self.meta_data.description = "Roman WFI integral non linearity reference file."
9890

99-
self.inl_correction = ref_type_data
100-
_, num_values = np.shape(ref_type_data)
101-
self.value_array = np.linspace(0, 65535, num_values, dtype=np.uint16)
102-
# TODO look at references for channel id number - https://roman-docs.stsci.edu/data-handbook-home/wfi-data-format/coordinate-systems
91+
if ref_type_data is None and file_list is None:
92+
msg = "RFP is simulating the INL correction"
93+
print(msg)
94+
95+
ref_type_data = np.asarray(simulate_inl_correction_array())
96+
97+
# If INL correction arrays are provded as input into class then check everything needed
98+
# for the array to be valid.
99+
elif ref_type_data is not None:
100+
# Convert to numpy array and enforce dtype in one go
101+
ref_type_data = np.asarray(ref_type_data, dtype=np.float64)
102+
103+
# Print message only for detectors that need LR flip
104+
flip_lr_detectors = {
105+
"WFI03", "WFI06", "WFI09",
106+
"WFI12", "WFI15", "WFI18"
107+
}
108+
if self.meta_data.instrument_detector in flip_lr_detectors:
109+
msg = (
110+
"RFP using input ref_type_data assumed to be transformed into the "
111+
"proper science coordinate reference frame."
112+
)
113+
print(msg)
114+
115+
# Validate array shape
116+
if ref_type_data.ndim != 2:
117+
raise ValueError(
118+
"IntegralNonLinearity expects ref_type_data to be a 2D array "
119+
"with shape (32, 65536)."
120+
)
121+
122+
n_chan, n_val = ref_type_data.shape
123+
if n_chan != 32 or n_val != 65536:
124+
raise ValueError(
125+
"Invalid INL correction array shape. "
126+
f"Expected (32, 65536), got ({n_chan}, {n_val})."
127+
)
128+
129+
# Set attributes
130+
self.inl_correction = ref_type_data
131+
self.value_array = np.linspace(0, 65535, n_val, dtype=np.uint16)
132+
133+
elif file_list is not None:
134+
raise ValueError(
135+
"Module currently not capable to support file list input."
136+
)
103137

104138
self.outfile = outfile
105139

@@ -117,44 +151,41 @@ def update_data_quality_array(self):
117151

118152
def _make_inl_table(self):
119153
"""
120-
Construct the integral non linearity correction table to populate the data model.
121-
"""
122-
inl_table = {}
123-
124-
# Assuming self.inl_correction is shaped (32, N)
125-
# Each row corresponds to a science chunk (0–31)
126-
for sci_chunk in range(32):
127-
# Reverse channel mapping: 0→32, 1→31, ..., 31→1 for a detector that is
128-
# needing to be flipped left right from detector to science coordinates
129-
# NOTE: other detectors
130-
channel = 32 - sci_chunk
131-
132-
inl_table[str(sci_chunk)] = {
133-
'channel': channel,
134-
'correction': self.inl_correction[sci_chunk]
135-
}
154+
Populate the INL table following the Roman INL reference schema.
136155
137-
return inl_table
156+
This method is explicit to map instrument channel number and index in the
157+
instrument coordinate reference frame to the science channel number and index
158+
in the science coorcinate reference frame.
138159
160+
Science channels are always numbered from bottom left to right for each detector
161+
from 1-32. The science coordinate reference frame is how the pixels on the detector
162+
look at the sky - after the hardware has been placed and some rotated to account
163+
for the position of detector electronics.
164+
165+
Instrument channels are indexed from 0-31 with the origin including detector electronics
166+
always in the lower left. The transformation from instrument to science coordinates is
167+
illustrated in https://roman-docs.stsci.edu/data-handbook/wfi-data-levels-and-products/coordinate-systems
168+
169+
"""
170+
table = {}
171+
for science_chan in range(32):
172+
key = f"science_channel_{science_chan+1:02d}"
173+
table[key] = {
174+
"instrument_channel": science_chan,
175+
"correction": self.inl_correction[science_chan],
176+
}
177+
return table
139178

140179
def populate_datamodel_tree(self):
141180
"""
142181
Build the Roman datamodel tree for the integral non-linearity reference.
143182
"""
144-
try:
145-
# Placeholder until official datamodel exists
146-
inl_ref = rds.IntegralNonLinearity()
147-
except AttributeError:
148-
inl_ref = {"meta": {},
149-
"inl_table": {},
150-
"value": {}
151-
}
152-
153-
inl_ref["meta"] = self.meta_data.export_asdf_meta()
154-
inl_ref["inl_table"] = self._make_inl_table()
155-
inl_ref["value"] = self.value_array
156-
157-
return inl_ref
183+
inl_datamodel = rds.IntegralnonlinearityRef()
184+
inl_datamodel["meta"] = self.meta_data.export_asdf_meta()
185+
inl_datamodel["inl_table"] = self._make_inl_table()
186+
inl_datamodel["value"] = self.value_array
187+
188+
return inl_datamodel
158189

159190

160191
def simulate_inl_correction_array():
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import numpy as np
2+
import pytest
3+
4+
from wfi_reference_pipeline.constants import REF_TYPE_INTEGRALNONLINEARITY
5+
from wfi_reference_pipeline.reference_types.integral_non_linearity.integral_non_linearity import (
6+
IntegralNonLinearity,
7+
)
8+
from wfi_reference_pipeline.resources.make_test_meta import MakeTestMeta
9+
10+
11+
@pytest.fixture
12+
def valid_meta_data():
13+
"""Fixture for generating valid WFIMetaIntegralNonLinearity metadata."""
14+
test_meta = MakeTestMeta(ref_type=REF_TYPE_INTEGRALNONLINEARITY)
15+
return test_meta.meta_integral_non_linearity
16+
17+
18+
@pytest.fixture
19+
def inl_array():
20+
"""Return a valid 2D INL correction array shape (32, 65536)."""
21+
return np.zeros((32, 65536), dtype=np.float64)
22+
23+
24+
@pytest.fixture
25+
def integral_non_linearity_object(valid_meta_data, inl_array):
26+
"""Fixture for initializing an IntegralNonLinearity object with valid metadata."""
27+
return IntegralNonLinearity(meta_data=valid_meta_data, ref_type_data=inl_array)
28+
29+
30+
class TestIntegralNonLinearity:
31+
32+
def test_integral_non_linearity_instantiation_with_valid_metadata(
33+
self, integral_non_linearity_object
34+
):
35+
"""
36+
Test that IntegralNonLinearity object is created successfully with valid metadata.
37+
"""
38+
assert isinstance(integral_non_linearity_object, IntegralNonLinearity)
39+
40+
def test_default_description_is_set_if_empty(self, valid_meta_data, inl_array):
41+
"""
42+
Test that a default description is populated if metadata description is empty.
43+
"""
44+
valid_meta_data.description = ""
45+
inl_obj = IntegralNonLinearity(meta_data=valid_meta_data, ref_type_data=inl_array)
46+
47+
assert inl_obj.meta_data.description == "Roman WFI integral non linearity reference file."
48+
49+
def test_populate_datamodel_tree(self, integral_non_linearity_object):
50+
"""
51+
Test that the data model tree is correctly populated.
52+
"""
53+
data_model_tree = integral_non_linearity_object.populate_datamodel_tree()
54+
55+
assert "meta" in data_model_tree
56+
assert "inl_table" in data_model_tree
57+
assert "value" in data_model_tree
58+
59+
inl_table = data_model_tree["inl_table"]
60+
assert isinstance(inl_table, dict)
61+
assert len(inl_table) == 32
62+
63+
# Ensure all keys exist and values are correct
64+
# There are 32 amplifiers to read 128 pixels at a time.
65+
# https://roman-docs.stsci.edu/data-handbook/wfi-data-levels-and-products/coordinate-systems
66+
for i in range(32):
67+
key = f"science_channel_{i+1:02d}"
68+
assert key in inl_table
69+
70+
table_entry = inl_table[key]
71+
assert "instrument_channel" in table_entry
72+
assert "correction" in table_entry
73+
74+
assert table_entry["instrument_channel"] == i
75+
assert isinstance(table_entry["correction"], np.ndarray)
76+
# The Analog to Digital conversion is in UINT16.
77+
assert table_entry["correction"].shape == (65536,)
78+
79+
value_array = data_model_tree["value"]
80+
assert isinstance(value_array, np.ndarray)
81+
assert value_array.shape == (65536,)
82+
83+
def test_integral_non_linearity_outfile_default(self, integral_non_linearity_object):
84+
"""
85+
Test that the default outfile name is correct.
86+
"""
87+
assert integral_non_linearity_object.outfile == "roman_inl.asdf"
88+
89+
def test_invalid_ref_type_data_shape_raises_value_error(self, valid_meta_data):
90+
"""
91+
Test that invalid INL array shapes raise a ValueError.
92+
"""
93+
bad_array = np.zeros((10, 10))
94+
with pytest.raises(ValueError):
95+
IntegralNonLinearity(meta_data=valid_meta_data, ref_type_data=bad_array)
96+
97+
def test_invalid_meta_type_raises_type_error(self, inl_array):
98+
"""
99+
Test that invalid meta type raises a TypeError.
100+
"""
101+
with pytest.raises(TypeError):
102+
IntegralNonLinearity(meta_data=object(), ref_type_data=inl_array)

src/wfi_reference_pipeline/resources/make_dev_meta.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,10 @@ def _create_dev_meta_gain(self, meta_data):
8989
self.meta_gain = WFIMetaGain(*meta_data)
9090

9191
def _create_dev_meta_integral_non_linearity(self, meta_data):
92-
n_channels = '32'
93-
n_pixels_per_channel = '128'
92+
# There are 32 amplifiers to read 128 pixels at a time.
93+
# https://roman-docs.stsci.edu/data-handbook/wfi-data-levels-and-products/coordinate-systems
94+
n_channels = 32
95+
n_pixels_per_channel = 128
9496

9597
meta_integral_non_linearity = [n_channels, n_pixels_per_channel]
9698

src/wfi_reference_pipeline/resources/make_test_meta.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,16 +87,16 @@ def _create_test_meta_flat(self, meta_data):
8787
def _create_test_meta_gain(self, meta_data):
8888
self.meta_gain = WFIMetaGain(*meta_data)
8989

90-
def _create_test_meta_intengral_non_linearity(self, meta_data):
91-
n_channels = '32'
92-
n_pixels_per_channel = '128'
90+
def _create_test_meta_integral_non_linearity(self, meta_data):
91+
# There are 32 amplifiers to read 128 pixels at a time.
92+
# https://roman-docs.stsci.edu/data-handbook/wfi-data-levels-and-products/coordinate-systems
93+
n_channels = 32
94+
n_pixels_per_channel = 128
9395

9496
meta_integral_non_linearity = [n_channels, n_pixels_per_channel]
9597
self.meta_integral_non_linearity = WFIMetaIntegralNonLinearity(*meta_data,
9698
*meta_integral_non_linearity)
9799

98-
self._create_test_meta_intengral_non_linearity = WFIMetaIntegralNonLinearity(*meta_data)
99-
100100
def _create_test_meta_interpixelcapacitance(self, meta_data):
101101
ref_optical_element = "F158"
102102

@@ -192,7 +192,7 @@ def __init__(self, ref_type):
192192
self._create_test_meta_gain(meta_data_params)
193193

194194
if ref_type == REF_TYPE_INTEGRALNONLINEARITY:
195-
self._create_test_meta_intengral_non_linearity(meta_data_params)
195+
self._create_test_meta_integral_non_linearity(meta_data_params)
196196

197197
if ref_type == REF_TYPE_INVERSELINEARITY:
198198
self._create_test_meta_inverselinearity(meta_data_params)

src/wfi_reference_pipeline/resources/wfi_meta_integral_non_linearity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class WFIMetaIntegralNonLinearity(WFIMetadata):
1212
All Fields are required and positional with base class fields first
1313
"""
1414
# These are required reftype specific
15-
n_channels: str
16-
n_pixels_per_channel: str
15+
n_channels: int
16+
n_pixels_per_channel: int
1717

1818
def __post_init__(self):
1919
super().__post_init__()

0 commit comments

Comments
 (0)