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
File without changes
@@ -6,8 +6,7 @@ import xarray as xr
6
6
  from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
7
7
  from imap_processing.mag import imap_mag_sdc_configuration_v001 as configuration
8
8
  from imap_processing.mag.constants import DataMode
9
- from imap_processing.mag.l1b.mag_l1b import calibrate_vector
10
- from imap_processing.mag.l2.mag_l2_data import MagL2
9
+ from imap_processing.mag.l2.mag_l2_data import MagL2, ValidFrames
11
10
 
12
11
 
13
12
  def mag_l2(
@@ -77,8 +76,8 @@ def mag_l2(
77
76
  always_output_mago = configuration.ALWAYS_OUTPUT_MAGO
78
77
 
79
78
  # TODO Check that the input file matches the offsets file
80
- # if not np.array_equal(input_data["epoch"].data, offsets_dataset["epoch"].data):
81
- # raise ValueError("Input file and offsets file must have the same timestamps.")
79
+ if not np.array_equal(input_data["epoch"].data, offsets_dataset["epoch"].data):
80
+ raise ValueError("Input file and offsets file must have the same timestamps.")
82
81
 
83
82
  day: np.datetime64 = day_to_process.astype("datetime64[D]")
84
83
 
@@ -86,29 +85,35 @@ def mag_l2(
86
85
  calibration_dataset, day, always_output_mago
87
86
  )
88
87
 
89
- vectors = np.apply_along_axis(
90
- func1d=calibrate_vector,
91
- axis=1,
92
- arr=input_data["vectors"].data,
93
- calibration_matrix=calibration_matrix,
88
+ cal_vectors = MagL2.apply_calibration(
89
+ vectors=input_data["vectors"].data, calibration_matrix=calibration_matrix
94
90
  )
95
-
96
- input_data = MagL2(
97
- vectors[:, :3], # level 2 vectors don't include range
98
- input_data["epoch"].data,
99
- input_data["vectors"].data[:, 3],
100
- {},
101
- np.zeros(len(input_data["epoch"].data)),
102
- np.zeros(len(input_data["epoch"].data)),
103
- mode,
91
+ # level 2 vectors don't include range
92
+ vectors = cal_vectors[:, :3]
93
+
94
+ l2_data = MagL2(
95
+ vectors=vectors,
96
+ epoch=input_data["epoch"].data,
97
+ range=input_data["vectors"].data[:, 3],
98
+ global_attributes={},
99
+ quality_flags=offsets_dataset["quality_flag"].data,
100
+ quality_bitmask=offsets_dataset["quality_bitmask"].data,
101
+ data_mode=mode,
104
102
  offsets=offsets_dataset["offsets"].data,
105
103
  timedelta=offsets_dataset["timedeltas"].data,
106
104
  )
105
+
107
106
  attributes = ImapCdfAttributes()
108
107
  attributes.add_instrument_global_attrs("mag")
109
- # temporarily point to l1c
110
108
  attributes.add_instrument_variable_attrs("mag", "l2")
111
- return [input_data.generate_dataset(attributes, day)]
109
+
110
+ # Rotate from the MAG frame into the SRF frame
111
+ l2_data.rotate_frame(ValidFrames.SRF)
112
+ imap_srf = l2_data.generate_dataset(attributes, day)
113
+ l2_data.rotate_frame(ValidFrames.DSRF)
114
+ imap_dsrf = l2_data.generate_dataset(attributes, day)
115
+
116
+ return [imap_dsrf, imap_srf]
112
117
 
113
118
 
114
119
  def retrieve_matrix_from_l2_calibration(
@@ -7,36 +7,43 @@ import numpy as np
7
7
  import xarray as xr
8
8
 
9
9
  from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
10
- from imap_processing.mag.constants import DataMode
10
+ from imap_processing.mag.constants import FILLVAL, DataMode
11
+ from imap_processing.mag.l1b.mag_l1b import calibrate_vector
12
+ from imap_processing.spice.geometry import SpiceFrame, frame_transform
11
13
  from imap_processing.spice.time import (
12
14
  et_to_ttj2000ns,
13
15
  str_to_et,
16
+ ttj2000ns_to_et,
14
17
  )
15
18
 
16
19
 
17
20
  class ValidFrames(Enum):
18
21
  """SPICE reference frames for output."""
19
22
 
20
- dsrf = "dsrf"
21
- srf = "srf"
22
- rtn = "rtn"
23
- gse = "gse"
23
+ MAG = SpiceFrame.IMAP_MAG
24
+ DSRF = SpiceFrame.IMAP_DPS
25
+ SRF = SpiceFrame.IMAP_SPACECRAFT
26
+ GSE = SpiceFrame.IMAP_GSE
27
+ RTN = SpiceFrame.IMAP_RTN
24
28
 
25
29
 
26
- @dataclass
27
- class MagL2:
30
+ @dataclass(kw_only=True)
31
+ class MagL2L1dBase:
28
32
  """
29
- Dataclass for MAG L2 data.
33
+ Base class for MAG L2 and L1D data.
30
34
 
31
- Since L2 and L1D should have the same structure, this can be used for either level.
35
+ Since these two data levels output identical files, and share some methods, this
36
+ superclass captures the tools in common, while allowing each subclass to define
37
+ individual attributes and algorithms.
32
38
 
33
- Some of the methods are also static, so they can be used in i-ALiRT processing.
39
+ May also be extended for I-ALiRT.
34
40
 
35
41
  Attributes
36
42
  ----------
37
43
  vectors: np.ndarray
38
44
  Magnetic field vectors of size (n, 3) where n is the number of vectors.
39
- Describes (x, y, z) components of the magnetic field.
45
+ Describes (x, y, z) components of the magnetic field. This field is the output
46
+ vectors, which are nominally from the MAGo sensor.
40
47
  epoch: np.ndarray
41
48
  Time of each vector in J2000 seconds. Should be of length n.
42
49
  range: np.ndarray
@@ -48,10 +55,11 @@ class MagL2:
48
55
  quality_bitmask: np.ndarray
49
56
  Quality bitmask for each vector. Should be of length n. Copied from offset
50
57
  file in L2, marked as good always in L1D.
51
- magnitude: np.ndarray
52
- Magnitude of each vector. Should be of length n. Calculated from L2 vectors.
53
- is_l1d: bool
54
- Flag to indicate if the data is L1D. Defaults to False.
58
+ frame:
59
+ The reference frame of the input vectors. Starts as the MAG instrument frame.
60
+ epoch_et: np.ndarray
61
+ The epoch timestamps converted to ET format. Used for frame transformations.
62
+ Calculated on first use and then saved. Should not be passed in.
55
63
  """
56
64
 
57
65
  vectors: np.ndarray
@@ -62,120 +70,13 @@ class MagL2:
62
70
  quality_bitmask: np.ndarray
63
71
  data_mode: DataMode
64
72
  magnitude: np.ndarray = field(init=False)
65
- is_l1d: bool = False
66
- offsets: InitVar[np.ndarray] = None
67
- timedelta: InitVar[np.ndarray] = None
68
-
69
- def __post_init__(self, offsets: np.ndarray, timedelta: np.ndarray) -> None:
70
- """
71
- Calculate the magnitude of the vectors after initialization.
72
-
73
- Parameters
74
- ----------
75
- offsets : np.ndarray
76
- Offsets to apply to the vectors. Should be of shape (n, 3) where n is the
77
- number of vectors.
78
- timedelta : np.ndarray
79
- Time deltas to shift the timestamps by. Should be of length n.
80
- Given in seconds.
81
- """
82
- if offsets is not None:
83
- self.vectors = self.apply_offsets(self.vectors, offsets)
84
- if timedelta is not None:
85
- self.epoch = self.shift_timestamps(self.epoch, timedelta)
86
-
87
- self.magnitude = self.calculate_magnitude(self.vectors)
88
-
89
- @staticmethod
90
- def calculate_magnitude(
91
- vectors: np.ndarray,
92
- ) -> np.ndarray:
93
- """
94
- Given a list of vectors (x, y, z), calculate the magnitude of each vector.
95
-
96
- For an input list of vectors of size (n, 3) returns a list of magnitudes of
97
- size (n,).
98
-
99
- Parameters
100
- ----------
101
- vectors : np.ndarray
102
- Array of vectors to calculate the magnitude of.
103
-
104
- Returns
105
- -------
106
- np.ndarray
107
- Array of magnitudes of the input vectors.
108
- """
109
- return np.linalg.norm(vectors, axis=1) # type: ignore
110
-
111
- @staticmethod
112
- def apply_offsets(vectors: np.ndarray, offsets: np.ndarray) -> np.ndarray:
113
- """
114
- Apply the offsets to the vectors by adding them together.
115
-
116
- These offsets are used to shift the vectors in the x, y, and z directions.
117
- They can either be provided through a custom offsets datafile, or calculated
118
- using a gradiometry algorithm.
119
-
120
- Parameters
121
- ----------
122
- vectors : np.ndarray
123
- Array of vectors to apply the offsets to. Should be of shape (n, 3) where n
124
- is the number of vectors.
125
- offsets : np.ndarray
126
- Array of offsets to apply to the vectors. Should be of shape (n, 3) where n
127
- is the number of vectors.
128
-
129
- Returns
130
- -------
131
- np.ndarray
132
- Array of vectors with offsets applied. Should be of shape (n, 3).
133
- """
134
- if vectors.shape[0] != offsets.shape[0]:
135
- raise ValueError("Vectors and offsets must have the same length.")
136
-
137
- offset_vectors: np.ndarray = vectors[:, :3] + offsets
138
-
139
- # TODO: CDF files don't have NaNs. Emailed MAG to ask what this will look like.
140
- # Any values where offsets is nan must also be nan
141
- offset_vectors[np.isnan(offsets).any(axis=1)] = np.nan
142
-
143
- return offset_vectors
144
-
145
- @staticmethod
146
- def shift_timestamps(epoch: np.ndarray, timedelta: np.ndarray) -> np.ndarray:
147
- """
148
- Shift the timestamps by the given timedelta.
149
-
150
- If timedelta is positive, the epochs are shifted forward in time.
151
-
152
- Parameters
153
- ----------
154
- epoch : np.ndarray
155
- Array of timestamps to shift. Should be of length n.
156
- timedelta : np.ndarray
157
- Array of time deltas to shift the timestamps by. Should be the same length
158
- as epoch. Given in seconds.
159
-
160
- Returns
161
- -------
162
- np.ndarray
163
- Shifted timestamps.
164
- """
165
- if epoch.shape[0] != timedelta.shape[0]:
166
- raise ValueError(
167
- "Input Epoch and offsets timedeltas must be the same length."
168
- )
169
-
170
- timedelta_ns = timedelta * 1e9
171
- shifted_timestamps = epoch + timedelta_ns
172
- return shifted_timestamps
73
+ frame: ValidFrames = ValidFrames.MAG
74
+ epoch_et: np.ndarray | None = field(init=False, default=None)
173
75
 
174
76
  def generate_dataset(
175
77
  self,
176
78
  attribute_manager: ImapCdfAttributes,
177
79
  day: np.datetime64,
178
- frame: ValidFrames = ValidFrames.dsrf,
179
80
  ) -> xr.Dataset:
180
81
  """
181
82
  Generate an xarray dataset from the dataclass.
@@ -189,8 +90,6 @@ class MagL2:
189
90
  CDF attributes object for the correct level.
190
91
  day : np.datetime64
191
92
  The 24 hour day to process, as a numpy datetime format.
192
- frame : ValidFrames
193
- SPICE reference frame to rotate the data into.
194
93
 
195
94
  Returns
196
95
  -------
@@ -199,7 +98,9 @@ class MagL2:
199
98
  """
200
99
  self.truncate_to_24h(day)
201
100
 
202
- logical_source_id = f"imap_mag_l2_{self.data_mode.value.lower()}-{frame.name}"
101
+ logical_source_id = (
102
+ f"imap_mag_l2_{self.data_mode.value.lower()}-{self.frame.name.lower()}"
103
+ )
203
104
  direction = xr.DataArray(
204
105
  np.arange(3),
205
106
  name="direction",
@@ -242,8 +143,8 @@ class MagL2:
242
143
  )
243
144
 
244
145
  quality_bitmask = xr.DataArray(
245
- self.quality_flags,
246
- name="quality_flags",
146
+ self.quality_bitmask,
147
+ name="quality_bitmask",
247
148
  dims=["epoch"],
248
149
  attrs=attribute_manager.get_variable_attributes("qf"),
249
150
  )
@@ -298,7 +199,6 @@ class MagL2:
298
199
  """
299
200
  if self.epoch.shape[0] != self.vectors.shape[0]:
300
201
  raise ValueError("Timestamps and vectors are not the same shape!")
301
-
302
202
  start_timestamp_j2000 = et_to_ttj2000ns(str_to_et(str(timestamp)))
303
203
  end_timestamp_j2000 = et_to_ttj2000ns(
304
204
  str_to_et(str(timestamp + np.timedelta64(1, "D")))
@@ -313,3 +213,172 @@ class MagL2:
313
213
  self.magnitude = self.magnitude[day_start_index:day_end_index]
314
214
  self.quality_flags = self.quality_flags[day_start_index:day_end_index]
315
215
  self.quality_bitmask = self.quality_bitmask[day_start_index:day_end_index]
216
+
217
+ @staticmethod
218
+ def calculate_magnitude(
219
+ vectors: np.ndarray,
220
+ ) -> np.ndarray:
221
+ """
222
+ Given a list of vectors (x, y, z), calculate the magnitude of each vector.
223
+
224
+ For an input list of vectors of size (n, 3) returns a list of magnitudes of
225
+ size (n,).
226
+
227
+ Parameters
228
+ ----------
229
+ vectors : np.ndarray
230
+ Array of vectors to calculate the magnitude of.
231
+
232
+ Returns
233
+ -------
234
+ np.ndarray
235
+ Array of magnitudes of the input vectors.
236
+ """
237
+ return np.linalg.norm(vectors, axis=1)
238
+
239
+ @staticmethod
240
+ def apply_calibration(
241
+ vectors: np.ndarray, calibration_matrix: np.ndarray
242
+ ) -> np.ndarray:
243
+ """
244
+ Apply the calibration matrix to the vectors.
245
+
246
+ This works by repeatedly calling the function calibrate_vector on the vectors
247
+ input.
248
+
249
+ Parameters
250
+ ----------
251
+ vectors : np.ndarray
252
+ Array of vectors to apply the calibration to, including x,y,z and range.
253
+ Should be of shape (n, 4) where n is the number of vectors.
254
+ calibration_matrix : np.ndarray
255
+ Calibration matrix to apply to the vectors. Should be of shape (3, 3, 4).
256
+
257
+ Returns
258
+ -------
259
+ np.ndarray
260
+ Array of calibrated vectors. Should be of shape (n, 4).
261
+ """
262
+ calibrated_vectors = np.apply_along_axis(
263
+ func1d=calibrate_vector,
264
+ axis=1,
265
+ arr=vectors,
266
+ calibration_matrix=calibration_matrix,
267
+ )
268
+
269
+ return calibrated_vectors
270
+
271
+ @staticmethod
272
+ def shift_timestamps(epoch: np.ndarray, timedelta: np.ndarray) -> np.ndarray:
273
+ """
274
+ Shift the timestamps by the given timedelta.
275
+
276
+ If timedelta is positive, the epochs are shifted forward in time.
277
+
278
+ Parameters
279
+ ----------
280
+ epoch : np.ndarray
281
+ Array of timestamps to shift. Should be of length n.
282
+ timedelta : np.ndarray
283
+ Array of time deltas to shift the timestamps by. Should be the same length
284
+ as epoch. Given in seconds.
285
+
286
+ Returns
287
+ -------
288
+ np.ndarray
289
+ Shifted timestamps.
290
+ """
291
+ if epoch.shape[0] != timedelta.shape[0]:
292
+ raise ValueError(
293
+ "Input Epoch and offsets timedeltas must be the same length."
294
+ )
295
+
296
+ timedelta_ns = timedelta * 1e9
297
+ shifted_timestamps = epoch + timedelta_ns
298
+ return shifted_timestamps
299
+
300
+ def rotate_frame(self, end_frame: ValidFrames) -> None:
301
+ """
302
+ Rotate the vector data in the class to the output frame.
303
+
304
+ Parameters
305
+ ----------
306
+ end_frame : ValidFrames
307
+ The frame to rotate the data to. Must be one of the ValidFrames enum
308
+ values.
309
+ """
310
+ if self.epoch_et is None:
311
+ self.epoch_et = ttj2000ns_to_et(self.epoch)
312
+ self.vectors = frame_transform(
313
+ self.epoch_et,
314
+ self.vectors,
315
+ from_frame=self.frame.value,
316
+ to_frame=end_frame.value,
317
+ )
318
+ self.frame = end_frame
319
+
320
+
321
+ @dataclass(kw_only=True)
322
+ class MagL2(MagL2L1dBase):
323
+ """
324
+ Dataclass for MAG L2 data.
325
+
326
+ Since L2 and L1D should have the same structure, this can be used for either level.
327
+
328
+ Some of the methods are also static, so they can be used in i-ALiRT processing.
329
+ """
330
+
331
+ offsets: InitVar[np.ndarray] = None
332
+ timedelta: InitVar[np.ndarray] = None
333
+
334
+ def __post_init__(self, offsets: np.ndarray, timedelta: np.ndarray) -> None:
335
+ """
336
+ Calculate the magnitude of the vectors after initialization.
337
+
338
+ Parameters
339
+ ----------
340
+ offsets : np.ndarray
341
+ Offsets to apply to the vectors. Should be of shape (n, 3) where n is the
342
+ number of vectors.
343
+ timedelta : np.ndarray
344
+ Time deltas to shift the timestamps by. Should be of length n.
345
+ Given in seconds.
346
+ """
347
+ if offsets is not None:
348
+ self.vectors = self.apply_offsets(self.vectors, offsets)
349
+ if timedelta is not None:
350
+ self.epoch = self.shift_timestamps(self.epoch, timedelta)
351
+
352
+ self.magnitude = self.calculate_magnitude(self.vectors)
353
+
354
+ @staticmethod
355
+ def apply_offsets(vectors: np.ndarray, offsets: np.ndarray) -> np.ndarray:
356
+ """
357
+ Apply the offsets to the vectors by adding them together.
358
+
359
+ These offsets are used to shift the vectors in the x, y, and z directions.
360
+ They can either be provided through a custom offsets datafile, or calculated
361
+ using a gradiometry algorithm.
362
+
363
+ Parameters
364
+ ----------
365
+ vectors : np.ndarray
366
+ Array of vectors to apply the offsets to. Should be of shape (n, 3) where n
367
+ is the number of vectors.
368
+ offsets : np.ndarray
369
+ Array of offsets to apply to the vectors. Should be of shape (n, 3) where n
370
+ is the number of vectors.
371
+
372
+ Returns
373
+ -------
374
+ np.ndarray
375
+ Array of vectors with offsets applied. Should be of shape (n, 3).
376
+ """
377
+ if vectors.shape[0] != offsets.shape[0]:
378
+ raise ValueError("Vectors and offsets must have the same length.")
379
+
380
+ offset_vectors: np.ndarray = vectors + offsets
381
+
382
+ # Any values where offsets is FILLVAL must also be FILLVAL
383
+ offset_vectors[(offsets == FILLVAL).any(axis=1), :] = FILLVAL
384
+ return offset_vectors
@@ -37,6 +37,14 @@ class ENAFlags(FlagNameMixin):
37
37
  BADSPIN = 2**2 # bit 2, Bad spin
38
38
 
39
39
 
40
+ class ImapDEOutliersUltraFlags(FlagNameMixin):
41
+ """IMAP Ultra flags."""
42
+
43
+ NONE = CommonFlags.NONE
44
+ FOV = 2**0 # bit 0
45
+ PHCORR = 2**1 # bit 1
46
+
47
+
40
48
  class ImapHkUltraFlags(FlagNameMixin):
41
49
  """IMAP Ultra flags."""
42
50
 
@@ -53,14 +61,32 @@ class ImapAttitudeUltraFlags(FlagNameMixin):
53
61
  NONE = CommonFlags.NONE
54
62
  SPINRATE = 2**0 # bit 0
55
63
  AUXMISMATCH = 2**1 # bit 1 # aux packet does not match Universal Spin Table
64
+ SPINPHASE = 2**2 # bit 2 # spin phase flagged by Universal Spin Table
65
+ SPINPERIOD = 2**3 # bit 3 # spin period flagged by Universal Spin Table
56
66
 
57
67
 
58
68
  class ImapRatesUltraFlags(FlagNameMixin):
59
69
  """IMAP Ultra Rates flags."""
60
70
 
61
71
  NONE = CommonFlags.NONE
62
- ZEROCOUNTS = 2**0 # bit 0
63
- HIGHRATES = 2**1 # bit 1
72
+ HIGHRATES = 2**0 # bit 0
73
+ FIRSTSPIN = 2**1 # bit 1
74
+ LASTSPIN = 2**2 # bit 2
75
+ PARTIALSPIN = 2**2 # bit 2
76
+
77
+
78
+ class ImapDEScatteringUltraFlags(FlagNameMixin):
79
+ """IMAP Ultra Scattering flags."""
80
+
81
+ NONE = CommonFlags.NONE
82
+ ABOVE_THRESHOLD = 2**0 # bit 0
83
+ NAN_PHI_OR_THETA = 2**1 # bit 1
84
+
85
+
86
+ class ImapInstrumentUltraFlags(FlagNameMixin):
87
+ """IMAP Ultra flags using other instruments."""
88
+
89
+ NONE = CommonFlags.NONE
64
90
 
65
91
 
66
92
  class ImapLoFlags(FlagNameMixin):