dkist-processing-cryonirsp 1.3.4__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.

Potentially problematic release.


This version of dkist-processing-cryonirsp might be problematic. Click here for more details.

Files changed (111) hide show
  1. changelog/.gitempty +0 -0
  2. dkist_processing_cryonirsp/__init__.py +11 -0
  3. dkist_processing_cryonirsp/config.py +12 -0
  4. dkist_processing_cryonirsp/models/__init__.py +1 -0
  5. dkist_processing_cryonirsp/models/constants.py +248 -0
  6. dkist_processing_cryonirsp/models/exposure_conditions.py +26 -0
  7. dkist_processing_cryonirsp/models/parameters.py +296 -0
  8. dkist_processing_cryonirsp/models/tags.py +168 -0
  9. dkist_processing_cryonirsp/models/task_name.py +14 -0
  10. dkist_processing_cryonirsp/parsers/__init__.py +1 -0
  11. dkist_processing_cryonirsp/parsers/cryonirsp_l0_fits_access.py +111 -0
  12. dkist_processing_cryonirsp/parsers/cryonirsp_l1_fits_access.py +30 -0
  13. dkist_processing_cryonirsp/parsers/exposure_conditions.py +163 -0
  14. dkist_processing_cryonirsp/parsers/map_repeats.py +40 -0
  15. dkist_processing_cryonirsp/parsers/measurements.py +55 -0
  16. dkist_processing_cryonirsp/parsers/modstates.py +31 -0
  17. dkist_processing_cryonirsp/parsers/optical_density_filters.py +40 -0
  18. dkist_processing_cryonirsp/parsers/polarimetric_check.py +120 -0
  19. dkist_processing_cryonirsp/parsers/scan_step.py +412 -0
  20. dkist_processing_cryonirsp/parsers/time.py +80 -0
  21. dkist_processing_cryonirsp/parsers/wavelength.py +26 -0
  22. dkist_processing_cryonirsp/tasks/__init__.py +19 -0
  23. dkist_processing_cryonirsp/tasks/assemble_movie.py +202 -0
  24. dkist_processing_cryonirsp/tasks/bad_pixel_map.py +96 -0
  25. dkist_processing_cryonirsp/tasks/beam_boundaries_base.py +279 -0
  26. dkist_processing_cryonirsp/tasks/ci_beam_boundaries.py +55 -0
  27. dkist_processing_cryonirsp/tasks/ci_science.py +169 -0
  28. dkist_processing_cryonirsp/tasks/cryonirsp_base.py +67 -0
  29. dkist_processing_cryonirsp/tasks/dark.py +98 -0
  30. dkist_processing_cryonirsp/tasks/gain.py +251 -0
  31. dkist_processing_cryonirsp/tasks/instrument_polarization.py +447 -0
  32. dkist_processing_cryonirsp/tasks/l1_output_data.py +44 -0
  33. dkist_processing_cryonirsp/tasks/linearity_correction.py +582 -0
  34. dkist_processing_cryonirsp/tasks/make_movie_frames.py +302 -0
  35. dkist_processing_cryonirsp/tasks/mixin/__init__.py +1 -0
  36. dkist_processing_cryonirsp/tasks/mixin/beam_access.py +52 -0
  37. dkist_processing_cryonirsp/tasks/mixin/corrections.py +177 -0
  38. dkist_processing_cryonirsp/tasks/mixin/intermediate_frame.py +193 -0
  39. dkist_processing_cryonirsp/tasks/mixin/linearized_frame.py +309 -0
  40. dkist_processing_cryonirsp/tasks/mixin/shift_measurements.py +297 -0
  41. dkist_processing_cryonirsp/tasks/parse.py +281 -0
  42. dkist_processing_cryonirsp/tasks/quality_metrics.py +271 -0
  43. dkist_processing_cryonirsp/tasks/science_base.py +511 -0
  44. dkist_processing_cryonirsp/tasks/sp_beam_boundaries.py +270 -0
  45. dkist_processing_cryonirsp/tasks/sp_dispersion_axis_correction.py +484 -0
  46. dkist_processing_cryonirsp/tasks/sp_geometric.py +585 -0
  47. dkist_processing_cryonirsp/tasks/sp_science.py +299 -0
  48. dkist_processing_cryonirsp/tasks/sp_solar_gain.py +475 -0
  49. dkist_processing_cryonirsp/tasks/trial_output_data.py +61 -0
  50. dkist_processing_cryonirsp/tasks/write_l1.py +1033 -0
  51. dkist_processing_cryonirsp/tests/__init__.py +1 -0
  52. dkist_processing_cryonirsp/tests/conftest.py +456 -0
  53. dkist_processing_cryonirsp/tests/header_models.py +592 -0
  54. dkist_processing_cryonirsp/tests/local_trial_workflows/__init__.py +0 -0
  55. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_cals_only.py +541 -0
  56. dkist_processing_cryonirsp/tests/local_trial_workflows/l0_to_l1.py +615 -0
  57. dkist_processing_cryonirsp/tests/local_trial_workflows/linearize_only.py +96 -0
  58. dkist_processing_cryonirsp/tests/local_trial_workflows/local_trial_helpers.py +592 -0
  59. dkist_processing_cryonirsp/tests/test_assemble_movie.py +144 -0
  60. dkist_processing_cryonirsp/tests/test_assemble_qualilty.py +517 -0
  61. dkist_processing_cryonirsp/tests/test_bad_pixel_maps.py +115 -0
  62. dkist_processing_cryonirsp/tests/test_ci_beam_boundaries.py +106 -0
  63. dkist_processing_cryonirsp/tests/test_ci_science.py +355 -0
  64. dkist_processing_cryonirsp/tests/test_corrections.py +126 -0
  65. dkist_processing_cryonirsp/tests/test_cryo_base.py +202 -0
  66. dkist_processing_cryonirsp/tests/test_cryo_constants.py +76 -0
  67. dkist_processing_cryonirsp/tests/test_dark.py +287 -0
  68. dkist_processing_cryonirsp/tests/test_gain.py +278 -0
  69. dkist_processing_cryonirsp/tests/test_instrument_polarization.py +531 -0
  70. dkist_processing_cryonirsp/tests/test_linearity_correction.py +245 -0
  71. dkist_processing_cryonirsp/tests/test_make_movie_frames.py +111 -0
  72. dkist_processing_cryonirsp/tests/test_parameters.py +266 -0
  73. dkist_processing_cryonirsp/tests/test_parse.py +1439 -0
  74. dkist_processing_cryonirsp/tests/test_quality.py +203 -0
  75. dkist_processing_cryonirsp/tests/test_sp_beam_boundaries.py +112 -0
  76. dkist_processing_cryonirsp/tests/test_sp_dispersion_axis_correction.py +155 -0
  77. dkist_processing_cryonirsp/tests/test_sp_geometric.py +319 -0
  78. dkist_processing_cryonirsp/tests/test_sp_make_movie_frames.py +121 -0
  79. dkist_processing_cryonirsp/tests/test_sp_science.py +483 -0
  80. dkist_processing_cryonirsp/tests/test_sp_solar.py +198 -0
  81. dkist_processing_cryonirsp/tests/test_trial_create_quality_report.py +79 -0
  82. dkist_processing_cryonirsp/tests/test_trial_output_data.py +251 -0
  83. dkist_processing_cryonirsp/tests/test_workflows.py +9 -0
  84. dkist_processing_cryonirsp/tests/test_write_l1.py +436 -0
  85. dkist_processing_cryonirsp/workflows/__init__.py +2 -0
  86. dkist_processing_cryonirsp/workflows/ci_l0_processing.py +77 -0
  87. dkist_processing_cryonirsp/workflows/sp_l0_processing.py +84 -0
  88. dkist_processing_cryonirsp/workflows/trial_workflows.py +190 -0
  89. dkist_processing_cryonirsp-1.3.4.dist-info/METADATA +194 -0
  90. dkist_processing_cryonirsp-1.3.4.dist-info/RECORD +111 -0
  91. dkist_processing_cryonirsp-1.3.4.dist-info/WHEEL +5 -0
  92. dkist_processing_cryonirsp-1.3.4.dist-info/top_level.txt +4 -0
  93. docs/Makefile +134 -0
  94. docs/bad_pixel_calibration.rst +47 -0
  95. docs/beam_angle_calculation.rst +53 -0
  96. docs/beam_boundary_computation.rst +88 -0
  97. docs/changelog.rst +7 -0
  98. docs/ci_science_calibration.rst +33 -0
  99. docs/conf.py +52 -0
  100. docs/index.rst +21 -0
  101. docs/l0_to_l1_cryonirsp_ci-full-trial.rst +10 -0
  102. docs/l0_to_l1_cryonirsp_ci.rst +10 -0
  103. docs/l0_to_l1_cryonirsp_sp-full-trial.rst +10 -0
  104. docs/l0_to_l1_cryonirsp_sp.rst +10 -0
  105. docs/linearization.rst +43 -0
  106. docs/make.bat +170 -0
  107. docs/requirements.txt +1 -0
  108. docs/requirements_table.rst +8 -0
  109. docs/scientific_changelog.rst +10 -0
  110. docs/sp_science_calibration.rst +59 -0
  111. licenses/LICENSE.rst +11 -0
@@ -0,0 +1,115 @@
1
+ from datetime import datetime
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from astropy.io import fits
6
+ from dkist_header_validator import spec122_validator
7
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
8
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
9
+ from dkist_processing_common.tests.conftest import FakeGQLClient
10
+ from dkist_service_configuration.logging import logger
11
+
12
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
13
+ from dkist_processing_cryonirsp.tasks.bad_pixel_map import BadPixelMapCalibration
14
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
15
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
16
+ from dkist_processing_cryonirsp.tests.conftest import generate_fits_frame
17
+ from dkist_processing_cryonirsp.tests.header_models import CryonirspHeadersValidCISolarGainFrames
18
+
19
+
20
+ @pytest.fixture(scope="function", params=["CI", "SP"])
21
+ def compute_bad_pixel_map_task(
22
+ tmp_path,
23
+ recipe_run_id,
24
+ assign_input_dataset_doc_to_task,
25
+ init_cryonirsp_constants_db,
26
+ request,
27
+ ):
28
+ arm_id = request.param
29
+ if arm_id == "SP":
30
+ dataset_shape = (1, 100, 200)
31
+ array_shape = (1, 100, 200)
32
+ else:
33
+ dataset_shape = (1, 100, 100)
34
+ array_shape = (1, 100, 100)
35
+ constants_db = CryonirspConstantsDb(ARM_ID=arm_id)
36
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
37
+ with BadPixelMapCalibration(
38
+ recipe_run_id=recipe_run_id,
39
+ workflow_name="sp_compute_bad_pixel_map",
40
+ workflow_version="VX.Y",
41
+ ) as task:
42
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
43
+ task.scratch = WorkflowFileSystem(
44
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
45
+ )
46
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
47
+ assign_input_dataset_doc_to_task(task, param_class())
48
+ start_time = datetime.now()
49
+ ds = CryonirspHeadersValidCISolarGainFrames(
50
+ dataset_shape=dataset_shape,
51
+ array_shape=array_shape,
52
+ time_delta=10,
53
+ start_time=start_time,
54
+ )
55
+ header_generator = (
56
+ spec122_validator.validate_and_translate_to_214_l0(
57
+ d.header(), return_type=fits.HDUList
58
+ )[0].header
59
+ for d in ds
60
+ )
61
+ hdul = generate_fits_frame(header_generator=header_generator, shape=array_shape)
62
+ # Create an array with a random number of zero and hot values
63
+ rng = np.random.default_rng()
64
+ # Generate rabdomly the number of bad pixels to be used in trange(50, 100)
65
+ num_bad_pixels = rng.integers(50, 100)
66
+ # Let 2/3 of the bad pixels be hot
67
+ num_hot_pixels = num_bad_pixels * 2 // 3
68
+ # Let the remaining 1/3 be zero
69
+ num_zero_pixels = num_bad_pixels - num_hot_pixels
70
+ logger.debug(f"{num_bad_pixels = }, {num_hot_pixels = }, {num_zero_pixels = }")
71
+ nelem = np.prod(array_shape)
72
+ array = 1000.0 * np.ones(nelem)
73
+ # Need choice here with replace=False to avoid generating duplicates
74
+ bad_pixel_locs = rng.choice(nelem, size=num_bad_pixels, replace=False)
75
+ hot_pixel_locs = sorted(bad_pixel_locs[:num_hot_pixels])
76
+ zero_pixel_locs = sorted(bad_pixel_locs[num_hot_pixels:])
77
+ array[zero_pixel_locs] = 0.0
78
+ array[hot_pixel_locs] = 2000.0
79
+ array = array.reshape(array_shape[1:])
80
+ hdul[0].data = array
81
+ task.write(
82
+ data=hdul,
83
+ tags=[
84
+ CryonirspTag.linearized(),
85
+ CryonirspTag.task_solar_gain(),
86
+ CryonirspTag.frame(),
87
+ ],
88
+ encoder=fits_hdulist_encoder,
89
+ )
90
+ yield task, num_zero_pixels, num_hot_pixels
91
+ finally:
92
+ task._purge()
93
+
94
+
95
+ def test_compute_bad_pixel_map_task(compute_bad_pixel_map_task, mocker):
96
+ """
97
+ Given: An BadPixelMapCalibration task
98
+ When: Calling the task instance with known input data
99
+ Then: The correct beam boundary values are created and saved as intermediate files
100
+ """
101
+ mocker.patch(
102
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
103
+ )
104
+ # Given
105
+ task, num_zeros, num_hot = compute_bad_pixel_map_task
106
+ # When
107
+ task()
108
+ # Then
109
+ tags = [CryonirspTag.task_bad_pixel_map()]
110
+ bad_pixel_map_paths = list(task.read(tags))
111
+ assert len(bad_pixel_map_paths) == 1
112
+ bad_pixel_map_hdul = fits.open(bad_pixel_map_paths[0])
113
+ bad_pixel_map = bad_pixel_map_hdul[0].data
114
+ num_bad_pixels = bad_pixel_map.sum()
115
+ assert num_bad_pixels == num_zeros + num_hot
@@ -0,0 +1,106 @@
1
+ from datetime import datetime
2
+
3
+ import numpy as np
4
+ import pytest
5
+ from astropy.io import fits
6
+ from dkist_header_validator import spec122_validator
7
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
8
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
9
+ from dkist_processing_common.tests.conftest import FakeGQLClient
10
+
11
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
12
+ from dkist_processing_cryonirsp.tasks.ci_beam_boundaries import CIBeamBoundariesCalibration
13
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
14
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
15
+ from dkist_processing_cryonirsp.tests.conftest import generate_fits_frame
16
+ from dkist_processing_cryonirsp.tests.header_models import CryonirspHeadersValidCISolarGainFrames
17
+
18
+
19
+ @pytest.fixture(scope="function")
20
+ def compute_beam_boundaries_task(
21
+ tmp_path,
22
+ recipe_run_id,
23
+ assign_input_dataset_doc_to_task,
24
+ init_cryonirsp_constants_db,
25
+ ):
26
+ arm_id = "CI"
27
+ dataset_shape = (1, 100, 100)
28
+ array_shape = (1, 100, 100)
29
+ constants_db = CryonirspConstantsDb(ARM_ID=arm_id)
30
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
31
+ with CIBeamBoundariesCalibration(
32
+ recipe_run_id=recipe_run_id,
33
+ workflow_name="ci_compute_beam_boundaries",
34
+ workflow_version="VX.Y",
35
+ ) as task:
36
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
37
+ task.scratch = WorkflowFileSystem(
38
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
39
+ )
40
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
41
+ assign_input_dataset_doc_to_task(task, param_class())
42
+ # Create fake bad pixel map
43
+ task.intermediate_frame_write_arrays(
44
+ arrays=np.zeros(array_shape[1:]),
45
+ task_tag=CryonirspTag.task_bad_pixel_map(),
46
+ )
47
+ start_time = datetime.now()
48
+ ds = CryonirspHeadersValidCISolarGainFrames(
49
+ dataset_shape=dataset_shape,
50
+ array_shape=array_shape,
51
+ time_delta=10,
52
+ start_time=start_time,
53
+ )
54
+ header_generator = (
55
+ spec122_validator.validate_and_translate_to_214_l0(
56
+ d.header(), return_type=fits.HDUList
57
+ )[0].header
58
+ for d in ds
59
+ )
60
+ hdul = generate_fits_frame(header_generator=header_generator, shape=array_shape)
61
+ # Tweak data to form a beam illumination pattern
62
+ # Data from generate_fits_frame are value 150
63
+ array = hdul[0].data
64
+ # Initial illumination borders that are made up. Precise border depends on the algorithm.
65
+ # [0:0, y_min:y_max, x_min:x_max]
66
+ array[:, 7:-5, 3:-8] = 1000.0
67
+ # Put some large vertical streaks in the image to help the shift measurement converge
68
+ minus_streak_pos = array_shape[2] // 4
69
+ plus_streak_pos = 3 * array_shape[2] // 4
70
+ array[:, :, minus_streak_pos - 5 : minus_streak_pos + 5] += 100
71
+ array[:, :, plus_streak_pos - 5 : plus_streak_pos + 5] += 100
72
+ hdul[0].data = array
73
+ task.write(
74
+ data=hdul,
75
+ tags=[
76
+ CryonirspTag.linearized(),
77
+ CryonirspTag.task_solar_gain(),
78
+ CryonirspTag.frame(),
79
+ ],
80
+ encoder=fits_hdulist_encoder,
81
+ )
82
+ yield task, arm_id
83
+ finally:
84
+ task._purge()
85
+
86
+
87
+ def test_compute_beam_boundaries_task(compute_beam_boundaries_task, mocker):
88
+ """
89
+ Given: A CIBeamBoundariesCalibration task
90
+ When: Calling the task instance with known input data
91
+ Then: The correct beam boundary values are created and saved as intermediate files
92
+ """
93
+ mocker.patch(
94
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
95
+ )
96
+ # Given
97
+ task, arm_id = compute_beam_boundaries_task
98
+ # When
99
+ task()
100
+ # Then
101
+ beam_1_tags = [CryonirspTag.task_beam_boundaries(), CryonirspTag.beam(1)]
102
+ beam_1_boundary = np.array([8, 94, 4, 91])
103
+ files_found = list(task.read(tags=beam_1_tags))
104
+ assert len(files_found) == 1
105
+ array = fits.open(files_found[0])[0].data
106
+ assert np.array_equal(array, beam_1_boundary)
@@ -0,0 +1,355 @@
1
+ import json
2
+ import random
3
+ from datetime import datetime
4
+
5
+ import numpy as np
6
+ import pytest
7
+ from astropy.io import fits
8
+ from astropy.time import Time
9
+ from astropy.time import TimeDelta
10
+ from dkist_header_validator import spec122_validator
11
+ from dkist_processing_common._util.scratch import WorkflowFileSystem
12
+ from dkist_processing_common.codecs.fits import fits_hdulist_encoder
13
+ from dkist_processing_common.tests.conftest import FakeGQLClient
14
+
15
+ from dkist_processing_cryonirsp.models.exposure_conditions import AllowableOpticalDensityFilterNames
16
+ from dkist_processing_cryonirsp.models.exposure_conditions import ExposureConditions
17
+ from dkist_processing_cryonirsp.models.tags import CryonirspStemName
18
+ from dkist_processing_cryonirsp.models.tags import CryonirspTag
19
+ from dkist_processing_cryonirsp.tasks.ci_science import CIScienceCalibration
20
+ from dkist_processing_cryonirsp.tests.conftest import cryonirsp_testing_parameters_factory
21
+ from dkist_processing_cryonirsp.tests.conftest import CryonirspConstantsDb
22
+ from dkist_processing_cryonirsp.tests.conftest import generate_fits_frame
23
+ from dkist_processing_cryonirsp.tests.header_models import CryonirspHeadersValidObserveFrames
24
+
25
+ # from dkist_processing_common.models.tags import Tag
26
+
27
+
28
+ @pytest.fixture(scope="function", params=["Full Stokes", "Stokes-I"])
29
+ def ci_science_calibration_task(
30
+ tmp_path,
31
+ recipe_run_id,
32
+ assign_input_dataset_doc_to_task,
33
+ init_cryonirsp_constants_db,
34
+ request,
35
+ ):
36
+ num_map_scans = 2
37
+ num_scan_steps = 2
38
+ num_meas = 1
39
+ exposure_time = 0.02 # From CryonirspHeadersValidObserveFrames fixture
40
+ exposure_conditions = ExposureConditions(
41
+ exposure_time, AllowableOpticalDensityFilterNames.OPEN.value
42
+ )
43
+ if request.param == "Full Stokes":
44
+ num_modstates = 2
45
+ else:
46
+ num_modstates = 1
47
+ array_shape = (1, 20, 20)
48
+ intermediate_shape = array_shape[1:]
49
+ dataset_shape = (num_map_scans * num_scan_steps * num_modstates,) + array_shape[1:]
50
+
51
+ constants_db = CryonirspConstantsDb(
52
+ ARM_ID="CI",
53
+ NUM_MODSTATES=num_modstates,
54
+ NUM_MAP_SCANS=num_map_scans,
55
+ NUM_SCAN_STEPS=num_scan_steps,
56
+ NUM_BEAMS=1,
57
+ OBSERVE_EXPOSURE_CONDITIONS_LIST=(exposure_conditions,),
58
+ MODULATOR_SPIN_MODE="Continuous" if request.param == "Full Stokes" else "Off",
59
+ )
60
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
61
+ with CIScienceCalibration(
62
+ recipe_run_id=recipe_run_id, workflow_name="ci_science_calibration", workflow_version="VX.Y"
63
+ ) as task:
64
+ try: # This try... block is here to make sure the dbs get cleaned up if there's a failure in the fixture
65
+ all_zeros = np.zeros(intermediate_shape)
66
+ all_ones = np.ones(intermediate_shape)
67
+
68
+ task.scratch = WorkflowFileSystem(
69
+ scratch_base_path=tmp_path, recipe_run_id=recipe_run_id
70
+ )
71
+
72
+ param_class = cryonirsp_testing_parameters_factory(param_path=tmp_path)
73
+ assign_input_dataset_doc_to_task(task, param_class())
74
+
75
+ # Need a beam boundary file
76
+ task.intermediate_frame_write_arrays(
77
+ arrays=np.array([0, intermediate_shape[0], 0, intermediate_shape[1]]),
78
+ task_tag=CryonirspTag.task_beam_boundaries(),
79
+ beam=1,
80
+ )
81
+ # Create fake bad pixel map
82
+ task.intermediate_frame_write_arrays(
83
+ arrays=np.zeros(array_shape[1:]), task_tag=CryonirspTag.task_bad_pixel_map()
84
+ )
85
+
86
+ # Create fake demodulation matrices
87
+ demod_matrices = np.zeros((1, 1, 4, num_modstates))
88
+ for modstate in range(num_modstates):
89
+ demod_matrices[0, 0, :, modstate] = [1, 2, 3, 4]
90
+ demod_hdul = fits.HDUList([fits.PrimaryHDU(data=demod_matrices)])
91
+ task.write(
92
+ data=demod_hdul,
93
+ tags=[
94
+ CryonirspTag.intermediate(),
95
+ CryonirspTag.frame(),
96
+ CryonirspTag.task_demodulation_matrices(),
97
+ CryonirspTag.beam(1),
98
+ ],
99
+ encoder=fits_hdulist_encoder,
100
+ )
101
+
102
+ # Create fake dark intermediate arrays
103
+ task.intermediate_frame_write_arrays(
104
+ all_zeros,
105
+ beam=1,
106
+ task_tag=CryonirspTag.task_dark(),
107
+ exposure_conditions=exposure_conditions,
108
+ )
109
+
110
+ # Create fake lamp and solar gain intermediate arrays
111
+ for modstate in range(1, num_modstates + 1):
112
+ gain_hdul = fits.HDUList([fits.PrimaryHDU(data=all_ones)])
113
+ task.write(
114
+ data=gain_hdul,
115
+ tags=[
116
+ CryonirspTag.intermediate(),
117
+ CryonirspTag.frame(),
118
+ CryonirspTag.task_lamp_gain(),
119
+ CryonirspTag.beam(1),
120
+ CryonirspTag.modstate(modstate),
121
+ ],
122
+ encoder=fits_hdulist_encoder,
123
+ )
124
+ task.write(
125
+ data=gain_hdul,
126
+ tags=[
127
+ CryonirspTag.intermediate(),
128
+ CryonirspTag.frame(),
129
+ CryonirspTag.task_solar_gain(),
130
+ CryonirspTag.beam(1),
131
+ CryonirspTag.modstate(modstate),
132
+ ],
133
+ encoder=fits_hdulist_encoder,
134
+ )
135
+
136
+ # Create fake observe arrays
137
+ start_time = datetime.now()
138
+ for map_scan in range(1, num_map_scans + 1):
139
+ for scan_step in range(1, num_scan_steps + 1):
140
+ for modstate in range(1, num_modstates + 1):
141
+ for meas_num in range(1, num_meas + 1):
142
+ ds = CryonirspHeadersValidObserveFrames(
143
+ dataset_shape=dataset_shape,
144
+ array_shape=array_shape,
145
+ time_delta=10,
146
+ scan_step=scan_step,
147
+ num_scan_steps=num_scan_steps,
148
+ num_map_scans=num_map_scans,
149
+ map_scan=map_scan,
150
+ num_modstates=num_modstates,
151
+ modstate=modstate,
152
+ start_time=start_time,
153
+ num_meas=num_meas,
154
+ meas_num=meas_num,
155
+ arm_id="CI",
156
+ )
157
+ header_generator = (
158
+ spec122_validator.validate_and_translate_to_214_l0(
159
+ d.header(), return_type=fits.HDUList
160
+ )[0].header
161
+ for d in ds
162
+ )
163
+
164
+ hdul = generate_fits_frame(
165
+ header_generator=header_generator, shape=array_shape
166
+ )
167
+ header = hdul[0].header
168
+ task.write(
169
+ data=hdul,
170
+ tags=[
171
+ CryonirspTag.task_observe(),
172
+ CryonirspTag.scan_step(scan_step),
173
+ CryonirspTag.map_scan(map_scan),
174
+ CryonirspTag.modstate(modstate),
175
+ CryonirspTag.linearized(),
176
+ CryonirspTag.frame(),
177
+ CryonirspTag.exposure_conditions(exposure_conditions),
178
+ CryonirspTag.meas_num(meas_num),
179
+ ],
180
+ encoder=fits_hdulist_encoder,
181
+ )
182
+ yield task, request.param, header, intermediate_shape
183
+ finally:
184
+ task._purge()
185
+
186
+
187
+ @pytest.fixture(scope="session")
188
+ def ci_headers_with_dates() -> tuple[list[fits.Header], str, int, int]:
189
+ num_headers = 5
190
+ start_time = "1969-12-06T18:00:00"
191
+ exp_time = 12
192
+ time_delta = 10
193
+ ds = CryonirspHeadersValidObserveFrames(
194
+ dataset_shape=(num_headers, 4, 4),
195
+ array_shape=(1, 4, 4),
196
+ time_delta=time_delta,
197
+ num_map_scans=1,
198
+ map_scan=1,
199
+ num_scan_steps=1,
200
+ scan_step=1,
201
+ num_meas=1,
202
+ meas_num=1,
203
+ num_modstates=num_headers,
204
+ modstate=1,
205
+ start_time=datetime.fromisoformat(start_time),
206
+ arm_id="CI",
207
+ )
208
+ headers = [
209
+ spec122_validator.validate_and_translate_to_214_l0(d.header(), return_type=fits.HDUList)[
210
+ 0
211
+ ].header
212
+ for d in ds
213
+ ]
214
+ random.shuffle(headers) # Shuffle to make sure they're not already in time order
215
+ for h in headers:
216
+ h["XPOSURE"] = exp_time # Exposure time, in ms
217
+
218
+ return headers, start_time, exp_time, time_delta
219
+
220
+
221
+ @pytest.fixture(scope="session")
222
+ def ci_compressed_headers_with_dates(
223
+ ci_headers_with_dates,
224
+ ) -> tuple[list[fits.Header], str, int, int]:
225
+ headers, start_time, exp_time, time_delta = ci_headers_with_dates
226
+ comp_headers = [fits.hdu.compressed.CompImageHeader(h, h) for h in headers]
227
+ return comp_headers, start_time, exp_time, time_delta
228
+
229
+
230
+ def test_ci_science_calibration_task(ci_science_calibration_task, mocker):
231
+ """
232
+ Given: A CIScienceCalibration task
233
+ When: Calling the task instance
234
+ Then: There are the expected number of science frames with the correct tags applied and the headers have been correctly updated
235
+ """
236
+
237
+ mocker.patch(
238
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
239
+ )
240
+
241
+ # When
242
+ task, polarization_mode, og_header, og_single_beam_shape = ci_science_calibration_task
243
+ task()
244
+
245
+ # 1 from re-dummification
246
+ expected_final_shape = (1, og_single_beam_shape[0], og_single_beam_shape[1])
247
+
248
+ # Then
249
+ tags = [
250
+ CryonirspTag.calibrated(),
251
+ CryonirspTag.frame(),
252
+ ]
253
+ files = list(task.read(tags=tags))
254
+ if polarization_mode == "Full Stokes":
255
+ # 2 scan steps * 2 map scans * 4 stokes params = 16 frames
256
+ assert len(files) == 16
257
+ elif polarization_mode == "Stokes-I":
258
+ # 2 scan steps * 2 map scans * 1 stokes param = 4 frames
259
+ assert len(files) == 4
260
+ for file in files:
261
+ hdul = fits.open(file)
262
+ assert len(hdul) == 1
263
+ hdu = hdul[0]
264
+ assert type(hdul[0]) is fits.PrimaryHDU
265
+ assert hdu.data.shape == expected_final_shape
266
+ assert "DATE-BEG" in hdu.header.keys()
267
+ assert "DATE-END" in hdu.header.keys()
268
+ if polarization_mode == "Full Stokes":
269
+ assert "POL_NOIS" in hdu.header.keys()
270
+ assert "POL_SENS" in hdu.header.keys()
271
+
272
+ # Check that scan step keys were updated
273
+ scan_step = [
274
+ int(t.split("_")[-1]) for t in task.tags(file) if CryonirspStemName.scan_step.value in t
275
+ ][0]
276
+
277
+ assert hdu.header["CNNUMSCN"] == 2
278
+ assert hdu.header["CNCURSCN"] == scan_step
279
+
280
+ quality_files = task.read(tags=[CryonirspTag.quality("TASK_TYPES")])
281
+ for file in quality_files:
282
+ with file.open() as f:
283
+ data = json.load(f)
284
+ assert isinstance(data, dict)
285
+ assert data["total_frames"] == task.scratch.count_all(
286
+ tags=[CryonirspTag.linearized(), CryonirspTag.frame(), CryonirspTag.task_observe()]
287
+ )
288
+
289
+
290
+ def test_compute_ci_date_keys(ci_headers_with_dates, recipe_run_id, init_cryonirsp_constants_db):
291
+ """
292
+ Given: A set of CI headers with different DATE-OBS values
293
+ When: Computing the time over which the headers were taken
294
+ Then: A header with correct DATE-BEG, DATE-END, and DATE-AVG keys is returned
295
+ """
296
+ headers, start_time, exp_time, time_delta = ci_headers_with_dates
297
+ constants_db = CryonirspConstantsDb()
298
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
299
+ with CIScienceCalibration(
300
+ recipe_run_id=recipe_run_id, workflow_name="science_calibration", workflow_version="VX.Y"
301
+ ) as task:
302
+ final_header = task.compute_date_keys(headers)
303
+ final_header_from_single = task.compute_date_keys(headers[0])
304
+
305
+ date_end = (
306
+ Time(start_time)
307
+ + (len(headers) - 1) * TimeDelta(time_delta, format="sec")
308
+ + TimeDelta(exp_time / 1000.0, format="sec")
309
+ ).isot
310
+
311
+ assert final_header["DATE-BEG"] == start_time
312
+ assert final_header["DATE-END"] == date_end
313
+
314
+ date_end_from_single = (
315
+ Time(headers[0]["DATE-BEG"])
316
+ # + TimeDelta(time_delta, format="sec")
317
+ + TimeDelta(exp_time / 1000.0, format="sec")
318
+ ).isot
319
+
320
+ assert final_header_from_single["DATE-BEG"] == headers[0]["DATE-BEG"]
321
+ assert final_header_from_single["DATE-END"] == date_end_from_single
322
+
323
+
324
+ def test_compute_ci_date_keys_compressed_headers(
325
+ ci_compressed_headers_with_dates, recipe_run_id, init_cryonirsp_constants_db
326
+ ):
327
+ """
328
+ Given: A set of CI compressed headers with different DATE-OBS values
329
+ When: Computing the time over which the headers were taken
330
+ Then: A header with correct DATE-BEG, DATE-END, and DATE-AVG keys is returned
331
+ """
332
+ headers, start_time, exp_time, time_delta = ci_compressed_headers_with_dates
333
+ constants_db = CryonirspConstantsDb()
334
+ init_cryonirsp_constants_db(recipe_run_id, constants_db)
335
+ with CIScienceCalibration(
336
+ recipe_run_id=recipe_run_id, workflow_name="science_calibration", workflow_version="VX.Y"
337
+ ) as task:
338
+ final_header = task.compute_date_keys(headers)
339
+ final_header_from_single = task.compute_date_keys(headers[0])
340
+
341
+ date_end = (
342
+ Time(start_time)
343
+ + (len(headers) - 1) * TimeDelta(time_delta, format="sec")
344
+ + TimeDelta(exp_time / 1000.0, format="sec")
345
+ ).isot
346
+
347
+ assert final_header["DATE-BEG"] == start_time
348
+ assert final_header["DATE-END"] == date_end
349
+
350
+ date_end_from_single = (
351
+ Time(headers[0]["DATE-BEG"]) + TimeDelta(exp_time / 1000.0, format="sec")
352
+ ).isot
353
+
354
+ assert final_header_from_single["DATE-BEG"] == headers[0]["DATE-BEG"]
355
+ assert final_header_from_single["DATE-END"] == date_end_from_single