dkist-processing-visp 2.20.14__py3-none-any.whl → 5.1.1__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.
- dkist_processing_visp/__init__.py +1 -0
- dkist_processing_visp/config.py +1 -0
- dkist_processing_visp/models/constants.py +61 -20
- dkist_processing_visp/models/fits_access.py +20 -0
- dkist_processing_visp/models/metric_code.py +10 -0
- dkist_processing_visp/models/parameters.py +129 -24
- dkist_processing_visp/models/tags.py +22 -1
- dkist_processing_visp/models/task_name.py +1 -0
- dkist_processing_visp/parsers/map_repeats.py +1 -0
- dkist_processing_visp/parsers/modulator_states.py +1 -0
- dkist_processing_visp/parsers/polarimeter_mode.py +4 -2
- dkist_processing_visp/parsers/raster_step.py +4 -1
- dkist_processing_visp/parsers/spectrograph_configuration.py +75 -0
- dkist_processing_visp/parsers/time.py +24 -14
- dkist_processing_visp/parsers/visp_l0_fits_access.py +19 -8
- dkist_processing_visp/parsers/visp_l1_fits_access.py +1 -0
- dkist_processing_visp/tasks/__init__.py +1 -0
- dkist_processing_visp/tasks/assemble_movie.py +1 -0
- dkist_processing_visp/tasks/background_light.py +2 -1
- dkist_processing_visp/tasks/dark.py +5 -4
- dkist_processing_visp/tasks/geometric.py +132 -20
- dkist_processing_visp/tasks/instrument_polarization.py +128 -18
- dkist_processing_visp/tasks/l1_output_data.py +203 -0
- dkist_processing_visp/tasks/lamp.py +53 -93
- dkist_processing_visp/tasks/make_movie_frames.py +8 -6
- dkist_processing_visp/tasks/mixin/beam_access.py +1 -0
- dkist_processing_visp/tasks/mixin/corrections.py +54 -4
- dkist_processing_visp/tasks/mixin/downsample.py +1 -0
- dkist_processing_visp/tasks/parse.py +50 -17
- dkist_processing_visp/tasks/quality_metrics.py +5 -4
- dkist_processing_visp/tasks/science.py +126 -46
- dkist_processing_visp/tasks/solar.py +896 -456
- dkist_processing_visp/tasks/visp_base.py +4 -3
- dkist_processing_visp/tasks/write_l1.py +38 -10
- dkist_processing_visp/tests/conftest.py +145 -47
- dkist_processing_visp/tests/header_models.py +157 -20
- dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +21 -78
- dkist_processing_visp/tests/local_trial_workflows/l0_polcals_as_science.py +421 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_solar_gain_as_science.py +387 -0
- dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +18 -75
- dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +346 -14
- dkist_processing_visp/tests/test_assemble_movie.py +2 -3
- dkist_processing_visp/tests/test_assemble_quality.py +89 -4
- dkist_processing_visp/tests/test_background_light.py +51 -44
- dkist_processing_visp/tests/test_dark.py +4 -3
- dkist_processing_visp/tests/test_downsample.py +1 -0
- dkist_processing_visp/tests/test_fits_access.py +43 -0
- dkist_processing_visp/tests/test_geometric.py +45 -4
- dkist_processing_visp/tests/test_instrument_polarization.py +72 -9
- dkist_processing_visp/tests/test_lamp.py +22 -26
- dkist_processing_visp/tests/test_make_movie_frames.py +4 -4
- dkist_processing_visp/tests/test_map_repeats.py +3 -1
- dkist_processing_visp/tests/test_parameters.py +122 -21
- dkist_processing_visp/tests/test_parse.py +164 -18
- dkist_processing_visp/tests/test_quality.py +3 -4
- dkist_processing_visp/tests/test_science.py +113 -15
- dkist_processing_visp/tests/test_solar.py +318 -99
- dkist_processing_visp/tests/test_visp_constants.py +38 -8
- dkist_processing_visp/tests/test_workflows.py +1 -0
- dkist_processing_visp/tests/test_write_l1.py +22 -3
- dkist_processing_visp/workflows/__init__.py +1 -0
- dkist_processing_visp/workflows/l0_processing.py +10 -3
- dkist_processing_visp/workflows/trial_workflows.py +8 -2
- dkist_processing_visp-5.1.1.dist-info/METADATA +552 -0
- dkist_processing_visp-5.1.1.dist-info/RECORD +94 -0
- {dkist_processing_visp-2.20.14.dist-info → dkist_processing_visp-5.1.1.dist-info}/WHEEL +1 -1
- docs/conf.py +5 -1
- docs/gain_correction.rst +52 -44
- docs/science_calibration.rst +7 -0
- dkist_processing_visp/tasks/mixin/line_zones.py +0 -115
- dkist_processing_visp-2.20.14.dist-info/METADATA +0 -196
- dkist_processing_visp-2.20.14.dist-info/RECORD +0 -89
- {dkist_processing_visp-2.20.14.dist-info → dkist_processing_visp-5.1.1.dist-info}/top_level.txt +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""ViSP instrument polarization task. See :doc:`this page </polarization_calibration>` for more information."""
|
|
2
|
+
|
|
2
3
|
from collections import defaultdict
|
|
3
4
|
|
|
4
5
|
import numpy as np
|
|
@@ -18,8 +19,8 @@ from dkist_processing_pac.input_data.drawer import Drawer
|
|
|
18
19
|
from dkist_processing_pac.input_data.dresser import Dresser
|
|
19
20
|
from dkist_service_configuration.logging import logger
|
|
20
21
|
from sklearn.linear_model import RANSACRegressor
|
|
21
|
-
from sklearn.pipeline import make_pipeline
|
|
22
22
|
from sklearn.pipeline import Pipeline
|
|
23
|
+
from sklearn.pipeline import make_pipeline
|
|
23
24
|
from sklearn.preprocessing import PolynomialFeatures
|
|
24
25
|
from sklearn.preprocessing import RobustScaler
|
|
25
26
|
|
|
@@ -72,6 +73,15 @@ class InstrumentPolarizationCalibration(
|
|
|
72
73
|
if not self.constants.correct_for_polarization:
|
|
73
74
|
return
|
|
74
75
|
|
|
76
|
+
polcal_readout_exposure_times = self.constants.polcal_readout_exp_times
|
|
77
|
+
if len(polcal_readout_exposure_times) > 1:
|
|
78
|
+
logger.info(
|
|
79
|
+
"WARNING: More than one polcal readout exposure time detected. "
|
|
80
|
+
"Everything *should* still work, but this is a weird condition that may produce "
|
|
81
|
+
"strange results."
|
|
82
|
+
)
|
|
83
|
+
logger.info(f"{polcal_readout_exposure_times = }")
|
|
84
|
+
|
|
75
85
|
# Process the pol cal frames
|
|
76
86
|
logger.info(
|
|
77
87
|
f"Demodulation matrices will span FOV with shape 1 spectral bin "
|
|
@@ -79,10 +89,22 @@ class InstrumentPolarizationCalibration(
|
|
|
79
89
|
)
|
|
80
90
|
remove_I_trend = self.parameters.pac_remove_linear_I_trend
|
|
81
91
|
for beam in range(1, self.constants.num_beams + 1):
|
|
82
|
-
with self.
|
|
92
|
+
with self.telemetry_span("Generate polcal DARK frame"):
|
|
93
|
+
logger.info("Generating polcal dark frame")
|
|
94
|
+
self.generate_polcal_dark_calibration(
|
|
95
|
+
readout_exp_times=polcal_readout_exposure_times, beam=beam
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
with self.telemetry_span("Generate polcal GAIN frame"):
|
|
99
|
+
logger.info("Generating polcal gain frame")
|
|
100
|
+
self.generate_polcal_gain_calibration(
|
|
101
|
+
readout_exp_times=polcal_readout_exposure_times, beam=beam
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
with self.telemetry_span(f"Reducing CS steps for {beam = }"):
|
|
83
105
|
local_reduced_arrays, global_reduced_arrays = self.reduce_cs_steps(beam)
|
|
84
106
|
|
|
85
|
-
with self.
|
|
107
|
+
with self.telemetry_span(f"Fit CU parameters for {beam = }"):
|
|
86
108
|
local_dresser = Dresser()
|
|
87
109
|
local_dresser.add_drawer(
|
|
88
110
|
Drawer(local_reduced_arrays, remove_I_trend=remove_I_trend)
|
|
@@ -95,13 +117,13 @@ class InstrumentPolarizationCalibration(
|
|
|
95
117
|
local_dresser=local_dresser,
|
|
96
118
|
global_dresser=global_dresser,
|
|
97
119
|
fit_mode=self.parameters.pac_fit_mode,
|
|
98
|
-
init_set=self.
|
|
120
|
+
init_set=self.constants.pac_init_set,
|
|
99
121
|
inherit_global_vary_in_local_fit=True,
|
|
100
122
|
suppress_local_starting_values=True,
|
|
101
123
|
fit_TM=False,
|
|
102
124
|
)
|
|
103
125
|
|
|
104
|
-
with self.
|
|
126
|
+
with self.telemetry_span(f"Resampling demodulation matrices for {beam = }"):
|
|
105
127
|
demod_matrices = pac_fitter.demodulation_matrices
|
|
106
128
|
|
|
107
129
|
self.write(
|
|
@@ -127,7 +149,7 @@ class InstrumentPolarizationCalibration(
|
|
|
127
149
|
demod_matrices = self.reshape_demod_matrices(smoothed_demod)
|
|
128
150
|
logger.info(f"Shape of resampled demodulation matrices: {demod_matrices.shape}")
|
|
129
151
|
|
|
130
|
-
with self.
|
|
152
|
+
with self.telemetry_span(f"Writing demodulation matrices for {beam = }"):
|
|
131
153
|
# Save the demod matrices as intermediate products
|
|
132
154
|
self.write(
|
|
133
155
|
data=demod_matrices,
|
|
@@ -138,10 +160,10 @@ class InstrumentPolarizationCalibration(
|
|
|
138
160
|
encoder=fits_array_encoder,
|
|
139
161
|
)
|
|
140
162
|
|
|
141
|
-
with self.
|
|
163
|
+
with self.telemetry_span("Computing and recording polcal quality metrics"):
|
|
142
164
|
self.record_polcal_quality_metrics(beam, polcal_fitter=pac_fitter)
|
|
143
165
|
|
|
144
|
-
with self.
|
|
166
|
+
with self.telemetry_span("Computing and logging quality metrics"):
|
|
145
167
|
no_of_raw_polcal_frames: int = self.scratch.count_all(
|
|
146
168
|
tags=[
|
|
147
169
|
VispTag.input(),
|
|
@@ -158,7 +180,7 @@ class InstrumentPolarizationCalibration(
|
|
|
158
180
|
self, beam: int
|
|
159
181
|
) -> tuple[dict[int, list[VispL0FitsAccess]], dict[int, list[VispL0FitsAccess]]]:
|
|
160
182
|
"""
|
|
161
|
-
Reduce all
|
|
183
|
+
Reduce all the data for the cal sequence steps for this beam.
|
|
162
184
|
|
|
163
185
|
Parameters
|
|
164
186
|
----------
|
|
@@ -215,10 +237,10 @@ class InstrumentPolarizationCalibration(
|
|
|
215
237
|
|
|
216
238
|
for readout_exp_time in self.constants.polcal_readout_exp_times:
|
|
217
239
|
# Put this loop here because the geometric objects will be constant across exposure times
|
|
218
|
-
logger.info(f"Loading dark for {
|
|
240
|
+
logger.info(f"Loading polcal dark for {beam = }")
|
|
219
241
|
dark_array = next(
|
|
220
242
|
self.read(
|
|
221
|
-
tags=VispTag.
|
|
243
|
+
tags=VispTag.intermediate_frame_polcal_dark(
|
|
222
244
|
beam=beam, readout_exp_time=readout_exp_time
|
|
223
245
|
),
|
|
224
246
|
decoder=fits_array_decoder,
|
|
@@ -318,24 +340,25 @@ class InstrumentPolarizationCalibration(
|
|
|
318
340
|
)
|
|
319
341
|
avg_inst_pol_cal_array = average_numpy_arrays(readout_normalized_arrays)
|
|
320
342
|
|
|
321
|
-
with self.
|
|
343
|
+
with self.telemetry_span(f"Apply basic corrections for {apm_str}"):
|
|
322
344
|
dark_corrected_array = subtract_array_from_arrays(avg_inst_pol_cal_array, dark_array)
|
|
323
345
|
|
|
324
346
|
background_corrected_array = subtract_array_from_arrays(
|
|
325
347
|
dark_corrected_array, background_array
|
|
326
348
|
)
|
|
327
349
|
|
|
328
|
-
|
|
350
|
+
polcal_gain_array = next(
|
|
329
351
|
self.read(
|
|
330
352
|
tags=[
|
|
331
|
-
VispTag.
|
|
332
|
-
|
|
353
|
+
VispTag.intermediate_frame_polcal_gain(
|
|
354
|
+
beam=beam, readout_exp_time=readout_exp_time
|
|
355
|
+
),
|
|
333
356
|
],
|
|
334
357
|
decoder=fits_array_decoder,
|
|
335
358
|
)
|
|
336
359
|
)
|
|
337
360
|
gain_corrected_array = next(
|
|
338
|
-
divide_arrays_by_array(background_corrected_array,
|
|
361
|
+
divide_arrays_by_array(background_corrected_array, polcal_gain_array)
|
|
339
362
|
)
|
|
340
363
|
|
|
341
364
|
geo_corrected_array = self.corrections_correct_geometry(
|
|
@@ -346,7 +369,7 @@ class InstrumentPolarizationCalibration(
|
|
|
346
369
|
self.corrections_remove_spec_geometry(geo_corrected_array, spec_shift)
|
|
347
370
|
)
|
|
348
371
|
|
|
349
|
-
with self.
|
|
372
|
+
with self.telemetry_span(f"Extract macro pixels from {apm_str}"):
|
|
350
373
|
self.set_original_beam_size(gain_corrected_array)
|
|
351
374
|
filtered_array = self.corrections_mask_hairlines(spectral_corrected_array)
|
|
352
375
|
|
|
@@ -366,7 +389,7 @@ class InstrumentPolarizationCalibration(
|
|
|
366
389
|
# Add two dummy dimensions just to keep it 2D.
|
|
367
390
|
global_binned_array = np.nanmedian(filtered_array)[None, None]
|
|
368
391
|
|
|
369
|
-
with self.
|
|
392
|
+
with self.telemetry_span(f"Create reduced VispL0FitsAccess for {apm_str}"):
|
|
370
393
|
local_result = VispL0FitsAccess(
|
|
371
394
|
fits.ImageHDU(local_array, avg_inst_pol_cal_header),
|
|
372
395
|
auto_squeeze=False,
|
|
@@ -527,3 +550,90 @@ class InstrumentPolarizationCalibration(
|
|
|
527
550
|
return next(
|
|
528
551
|
resize_arrays(array, output_shape, order=self.parameters.polcal_demod_upsample_order)
|
|
529
552
|
)
|
|
553
|
+
|
|
554
|
+
def generate_polcal_dark_calibration(
|
|
555
|
+
self, readout_exp_times: list[float] | tuple[float], beam: int
|
|
556
|
+
) -> None:
|
|
557
|
+
"""Compute an average polcal dark array for all polcal exposure times."""
|
|
558
|
+
for readout_exp_time in readout_exp_times:
|
|
559
|
+
logger.info(f"Computing polcal dark for {readout_exp_time = }")
|
|
560
|
+
|
|
561
|
+
dark_arrays = self.read(
|
|
562
|
+
tags=[
|
|
563
|
+
VispTag.input(),
|
|
564
|
+
VispTag.frame(),
|
|
565
|
+
VispTag.task_polcal_dark(),
|
|
566
|
+
VispTag.readout_exp_time(readout_exp_time),
|
|
567
|
+
],
|
|
568
|
+
decoder=fits_access_decoder,
|
|
569
|
+
fits_access_class=VispL0FitsAccess,
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
readout_normalized_arrays = (
|
|
573
|
+
self.beam_access_get_beam(o.data, beam=beam) / o.num_raw_frames_per_fpa
|
|
574
|
+
for o in dark_arrays
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
avg_array = average_numpy_arrays(readout_normalized_arrays)
|
|
578
|
+
self.write(
|
|
579
|
+
data=avg_array,
|
|
580
|
+
tags=[
|
|
581
|
+
VispTag.intermediate_frame_polcal_dark(
|
|
582
|
+
beam=beam, readout_exp_time=readout_exp_time
|
|
583
|
+
),
|
|
584
|
+
],
|
|
585
|
+
encoder=fits_array_encoder,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
def generate_polcal_gain_calibration(
|
|
589
|
+
self, readout_exp_times: list[float] | tuple[float], beam: int
|
|
590
|
+
) -> None:
|
|
591
|
+
"""
|
|
592
|
+
Average 'clear' polcal frames to produce a polcal gain calibration.
|
|
593
|
+
|
|
594
|
+
The polcal dark calibration is applied prior to averaging.
|
|
595
|
+
"""
|
|
596
|
+
for readout_exp_time in readout_exp_times:
|
|
597
|
+
logger.info(f"Computing polcal gain for {readout_exp_time = }")
|
|
598
|
+
|
|
599
|
+
dark_array = next(
|
|
600
|
+
self.read(
|
|
601
|
+
tags=[
|
|
602
|
+
VispTag.intermediate_frame(beam=beam),
|
|
603
|
+
VispTag.task_polcal_dark(),
|
|
604
|
+
VispTag.readout_exp_time(readout_exp_time),
|
|
605
|
+
],
|
|
606
|
+
decoder=fits_array_decoder,
|
|
607
|
+
)
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
gain_arrays = self.read(
|
|
611
|
+
tags=[
|
|
612
|
+
VispTag.input(),
|
|
613
|
+
VispTag.frame(),
|
|
614
|
+
VispTag.task_polcal_gain(),
|
|
615
|
+
VispTag.readout_exp_time(readout_exp_time),
|
|
616
|
+
],
|
|
617
|
+
decoder=fits_access_decoder,
|
|
618
|
+
fits_access_class=VispL0FitsAccess,
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
readout_normalized_arrays = (
|
|
622
|
+
self.beam_access_get_beam(o.data, beam=beam) / o.num_raw_frames_per_fpa
|
|
623
|
+
for o in gain_arrays
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
dark_corrected_arrays = subtract_array_from_arrays(
|
|
627
|
+
arrays=readout_normalized_arrays, array_to_subtract=dark_array
|
|
628
|
+
)
|
|
629
|
+
|
|
630
|
+
avg_array = average_numpy_arrays(dark_corrected_arrays)
|
|
631
|
+
self.write(
|
|
632
|
+
data=avg_array,
|
|
633
|
+
tags=[
|
|
634
|
+
VispTag.intermediate_frame_polcal_gain(
|
|
635
|
+
beam=beam, readout_exp_time=readout_exp_time
|
|
636
|
+
),
|
|
637
|
+
],
|
|
638
|
+
encoder=fits_array_encoder,
|
|
639
|
+
)
|
|
@@ -1,13 +1,216 @@
|
|
|
1
1
|
"""Subclass of AssembleQualityData that causes the correct polcal metrics to build."""
|
|
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
|
|
2
8
|
from dkist_processing_common.tasks import AssembleQualityData
|
|
3
9
|
|
|
4
10
|
__all__ = ["VispAssembleQualityData"]
|
|
5
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
|
+
|
|
6
16
|
|
|
7
17
|
class VispAssembleQualityData(AssembleQualityData):
|
|
8
18
|
"""Subclass just so that the polcal_label_list can be populated."""
|
|
9
19
|
|
|
20
|
+
constants: VispConstants
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def constants_model_class(self):
|
|
24
|
+
"""Get ViSP pipeline constants."""
|
|
25
|
+
return VispConstants
|
|
26
|
+
|
|
10
27
|
@property
|
|
11
28
|
def polcal_label_list(self) -> list[str]:
|
|
12
29
|
"""Return labels for beams 1 and 2."""
|
|
13
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,5 +1,5 @@
|
|
|
1
1
|
"""ViSP lamp calibration task. See :doc:`this page </gain_correction>` for more information."""
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
from dkist_processing_common.codecs.fits import fits_access_decoder
|
|
4
4
|
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
5
5
|
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
@@ -29,13 +29,14 @@ class LampCalibration(
|
|
|
29
29
|
|
|
30
30
|
Parameters
|
|
31
31
|
----------
|
|
32
|
-
recipe_run_id
|
|
32
|
+
recipe_run_id
|
|
33
33
|
id of the recipe run used to identify the workflow run this task is part of
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
workflow_name
|
|
35
36
|
name of the workflow to which this instance of the task belongs
|
|
36
|
-
workflow_version : str
|
|
37
|
-
version of the workflow to which this instance of the task belongs
|
|
38
37
|
|
|
38
|
+
workflow_version
|
|
39
|
+
version of the workflow to which this instance of the task belongs
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
record_provenance = True
|
|
@@ -44,9 +45,11 @@ class LampCalibration(
|
|
|
44
45
|
"""
|
|
45
46
|
For each beam.
|
|
46
47
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
-
|
|
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
|
|
50
53
|
- Record quality metrics
|
|
51
54
|
|
|
52
55
|
Returns
|
|
@@ -54,12 +57,14 @@ class LampCalibration(
|
|
|
54
57
|
None
|
|
55
58
|
|
|
56
59
|
"""
|
|
57
|
-
with self.
|
|
60
|
+
with self.telemetry_span(
|
|
58
61
|
f"Generate lamp gains for {self.constants.num_beams} beams and {len(self.constants.lamp_readout_exp_times)} exposure times"
|
|
59
62
|
):
|
|
60
|
-
for
|
|
61
|
-
|
|
62
|
-
|
|
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}")
|
|
63
68
|
dark_array = next(
|
|
64
69
|
self.read(
|
|
65
70
|
tags=VispTag.intermediate_frame_dark(
|
|
@@ -69,20 +74,45 @@ class LampCalibration(
|
|
|
69
74
|
)
|
|
70
75
|
)
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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,
|
|
77
88
|
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
93
|
+
)
|
|
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)
|
|
83
98
|
)
|
|
84
99
|
|
|
85
|
-
|
|
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
|
+
|
|
115
|
+
with self.telemetry_span("Computing and logging quality metrics"):
|
|
86
116
|
no_of_raw_lamp_frames: int = self.scratch.count_all(
|
|
87
117
|
tags=[
|
|
88
118
|
VispTag.input(),
|
|
@@ -94,73 +124,3 @@ class LampCalibration(
|
|
|
94
124
|
self.quality_store_task_type_counts(
|
|
95
125
|
task_type=TaskName.lamp_gain.value, total_frames=no_of_raw_lamp_frames
|
|
96
126
|
)
|
|
97
|
-
|
|
98
|
-
def compute_and_write_master_lamp_gain_for_modstate(
|
|
99
|
-
self,
|
|
100
|
-
modstate: int,
|
|
101
|
-
dark_array: np.ndarray,
|
|
102
|
-
beam: int,
|
|
103
|
-
readout_exp_time: float,
|
|
104
|
-
) -> None:
|
|
105
|
-
"""
|
|
106
|
-
Compute and write master lamp gain for a given modstate and beam.
|
|
107
|
-
|
|
108
|
-
Generally the algorithm is:
|
|
109
|
-
1. Average input gain arrays
|
|
110
|
-
2. Subtract average dark to get the dark corrected gain data
|
|
111
|
-
3. Normalize each beam to unity mean
|
|
112
|
-
4. Write to disk
|
|
113
|
-
|
|
114
|
-
Parameters
|
|
115
|
-
----------
|
|
116
|
-
modstate : int
|
|
117
|
-
The modulator state to calculate the master lamp gain for
|
|
118
|
-
|
|
119
|
-
dark_array : np.ndarray
|
|
120
|
-
The master dark to be subtracted from each lamp gain file
|
|
121
|
-
|
|
122
|
-
beam : int
|
|
123
|
-
The number of the beam
|
|
124
|
-
|
|
125
|
-
readout_exp_time : float
|
|
126
|
-
Exposure time of single readout
|
|
127
|
-
|
|
128
|
-
Returns
|
|
129
|
-
-------
|
|
130
|
-
None
|
|
131
|
-
"""
|
|
132
|
-
apm_str = f"{beam = }, {modstate = }, and {readout_exp_time = }"
|
|
133
|
-
# Get the input lamp gain arrays
|
|
134
|
-
tags = [
|
|
135
|
-
VispTag.input(),
|
|
136
|
-
VispTag.frame(),
|
|
137
|
-
VispTag.task_lamp_gain(),
|
|
138
|
-
VispTag.modstate(modstate),
|
|
139
|
-
VispTag.readout_exp_time(readout_exp_time),
|
|
140
|
-
]
|
|
141
|
-
input_lamp_gain_objs = self.read(
|
|
142
|
-
tags=tags, decoder=fits_access_decoder, fits_access_class=VispL0FitsAccess
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
with self.apm_processing_step(f"Computing gain for {apm_str}"):
|
|
146
|
-
|
|
147
|
-
readout_normalized_arrays = (
|
|
148
|
-
self.beam_access_get_beam(o.data, beam=beam) / o.num_raw_frames_per_fpa
|
|
149
|
-
for o in input_lamp_gain_objs
|
|
150
|
-
)
|
|
151
|
-
averaged_gain_data = average_numpy_arrays(readout_normalized_arrays)
|
|
152
|
-
|
|
153
|
-
dark_corrected_gain_data = next(
|
|
154
|
-
subtract_array_from_arrays(averaged_gain_data, dark_array)
|
|
155
|
-
)
|
|
156
|
-
filtered_gain_data = self.corrections_mask_hairlines(dark_corrected_gain_data)
|
|
157
|
-
|
|
158
|
-
with self.apm_writing_step(f"Writing gain array for {apm_str}"):
|
|
159
|
-
self.write(
|
|
160
|
-
data=filtered_gain_data,
|
|
161
|
-
tags=[
|
|
162
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
163
|
-
VispTag.task_lamp_gain(),
|
|
164
|
-
],
|
|
165
|
-
encoder=fits_array_encoder,
|
|
166
|
-
)
|