dkist-processing-visp 3.3.0__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 (71) 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 +52 -21
  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 -19
  7. dkist_processing_visp/models/tags.py +1 -0
  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 +3 -1
  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 +15 -7
  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 +13 -12
  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 +34 -4
  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 +2 -0
  34. dkist_processing_visp/tasks/write_l1.py +25 -5
  35. dkist_processing_visp/tests/conftest.py +99 -35
  36. dkist_processing_visp/tests/header_models.py +92 -20
  37. dkist_processing_visp/tests/local_trial_workflows/l0_cals_only.py +4 -23
  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 +10 -29
  40. dkist_processing_visp/tests/local_trial_workflows/l0_to_l1.py +1 -21
  41. dkist_processing_visp/tests/local_trial_workflows/local_trial_helpers.py +98 -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 +8 -5
  45. dkist_processing_visp/tests/test_dark.py +4 -3
  46. dkist_processing_visp/tests/test_fits_access.py +43 -0
  47. dkist_processing_visp/tests/test_geometric.py +45 -4
  48. dkist_processing_visp/tests/test_instrument_polarization.py +4 -3
  49. dkist_processing_visp/tests/test_lamp.py +22 -26
  50. dkist_processing_visp/tests/test_make_movie_frames.py +4 -4
  51. dkist_processing_visp/tests/test_map_repeats.py +3 -1
  52. dkist_processing_visp/tests/test_parameters.py +122 -21
  53. dkist_processing_visp/tests/test_parse.py +98 -14
  54. dkist_processing_visp/tests/test_quality.py +2 -3
  55. dkist_processing_visp/tests/test_science.py +113 -15
  56. dkist_processing_visp/tests/test_solar.py +318 -99
  57. dkist_processing_visp/tests/test_visp_constants.py +36 -8
  58. dkist_processing_visp/tests/test_workflows.py +1 -0
  59. dkist_processing_visp/tests/test_write_l1.py +17 -3
  60. dkist_processing_visp/workflows/__init__.py +1 -0
  61. dkist_processing_visp/workflows/l0_processing.py +8 -2
  62. dkist_processing_visp/workflows/trial_workflows.py +8 -2
  63. dkist_processing_visp-5.1.1.dist-info/METADATA +552 -0
  64. dkist_processing_visp-5.1.1.dist-info/RECORD +94 -0
  65. docs/conf.py +5 -1
  66. docs/gain_correction.rst +50 -42
  67. dkist_processing_visp/tasks/mixin/line_zones.py +0 -115
  68. dkist_processing_visp-3.3.0.dist-info/METADATA +0 -459
  69. dkist_processing_visp-3.3.0.dist-info/RECORD +0 -90
  70. {dkist_processing_visp-3.3.0.dist-info → dkist_processing_visp-5.1.1.dist-info}/WHEEL +0 -0
  71. {dkist_processing_visp-3.3.0.dist-info → dkist_processing_visp-5.1.1.dist-info}/top_level.txt +0 -0
@@ -2,7 +2,6 @@ import pytest
2
2
  from dkist_data_simulator.dataset import key_function
3
3
  from dkist_processing_common._util.scratch import WorkflowFileSystem
4
4
  from dkist_processing_common.models.tags import Tag
5
- from dkist_processing_common.tests.conftest import FakeGQLClient
6
5
 
7
6
  from dkist_processing_visp.models.parameters import VispParsingParameters
8
7
  from dkist_processing_visp.models.tags import VispTag
@@ -87,6 +86,7 @@ def write_input_dark_frames_to_task(
87
86
  time_delta: float = 10.0,
88
87
  num_modstates: int = 2,
89
88
  data_shape: tuple[int, int] = (2, 2),
89
+ **kwargs,
90
90
  ):
91
91
  array_shape = (1, *data_shape)
92
92
  dataset = VispHeadersInputDarkFrames(
@@ -95,6 +95,7 @@ def write_input_dark_frames_to_task(
95
95
  exp_time=exp_time,
96
96
  readout_exp_time=readout_exp_time,
97
97
  num_modstates=num_modstates,
98
+ **kwargs,
98
99
  )
99
100
 
100
101
  num_written_frames = write_frames_to_task(
@@ -110,6 +111,7 @@ def write_input_lamp_frames_to_task(
110
111
  time_delta: float = 10.0,
111
112
  num_modstates: int = 2,
112
113
  data_shape: tuple[int, int] = (2, 2),
114
+ **kwargs,
113
115
  ):
114
116
  array_shape = (1, *data_shape)
115
117
  dataset = VispHeadersInputLampGainFrames(
@@ -118,6 +120,7 @@ def write_input_lamp_frames_to_task(
118
120
  exp_time=exp_time,
119
121
  readout_exp_time=readout_exp_time,
120
122
  num_modstates=num_modstates,
123
+ **kwargs,
121
124
  )
122
125
 
123
126
  num_written_frames = write_frames_to_task(
@@ -133,6 +136,7 @@ def write_input_solar_frames_to_task(
133
136
  time_delta: float = 10.0,
134
137
  num_modstates: int = 2,
135
138
  data_shape: tuple[int, int] = (2, 2),
139
+ **kwargs,
136
140
  ):
137
141
  array_shape = (1, *data_shape)
138
142
  dataset = VispHeadersInputSolarGainFrames(
@@ -141,6 +145,7 @@ def write_input_solar_frames_to_task(
141
145
  exp_time=exp_time,
142
146
  readout_exp_time=readout_exp_time,
143
147
  num_modstates=num_modstates,
148
+ **kwargs,
144
149
  )
145
150
 
146
151
  num_written_frames = write_frames_to_task(
@@ -156,6 +161,7 @@ def write_input_polcal_frames_to_task(
156
161
  time_delta: float = 30.0,
157
162
  num_modstates: int = 2,
158
163
  data_shape: tuple[int, int] = (2, 2),
164
+ **kwargs,
159
165
  ):
160
166
  array_shape = (1, *data_shape)
161
167
  dataset = VispHeadersInputPolcalFrames(
@@ -164,6 +170,7 @@ def write_input_polcal_frames_to_task(
164
170
  exp_time=exp_time,
165
171
  readout_exp_time=readout_exp_time,
166
172
  num_modstates=num_modstates,
173
+ **kwargs,
167
174
  )
168
175
 
169
176
  num_written_frames = write_frames_to_task(
@@ -179,6 +186,7 @@ def write_input_polcal_dark_frames_to_task(
179
186
  time_delta: float = 30.0,
180
187
  num_modstates: int = 2,
181
188
  data_shape: tuple[int, int] = (2, 2),
189
+ **kwargs,
182
190
  ):
183
191
  array_shape = (1, *data_shape)
184
192
  dataset = VispHeadersInputPolcalDarkFrames(
@@ -187,6 +195,7 @@ def write_input_polcal_dark_frames_to_task(
187
195
  exp_time=exp_time,
188
196
  readout_exp_time=readout_exp_time,
189
197
  num_modstates=num_modstates,
198
+ **kwargs,
190
199
  )
191
200
 
192
201
  num_written_frames = write_frames_to_task(
@@ -202,6 +211,7 @@ def write_input_polcal_gain_frames_to_task(
202
211
  time_delta: float = 30.0,
203
212
  num_modstates: int = 2,
204
213
  data_shape: tuple[int, int] = (2, 2),
214
+ **kwargs,
205
215
  ):
206
216
  array_shape = (1, *data_shape)
207
217
  dataset = VispHeadersInputPolcalGainFrames(
@@ -210,6 +220,7 @@ def write_input_polcal_gain_frames_to_task(
210
220
  exp_time=exp_time,
211
221
  readout_exp_time=readout_exp_time,
212
222
  num_modstates=num_modstates,
223
+ **kwargs,
213
224
  )
214
225
 
215
226
  num_written_frames = write_frames_to_task(
@@ -228,6 +239,7 @@ def write_input_observe_frames_to_task(
228
239
  time_delta: float = 10.0,
229
240
  data_shape: tuple[int, int] = (2, 2),
230
241
  obs_dataset_class=VispHeadersValidObserveFrames,
242
+ **kwargs,
231
243
  ):
232
244
  array_shape = (1, *data_shape)
233
245
  dataset = obs_dataset_class(
@@ -238,6 +250,7 @@ def write_input_observe_frames_to_task(
238
250
  num_modstates=num_modstates,
239
251
  exp_time=exp_time,
240
252
  readout_exp_time=readout_exp_time,
253
+ **kwargs,
241
254
  )
242
255
  num_written_frames = write_frames_to_task(
243
256
  task=task, frame_generator=dataset, extra_tags=[VispTag.input()]
@@ -273,6 +286,11 @@ def write_input_cal_frames_to_task(
273
286
  solar_exp_time,
274
287
  polcal_exp_time,
275
288
  num_modstates,
289
+ testing_arm_id,
290
+ testing_solar_ip_start_time,
291
+ testing_grating_constant,
292
+ testing_grating_angle,
293
+ testing_arm_position,
276
294
  ):
277
295
  def write_frames_to_task(task):
278
296
  for readout_exp_time in required_dark_readout_exp_times:
@@ -281,6 +299,7 @@ def write_input_cal_frames_to_task(
281
299
  readout_exp_time=readout_exp_time,
282
300
  exp_time=dark_exp_time,
283
301
  num_modstates=num_modstates,
302
+ arm_id=testing_arm_id,
284
303
  )
285
304
 
286
305
  write_input_lamp_frames_to_task(
@@ -288,30 +307,39 @@ def write_input_cal_frames_to_task(
288
307
  readout_exp_time=lamp_readout_exp_time,
289
308
  exp_time=lamp_exp_time,
290
309
  num_modstates=num_modstates,
310
+ arm_id=testing_arm_id,
291
311
  )
292
312
  write_input_solar_frames_to_task(
293
313
  task=task,
294
314
  readout_exp_time=solar_readout_exp_time,
295
315
  exp_time=solar_exp_time,
296
316
  num_modstates=num_modstates,
317
+ arm_id=testing_arm_id,
318
+ ip_start_time=testing_solar_ip_start_time,
319
+ grating_constant=testing_grating_constant,
320
+ grating_angle=testing_grating_angle,
321
+ arm_position=testing_arm_position,
297
322
  )
298
323
  write_input_polcal_frames_to_task(
299
324
  task=task,
300
325
  readout_exp_time=polcal_readout_exp_time,
301
326
  exp_time=polcal_exp_time,
302
327
  num_modstates=num_modstates,
328
+ arm_id=testing_arm_id,
303
329
  )
304
330
  write_input_polcal_dark_frames_to_task(
305
331
  task=task,
306
332
  readout_exp_time=polcal_readout_exp_time,
307
333
  exp_time=polcal_exp_time,
308
334
  num_modstates=num_modstates,
335
+ arm_id=testing_arm_id,
309
336
  )
310
337
  write_input_polcal_gain_frames_to_task(
311
338
  task,
312
339
  readout_exp_time=polcal_readout_exp_time,
313
340
  exp_time=polcal_exp_time,
314
341
  num_modstates=num_modstates,
342
+ arm_id=testing_arm_id,
315
343
  )
316
344
 
317
345
  return write_frames_to_task
@@ -350,6 +378,11 @@ def test_parse_visp_input_data(
350
378
  observe_exp_times,
351
379
  num_modstates,
352
380
  mocker,
381
+ fake_gql_client,
382
+ testing_arm_id,
383
+ testing_grating_constant,
384
+ testing_grating_angle,
385
+ testing_arm_position,
353
386
  ):
354
387
  """
355
388
  Given: A ParseVispInputData task
@@ -357,7 +390,7 @@ def test_parse_visp_input_data(
357
390
  Then: All tagged files exist and individual task tags are applied
358
391
  """
359
392
  mocker.patch(
360
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
393
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
361
394
  )
362
395
  task = parse_task_with_no_data
363
396
  write_input_cal_frames_to_task(task)
@@ -369,6 +402,10 @@ def test_parse_visp_input_data(
369
402
  num_steps=3,
370
403
  readout_exp_time=obs_readout_exp_time,
371
404
  exp_time=obs_exp_time,
405
+ arm_id=testing_arm_id,
406
+ grating_constant=testing_grating_constant,
407
+ grating_angle=testing_grating_angle,
408
+ arm_position=testing_arm_position,
372
409
  )
373
410
 
374
411
  # When
@@ -393,6 +430,7 @@ def test_parse_visp_input_data_constants(
393
430
  parse_task_with_no_data,
394
431
  write_input_cal_frames_to_task,
395
432
  mocker,
433
+ fake_gql_client,
396
434
  lamp_readout_exp_time,
397
435
  solar_readout_exp_time,
398
436
  polcal_readout_exp_time,
@@ -403,6 +441,12 @@ def test_parse_visp_input_data_constants(
403
441
  polcal_exp_time,
404
442
  observe_exp_times,
405
443
  num_modstates,
444
+ testing_arm_id,
445
+ testing_obs_ip_start_time,
446
+ testing_solar_ip_start_time,
447
+ testing_grating_constant,
448
+ testing_grating_angle,
449
+ testing_arm_position,
406
450
  ):
407
451
  """
408
452
  Given: A ParseVispInputData task
@@ -410,7 +454,7 @@ def test_parse_visp_input_data_constants(
410
454
  Then: Constants are in the constants object as expected
411
455
  """
412
456
  mocker.patch(
413
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
457
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
414
458
  )
415
459
  task = parse_task_with_no_data
416
460
  write_input_cal_frames_to_task(task)
@@ -425,16 +469,29 @@ def test_parse_visp_input_data_constants(
425
469
  num_steps=num_steps,
426
470
  readout_exp_time=obs_readout_exp_time,
427
471
  exp_time=obs_exp_time,
472
+ arm_id=testing_arm_id,
473
+ ip_start_time=testing_obs_ip_start_time,
474
+ grating_constant=testing_grating_constant,
475
+ grating_angle=testing_grating_angle,
476
+ arm_position=testing_arm_position,
428
477
  )
429
478
 
430
479
  # When
431
480
  task()
432
481
  # Then
482
+ assert task.constants._db_dict["ARM_ID"] == testing_arm_id
433
483
  expected_dark_readout_exp_times = [
434
484
  lamp_readout_exp_time,
435
485
  solar_readout_exp_time,
436
486
  ] + observe_readout_exp_times
437
- assert task.constants._db_dict["OBS_IP_START_TIME"] == "2022-11-28T13:55:00"
487
+ assert task.constants._db_dict["OBS_IP_START_TIME"] == testing_obs_ip_start_time
488
+ assert task.constants._db_dict["INCIDENT_LIGHT_ANGLE_DEG"] == -1 * testing_grating_angle
489
+ assert (
490
+ task.constants._db_dict["REFLECTED_LIGHT_ANGLE_DEG"]
491
+ == -1 * testing_grating_angle + testing_arm_position
492
+ )
493
+ assert task.constants._db_dict["GRATING_CONSTANT_INVERSE_MM"] == testing_grating_constant
494
+ assert task.constants._db_dict["SOLAR_GAIN_IP_START_TIME"] == testing_solar_ip_start_time
438
495
  assert task.constants._db_dict["NUM_MODSTATES"] == num_modstates
439
496
  assert task.constants._db_dict["NUM_MAP_SCANS"] == num_maps_per_readout_exp_time * len(
440
497
  observe_readout_exp_times
@@ -454,6 +511,10 @@ def test_parse_visp_input_data_constants(
454
511
  observe_readout_exp_times
455
512
  )
456
513
  assert task.constants._db_dict["RETARDER_NAME"] == "SiO2 OC"
514
+ assert task.constants._db_dict["DARK_GOS_LEVEL3_STATUS"] == "lamp"
515
+ assert task.constants._db_dict["SOLAR_GAIN_GOS_LEVEL3_STATUS"] == "clear"
516
+ assert task.constants._db_dict["SOLAR_GAIN_NUM_RAW_FRAMES_PER_FPA"] == 10
517
+ assert task.constants._db_dict["POLCAL_NUM_RAW_FRAMES_PER_FPA"] == 10
457
518
 
458
519
 
459
520
  def test_parse_visp_values(
@@ -462,6 +523,11 @@ def test_parse_visp_values(
462
523
  observe_readout_exp_times,
463
524
  num_modstates,
464
525
  mocker,
526
+ fake_gql_client,
527
+ testing_arm_id,
528
+ testing_grating_constant,
529
+ testing_grating_angle,
530
+ testing_arm_position,
465
531
  ):
466
532
  """
467
533
  :Given: A valid parse input task
@@ -469,7 +535,7 @@ def test_parse_visp_values(
469
535
  :Then: Values are correctly loaded into the constants mutable mapping
470
536
  """
471
537
  mocker.patch(
472
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
538
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
473
539
  )
474
540
  task = parse_task_with_no_data
475
541
  write_input_cal_frames_to_task(task)
@@ -480,6 +546,10 @@ def test_parse_visp_values(
480
546
  num_maps=1,
481
547
  num_steps=1,
482
548
  num_modstates=num_modstates,
549
+ arm_id=testing_arm_id,
550
+ grating_constant=testing_grating_constant,
551
+ grating_angle=testing_grating_angle,
552
+ arm_position=testing_arm_position,
483
553
  )
484
554
 
485
555
  task()
@@ -488,16 +558,19 @@ def test_parse_visp_values(
488
558
  assert task.constants.maximum_cadence == 10
489
559
  assert task.constants.minimum_cadence == 10
490
560
  assert task.constants.variance_cadence == 0
561
+ assert task.constants.camera_name == "camera_name"
491
562
 
492
563
 
493
- def test_multiple_num_raster_steps_raises_error(parse_task_with_no_data, num_modstates, mocker):
564
+ def test_multiple_num_raster_steps_raises_error(
565
+ parse_task_with_no_data, num_modstates, mocker, fake_gql_client
566
+ ):
494
567
  """
495
568
  :Given: A prase task with data that have inconsistent VSPNSTP values
496
569
  :When: Calling the parse task
497
570
  :Then: The correct error is raised
498
571
  """
499
572
  mocker.patch(
500
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
573
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
501
574
  )
502
575
  task = parse_task_with_no_data
503
576
  write_input_dark_frames_to_task(task, readout_exp_time=0.1, exp_time=0.2)
@@ -515,14 +588,14 @@ def test_multiple_num_raster_steps_raises_error(parse_task_with_no_data, num_mod
515
588
  task()
516
589
 
517
590
 
518
- def test_incomplete_single_map(parse_task_with_no_data, num_modstates, mocker):
591
+ def test_incomplete_single_map(parse_task_with_no_data, num_modstates, mocker, fake_gql_client):
519
592
  """
520
593
  :Given: A parse task with data that has an incomplete raster scan
521
594
  :When: Calling the parse task
522
595
  :Then: The correct number of raster steps are found
523
596
  """
524
597
  mocker.patch(
525
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
598
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
526
599
  )
527
600
  task = parse_task_with_no_data
528
601
  num_steps = 4
@@ -542,14 +615,14 @@ def test_incomplete_single_map(parse_task_with_no_data, num_modstates, mocker):
542
615
  assert task.constants._db_dict["NUM_MAP_SCANS"] == num_map_scans
543
616
 
544
617
 
545
- def test_incomplete_final_map(parse_task_with_no_data, num_modstates, mocker):
618
+ def test_incomplete_final_map(parse_task_with_no_data, num_modstates, mocker, fake_gql_client):
546
619
  """
547
620
  :Given: A parse task with data that has complete raster scans along with an incomplete raster scan
548
621
  :When: Calling the parse task
549
622
  :Then: The correct number of raster steps and maps are found
550
623
  """
551
624
  mocker.patch(
552
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
625
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
553
626
  )
554
627
  task = parse_task_with_no_data
555
628
  num_steps = 4
@@ -575,6 +648,11 @@ def test_intensity_observes_and_polarimetric_cals(
575
648
  observe_readout_exp_times,
576
649
  observe_exp_times,
577
650
  mocker,
651
+ fake_gql_client,
652
+ testing_arm_id,
653
+ testing_grating_constant,
654
+ testing_grating_angle,
655
+ testing_arm_position,
578
656
  ):
579
657
  """
580
658
  :Given: Data where the observe frames are in intensity mode and the calibration frames are in polarimetric mode
@@ -582,7 +660,7 @@ def test_intensity_observes_and_polarimetric_cals(
582
660
  :Then: All modulator state keys generated for all frames are in the first modulator state
583
661
  """
584
662
  mocker.patch(
585
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
663
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
586
664
  )
587
665
  task = parse_task_with_no_data
588
666
  write_input_cal_frames_to_task(task)
@@ -594,6 +672,10 @@ def test_intensity_observes_and_polarimetric_cals(
594
672
  readout_exp_time=observe_readout_exp_times[0],
595
673
  exp_time=observe_exp_times[0],
596
674
  obs_dataset_class=VispHeadersIntensityObserveFrames,
675
+ arm_id=testing_arm_id,
676
+ grating_constant=testing_grating_constant,
677
+ grating_angle=testing_grating_angle,
678
+ arm_position=testing_arm_position,
597
679
  )
598
680
  task()
599
681
  assert task.constants._db_dict["NUM_MODSTATES"] == 1
@@ -603,14 +685,16 @@ def test_intensity_observes_and_polarimetric_cals(
603
685
  assert "MODSTATE_1" in task.scratch.tags(file)
604
686
 
605
687
 
606
- def test_dark_readout_exp_time_picky_bud(parse_task_with_no_data, mocker, lamp_readout_exp_time):
688
+ def test_dark_readout_exp_time_picky_bud(
689
+ parse_task_with_no_data, mocker, fake_gql_client, lamp_readout_exp_time
690
+ ):
607
691
  """
608
692
  :Given: Dataset where non-dark readout exp time values are missing from the set of dark IP frames.
609
693
  :When: Parsing
610
694
  :Then: The `DarkReadoutExpTimePickyBud` raises an error
611
695
  """
612
696
  mocker.patch(
613
- "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=FakeGQLClient
697
+ "dkist_processing_common.tasks.mixin.metadata_store.GraphQLClient", new=fake_gql_client
614
698
  )
615
699
  bad_readout_exp_time = lamp_readout_exp_time + 0.02
616
700
  dummy_exp_time = 99.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
@@ -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}