imap-processing 0.18.0__py3-none-any.whl → 0.19.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (104) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +301 -274
  4. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +28 -28
  5. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1044 -203
  6. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  7. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +12 -0
  8. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +5 -0
  9. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +10 -4
  10. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  11. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +8 -91
  12. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +106 -16
  13. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  14. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  15. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +85 -2
  16. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  17. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +12 -4
  18. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +50 -7
  19. imap_processing/cli.py +95 -41
  20. imap_processing/codice/codice_l1a.py +131 -31
  21. imap_processing/codice/codice_l2.py +118 -10
  22. imap_processing/codice/constants.py +740 -595
  23. imap_processing/decom.py +1 -4
  24. imap_processing/ena_maps/ena_maps.py +32 -25
  25. imap_processing/ena_maps/utils/naming.py +8 -2
  26. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  27. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  28. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  29. imap_processing/glows/ancillary/imap_glows_pipeline_settings_20250923_v002.json +54 -0
  30. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  31. imap_processing/glows/l1b/glows_l1b.py +99 -9
  32. imap_processing/glows/l1b/glows_l1b_data.py +350 -38
  33. imap_processing/glows/l2/glows_l2.py +11 -0
  34. imap_processing/hi/hi_l1a.py +124 -3
  35. imap_processing/hi/hi_l1b.py +154 -71
  36. imap_processing/hi/hi_l2.py +84 -51
  37. imap_processing/hi/utils.py +153 -8
  38. imap_processing/hit/l0/constants.py +3 -0
  39. imap_processing/hit/l0/decom_hit.py +3 -6
  40. imap_processing/hit/l1a/hit_l1a.py +311 -21
  41. imap_processing/hit/l1b/hit_l1b.py +54 -126
  42. imap_processing/hit/l2/hit_l2.py +6 -6
  43. imap_processing/ialirt/calculate_ingest.py +219 -0
  44. imap_processing/ialirt/constants.py +12 -2
  45. imap_processing/ialirt/generate_coverage.py +15 -2
  46. imap_processing/ialirt/l0/ialirt_spice.py +5 -2
  47. imap_processing/ialirt/l0/parse_mag.py +293 -42
  48. imap_processing/ialirt/l0/process_hit.py +5 -3
  49. imap_processing/ialirt/l0/process_swapi.py +41 -25
  50. imap_processing/ialirt/process_ephemeris.py +70 -14
  51. imap_processing/idex/idex_l0.py +2 -2
  52. imap_processing/idex/idex_l1a.py +2 -3
  53. imap_processing/idex/idex_l1b.py +2 -3
  54. imap_processing/idex/idex_l2a.py +130 -4
  55. imap_processing/idex/idex_l2b.py +158 -143
  56. imap_processing/idex/idex_utils.py +1 -3
  57. imap_processing/lo/l0/lo_science.py +25 -24
  58. imap_processing/lo/l1b/lo_l1b.py +3 -3
  59. imap_processing/lo/l1c/lo_l1c.py +116 -50
  60. imap_processing/lo/l2/lo_l2.py +29 -29
  61. imap_processing/lo/lo_ancillary.py +55 -0
  62. imap_processing/mag/l1a/mag_l1a.py +1 -0
  63. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  64. imap_processing/mag/l1b/mag_l1b.py +3 -2
  65. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  66. imap_processing/mag/l1c/mag_l1c.py +23 -6
  67. imap_processing/mag/l1d/mag_l1d.py +57 -14
  68. imap_processing/mag/l1d/mag_l1d_data.py +167 -30
  69. imap_processing/mag/l2/mag_l2_data.py +10 -2
  70. imap_processing/quality_flags.py +9 -1
  71. imap_processing/spice/geometry.py +76 -33
  72. imap_processing/spice/pointing_frame.py +0 -6
  73. imap_processing/spice/repoint.py +29 -2
  74. imap_processing/spice/spin.py +28 -8
  75. imap_processing/spice/time.py +12 -22
  76. imap_processing/swapi/l1/swapi_l1.py +10 -4
  77. imap_processing/swapi/l2/swapi_l2.py +15 -17
  78. imap_processing/swe/l1b/swe_l1b.py +1 -2
  79. imap_processing/ultra/constants.py +1 -24
  80. imap_processing/ultra/l0/ultra_utils.py +9 -11
  81. imap_processing/ultra/l1a/ultra_l1a.py +1 -2
  82. imap_processing/ultra/l1b/cullingmask.py +6 -3
  83. imap_processing/ultra/l1b/de.py +81 -23
  84. imap_processing/ultra/l1b/extendedspin.py +13 -10
  85. imap_processing/ultra/l1b/lookup_utils.py +281 -28
  86. imap_processing/ultra/l1b/quality_flag_filters.py +10 -1
  87. imap_processing/ultra/l1b/ultra_l1b_culling.py +161 -3
  88. imap_processing/ultra/l1b/ultra_l1b_extended.py +253 -47
  89. imap_processing/ultra/l1c/helio_pset.py +97 -24
  90. imap_processing/ultra/l1c/l1c_lookup_utils.py +256 -0
  91. imap_processing/ultra/l1c/spacecraft_pset.py +83 -16
  92. imap_processing/ultra/l1c/ultra_l1c.py +6 -2
  93. imap_processing/ultra/l1c/ultra_l1c_culling.py +85 -0
  94. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +385 -277
  95. imap_processing/ultra/l2/ultra_l2.py +0 -1
  96. imap_processing/ultra/utils/ultra_l1_utils.py +28 -3
  97. imap_processing/utils.py +3 -4
  98. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/METADATA +2 -2
  99. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/RECORD +102 -95
  100. imap_processing/idex/idex_l2c.py +0 -84
  101. imap_processing/spice/kernels.py +0 -187
  102. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/LICENSE +0 -0
  103. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/WHEEL +0 -0
  104. {imap_processing-0.18.0.dist-info → imap_processing-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -2,11 +2,13 @@
2
2
 
3
3
  import logging
4
4
  from decimal import Decimal
5
- from typing import Union
6
5
 
7
6
  import numpy as np
8
7
  import xarray as xr
9
8
 
9
+ from imap_processing.ialirt.l0.ialirt_spice import (
10
+ transform_instrument_vectors_to_inertial,
11
+ )
10
12
  from imap_processing.ialirt.l0.mag_l0_ialirt_data import (
11
13
  Packet0,
12
14
  Packet1,
@@ -22,7 +24,13 @@ from imap_processing.mag.l1b.mag_l1b import (
22
24
  )
23
25
  from imap_processing.mag.l1d.mag_l1d_data import MagL1d
24
26
  from imap_processing.mag.l2.mag_l2_data import MagL2L1dBase
25
- from imap_processing.spice.time import met_to_ttj2000ns, met_to_utc
27
+ from imap_processing.spice.geometry import (
28
+ SpiceFrame,
29
+ cartesian_to_spherical,
30
+ frame_transform,
31
+ spherical_to_cartesian,
32
+ )
33
+ from imap_processing.spice.time import met_to_ttj2000ns, met_to_utc, ttj2000ns_to_et
26
34
 
27
35
  logger = logging.getLogger(__name__)
28
36
 
@@ -195,7 +203,7 @@ def get_time(
195
203
  (grouped_data["group"] == group).values
196
204
  ][pkt_counter == 2]
197
205
 
198
- time_data: dict[str, Union[int, float]] = {
206
+ time_data: dict[str, int | float] = {
199
207
  "pri_coarsetm": int(pri_coarsetm.item()),
200
208
  "pri_fintm": int(pri_fintm.item()),
201
209
  "sec_coarsetm": int(sec_coarsetm.item()),
@@ -290,7 +298,6 @@ def calculate_l1b(
290
298
 
291
299
  def calibrate_and_offset_vectors(
292
300
  vectors: np.ndarray,
293
- range_vals: np.ndarray,
294
301
  calibration: np.ndarray,
295
302
  offsets: np.ndarray,
296
303
  is_magi: bool = False,
@@ -301,9 +308,7 @@ def calibrate_and_offset_vectors(
301
308
  Parameters
302
309
  ----------
303
310
  vectors : np.ndarray
304
- Raw magnetic vectors, shape (n, 3).
305
- range_vals : np.ndarray
306
- Range indices for each vector, shape (n). Values 0–3.
311
+ Raw magnetic vectors, shape (n, 4).
307
312
  calibration : np.ndarray
308
313
  Calibration matrix, shape (3, 3, 4).
309
314
  offsets : np.ndarray
@@ -319,11 +324,9 @@ def calibrate_and_offset_vectors(
319
324
  calibrated_and_offset_vectors : np.ndarray
320
325
  Calibrated and offset vectors, shape (n, 3).
321
326
  """
322
- # Append range as 4th column
323
- vec_plus_range = np.concatenate((vectors, range_vals[:, np.newaxis]), axis=1)
324
-
325
327
  # Apply calibration matrix -> (n,4)
326
- calibrated = MagL2L1dBase.apply_calibration(vec_plus_range, calibration)
328
+ # apply_calibration_offset_single_vector
329
+ calibrated = MagL2L1dBase.apply_calibration(vectors.reshape(1, 4), calibration)
327
330
 
328
331
  # Apply offsets per vector
329
332
  # vec shape (4)
@@ -338,9 +341,193 @@ def calibrate_and_offset_vectors(
338
341
  return calibrated[:, :3]
339
342
 
340
343
 
344
+ def apply_gradiometry_correction(
345
+ mago_vectors_eclipj2000: np.ndarray,
346
+ mago_time_data: np.ndarray,
347
+ magi_vectors_eclipj2000: np.ndarray,
348
+ magi_time_data: np.ndarray,
349
+ gradiometer_factor: np.ndarray,
350
+ ) -> tuple[np.ndarray, np.ndarray]:
351
+ """
352
+ Align MAGi to MAGo timestamps and apply gradiometry correction.
353
+
354
+ Parameters
355
+ ----------
356
+ mago_vectors_eclipj2000 : np.ndarray
357
+ MAGo vectors in inertial frame, shape (N, 3).
358
+ mago_time_data : np.ndarray
359
+ Time for primary sensor, shape (N, 3).
360
+ magi_vectors_eclipj2000 : np.ndarray
361
+ MAGi vectors in inertial frame, shape (M, 3).
362
+ magi_time_data : np.ndarray
363
+ Time for secondary sensor, shape (N, 3).
364
+ gradiometer_factor : np.ndarray
365
+ A (3,3) element matrix to scale and rotate the gradiometer offsets.
366
+
367
+ Returns
368
+ -------
369
+ mago_corrected : np.ndarray
370
+ Corrected MAGo vectors in inertial frame, shape (N, 3).
371
+ magnitude : np.ndarray
372
+ Magnitude of corrected MAGo vectors, shape (N,).
373
+ """
374
+ gradiometry_offsets = MagL1d.calculate_gradiometry_offsets(
375
+ mago_vectors_eclipj2000,
376
+ mago_time_data,
377
+ magi_vectors_eclipj2000,
378
+ magi_time_data,
379
+ )
380
+ mago_corrected = MagL1d.apply_gradiometry_offsets(
381
+ gradiometry_offsets, mago_vectors_eclipj2000, gradiometer_factor
382
+ )
383
+ magnitude = np.linalg.norm(mago_corrected, axis=-1).squeeze()
384
+
385
+ return mago_corrected, magnitude
386
+
387
+
388
+ def transform_to_inertial(
389
+ sc_spin_phase_rad: np.ndarray,
390
+ sc_inertial_right: np.ndarray,
391
+ sc_inertial_decline: np.ndarray,
392
+ attitude_time: np.ndarray,
393
+ target_time: float,
394
+ mag_vector: np.ndarray,
395
+ ) -> np.ndarray:
396
+ """
397
+ Transform vector to ECLIPJ2000.
398
+
399
+ Parameters
400
+ ----------
401
+ sc_spin_phase_rad : numpy.ndarray
402
+ Spin phase for 4 packets 0 to 2π radians, shape (4).
403
+ sc_inertial_right : numpy.ndarray
404
+ Inertial right ascension for 4 packets 0 to 2π radians, shape (4).
405
+ sc_inertial_decline : numpy.ndarray
406
+ Inertial declination for 4 packets -π/2 to π/2 radians, shape (4).
407
+ attitude_time : np.ndarray
408
+ Timestamps for the 4 packets.
409
+ Example: test_met = grouped_data["met"][
410
+ (grouped_data["group"] == group).values].
411
+ ttj2000ns = met_to_ttj2000ns(test_met.values).
412
+ target_time : float
413
+ Time at which to apply the transformation.
414
+ Will be primary_epoch (mago vector) or secondary_epoch (magi vector).
415
+ Example: time_data['primary_epoch'].
416
+ mag_vector : numpy.ndarray
417
+ Vector, shape (3).
418
+
419
+ Returns
420
+ -------
421
+ inertial_vector : np.ndarray
422
+ Transformed vector in the ECLIPJ2000 frame, shape (3,).
423
+
424
+ Notes
425
+ -----
426
+ The MAG vectors are calculated based on 4 packets,
427
+ each of which contains its own spin phase,
428
+ inertial right ascension, and inertial decline.
429
+ """
430
+ if target_time < attitude_time.min() or target_time > attitude_time.max():
431
+ logger.warning(
432
+ f"target_time {target_time} is outside attitude_time bounds "
433
+ f"[{attitude_time.min()}, {attitude_time.max()}]; using edge values."
434
+ )
435
+
436
+ # Get sort order based on attitude_time
437
+ sort_idx = np.argsort(attitude_time)
438
+
439
+ # Sort all arrays accordingly
440
+ attitude_time = attitude_time[sort_idx]
441
+ sc_spin_phase_rad = sc_spin_phase_rad[sort_idx]
442
+ sc_inertial_right = sc_inertial_right[sort_idx]
443
+ sc_inertial_decline = sc_inertial_decline[sort_idx]
444
+
445
+ # Interpolate spin phase, RA, and Dec at target_time
446
+ # Convert RA/Dec to unit cartesian vectors
447
+ spherical_coords = np.stack(
448
+ [
449
+ np.ones_like(sc_inertial_right),
450
+ np.degrees(sc_inertial_right),
451
+ np.degrees(sc_inertial_decline),
452
+ ],
453
+ axis=-1,
454
+ )
455
+ vecs = spherical_to_cartesian(spherical_coords)
456
+
457
+ # Interpolate in Cartesian space
458
+ vx = np.interp(target_time, attitude_time, vecs[:, 0])
459
+ vy = np.interp(target_time, attitude_time, vecs[:, 1])
460
+ vz = np.interp(target_time, attitude_time, vecs[:, 2])
461
+ v_interp = np.array([vx, vy, vz])
462
+ # Normalize vector so that its magnitude is 1.
463
+ v_interp /= np.linalg.norm(v_interp)
464
+
465
+ # Convert back to spherical
466
+ ra_dec = cartesian_to_spherical(v_interp)
467
+ ra_deg = ra_dec[1]
468
+ dec_deg = ra_dec[2]
469
+
470
+ # Account for discontinuities in spin phase.
471
+ spin_phase_unwrapped = np.unwrap(sc_spin_phase_rad)
472
+ spin_phase_interp = np.interp(target_time, attitude_time, spin_phase_unwrapped)
473
+ spin_phase_deg = np.degrees(spin_phase_interp) % 360
474
+
475
+ # Transform each into ECLIPJ2000
476
+ inertial_vector = transform_instrument_vectors_to_inertial(
477
+ np.asarray(mag_vector).reshape(1, 3),
478
+ np.array([spin_phase_deg]),
479
+ np.array([ra_deg]),
480
+ np.array([dec_deg]),
481
+ )[0]
482
+
483
+ return inertial_vector
484
+
485
+
486
+ def transform_to_frames(
487
+ target_time: np.ndarray,
488
+ inertial_vector: np.ndarray,
489
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
490
+ """
491
+ Transform vector to different frames.
492
+
493
+ Parameters
494
+ ----------
495
+ target_time : np.ndarray
496
+ Time at which to apply the transformation.
497
+ Will be primary_epoch (mago vector).
498
+ Example: time_data['primary_epoch'].
499
+ inertial_vector : np.ndarray
500
+ Transformed vector in the ECLIPJ2000 frame, shape (3,).
501
+
502
+ Returns
503
+ -------
504
+ gse_vector : np.ndarray
505
+ Transformed vector in the GSE frame, shape (3,).
506
+ gsm_vector : np.ndarray
507
+ Transformed vector in the GSM frame, shape (3,).
508
+ rtn_vector : np.ndarray
509
+ Transformed vector in the RTN frame, shape (3,).
510
+ """
511
+ et_target_time = ttj2000ns_to_et(target_time)
512
+
513
+ gse_vector = frame_transform(
514
+ et_target_time, inertial_vector, SpiceFrame.ECLIPJ2000, SpiceFrame.IMAP_GSE
515
+ )
516
+ gsm_vector = frame_transform(
517
+ et_target_time, inertial_vector, SpiceFrame.ECLIPJ2000, SpiceFrame.IMAP_GSM
518
+ )
519
+ rtn_vector = frame_transform(
520
+ et_target_time, inertial_vector, SpiceFrame.ECLIPJ2000, SpiceFrame.IMAP_RTN
521
+ )
522
+
523
+ return gse_vector, gsm_vector, rtn_vector
524
+
525
+
341
526
  def process_packet(
342
- accumulated_data: xr.Dataset, calibration_dataset: xr.Dataset
343
- ) -> tuple[list[dict], list[dict]]:
527
+ accumulated_data: xr.Dataset,
528
+ engineering_calibration_dataset: xr.Dataset,
529
+ l1d_calibration_dataset: xr.Dataset,
530
+ ) -> list[dict]:
344
531
  """
345
532
  Parse the MAG packets.
346
533
 
@@ -348,8 +535,10 @@ def process_packet(
348
535
  ----------
349
536
  accumulated_data : xr.Dataset
350
537
  Packets dataset accumulated over 1 min.
351
- calibration_dataset : xr.Dataset
352
- Calibration dataset.
538
+ engineering_calibration_dataset : xr.Dataset
539
+ Engineering calibration dataset.
540
+ l1d_calibration_dataset : xr.Dataset
541
+ L1D calibration dataset.
353
542
 
354
543
  Returns
355
544
  -------
@@ -375,8 +564,12 @@ def process_packet(
375
564
  grouped_data = find_groups(accumulated_data, (0, 3), "pkt_counter", "met")
376
565
 
377
566
  unique_groups = np.unique(grouped_data["group"])
378
- l1b_data = []
379
567
  mag_data = []
568
+ met_all = []
569
+ mago_vectors_all = []
570
+ mago_times_all = []
571
+ magi_vectors_all = []
572
+ magi_times_all = []
380
573
 
381
574
  for group in unique_groups:
382
575
  # Get status values for each group.
@@ -412,7 +605,7 @@ def process_packet(
412
605
  pkt_counter,
413
606
  science_data,
414
607
  status_data,
415
- calibration_dataset,
608
+ engineering_calibration_dataset,
416
609
  )
417
610
 
418
611
  # Note: primary = MAGo, secondary = MAGi.
@@ -423,41 +616,99 @@ def process_packet(
423
616
  if status_data["sec_isvalid"] == 0:
424
617
  updated_vector_magi = np.full(4, -32768)
425
618
 
426
- science_data.update(
427
- {
428
- "calibrated_pri_x": updated_vector_mago[0],
429
- "calibrated_pri_y": updated_vector_mago[1],
430
- "calibrated_pri_z": updated_vector_mago[2],
431
- "calibrated_sec_x": updated_vector_magi[0],
432
- "calibrated_sec_y": updated_vector_magi[1],
433
- "calibrated_sec_z": updated_vector_magi[2],
434
- }
619
+ mago_calibration = l1d_calibration_dataset["URFTOORFO"][0]
620
+ magi_calibration = l1d_calibration_dataset["URFTOORFI"][0]
621
+ offsets = l1d_calibration_dataset["offsets"][0]
622
+
623
+ mago_out = calibrate_and_offset_vectors(
624
+ updated_vector_mago, mago_calibration, offsets, is_magi=False
625
+ )
626
+ magi_out = calibrate_and_offset_vectors(
627
+ updated_vector_magi, magi_calibration, offsets, is_magi=True
628
+ )
629
+ sc_spin_phase_rad = grouped_data["sc_spin_phase"][
630
+ (grouped_data["group"] == group).values
631
+ ]
632
+ sc_inertial_right = grouped_data["sc_inertial_right"][
633
+ (grouped_data["group"] == group).values
634
+ ]
635
+ sc_inertial_decline = grouped_data["sc_inertial_decline"][
636
+ (grouped_data["group"] == group).values
637
+ ]
638
+
639
+ attitude_time = met_to_ttj2000ns(
640
+ grouped_data["met"][(grouped_data["group"] == group).values]
435
641
  )
436
642
 
437
- l1b_data.append({**status_data, **science_data, **time_data})
643
+ # Convert to ECLIPJ2000 frame.
644
+ mago_inertial_vector = transform_to_inertial(
645
+ sc_spin_phase_rad.values,
646
+ sc_inertial_right.values,
647
+ sc_inertial_decline.values,
648
+ attitude_time,
649
+ time_data["primary_epoch"],
650
+ mago_out,
651
+ )
652
+ magi_inertial_vector = transform_to_inertial(
653
+ sc_spin_phase_rad.values,
654
+ sc_inertial_right.values,
655
+ sc_inertial_decline.values,
656
+ attitude_time,
657
+ time_data["secondary_epoch"],
658
+ magi_out,
659
+ )
438
660
 
439
- # Placeholder for real data.
440
661
  met = grouped_data["met"][(grouped_data["group"] == group).values]
662
+ met_all.append(met.values[0])
663
+ mago_times_all.append(time_data["primary_epoch"])
664
+ mago_vectors_all.append(mago_inertial_vector)
665
+ magi_vectors_all.append(magi_inertial_vector)
666
+ magi_times_all.append(time_data["secondary_epoch"])
667
+
668
+ mago_corrected, magnitude = apply_gradiometry_correction(
669
+ np.array(mago_vectors_all),
670
+ np.array(mago_times_all),
671
+ np.array(magi_vectors_all),
672
+ np.array(magi_times_all),
673
+ l1d_calibration_dataset["gradiometer_factor"].values.squeeze(),
674
+ )
675
+
676
+ gse_vector, gsm_vector, rtn_vector = transform_to_frames(
677
+ np.array(mago_times_all), mago_corrected
678
+ )
679
+
680
+ spherical = cartesian_to_spherical(gsm_vector)
681
+ phi_gsm = spherical[:, 1]
682
+ theta_gsm = spherical[:, 2]
683
+
684
+ spherical = cartesian_to_spherical(gse_vector)
685
+ phi_gse = spherical[:, 1]
686
+ theta_gse = spherical[:, 2]
687
+
688
+ # Omit the first value since we expect it to be extrapolated.
689
+ for i in range(len(mago_corrected)):
690
+ if i == 0:
691
+ continue
692
+
441
693
  mag_data.append(
442
694
  {
443
695
  "apid": 478,
444
- "met": int(met.values.min()),
445
- "met_in_utc": met_to_utc(met.values.min()).split(".")[0],
446
- "ttj2000ns": int(met_to_ttj2000ns(met.values.min())),
447
- # TODO: Placeholder for mag_epoch
448
- "mag_epoch": int(met.values.min()),
449
- "mag_B_GSE": [Decimal("0.0") for _ in range(3)],
450
- "mag_B_GSM": [Decimal("0.0") for _ in range(3)],
451
- "mag_B_RTN": [Decimal("0.0") for _ in range(3)],
452
- "mag_B_magnitude": Decimal("0.0"),
453
- "mag_phi_B_GSM": Decimal("0.0"),
454
- "mag_theta_B_GSM": Decimal("0.0"),
455
- "mag_phi_B_GSE": Decimal("0.0"),
456
- "mag_theta_B_GSE": Decimal("0.0"),
696
+ "met": int(met_all[i]),
697
+ "met_in_utc": met_to_utc(met_all[i]).split(".")[0],
698
+ "ttj2000ns": int(met_to_ttj2000ns(met_all[i])),
699
+ "mag_epoch": int(mago_times_all[i]),
700
+ "mag_B_GSE": [Decimal(str(v)) for v in gse_vector[i]],
701
+ "mag_B_GSM": [Decimal(str(v)) for v in gsm_vector[i]],
702
+ "mag_B_RTN": [Decimal(str(v)) for v in rtn_vector[i]],
703
+ "mag_B_magnitude": Decimal(str(magnitude[i])),
704
+ "mag_phi_B_GSM": Decimal(str(phi_gsm[i])),
705
+ "mag_theta_B_GSM": Decimal(str(theta_gsm[i])),
706
+ "mag_phi_B_GSE": Decimal(str(phi_gse[i])),
707
+ "mag_theta_B_GSE": Decimal(str(theta_gse[i])),
457
708
  }
458
709
  )
459
710
 
460
- return mag_data, l1b_data
711
+ return mag_data
461
712
 
462
713
 
463
714
  def retrieve_matrix_from_single_l1b_calibration(
@@ -91,18 +91,20 @@ def create_l1(
91
91
  fast_rate_1_dict = {
92
92
  prefix: value
93
93
  for prefix, value in zip(
94
- HIT_PREFIX_TO_RATE_TYPE["FAST_RATE_1"], fast_rate_1.data
94
+ HIT_PREFIX_TO_RATE_TYPE["FAST_RATE_1"], fast_rate_1.data, strict=False
95
95
  )
96
96
  }
97
97
  fast_rate_2_dict = {
98
98
  prefix: value
99
99
  for prefix, value in zip(
100
- HIT_PREFIX_TO_RATE_TYPE["FAST_RATE_2"], fast_rate_2.data
100
+ HIT_PREFIX_TO_RATE_TYPE["FAST_RATE_2"], fast_rate_2.data, strict=False
101
101
  )
102
102
  }
103
103
  slow_rate_dict = {
104
104
  prefix: value
105
- for prefix, value in zip(HIT_PREFIX_TO_RATE_TYPE["SLOW_RATE"], slow_rate.data)
105
+ for prefix, value in zip(
106
+ HIT_PREFIX_TO_RATE_TYPE["SLOW_RATE"], slow_rate.data, strict=False
107
+ )
106
108
  }
107
109
 
108
110
  l1 = {**fast_rate_1_dict, **fast_rate_2_dict, **slow_rate_dict}
@@ -2,7 +2,6 @@
2
2
 
3
3
  import logging
4
4
  from decimal import Decimal
5
- from typing import Optional
6
5
 
7
6
  import numpy as np
8
7
  import pandas as pd
@@ -10,13 +9,12 @@ import xarray as xr
10
9
  from scipy.optimize import curve_fit
11
10
  from scipy.special import erf
12
11
 
13
- from imap_processing import imap_module_directory
14
12
  from imap_processing.ialirt.constants import IalirtSwapiConstants as Consts
15
13
  from imap_processing.ialirt.utils.grouping import find_groups
16
14
  from imap_processing.ialirt.utils.time import calculate_time
17
15
  from imap_processing.spice.time import met_to_ttj2000ns, met_to_utc
18
16
  from imap_processing.swapi.l1.swapi_l1 import process_sweep_data
19
- from imap_processing.swapi.l2.swapi_l2 import TIME_PER_BIN
17
+ from imap_processing.swapi.l2.swapi_l2 import SWAPI_LIVETIME
20
18
 
21
19
  logger = logging.getLogger(__name__)
22
20
 
@@ -70,7 +68,7 @@ def count_rate(
70
68
  def optimize_pseudo_parameters(
71
69
  count_rates: np.ndarray,
72
70
  count_rate_error: np.ndarray,
73
- energy_passbands: Optional[np.ndarray] = None,
71
+ energy_passbands: np.ndarray,
74
72
  ) -> (dict)[str, list[float]]:
75
73
  """
76
74
  Find the pseudo speed (u), density (n) and temperature (T) of solar wind particles.
@@ -84,7 +82,7 @@ def optimize_pseudo_parameters(
84
82
  count_rate_error : np.ndarray
85
83
  Standard deviation of the coincidence count rates parameter.
86
84
  energy_passbands : np.ndarray, default None
87
- Energy passbands, passed in only for testing purposes.
85
+ Energy values, taken from the SWAPI lookup table.
88
86
 
89
87
  Returns
90
88
  -------
@@ -92,21 +90,6 @@ def optimize_pseudo_parameters(
92
90
  Dictionary containing the optimized speed, density, and temperature values for
93
91
  each sweep included in the input count_rates array.
94
92
  """
95
- if not energy_passbands:
96
- # Read in energy passbands
97
- energy_data = pd.read_csv(
98
- f"{imap_module_directory}/tests/swapi/lut/imap_swapi_esa-unit"
99
- f"-conversion_20250626_v001.csv"
100
- )
101
- energy_passbands = (
102
- energy_data["Energy"][0:63]
103
- .replace(",", "", regex=True)
104
- .to_numpy()
105
- .astype(float)
106
- )
107
-
108
- # Initial guess pulled from page 52 of the IMAP SWAPI Instrument Algorithms Document
109
- initial_param_guess = np.array([550, 5.27, 1e5])
110
93
  solution_dict = { # type: ignore
111
94
  "pseudo_speed": [],
112
95
  "pseudo_density": [],
@@ -116,8 +99,16 @@ def optimize_pseudo_parameters(
116
99
  for sweep in np.arange(count_rates.shape[0]):
117
100
  current_sweep_count_rates = count_rates[sweep, :]
118
101
  current_sweep_count_rate_errors = count_rate_error[sweep, :]
119
- # Find the max count rate, and use the 6 points surrounding it (inclusive)
102
+ # Find the max count rate, and use the 5 points surrounding it
120
103
  max_index = np.argmax(current_sweep_count_rates)
104
+ initial_speed_guess = np.sqrt(energy_passbands[max_index]) * Consts.speed_coeff
105
+ initial_param_guess = np.array(
106
+ [
107
+ initial_speed_guess,
108
+ 5 * (400 / initial_speed_guess) ** 2,
109
+ 60000 * (initial_speed_guess / 400) ** 2,
110
+ ]
111
+ )
121
112
  sol = curve_fit(
122
113
  f=count_rate,
123
114
  xdata=energy_passbands.take(
@@ -138,7 +129,9 @@ def optimize_pseudo_parameters(
138
129
  return solution_dict
139
130
 
140
131
 
141
- def process_swapi_ialirt(unpacked_data: xr.Dataset) -> list[dict]:
132
+ def process_swapi_ialirt(
133
+ unpacked_data: xr.Dataset, calibration_lut_table: pd.DataFrame
134
+ ) -> list[dict]:
142
135
  """
143
136
  Extract I-ALiRT variables and calculate coincidence count rate.
144
137
 
@@ -146,6 +139,8 @@ def process_swapi_ialirt(unpacked_data: xr.Dataset) -> list[dict]:
146
139
  ----------
147
140
  unpacked_data : xr.Dataset
148
141
  SWAPI I-ALiRT data that has been parsed from the spacecraft packet.
142
+ calibration_lut_table : pd.DataFrame
143
+ DataFrame containing the contents of the SWAPI esa-unit-conversion lookup table.
149
144
 
150
145
  Returns
151
146
  -------
@@ -191,10 +186,31 @@ def process_swapi_ialirt(unpacked_data: xr.Dataset) -> list[dict]:
191
186
  continue
192
187
 
193
188
  raw_coin_count = process_sweep_data(grouped_dataset, "swapi_coin_cnt")
194
- raw_coin_rate = raw_coin_count / TIME_PER_BIN
195
- count_rate_error = np.sqrt(raw_coin_count) / TIME_PER_BIN
189
+ raw_coin_rate = raw_coin_count / SWAPI_LIVETIME
190
+ count_rate_error = np.sqrt(raw_coin_count) / SWAPI_LIVETIME
196
191
 
197
- solution = optimize_pseudo_parameters(raw_coin_rate, count_rate_error)
192
+ # Extract energy values from the calibration lookup table file
193
+ calibration_lut_table["timestamp"] = pd.to_datetime(
194
+ calibration_lut_table["timestamp"], format="%m/%d/%Y %H:%M"
195
+ )
196
+ calibration_lut_table["timestamp"] = calibration_lut_table["timestamp"].to_numpy(
197
+ dtype="datetime64[ns]"
198
+ )
199
+
200
+ # Find the sweep's energy data for the latest time, where sweep_id == 2
201
+ subset = calibration_lut_table[
202
+ (calibration_lut_table["timestamp"] == calibration_lut_table["timestamp"].max())
203
+ & (calibration_lut_table["Sweep #"] == 2)
204
+ ]
205
+ if subset.empty:
206
+ energy_passbands = np.full(63, np.nan, dtype=np.float64)
207
+ else:
208
+ subset = subset.sort_values(["timestamp", "ESA Step #"])
209
+ energy_passbands = subset["Energy"][0:63].to_numpy().astype(float)
210
+
211
+ solution = optimize_pseudo_parameters(
212
+ raw_coin_rate, count_rate_error, energy_passbands
213
+ )
198
214
 
199
215
  swapi_data = []
200
216