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.
Files changed (73) hide show
  1. dkist_processing_visp/__init__.py +1 -0
  2. dkist_processing_visp/config.py +1 -0
  3. dkist_processing_visp/models/constants.py +61 -20
  4. dkist_processing_visp/models/fits_access.py +20 -0
  5. dkist_processing_visp/models/metric_code.py +10 -0
  6. dkist_processing_visp/models/parameters.py +129 -24
  7. dkist_processing_visp/models/tags.py +22 -1
  8. dkist_processing_visp/models/task_name.py +1 -0
  9. dkist_processing_visp/parsers/map_repeats.py +1 -0
  10. dkist_processing_visp/parsers/modulator_states.py +1 -0
  11. dkist_processing_visp/parsers/polarimeter_mode.py +4 -2
  12. dkist_processing_visp/parsers/raster_step.py +4 -1
  13. dkist_processing_visp/parsers/spectrograph_configuration.py +75 -0
  14. dkist_processing_visp/parsers/time.py +24 -14
  15. dkist_processing_visp/parsers/visp_l0_fits_access.py +19 -8
  16. dkist_processing_visp/parsers/visp_l1_fits_access.py +1 -0
  17. dkist_processing_visp/tasks/__init__.py +1 -0
  18. dkist_processing_visp/tasks/assemble_movie.py +1 -0
  19. dkist_processing_visp/tasks/background_light.py +2 -1
  20. dkist_processing_visp/tasks/dark.py +5 -4
  21. dkist_processing_visp/tasks/geometric.py +132 -20
  22. dkist_processing_visp/tasks/instrument_polarization.py +128 -18
  23. dkist_processing_visp/tasks/l1_output_data.py +203 -0
  24. dkist_processing_visp/tasks/lamp.py +53 -93
  25. dkist_processing_visp/tasks/make_movie_frames.py +8 -6
  26. dkist_processing_visp/tasks/mixin/beam_access.py +1 -0
  27. dkist_processing_visp/tasks/mixin/corrections.py +54 -4
  28. dkist_processing_visp/tasks/mixin/downsample.py +1 -0
  29. dkist_processing_visp/tasks/parse.py +50 -17
  30. dkist_processing_visp/tasks/quality_metrics.py +5 -4
  31. dkist_processing_visp/tasks/science.py +126 -46
  32. dkist_processing_visp/tasks/solar.py +896 -456
  33. dkist_processing_visp/tasks/visp_base.py +4 -3
  34. dkist_processing_visp/tasks/write_l1.py +38 -10
  35. dkist_processing_visp/tests/conftest.py +145 -47
  36. dkist_processing_visp/tests/header_models.py +157 -20
  37. dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +21 -78
  38. dkist_processing_visp/tests/local_trial_workflows/l0_polcals_as_science.py +421 -0
  39. dkist_processing_visp/tests/local_trial_workflows/l0_solar_gain_as_science.py +387 -0
  40. dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +18 -75
  41. dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +346 -14
  42. dkist_processing_visp/tests/test_assemble_movie.py +2 -3
  43. dkist_processing_visp/tests/test_assemble_quality.py +89 -4
  44. dkist_processing_visp/tests/test_background_light.py +51 -44
  45. dkist_processing_visp/tests/test_dark.py +4 -3
  46. dkist_processing_visp/tests/test_downsample.py +1 -0
  47. dkist_processing_visp/tests/test_fits_access.py +43 -0
  48. dkist_processing_visp/tests/test_geometric.py +45 -4
  49. dkist_processing_visp/tests/test_instrument_polarization.py +72 -9
  50. dkist_processing_visp/tests/test_lamp.py +22 -26
  51. dkist_processing_visp/tests/test_make_movie_frames.py +4 -4
  52. dkist_processing_visp/tests/test_map_repeats.py +3 -1
  53. dkist_processing_visp/tests/test_parameters.py +122 -21
  54. dkist_processing_visp/tests/test_parse.py +164 -18
  55. dkist_processing_visp/tests/test_quality.py +3 -4
  56. dkist_processing_visp/tests/test_science.py +113 -15
  57. dkist_processing_visp/tests/test_solar.py +318 -99
  58. dkist_processing_visp/tests/test_visp_constants.py +38 -8
  59. dkist_processing_visp/tests/test_workflows.py +1 -0
  60. dkist_processing_visp/tests/test_write_l1.py +22 -3
  61. dkist_processing_visp/workflows/__init__.py +1 -0
  62. dkist_processing_visp/workflows/l0_processing.py +10 -3
  63. dkist_processing_visp/workflows/trial_workflows.py +8 -2
  64. dkist_processing_visp-5.1.1.dist-info/METADATA +552 -0
  65. dkist_processing_visp-5.1.1.dist-info/RECORD +94 -0
  66. {dkist_processing_visp-2.20.14.dist-info → dkist_processing_visp-5.1.1.dist-info}/WHEEL +1 -1
  67. docs/conf.py +5 -1
  68. docs/gain_correction.rst +52 -44
  69. docs/science_calibration.rst +7 -0
  70. dkist_processing_visp/tasks/mixin/line_zones.py +0 -115
  71. dkist_processing_visp-2.20.14.dist-info/METADATA +0 -196
  72. dkist_processing_visp-2.20.14.dist-info/RECORD +0 -89
  73. {dkist_processing_visp-2.20.14.dist-info → dkist_processing_visp-5.1.1.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,6 @@ from dkist_processing_common._util.scratch import WorkflowFileSystem
6
6
  from dkist_processing_common.codecs.fits import fits_array_encoder
7
7
  from dkist_processing_common.models.tags import Tag
8
8
  from dkist_processing_common.models.task_name import TaskName
9
- from dkist_processing_common.tests.conftest import FakeGQLClient
10
9
 
11
10
  from dkist_processing_visp.models.tags import VispTag
12
11
  from dkist_processing_visp.tasks.quality_metrics import VispL0QualityMetrics
@@ -92,7 +91,7 @@ def visp_l1_quality_task(tmp_path, pol_mode, recipe_run_id, init_visp_constants_
92
91
  with VispL1QualityMetrics(
93
92
  recipe_run_id=recipe_run_id, workflow_name="science_calibration", workflow_version="VX.Y"
94
93
  ) as task:
95
- task.scratch = WorkflowFileSystem(scratch_base_path=tmp_path)
94
+ task.scratch = WorkflowFileSystem(scratch_base_path=tmp_path, recipe_run_id=recipe_run_id)
96
95
 
97
96
  yield task, num_map_scans, num_raster_steps, num_stokes
98
97
  task._purge()
@@ -132,7 +131,7 @@ def test_l0_quality_task(
132
131
 
133
132
 
134
133
  @pytest.mark.parametrize("pol_mode", ["observe_polarimetric", "observe_intensity"])
135
- def test_l1_quality_task(visp_l1_quality_task, pol_mode, mocker):
134
+ def test_l1_quality_task(visp_l1_quality_task, pol_mode, mocker, fake_gql_client):
136
135
  """
137
136
  Given: A VispL1QualityMetrics task
138
137
  When: Calling the task instance
@@ -140,7 +139,7 @@ def test_l1_quality_task(visp_l1_quality_task, pol_mode, mocker):
140
139
  and a single noise measurement and datetime is recorded for L1 file for each Stokes Q, U, and V
141
140
  """
142
141
  mocker.patch(
143
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
142
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
144
143
  )
145
144
  # When
146
145
  task, num_maps, num_steps, num_stokes = visp_l1_quality_task
@@ -12,8 +12,8 @@ from dkist_header_validator import spec122_validator
12
12
  from dkist_processing_common._util.scratch import WorkflowFileSystem
13
13
  from dkist_processing_common.codecs.fits import fits_array_encoder
14
14
  from dkist_processing_common.codecs.fits import fits_hdu_decoder
15
+ from dkist_processing_common.models.fits_access import MetadataKey
15
16
  from dkist_processing_common.models.tags import Tag
16
- from dkist_processing_common.tests.conftest import FakeGQLClient
17
17
 
18
18
  from dkist_processing_visp.models.tags import VispStemName
19
19
  from dkist_processing_visp.models.tags import VispTag
@@ -144,7 +144,7 @@ def dummy_calibration_collection():
144
144
 
145
145
  dark_dict = {VispTag.beam(beam): {VispTag.readout_exp_time(0.04): np.zeros(intermediate_shape)}}
146
146
  background_dict = {VispTag.beam(beam): np.zeros(intermediate_shape)}
147
- solar_dict = {VispTag.beam(beam): {VispTag.modstate(modstate): np.ones(intermediate_shape)}}
147
+ solar_dict = {VispTag.beam(beam): np.ones(intermediate_shape)}
148
148
  angle_dict = {VispTag.beam(beam): 0.0}
149
149
  spec_dict = {VispTag.beam(beam): np.zeros(intermediate_shape[1])}
150
150
  offset_dict = {VispTag.beam(beam): {VispTag.modstate(modstate): np.zeros(2)}}
@@ -184,7 +184,7 @@ def headers_with_dates() -> tuple[list[fits.Header], str, int, int]:
184
184
  ]
185
185
  random.shuffle(headers) # Shuffle to make sure they're not already in time order
186
186
  for h in headers:
187
- h["XPOSURE"] = exp_time # Exposure time, in ms
187
+ h[MetadataKey.fpa_exposure_time_ms] = exp_time # Exposure time, in ms
188
188
 
189
189
  return headers, start_time, exp_time, time_delta
190
190
 
@@ -252,10 +252,14 @@ def calibration_collection_with_full_overlap_slice() -> CalibrationCollection:
252
252
 
253
253
  @pytest.mark.parametrize(
254
254
  "background_on",
255
- [pytest.param(True, id="Background on"), pytest.param(False, id="Background off")],
255
+ [pytest.param(True, id="background_on"), pytest.param(False, id="background_off")],
256
256
  )
257
257
  def test_science_calibration_task(
258
- science_calibration_task, background_on, assign_input_dataset_doc_to_task, mocker
258
+ science_calibration_task,
259
+ background_on,
260
+ assign_input_dataset_doc_to_task,
261
+ mocker,
262
+ fake_gql_client,
259
263
  ):
260
264
  """
261
265
  Given: A ScienceCalibration task
@@ -264,7 +268,7 @@ def test_science_calibration_task(
264
268
  """
265
269
 
266
270
  mocker.patch(
267
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
271
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
268
272
  )
269
273
 
270
274
  # When
@@ -304,7 +308,8 @@ def test_science_calibration_task(
304
308
  task=task, num_modstates=num_modstates, data_shape=intermediate_shape, offsets=offsets
305
309
  )
306
310
  write_dummy_intermediate_solar_cals_to_task(
307
- task=task, data_shape=intermediate_shape, num_modstates=num_modstates
311
+ task=task,
312
+ data_shape=intermediate_shape,
308
313
  )
309
314
  write_demod_matrices_to_task(task=task, num_modstates=num_modstates)
310
315
  write_input_observe_frames_to_task(
@@ -360,10 +365,10 @@ def test_science_calibration_task(
360
365
  assert header["VSPMAP"] == map_scan
361
366
 
362
367
  # Check that WCS keys were updated
363
- if offsets[1, 0, 0] > 0:
364
- assert header["CRPIX2"] == input_header["CRPIX2"] - np.ceil(offsets[1, 0, 0])
365
- if offsets[1, 0, 1] > 0:
366
- assert header["CRPIX1"] == input_header["CRPIX1"] - np.ceil(offsets[1, 0, 1])
368
+ if offsets[1, 0, 0] < 0:
369
+ assert header["CRPIX2"] == input_header["CRPIX2"] - np.ceil(-offsets[1, 0, 0])
370
+ if offsets[1, 0, 1] < 0:
371
+ assert header["CRPIX1"] == input_header["CRPIX1"] - np.ceil(-offsets[1, 0, 1])
367
372
 
368
373
  quality_files = task.read(tags=[Tag.quality("TASK_TYPES")])
369
374
  for file in quality_files:
@@ -416,7 +421,7 @@ def test_readout_normalization_correct(
416
421
  )
417
422
 
418
423
  # When:
419
- corrected_array, _ = task.correct_single_frame(
424
+ corrected_array, _, _ = task.correct_single_frame(
420
425
  beam=1,
421
426
  modstate=1,
422
427
  raster_step=1,
@@ -509,7 +514,7 @@ def test_compute_date_keys_compressed_headers(
509
514
  [[1.0, 2.0], [11.0, 10.0], [3.0, 2.0]], # Beam 2
510
515
  ]
511
516
  ),
512
- [slice(11, None, None), slice(10, None, None)],
517
+ [slice(0, -11, None), slice(0, -10, None)],
513
518
  ),
514
519
  (
515
520
  np.array(
@@ -518,7 +523,7 @@ def test_compute_date_keys_compressed_headers(
518
523
  [[-1.0, -2.0], [-11.0, -10.0], [-3.0, -2.0]], # Beam 2
519
524
  ]
520
525
  ),
521
- [slice(0, -11, None), slice(0, -10, None)],
526
+ [slice(11, None, None), slice(10, None, None)],
522
527
  ),
523
528
  (
524
529
  np.array(
@@ -527,7 +532,7 @@ def test_compute_date_keys_compressed_headers(
527
532
  [[1.0, 2.0], [-11.0, 10.0], [-3.0, -2.0]], # Beam 2
528
533
  ]
529
534
  ),
530
- [slice(10, -11, None), slice(10, -2, None)],
535
+ [slice(11, -10, None), slice(2, -10, None)],
531
536
  ),
532
537
  ],
533
538
  ids=["All positive", "All negative", "Positive and negative"],
@@ -577,3 +582,96 @@ def test_combine_beams(
577
582
  expected = np.ones((10, 10, 4)) * 2.5
578
583
 
579
584
  np.testing.assert_array_equal(data, expected)
585
+
586
+
587
+ @pytest.mark.parametrize(
588
+ "shifts",
589
+ # Shifts have shape (num_beams, num_modstates, 2)
590
+ # So the inner-most lists below (e.g., [5.0, 6.0]) correspond to [x_shift, y_shit]
591
+ [
592
+ np.array(
593
+ [
594
+ [[0.0, 0.0], [10.0, 2.0], [5.0, 6.0]], # Beam 1
595
+ [[1.0, 2.0], [-11.0, 10.0], [-3.0, -2.0]], # Beam 2
596
+ ]
597
+ ),
598
+ ],
599
+ ids=["Positive and negative"],
600
+ )
601
+ def test_combine_and_cut_nan_masks(
602
+ science_calibration_task, calibration_collection_with_geo_shifts, shifts
603
+ ):
604
+ """
605
+ Given: A ScienceCalibration task and NaN masks, along with geometric shifts
606
+ When: Combining the two NaN masks
607
+ Then: The final mask has NaN values in the correct place and is correctly cropped
608
+ """
609
+ nan_1_location = [0, 1]
610
+ nan_2_location = [50, 50]
611
+ nan_3_location = [4, 1]
612
+ nan_4_location = [55, 63]
613
+ nan_mask_shape = (100, 100)
614
+ nan_mask_1 = np.zeros(shape=nan_mask_shape)
615
+ nan_mask_1[nan_1_location[0], nan_1_location[1]] = np.nan
616
+ nan_mask_1[nan_2_location[0], nan_2_location[1]] = np.nan
617
+ nan_mask_2 = np.zeros(shape=nan_mask_shape)
618
+ nan_mask_2[nan_3_location[0], nan_3_location[1]] = np.nan
619
+ nan_mask_2[nan_4_location[0], nan_4_location[1]] = np.nan
620
+ task, _, _, _, _, _ = science_calibration_task
621
+ combined_nan_mask = task.combine_and_cut_nan_masks(
622
+ nan_masks=[nan_mask_1, nan_mask_2], calibrations=calibration_collection_with_geo_shifts
623
+ )
624
+ beam_1_shifts = shifts[0]
625
+ beam_2_shifts = shifts[1]
626
+ beam_1_x_shifts = [i[0] for i in beam_1_shifts]
627
+ beam_2_x_shifts = [i[0] for i in beam_2_shifts]
628
+ beam_1_y_shifts = [i[1] for i in beam_1_shifts]
629
+ beam_2_y_shifts = [i[1] for i in beam_2_shifts]
630
+ x_shifts = beam_1_x_shifts + beam_2_x_shifts
631
+ y_shifts = beam_1_y_shifts + beam_2_y_shifts
632
+ assert combined_nan_mask.shape == (
633
+ nan_mask_shape[0] - (max(x_shifts) - min(x_shifts)),
634
+ nan_mask_shape[1] - (max(y_shifts) - min(y_shifts)),
635
+ )
636
+ # Check that one NaN value from each original mask is present in the combined mask and in the correct place
637
+ assert (
638
+ combined_nan_mask[
639
+ nan_2_location[0] - int(abs(min(x_shifts))), nan_2_location[1] - int(abs(min(y_shifts)))
640
+ ]
641
+ == True
642
+ )
643
+ assert (
644
+ combined_nan_mask[
645
+ nan_4_location[0] - int(abs(min(x_shifts))), nan_4_location[1] - int(abs(min(y_shifts)))
646
+ ]
647
+ == True
648
+ )
649
+ assert np.sum(combined_nan_mask) == 2 # only two NaN values are in the final mask
650
+
651
+
652
+ def test_generate_nan_mask(science_calibration_task, dummy_calibration_collection):
653
+ """
654
+ Given: a calibration collection
655
+ When: calculating the NaN mask to use
656
+ Then: the mask takes up some, but not all, of the frame size
657
+ """
658
+ task, _, _, _, _, _ = science_calibration_task
659
+ calibration_collection, _, _ = dummy_calibration_collection
660
+ beam = 1
661
+ modstate = 1
662
+ solar_gain_array = calibration_collection.solar_gain[VispTag.beam(beam)]
663
+ angle = calibration_collection.angle[VispTag.beam(beam)]
664
+ spec_shift = calibration_collection.spec_shift[VispTag.beam(beam)]
665
+ state_offset = calibration_collection.state_offset[VispTag.beam(beam)][
666
+ VispTag.modstate(modstate)
667
+ ]
668
+ nan_mask = task.generate_nan_mask(
669
+ solar_corrected_array=np.random.random(size=solar_gain_array.shape),
670
+ state_offset=state_offset,
671
+ angle=angle,
672
+ spec_shift=spec_shift,
673
+ )
674
+ # Some of the mask is marked as NaN but not all
675
+ assert np.sum(nan_mask) < np.size(nan_mask)
676
+ # Ensure that only zeroes and ones are in the mask
677
+ assert set(np.unique(nan_mask)) == {0, 1}