dkist-processing-common 10.5.4__py3-none-any.whl → 12.1.0rc1__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.
- changelog/280.misc.rst +1 -0
- changelog/282.feature.2.rst +2 -0
- changelog/282.feature.rst +2 -0
- changelog/284.feature.rst +1 -0
- changelog/285.feature.rst +2 -0
- changelog/285.misc.rst +2 -0
- changelog/286.feature.rst +2 -0
- changelog/287.misc.rst +1 -0
- dkist_processing_common/__init__.py +1 -0
- dkist_processing_common/_util/constants.py +1 -0
- dkist_processing_common/_util/graphql.py +1 -0
- dkist_processing_common/_util/scratch.py +9 -9
- dkist_processing_common/_util/tags.py +1 -0
- dkist_processing_common/codecs/array.py +20 -0
- dkist_processing_common/codecs/asdf.py +9 -3
- dkist_processing_common/codecs/basemodel.py +22 -0
- dkist_processing_common/codecs/bytes.py +1 -0
- dkist_processing_common/codecs/fits.py +37 -9
- dkist_processing_common/codecs/iobase.py +1 -0
- dkist_processing_common/codecs/json.py +1 -0
- dkist_processing_common/codecs/path.py +1 -0
- dkist_processing_common/codecs/quality.py +1 -1
- dkist_processing_common/codecs/str.py +1 -0
- dkist_processing_common/config.py +64 -25
- dkist_processing_common/manual.py +6 -8
- dkist_processing_common/models/constants.py +373 -37
- dkist_processing_common/models/dkist_location.py +27 -0
- dkist_processing_common/models/fits_access.py +48 -0
- dkist_processing_common/models/flower_pot.py +231 -9
- dkist_processing_common/models/fried_parameter.py +41 -0
- dkist_processing_common/models/graphql.py +66 -75
- dkist_processing_common/models/input_dataset.py +117 -0
- dkist_processing_common/models/message.py +1 -1
- dkist_processing_common/models/message_queue_binding.py +1 -1
- dkist_processing_common/models/metric_code.py +2 -0
- dkist_processing_common/models/parameters.py +65 -28
- dkist_processing_common/models/quality.py +50 -5
- dkist_processing_common/models/tags.py +23 -21
- dkist_processing_common/models/task_name.py +3 -2
- dkist_processing_common/models/telemetry.py +28 -0
- dkist_processing_common/models/wavelength.py +3 -1
- dkist_processing_common/parsers/average_bud.py +46 -0
- dkist_processing_common/parsers/cs_step.py +13 -12
- dkist_processing_common/parsers/dsps_repeat.py +6 -4
- dkist_processing_common/parsers/experiment_id_bud.py +12 -4
- dkist_processing_common/parsers/id_bud.py +42 -27
- dkist_processing_common/parsers/l0_fits_access.py +5 -3
- dkist_processing_common/parsers/l1_fits_access.py +51 -23
- dkist_processing_common/parsers/lookup_bud.py +125 -0
- dkist_processing_common/parsers/near_bud.py +21 -20
- dkist_processing_common/parsers/observing_program_id_bud.py +24 -0
- dkist_processing_common/parsers/proposal_id_bud.py +13 -5
- dkist_processing_common/parsers/quality.py +2 -0
- dkist_processing_common/parsers/retarder.py +32 -0
- dkist_processing_common/parsers/single_value_single_key_flower.py +6 -1
- dkist_processing_common/parsers/task.py +8 -6
- dkist_processing_common/parsers/time.py +178 -72
- dkist_processing_common/parsers/unique_bud.py +21 -22
- dkist_processing_common/parsers/wavelength.py +5 -3
- dkist_processing_common/tasks/__init__.py +3 -2
- dkist_processing_common/tasks/assemble_movie.py +4 -3
- dkist_processing_common/tasks/base.py +59 -60
- dkist_processing_common/tasks/l1_output_data.py +54 -53
- dkist_processing_common/tasks/mixin/globus.py +24 -27
- dkist_processing_common/tasks/mixin/interservice_bus.py +1 -0
- dkist_processing_common/tasks/mixin/metadata_store.py +108 -243
- dkist_processing_common/tasks/mixin/object_store.py +22 -0
- dkist_processing_common/tasks/mixin/quality/__init__.py +1 -0
- dkist_processing_common/tasks/mixin/quality/_base.py +8 -1
- dkist_processing_common/tasks/mixin/quality/_metrics.py +166 -14
- dkist_processing_common/tasks/output_data_base.py +4 -3
- dkist_processing_common/tasks/parse_l0_input_data.py +277 -15
- dkist_processing_common/tasks/quality_metrics.py +9 -9
- dkist_processing_common/tasks/teardown.py +7 -7
- dkist_processing_common/tasks/transfer_input_data.py +67 -69
- dkist_processing_common/tasks/trial_catalog.py +77 -17
- dkist_processing_common/tasks/trial_output_data.py +16 -17
- dkist_processing_common/tasks/write_l1.py +102 -72
- dkist_processing_common/tests/conftest.py +32 -173
- dkist_processing_common/tests/mock_metadata_store.py +271 -0
- dkist_processing_common/tests/test_assemble_movie.py +4 -4
- dkist_processing_common/tests/test_assemble_quality.py +32 -4
- dkist_processing_common/tests/test_base.py +5 -19
- dkist_processing_common/tests/test_codecs.py +103 -12
- dkist_processing_common/tests/test_constants.py +15 -0
- dkist_processing_common/tests/test_dkist_location.py +15 -0
- dkist_processing_common/tests/test_fits_access.py +56 -19
- dkist_processing_common/tests/test_flower_pot.py +147 -5
- dkist_processing_common/tests/test_fried_parameter.py +27 -0
- dkist_processing_common/tests/test_input_dataset.py +78 -361
- dkist_processing_common/tests/test_interservice_bus.py +1 -0
- dkist_processing_common/tests/test_interservice_bus_mixin.py +1 -1
- dkist_processing_common/tests/test_manual_processing.py +33 -0
- dkist_processing_common/tests/test_output_data_base.py +5 -7
- dkist_processing_common/tests/test_parameters.py +71 -22
- dkist_processing_common/tests/test_parse_l0_input_data.py +115 -32
- dkist_processing_common/tests/test_publish_catalog_messages.py +2 -24
- dkist_processing_common/tests/test_quality.py +1 -0
- dkist_processing_common/tests/test_quality_mixin.py +255 -23
- dkist_processing_common/tests/test_scratch.py +2 -1
- dkist_processing_common/tests/test_stems.py +511 -168
- dkist_processing_common/tests/test_submit_dataset_metadata.py +3 -7
- dkist_processing_common/tests/test_tags.py +1 -0
- dkist_processing_common/tests/test_task_name.py +1 -1
- dkist_processing_common/tests/test_task_parsing.py +17 -7
- dkist_processing_common/tests/test_teardown.py +28 -24
- dkist_processing_common/tests/test_transfer_input_data.py +270 -125
- dkist_processing_common/tests/test_transfer_l1_output_data.py +2 -3
- dkist_processing_common/tests/test_trial_catalog.py +83 -8
- dkist_processing_common/tests/test_trial_output_data.py +46 -73
- dkist_processing_common/tests/test_workflow_task_base.py +8 -10
- dkist_processing_common/tests/test_write_l1.py +298 -76
- dkist_processing_common-12.1.0rc1.dist-info/METADATA +265 -0
- dkist_processing_common-12.1.0rc1.dist-info/RECORD +134 -0
- {dkist_processing_common-10.5.4.dist-info → dkist_processing_common-12.1.0rc1.dist-info}/WHEEL +1 -1
- docs/conf.py +1 -0
- docs/index.rst +1 -1
- docs/landing_page.rst +13 -0
- dkist_processing_common/tasks/mixin/input_dataset.py +0 -166
- dkist_processing_common-10.5.4.dist-info/METADATA +0 -175
- dkist_processing_common-10.5.4.dist-info/RECORD +0 -112
- {dkist_processing_common-10.5.4.dist-info → dkist_processing_common-12.1.0rc1.dist-info}/top_level.txt +0 -0
|
@@ -3,13 +3,21 @@ import json
|
|
|
3
3
|
from io import StringIO
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
|
+
import astropy.units as u
|
|
6
7
|
import numpy as np
|
|
7
8
|
import pandas
|
|
8
9
|
import pytest
|
|
10
|
+
from lmfit.minimizer import MinimizerResult
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import FitResult
|
|
13
|
+
from solar_wavelength_calibration.fitter.wavelength_fitter import WavelengthParameters
|
|
9
14
|
|
|
10
15
|
from dkist_processing_common._util.scratch import WorkflowFileSystem
|
|
11
16
|
from dkist_processing_common.codecs.json import json_encoder
|
|
12
17
|
from dkist_processing_common.codecs.quality import QualityValueEncoder
|
|
18
|
+
from dkist_processing_common.models.metric_code import MetricCode
|
|
19
|
+
from dkist_processing_common.models.quality import Plot2D
|
|
20
|
+
from dkist_processing_common.models.quality import VerticalMultiPanePlot2D
|
|
13
21
|
from dkist_processing_common.models.tags import Tag
|
|
14
22
|
from dkist_processing_common.tasks import WorkflowTaskBase
|
|
15
23
|
from dkist_processing_common.tasks.mixin.quality import QualityMixin
|
|
@@ -21,7 +29,7 @@ class Task(WorkflowTaskBase, QualityMixin):
|
|
|
21
29
|
|
|
22
30
|
|
|
23
31
|
@pytest.fixture
|
|
24
|
-
def quality_task(tmp_path, recipe_run_id):
|
|
32
|
+
def quality_task(tmp_path, recipe_run_id) -> Task:
|
|
25
33
|
with Task(
|
|
26
34
|
recipe_run_id=recipe_run_id,
|
|
27
35
|
workflow_name="workflow_name",
|
|
@@ -39,9 +47,9 @@ def quality_task(tmp_path, recipe_run_id):
|
|
|
39
47
|
@pytest.fixture
|
|
40
48
|
def plot_data():
|
|
41
49
|
datetimes_a = ["2021-01-01T01:01:01", "2021-01-01T02:01:01"]
|
|
42
|
-
values_a = [
|
|
50
|
+
values_a = [0.1, 0.2]
|
|
43
51
|
datetimes_b = ["2020-01-01T01:01:01", "2020-01-01T02:01:01"]
|
|
44
|
-
values_b = [
|
|
52
|
+
values_b = [0.15, 0.25]
|
|
45
53
|
return datetimes_a, values_a, datetimes_b, values_b
|
|
46
54
|
|
|
47
55
|
|
|
@@ -106,6 +114,7 @@ def test_create_2d_plot_with_datetime_metric(quality_task):
|
|
|
106
114
|
"facet",
|
|
107
115
|
"statement",
|
|
108
116
|
"plot_data",
|
|
117
|
+
"multi_plot_data",
|
|
109
118
|
"histogram_data",
|
|
110
119
|
"table_data",
|
|
111
120
|
"modmat_data",
|
|
@@ -198,8 +207,9 @@ def test_store_ao_status_and_fried_parameter(quality_task, ao_values):
|
|
|
198
207
|
"""
|
|
199
208
|
task = quality_task
|
|
200
209
|
datetimes = ["2020-01-01T01:01:01", "2020-01-01T02:01:01"]
|
|
201
|
-
fried_values = [
|
|
202
|
-
|
|
210
|
+
fried_values = [0.1, 0.2]
|
|
211
|
+
oob_values = [25, 50]
|
|
212
|
+
combined_values = [[ao, r0, oob] for ao, r0, oob in zip(ao_values, fried_values, oob_values)]
|
|
203
213
|
task.quality_store_ao_status_and_fried_parameter(datetimes=datetimes, values=combined_values)
|
|
204
214
|
path = list(task.read(tags=Tag.quality("AO_STATUS")))
|
|
205
215
|
assert len(path) == 1
|
|
@@ -220,10 +230,11 @@ def test_store_ao_status_and_fried_parameter(quality_task, ao_values):
|
|
|
220
230
|
@pytest.mark.parametrize(
|
|
221
231
|
"combined_values",
|
|
222
232
|
[
|
|
223
|
-
pytest.param([[True, 1], [None, 2]], id="AO_some_none"),
|
|
224
|
-
pytest.param([[True, 1], [True, None]], id="Fried_some_none"),
|
|
225
|
-
pytest.param([[None, 1], [None, 2]], id="AO_all_none"),
|
|
226
|
-
pytest.param([[True, None], [True, None]], id="Fried_all_none"),
|
|
233
|
+
pytest.param([[True, 0.1, 25], [None, 0.2, 25]], id="AO_some_none"),
|
|
234
|
+
pytest.param([[True, 0.1, 25], [True, None, 25]], id="Fried_some_none"),
|
|
235
|
+
pytest.param([[None, 0.1, 25], [None, 0.2, 25]], id="AO_all_none"),
|
|
236
|
+
pytest.param([[True, None, 25], [True, None, 25]], id="Fried_all_none"),
|
|
237
|
+
pytest.param([[True, 0.1, None], [True, 0.2, None]], id="Out_of_bounds_all_none"),
|
|
227
238
|
],
|
|
228
239
|
)
|
|
229
240
|
def test_store_ao_status_and_fried_parameter_with_nones(quality_task, combined_values):
|
|
@@ -231,13 +242,14 @@ def test_store_ao_status_and_fried_parameter_with_nones(quality_task, combined_v
|
|
|
231
242
|
datetimes = ["2020-01-01T01:01:01", "2020-01-01T02:01:01"]
|
|
232
243
|
task.quality_store_ao_status_and_fried_parameter(datetimes=datetimes, values=combined_values)
|
|
233
244
|
path = list(task.read(tags=Tag.quality("AO_STATUS")))
|
|
234
|
-
ao_values = [ao for ao, r0 in combined_values]
|
|
235
|
-
fried_values = [r0 for ao, r0 in combined_values]
|
|
245
|
+
ao_values = [ao for ao, r0, oob in combined_values]
|
|
246
|
+
fried_values = [r0 for ao, r0, oob in combined_values]
|
|
247
|
+
ao_out_of_bounds = [oob for ao, r0, oob in combined_values]
|
|
236
248
|
if not all(ao is None for ao in ao_values):
|
|
237
249
|
assert len(path) == 1
|
|
238
250
|
with path[0].open() as f:
|
|
239
251
|
data = json.load(f)
|
|
240
|
-
assert len(data) == sum(1 for ao, r0 in combined_values if ao is not None)
|
|
252
|
+
assert len(data) == sum(1 for ao, r0, oob in combined_values if ao is not None)
|
|
241
253
|
else:
|
|
242
254
|
assert len(path) == 0
|
|
243
255
|
path = list(task.read(tags=Tag.quality("FRIED_PARAMETER")))
|
|
@@ -246,7 +258,7 @@ def test_store_ao_status_and_fried_parameter_with_nones(quality_task, combined_v
|
|
|
246
258
|
with path[0].open() as f:
|
|
247
259
|
data = json.load(f)
|
|
248
260
|
assert len(data["y_values"]) == sum(
|
|
249
|
-
1 for ao, r0 in combined_values if ao is True and r0 is not None
|
|
261
|
+
1 for ao, r0, oob in combined_values if ao is True and r0 is not None
|
|
250
262
|
)
|
|
251
263
|
else:
|
|
252
264
|
assert len(path) == 0
|
|
@@ -263,7 +275,8 @@ def test_build_ao_status(quality_task, plot_data):
|
|
|
263
275
|
datetimes = datetimes_a + datetimes_b
|
|
264
276
|
fried_values = values_a + values_b
|
|
265
277
|
ao_values = [False, True, True, True]
|
|
266
|
-
|
|
278
|
+
oob_values = [25, 50, None, 50]
|
|
279
|
+
combined_values = [[ao, r0, oob] for ao, r0, oob in zip(ao_values, fried_values, oob_values)]
|
|
267
280
|
task.quality_store_ao_status_and_fried_parameter(datetimes=datetimes, values=combined_values)
|
|
268
281
|
metric = task.quality_build_ao_status()
|
|
269
282
|
assert metric["name"] == "Adaptive Optics Status"
|
|
@@ -284,9 +297,15 @@ def test_build_fried_parameter(quality_task, plot_data):
|
|
|
284
297
|
task = quality_task
|
|
285
298
|
datetimes_a, fried_values_a, datetimes_b, fried_values_b = plot_data
|
|
286
299
|
ao_values_a = [True, True]
|
|
287
|
-
|
|
300
|
+
oob_values_a = [25, 50]
|
|
301
|
+
combined_values_a = [
|
|
302
|
+
[ao, r0, oob] for ao, r0, oob in zip(ao_values_a, fried_values_a, oob_values_a)
|
|
303
|
+
]
|
|
288
304
|
ao_values_b = [True, True]
|
|
289
|
-
|
|
305
|
+
oob_values_b = [25, 50]
|
|
306
|
+
combined_values_b = [
|
|
307
|
+
[ao, r0, oob] for ao, r0, oob in zip(ao_values_b, fried_values_b, oob_values_b)
|
|
308
|
+
]
|
|
290
309
|
task.quality_store_ao_status_and_fried_parameter(
|
|
291
310
|
datetimes=datetimes_a, values=combined_values_a
|
|
292
311
|
)
|
|
@@ -307,14 +326,14 @@ def test_build_fried_parameter(quality_task, plot_data):
|
|
|
307
326
|
"2021-01-01T02:01:01",
|
|
308
327
|
]
|
|
309
328
|
]
|
|
310
|
-
assert metric["plot_data"]["series_data"][""][1] == [
|
|
329
|
+
assert metric["plot_data"]["series_data"][""][1] == [0.15, 0.25, 0.1, 0.2]
|
|
311
330
|
assert metric["name"] == "Fried Parameter"
|
|
312
331
|
assert metric["metric_code"] == "FRIED_PARAMETER"
|
|
313
332
|
assert metric["facet"] is None
|
|
314
333
|
assert metric["warnings"] is None
|
|
315
334
|
assert (
|
|
316
335
|
metric["statement"]
|
|
317
|
-
== "Average valid Fried Parameter measurements for L1 dataset:
|
|
336
|
+
== "Average valid Fried Parameter measurements for L1 dataset: 0.18 ± 0.06 m"
|
|
318
337
|
)
|
|
319
338
|
|
|
320
339
|
|
|
@@ -340,12 +359,12 @@ def test_build_light_level(quality_task, plot_data):
|
|
|
340
359
|
"2021-01-01T02:01:01",
|
|
341
360
|
]
|
|
342
361
|
]
|
|
343
|
-
assert metric["plot_data"]["series_data"][""][1] == [
|
|
362
|
+
assert metric["plot_data"]["series_data"][""][1] == [0.15, 0.25, 0.1, 0.2]
|
|
344
363
|
assert metric["name"] == "Light Level"
|
|
345
364
|
assert metric["metric_code"] == "LIGHT_LEVEL"
|
|
346
365
|
assert metric["facet"] is None
|
|
347
366
|
assert metric["warnings"] is None
|
|
348
|
-
assert metric["statement"] == f"Average Light Level for L1 dataset:
|
|
367
|
+
assert metric["statement"] == f"Average Light Level for L1 dataset: 0.18 ± 0.06 adu"
|
|
349
368
|
|
|
350
369
|
|
|
351
370
|
def test_build_frame_average(quality_task, plot_data):
|
|
@@ -461,7 +480,7 @@ def test_build_noise(quality_task, plot_data):
|
|
|
461
480
|
"2021-01-01T02:01:01",
|
|
462
481
|
]
|
|
463
482
|
]
|
|
464
|
-
assert metric["plot_data"]["series_data"]["I"][1] == [
|
|
483
|
+
assert metric["plot_data"]["series_data"]["I"][1] == [0.15, 0.25, 0.1, 0.2]
|
|
465
484
|
assert metric["name"] == "Noise Estimation"
|
|
466
485
|
assert metric["metric_code"] == "NOISE"
|
|
467
486
|
assert metric["facet"] is None
|
|
@@ -493,7 +512,7 @@ def test_build_sensitivity(quality_task, plot_data):
|
|
|
493
512
|
"2021-01-01T02:01:01",
|
|
494
513
|
]
|
|
495
514
|
]
|
|
496
|
-
assert metric["plot_data"]["series_data"]["I"][1] == [
|
|
515
|
+
assert metric["plot_data"]["series_data"]["I"][1] == [0.15, 0.25, 0.1, 0.2]
|
|
497
516
|
assert metric["name"] == f"Sensitivity"
|
|
498
517
|
assert metric["metric_code"] == "SENSITIVITY"
|
|
499
518
|
assert metric["facet"] is None
|
|
@@ -692,7 +711,7 @@ def test_build_report(quality_task, plot_data):
|
|
|
692
711
|
task.quality_store_task_type_counts(task_type="dark", total_frames=100, frames_not_used=7)
|
|
693
712
|
task.quality_store_task_type_counts(task_type="gain", total_frames=100, frames_not_used=0)
|
|
694
713
|
task.quality_store_ao_status_and_fried_parameter(
|
|
695
|
-
datetimes=datetimes, values=[[True, values[0]], [True, values[1]]]
|
|
714
|
+
datetimes=datetimes, values=[[True, values[0], values[0]], [True, values[1], values[1]]]
|
|
696
715
|
)
|
|
697
716
|
task.quality_store_light_level(datetimes=datetimes, values=values)
|
|
698
717
|
task.quality_store_frame_average(
|
|
@@ -1175,6 +1194,189 @@ def test_avg_noise_nan_values(quality_task, array_shape):
|
|
|
1175
1194
|
assert not np.isnan(result)
|
|
1176
1195
|
|
|
1177
1196
|
|
|
1197
|
+
@pytest.fixture(scope="session")
|
|
1198
|
+
def wavecal_input_wavelength() -> u.Quantity:
|
|
1199
|
+
return np.arange(100) * u.nm
|
|
1200
|
+
|
|
1201
|
+
|
|
1202
|
+
@pytest.fixture(scope="session")
|
|
1203
|
+
def wavecal_input_spectrum(wavecal_input_wavelength) -> np.ndarray:
|
|
1204
|
+
spec = (wavecal_input_wavelength.value - wavecal_input_wavelength.size // 2) ** 2 + 10.0
|
|
1205
|
+
spec[spec.size // 2] = np.nan
|
|
1206
|
+
return spec
|
|
1207
|
+
|
|
1208
|
+
|
|
1209
|
+
@pytest.fixture(scope="session")
|
|
1210
|
+
def wavecal_weights(wavecal_input_wavelength) -> np.ndarray:
|
|
1211
|
+
weights = np.arange(wavecal_input_wavelength.size, dtype=float)
|
|
1212
|
+
weights[0] = np.inf
|
|
1213
|
+
return weights
|
|
1214
|
+
|
|
1215
|
+
|
|
1216
|
+
@pytest.fixture(scope="session")
|
|
1217
|
+
def wavecal_fit_result(wavecal_input_wavelength, wavecal_input_spectrum) -> FitResult:
|
|
1218
|
+
wavelength_params = WavelengthParameters(
|
|
1219
|
+
crpix=1, crval=10.0, dispersion=1, grating_constant=1, order=1, incident_light_angle=0
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
residuals = np.random.random(wavecal_input_wavelength.size)
|
|
1223
|
+
residuals[-1] = np.nan
|
|
1224
|
+
minimizer_result = MinimizerResult(residual=residuals)
|
|
1225
|
+
return FitResult(
|
|
1226
|
+
wavelength_parameters=wavelength_params,
|
|
1227
|
+
minimizer_result=minimizer_result,
|
|
1228
|
+
input_wavelength_vector=wavecal_input_wavelength,
|
|
1229
|
+
input_spectrum=wavecal_input_spectrum,
|
|
1230
|
+
)
|
|
1231
|
+
|
|
1232
|
+
|
|
1233
|
+
@pytest.mark.parametrize(
|
|
1234
|
+
"use_weights",
|
|
1235
|
+
[pytest.param(True, id="custom_weights"), pytest.param(False, id="default_weights")],
|
|
1236
|
+
)
|
|
1237
|
+
def test_wavecal_store_results(
|
|
1238
|
+
quality_task,
|
|
1239
|
+
wavecal_input_wavelength,
|
|
1240
|
+
wavecal_input_spectrum,
|
|
1241
|
+
wavecal_fit_result,
|
|
1242
|
+
wavecal_weights,
|
|
1243
|
+
use_weights,
|
|
1244
|
+
):
|
|
1245
|
+
"""
|
|
1246
|
+
Given: A task with the QualityMixin and the results of a wavecal fit
|
|
1247
|
+
When: Storing the wavecal metric
|
|
1248
|
+
Then: The correct metric json files are written and their contents contain the correct types of data
|
|
1249
|
+
"""
|
|
1250
|
+
quality_task.quality_store_wavecal_results(
|
|
1251
|
+
input_wavelength=wavecal_input_wavelength,
|
|
1252
|
+
input_spectrum=wavecal_input_spectrum,
|
|
1253
|
+
fit_result=wavecal_fit_result,
|
|
1254
|
+
weights=wavecal_weights if use_weights else None,
|
|
1255
|
+
)
|
|
1256
|
+
|
|
1257
|
+
wavecal_quality_files = list(quality_task.read(tags=[Tag.quality(MetricCode.wavecal_fit)]))
|
|
1258
|
+
assert len(wavecal_quality_files) == 1
|
|
1259
|
+
with open(wavecal_quality_files[0], "r") as f:
|
|
1260
|
+
results_dict = json.load(f)
|
|
1261
|
+
assert sorted(results_dict.keys()) == sorted(
|
|
1262
|
+
[
|
|
1263
|
+
"input_wavelength_nm",
|
|
1264
|
+
"input_spectrum",
|
|
1265
|
+
"best_fit_wavelength_nm",
|
|
1266
|
+
"best_fit_atlas",
|
|
1267
|
+
"normalized_residuals",
|
|
1268
|
+
"weights",
|
|
1269
|
+
]
|
|
1270
|
+
)
|
|
1271
|
+
for k, v in results_dict.items():
|
|
1272
|
+
if k != "weights" or use_weights:
|
|
1273
|
+
assert isinstance(v, list)
|
|
1274
|
+
assert len(v) == len(results_dict["input_wavelength_nm"])
|
|
1275
|
+
if not use_weights:
|
|
1276
|
+
assert results_dict["weights"] is None
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
@pytest.fixture(
|
|
1280
|
+
scope="session",
|
|
1281
|
+
params=[pytest.param([0, 1.0, 0.8, 0.0], id="weights"), pytest.param(None, id="no_weights")],
|
|
1282
|
+
)
|
|
1283
|
+
def wavecal_data_json(request) -> dict:
|
|
1284
|
+
weights = request.param
|
|
1285
|
+
return {
|
|
1286
|
+
"input_wavelength_nm": [1001.0, 1002.0, 1003.0, 1004.0],
|
|
1287
|
+
"input_spectrum": [1.0, 1.0, 0.5, 1.0],
|
|
1288
|
+
"best_fit_wavelength_nm": [1001.5, 1002.6, 1003.7, 1004.8],
|
|
1289
|
+
"best_fit_atlas": [1.0, 1.0, 0.4, 1.0],
|
|
1290
|
+
"normalized_residuals": [0.0, 0.0, 0.1, 0.0],
|
|
1291
|
+
"weights": weights,
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
|
|
1295
|
+
def test_build_wavecal_results(quality_task, wavecal_data_json):
|
|
1296
|
+
"""
|
|
1297
|
+
Given: A task with the QualityMixin
|
|
1298
|
+
When: Building the wavecal results quality metric
|
|
1299
|
+
Then: The correct metric model is returned
|
|
1300
|
+
"""
|
|
1301
|
+
weights_included = wavecal_data_json["weights"] is not None
|
|
1302
|
+
quality_task.write(
|
|
1303
|
+
data=wavecal_data_json,
|
|
1304
|
+
tags=[Tag.quality(MetricCode.wavecal_fit)],
|
|
1305
|
+
encoder=json_encoder,
|
|
1306
|
+
allow_nan=False,
|
|
1307
|
+
cls=QualityValueEncoder,
|
|
1308
|
+
)
|
|
1309
|
+
metric = quality_task.quality_build_wavecal_results()
|
|
1310
|
+
|
|
1311
|
+
assert metric["name"] == "Wavelength Calibration Results"
|
|
1312
|
+
assert metric["description"] == (
|
|
1313
|
+
"These plots show the wavelength solution computed based on fits to a Solar FTS atlas. "
|
|
1314
|
+
"The top plot shows the input and best-fit spectra along with the best-fit atlas, which is "
|
|
1315
|
+
"a combination of Solar and Telluric spectra. The bottom plot shows the fit residuals."
|
|
1316
|
+
)
|
|
1317
|
+
assert metric["metric_code"] == MetricCode.wavecal_fit.value
|
|
1318
|
+
assert metric["facet"] is None
|
|
1319
|
+
assert metric["statement"] is None
|
|
1320
|
+
assert metric["plot_data"] is None
|
|
1321
|
+
assert metric["histogram_data"] is None
|
|
1322
|
+
assert metric["table_data"] is None
|
|
1323
|
+
assert metric["modmat_data"] is None
|
|
1324
|
+
assert metric["efficiency_data"] is None
|
|
1325
|
+
assert metric["raincloud_data"] is None
|
|
1326
|
+
assert metric["warnings"] is None
|
|
1327
|
+
|
|
1328
|
+
multi_plot_data = metric["multi_plot_data"]
|
|
1329
|
+
assert multi_plot_data["match_x_axes"] is True
|
|
1330
|
+
assert multi_plot_data["no_gap"] is True
|
|
1331
|
+
assert (
|
|
1332
|
+
multi_plot_data["top_to_bottom_height_ratios"] == [1.5, 1, 1]
|
|
1333
|
+
if weights_included
|
|
1334
|
+
else [1.5, 1]
|
|
1335
|
+
)
|
|
1336
|
+
plot_list = multi_plot_data["top_to_bottom_plot_list"]
|
|
1337
|
+
assert isinstance(plot_list, list)
|
|
1338
|
+
assert len(plot_list) == 3 if weights_included else 3
|
|
1339
|
+
|
|
1340
|
+
fit_plot = plot_list[0]
|
|
1341
|
+
assert fit_plot["sort_series"] is False
|
|
1342
|
+
assert fit_plot["xlabel"] == "Wavelength [nm]"
|
|
1343
|
+
assert fit_plot["ylabel"] == "Signal"
|
|
1344
|
+
assert fit_plot["series_data"] == {
|
|
1345
|
+
"Input Spectrum": [[1001.0, 1002.0, 1003.0, 1004.0], [1.0, 1.0, 0.5, 1.0]],
|
|
1346
|
+
"Best Fit Observations": [[1001.5, 1002.6, 1003.7, 1004.8], [1.0, 1.0, 0.5, 1.0]],
|
|
1347
|
+
"Best Fit Atlas": [[1001.5, 1002.6, 1003.7, 1004.8], [1.0, 1.0, 0.4, 1.0]],
|
|
1348
|
+
}
|
|
1349
|
+
assert fit_plot["plot_kwargs"] == {
|
|
1350
|
+
"Input Spectrum": {"ls": "-", "alpha": 0.4, "ms": 0, "color": "#1E317A", "zorder": 2.1},
|
|
1351
|
+
"Best Fit Observations": {
|
|
1352
|
+
"ls": "-",
|
|
1353
|
+
"lw": 4,
|
|
1354
|
+
"alpha": 0.8,
|
|
1355
|
+
"ms": 0,
|
|
1356
|
+
"color": "#FAA61C",
|
|
1357
|
+
"zorder": 2.0,
|
|
1358
|
+
},
|
|
1359
|
+
"Best Fit Atlas": {"color": "k", "ls": "-", "ms": 0, "zorder": 2.2},
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
residuals_plot = plot_list[1]
|
|
1363
|
+
assert residuals_plot["xlabel"] == "Wavelength [nm]"
|
|
1364
|
+
assert residuals_plot["ylabel"] == r"$\frac{\mathrm{Obs - Atlas}}{\mathrm{Obs}}$"
|
|
1365
|
+
assert residuals_plot["series_data"] == {
|
|
1366
|
+
"Residuals": [[1001.5, 1002.6, 1003.7, 1004.8], [0.0, 0.0, 0.1, 0.0]]
|
|
1367
|
+
}
|
|
1368
|
+
assert residuals_plot["plot_kwargs"] == {"Residuals": {"ls": "-", "color": "k", "ms": 0}}
|
|
1369
|
+
|
|
1370
|
+
if weights_included:
|
|
1371
|
+
weights_plot = plot_list[2]
|
|
1372
|
+
assert weights_plot["xlabel"] == "Wavelength [nm]"
|
|
1373
|
+
assert weights_plot["ylabel"] == "Fit Weights"
|
|
1374
|
+
assert weights_plot["series_data"] == {
|
|
1375
|
+
"Weights": [[1001.5, 1002.6, 1003.7, 1004.8], [0.0, 1.0, 0.8, 0.0]]
|
|
1376
|
+
}
|
|
1377
|
+
assert weights_plot["plot_kwargs"] == {"Weights": {"ls": "-", "color": "k", "ms": 0}}
|
|
1378
|
+
|
|
1379
|
+
|
|
1178
1380
|
@pytest.mark.parametrize(
|
|
1179
1381
|
"bin_strs, sampled_bins, expected_bin_str, expected_sample_str",
|
|
1180
1382
|
[
|
|
@@ -1245,3 +1447,33 @@ def test_format_facet(label: str | Any, expected_result: str):
|
|
|
1245
1447
|
Then: the label is properly formatted
|
|
1246
1448
|
"""
|
|
1247
1449
|
assert QualityMixin._format_facet(label) == expected_result
|
|
1450
|
+
|
|
1451
|
+
|
|
1452
|
+
def test_validate_vertical_multi_pane_plot_model():
|
|
1453
|
+
"""
|
|
1454
|
+
Given: A `VerticalMultiPanePlot2D` model and some `Plot2D` models
|
|
1455
|
+
When: Instantiating the `VerticalMultiPanePlot2D` with various parameters
|
|
1456
|
+
Then: The `top_to_bottom_plot_ratios` property is correctly populated
|
|
1457
|
+
"""
|
|
1458
|
+
plot2d = Plot2D(xlabel="X", ylabel="Y", series_data={"Foo": [[1.0], [2.0]]})
|
|
1459
|
+
|
|
1460
|
+
# Test given ratios valid case
|
|
1461
|
+
_ = VerticalMultiPanePlot2D(
|
|
1462
|
+
top_to_bottom_plot_list=[plot2d, plot2d], top_to_bottom_height_ratios=[1.0, 2.0]
|
|
1463
|
+
)
|
|
1464
|
+
|
|
1465
|
+
# Test None ratios
|
|
1466
|
+
vertical_plots = VerticalMultiPanePlot2D(
|
|
1467
|
+
top_to_bottom_plot_list=[plot2d, plot2d], top_to_bottom_height_ratios=None
|
|
1468
|
+
)
|
|
1469
|
+
assert vertical_plots.top_to_bottom_height_ratios == [1.0, 1.0]
|
|
1470
|
+
|
|
1471
|
+
# Test invalid case
|
|
1472
|
+
with pytest.raises(
|
|
1473
|
+
ValidationError,
|
|
1474
|
+
match="The number of items in `top_to_bottom_height_ratios` list \(3\) is not "
|
|
1475
|
+
"the same as the number of plots \(2\)",
|
|
1476
|
+
):
|
|
1477
|
+
_ = VerticalMultiPanePlot2D(
|
|
1478
|
+
top_to_bottom_plot_list=[plot2d, plot2d], top_to_bottom_height_ratios=[1.0, 2.0, 3.0]
|
|
1479
|
+
)
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Tests for the workflow file system wrapper
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Callable
|
|
6
7
|
from uuid import uuid4
|
|
7
8
|
|
|
8
9
|
import pytest
|
|
9
10
|
|
|
10
|
-
from dkist_processing_common._util.scratch import _flatten_list
|
|
11
11
|
from dkist_processing_common._util.scratch import WorkflowFileSystem
|
|
12
|
+
from dkist_processing_common._util.scratch import _flatten_list
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
@pytest.fixture(
|