dkist-processing-visp 4.0.0__py3-none-any.whl → 5.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dkist_processing_visp/models/constants.py +50 -9
- dkist_processing_visp/models/fits_access.py +5 -1
- dkist_processing_visp/models/metric_code.py +10 -0
- dkist_processing_visp/models/parameters.py +128 -19
- dkist_processing_visp/parsers/spectrograph_configuration.py +75 -0
- dkist_processing_visp/parsers/visp_l0_fits_access.py +6 -0
- dkist_processing_visp/tasks/geometric.py +115 -7
- dkist_processing_visp/tasks/l1_output_data.py +202 -0
- dkist_processing_visp/tasks/lamp.py +50 -91
- dkist_processing_visp/tasks/parse.py +19 -0
- dkist_processing_visp/tasks/science.py +14 -14
- dkist_processing_visp/tasks/solar.py +894 -451
- dkist_processing_visp/tasks/visp_base.py +1 -0
- dkist_processing_visp/tests/conftest.py +98 -35
- dkist_processing_visp/tests/header_models.py +71 -20
- dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +25 -1
- dkist_processing_visp/tests/test_assemble_quality.py +89 -4
- dkist_processing_visp/tests/test_geometric.py +40 -0
- dkist_processing_visp/tests/test_instrument_polarization.py +2 -1
- dkist_processing_visp/tests/test_lamp.py +17 -22
- dkist_processing_visp/tests/test_parameters.py +120 -18
- dkist_processing_visp/tests/test_parse.py +73 -1
- dkist_processing_visp/tests/test_science.py +5 -6
- dkist_processing_visp/tests/test_solar.py +319 -102
- dkist_processing_visp/tests/test_visp_constants.py +35 -6
- {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/METADATA +40 -37
- {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/RECORD +31 -30
- docs/conf.py +4 -1
- docs/gain_correction.rst +50 -42
- dkist_processing_visp/tasks/mixin/line_zones.py +0 -116
- {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/WHEEL +0 -0
- {dkist_processing_visp-4.0.0.dist-info → dkist_processing_visp-5.0.0.dist-info}/top_level.txt +0 -0
|
@@ -2,14 +2,28 @@ import json
|
|
|
2
2
|
from functools import partial
|
|
3
3
|
from typing import Callable
|
|
4
4
|
|
|
5
|
+
import asdf
|
|
6
|
+
import astropy.units as u
|
|
5
7
|
import numpy as np
|
|
6
8
|
import pytest
|
|
9
|
+
from asdf.tags.core import NDArrayType
|
|
10
|
+
from astropy import constants
|
|
11
|
+
from astropy.coordinates import EarthLocation
|
|
12
|
+
from astropy.io import fits
|
|
13
|
+
from astropy.stats import gaussian_fwhm_to_sigma
|
|
14
|
+
from astropy.time import Time
|
|
7
15
|
from dkist_processing_common._util.scratch import WorkflowFileSystem
|
|
8
16
|
from dkist_processing_common.codecs.fits import fits_array_decoder
|
|
17
|
+
from dkist_processing_common.codecs.fits import fits_array_encoder
|
|
9
18
|
from dkist_processing_common.models.tags import Tag
|
|
19
|
+
from scipy.ndimage import gaussian_filter1d
|
|
20
|
+
from solar_wavelength_calibration import Atlas
|
|
21
|
+
from sunpy.coordinates import HeliocentricInertial
|
|
10
22
|
|
|
11
23
|
from dkist_processing_visp.models.tags import VispTag
|
|
12
24
|
from dkist_processing_visp.tasks.solar import SolarCalibration
|
|
25
|
+
from dkist_processing_visp.tasks.solar import WavelengthCalibrationParametersWithContinuum
|
|
26
|
+
from dkist_processing_visp.tasks.solar import compute_initial_dispersion
|
|
13
27
|
from dkist_processing_visp.tests.conftest import VispConstantsDb
|
|
14
28
|
from dkist_processing_visp.tests.conftest import VispInputDatasetParameterValues
|
|
15
29
|
from dkist_processing_visp.tests.conftest import tag_on_modstate
|
|
@@ -17,36 +31,188 @@ from dkist_processing_visp.tests.conftest import write_frames_to_task
|
|
|
17
31
|
from dkist_processing_visp.tests.conftest import write_intermediate_background_to_task
|
|
18
32
|
from dkist_processing_visp.tests.conftest import write_intermediate_darks_to_task
|
|
19
33
|
from dkist_processing_visp.tests.conftest import write_intermediate_geometric_to_task
|
|
20
|
-
from dkist_processing_visp.tests.conftest import write_intermediate_lamp_to_task
|
|
21
34
|
from dkist_processing_visp.tests.header_models import VispHeadersInputSolarGainFrames
|
|
22
35
|
|
|
23
36
|
|
|
24
|
-
def lamp_signal_func(beam: int
|
|
25
|
-
return 10 * beam
|
|
37
|
+
def lamp_signal_func(beam: int):
|
|
38
|
+
return 10 * beam
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.fixture(scope="session")
|
|
42
|
+
def num_wave_pix() -> int:
|
|
43
|
+
return 1000
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture(scope="session")
|
|
47
|
+
def telluric_opacity_factor() -> float:
|
|
48
|
+
return 5.0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture(scope="session")
|
|
52
|
+
def solar_ip_start_time() -> str:
|
|
53
|
+
return "1988-07-02T10:00:00"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@pytest.fixture(scope="session")
|
|
57
|
+
def doppler_velocity(solar_ip_start_time) -> u.Quantity:
|
|
58
|
+
_dkist_site_info = {
|
|
59
|
+
"aliases": ["DKIST", "ATST"],
|
|
60
|
+
"name": "Daniel K. Inouye Solar Telescope",
|
|
61
|
+
"elevation": 3067,
|
|
62
|
+
"elevation_unit": "meter",
|
|
63
|
+
"latitude": 20.7067,
|
|
64
|
+
"latitude_unit": "degree",
|
|
65
|
+
"longitude": 203.7436,
|
|
66
|
+
"longitude_unit": "degree",
|
|
67
|
+
"timezone": "US/Hawaii",
|
|
68
|
+
"source": "DKIST website: https://www.nso.edu/telescopes/dki-solar-telescope/",
|
|
69
|
+
}
|
|
70
|
+
location_of_dkist = EarthLocation.from_geodetic(
|
|
71
|
+
_dkist_site_info["longitude"] * u.Unit(_dkist_site_info["longitude_unit"]),
|
|
72
|
+
_dkist_site_info["latitude"] * u.Unit(_dkist_site_info["latitude_unit"]),
|
|
73
|
+
_dkist_site_info["elevation"] * u.Unit(_dkist_site_info["elevation_unit"]),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
coord = location_of_dkist.get_gcrs(obstime=Time(solar_ip_start_time))
|
|
77
|
+
heliocentric_coord = coord.transform_to(HeliocentricInertial(obstime=Time(solar_ip_start_time)))
|
|
78
|
+
obs_vr_kms = heliocentric_coord.d_distance
|
|
79
|
+
|
|
80
|
+
return obs_vr_kms
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.fixture(scope="session")
|
|
84
|
+
def central_wavelength() -> u.Quantity:
|
|
85
|
+
return 854.2 * u.nm
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.fixture(scope="session")
|
|
89
|
+
def grating_constant() -> u.Quantity:
|
|
90
|
+
return 316.0 / u.mm
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.fixture(scope="session")
|
|
94
|
+
def incident_light_angle() -> u.Quantity:
|
|
95
|
+
return 73.22 * u.deg
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.fixture(scope="session")
|
|
99
|
+
def reflected_light_angle() -> u.Quantity:
|
|
100
|
+
return 68.76 * u.deg
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.fixture(scope="session")
|
|
104
|
+
def pixel_pitch() -> u.Quantity:
|
|
105
|
+
return 6.5 * u.micron / u.pix
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.fixture(scope="session")
|
|
109
|
+
def lens_parameters_no_units() -> list[float]:
|
|
110
|
+
return [0.7613, 1.720e-4, -8.139e-8]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@pytest.fixture(scope="session")
|
|
114
|
+
def lens_parameters(lens_parameters_no_units) -> list[u.Quantity]:
|
|
115
|
+
return [
|
|
116
|
+
lens_parameters_no_units[0] * u.m,
|
|
117
|
+
lens_parameters_no_units[1] * u.m / u.nm,
|
|
118
|
+
lens_parameters_no_units[2] * u.m / u.nm**2,
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.fixture(scope="session")
|
|
123
|
+
def dispersion(
|
|
124
|
+
central_wavelength, incident_light_angle, reflected_light_angle, lens_parameters, pixel_pitch
|
|
125
|
+
) -> u.Quantity:
|
|
126
|
+
return compute_initial_dispersion(
|
|
127
|
+
central_wavelength=central_wavelength,
|
|
128
|
+
incident_light_angle=incident_light_angle,
|
|
129
|
+
reflected_light_angle=reflected_light_angle,
|
|
130
|
+
lens_parameters=lens_parameters,
|
|
131
|
+
pixel_pitch=pixel_pitch,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.fixture(scope="session")
|
|
136
|
+
def resolving_power() -> int:
|
|
137
|
+
return int(1.15e5)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@pytest.fixture(scope="session")
|
|
141
|
+
def solar_atlas() -> Atlas:
|
|
142
|
+
return Atlas()
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@pytest.fixture(scope="session")
|
|
146
|
+
def spectral_vignette_function(num_wave_pix) -> np.ndarray:
|
|
147
|
+
return (np.arange(num_wave_pix) - num_wave_pix // 2) / (num_wave_pix * 10) + 1.0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@pytest.fixture
|
|
151
|
+
def observed_solar_spectrum(
|
|
152
|
+
central_wavelength,
|
|
153
|
+
dispersion,
|
|
154
|
+
num_wave_pix,
|
|
155
|
+
solar_atlas,
|
|
156
|
+
telluric_opacity_factor,
|
|
157
|
+
doppler_velocity,
|
|
158
|
+
resolving_power,
|
|
159
|
+
) -> np.ndarray:
|
|
160
|
+
"""Make an "observed" solar spectrum from a shifted and atmospherically corrected combination of solar and telluric atlases."""
|
|
161
|
+
# Making a simple solution with just y = mx + b (no higher order angle or order stuff)
|
|
162
|
+
wave_vec = (
|
|
163
|
+
np.arange(num_wave_pix) - num_wave_pix // 2
|
|
164
|
+
) * u.pix * dispersion + central_wavelength
|
|
165
|
+
|
|
166
|
+
doppler_shift = doppler_velocity / constants.c * central_wavelength
|
|
167
|
+
solar_signal = np.interp(
|
|
168
|
+
wave_vec,
|
|
169
|
+
solar_atlas.solar_atlas_wavelength + doppler_shift,
|
|
170
|
+
solar_atlas.solar_atlas_transmission,
|
|
171
|
+
)
|
|
172
|
+
telluric_signal = (
|
|
173
|
+
np.interp(
|
|
174
|
+
wave_vec,
|
|
175
|
+
solar_atlas.telluric_atlas_wavelength,
|
|
176
|
+
solar_atlas.telluric_atlas_transmission,
|
|
177
|
+
)
|
|
178
|
+
** telluric_opacity_factor
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
combined_spec = solar_signal * telluric_signal
|
|
182
|
+
|
|
183
|
+
sigma_wavelength = (
|
|
184
|
+
central_wavelength.to_value(u.nm) / resolving_power
|
|
185
|
+
) * gaussian_fwhm_to_sigma
|
|
186
|
+
sigma_pix = sigma_wavelength / np.abs(dispersion.to_value(u.nm / u.pix))
|
|
187
|
+
observed_spec = gaussian_filter1d(combined_spec, np.abs(sigma_pix))
|
|
188
|
+
|
|
189
|
+
return observed_spec
|
|
26
190
|
|
|
27
191
|
|
|
28
192
|
def write_full_set_of_intermediate_lamp_cals_to_task(
|
|
29
193
|
task,
|
|
30
194
|
data_shape: tuple[int, int],
|
|
31
|
-
|
|
32
|
-
lamp_signal_func: Callable[[int, int], float] = lamp_signal_func,
|
|
195
|
+
lamp_signal_func: Callable[[int], float] = lamp_signal_func,
|
|
33
196
|
):
|
|
34
197
|
for beam in [1, 2]:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
beam=beam,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
198
|
+
lamp_signal = lamp_signal_func(beam)
|
|
199
|
+
lamp_array = np.ones(data_shape) * lamp_signal
|
|
200
|
+
task.write(
|
|
201
|
+
data=lamp_array,
|
|
202
|
+
tags=[
|
|
203
|
+
VispTag.intermediate_frame(beam=beam),
|
|
204
|
+
VispTag.task_lamp_gain(),
|
|
205
|
+
],
|
|
206
|
+
encoder=fits_array_encoder,
|
|
207
|
+
)
|
|
44
208
|
|
|
45
209
|
|
|
46
210
|
def make_solar_input_array_data(
|
|
47
211
|
frame: VispHeadersInputSolarGainFrames,
|
|
48
212
|
dark_signal: float,
|
|
49
|
-
|
|
213
|
+
true_solar_signal: np.ndarray,
|
|
214
|
+
spectral_vignette_signal: np.ndarray,
|
|
215
|
+
lamp_signal_func: Callable[[int], float] = lamp_signal_func,
|
|
50
216
|
):
|
|
51
217
|
data_shape = frame.array_shape[1:]
|
|
52
218
|
beam_shape = (data_shape[0] // 2, data_shape[1])
|
|
@@ -55,12 +221,14 @@ def make_solar_input_array_data(
|
|
|
55
221
|
|
|
56
222
|
beam_list = []
|
|
57
223
|
for beam in [1, 2]:
|
|
58
|
-
true_gain = np.ones(beam_shape)
|
|
59
|
-
true_solar_signal = np.arange(1, beam_shape[0] + 1) / 5
|
|
224
|
+
true_gain = np.ones(beam_shape) * spectral_vignette_signal[:, None]
|
|
60
225
|
true_solar_gain = true_gain * true_solar_signal[:, None]
|
|
61
|
-
lamp_signal = lamp_signal_func(beam
|
|
226
|
+
lamp_signal = lamp_signal_func(beam)
|
|
62
227
|
raw_beam = (true_solar_gain * lamp_signal) + dark_signal
|
|
63
|
-
|
|
228
|
+
if beam == 2:
|
|
229
|
+
beam_list.append(raw_beam[::-1, :])
|
|
230
|
+
else:
|
|
231
|
+
beam_list.append(raw_beam)
|
|
64
232
|
|
|
65
233
|
raw_solar = np.concatenate(beam_list) * num_raw_per_fpa
|
|
66
234
|
return raw_solar
|
|
@@ -72,6 +240,8 @@ def write_input_solar_gains_to_task(
|
|
|
72
240
|
dark_signal: float,
|
|
73
241
|
readout_exp_time: float,
|
|
74
242
|
num_modstates: int,
|
|
243
|
+
true_solar_signal: np.ndarray,
|
|
244
|
+
spectra_vignette_signal: np.ndarray,
|
|
75
245
|
lamp_signal_func: Callable[[int, int], float] = lamp_signal_func,
|
|
76
246
|
):
|
|
77
247
|
array_shape = (1, *data_shape)
|
|
@@ -81,7 +251,11 @@ def write_input_solar_gains_to_task(
|
|
|
81
251
|
num_modstates=num_modstates,
|
|
82
252
|
)
|
|
83
253
|
data_func = partial(
|
|
84
|
-
make_solar_input_array_data,
|
|
254
|
+
make_solar_input_array_data,
|
|
255
|
+
dark_signal=dark_signal,
|
|
256
|
+
lamp_signal_func=lamp_signal_func,
|
|
257
|
+
true_solar_signal=true_solar_signal,
|
|
258
|
+
spectral_vignette_signal=spectra_vignette_signal,
|
|
85
259
|
)
|
|
86
260
|
write_frames_to_task(
|
|
87
261
|
task=task,
|
|
@@ -101,11 +275,23 @@ def solar_gain_task(
|
|
|
101
275
|
tmp_path,
|
|
102
276
|
recipe_run_id,
|
|
103
277
|
init_visp_constants_db,
|
|
278
|
+
central_wavelength,
|
|
279
|
+
incident_light_angle,
|
|
280
|
+
reflected_light_angle,
|
|
281
|
+
grating_constant,
|
|
282
|
+
solar_ip_start_time,
|
|
104
283
|
):
|
|
105
284
|
number_of_modstates = 3
|
|
106
285
|
readout_exp_time = 40.0
|
|
107
286
|
constants_db = VispConstantsDb(
|
|
108
|
-
|
|
287
|
+
ARM_ID=1,
|
|
288
|
+
WAVELENGTH=central_wavelength.to_value(u.nm),
|
|
289
|
+
NUM_MODSTATES=number_of_modstates,
|
|
290
|
+
SOLAR_READOUT_EXP_TIMES=(readout_exp_time,),
|
|
291
|
+
SOLAR_GAIN_IP_START_TIME=solar_ip_start_time,
|
|
292
|
+
INCIDENT_LIGHT_ANGLE_DEG=incident_light_angle.to_value(u.deg),
|
|
293
|
+
REFLECTED_LIGHT_ANGLE_DEG=reflected_light_angle.to_value(u.deg),
|
|
294
|
+
GRATING_CONSTANT_INVERSE_MM=grating_constant.to_value(1 / u.mm),
|
|
109
295
|
)
|
|
110
296
|
init_visp_constants_db(recipe_run_id, constants_db)
|
|
111
297
|
with SolarCalibration(
|
|
@@ -125,10 +311,24 @@ def solar_gain_task(
|
|
|
125
311
|
|
|
126
312
|
@pytest.mark.parametrize(
|
|
127
313
|
"background_on",
|
|
128
|
-
[
|
|
314
|
+
[
|
|
315
|
+
pytest.param(True, id="background_on"),
|
|
316
|
+
pytest.param(False, id="background_off"),
|
|
317
|
+
],
|
|
129
318
|
)
|
|
130
319
|
def test_solar_gain_task(
|
|
131
|
-
solar_gain_task,
|
|
320
|
+
solar_gain_task,
|
|
321
|
+
background_on,
|
|
322
|
+
num_wave_pix,
|
|
323
|
+
spectral_vignette_function,
|
|
324
|
+
observed_solar_spectrum,
|
|
325
|
+
solar_ip_start_time,
|
|
326
|
+
lens_parameters_no_units,
|
|
327
|
+
resolving_power,
|
|
328
|
+
telluric_opacity_factor,
|
|
329
|
+
assign_input_dataset_doc_to_task,
|
|
330
|
+
mocker,
|
|
331
|
+
fake_gql_client,
|
|
132
332
|
):
|
|
133
333
|
"""
|
|
134
334
|
Given: A set of raw solar gain images and necessary intermediate calibrations
|
|
@@ -139,23 +339,22 @@ def test_solar_gain_task(
|
|
|
139
339
|
"dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
|
|
140
340
|
)
|
|
141
341
|
|
|
142
|
-
# It's way too hard to make data for a unit test that will get through the line zones calculation.
|
|
143
|
-
# Leave that for grogu.
|
|
144
|
-
mocker.patch(
|
|
145
|
-
"dkist_processing_visp.tasks.solar.SolarCalibration.compute_line_zones",
|
|
146
|
-
return_value=[(4, 7)],
|
|
147
|
-
)
|
|
148
|
-
|
|
149
342
|
task, readout_exp_time, num_modstates = solar_gain_task
|
|
150
343
|
dark_signal = 3.0
|
|
151
|
-
input_shape = (
|
|
152
|
-
intermediate_shape = (
|
|
344
|
+
input_shape = (num_wave_pix * 2, 100)
|
|
345
|
+
intermediate_shape = (num_wave_pix, 100)
|
|
153
346
|
beam_border = input_shape[0] // 2
|
|
154
347
|
assign_input_dataset_doc_to_task(
|
|
155
348
|
task,
|
|
156
349
|
VispInputDatasetParameterValues(
|
|
157
|
-
visp_background_on=background_on,
|
|
350
|
+
visp_background_on=background_on,
|
|
351
|
+
visp_beam_border=beam_border,
|
|
352
|
+
visp_wavecal_init_opacity_factor=telluric_opacity_factor,
|
|
353
|
+
visp_wavecal_init_resolving_power=resolving_power,
|
|
354
|
+
visp_wavecal_camera_lens_parameters_1=lens_parameters_no_units,
|
|
355
|
+
visp_solar_vignette_initial_continuum_poly_fit_order=1,
|
|
158
356
|
),
|
|
357
|
+
arm_id=1,
|
|
159
358
|
)
|
|
160
359
|
write_intermediate_darks_to_task(
|
|
161
360
|
task=task,
|
|
@@ -168,7 +367,8 @@ def test_solar_gain_task(
|
|
|
168
367
|
task=task, background_signal=0.0, data_shape=intermediate_shape
|
|
169
368
|
)
|
|
170
369
|
write_full_set_of_intermediate_lamp_cals_to_task(
|
|
171
|
-
task=task,
|
|
370
|
+
task=task,
|
|
371
|
+
data_shape=intermediate_shape,
|
|
172
372
|
)
|
|
173
373
|
write_intermediate_geometric_to_task(
|
|
174
374
|
task=task, num_modstates=num_modstates, data_shape=intermediate_shape
|
|
@@ -179,43 +379,38 @@ def test_solar_gain_task(
|
|
|
179
379
|
dark_signal=dark_signal,
|
|
180
380
|
readout_exp_time=readout_exp_time,
|
|
181
381
|
num_modstates=num_modstates,
|
|
382
|
+
true_solar_signal=observed_solar_spectrum,
|
|
383
|
+
spectra_vignette_signal=spectral_vignette_function,
|
|
182
384
|
)
|
|
183
385
|
|
|
184
386
|
task()
|
|
185
387
|
for beam in range(1, task.constants.num_beams + 1):
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
388
|
+
# We need to multiply by the percentile normalization factor that is divided from the characteristic spectra.
|
|
389
|
+
# I.e., the `true_solar_signal` defined above is modified by this scalar prior to removal from the raw spectra
|
|
390
|
+
# so we need to undo that modification here.
|
|
391
|
+
expected_signal = (
|
|
392
|
+
np.ones(intermediate_shape)
|
|
393
|
+
* spectral_vignette_function[:, None]
|
|
394
|
+
* 10
|
|
395
|
+
* beam # Lamp signal
|
|
396
|
+
* np.nanpercentile(
|
|
397
|
+
observed_solar_spectrum,
|
|
398
|
+
task.parameters.solar_characteristic_spatial_normalization_percentile,
|
|
399
|
+
)
|
|
198
400
|
)
|
|
199
401
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
402
|
+
solar_gain = next(
|
|
403
|
+
task.read(
|
|
404
|
+
tags=[
|
|
405
|
+
VispTag.intermediate_frame(beam=beam),
|
|
406
|
+
VispTag.task_solar_gain(),
|
|
407
|
+
],
|
|
408
|
+
decoder=fits_array_decoder,
|
|
207
409
|
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
VispTag.intermediate_frame(beam=beam, modstate=modstate),
|
|
213
|
-
VispTag.task_solar_gain(),
|
|
214
|
-
],
|
|
215
|
-
decoder=fits_array_decoder,
|
|
216
|
-
)
|
|
217
|
-
)
|
|
218
|
-
np.testing.assert_allclose(expected, solar_gain)
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Testing that the ratio is close to 1.0 give us a bit more leeway in the deviation
|
|
413
|
+
np.testing.assert_allclose(expected_signal / solar_gain, 1.0, atol=1e-2, rtol=1e-2)
|
|
219
414
|
|
|
220
415
|
quality_files = task.read(tags=[Tag.quality("TASK_TYPES")])
|
|
221
416
|
for file in quality_files:
|
|
@@ -226,43 +421,65 @@ def test_solar_gain_task(
|
|
|
226
421
|
tags=[VispTag.input(), VispTag.frame(), VispTag.task_solar_gain()]
|
|
227
422
|
)
|
|
228
423
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
zones = task.compute_line_zones(spec[:, None], bg_order=0, rel_height=0.5)
|
|
253
|
-
assert zones == expected
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
def test_identify_overlapping_zones(solar_gain_task):
|
|
424
|
+
first_vignette_quality_files = list(task.read(tags=[Tag.quality("SOLAR_CAL_FIRST_VIGNETTE")]))
|
|
425
|
+
assert len(first_vignette_quality_files) == 2
|
|
426
|
+
for beam_file in first_vignette_quality_files:
|
|
427
|
+
with asdf.open(beam_file, lazy_load=False, memmap=False) as f:
|
|
428
|
+
results = f.tree
|
|
429
|
+
assert isinstance(results["output_wave_vec"], NDArrayType)
|
|
430
|
+
assert isinstance(results["input_spectrum"], NDArrayType)
|
|
431
|
+
assert isinstance(results["best_fit_atlas"], NDArrayType)
|
|
432
|
+
assert isinstance(results["best_fit_continuum"], NDArrayType)
|
|
433
|
+
assert isinstance(results["residuals"], NDArrayType)
|
|
434
|
+
|
|
435
|
+
final_vignette_quality_files = list(task.read(tags=[Tag.quality("SOLAR_CAL_FINAL_VIGNETTE")]))
|
|
436
|
+
assert len(final_vignette_quality_files) == 2
|
|
437
|
+
for beam_file in final_vignette_quality_files:
|
|
438
|
+
with asdf.open(beam_file, lazy_load=False, memmap=False) as f:
|
|
439
|
+
results = f.tree
|
|
440
|
+
assert isinstance(results["output_wave_vec"], NDArrayType)
|
|
441
|
+
assert isinstance(results["median_spec"], NDArrayType)
|
|
442
|
+
assert isinstance(results["low_deviation"], NDArrayType)
|
|
443
|
+
assert isinstance(results["high_deviation"], NDArrayType)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def test_continuum_wavecal_parameters():
|
|
257
447
|
"""
|
|
258
|
-
Given: A
|
|
259
|
-
When:
|
|
260
|
-
Then: The
|
|
448
|
+
Given: A `WavelengthCalibrationParametersWithContinuum` class instantiated with a poly fit order
|
|
449
|
+
When: Constructing the `lmfit_parameters` object
|
|
450
|
+
Then: The polynomial coefficients are present
|
|
261
451
|
"""
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
452
|
+
order = 3
|
|
453
|
+
zeroth_order = 6.28
|
|
454
|
+
param_object = WavelengthCalibrationParametersWithContinuum(
|
|
455
|
+
continuum_poly_fit_order=order,
|
|
456
|
+
zeroth_order_continuum_coefficient=zeroth_order,
|
|
457
|
+
crval=400 * u.nm,
|
|
458
|
+
dispersion=3 * u.nm / u.pix,
|
|
459
|
+
incident_light_angle=4 * u.deg,
|
|
460
|
+
grating_constant=3 / u.mm,
|
|
461
|
+
doppler_velocity=0 * u.km / u.s,
|
|
462
|
+
order=1,
|
|
463
|
+
normalized_abscissa=np.arange(10),
|
|
464
|
+
continuum_level=99999.9,
|
|
465
|
+
)
|
|
466
|
+
lmfit_params = param_object.lmfit_parameters
|
|
467
|
+
|
|
468
|
+
assert "poly_coeff_03" in lmfit_params
|
|
469
|
+
assert "poly_coeff_02" in lmfit_params
|
|
470
|
+
assert "poly_coeff_01" in lmfit_params
|
|
471
|
+
assert "poly_coeff_00" in lmfit_params
|
|
472
|
+
|
|
473
|
+
assert lmfit_params["poly_coeff_03"].init_value == zeroth_order
|
|
474
|
+
assert lmfit_params["poly_coeff_02"].init_value == 0
|
|
475
|
+
assert lmfit_params["poly_coeff_01"].init_value == 0
|
|
476
|
+
assert lmfit_params["poly_coeff_00"].init_value == 0
|
|
477
|
+
|
|
478
|
+
assert lmfit_params["poly_coeff_03"].min == zeroth_order * 0.5
|
|
479
|
+
assert lmfit_params["poly_coeff_03"].max == zeroth_order * 1.5
|
|
480
|
+
assert lmfit_params["poly_coeff_02"].min == -1
|
|
481
|
+
assert lmfit_params["poly_coeff_02"].max == 1
|
|
482
|
+
assert lmfit_params["poly_coeff_01"].min == -1
|
|
483
|
+
assert lmfit_params["poly_coeff_01"].max == 1
|
|
484
|
+
assert lmfit_params["poly_coeff_00"].min == -1
|
|
485
|
+
assert lmfit_params["poly_coeff_00"].max == 1
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from dataclasses import asdict
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
|
|
4
|
+
import astropy.units as u
|
|
4
5
|
import pytest
|
|
5
6
|
|
|
6
7
|
from dkist_processing_visp.tasks.visp_base import VispTaskBase
|
|
@@ -8,6 +9,7 @@ from dkist_processing_visp.tasks.visp_base import VispTaskBase
|
|
|
8
9
|
|
|
9
10
|
@dataclass
|
|
10
11
|
class testing_constants:
|
|
12
|
+
arm_id: int = 2
|
|
11
13
|
obs_ip_start_time: str = "1999-12-31T23:59:59"
|
|
12
14
|
num_modstates: int = 10
|
|
13
15
|
num_beams: int = 2
|
|
@@ -22,10 +24,23 @@ class testing_constants:
|
|
|
22
24
|
solar_readout_exp_times: tuple[float] = (2.0,)
|
|
23
25
|
observe_readout_exp_times: tuple[float] = (0.02,)
|
|
24
26
|
retarder_name: str = "SiO2 OC"
|
|
27
|
+
incident_light_angle_deg: float = 74.2
|
|
28
|
+
reflected_light_angle_deg: float = 71.3
|
|
29
|
+
grating_constant_inverse_mm: float = 316.0
|
|
30
|
+
solar_gain_ip_start_time: str = "2000-01-01T00:00:01"
|
|
25
31
|
# We don't need all the common ones, but let's put one just to check
|
|
26
32
|
instrument: str = "CHECK_OUT_THIS_INSTRUMENT"
|
|
27
33
|
|
|
28
34
|
|
|
35
|
+
@pytest.fixture(scope="session")
|
|
36
|
+
def unit_constant_names_and_units() -> dict[str, u.Unit]:
|
|
37
|
+
return {
|
|
38
|
+
"incident_light_angle_deg": u.deg,
|
|
39
|
+
"reflected_light_angle_deg": u.deg,
|
|
40
|
+
"grating_constant_inverse_mm": 1 / u.mm,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
29
44
|
@pytest.fixture(scope="session")
|
|
30
45
|
def expected_constant_dict() -> dict:
|
|
31
46
|
lower_dict = asdict(testing_constants())
|
|
@@ -49,14 +64,28 @@ def visp_science_task_with_constants(recipe_run_id, expected_constant_dict, init
|
|
|
49
64
|
task._purge()
|
|
50
65
|
|
|
51
66
|
|
|
52
|
-
def test_visp_constants(
|
|
53
|
-
|
|
67
|
+
def test_visp_constants(
|
|
68
|
+
visp_science_task_with_constants, unit_constant_names_and_units, expected_constant_dict
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
Given: A task with an attached `VispConstants` object and a populated constants db
|
|
72
|
+
When: Accessing values through the `.constants.VALUE` machinery
|
|
73
|
+
Then: The correct values form the db are returned
|
|
74
|
+
"""
|
|
54
75
|
task = visp_science_task_with_constants
|
|
55
76
|
for k, v in expected_constant_dict.items():
|
|
56
|
-
if
|
|
57
|
-
|
|
58
|
-
if k in ["POLARIMETER_MODE", "RETARDER_NAME"]:
|
|
77
|
+
if k in ["ARM_ID", "POLARIMETER_MODE", "RETARDER_NAME"]:
|
|
78
|
+
# These have some extra logic after db retrieval
|
|
59
79
|
continue
|
|
60
|
-
|
|
80
|
+
if k.lower() in unit_constant_names_and_units:
|
|
81
|
+
united_value = getattr(task.constants, k.lower())
|
|
82
|
+
assert united_value.value == v
|
|
83
|
+
assert united_value.unit == unit_constant_names_and_units[k.lower()]
|
|
84
|
+
else:
|
|
85
|
+
if type(v) is tuple:
|
|
86
|
+
v = list(v)
|
|
87
|
+
assert getattr(task.constants, k.lower()) == v
|
|
88
|
+
|
|
61
89
|
assert task.constants.correct_for_polarization == True
|
|
62
90
|
assert task.constants.pac_init_set == "OCCal_VIS"
|
|
91
|
+
assert task.constants.arm_id == str(expected_constant_dict["ARM_ID"])
|