dkist-processing-visp 4.0.0__py3-none-any.whl → 5.0.0__py3-none-any.whl

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.
Files changed (32) hide show
  1. dkist_processing_visp/models/constants.py +50 -9
  2. dkist_processing_visp/models/fits_access.py +5 -1
  3. dkist_processing_visp/models/metric_code.py +10 -0
  4. dkist_processing_visp/models/parameters.py +128 -19
  5. dkist_processing_visp/parsers/spectrograph_configuration.py +75 -0
  6. dkist_processing_visp/parsers/visp_l0_fits_access.py +6 -0
  7. dkist_processing_visp/tasks/geometric.py +115 -7
  8. dkist_processing_visp/tasks/l1_output_data.py +202 -0
  9. dkist_processing_visp/tasks/lamp.py +50 -91
  10. dkist_processing_visp/tasks/parse.py +19 -0
  11. dkist_processing_visp/tasks/science.py +14 -14
  12. dkist_processing_visp/tasks/solar.py +894 -451
  13. dkist_processing_visp/tasks/visp_base.py +1 -0
  14. dkist_processing_visp/tests/conftest.py +98 -35
  15. dkist_processing_visp/tests/header_models.py +71 -20
  16. dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +25 -1
  17. dkist_processing_visp/tests/test_assemble_quality.py +89 -4
  18. dkist_processing_visp/tests/test_geometric.py +40 -0
  19. dkist_processing_visp/tests/test_instrument_polarization.py +2 -1
  20. dkist_processing_visp/tests/test_lamp.py +17 -22
  21. dkist_processing_visp/tests/test_parameters.py +120 -18
  22. dkist_processing_visp/tests/test_parse.py +73 -1
  23. dkist_processing_visp/tests/test_science.py +5 -6
  24. dkist_processing_visp/tests/test_solar.py +319 -102
  25. dkist_processing_visp/tests/test_visp_constants.py +35 -6
  26. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/METADATA +40 -37
  27. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/RECORD +31 -30
  28. docs/conf.py +4 -1
  29. docs/gain_correction.rst +50 -42
  30. dkist_processing_visp/tasks/mixin/line_zones.py +0 -116
  31. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/WHEEL +0 -0
  32. {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/top_level.txt +0 -0
@@ -1,14 +1,216 @@
1
1
  """Subclass of AssembleQualityData that causes the correct polcal metrics to build."""
2
2
 
3
+ import numpy as np
4
+ from dkist_processing_common.codecs.asdf import asdf_decoder
5
+ from dkist_processing_common.models.quality import Plot2D
6
+ from dkist_processing_common.models.quality import ReportMetric
7
+ from dkist_processing_common.models.quality import VerticalMultiPanePlot2D
3
8
  from dkist_processing_common.tasks import AssembleQualityData
4
9
 
5
10
  __all__ = ["VispAssembleQualityData"]
6
11
 
12
+ from dkist_processing_visp.models.constants import VispConstants
13
+ from dkist_processing_visp.models.metric_code import VispMetricCode
14
+ from dkist_processing_visp.models.tags import VispTag
15
+
7
16
 
8
17
  class VispAssembleQualityData(AssembleQualityData):
9
18
  """Subclass just so that the polcal_label_list can be populated."""
10
19
 
20
+ constants: VispConstants
21
+
22
+ @property
23
+ def constants_model_class(self):
24
+ """Get ViSP pipeline constants."""
25
+ return VispConstants
26
+
11
27
  @property
12
28
  def polcal_label_list(self) -> list[str]:
13
29
  """Return labels for beams 1 and 2."""
14
30
  return ["Beam 1", "Beam 2"]
31
+
32
+ def quality_assemble_data(self, polcal_label_list: list[str] | None = None) -> list[dict]:
33
+ """
34
+ Assemble the full quality report and insert ViSP-specific metrics.
35
+
36
+ We try to place the new metrics right before default polcal ones, if possible.
37
+ """
38
+ vignette_metrics = []
39
+ for beam in range(1, self.constants.num_beams + 1):
40
+ vignette_metrics.append(self.build_first_vignette_metric(beam=beam))
41
+ vignette_metrics.append(self.build_final_vignette_metric(beam=beam))
42
+
43
+ report = super().quality_assemble_data(polcal_label_list=polcal_label_list)
44
+
45
+ # Look for the first "PolCal" metric
46
+ first_polcal_metric_index = 0
47
+ try:
48
+ while not report[first_polcal_metric_index]["name"].lower().startswith("polcal"):
49
+ first_polcal_metric_index += 1
50
+ except:
51
+ # Wasn't found for whatever reason. No big deal, just put the new metrics at the front of the list
52
+ first_polcal_metric_index = 0
53
+
54
+ final_report = (
55
+ report[:first_polcal_metric_index]
56
+ + vignette_metrics
57
+ + report[first_polcal_metric_index:]
58
+ )
59
+
60
+ return final_report
61
+
62
+ def build_first_vignette_metric(self, beam: int) -> dict:
63
+ """Build a ReportMetric showing the initial atlas-with-continuum fit and residuals."""
64
+ data = next(
65
+ self.read(
66
+ tags=[VispTag.quality(VispMetricCode.solar_first_vignette), VispTag.beam(beam)],
67
+ decoder=asdf_decoder,
68
+ )
69
+ )
70
+
71
+ wave_vec = data["output_wave_vec"].tolist()
72
+ input_spectrum = data["input_spectrum"].tolist()
73
+ best_fit_atlas = data["best_fit_atlas"].tolist()
74
+ continuum = data["best_fit_continuum"].tolist()
75
+ residuals = data["residuals"].tolist()
76
+
77
+ fit_series = {
78
+ "Raw input spectrum": [wave_vec, input_spectrum],
79
+ "Best fit atlas": [wave_vec, best_fit_atlas],
80
+ "Best fit continuum": [wave_vec, continuum],
81
+ }
82
+
83
+ fit_plot_kwargs = {
84
+ "Raw input spectrum": {
85
+ "ls": "-",
86
+ "ms": 0,
87
+ "color": "#FAA61C",
88
+ "zorder": 2.0,
89
+ "lw": 4,
90
+ "alpha": 0.6,
91
+ },
92
+ "Best fit atlas": {"color": "k", "ls": "-", "ms": 0, "zorder": 2.1},
93
+ "Best fit continuum": {"ls": "-", "ms": 0, "color": "g", "zorder": 2.2},
94
+ }
95
+
96
+ fit_plot = Plot2D(
97
+ xlabel="Wavelength [nm]",
98
+ ylabel="Signal",
99
+ series_data=fit_series,
100
+ plot_kwargs=fit_plot_kwargs,
101
+ sort_series=False,
102
+ )
103
+
104
+ residuals_series = {"Residuals": [wave_vec, residuals]}
105
+ residuals_plot_kwargs = {"Residuals": {"ls": "-", "color": "k", "ms": 0}}
106
+
107
+ y_min = np.nanpercentile(residuals, 2)
108
+ y_max = np.nanpercentile(residuals, 98)
109
+ y_range = y_max - y_min
110
+ y_min -= 0.1 * y_range
111
+ y_max += 0.1 * y_range
112
+ residuals_plot = Plot2D(
113
+ xlabel="Wavelength [nm]",
114
+ ylabel=r"$\frac{\mathrm{Obs - Atlas}}{\mathrm{Obs}}$",
115
+ series_data=residuals_series,
116
+ plot_kwargs=residuals_plot_kwargs,
117
+ ylim=(y_min, y_max),
118
+ )
119
+
120
+ plot_list = [fit_plot, residuals_plot]
121
+ height_ratios = [1.5, 1.0]
122
+
123
+ full_plot = VerticalMultiPanePlot2D(
124
+ top_to_bottom_plot_list=plot_list,
125
+ match_x_axes=True,
126
+ no_gap=True,
127
+ top_to_bottom_height_ratios=height_ratios,
128
+ )
129
+
130
+ metric = ReportMetric(
131
+ name=f"Initial Vignette Estimation - Beam {beam}",
132
+ description="These plots show the solar atlas fit used to estimate the initial, 1D spectral vignette "
133
+ "present in solar gain frames. The vignette signature is taken to be the fit continuum shown.",
134
+ metric_code=VispMetricCode.solar_first_vignette,
135
+ facet=self._format_facet(f"Beam {beam}"),
136
+ multi_plot_data=full_plot,
137
+ )
138
+
139
+ return metric.model_dump()
140
+
141
+ def build_final_vignette_metric(self, beam: int) -> dict:
142
+ """Build a ReportMetric showing the quality of the vignette correction on solar gain data."""
143
+ data = next(
144
+ self.read(
145
+ tags=[VispTag.quality(VispMetricCode.solar_final_vignette), VispTag.beam(beam)],
146
+ decoder=asdf_decoder,
147
+ )
148
+ )
149
+
150
+ wave_vec = data["output_wave_vec"].tolist()
151
+ median_spec = data["median_spec"].tolist()
152
+ low_deviation = data["low_deviation"]
153
+ high_deviation = data["high_deviation"]
154
+ diff = (high_deviation - low_deviation).tolist()
155
+ low_deviation = low_deviation.tolist()
156
+ high_deviation = high_deviation.tolist()
157
+
158
+ bounds_series = {
159
+ "Median solar signal": [wave_vec, median_spec],
160
+ "5th percentile bounds": [wave_vec, low_deviation],
161
+ "95th percentile bounds": [wave_vec, high_deviation],
162
+ }
163
+
164
+ bounds_plot_kwargs = {
165
+ "Median solar signal": {"ls": "-", "color": "k", "alpha": 0.8, "ms": 0, "zorder": 2.2},
166
+ "5th percentile bounds": {"color": "#1E317A", "ls": "-", "ms": 0, "zorder": 2.0},
167
+ "95th percentile bounds": {"ls": "-", "color": "#FAA61C", "ms": 0, "zorder": 2.1},
168
+ }
169
+
170
+ bounds_plot = Plot2D(
171
+ xlabel="Wavelength [nm]",
172
+ ylabel="Signal",
173
+ series_data=bounds_series,
174
+ plot_kwargs=bounds_plot_kwargs,
175
+ sort_series=False,
176
+ )
177
+
178
+ residuals_series = {"Residuals": [wave_vec, diff]}
179
+ residuals_plot_kwargs = {"Residuals": {"ls": "-", "color": "k", "ms": 0}}
180
+
181
+ y_min = np.nanpercentile(diff, 5)
182
+ y_max = np.nanpercentile(diff, 95)
183
+ y_range = y_max - y_min
184
+ y_min -= 0.1 * y_range
185
+ y_max += 0.1 * y_range
186
+ residuals_plot = Plot2D(
187
+ xlabel="Wavelength [nm]",
188
+ ylabel="95th - 5th percentile",
189
+ series_data=residuals_series,
190
+ plot_kwargs=residuals_plot_kwargs,
191
+ ylim=(y_min, y_max),
192
+ )
193
+
194
+ plot_list = [bounds_plot, residuals_plot]
195
+ height_ratios = [1.5, 1.0]
196
+
197
+ full_plot = VerticalMultiPanePlot2D(
198
+ top_to_bottom_plot_list=plot_list,
199
+ match_x_axes=True,
200
+ no_gap=True,
201
+ top_to_bottom_height_ratios=height_ratios,
202
+ )
203
+
204
+ metric = ReportMetric(
205
+ name=f"Final Vignette Estimation - Beam {beam}",
206
+ description="These plots show how well the full, 2D vignette signal was removed from solar gain frames. "
207
+ "The median solar signal shows a full spatial median of the vignette corrected solar gain; "
208
+ "this should be very close to the true solar spectrum incident on the DKIST optics. "
209
+ "The 5th and 9th percentile ranges show how stable this spectrum is along the spatial dimension "
210
+ "after removing the vignette signal.",
211
+ metric_code=VispMetricCode.solar_final_vignette,
212
+ facet=self._format_facet(f"Beam {beam}"),
213
+ multi_plot_data=full_plot,
214
+ )
215
+
216
+ return metric.model_dump()
@@ -1,6 +1,5 @@
1
1
  """ViSP lamp calibration task. See :doc:`this page </gain_correction>` for more information."""
2
2
 
3
- import numpy as np
4
3
  from dkist_processing_common.codecs.fits import fits_access_decoder
5
4
  from dkist_processing_common.codecs.fits import fits_array_decoder
6
5
  from dkist_processing_common.codecs.fits import fits_array_encoder
@@ -30,13 +29,14 @@ class LampCalibration(
30
29
 
31
30
  Parameters
32
31
  ----------
33
- recipe_run_id : int
32
+ recipe_run_id
34
33
  id of the recipe run used to identify the workflow run this task is part of
35
- workflow_name : str
34
+
35
+ workflow_name
36
36
  name of the workflow to which this instance of the task belongs
37
- workflow_version : str
38
- version of the workflow to which this instance of the task belongs
39
37
 
38
+ workflow_version
39
+ version of the workflow to which this instance of the task belongs
40
40
  """
41
41
 
42
42
  record_provenance = True
@@ -45,9 +45,11 @@ class LampCalibration(
45
45
  """
46
46
  For each beam.
47
47
 
48
- - Gather input lamp gain and averaged dark arrays
49
- - Calculate master lamp
50
- - Write master lamp
48
+ - Normalize all input arrays by the number of frames per FPA
49
+ - Subtract the average dark frame corresponding to the matching readout exposure time
50
+ - Average all different readout exposure time arrays (if applicable)
51
+ - Interpolate over the hairlines
52
+ - Write final lamp gain to disk
51
53
  - Record quality metrics
52
54
 
53
55
  Returns
@@ -58,9 +60,11 @@ class LampCalibration(
58
60
  with self.telemetry_span(
59
61
  f"Generate lamp gains for {self.constants.num_beams} beams and {len(self.constants.lamp_readout_exp_times)} exposure times"
60
62
  ):
61
- for readout_exp_time in self.constants.lamp_readout_exp_times:
62
- for beam in range(1, self.constants.num_beams + 1):
63
- logger.info(f"Load dark for beam {beam}")
63
+ for beam in range(1, self.constants.num_beams + 1):
64
+ all_exp_time_arrays = []
65
+ for readout_exp_time in self.constants.lamp_readout_exp_times:
66
+ apm_str = f"{beam = } and {readout_exp_time = }"
67
+ logger.info(f"Load dark for beam {apm_str}")
64
68
  dark_array = next(
65
69
  self.read(
66
70
  tags=VispTag.intermediate_frame_dark(
@@ -70,19 +74,44 @@ class LampCalibration(
70
74
  )
71
75
  )
72
76
 
73
- for state_num in range(
74
- 1, self.constants.num_modstates + 1
75
- ): # modulator states go from 1 to n
76
- logger.info(
77
- f"Calculating average lamp gain for beam {beam}, modulator state {state_num}"
77
+ with self.telemetry_span(f"Computing gain for {apm_str}"):
78
+ tags = [
79
+ VispTag.input(),
80
+ VispTag.frame(),
81
+ VispTag.task_lamp_gain(),
82
+ VispTag.readout_exp_time(readout_exp_time),
83
+ ]
84
+ input_lamp_gain_objs = self.read(
85
+ tags=tags,
86
+ decoder=fits_access_decoder,
87
+ fits_access_class=VispL0FitsAccess,
88
+ )
89
+
90
+ readout_normalized_arrays = (
91
+ self.beam_access_get_beam(o.data, beam=beam) / o.num_raw_frames_per_fpa
92
+ for o in input_lamp_gain_objs
78
93
  )
79
- self.compute_and_write_master_lamp_gain_for_modstate(
80
- modstate=state_num,
81
- dark_array=dark_array,
82
- beam=beam,
83
- readout_exp_time=readout_exp_time,
94
+ averaged_gain_data = average_numpy_arrays(readout_normalized_arrays)
95
+
96
+ dark_corrected_gain_data = next(
97
+ subtract_array_from_arrays(averaged_gain_data, dark_array)
84
98
  )
85
99
 
100
+ all_exp_time_arrays.append(dark_corrected_gain_data)
101
+
102
+ avg_gain_array = average_numpy_arrays(all_exp_time_arrays)
103
+ filtered_gain_data = self.corrections_mask_hairlines(avg_gain_array)
104
+
105
+ with self.telemetry_span(f"Writing gain array for {apm_str}"):
106
+ self.write(
107
+ data=filtered_gain_data,
108
+ tags=[
109
+ VispTag.intermediate_frame(beam=beam),
110
+ VispTag.task_lamp_gain(),
111
+ ],
112
+ encoder=fits_array_encoder,
113
+ )
114
+
86
115
  with self.telemetry_span("Computing and logging quality metrics"):
87
116
  no_of_raw_lamp_frames: int = self.scratch.count_all(
88
117
  tags=[
@@ -95,73 +124,3 @@ class LampCalibration(
95
124
  self.quality_store_task_type_counts(
96
125
  task_type=TaskName.lamp_gain.value, total_frames=no_of_raw_lamp_frames
97
126
  )
98
-
99
- def compute_and_write_master_lamp_gain_for_modstate(
100
- self,
101
- modstate: int,
102
- dark_array: np.ndarray,
103
- beam: int,
104
- readout_exp_time: float,
105
- ) -> None:
106
- """
107
- Compute and write master lamp gain for a given modstate and beam.
108
-
109
- Generally the algorithm is:
110
- 1. Average input gain arrays
111
- 2. Subtract average dark to get the dark corrected gain data
112
- 3. Normalize each beam to unity mean
113
- 4. Write to disk
114
-
115
- Parameters
116
- ----------
117
- modstate : int
118
- The modulator state to calculate the master lamp gain for
119
-
120
- dark_array : np.ndarray
121
- The master dark to be subtracted from each lamp gain file
122
-
123
- beam : int
124
- The number of the beam
125
-
126
- readout_exp_time : float
127
- Exposure time of single readout
128
-
129
- Returns
130
- -------
131
- None
132
- """
133
- apm_str = f"{beam = }, {modstate = }, and {readout_exp_time = }"
134
- # Get the input lamp gain arrays
135
- tags = [
136
- VispTag.input(),
137
- VispTag.frame(),
138
- VispTag.task_lamp_gain(),
139
- VispTag.modstate(modstate),
140
- VispTag.readout_exp_time(readout_exp_time),
141
- ]
142
- input_lamp_gain_objs = self.read(
143
- tags=tags, decoder=fits_access_decoder, fits_access_class=VispL0FitsAccess
144
- )
145
-
146
- with self.telemetry_span(f"Computing gain for {apm_str}"):
147
-
148
- readout_normalized_arrays = (
149
- self.beam_access_get_beam(o.data, beam=beam) / o.num_raw_frames_per_fpa
150
- for o in input_lamp_gain_objs
151
- )
152
- averaged_gain_data = average_numpy_arrays(readout_normalized_arrays)
153
-
154
- dark_corrected_gain_data = next(
155
- subtract_array_from_arrays(averaged_gain_data, dark_array)
156
- )
157
- filtered_gain_data = self.corrections_mask_hairlines(dark_corrected_gain_data)
158
-
159
- with self.telemetry_span(f"Writing gain array for {apm_str}"):
160
- self.write(
161
- data=filtered_gain_data,
162
- tags=[
163
- VispTag.intermediate_frame(beam=beam, modstate=modstate),
164
- VispTag.task_lamp_gain(),
165
- ],
166
- encoder=fits_array_encoder,
167
- )
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import TypeVar
4
4
 
5
+ from dkist_processing_common.models.fits_access import MetadataKey
5
6
  from dkist_processing_common.models.flower_pot import Stem
6
7
  from dkist_processing_common.models.task_name import TaskName
7
8
  from dkist_processing_common.parsers.cs_step import CSStepFlower
@@ -15,6 +16,7 @@ from dkist_processing_common.parsers.time import ObsIpStartTimeBud
15
16
  from dkist_processing_common.parsers.time import ReadoutExpTimeFlower
16
17
  from dkist_processing_common.parsers.time import TaskExposureTimesBud
17
18
  from dkist_processing_common.parsers.time import TaskReadoutExpTimesBud
19
+ from dkist_processing_common.parsers.unique_bud import TaskUniqueBud
18
20
  from dkist_processing_common.parsers.unique_bud import UniqueBud
19
21
  from dkist_processing_common.parsers.wavelength import ObserveWavelengthBud
20
22
  from dkist_processing_common.tasks import ParseL0InputDataBase
@@ -29,6 +31,8 @@ from dkist_processing_visp.parsers.modulator_states import NumberModulatorStates
29
31
  from dkist_processing_visp.parsers.polarimeter_mode import PolarimeterModeBud
30
32
  from dkist_processing_visp.parsers.raster_step import RasterScanStepFlower
31
33
  from dkist_processing_visp.parsers.raster_step import TotalRasterStepsBud
34
+ from dkist_processing_visp.parsers.spectrograph_configuration import IncidentLightAngleBud
35
+ from dkist_processing_visp.parsers.spectrograph_configuration import ReflectedLightAngleBud
32
36
  from dkist_processing_visp.parsers.time import DarkReadoutExpTimePickyBud
33
37
  from dkist_processing_visp.parsers.time import NonDarkNonPolcalTaskReadoutExpTimesBud
34
38
  from dkist_processing_visp.parsers.visp_l0_fits_access import VispL0FitsAccess
@@ -74,6 +78,7 @@ class ParseL0VispInputData(ParseL0InputDataBase):
74
78
  def constant_buds(self) -> list[S]:
75
79
  """Add ViSP specific constants to common constants."""
76
80
  return super().constant_buds + [
81
+ UniqueBud(constant_name=VispBudName.arm_id.value, metadata_key=VispMetadataKey.arm_id),
77
82
  NumMapScansBud(),
78
83
  TotalRasterStepsBud(),
79
84
  NumCSStepBud(self.parameters.max_cs_step_time_sec),
@@ -84,6 +89,20 @@ class ParseL0VispInputData(ParseL0InputDataBase):
84
89
  RetarderNameBud(),
85
90
  NonDarkNonPolcalTaskReadoutExpTimesBud(),
86
91
  DarkReadoutExpTimePickyBud(),
92
+ IncidentLightAngleBud(),
93
+ ReflectedLightAngleBud(),
94
+ TaskUniqueBud(
95
+ constant_name=VispBudName.grating_constant_inverse_mm.value,
96
+ metadata_key=VispMetadataKey.grating_constant_inverse_mm,
97
+ ip_task_types=[TaskName.observe.value, TaskName.solar_gain.value],
98
+ task_type_parsing_function=parse_header_ip_task_with_gains,
99
+ ),
100
+ TaskUniqueBud(
101
+ constant_name=VispBudName.solar_gain_ip_start_time.value,
102
+ metadata_key=MetadataKey.ip_start_time,
103
+ ip_task_types=TaskName.solar_gain,
104
+ task_type_parsing_function=parse_header_ip_task_with_gains,
105
+ ),
87
106
  TaskExposureTimesBud(
88
107
  stem_name=VispBudName.lamp_exposure_times.value,
89
108
  ip_task_types=TaskName.lamp_gain.value,
@@ -170,7 +170,7 @@ class ScienceCalibration(
170
170
  """
171
171
  dark_dict = defaultdict(dict)
172
172
  background_dict = dict()
173
- solar_dict = defaultdict(dict)
173
+ solar_dict = dict()
174
174
  angle_dict = dict()
175
175
  state_offset_dict = defaultdict(dict)
176
176
  spec_shift_dict = dict()
@@ -225,6 +225,18 @@ class ScienceCalibration(
225
225
  )
226
226
  )
227
227
 
228
+ # Solar
229
+ #######
230
+ solar_dict[VispTag.beam(beam)] = next(
231
+ self.read(
232
+ tags=[
233
+ VispTag.intermediate_frame(beam=beam),
234
+ VispTag.task_solar_gain(),
235
+ ],
236
+ decoder=fits_array_decoder,
237
+ )
238
+ )
239
+
228
240
  # Demod
229
241
  #######
230
242
  if self.constants.correct_for_polarization:
@@ -239,18 +251,6 @@ class ScienceCalibration(
239
251
  )
240
252
 
241
253
  for modstate in range(1, self.constants.num_modstates + 1):
242
- # Solar
243
- #######
244
- solar_dict[VispTag.beam(beam)][VispTag.modstate(modstate)] = next(
245
- self.read(
246
- tags=[
247
- VispTag.intermediate_frame(beam=beam, modstate=modstate),
248
- VispTag.task_solar_gain(),
249
- ],
250
- decoder=fits_array_decoder,
251
- )
252
- )
253
-
254
254
  # State Offset
255
255
  ##############
256
256
  state_offset_dict[VispTag.beam(beam)][VispTag.modstate(modstate)] = next(
@@ -586,7 +586,7 @@ class ScienceCalibration(
586
586
  VispTag.readout_exp_time(readout_exp_time)
587
587
  ]
588
588
  background_array = calibrations.background[VispTag.beam(beam)]
589
- solar_gain_array = calibrations.solar_gain[VispTag.beam(beam)][VispTag.modstate(modstate)]
589
+ solar_gain_array = calibrations.solar_gain[VispTag.beam(beam)]
590
590
  angle = calibrations.angle[VispTag.beam(beam)]
591
591
  spec_shift = calibrations.spec_shift[VispTag.beam(beam)]
592
592
  state_offset = calibrations.state_offset[VispTag.beam(beam)][VispTag.modstate(modstate)]