imap-processing 0.17.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 (141) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/ancillary/ancillary_dataset_combiner.py +161 -1
  3. imap_processing/ccsds/excel_to_xtce.py +12 -0
  4. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +6 -6
  5. imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +312 -274
  6. imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +39 -28
  7. imap_processing/cdf/config/imap_codice_l2_variable_attrs.yaml +1048 -183
  8. imap_processing/cdf/config/imap_constant_attrs.yaml +4 -2
  9. imap_processing/cdf/config/imap_glows_l1b_variable_attrs.yaml +12 -0
  10. imap_processing/cdf/config/imap_hi_global_cdf_attrs.yaml +5 -0
  11. imap_processing/cdf/config/imap_hit_global_cdf_attrs.yaml +10 -4
  12. imap_processing/cdf/config/imap_hit_l1a_variable_attrs.yaml +163 -100
  13. imap_processing/cdf/config/imap_hit_l2_variable_attrs.yaml +4 -4
  14. imap_processing/cdf/config/imap_ialirt_l1_variable_attrs.yaml +97 -54
  15. imap_processing/cdf/config/imap_idex_l2a_variable_attrs.yaml +33 -4
  16. imap_processing/cdf/config/imap_idex_l2b_variable_attrs.yaml +44 -44
  17. imap_processing/cdf/config/imap_idex_l2c_variable_attrs.yaml +77 -61
  18. imap_processing/cdf/config/imap_lo_global_cdf_attrs.yaml +30 -0
  19. imap_processing/cdf/config/imap_lo_l1a_variable_attrs.yaml +4 -15
  20. imap_processing/cdf/config/imap_lo_l1c_variable_attrs.yaml +189 -98
  21. imap_processing/cdf/config/imap_mag_global_cdf_attrs.yaml +99 -2
  22. imap_processing/cdf/config/imap_mag_l1c_variable_attrs.yaml +24 -1
  23. imap_processing/cdf/config/imap_ultra_global_cdf_attrs.yaml +60 -0
  24. imap_processing/cdf/config/imap_ultra_l1b_variable_attrs.yaml +99 -11
  25. imap_processing/cdf/config/imap_ultra_l1c_variable_attrs.yaml +50 -7
  26. imap_processing/cli.py +121 -44
  27. imap_processing/codice/codice_l1a.py +165 -77
  28. imap_processing/codice/codice_l1b.py +1 -1
  29. imap_processing/codice/codice_l2.py +118 -19
  30. imap_processing/codice/constants.py +1217 -1089
  31. imap_processing/decom.py +1 -4
  32. imap_processing/ena_maps/ena_maps.py +32 -25
  33. imap_processing/ena_maps/utils/naming.py +8 -2
  34. imap_processing/glows/ancillary/imap_glows_exclusions-by-instr-team_20250923_v002.dat +10 -0
  35. imap_processing/glows/ancillary/imap_glows_map-of-excluded-regions_20250923_v002.dat +393 -0
  36. imap_processing/glows/ancillary/imap_glows_map-of-uv-sources_20250923_v002.dat +593 -0
  37. imap_processing/glows/ancillary/imap_glows_pipeline_settings_20250923_v002.json +54 -0
  38. imap_processing/glows/ancillary/imap_glows_suspected-transients_20250923_v002.dat +10 -0
  39. imap_processing/glows/l1b/glows_l1b.py +99 -9
  40. imap_processing/glows/l1b/glows_l1b_data.py +350 -38
  41. imap_processing/glows/l2/glows_l2.py +11 -0
  42. imap_processing/hi/hi_l1a.py +124 -3
  43. imap_processing/hi/hi_l1b.py +154 -71
  44. imap_processing/hi/hi_l2.py +84 -51
  45. imap_processing/hi/utils.py +153 -8
  46. imap_processing/hit/l0/constants.py +3 -0
  47. imap_processing/hit/l0/decom_hit.py +5 -8
  48. imap_processing/hit/l1a/hit_l1a.py +375 -45
  49. imap_processing/hit/l1b/constants.py +5 -0
  50. imap_processing/hit/l1b/hit_l1b.py +61 -131
  51. imap_processing/hit/l2/constants.py +1 -1
  52. imap_processing/hit/l2/hit_l2.py +10 -11
  53. imap_processing/ialirt/calculate_ingest.py +219 -0
  54. imap_processing/ialirt/constants.py +32 -1
  55. imap_processing/ialirt/generate_coverage.py +201 -0
  56. imap_processing/ialirt/l0/ialirt_spice.py +5 -2
  57. imap_processing/ialirt/l0/parse_mag.py +337 -29
  58. imap_processing/ialirt/l0/process_hit.py +5 -3
  59. imap_processing/ialirt/l0/process_swapi.py +41 -25
  60. imap_processing/ialirt/l0/process_swe.py +23 -7
  61. imap_processing/ialirt/process_ephemeris.py +70 -14
  62. imap_processing/ialirt/utils/constants.py +22 -16
  63. imap_processing/ialirt/utils/create_xarray.py +42 -19
  64. imap_processing/idex/idex_constants.py +1 -5
  65. imap_processing/idex/idex_l0.py +2 -2
  66. imap_processing/idex/idex_l1a.py +2 -3
  67. imap_processing/idex/idex_l1b.py +2 -3
  68. imap_processing/idex/idex_l2a.py +130 -4
  69. imap_processing/idex/idex_l2b.py +313 -119
  70. imap_processing/idex/idex_utils.py +1 -3
  71. imap_processing/lo/l0/lo_apid.py +1 -0
  72. imap_processing/lo/l0/lo_science.py +25 -24
  73. imap_processing/lo/l1a/lo_l1a.py +44 -0
  74. imap_processing/lo/l1b/lo_l1b.py +3 -3
  75. imap_processing/lo/l1c/lo_l1c.py +116 -50
  76. imap_processing/lo/l2/lo_l2.py +29 -29
  77. imap_processing/lo/lo_ancillary.py +55 -0
  78. imap_processing/lo/packet_definitions/lo_xtce.xml +5359 -106
  79. imap_processing/mag/constants.py +1 -0
  80. imap_processing/mag/l1a/mag_l1a.py +1 -0
  81. imap_processing/mag/l1a/mag_l1a_data.py +26 -0
  82. imap_processing/mag/l1b/mag_l1b.py +3 -2
  83. imap_processing/mag/l1c/interpolation_methods.py +14 -15
  84. imap_processing/mag/l1c/mag_l1c.py +23 -6
  85. imap_processing/mag/l1d/__init__.py +0 -0
  86. imap_processing/mag/l1d/mag_l1d.py +176 -0
  87. imap_processing/mag/l1d/mag_l1d_data.py +725 -0
  88. imap_processing/mag/l2/__init__.py +0 -0
  89. imap_processing/mag/l2/mag_l2.py +25 -20
  90. imap_processing/mag/l2/mag_l2_data.py +199 -130
  91. imap_processing/quality_flags.py +28 -2
  92. imap_processing/spice/geometry.py +101 -36
  93. imap_processing/spice/pointing_frame.py +1 -7
  94. imap_processing/spice/repoint.py +29 -2
  95. imap_processing/spice/spin.py +32 -8
  96. imap_processing/spice/time.py +60 -19
  97. imap_processing/swapi/l1/swapi_l1.py +10 -4
  98. imap_processing/swapi/l2/swapi_l2.py +66 -24
  99. imap_processing/swapi/swapi_utils.py +1 -1
  100. imap_processing/swe/l1b/swe_l1b.py +3 -6
  101. imap_processing/ultra/constants.py +28 -3
  102. imap_processing/ultra/l0/decom_tools.py +15 -8
  103. imap_processing/ultra/l0/decom_ultra.py +35 -11
  104. imap_processing/ultra/l0/ultra_utils.py +102 -12
  105. imap_processing/ultra/l1a/ultra_l1a.py +26 -6
  106. imap_processing/ultra/l1b/cullingmask.py +6 -3
  107. imap_processing/ultra/l1b/de.py +122 -26
  108. imap_processing/ultra/l1b/extendedspin.py +29 -2
  109. imap_processing/ultra/l1b/lookup_utils.py +424 -50
  110. imap_processing/ultra/l1b/quality_flag_filters.py +23 -0
  111. imap_processing/ultra/l1b/ultra_l1b_culling.py +356 -5
  112. imap_processing/ultra/l1b/ultra_l1b_extended.py +534 -90
  113. imap_processing/ultra/l1c/helio_pset.py +127 -7
  114. imap_processing/ultra/l1c/l1c_lookup_utils.py +256 -0
  115. imap_processing/ultra/l1c/spacecraft_pset.py +90 -15
  116. imap_processing/ultra/l1c/ultra_l1c.py +6 -0
  117. imap_processing/ultra/l1c/ultra_l1c_culling.py +85 -0
  118. imap_processing/ultra/l1c/ultra_l1c_pset_bins.py +446 -341
  119. imap_processing/ultra/l2/ultra_l2.py +0 -1
  120. imap_processing/ultra/utils/ultra_l1_utils.py +40 -3
  121. imap_processing/utils.py +3 -4
  122. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/METADATA +3 -3
  123. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/RECORD +126 -126
  124. imap_processing/idex/idex_l2c.py +0 -250
  125. imap_processing/spice/kernels.py +0 -187
  126. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_LeftSlit.csv +0 -526
  127. imap_processing/ultra/lookup_tables/Angular_Profiles_FM45_RightSlit.csv +0 -526
  128. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_LeftSlit.csv +0 -526
  129. imap_processing/ultra/lookup_tables/Angular_Profiles_FM90_RightSlit.csv +0 -524
  130. imap_processing/ultra/lookup_tables/EgyNorm.mem.csv +0 -32769
  131. imap_processing/ultra/lookup_tables/FM45_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  132. imap_processing/ultra/lookup_tables/FM90_Startup1_ULTRA_IMGPARAMS_20240719.csv +0 -2
  133. imap_processing/ultra/lookup_tables/dps_grid45_compressed.cdf +0 -0
  134. imap_processing/ultra/lookup_tables/ultra45_back-pos-luts.csv +0 -4097
  135. imap_processing/ultra/lookup_tables/ultra45_tdc_norm.csv +0 -2050
  136. imap_processing/ultra/lookup_tables/ultra90_back-pos-luts.csv +0 -4097
  137. imap_processing/ultra/lookup_tables/ultra90_tdc_norm.csv +0 -2050
  138. imap_processing/ultra/lookup_tables/yadjust.csv +0 -257
  139. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/LICENSE +0 -0
  140. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/WHEEL +0 -0
  141. {imap_processing-0.17.0.dist-info → imap_processing-0.19.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,201 @@
1
+ """Coverage time for each station."""
2
+
3
+ import logging
4
+
5
+ import numpy as np
6
+
7
+ from imap_processing.ialirt.constants import STATIONS
8
+ from imap_processing.ialirt.process_ephemeris import calculate_azimuth_and_elevation
9
+ from imap_processing.spice.time import et_to_utc, str_to_et
10
+
11
+ # Logger setup
12
+ logger = logging.getLogger(__name__)
13
+
14
+ ALL_STATIONS = [
15
+ *STATIONS.keys(),
16
+ "DSS-24",
17
+ "DSS-25",
18
+ "DSS-26",
19
+ "DSS-34",
20
+ "DSS-35",
21
+ "DSS-36",
22
+ "DSS-53",
23
+ "DSS-54",
24
+ "DSS-55",
25
+ "DSS-56",
26
+ "DSS-74",
27
+ "DSS-75",
28
+ ]
29
+
30
+
31
+ def generate_coverage(
32
+ start_time: str,
33
+ outages: dict | None = None,
34
+ dsn: dict | None = None,
35
+ ) -> tuple[dict, dict]:
36
+ """
37
+ Build the output dictionary containing coverage and outage time for each station.
38
+
39
+ Parameters
40
+ ----------
41
+ start_time : str
42
+ Start time in UTC.
43
+ outages : dict, optional
44
+ Dictionary of outages for each station.
45
+ dsn : dict, optional
46
+ Dictionary of Deep Space Network (DSN) stations.
47
+
48
+ Returns
49
+ -------
50
+ coverage_dict : dict
51
+ Visibility times per station.
52
+ outage_dict : dict
53
+ Outage times per station.
54
+ """
55
+ duration_seconds = 24 * 60 * 60 # 86400 seconds in 24 hours
56
+ time_step = 3600 # 1 hr in seconds
57
+
58
+ stations = {
59
+ "Kiel": STATIONS["Kiel"],
60
+ }
61
+ coverage_dict = {}
62
+ outage_dict = {}
63
+
64
+ start_et_input = str_to_et(start_time)
65
+ stop_et_input = start_et_input + duration_seconds
66
+
67
+ time_range = np.arange(start_et_input, stop_et_input, time_step)
68
+ total_visible_mask = np.zeros(time_range.shape, dtype=bool)
69
+
70
+ # Precompute DSN outage mask for non-DSN stations
71
+ dsn_outage_mask = np.zeros(time_range.shape, dtype=bool)
72
+ if dsn:
73
+ for dsn_contacts in dsn.values():
74
+ for start, end in dsn_contacts:
75
+ start_et = str_to_et(start)
76
+ end_et = str_to_et(end)
77
+ dsn_outage_mask |= (time_range >= start_et) & (time_range <= end_et)
78
+
79
+ for station_name, (lon, lat, alt, min_elevation) in stations.items():
80
+ azimuth, elevation = calculate_azimuth_and_elevation(lon, lat, alt, time_range)
81
+ visible = elevation > min_elevation
82
+
83
+ outage_mask = np.zeros(time_range.shape, dtype=bool)
84
+ if outages and station_name in outages:
85
+ for start, end in outages[station_name]:
86
+ start_et = str_to_et(start)
87
+ end_et = str_to_et(end)
88
+ outage_mask |= (time_range >= start_et) & (time_range <= end_et)
89
+
90
+ visible[outage_mask] = False
91
+ # DSN contacts block other stations
92
+ visible[dsn_outage_mask] = False
93
+ total_visible_mask |= visible
94
+
95
+ coverage_dict[station_name] = et_to_utc(time_range[visible], format_str="ISOC")
96
+ outage_dict[station_name] = et_to_utc(
97
+ time_range[outage_mask], format_str="ISOC"
98
+ )
99
+
100
+ # --- DSN Stations ---
101
+ if dsn:
102
+ for dsn_station, contacts in dsn.items():
103
+ dsn_visible_mask = np.zeros(time_range.shape, dtype=bool)
104
+ for start, end in contacts:
105
+ start_et = str_to_et(start)
106
+ end_et = str_to_et(end)
107
+ dsn_visible_mask |= (time_range >= start_et) & (time_range <= end_et)
108
+
109
+ # Apply DSN outages if present
110
+ outage_mask = np.zeros(time_range.shape, dtype=bool)
111
+ if outages and dsn_station in outages:
112
+ for start, end in outages[dsn_station]:
113
+ start_et = str_to_et(start)
114
+ end_et = str_to_et(end)
115
+ outage_mask |= (time_range >= start_et) & (time_range <= end_et)
116
+
117
+ dsn_visible_mask[outage_mask] = False
118
+ total_visible_mask |= dsn_visible_mask
119
+
120
+ coverage_dict[f"{dsn_station}"] = et_to_utc(
121
+ time_range[dsn_visible_mask], format_str="ISOC"
122
+ )
123
+ outage_dict[f"{dsn_station}"] = et_to_utc(
124
+ time_range[outage_mask], format_str="ISOC"
125
+ )
126
+
127
+ # Total coverage percentage
128
+ total_coverage_percent = (
129
+ np.count_nonzero(total_visible_mask) / time_range.size
130
+ ) * 100
131
+ coverage_dict["total_coverage_percent"] = total_coverage_percent
132
+
133
+ # Ensure all stations are present in both dicts
134
+ for station in ALL_STATIONS:
135
+ coverage_dict.setdefault(station, np.array([], dtype="<U23"))
136
+ outage_dict.setdefault(station, np.array([], dtype="<U23"))
137
+
138
+ return coverage_dict, outage_dict
139
+
140
+
141
+ def format_coverage_summary(
142
+ coverage_dict: dict, outage_dict: dict, start_time: str
143
+ ) -> dict:
144
+ """
145
+ Build the output dictionary containing coverage time for each station.
146
+
147
+ Parameters
148
+ ----------
149
+ coverage_dict : dict
150
+ Coverage for each station, keyed by station name with arrays of UTC times.
151
+ outage_dict : dict
152
+ Outage times for each station, keyed by station name with arrays of UTC times.
153
+ start_time : str
154
+ Start time in UTC.
155
+
156
+ Returns
157
+ -------
158
+ output_dict : dict
159
+ Formatted coverage summary.
160
+ """
161
+ # Include all known stations,
162
+ # plus any new ones that appear in coverage_dict.
163
+ all_stations = ALL_STATIONS + [
164
+ station
165
+ for station in coverage_dict.keys()
166
+ if station not in ALL_STATIONS and station != "total_coverage_percent"
167
+ ]
168
+
169
+ duration_seconds = 24 * 60 * 60 # 86400 seconds in 24 hours
170
+ time_step = 3600 # 1 hr in seconds
171
+
172
+ start_et_input = str_to_et(start_time)
173
+ stop_et_input = start_et_input + duration_seconds
174
+
175
+ time_range = np.arange(start_et_input, stop_et_input, time_step)
176
+ all_times = et_to_utc(time_range, format_str="ISOC")
177
+
178
+ data_rows = []
179
+ for time in all_times:
180
+ row = {"time": time}
181
+ for station in all_stations:
182
+ visible_times = coverage_dict.get(station, [])
183
+ outage_times = outage_dict.get(station, [])
184
+ if time in outage_times:
185
+ row[station] = "X"
186
+ elif time in visible_times:
187
+ row[station] = "1"
188
+ else:
189
+ row[station] = "0"
190
+ data_rows.append(row)
191
+
192
+ output_dict = {
193
+ "summary": "I-ALiRT Coverage Summary",
194
+ "generated": start_time,
195
+ "time_format": "UTC (ISOC)",
196
+ "stations": all_stations,
197
+ "total_coverage_percent": round(coverage_dict["total_coverage_percent"], 1),
198
+ "data": data_rows,
199
+ }
200
+
201
+ return output_dict
@@ -56,7 +56,10 @@ def get_rotation_matrix(axis: NDArray, angle: NDArray) -> NDArray:
56
56
  """
57
57
  angle_rad = np.radians(angle)
58
58
  rot_matrices = np.array(
59
- [spice.axisar(z, float(phase)) for z, phase in zip(axis, angle_rad)]
59
+ [
60
+ spice.axisar(z, float(phase))
61
+ for z, phase in zip(axis, angle_rad, strict=False)
62
+ ]
60
63
  )
61
64
 
62
65
  return rot_matrices
@@ -185,7 +188,7 @@ def transform_instrument_vectors_to_inertial(
185
188
  vectors = np.array(
186
189
  [
187
190
  spice.mxv(rot.T.copy(), vec)
188
- for rot, vec in zip(total_rotations, instrument_vectors)
191
+ for rot, vec in zip(total_rotations, instrument_vectors, strict=False)
189
192
  ]
190
193
  )
191
194
 
@@ -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,
@@ -20,7 +22,15 @@ from imap_processing.mag.l1b.mag_l1b import (
20
22
  calibrate_vector,
21
23
  shift_time,
22
24
  )
23
- from imap_processing.spice.time import met_to_ttj2000ns, met_to_utc
25
+ from imap_processing.mag.l1d.mag_l1d_data import MagL1d
26
+ from imap_processing.mag.l2.mag_l2_data import MagL2L1dBase
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
24
34
 
25
35
  logger = logging.getLogger(__name__)
26
36
 
@@ -193,7 +203,7 @@ def get_time(
193
203
  (grouped_data["group"] == group).values
194
204
  ][pkt_counter == 2]
195
205
 
196
- time_data: dict[str, Union[int, float]] = {
206
+ time_data: dict[str, int | float] = {
197
207
  "pri_coarsetm": int(pri_coarsetm.item()),
198
208
  "pri_fintm": int(pri_fintm.item()),
199
209
  "sec_coarsetm": int(sec_coarsetm.item()),
@@ -286,9 +296,238 @@ def calculate_l1b(
286
296
  return updated_vector_mago, updated_vector_magi, time_data
287
297
 
288
298
 
299
+ def calibrate_and_offset_vectors(
300
+ vectors: np.ndarray,
301
+ calibration: np.ndarray,
302
+ offsets: np.ndarray,
303
+ is_magi: bool = False,
304
+ ) -> np.ndarray:
305
+ """
306
+ Apply calibration and offsets to magnetic vectors.
307
+
308
+ Parameters
309
+ ----------
310
+ vectors : np.ndarray
311
+ Raw magnetic vectors, shape (n, 4).
312
+ calibration : np.ndarray
313
+ Calibration matrix, shape (3, 3, 4).
314
+ offsets : np.ndarray
315
+ Offsets array, shape (2, 4, 3) where:
316
+ - index 0 = MAGo, 1 = MAGi
317
+ - second index = range (0–3)
318
+ - third index = axis (x, y, z)
319
+ is_magi : bool, optional
320
+ True if applying to MAGi data, False for MAGo.
321
+
322
+ Returns
323
+ -------
324
+ calibrated_and_offset_vectors : np.ndarray
325
+ Calibrated and offset vectors, shape (n, 3).
326
+ """
327
+ # Apply calibration matrix -> (n,4)
328
+ # apply_calibration_offset_single_vector
329
+ calibrated = MagL2L1dBase.apply_calibration(vectors.reshape(1, 4), calibration)
330
+
331
+ # Apply offsets per vector
332
+ # vec shape (4)
333
+ # offsets shape (2, 4, 3) where first index is 0 for MAGo and 1 for MAGi
334
+ calibrated = np.array(
335
+ [
336
+ MagL1d.apply_calibration_offset_single_vector(vec, offsets, is_magi=is_magi)
337
+ for vec in calibrated
338
+ ]
339
+ )
340
+
341
+ return calibrated[:, :3]
342
+
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
+
289
526
  def process_packet(
290
- accumulated_data: xr.Dataset, calibration_dataset: xr.Dataset
291
- ) -> 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]:
292
531
  """
293
532
  Parse the MAG packets.
294
533
 
@@ -296,8 +535,10 @@ def process_packet(
296
535
  ----------
297
536
  accumulated_data : xr.Dataset
298
537
  Packets dataset accumulated over 1 min.
299
- calibration_dataset : xr.Dataset
300
- Calibration dataset.
538
+ engineering_calibration_dataset : xr.Dataset
539
+ Engineering calibration dataset.
540
+ l1d_calibration_dataset : xr.Dataset
541
+ L1D calibration dataset.
301
542
 
302
543
  Returns
303
544
  -------
@@ -323,8 +564,12 @@ def process_packet(
323
564
  grouped_data = find_groups(accumulated_data, (0, 3), "pkt_counter", "met")
324
565
 
325
566
  unique_groups = np.unique(grouped_data["group"])
326
- l1b_data = []
327
567
  mag_data = []
568
+ met_all = []
569
+ mago_vectors_all = []
570
+ mago_times_all = []
571
+ magi_vectors_all = []
572
+ magi_times_all = []
328
573
 
329
574
  for group in unique_groups:
330
575
  # Get status values for each group.
@@ -360,7 +605,7 @@ def process_packet(
360
605
  pkt_counter,
361
606
  science_data,
362
607
  status_data,
363
- calibration_dataset,
608
+ engineering_calibration_dataset,
364
609
  )
365
610
 
366
611
  # Note: primary = MAGo, secondary = MAGi.
@@ -371,36 +616,99 @@ def process_packet(
371
616
  if status_data["sec_isvalid"] == 0:
372
617
  updated_vector_magi = np.full(4, -32768)
373
618
 
374
- science_data.update(
375
- {
376
- "calibrated_pri_x": updated_vector_mago[0],
377
- "calibrated_pri_y": updated_vector_mago[1],
378
- "calibrated_pri_z": updated_vector_mago[2],
379
- "calibrated_sec_x": updated_vector_magi[0],
380
- "calibrated_sec_y": updated_vector_magi[1],
381
- "calibrated_sec_z": updated_vector_magi[2],
382
- }
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
383
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
+ ]
384
638
 
385
- l1b_data.append({**status_data, **science_data, **time_data})
639
+ attitude_time = met_to_ttj2000ns(
640
+ grouped_data["met"][(grouped_data["group"] == group).values]
641
+ )
642
+
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
+ )
386
660
 
387
- # Placeholder for real data.
388
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
+
389
693
  mag_data.append(
390
694
  {
391
695
  "apid": 478,
392
- "met": int(met.values.min()),
393
- "met_in_utc": met_to_utc(met.values.min()).split(".")[0],
394
- "ttj2000ns": int(met_to_ttj2000ns(met.values.min())),
395
- "mag_4s_b_gse": [Decimal("0.0") for _ in range(3)],
396
- "mag_4s_b_gsm": [Decimal("0.0") for _ in range(3)],
397
- "mag_4s_b_rtn": [Decimal("0.0") for _ in range(3)],
398
- "mag_phi_4s_b_gsm": Decimal("0.0"),
399
- "mag_theta_4s_b_gsm": 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])),
400
708
  }
401
709
  )
402
710
 
403
- return mag_data, l1b_data
711
+ return mag_data
404
712
 
405
713
 
406
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}