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,25 +1,124 @@
|
|
|
1
1
|
"""ViSP solar calibration task. See :doc:`this page </gain_correction>` for more information."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import partial
|
|
6
|
+
from typing import Callable
|
|
7
|
+
|
|
8
|
+
import astropy.units as u
|
|
2
9
|
import numpy as np
|
|
3
10
|
import scipy.ndimage as spnd
|
|
4
|
-
|
|
11
|
+
from astropy.time import Time
|
|
12
|
+
from astropy.units import Quantity
|
|
13
|
+
from astropy.wcs import WCS
|
|
14
|
+
from dkist_processing_common.codecs.asdf import asdf_encoder
|
|
5
15
|
from dkist_processing_common.codecs.fits import fits_access_decoder
|
|
6
16
|
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
7
17
|
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
18
|
+
from dkist_processing_common.models.dkist_location import location_of_dkist
|
|
8
19
|
from dkist_processing_common.models.task_name import TaskName
|
|
9
20
|
from dkist_processing_common.tasks.mixin.quality import QualityMixin
|
|
10
21
|
from dkist_processing_math.arithmetic import divide_arrays_by_array
|
|
11
22
|
from dkist_processing_math.arithmetic import subtract_array_from_arrays
|
|
12
23
|
from dkist_processing_math.statistics import average_numpy_arrays
|
|
13
24
|
from dkist_service_configuration.logging import logger
|
|
14
|
-
|
|
25
|
+
from lmfit.parameter import Parameters
|
|
26
|
+
from sklearn.linear_model import RANSACRegressor
|
|
27
|
+
from sklearn.pipeline import Pipeline
|
|
28
|
+
from sklearn.pipeline import make_pipeline
|
|
29
|
+
from sklearn.preprocessing import PolynomialFeatures
|
|
30
|
+
from sklearn.preprocessing import RobustScaler
|
|
31
|
+
from solar_wavelength_calibration import AngleBoundRange
|
|
32
|
+
from solar_wavelength_calibration import Atlas
|
|
33
|
+
from solar_wavelength_calibration import BoundsModel
|
|
34
|
+
from solar_wavelength_calibration import DispersionBoundRange
|
|
35
|
+
from solar_wavelength_calibration import FitFlagsModel
|
|
36
|
+
from solar_wavelength_calibration import LengthBoundRange
|
|
37
|
+
from solar_wavelength_calibration import UnitlessBoundRange
|
|
38
|
+
from solar_wavelength_calibration import WavelengthCalibrationFitter
|
|
39
|
+
from solar_wavelength_calibration import WavelengthCalibrationParameters
|
|
40
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import FitResult
|
|
41
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import WavelengthParameters
|
|
42
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import calculate_initial_crval_guess
|
|
43
|
+
from sunpy.coordinates import HeliocentricInertial
|
|
44
|
+
|
|
45
|
+
from dkist_processing_visp.models.metric_code import VispMetricCode
|
|
15
46
|
from dkist_processing_visp.models.tags import VispTag
|
|
16
47
|
from dkist_processing_visp.parsers.visp_l0_fits_access import VispL0FitsAccess
|
|
17
48
|
from dkist_processing_visp.tasks.mixin.beam_access import BeamAccessMixin
|
|
18
49
|
from dkist_processing_visp.tasks.mixin.corrections import CorrectionsMixin
|
|
19
|
-
from dkist_processing_visp.tasks.mixin.line_zones import LineZonesMixin
|
|
20
50
|
from dkist_processing_visp.tasks.visp_base import VispTaskBase
|
|
21
51
|
|
|
22
|
-
__all__ = [
|
|
52
|
+
__all__ = [
|
|
53
|
+
"SolarCalibration",
|
|
54
|
+
"WavelengthCalibrationParametersWithContinuum",
|
|
55
|
+
"polynomial_continuum_model",
|
|
56
|
+
"compute_order",
|
|
57
|
+
"compute_initial_dispersion",
|
|
58
|
+
"compute_doppler_velocity",
|
|
59
|
+
"compute_input_wavelength_vector",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class WavelengthCalibrationParametersWithContinuum(WavelengthCalibrationParameters):
|
|
64
|
+
"""
|
|
65
|
+
Subclass of `~solar_wavelength_calibration.WavelengthCalibrationParameters` that adds a polynomial continuum parameterization.
|
|
66
|
+
|
|
67
|
+
The order of the continuum polynomial is set with the new ``continuum_poly_fit_oder`` model field. The `lmfit_parameters`
|
|
68
|
+
property now adds a set of parameters that represent the polynomial coefficients.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
continuum_poly_fit_order: int
|
|
72
|
+
normalized_abscissa: np.ndarray
|
|
73
|
+
zeroth_order_continuum_coefficient: float
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def continuum_function(self) -> Callable[[np.ndarray, Parameters], np.ndarray]:
|
|
77
|
+
"""Return a partial function of `polynomial_continuum_model` pre-loaded with the fit order and normalized abscissa."""
|
|
78
|
+
return partial(
|
|
79
|
+
polynomial_continuum_model,
|
|
80
|
+
fit_order=self.continuum_poly_fit_order,
|
|
81
|
+
abscissa=self.normalized_abscissa,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def lmfit_parameters(self) -> Parameters:
|
|
86
|
+
"""
|
|
87
|
+
Add continuum polynomial coefficient parameters to the standard `~solar_wavelength_calibration.WavelengthCalibrationParameters.lmfit_parameters`.
|
|
88
|
+
|
|
89
|
+
Each coefficient gets its own parameter called ``poly_coeff_{o:02n}``. The 0th order (i.e., constant continuum
|
|
90
|
+
level) coefficient has bounds [0.7, 1.3] and all higher-order coefficients have bounds [-1, 1].
|
|
91
|
+
"""
|
|
92
|
+
# NOTE: We set `vary=True` because otherwise we just wouldn't use this class
|
|
93
|
+
# We set the bounds here because it's easier that defining a custom `BoundsModel` class that can
|
|
94
|
+
# dynamically create the required number of "poly_coeff_{o:02n}" fields. Sorry if this bites you!
|
|
95
|
+
params = super().lmfit_parameters
|
|
96
|
+
for o in range(self.continuum_poly_fit_order + 1):
|
|
97
|
+
# `np.polyval` uses its input coefficient list "backwards", so `poly_coeff_{self.continuum_poly_fit_order}`
|
|
98
|
+
# is the 0th order polynomial term.
|
|
99
|
+
params.add(
|
|
100
|
+
f"poly_coeff_{o:02n}",
|
|
101
|
+
vary=True,
|
|
102
|
+
value=(
|
|
103
|
+
self.zeroth_order_continuum_coefficient
|
|
104
|
+
if o == self.continuum_poly_fit_order
|
|
105
|
+
else 0
|
|
106
|
+
),
|
|
107
|
+
min=-1,
|
|
108
|
+
max=1,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
params[f"poly_coeff_{self.continuum_poly_fit_order:02n}"].min = (
|
|
112
|
+
self.zeroth_order_continuum_coefficient * 0.5
|
|
113
|
+
)
|
|
114
|
+
params[f"poly_coeff_{self.continuum_poly_fit_order:02n}"].max = (
|
|
115
|
+
self.zeroth_order_continuum_coefficient * 1.5
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Remove the default continuum parameterization
|
|
119
|
+
del params["continuum_level"]
|
|
120
|
+
|
|
121
|
+
return params
|
|
23
122
|
|
|
24
123
|
|
|
25
124
|
class SolarCalibration(
|
|
@@ -27,18 +126,19 @@ class SolarCalibration(
|
|
|
27
126
|
BeamAccessMixin,
|
|
28
127
|
CorrectionsMixin,
|
|
29
128
|
QualityMixin,
|
|
30
|
-
LineZonesMixin,
|
|
31
129
|
):
|
|
32
130
|
"""
|
|
33
131
|
Task class for generating Solar Gain images for each beam/modstate.
|
|
34
132
|
|
|
35
133
|
Parameters
|
|
36
134
|
----------
|
|
37
|
-
recipe_run_id
|
|
135
|
+
recipe_run_id
|
|
38
136
|
id of the recipe run used to identify the workflow run this task is part of
|
|
39
|
-
|
|
137
|
+
|
|
138
|
+
workflow_name
|
|
40
139
|
name of the workflow to which this instance of the task belongs
|
|
41
|
-
|
|
140
|
+
|
|
141
|
+
workflow_version
|
|
42
142
|
version of the workflow to which this instance of the task belongs
|
|
43
143
|
|
|
44
144
|
"""
|
|
@@ -49,127 +149,162 @@ class SolarCalibration(
|
|
|
49
149
|
"""
|
|
50
150
|
For each beam.
|
|
51
151
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
-------
|
|
62
|
-
None
|
|
63
|
-
|
|
152
|
+
#. Do dark, background, lamp, and geometric corrections
|
|
153
|
+
#. Compute an initial separation of low-order spectral vignette signal from the true solar spectrum
|
|
154
|
+
#. Compute a 2D vignette signal by fitting solar spectrum residuals along the slit
|
|
155
|
+
#. Remove the 2D vignette signal from the averaged gain array
|
|
156
|
+
#. Compute the characteristic spectra
|
|
157
|
+
#. Re-apply the spectral curvature to the characteristic spectra
|
|
158
|
+
#. Re-apply angle and state offset distortions to the characteristic spectra
|
|
159
|
+
#. Remove the distorted characteristic solar spectra from the original, dark-corrected spectra
|
|
160
|
+
#. Write final gain to disk
|
|
64
161
|
"""
|
|
65
162
|
for beam in range(1, self.constants.num_beams + 1):
|
|
66
163
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
apm_str = f"{beam = } and {modstate = }"
|
|
71
|
-
with self.apm_processing_step(f"Initial corrections for {apm_str}"):
|
|
72
|
-
self.do_initial_corrections(beam=beam, modstate=modstate)
|
|
73
|
-
|
|
74
|
-
with self.apm_processing_step(f"Computing characteristic spectra for {apm_str}"):
|
|
75
|
-
char_spec = self.compute_characteristic_spectra(beam=beam, modstate=modstate)
|
|
76
|
-
self.write(
|
|
77
|
-
data=char_spec,
|
|
78
|
-
encoder=fits_array_encoder,
|
|
79
|
-
tags=[VispTag.debug(), VispTag.frame()],
|
|
80
|
-
relative_path=f"DEBUG_SC_CHAR_SPEC_BEAM_{beam}_MODSTATE_{modstate}.dat",
|
|
81
|
-
overwrite=True,
|
|
82
|
-
)
|
|
164
|
+
apm_str = f"{beam = }"
|
|
165
|
+
with self.telemetry_span(f"Initial corrections for {apm_str}"):
|
|
166
|
+
self.do_initial_corrections(beam=beam)
|
|
83
167
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
):
|
|
87
|
-
spec_shift = next(
|
|
88
|
-
self.read(
|
|
89
|
-
tags=[
|
|
90
|
-
VispTag.intermediate_frame(beam=beam),
|
|
91
|
-
VispTag.task_geometric_spectral_shifts(),
|
|
92
|
-
],
|
|
93
|
-
decoder=fits_array_decoder,
|
|
94
|
-
)
|
|
95
|
-
)
|
|
96
|
-
redistorted_char_spec = next(
|
|
97
|
-
self.corrections_remove_spec_geometry(
|
|
98
|
-
arrays=char_spec, spec_shift=-1 * spec_shift
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
self.write(
|
|
102
|
-
data=redistorted_char_spec,
|
|
103
|
-
encoder=fits_array_encoder,
|
|
104
|
-
tags=[VispTag.debug(), VispTag.frame()],
|
|
105
|
-
relative_path=f"DEBUG_SC_CHAR_DISTORT_BEAM_{beam}_MODSTATE_{modstate}.dat",
|
|
106
|
-
overwrite=True,
|
|
107
|
-
)
|
|
168
|
+
with self.telemetry_span(f"Fit atlas with continuum for {apm_str}"):
|
|
169
|
+
representative_spectrum = self.get_representative_spectrum(beam)
|
|
108
170
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
171
|
+
self.write(
|
|
172
|
+
data=representative_spectrum,
|
|
173
|
+
tags=[VispTag.debug(), VispTag.beam(beam), VispTag.task("REP_SPEC")],
|
|
174
|
+
encoder=fits_array_encoder,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
logger.info("Deriving values from instrument configuration")
|
|
178
|
+
order = compute_order(
|
|
179
|
+
central_wavelength=self.constants.wavelength * u.nm,
|
|
180
|
+
incident_light_angle=self.constants.incident_light_angle_deg,
|
|
181
|
+
reflected_light_angle=self.constants.reflected_light_angle_deg,
|
|
182
|
+
grating_constant=self.constants.grating_constant_inverse_mm,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
initial_dispersion = compute_initial_dispersion(
|
|
186
|
+
central_wavelength=self.constants.wavelength * u.nm,
|
|
187
|
+
incident_light_angle=self.constants.incident_light_angle_deg,
|
|
188
|
+
reflected_light_angle=self.constants.reflected_light_angle_deg,
|
|
189
|
+
lens_parameters=self.parameters.wavecal_camera_lens_parameters,
|
|
190
|
+
pixel_pitch=self.parameters.wavecal_pixel_pitch_micron_per_pix,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
doppler_velocity = compute_doppler_velocity(
|
|
194
|
+
time_of_observation=self.constants.solar_gain_ip_start_time
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
self._log_wavecal_parameters(
|
|
198
|
+
dispersion=initial_dispersion, order=order, doppler_velocity=doppler_velocity
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
fit_result = self.fit_initial_vignette(
|
|
202
|
+
representative_spectrum=representative_spectrum,
|
|
203
|
+
dispersion=initial_dispersion,
|
|
204
|
+
spectral_order=order,
|
|
205
|
+
doppler_velocity=doppler_velocity,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
first_vignette_estimation = self.compute_initial_vignette_estimation(
|
|
209
|
+
beam=beam,
|
|
210
|
+
representative_spectrum=representative_spectrum,
|
|
211
|
+
continuum=fit_result.best_fit_continuum,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
self.write(
|
|
215
|
+
data=first_vignette_estimation,
|
|
216
|
+
tags=[VispTag.debug(), VispTag.beam(beam), VispTag.task("FIRST_VIGNETTE_EST")],
|
|
217
|
+
encoder=fits_array_encoder,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
with self.telemetry_span(f"Estimate 2D vignetting signature for {apm_str}"):
|
|
221
|
+
final_vignette = self.compute_final_vignette_estimate(
|
|
222
|
+
init_vignette_correction=first_vignette_estimation
|
|
223
|
+
)
|
|
224
|
+
vignette_corrected_gain = self.geo_corrected_beam_data(beam=beam) / final_vignette
|
|
225
|
+
|
|
226
|
+
self.write(
|
|
227
|
+
data=final_vignette,
|
|
228
|
+
tags=[VispTag.debug(), VispTag.beam(beam), VispTag.task("FINAL_VIGNETTE")],
|
|
229
|
+
encoder=fits_array_encoder,
|
|
230
|
+
)
|
|
231
|
+
self.write(
|
|
232
|
+
data=vignette_corrected_gain,
|
|
233
|
+
tags=[VispTag.debug(), VispTag.beam(beam), VispTag.task("VIGNETTE_CORR")],
|
|
234
|
+
encoder=fits_array_encoder,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
with self.telemetry_span(f"Save vignette quality metrics for {apm_str}"):
|
|
238
|
+
self.record_vignette_quality_metrics(
|
|
239
|
+
beam=beam,
|
|
240
|
+
representative_spectrum=representative_spectrum,
|
|
241
|
+
fit_result=fit_result,
|
|
242
|
+
vignette_corrected_gain=vignette_corrected_gain,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
with self.telemetry_span(f"Compute characteristic spectra for {apm_str}"):
|
|
246
|
+
char_spec = self.compute_characteristic_spectra(vignette_corrected_gain)
|
|
247
|
+
self.write(
|
|
248
|
+
data=char_spec,
|
|
249
|
+
encoder=fits_array_encoder,
|
|
250
|
+
tags=[VispTag.debug(), VispTag.frame()],
|
|
251
|
+
relative_path=f"DEBUG_SC_CHAR_SPEC_BEAM_{beam}.dat",
|
|
252
|
+
overwrite=True,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
with self.telemetry_span(
|
|
256
|
+
f"Applying spectral shifts to characteristic spectra for {apm_str}"
|
|
257
|
+
):
|
|
258
|
+
spec_shift = next(
|
|
259
|
+
self.read(
|
|
116
260
|
tags=[
|
|
117
|
-
VispTag.
|
|
118
|
-
VispTag.
|
|
119
|
-
VispTag.task("CHAR_SPEC_DISTORT_SHIFT"),
|
|
261
|
+
VispTag.intermediate_frame(beam=beam),
|
|
262
|
+
VispTag.task_geometric_spectral_shifts(),
|
|
120
263
|
],
|
|
121
|
-
|
|
122
|
-
overwrite=True,
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
with self.apm_processing_step(
|
|
126
|
-
f"Refining characteristic spectral shifts for {apm_str}"
|
|
127
|
-
):
|
|
128
|
-
refined_char_spec = self.refine_gain_shifts(
|
|
129
|
-
char_spec=reshifted_char_spec, beam=beam, modstate=modstate
|
|
130
|
-
)
|
|
131
|
-
self.write(
|
|
132
|
-
data=refined_char_spec,
|
|
133
|
-
encoder=fits_array_encoder,
|
|
134
|
-
tags=[VispTag.debug(), VispTag.frame()],
|
|
135
|
-
relative_path=f"DEBUG_SC_CHAR_SPEC_REFINE_BEAM_{beam}_MODSTATE_{modstate}.dat",
|
|
136
|
-
overwrite=True,
|
|
264
|
+
decoder=fits_array_decoder,
|
|
137
265
|
)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
266
|
+
)
|
|
267
|
+
redistorted_char_spec = next(
|
|
268
|
+
self.corrections_remove_spec_geometry(
|
|
269
|
+
arrays=char_spec, spec_shift=-1 * spec_shift
|
|
142
270
|
)
|
|
271
|
+
)
|
|
272
|
+
self.write(
|
|
273
|
+
data=redistorted_char_spec,
|
|
274
|
+
encoder=fits_array_encoder,
|
|
275
|
+
tags=[VispTag.debug(), VispTag.frame()],
|
|
276
|
+
relative_path=f"DEBUG_SC_CHAR_DISTORT_BEAM_{beam}.dat",
|
|
277
|
+
overwrite=True,
|
|
278
|
+
)
|
|
143
279
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
280
|
+
with self.telemetry_span(f"Re-distorting characteristic spectra for {apm_str}"):
|
|
281
|
+
reshifted_char_spec = self.distort_characteristic_spectra(
|
|
282
|
+
char_spec=redistorted_char_spec, beam=beam
|
|
283
|
+
)
|
|
147
284
|
self.write(
|
|
148
|
-
data=
|
|
285
|
+
data=reshifted_char_spec,
|
|
149
286
|
encoder=fits_array_encoder,
|
|
150
287
|
tags=[
|
|
151
|
-
VispTag.
|
|
152
|
-
VispTag.
|
|
288
|
+
VispTag.beam(beam),
|
|
289
|
+
VispTag.task("CHAR_SPEC_DISTORT_SHIFT"),
|
|
153
290
|
],
|
|
154
|
-
relative_path=f"
|
|
291
|
+
relative_path=f"DEBUG_SC_CHAR_SPEC_DISTORT_SHIFT_BEAM_{beam}.dat",
|
|
155
292
|
overwrite=True,
|
|
156
293
|
)
|
|
157
294
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
with self.apm_processing_step(f"Equalizing modstates for {beam = }"):
|
|
161
|
-
equalized_gain_dict = self.equalize_modstates(pre_equalized_gain_dict)
|
|
162
|
-
|
|
163
|
-
for modstate in range(1, self.constants.num_modstates + 1):
|
|
295
|
+
with self.telemetry_span(f"Removing solar signal from {apm_str}"):
|
|
296
|
+
gain = self.remove_solar_signal(char_solar_spectra=reshifted_char_spec, beam=beam)
|
|
164
297
|
|
|
165
|
-
|
|
298
|
+
with self.telemetry_span(f"Masking hairlines from {apm_str}"):
|
|
299
|
+
final_gain = self.corrections_mask_hairlines(gain)
|
|
166
300
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
301
|
+
with self.telemetry_span(f"Writing solar gain for {beam = }"):
|
|
302
|
+
self.write_solar_gain_calibration(
|
|
303
|
+
gain_array=final_gain,
|
|
304
|
+
beam=beam,
|
|
305
|
+
)
|
|
171
306
|
|
|
172
|
-
with self.
|
|
307
|
+
with self.telemetry_span("Computing and logging quality metrics"):
|
|
173
308
|
no_of_raw_solar_frames: int = self.scratch.count_all(
|
|
174
309
|
tags=[
|
|
175
310
|
VispTag.input(),
|
|
@@ -182,84 +317,36 @@ class SolarCalibration(
|
|
|
182
317
|
task_type=TaskName.solar_gain.value, total_frames=no_of_raw_solar_frames
|
|
183
318
|
)
|
|
184
319
|
|
|
185
|
-
def
|
|
186
|
-
"""
|
|
187
|
-
Array for a single beam/modstate that has dark, lamp, angle, and state offset corrections.
|
|
188
|
-
|
|
189
|
-
Parameters
|
|
190
|
-
----------
|
|
191
|
-
beam : int
|
|
192
|
-
The beam number for this array
|
|
193
|
-
|
|
194
|
-
modstate : int
|
|
195
|
-
The modulator state for this array
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
Returns
|
|
199
|
-
-------
|
|
200
|
-
np.ndarray
|
|
201
|
-
Array with dark signal, lamp signal, angle and state offset removed
|
|
202
|
-
|
|
203
|
-
"""
|
|
204
|
-
tags = [
|
|
205
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
206
|
-
VispTag.task("SC_GEO_NOSHIFT"),
|
|
207
|
-
]
|
|
208
|
-
array_generator = self.read(tags=tags, decoder=fits_array_decoder)
|
|
209
|
-
return next(array_generator)
|
|
210
|
-
|
|
211
|
-
def geo_corrected_modstate_data(self, beam: int, modstate: int) -> np.ndarray:
|
|
320
|
+
def geo_corrected_beam_data(self, beam: int) -> np.ndarray:
|
|
212
321
|
"""
|
|
213
|
-
Array for a single beam
|
|
322
|
+
Array for a single beam that has dark, lamp, and ALL of the geometric corrects.
|
|
214
323
|
|
|
215
324
|
Parameters
|
|
216
325
|
----------
|
|
217
|
-
beam
|
|
326
|
+
beam
|
|
218
327
|
The beam number for this array
|
|
219
328
|
|
|
220
|
-
modstate : int
|
|
221
|
-
The modulator state for this array
|
|
222
|
-
|
|
223
|
-
|
|
224
329
|
Returns
|
|
225
330
|
-------
|
|
226
|
-
|
|
331
|
+
Fully corrected array
|
|
227
332
|
Array with dark signal, and lamp signal removed, and all geometric corrections made
|
|
228
333
|
"""
|
|
229
334
|
tags = [
|
|
230
|
-
VispTag.intermediate_frame(beam=beam
|
|
335
|
+
VispTag.intermediate_frame(beam=beam),
|
|
231
336
|
VispTag.task("SC_GEO_ALL"),
|
|
232
337
|
]
|
|
233
338
|
array_generator = self.read(tags=tags, decoder=fits_array_decoder)
|
|
234
339
|
return next(array_generator)
|
|
235
340
|
|
|
236
|
-
def
|
|
237
|
-
"""
|
|
238
|
-
Array for a single beam/modstate that has dark, background, and lamp gain applied.
|
|
239
|
-
|
|
240
|
-
This is used to refine the final shifts in the re-distorted characteristic spectra. Having the lamp gain applied
|
|
241
|
-
removes large optical features that would otherwise pollute the match to the characteristic spectra (which has
|
|
242
|
-
no optical features).
|
|
243
|
-
"""
|
|
244
|
-
tags = [
|
|
245
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
246
|
-
VispTag.task("SC_LAMP_CORR"),
|
|
247
|
-
]
|
|
248
|
-
array_generator = self.read(tags=tags, decoder=fits_array_decoder)
|
|
249
|
-
return next(array_generator)
|
|
250
|
-
|
|
251
|
-
def bg_corrected_modstate_data(self, beam: int, modstate: int) -> np.ndarray:
|
|
341
|
+
def bg_corrected_beam_data(self, beam: int) -> np.ndarray:
|
|
252
342
|
"""
|
|
253
|
-
Array for a single beam
|
|
343
|
+
Array for a single beam that has only has dark and background corrects applied.
|
|
254
344
|
|
|
255
345
|
Parameters
|
|
256
346
|
----------
|
|
257
|
-
beam
|
|
347
|
+
beam
|
|
258
348
|
The beam number for this array
|
|
259
349
|
|
|
260
|
-
modstate : int
|
|
261
|
-
The modulator state for this array
|
|
262
|
-
|
|
263
350
|
|
|
264
351
|
Returns
|
|
265
352
|
-------
|
|
@@ -267,37 +354,25 @@ class SolarCalibration(
|
|
|
267
354
|
Array with dark and background signals removed
|
|
268
355
|
"""
|
|
269
356
|
tags = [
|
|
270
|
-
VispTag.intermediate_frame(beam=beam
|
|
357
|
+
VispTag.intermediate_frame(beam=beam),
|
|
271
358
|
VispTag.task("SC_BG_ONLY"),
|
|
272
359
|
]
|
|
273
360
|
array_generator = self.read(tags=tags, decoder=fits_array_decoder)
|
|
274
361
|
return next(array_generator)
|
|
275
362
|
|
|
276
|
-
def do_initial_corrections(self, beam: int
|
|
363
|
+
def do_initial_corrections(self, beam: int) -> None:
|
|
277
364
|
"""
|
|
278
365
|
Do dark, lamp, and geometric corrections for all data that will be used.
|
|
279
366
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
SC_BG_ONLY - The solar gain arrays with only a dark and background correction.
|
|
283
|
-
|
|
284
|
-
SC_GEO_NOSHIFT - The solar gain arrays after dark, lamp, angle, and state offset correction. In other words,
|
|
285
|
-
they do not have spectral curvature removed. These are used to reshift the characteristic
|
|
286
|
-
spectra to the original spectral curvature.
|
|
367
|
+
We also save an intermediate product of the average solar gain array with only dark and background corrections.
|
|
368
|
+
This array will later be used to produce the final solar gain. It's task tag is SC_BG_ONLY.
|
|
287
369
|
|
|
288
370
|
Parameters
|
|
289
371
|
----------
|
|
290
|
-
beam
|
|
372
|
+
beam
|
|
291
373
|
The beam number for this array
|
|
292
|
-
|
|
293
|
-
modstate : int
|
|
294
|
-
The modulator state for this array
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
Returns
|
|
298
|
-
-------
|
|
299
|
-
None
|
|
300
374
|
"""
|
|
375
|
+
all_exp_time_arrays = []
|
|
301
376
|
for readout_exp_time in self.constants.solar_readout_exp_times:
|
|
302
377
|
dark_array = next(
|
|
303
378
|
self.read(
|
|
@@ -317,15 +392,12 @@ class SolarCalibration(
|
|
|
317
392
|
)
|
|
318
393
|
)
|
|
319
394
|
|
|
320
|
-
logger.info(
|
|
321
|
-
f"Doing dark, background, lamp, and geo corrections for {beam=} and {modstate=}"
|
|
322
|
-
)
|
|
395
|
+
logger.info(f"Doing dark, background, lamp, and geo corrections for {beam=}")
|
|
323
396
|
## Load frames
|
|
324
397
|
tags = [
|
|
325
398
|
VispTag.input(),
|
|
326
399
|
VispTag.frame(),
|
|
327
400
|
VispTag.task_solar_gain(),
|
|
328
|
-
VispTag.modstate(modstate),
|
|
329
401
|
VispTag.readout_exp_time(readout_exp_time),
|
|
330
402
|
]
|
|
331
403
|
input_solar_gain_objs = self.read(
|
|
@@ -337,140 +409,120 @@ class SolarCalibration(
|
|
|
337
409
|
for o in input_solar_gain_objs
|
|
338
410
|
)
|
|
339
411
|
|
|
340
|
-
## Average
|
|
341
412
|
avg_solar_array = average_numpy_arrays(readout_normalized_arrays)
|
|
342
413
|
|
|
343
|
-
## Dark correction
|
|
344
414
|
dark_corrected_solar_array = subtract_array_from_arrays(
|
|
345
415
|
arrays=avg_solar_array, array_to_subtract=dark_array
|
|
346
416
|
)
|
|
347
417
|
|
|
348
|
-
## Residual background correction
|
|
349
418
|
background_corrected_solar_array = next(
|
|
350
419
|
subtract_array_from_arrays(dark_corrected_solar_array, background_array)
|
|
351
420
|
)
|
|
352
421
|
|
|
353
|
-
|
|
354
|
-
self.write(
|
|
355
|
-
data=background_corrected_solar_array,
|
|
356
|
-
tags=[
|
|
357
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
358
|
-
VispTag.task("SC_BG_ONLY"),
|
|
359
|
-
],
|
|
360
|
-
encoder=fits_array_encoder,
|
|
361
|
-
)
|
|
422
|
+
all_exp_time_arrays.append(background_corrected_solar_array)
|
|
362
423
|
|
|
363
|
-
|
|
364
|
-
lamp_array = next(
|
|
365
|
-
self.read(
|
|
366
|
-
tags=[
|
|
367
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
368
|
-
VispTag.task_lamp_gain(),
|
|
369
|
-
],
|
|
370
|
-
decoder=fits_array_decoder,
|
|
371
|
-
)
|
|
372
|
-
)
|
|
373
|
-
lamp_corrected_solar_array = next(
|
|
374
|
-
divide_arrays_by_array(
|
|
375
|
-
arrays=background_corrected_solar_array, array_to_divide_by=lamp_array
|
|
376
|
-
)
|
|
377
|
-
)
|
|
424
|
+
avg_dark_bg_corrected_array = average_numpy_arrays(all_exp_time_arrays)
|
|
378
425
|
|
|
379
|
-
|
|
380
|
-
|
|
426
|
+
# Save the only-dark-corr because this will be used to make the final Solar Gain object
|
|
427
|
+
self.write(
|
|
428
|
+
data=avg_dark_bg_corrected_array,
|
|
429
|
+
tags=[
|
|
430
|
+
VispTag.intermediate_frame(beam=beam),
|
|
431
|
+
VispTag.task("SC_BG_ONLY"),
|
|
432
|
+
],
|
|
433
|
+
encoder=fits_array_encoder,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
## Lamp correction
|
|
437
|
+
lamp_array = next(
|
|
438
|
+
self.read(
|
|
381
439
|
tags=[
|
|
382
|
-
VispTag.intermediate_frame(beam=beam
|
|
383
|
-
VispTag.
|
|
440
|
+
VispTag.intermediate_frame(beam=beam),
|
|
441
|
+
VispTag.task_lamp_gain(),
|
|
384
442
|
],
|
|
385
|
-
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
## Geo correction
|
|
389
|
-
angle_array = next(
|
|
390
|
-
self.read(
|
|
391
|
-
tags=[VispTag.intermediate_frame(beam=beam), VispTag.task_geometric_angle()],
|
|
392
|
-
decoder=fits_array_decoder,
|
|
393
|
-
)
|
|
394
|
-
)
|
|
395
|
-
angle = angle_array[0]
|
|
396
|
-
state_offset = next(
|
|
397
|
-
self.read(
|
|
398
|
-
tags=[
|
|
399
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
400
|
-
VispTag.task_geometric_offset(),
|
|
401
|
-
],
|
|
402
|
-
decoder=fits_array_decoder,
|
|
403
|
-
)
|
|
443
|
+
decoder=fits_array_decoder,
|
|
404
444
|
)
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
VispTag.task_geometric_spectral_shifts(),
|
|
410
|
-
],
|
|
411
|
-
decoder=fits_array_decoder,
|
|
412
|
-
)
|
|
445
|
+
)
|
|
446
|
+
lamp_corrected_solar_array = next(
|
|
447
|
+
divide_arrays_by_array(
|
|
448
|
+
arrays=avg_dark_bg_corrected_array, array_to_divide_by=lamp_array
|
|
413
449
|
)
|
|
450
|
+
)
|
|
414
451
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
],
|
|
425
|
-
encoder=fits_array_encoder,
|
|
426
|
-
)
|
|
452
|
+
self.write(
|
|
453
|
+
data=lamp_corrected_solar_array,
|
|
454
|
+
tags=[
|
|
455
|
+
VispTag.debug(),
|
|
456
|
+
VispTag.beam(beam),
|
|
457
|
+
VispTag.task("SC_LAMP_CORR"),
|
|
458
|
+
],
|
|
459
|
+
encoder=fits_array_encoder,
|
|
460
|
+
)
|
|
427
461
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
462
|
+
## Geo correction
|
|
463
|
+
angle_array = next(
|
|
464
|
+
self.read(
|
|
465
|
+
tags=[VispTag.intermediate_frame(beam=beam), VispTag.task_geometric_angle()],
|
|
466
|
+
decoder=fits_array_decoder,
|
|
431
467
|
)
|
|
432
|
-
|
|
433
|
-
|
|
468
|
+
)
|
|
469
|
+
angle = angle_array[0]
|
|
470
|
+
|
|
471
|
+
# Because we have averaged over all modstates, the per-modestate offsets don't matter. Also, we're going to
|
|
472
|
+
# undo these corrections to make the final gain, so the beam2 -> beam1 offset doesn't matter either.
|
|
473
|
+
state_offset = np.array([0.0, 0.0])
|
|
474
|
+
spec_shift = next(
|
|
475
|
+
self.read(
|
|
434
476
|
tags=[
|
|
435
|
-
VispTag.intermediate_frame(beam=beam
|
|
436
|
-
VispTag.
|
|
477
|
+
VispTag.intermediate_frame(beam=beam),
|
|
478
|
+
VispTag.task_geometric_spectral_shifts(),
|
|
437
479
|
],
|
|
438
|
-
|
|
480
|
+
decoder=fits_array_decoder,
|
|
439
481
|
)
|
|
482
|
+
)
|
|
440
483
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
----------
|
|
454
|
-
beam : int
|
|
455
|
-
The beam number for this array
|
|
484
|
+
geo_corrected_array = next(
|
|
485
|
+
self.corrections_correct_geometry(lamp_corrected_solar_array, state_offset, angle)
|
|
486
|
+
)
|
|
487
|
+
self.write(
|
|
488
|
+
data=geo_corrected_array,
|
|
489
|
+
tags=[
|
|
490
|
+
VispTag.debug(),
|
|
491
|
+
VispTag.beam(beam),
|
|
492
|
+
VispTag.task("SC_GEO_NOSHIFT"),
|
|
493
|
+
],
|
|
494
|
+
encoder=fits_array_encoder,
|
|
495
|
+
)
|
|
456
496
|
|
|
457
|
-
|
|
458
|
-
|
|
497
|
+
# Now finish the spectral shift correction
|
|
498
|
+
spectral_corrected_array = next(
|
|
499
|
+
self.corrections_remove_spec_geometry(geo_corrected_array, spec_shift)
|
|
500
|
+
)
|
|
501
|
+
self.write(
|
|
502
|
+
data=spectral_corrected_array,
|
|
503
|
+
tags=[
|
|
504
|
+
VispTag.intermediate_frame(beam=beam),
|
|
505
|
+
VispTag.task("SC_GEO_ALL"),
|
|
506
|
+
],
|
|
507
|
+
encoder=fits_array_encoder,
|
|
508
|
+
)
|
|
459
509
|
|
|
510
|
+
def get_representative_spectrum(self, beam: int) -> np.ndarray:
|
|
511
|
+
"""
|
|
512
|
+
Compute a representative spectrum that will be used for solar atlas and continuum fitting.
|
|
460
513
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
514
|
+
The spectrum is the spatial median of the lamp-and-spectral-shift corrected solar gain image for this beam.
|
|
515
|
+
Prior to computing the spatial median each spatial pixel is normalized by its continuum level so that variations
|
|
516
|
+
in overall scaling as a function of slit position don't skew the median line shapes. The continuum for each
|
|
517
|
+
spatial pixel is estimated from a percentage of the CDF; this percentage is a pipeline parameter.
|
|
465
518
|
"""
|
|
466
|
-
spectral_avg_window = self.parameters.solar_spectral_avg_window
|
|
467
519
|
normalization_percentile = (
|
|
468
520
|
self.parameters.solar_characteristic_spatial_normalization_percentile
|
|
469
521
|
)
|
|
470
522
|
logger.info(
|
|
471
|
-
f"Computing
|
|
523
|
+
f"Computing representative spectra for {beam = } with {normalization_percentile = }"
|
|
472
524
|
)
|
|
473
|
-
full_spectra = self.
|
|
525
|
+
full_spectra = self.geo_corrected_beam_data(beam=beam)
|
|
474
526
|
|
|
475
527
|
full_spectra = self.corrections_mask_hairlines(full_spectra)
|
|
476
528
|
# Normalize each spatial pixel by its own percentile. This removes large spatial gradients that are not solar
|
|
@@ -479,86 +531,315 @@ class SolarCalibration(
|
|
|
479
531
|
full_spectra, normalization_percentile, axis=0
|
|
480
532
|
)
|
|
481
533
|
|
|
482
|
-
|
|
483
|
-
|
|
534
|
+
representative_spectrum = np.nanmedian(normed_spectra, axis=1)
|
|
535
|
+
|
|
536
|
+
return representative_spectrum
|
|
537
|
+
|
|
538
|
+
def fit_initial_vignette(
|
|
539
|
+
self,
|
|
540
|
+
representative_spectrum: np.ndarray,
|
|
541
|
+
dispersion: Quantity,
|
|
542
|
+
spectral_order: int,
|
|
543
|
+
doppler_velocity: Quantity,
|
|
544
|
+
) -> FitResult:
|
|
545
|
+
"""
|
|
546
|
+
Fit a global continuum to a single, representative spectrum.
|
|
547
|
+
|
|
548
|
+
The representative spectrum's continuum is estimated by fitting the spectrum to a solar atlas. The continuum
|
|
549
|
+
is parameterized with a polynomial as seen in `WavelengthCalibrationParametersWithContinuum` and
|
|
550
|
+
`polynomial_continuum_model`.
|
|
551
|
+
"""
|
|
552
|
+
atlas = Atlas(self.parameters.wavecal_atlas_download_config)
|
|
553
|
+
|
|
554
|
+
logger.info("Initializing wavecal fit parameters")
|
|
555
|
+
init_parameters = self.initialize_starting_fit_parameters(
|
|
556
|
+
representative_spectrum=representative_spectrum,
|
|
557
|
+
dispersion=dispersion,
|
|
558
|
+
spectral_order=spectral_order,
|
|
559
|
+
doppler_velocity=doppler_velocity,
|
|
560
|
+
atlas=atlas,
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
fitter = WavelengthCalibrationFitter(input_parameters=init_parameters, atlas=atlas)
|
|
564
|
+
with self.telemetry_span("Fit atlas and continuum"):
|
|
565
|
+
extra_kwargs = self.parameters.solar_vignette_wavecal_fit_kwargs
|
|
566
|
+
logger.info(f"Calling fitter with extra kwargs: {extra_kwargs}")
|
|
567
|
+
fit_result = fitter(
|
|
568
|
+
input_spectrum=representative_spectrum,
|
|
569
|
+
**extra_kwargs,
|
|
570
|
+
)
|
|
484
571
|
|
|
485
|
-
return
|
|
572
|
+
return fit_result
|
|
486
573
|
|
|
487
|
-
def
|
|
574
|
+
def compute_initial_vignette_estimation(
|
|
575
|
+
self, beam: int, representative_spectrum: np.ndarray, continuum: np.ndarray
|
|
576
|
+
) -> np.ndarray:
|
|
488
577
|
"""
|
|
489
|
-
|
|
578
|
+
Compute the initial, 1D estimate of the spectral vignette signal.
|
|
490
579
|
|
|
491
|
-
|
|
492
|
-
|
|
580
|
+
The continuum is first removed from the representative spectrum to make the first guess at a vignette-corrected
|
|
581
|
+
solar spectrum. Then this solar spectrum is divided from the 2D lamp-and-spectral-shift corrected solar gain data.
|
|
493
582
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
583
|
+
Because the continuum was fit for a single spectrum, the result is a 2D array that contains the *spatial*
|
|
584
|
+
variation in the vignetting signal.
|
|
585
|
+
"""
|
|
586
|
+
first_vignette_array = (
|
|
587
|
+
self.geo_corrected_beam_data(beam=beam) / (representative_spectrum / continuum)[:, None]
|
|
588
|
+
)
|
|
589
|
+
return first_vignette_array
|
|
590
|
+
|
|
591
|
+
def initialize_starting_fit_parameters(
|
|
592
|
+
self,
|
|
593
|
+
representative_spectrum: np.ndarray,
|
|
594
|
+
dispersion: Quantity,
|
|
595
|
+
spectral_order: int,
|
|
596
|
+
doppler_velocity: Quantity,
|
|
597
|
+
atlas: Atlas,
|
|
598
|
+
) -> WavelengthCalibrationParametersWithContinuum:
|
|
599
|
+
"""
|
|
600
|
+
Construct a `WavelengthCalibrationParametersWithContinuum` object containing initial guesses, fit flags, and bounds.
|
|
498
601
|
|
|
499
|
-
|
|
500
|
-
|
|
602
|
+
A rough estimate of the initial CRVAL value is made using `solar_wavelength_calibration.calculate_initial_crval_guess`.
|
|
603
|
+
"""
|
|
604
|
+
num_wave = representative_spectrum.size
|
|
605
|
+
normalized_abscissa = np.linspace(-1, 1, num_wave)
|
|
606
|
+
|
|
607
|
+
logger.info("Computing input wavelength vector")
|
|
608
|
+
input_wavelength_vector = compute_input_wavelength_vector(
|
|
609
|
+
central_wavelength=self.constants.wavelength * u.nm,
|
|
610
|
+
dispersion=dispersion,
|
|
611
|
+
grating_constant=self.constants.grating_constant_inverse_mm,
|
|
612
|
+
order=spectral_order,
|
|
613
|
+
incident_light_angle=self.constants.incident_light_angle_deg,
|
|
614
|
+
num_spec_px=representative_spectrum.size,
|
|
615
|
+
)
|
|
616
|
+
wavelength_range = input_wavelength_vector.max() - input_wavelength_vector.min()
|
|
617
|
+
|
|
618
|
+
logger.info("Computing initial CRVAL guess")
|
|
619
|
+
crval_init = calculate_initial_crval_guess(
|
|
620
|
+
input_wavelength_vector=input_wavelength_vector,
|
|
621
|
+
input_spectrum=representative_spectrum,
|
|
622
|
+
atlas=atlas,
|
|
623
|
+
negative_limit=-wavelength_range / 2,
|
|
624
|
+
positive_limit=wavelength_range / 2,
|
|
625
|
+
num_steps=500,
|
|
626
|
+
normalization_percentile=self.parameters.wavecal_init_crval_guess_normalization_percentile,
|
|
627
|
+
)
|
|
501
628
|
|
|
502
|
-
|
|
503
|
-
The modulator state for this array
|
|
629
|
+
logger.info(f"{crval_init = !s}")
|
|
504
630
|
|
|
631
|
+
fit_flags = FitFlagsModel(
|
|
632
|
+
continuum_level=False, # Because we're fitting the continuum with a function
|
|
633
|
+
incident_light_angle=False,
|
|
634
|
+
straylight_fraction=True,
|
|
635
|
+
resolving_power=True,
|
|
636
|
+
opacity_factor=True,
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
incident_light_angle = self.constants.incident_light_angle_deg
|
|
640
|
+
grating_constant = self.constants.grating_constant_inverse_mm
|
|
641
|
+
resolving_power = self.parameters.wavecal_init_resolving_power
|
|
642
|
+
opacity_factor = self.parameters.wavecal_init_opacity_factor
|
|
643
|
+
straylight_faction = self.parameters.wavecal_init_straylight_fraction
|
|
644
|
+
relative_atlas_scaling = self.estimate_relative_continuum_level(
|
|
645
|
+
crval_init=crval_init,
|
|
646
|
+
wavelength_range=wavelength_range,
|
|
647
|
+
atlas=atlas,
|
|
648
|
+
representative_spectrum=representative_spectrum,
|
|
649
|
+
)
|
|
650
|
+
logger.info(f"0th order coefficient initial guess: {relative_atlas_scaling}")
|
|
651
|
+
|
|
652
|
+
wavelength_search_width = dispersion * self.parameters.solar_vignette_crval_bounds_px
|
|
653
|
+
bounds = BoundsModel(
|
|
654
|
+
crval=LengthBoundRange(
|
|
655
|
+
min=crval_init - wavelength_search_width, max=crval_init + wavelength_search_width
|
|
656
|
+
),
|
|
657
|
+
dispersion=DispersionBoundRange(
|
|
658
|
+
min=dispersion * (1 - self.parameters.solar_vignette_dispersion_bounds_fraction),
|
|
659
|
+
max=dispersion * (1 + self.parameters.solar_vignette_dispersion_bounds_fraction),
|
|
660
|
+
),
|
|
661
|
+
incident_light_angle=AngleBoundRange(
|
|
662
|
+
min=incident_light_angle - 1 * u.deg, max=incident_light_angle + 1 * u.deg
|
|
663
|
+
),
|
|
664
|
+
resolving_power=UnitlessBoundRange(min=1e5, max=5e5),
|
|
665
|
+
opacity_factor=UnitlessBoundRange(min=1.0, max=10),
|
|
666
|
+
straylight_fraction=UnitlessBoundRange(min=0.0, max=0.8),
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
init_params = WavelengthCalibrationParametersWithContinuum(
|
|
670
|
+
crval=crval_init,
|
|
671
|
+
dispersion=dispersion,
|
|
672
|
+
incident_light_angle=incident_light_angle,
|
|
673
|
+
grating_constant=grating_constant,
|
|
674
|
+
doppler_velocity=doppler_velocity,
|
|
675
|
+
order=spectral_order,
|
|
676
|
+
continuum_level=1,
|
|
677
|
+
resolving_power=resolving_power,
|
|
678
|
+
opacity_factor=opacity_factor,
|
|
679
|
+
straylight_fraction=straylight_faction,
|
|
680
|
+
fit_flags=fit_flags,
|
|
681
|
+
bounds=bounds,
|
|
682
|
+
continuum_poly_fit_order=self.parameters.solar_vignette_initial_continuum_poly_fit_order,
|
|
683
|
+
normalized_abscissa=normalized_abscissa,
|
|
684
|
+
zeroth_order_continuum_coefficient=relative_atlas_scaling,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
return init_params
|
|
688
|
+
|
|
689
|
+
def estimate_relative_continuum_level(
|
|
690
|
+
self,
|
|
691
|
+
*,
|
|
692
|
+
crval_init: Quantity,
|
|
693
|
+
wavelength_range: Quantity,
|
|
694
|
+
atlas: Atlas,
|
|
695
|
+
representative_spectrum: np.ndarray,
|
|
696
|
+
) -> float:
|
|
697
|
+
"""
|
|
698
|
+
Estimate the multiplicative scaling between the representative spectrum and atlas solar transmission.
|
|
699
|
+
|
|
700
|
+
This scaling is used to set the initial guess of 0th-order polynomial fit coefficient. We estimate the scaling
|
|
701
|
+
factor by comparing the values of the two spectra at a given percent of the CDF. This percent is taken from
|
|
702
|
+
the `~dkist_processing_visp.models.parameters.VispParameters.wavecal_init_crval_guess_normalization_percentile`
|
|
703
|
+
pipeline parameter.
|
|
704
|
+
"""
|
|
705
|
+
wave_min = crval_init - wavelength_range / 2
|
|
706
|
+
wave_max = crval_init + wavelength_range / 2
|
|
707
|
+
|
|
708
|
+
atlas_idx = np.where(
|
|
709
|
+
(atlas.solar_atlas_wavelength >= wave_min) & (atlas.solar_atlas_wavelength <= wave_max)
|
|
710
|
+
)
|
|
711
|
+
atlas_norm = np.nanpercentile(
|
|
712
|
+
atlas.solar_atlas_transmission[atlas_idx],
|
|
713
|
+
self.parameters.wavecal_init_crval_guess_normalization_percentile,
|
|
714
|
+
)
|
|
715
|
+
spec_norm = np.nanpercentile(
|
|
716
|
+
representative_spectrum,
|
|
717
|
+
self.parameters.wavecal_init_crval_guess_normalization_percentile,
|
|
718
|
+
)
|
|
719
|
+
|
|
720
|
+
return spec_norm / atlas_norm
|
|
721
|
+
|
|
722
|
+
def compute_final_vignette_estimate(self, init_vignette_correction: np.ndarray) -> np.ndarray:
|
|
723
|
+
"""
|
|
724
|
+
Fit the spectral shape of continuum residuals for each spatial pixel.
|
|
725
|
+
|
|
726
|
+
The vignette estimation produced by `compute_initial_vignette_estimation` used a single continuum function for
|
|
727
|
+
the entire slit. This method fits the continuum for all pixels along the slit to build up a fully 2D estimate
|
|
728
|
+
of the vignette signal.
|
|
729
|
+
|
|
730
|
+
Each spatial pixel is fit separately with a polynomial via a RANSAC estimator.
|
|
731
|
+
"""
|
|
732
|
+
model = self.build_RANSAC_model()
|
|
733
|
+
num_spectral, num_spatial = init_vignette_correction.shape
|
|
734
|
+
abscissa = np.arange(num_spectral)[
|
|
735
|
+
:, None
|
|
736
|
+
] # Add extra dimension because sklearn requires it
|
|
737
|
+
|
|
738
|
+
logger.info(f"Fitting spectral vignetting for {num_spatial} spatial pixels")
|
|
739
|
+
final_vignette = np.zeros_like(init_vignette_correction)
|
|
740
|
+
for spatial_px in range(num_spatial):
|
|
741
|
+
finite_idx = np.isfinite(init_vignette_correction[:, spatial_px])
|
|
742
|
+
model.fit(abscissa[finite_idx, :], init_vignette_correction[finite_idx, spatial_px])
|
|
743
|
+
final_vignette[:, spatial_px] = model.predict(abscissa)
|
|
744
|
+
|
|
745
|
+
return final_vignette
|
|
746
|
+
|
|
747
|
+
def build_RANSAC_model(self) -> Pipeline:
|
|
748
|
+
"""
|
|
749
|
+
Build a scikit-learn pipeline from a set of estimators.
|
|
750
|
+
|
|
751
|
+
Namely, construct a `~sklearn.pipeline.Pipeline` built from
|
|
752
|
+
|
|
753
|
+
`~sklearn.preprocessing.PolynomialFeatures` -> `~sklearn.preprocessing.RobustScaler` -> `~sklearn.linear_model.RANSACRegressor`
|
|
754
|
+
"""
|
|
755
|
+
fit_order = self.parameters.solar_vignette_spectral_poly_fit_order
|
|
756
|
+
min_samples = self.parameters.solar_vignette_min_samples
|
|
757
|
+
logger.info(f"Building RANSAC model with {fit_order = } and {min_samples = }")
|
|
758
|
+
|
|
759
|
+
# PolynomialFeatures casts the pipeline as a polynomial fit
|
|
760
|
+
# see https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.PolynomialFeatures.html#sklearn.preprocessing.PolynomialFeatures
|
|
761
|
+
poly_feature = PolynomialFeatures(degree=fit_order)
|
|
762
|
+
|
|
763
|
+
# RobustScaler is a scale factor that is robust to outliers
|
|
764
|
+
# see https://scikit-learn.org/stable/modules/generated/sklearn.preprocessing.RobustScaler.html#sklearn.preprocessing.RobustScaler
|
|
765
|
+
scaler = RobustScaler()
|
|
766
|
+
|
|
767
|
+
# The RANSAC regressor iteratively sub-samples the input data and constructs a model with this sub-sample.
|
|
768
|
+
# The method used allows it to be robust to outliers.
|
|
769
|
+
# see https://scikit-learn.org/stable/modules/linear_model.html#ransac-regression
|
|
770
|
+
RANSAC = RANSACRegressor(min_samples=min_samples)
|
|
771
|
+
|
|
772
|
+
return make_pipeline(poly_feature, scaler, RANSAC)
|
|
773
|
+
|
|
774
|
+
def compute_characteristic_spectra(self, vignette_corrected_data: np.ndarray) -> np.ndarray:
|
|
775
|
+
"""
|
|
776
|
+
Compute the 2D characteristic spectra via a median smooth in the spatial dimension.
|
|
777
|
+
|
|
778
|
+
A 2D characteristic spectra is needed because the line shape varys along the slit to the degree that a
|
|
779
|
+
single, 1D characteristic spectrum will not fully remove the solar lines for all positions in the final gain.
|
|
780
|
+
|
|
781
|
+
In this step we also normalize each spatial position by its continuum value. This removes low-order gradients in
|
|
782
|
+
the spatial direction that are known to be caused by imperfect illumination of the Lamp gains (which were used
|
|
783
|
+
to correct the data that will become the characteristic spectra).
|
|
784
|
+
|
|
785
|
+
Parameters
|
|
786
|
+
----------
|
|
787
|
+
beam
|
|
788
|
+
The beam number for this array
|
|
505
789
|
|
|
506
790
|
Returns
|
|
507
791
|
-------
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
"""
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
logger.info(
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
792
|
+
char_spec
|
|
793
|
+
2D characteristic spectra
|
|
794
|
+
"""
|
|
795
|
+
spatial_median_window = self.parameters.solar_spatial_median_filter_width_px
|
|
796
|
+
normalization_percentile = (
|
|
797
|
+
self.parameters.solar_characteristic_spatial_normalization_percentile
|
|
798
|
+
)
|
|
799
|
+
logger.info(
|
|
800
|
+
f"Computing characteristic spectra for {spatial_median_window = } and {normalization_percentile = }"
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
masked_spectra = self.corrections_mask_hairlines(vignette_corrected_data)
|
|
804
|
+
# Normalize each spatial pixel by its own percentile. This removes large spatial gradients that are not solar
|
|
805
|
+
# signal.
|
|
806
|
+
normed_spectra = masked_spectra / np.nanpercentile(
|
|
807
|
+
masked_spectra, normalization_percentile, axis=0
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# size = (1, window) means don't smooth in the spectral dimension
|
|
811
|
+
char_spec = spnd.median_filter(normed_spectra, size=(1, spatial_median_window))
|
|
527
812
|
|
|
528
|
-
|
|
529
|
-
logger.info(f"Refining shifts for {beam=} and {modstate=}")
|
|
530
|
-
for i in range(num_spec):
|
|
531
|
-
ref_spec = target_spectra[:, i] / np.nanmedian(target_spectra[:, i])
|
|
532
|
-
spec = char_spec[:, i] / np.nanmedian(char_spec[:, i])
|
|
533
|
-
shift = SolarCalibration.refine_shift(spec, ref_spec, zones=zones, x_init=0.0)
|
|
534
|
-
reshift_char_spec[:, i] = spnd.shift(char_spec[:, i], shift, mode="reflect")
|
|
813
|
+
normed_char_spec = char_spec / np.nanpercentile(char_spec, normalization_percentile, axis=0)
|
|
535
814
|
|
|
536
|
-
return
|
|
815
|
+
return normed_char_spec
|
|
537
816
|
|
|
538
817
|
def distort_characteristic_spectra(
|
|
539
|
-
self,
|
|
818
|
+
self,
|
|
819
|
+
char_spec: np.ndarray,
|
|
820
|
+
beam: int,
|
|
540
821
|
) -> np.ndarray:
|
|
541
822
|
"""
|
|
542
823
|
Re-apply angle and state offset distortions to the characteristic spectra.
|
|
543
824
|
|
|
544
825
|
Parameters
|
|
545
826
|
----------
|
|
546
|
-
char_spec
|
|
827
|
+
char_spec
|
|
547
828
|
Computed characteristic spectra
|
|
548
829
|
|
|
549
|
-
beam
|
|
830
|
+
beam
|
|
550
831
|
The beam number for this array
|
|
551
832
|
|
|
552
|
-
modstate
|
|
833
|
+
modstate
|
|
553
834
|
The modulator state for this array
|
|
554
835
|
|
|
555
836
|
|
|
556
837
|
Returns
|
|
557
838
|
-------
|
|
558
|
-
|
|
839
|
+
distorted characteristic array
|
|
559
840
|
Characteristic spectra array with angle and offset distortions re-applied
|
|
560
841
|
"""
|
|
561
|
-
logger.info(f"Re-distorting characteristic spectra for {beam=}
|
|
842
|
+
logger.info(f"Re-distorting characteristic spectra for {beam=}")
|
|
562
843
|
angle_array = next(
|
|
563
844
|
self.read(
|
|
564
845
|
tags=[VispTag.intermediate_frame(beam=beam), VispTag.task_geometric_angle()],
|
|
@@ -566,15 +847,9 @@ class SolarCalibration(
|
|
|
566
847
|
)
|
|
567
848
|
)
|
|
568
849
|
angle = angle_array[0]
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
573
|
-
VispTag.task_geometric_offset(),
|
|
574
|
-
],
|
|
575
|
-
decoder=fits_array_decoder,
|
|
576
|
-
)
|
|
577
|
-
)
|
|
850
|
+
|
|
851
|
+
# See comment in `do_initial_corrections`; we don't care about the state_offset when making the solar gain
|
|
852
|
+
state_offset = np.array([0.0, 0.0])
|
|
578
853
|
|
|
579
854
|
distorted_spec = next(
|
|
580
855
|
self.corrections_correct_geometry(char_spec, -1 * state_offset, -1 * angle)
|
|
@@ -582,164 +857,329 @@ class SolarCalibration(
|
|
|
582
857
|
|
|
583
858
|
return distorted_spec
|
|
584
859
|
|
|
585
|
-
def equalize_modstates(self, array_dict: dict[int, np.ndarray]) -> dict[int, np.ndarray]:
|
|
586
|
-
"""Adjust the flux of all modstates for a single beam so they all have the same median.
|
|
587
|
-
|
|
588
|
-
That median is the global median over all modstates.
|
|
589
|
-
"""
|
|
590
|
-
global_median_array = np.nanmedian(list(array_dict.values()), axis=0)
|
|
591
|
-
output_dict = dict()
|
|
592
|
-
|
|
593
|
-
for modstate, array in array_dict.items():
|
|
594
|
-
correction = np.nanmedian(global_median_array / array)
|
|
595
|
-
logger.info(f"Equalization correction for {modstate = } is {correction:.6f}")
|
|
596
|
-
final_gain = array * correction
|
|
597
|
-
output_dict[modstate] = final_gain
|
|
598
|
-
|
|
599
|
-
return output_dict
|
|
600
|
-
|
|
601
860
|
def remove_solar_signal(
|
|
602
|
-
self,
|
|
861
|
+
self,
|
|
862
|
+
char_solar_spectra: np.ndarray,
|
|
863
|
+
beam: int,
|
|
603
864
|
) -> np.ndarray:
|
|
604
865
|
"""
|
|
605
866
|
Remove the distorted characteristic solar spectra from the original spectra.
|
|
606
867
|
|
|
607
868
|
Parameters
|
|
608
869
|
----------
|
|
609
|
-
char_solar_spectra
|
|
870
|
+
char_solar_spectra
|
|
610
871
|
Characteristic solar spectra
|
|
611
872
|
|
|
612
|
-
beam
|
|
873
|
+
beam
|
|
613
874
|
The beam number for this array
|
|
614
875
|
|
|
615
|
-
modstate : int
|
|
616
|
-
The modulator state for this array
|
|
617
|
-
|
|
618
|
-
|
|
619
876
|
Returns
|
|
620
877
|
-------
|
|
621
|
-
|
|
878
|
+
final gain
|
|
622
879
|
Original spectral array with characteristic solar spectra removed
|
|
623
|
-
|
|
624
880
|
"""
|
|
625
|
-
logger.info(f"Removing characteristic solar spectra from {beam=}
|
|
626
|
-
input_gain = self.
|
|
881
|
+
logger.info(f"Removing characteristic solar spectra from {beam=}")
|
|
882
|
+
input_gain = self.bg_corrected_beam_data(beam=beam)
|
|
627
883
|
|
|
628
884
|
final_gain = input_gain / char_solar_spectra
|
|
629
885
|
|
|
630
886
|
return final_gain
|
|
631
887
|
|
|
632
888
|
def write_solar_gain_calibration(
|
|
633
|
-
self,
|
|
889
|
+
self,
|
|
890
|
+
gain_array: np.ndarray,
|
|
891
|
+
beam: int,
|
|
634
892
|
) -> None:
|
|
635
893
|
"""
|
|
636
|
-
Write a solar gain array for a single beam
|
|
894
|
+
Write a solar gain array for a single beam.
|
|
637
895
|
|
|
638
896
|
Parameters
|
|
639
897
|
----------
|
|
640
|
-
gain_array
|
|
898
|
+
gain_array
|
|
641
899
|
Solar gain array
|
|
642
900
|
|
|
643
|
-
beam
|
|
901
|
+
beam
|
|
644
902
|
The beam number for this array
|
|
645
|
-
|
|
646
|
-
modstate : int
|
|
647
|
-
The modulator state for this array
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
Returns
|
|
651
|
-
-------
|
|
652
|
-
None
|
|
653
903
|
"""
|
|
654
|
-
logger.info(f"Writing final SolarGain for {beam=
|
|
904
|
+
logger.info(f"Writing final SolarGain for {beam = }")
|
|
655
905
|
self.write(
|
|
656
906
|
data=gain_array,
|
|
657
907
|
tags=[
|
|
658
|
-
VispTag.intermediate_frame(beam=beam
|
|
908
|
+
VispTag.intermediate_frame(beam=beam),
|
|
659
909
|
VispTag.task_solar_gain(),
|
|
660
910
|
],
|
|
661
911
|
encoder=fits_array_encoder,
|
|
662
912
|
)
|
|
663
913
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
914
|
+
def record_vignette_quality_metrics(
|
|
915
|
+
self,
|
|
916
|
+
beam: int,
|
|
917
|
+
representative_spectrum: np.ndarray,
|
|
918
|
+
fit_result: FitResult,
|
|
919
|
+
vignette_corrected_gain: np.ndarray,
|
|
920
|
+
) -> None:
|
|
668
921
|
"""
|
|
669
|
-
|
|
922
|
+
Save vignette fit results to disk for later quality metric building.
|
|
670
923
|
|
|
671
|
-
|
|
672
|
-
|
|
924
|
+
We save the atlas fit of the initial vignette estimation and the global solar spectrum derived from the
|
|
925
|
+
vignette-corrected 2D gain images.
|
|
926
|
+
"""
|
|
927
|
+
logger.info(f"Recording initial vignette quality data for {beam = }")
|
|
928
|
+
first_vignette_quality_outputs = {
|
|
929
|
+
"output_wave_vec": fit_result.best_fit_wavelength_vector,
|
|
930
|
+
"input_spectrum": representative_spectrum,
|
|
931
|
+
"best_fit_atlas": fit_result.best_fit_atlas,
|
|
932
|
+
"best_fit_continuum": fit_result.best_fit_continuum,
|
|
933
|
+
"residuals": fit_result.minimizer_result.residual,
|
|
934
|
+
}
|
|
935
|
+
self.write(
|
|
936
|
+
data=first_vignette_quality_outputs,
|
|
937
|
+
tags=[VispTag.quality(VispMetricCode.solar_first_vignette), VispTag.beam(beam)],
|
|
938
|
+
encoder=asdf_encoder,
|
|
939
|
+
)
|
|
673
940
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
941
|
+
logger.info(f"Recording final vignette-correced gain quality data for {beam = }")
|
|
942
|
+
global_median = np.nanmedian(vignette_corrected_gain, axis=1)
|
|
943
|
+
low_deviation = np.nanpercentile(vignette_corrected_gain, 5, axis=1)
|
|
944
|
+
high_deviation = np.nanpercentile(vignette_corrected_gain, 95, axis=1)
|
|
945
|
+
final_correction_quality_outputs = {
|
|
946
|
+
"output_wave_vec": fit_result.best_fit_wavelength_vector,
|
|
947
|
+
"median_spec": global_median,
|
|
948
|
+
"low_deviation": low_deviation,
|
|
949
|
+
"high_deviation": high_deviation,
|
|
950
|
+
}
|
|
951
|
+
self.write(
|
|
952
|
+
data=final_correction_quality_outputs,
|
|
953
|
+
tags=[VispTag.quality(VispMetricCode.solar_final_vignette), VispTag.beam(beam)],
|
|
954
|
+
encoder=asdf_encoder,
|
|
955
|
+
)
|
|
678
956
|
|
|
679
|
-
|
|
680
|
-
|
|
957
|
+
def _log_wavecal_parameters(
|
|
958
|
+
self, dispersion: Quantity, order: int, doppler_velocity: Quantity
|
|
959
|
+
) -> None:
|
|
960
|
+
"""Log initial guess and instrument-derived wavecal parameters."""
|
|
961
|
+
logger.info(f"central wavelength = {self.constants.wavelength * u.nm !s}")
|
|
962
|
+
logger.info(f"{dispersion = !s}")
|
|
963
|
+
logger.info(f"{order = }")
|
|
964
|
+
logger.info(f"grating constant = {self.constants.grating_constant_inverse_mm !s}")
|
|
965
|
+
logger.info(f"incident light angle = {self.constants.incident_light_angle_deg !s}")
|
|
966
|
+
logger.info(f"reflected light angle = {self.constants.reflected_light_angle_deg !s}")
|
|
967
|
+
logger.info(f"{doppler_velocity = !s}")
|
|
968
|
+
logger.info(f"pixel pitch = {self.parameters.wavecal_pixel_pitch_micron_per_pix !s}")
|
|
969
|
+
logger.info(f"solar ip start time = {self.constants.solar_gain_ip_start_time}")
|
|
970
|
+
|
|
971
|
+
|
|
972
|
+
def polynomial_continuum_model(
|
|
973
|
+
wavelength_vector: np.ndarray, fit_parameters: Parameters, fit_order: int, abscissa: np.ndarray
|
|
974
|
+
) -> np.ndarray:
|
|
975
|
+
"""
|
|
976
|
+
Parameterize the continuum as a polynomial.
|
|
681
977
|
|
|
682
|
-
|
|
683
|
-
|
|
978
|
+
Polynomial coefficients are taken from the input ``fit_parameters`` object. The ``wavelength_vector`` argument is not
|
|
979
|
+
used by required to conform to the signature expected by `WavelengthCalibrationFitter`.
|
|
684
980
|
|
|
685
|
-
|
|
686
|
-
|
|
981
|
+
Parameters
|
|
982
|
+
----------
|
|
983
|
+
wavelength_vector
|
|
984
|
+
Unused, but required by `WavelengthCalibrationFitter`
|
|
687
985
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
float
|
|
691
|
-
The shift value
|
|
692
|
-
"""
|
|
693
|
-
shifts = np.zeros(len(zones))
|
|
694
|
-
for i, z in enumerate(zones):
|
|
695
|
-
if z[1] + int(x_init) >= spec.size:
|
|
696
|
-
logger.info(f"Ignoring zone {z} with init {x_init} because it's out of range")
|
|
697
|
-
continue
|
|
698
|
-
idx = np.arange(z[0], z[1]) + int(x_init)
|
|
699
|
-
shift = spo.minimize(
|
|
700
|
-
SolarCalibration.shift_func,
|
|
701
|
-
np.array([x_init]),
|
|
702
|
-
args=(target_spec, spec, idx),
|
|
703
|
-
method="nelder-mead",
|
|
704
|
-
).x[0]
|
|
705
|
-
shifts[i] = shift
|
|
706
|
-
|
|
707
|
-
return np.nanmedian(shifts)
|
|
708
|
-
|
|
709
|
-
@staticmethod
|
|
710
|
-
def shift_func(
|
|
711
|
-
par: list[float], ref_spec: np.ndarray, spec: np.ndarray, idx: np.ndarray
|
|
712
|
-
) -> float:
|
|
713
|
-
"""
|
|
714
|
-
Non-chisq based goodness of fit calculator for computing spectral shifts.
|
|
986
|
+
fit_parameters
|
|
987
|
+
Object containing the current fit parameters
|
|
715
988
|
|
|
716
|
-
|
|
989
|
+
fit_order
|
|
990
|
+
Polynomial order. This needs to match the number of polynomial coefficients in the ``fit_parameters`` argument.
|
|
991
|
+
Specifically, this method expects parameters called `[f"poly_coeff_{i:02n}" for i in range(fit_order + 1)]`.
|
|
717
992
|
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
993
|
+
abscissa
|
|
994
|
+
Array of values used to compute the continuum. For the fit to be independent of wavelength these values should
|
|
995
|
+
be in the range [-1, 1].
|
|
996
|
+
"""
|
|
997
|
+
coeffs = [fit_parameters[f"poly_coeff_{i:02n}"].value for i in range(fit_order + 1)]
|
|
998
|
+
return np.polyval(coeffs, abscissa)
|
|
722
999
|
|
|
723
|
-
ref_spec : np.ndarray
|
|
724
|
-
Reference spectra
|
|
725
1000
|
|
|
726
|
-
|
|
727
|
-
|
|
1001
|
+
##################################################################################
|
|
1002
|
+
# TODO: The following definitions should go in the wavecal module once it exists #
|
|
1003
|
+
##################################################################################
|
|
728
1004
|
|
|
729
|
-
idx : np.ndarray
|
|
730
|
-
Range of wavelength pixels that will be compared in fit
|
|
731
1005
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1006
|
+
def compute_order(
|
|
1007
|
+
central_wavelength: Quantity,
|
|
1008
|
+
incident_light_angle: Quantity,
|
|
1009
|
+
reflected_light_angle: Quantity,
|
|
1010
|
+
grating_constant: Quantity,
|
|
1011
|
+
) -> int:
|
|
1012
|
+
r"""
|
|
1013
|
+
Compute the spectral order from the spectrograph setup.
|
|
1014
|
+
|
|
1015
|
+
From the grating equation, the spectral order, :math:`m`:, is
|
|
1016
|
+
|
|
1017
|
+
.. math::
|
|
1018
|
+
m = \frac{\sin\alpha + \sin\beta}{G \lambda}
|
|
1019
|
+
|
|
1020
|
+
where :math:`\alpha` and :math:`\beta` are the incident and reflected light angles, respectively, :math:`G` is the
|
|
1021
|
+
grating constant (lines per mm), and :math:`\lambda` is the central wavelength. All of these values come from the
|
|
1022
|
+
input headers.
|
|
1023
|
+
|
|
1024
|
+
Parameters
|
|
1025
|
+
----------
|
|
1026
|
+
central_wavelength
|
|
1027
|
+
Wavelength of the center of the spectral window.
|
|
1028
|
+
|
|
1029
|
+
incident_light_angle
|
|
1030
|
+
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
1031
|
+
|
|
1032
|
+
reflected_light_angle
|
|
1033
|
+
Angle of light reflected from spectrograph grating. Often called :math:`\beta`.
|
|
1034
|
+
|
|
1035
|
+
grating_constant
|
|
1036
|
+
Grating constant of the spectrograph grating [lines per mm]
|
|
1037
|
+
|
|
1038
|
+
Returns
|
|
1039
|
+
-------
|
|
1040
|
+
spectral_order
|
|
1041
|
+
The order of the given spectrograph configuration
|
|
1042
|
+
"""
|
|
1043
|
+
return int(
|
|
1044
|
+
(np.sin(incident_light_angle) + np.sin(reflected_light_angle))
|
|
1045
|
+
/ (grating_constant * central_wavelength)
|
|
1046
|
+
)
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
def compute_initial_dispersion(
|
|
1050
|
+
central_wavelength: Quantity,
|
|
1051
|
+
incident_light_angle: Quantity,
|
|
1052
|
+
reflected_light_angle: Quantity,
|
|
1053
|
+
lens_parameters: list[Quantity],
|
|
1054
|
+
pixel_pitch: Quantity,
|
|
1055
|
+
) -> Quantity:
|
|
1056
|
+
r"""
|
|
1057
|
+
Compute the dispersion (:math:`d\,\lambda/d\, px`) given the spectrograph setup.
|
|
1058
|
+
|
|
1059
|
+
The dispersion is given via
|
|
1060
|
+
|
|
1061
|
+
.. math::
|
|
1062
|
+
d\,\lambda / d\, px = \frac{p \lambda_0 \cos\beta}{f (\sin\alpha + \sin\beta)}
|
|
1063
|
+
|
|
1064
|
+
where :math:`p` is the pixel pitch (microns per pix), :math:`\lambda_0` is the central wavelength, :math:`f` is the
|
|
1065
|
+
camera focal length, and :math:`\alpha` and :math:`\beta` are the incident and reflected light angles, respectively.
|
|
1066
|
+
:math:`\lambda_0`, :math:`\alpha`, and :math:`\beta` are taken from input headers, while :math:`f` and :math:`p` are
|
|
1067
|
+
pipeline parameters.
|
|
1068
|
+
|
|
1069
|
+
Parameters
|
|
1070
|
+
----------
|
|
1071
|
+
central_wavelength
|
|
1072
|
+
Wavelength of the center of the spectral window.
|
|
1073
|
+
|
|
1074
|
+
incident_light_angle
|
|
1075
|
+
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
1076
|
+
|
|
1077
|
+
reflected_light_angle
|
|
1078
|
+
Angle of light reflected from spectrograph grating. Often called :math:`\beta`.
|
|
1079
|
+
|
|
1080
|
+
lens_parameters
|
|
1081
|
+
Parameterization of lense focal length as zero, first, and second orders of wavelength. If the total focal
|
|
1082
|
+
length of the lens is :math:`f = a_0 + a_1\lambda + a_2\lambda^2` then this list is :math:`[a_0, a_1, a_2]`.
|
|
1083
|
+
|
|
1084
|
+
pixel_pitch
|
|
1085
|
+
The physical size of a single pixel
|
|
1086
|
+
|
|
1087
|
+
Returns
|
|
1088
|
+
-------
|
|
1089
|
+
dispersion
|
|
1090
|
+
The computed dispersion in units of nm / px
|
|
1091
|
+
"""
|
|
1092
|
+
camera_focal_length = lens_parameters[0] + central_wavelength * (
|
|
1093
|
+
lens_parameters[1] + central_wavelength * lens_parameters[2]
|
|
1094
|
+
)
|
|
1095
|
+
logger.info(f"{camera_focal_length = !s}")
|
|
1096
|
+
|
|
1097
|
+
linear_dispersion = (
|
|
1098
|
+
camera_focal_length
|
|
1099
|
+
* (np.sin(incident_light_angle) + np.sin(reflected_light_angle))
|
|
1100
|
+
/ (np.cos(reflected_light_angle) * central_wavelength)
|
|
1101
|
+
)
|
|
1102
|
+
|
|
1103
|
+
dispersion = pixel_pitch / linear_dispersion
|
|
1104
|
+
|
|
1105
|
+
return dispersion.to(u.nm / u.pix)
|
|
1106
|
+
|
|
1107
|
+
|
|
1108
|
+
def compute_doppler_velocity(time_of_observation: str) -> Quantity:
|
|
1109
|
+
"""Find the speed at which DKIST is moving relative to the Sun's center.
|
|
1110
|
+
|
|
1111
|
+
Positive values refer to when DKIST is moving away from the sun.
|
|
1112
|
+
|
|
1113
|
+
Parameters
|
|
1114
|
+
----------
|
|
1115
|
+
time_of_observation
|
|
1116
|
+
Time at which to compute the relative Dopper velocity. Any string that can be parsed by `astropy.time.Time` is
|
|
1117
|
+
acceptable.
|
|
1118
|
+
|
|
1119
|
+
Returns
|
|
1120
|
+
-------
|
|
1121
|
+
doppler_velocity
|
|
1122
|
+
The relative velocity between observer and the Sun with units of km / s
|
|
1123
|
+
"""
|
|
1124
|
+
coord = location_of_dkist.get_gcrs(obstime=Time(time_of_observation))
|
|
1125
|
+
heliocentric_coord = coord.transform_to(HeliocentricInertial(obstime=Time(time_of_observation)))
|
|
1126
|
+
obs_vr_kms = heliocentric_coord.d_distance
|
|
1127
|
+
return obs_vr_kms
|
|
1128
|
+
|
|
1129
|
+
|
|
1130
|
+
def compute_input_wavelength_vector(
|
|
1131
|
+
*,
|
|
1132
|
+
central_wavelength: Quantity,
|
|
1133
|
+
dispersion: Quantity,
|
|
1134
|
+
grating_constant: Quantity,
|
|
1135
|
+
order: int,
|
|
1136
|
+
incident_light_angle: Quantity,
|
|
1137
|
+
num_spec_px: int,
|
|
1138
|
+
) -> u.Quantity:
|
|
1139
|
+
r"""
|
|
1140
|
+
Compute a wavelength vector based on information about the spectrograph setup.
|
|
1141
|
+
|
|
1142
|
+
The parameterization of the grating equation is via `astropy.wcs.WCS`, which follows section 5 of
|
|
1143
|
+
`Greisen et al (2006) <https://ui.adsabs.harvard.edu/abs/2006A%26A...446..747G/abstract>`_.
|
|
1144
|
+
|
|
1145
|
+
Parameters
|
|
1146
|
+
----------
|
|
1147
|
+
central_wavelength
|
|
1148
|
+
Wavelength at the center of the spectral window. This function forces the value of the output vector to be
|
|
1149
|
+
``central_wavelength`` at index ``num_spec // 2 + 1``.
|
|
1150
|
+
|
|
1151
|
+
dispersion
|
|
1152
|
+
Spectrograph dispersion [nm / px]
|
|
1153
|
+
|
|
1154
|
+
grating_constant
|
|
1155
|
+
Grating constant of the spectrograph grating [lines per mm]
|
|
1156
|
+
|
|
1157
|
+
order
|
|
1158
|
+
Spectrograph order
|
|
1159
|
+
|
|
1160
|
+
incident_light_angle
|
|
1161
|
+
Angle of light incident to the spectrograph grating. Often called :math:`\alpha`.
|
|
1162
|
+
|
|
1163
|
+
num_spec_px
|
|
1164
|
+
The length of the output wavelength vector. Defines size and physical limits of the output.
|
|
1165
|
+
|
|
1166
|
+
Returns
|
|
1167
|
+
-------
|
|
1168
|
+
wave_vec
|
|
1169
|
+
1D array of length ``num_spec`` containing the wavelength values described by the input WCS parameterization.
|
|
1170
|
+
The units of this array will be nanometers.
|
|
1171
|
+
"""
|
|
1172
|
+
wavelength_parameters = WavelengthParameters(
|
|
1173
|
+
crpix=num_spec_px // 2 + 1,
|
|
1174
|
+
crval=central_wavelength.to_value(u.nm),
|
|
1175
|
+
dispersion=dispersion.to_value(u.nm / u.pix),
|
|
1176
|
+
grating_constant=grating_constant.to_value(1 / u.mm),
|
|
1177
|
+
order=order,
|
|
1178
|
+
incident_light_angle=incident_light_angle.to_value(u.deg),
|
|
1179
|
+
cunit="nm",
|
|
1180
|
+
)
|
|
1181
|
+
header = wavelength_parameters.to_header(axis_num=1)
|
|
1182
|
+
wcs = WCS(header)
|
|
1183
|
+
input_wavelength_vector = wcs.spectral.pixel_to_world(np.arange(num_spec_px)).to(u.nm)
|
|
1184
|
+
|
|
1185
|
+
return input_wavelength_vector
|