imap-processing 1.0.0__py3-none-any.whl → 1.0.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.

Potentially problematic release.


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

Files changed (43) hide show
  1. imap_processing/_version.py +2 -2
  2. imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +13 -1
  3. imap_processing/cdf/config/imap_codice_l2-hi-omni_variable_attrs.yaml +635 -0
  4. imap_processing/cdf/config/imap_codice_l2-hi-sectored_variable_attrs.yaml +422 -0
  5. imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +28 -21
  6. imap_processing/cdf/config/imap_enamaps_l2-healpix_variable_attrs.yaml +2 -0
  7. imap_processing/cdf/config/imap_enamaps_l2-rectangular_variable_attrs.yaml +12 -2
  8. imap_processing/cli.py +6 -11
  9. imap_processing/codice/codice_l2.py +640 -127
  10. imap_processing/codice/constants.py +61 -0
  11. imap_processing/ena_maps/ena_maps.py +111 -60
  12. imap_processing/ena_maps/utils/coordinates.py +5 -0
  13. imap_processing/ena_maps/utils/corrections.py +268 -0
  14. imap_processing/ena_maps/utils/map_utils.py +143 -42
  15. imap_processing/hi/hi_l2.py +3 -8
  16. imap_processing/ialirt/constants.py +7 -1
  17. imap_processing/ialirt/generate_coverage.py +1 -1
  18. imap_processing/ialirt/l0/process_codice.py +66 -0
  19. imap_processing/ialirt/utils/create_xarray.py +1 -0
  20. imap_processing/idex/idex_l2a.py +2 -2
  21. imap_processing/idex/idex_l2b.py +1 -1
  22. imap_processing/lo/l1c/lo_l1c.py +61 -3
  23. imap_processing/lo/l2/lo_l2.py +79 -11
  24. imap_processing/mag/l1a/mag_l1a.py +2 -2
  25. imap_processing/mag/l1a/mag_l1a_data.py +71 -13
  26. imap_processing/mag/l1c/interpolation_methods.py +34 -13
  27. imap_processing/mag/l1c/mag_l1c.py +117 -67
  28. imap_processing/mag/l1d/mag_l1d_data.py +3 -1
  29. imap_processing/spice/geometry.py +11 -9
  30. imap_processing/spice/pointing_frame.py +77 -50
  31. imap_processing/swapi/l1/swapi_l1.py +12 -4
  32. imap_processing/swe/utils/swe_constants.py +7 -7
  33. imap_processing/ultra/l1b/extendedspin.py +1 -1
  34. imap_processing/ultra/l1b/ultra_l1b_culling.py +2 -2
  35. imap_processing/ultra/l1b/ultra_l1b_extended.py +1 -1
  36. imap_processing/ultra/l1c/helio_pset.py +1 -1
  37. imap_processing/ultra/l1c/spacecraft_pset.py +2 -2
  38. imap_processing-1.0.1.dist-info/METADATA +121 -0
  39. {imap_processing-1.0.0.dist-info → imap_processing-1.0.1.dist-info}/RECORD +42 -40
  40. imap_processing-1.0.0.dist-info/METADATA +0 -120
  41. {imap_processing-1.0.0.dist-info → imap_processing-1.0.1.dist-info}/LICENSE +0 -0
  42. {imap_processing-1.0.0.dist-info → imap_processing-1.0.1.dist-info}/WHEEL +0 -0
  43. {imap_processing-1.0.0.dist-info → imap_processing-1.0.1.dist-info}/entry_points.txt +0 -0
@@ -92,6 +92,26 @@ LO_SW_SPECIES_VARIABLE_NAMES = [
92
92
  "heplus",
93
93
  "cnoplus",
94
94
  ]
95
+ LO_SW_SOLAR_WIND_SPECIES_VARIABLE_NAMES = [
96
+ "hplus",
97
+ "heplusplus",
98
+ "cplus4",
99
+ "cplus5",
100
+ "cplus6",
101
+ "oplus5",
102
+ "oplus6",
103
+ "oplus7",
104
+ "oplus8",
105
+ "ne",
106
+ "mg",
107
+ "si",
108
+ "fe_loq",
109
+ "fe_hiq",
110
+ ]
111
+ LO_SW_PICKUP_ION_SPECIES_VARIABLE_NAMES = [
112
+ "heplus",
113
+ "cnoplus",
114
+ ]
95
115
  LO_NSW_SPECIES_VARIABLE_NAMES = [
96
116
  "hplus",
97
117
  "heplusplus",
@@ -2259,3 +2279,44 @@ HALF_SPIN_LUT = {
2259
2279
  30: [116, 117, 118, 119, 120, 121],
2260
2280
  31: [122, 123, 124, 125, 126, 127],
2261
2281
  }
2282
+
2283
+ NSW_POSITIONS = [x for x in range(3, 22)]
2284
+ SW_POSITIONS = [0]
2285
+ PUI_POSITIONS = [0, 1, 2, 22, 23]
2286
+ L2_GEOMETRIC_FACTOR = 0.013
2287
+ L2_HI_NUMBER_OF_SSD = 12.0
2288
+
2289
+ L2_HI_SECTORED_ANGLE = np.array(
2290
+ [
2291
+ 285.00,
2292
+ 244.11,
2293
+ 228.69,
2294
+ 225.00,
2295
+ 228.69,
2296
+ 244.11,
2297
+ 285.00,
2298
+ 325.89,
2299
+ 341.31,
2300
+ 345.00,
2301
+ 341.31,
2302
+ 325.89,
2303
+ ]
2304
+ )
2305
+
2306
+ HI_L2_ELEVATION_ANGLE = np.array(
2307
+ [
2308
+ 150.0,
2309
+ 138.6,
2310
+ 115.7,
2311
+ 90.0,
2312
+ 64.3,
2313
+ 41.4,
2314
+ 30.0,
2315
+ 41.4,
2316
+ 64.3,
2317
+ 90.0,
2318
+ 115.7,
2319
+ 138.6,
2320
+ ],
2321
+ dtype=float,
2322
+ )
@@ -73,7 +73,7 @@ def match_coords_to_indices(
73
73
  input_object: PointingSet | AbstractSkyMap,
74
74
  output_object: PointingSet | AbstractSkyMap,
75
75
  event_et: float | None = None,
76
- ) -> NDArray:
76
+ ) -> NDArray | xr.DataArray:
77
77
  """
78
78
  Find the output indices corresponding to each input coord between 2 spatial objects.
79
79
 
@@ -87,7 +87,7 @@ def match_coords_to_indices(
87
87
  This function always "pushes" the pixels of the input object to corresponding pixels
88
88
  in the output object's unwrapped rectangular grid or healpix tessellation;
89
89
  however, by swapping the input and output objects, one can apply the "pull" method
90
- of index matching.
90
+ of index matching.
91
91
 
92
92
  At present, the allowable inputs are either:
93
93
  - A PointingSet object and a SkyMap object, in either order of input/output.
@@ -97,7 +97,7 @@ def match_coords_to_indices(
97
97
  Parameters
98
98
  ----------
99
99
  input_object : PointingSet | AbstractSkyMap
100
- An object containing 1D spatial pixel centers in azimuth and elevation,
100
+ An object containing spatial pixel centers in azimuth and elevation,
101
101
  which will be matched to 1D indices of spatial pixels in the output frame.
102
102
  Must contain the Spice frame in which the pixel centers are defined.
103
103
  output_object : PointingSet | AbstractSkyMap
@@ -114,11 +114,13 @@ def match_coords_to_indices(
114
114
 
115
115
  Returns
116
116
  -------
117
- flat_indices_input_grid_output_frame : NDArray
118
- 1D array of pixel indices of the output object corresponding to each pixel in
119
- the input object. The length of the array is equal to the number of pixels in
120
- the input object, and may contain 0, 1, or multiple occurrences of the same
121
- output index.
117
+ flat_indices_input_grid_output_frame : xr.DataArray
118
+ Array of pixel indices mapping each input object pixel center to a pixel
119
+ in the output object. The output xr.DataArray will have the same leading
120
+ dimension labels preserved. The shape of the output array is (..., n)
121
+ where ... matches the non-spatial dimensions of the input object, and n
122
+ is the number of spatial pixels in the input object. Output indices may
123
+ contain 0, 1, or multiple occurrences of the same output index.
122
124
 
123
125
  Raises
124
126
  ------
@@ -166,14 +168,14 @@ def match_coords_to_indices(
166
168
  # use ravel_multi_index to get the 1D indices of the pixels in the output frame.
167
169
  az_indices = (
168
170
  np.digitize(
169
- input_obj_az_el_output_frame[:, 0],
171
+ input_obj_az_el_output_frame[..., 0],
170
172
  output_object.sky_grid.az_bin_edges,
171
173
  )
172
174
  - 1
173
175
  )
174
176
  el_indices = (
175
177
  np.digitize(
176
- input_obj_az_el_output_frame[:, 1],
178
+ input_obj_az_el_output_frame[..., 1],
177
179
  output_object.sky_grid.el_bin_edges,
178
180
  )
179
181
  - 1
@@ -191,8 +193,8 @@ def match_coords_to_indices(
191
193
  # which directly returns the index on the output frame's Healpix tessellation.
192
194
  flat_indices_input_grid_output_frame = hp.ang2pix(
193
195
  nside=output_object.nside,
194
- theta=input_obj_az_el_output_frame[:, 0], # Lon in degrees
195
- phi=input_obj_az_el_output_frame[:, 1], # Lat in degrees
196
+ theta=input_obj_az_el_output_frame[..., 0], # Lon in degrees
197
+ phi=input_obj_az_el_output_frame[..., 1], # Lat in degrees
196
198
  nest=output_object.nested,
197
199
  lonlat=True,
198
200
  )
@@ -202,6 +204,14 @@ def match_coords_to_indices(
202
204
  f"Received: {output_object.tiling_type}"
203
205
  )
204
206
 
207
+ # Wrap the output indices in a DataArray with the same leading dimensions as
208
+ # the input object az_el_points to preserve broadcasting information
209
+ input_dims = input_obj_az_el_input_frame.dims[:-1]
210
+ flat_indices_input_grid_output_frame = xr.DataArray(
211
+ flat_indices_input_grid_output_frame,
212
+ dims=input_dims,
213
+ )
214
+
205
215
  return flat_indices_input_grid_output_frame
206
216
 
207
217
 
@@ -236,9 +246,10 @@ class PointingSet(ABC):
236
246
  spice_reference_frame: geometry.SpiceFrame
237
247
 
238
248
  # ======== Attributes required to be set in a subclass ========
239
- # Azimuth and elevation coordinates of each spatial pixel. The ndarray should
240
- # have the shape (n, 2) where n is the number of spatial pixels
241
- az_el_points: np.ndarray
249
+ # Azimuth and elevation coordinates of each spatial pixel. Must be an
250
+ # xr.DataArray with dimensions (..., spatial_dim, az_el_coord) to preserve
251
+ # dimension labels
252
+ az_el_points: xr.DataArray
242
253
  # Tuple containing the names of each spatial coordinate of the xarray.Dataset
243
254
  # stored in the data attribute
244
255
  spatial_coords: tuple[str, ...]
@@ -274,7 +285,9 @@ class PointingSet(ABC):
274
285
  num_points: int
275
286
  The number of spatial pixels in the pointing set.
276
287
  """
277
- return self.az_el_points.shape[0]
288
+ # Last dimension is az/el vector, the second to last dimension is
289
+ # the number of pixels.
290
+ return self.az_el_points.shape[-2]
278
291
 
279
292
  @property
280
293
  def epoch(self) -> int:
@@ -430,11 +443,12 @@ class RectangularPointingSet(PointingSet):
430
443
  # into shape (number of points in tiling of the sky, 2) where
431
444
  # column 0 (az_el_points[:, 0]) is the azimuth of that point and
432
445
  # column 1 (az_el_points[:, 1]) is the elevation of that point.
433
- self.az_el_points = np.column_stack(
434
- (
435
- self.sky_grid.az_grid.ravel(),
436
- self.sky_grid.el_grid.ravel(),
437
- )
446
+ self.az_el_points = xr.DataArray(
447
+ np.stack(
448
+ (self.sky_grid.az_grid.ravel(), self.sky_grid.el_grid.ravel()),
449
+ axis=-1,
450
+ ),
451
+ dims=[CoordNames.GENERIC_PIXEL.value, CoordNames.AZ_EL_VECTOR.value],
438
452
  )
439
453
 
440
454
 
@@ -540,8 +554,9 @@ class UltraPointingSet(HealpixPointingSet):
540
554
  # The coordinates of the healpix pixel centers are stored as a 2D array
541
555
  # of shape (num_points, 2) where column 0 is the lon/az
542
556
  # and column 1 is the lat/el.
543
- self.az_el_points = np.column_stack(
544
- (azimuth_pixel_center, elevation_pixel_center)
557
+ self.az_el_points = xr.DataArray(
558
+ np.stack((azimuth_pixel_center, elevation_pixel_center), axis=-1),
559
+ dims=[CoordNames.GENERIC_PIXEL.value, CoordNames.AZ_EL_VECTOR.value],
545
560
  )
546
561
 
547
562
  @property
@@ -586,7 +601,45 @@ class UltraPointingSet(HealpixPointingSet):
586
601
  )
587
602
 
588
603
 
589
- class HiPointingSet(PointingSet):
604
+ class LoHiBasePointingSet(PointingSet):
605
+ """
606
+ Base class for Lo and Hi pointing sets with HAE coordinate data.
607
+
608
+ This class provides common functionality for pointing sets that contain
609
+ hae_longitude and hae_latitude coordinates in the dataset.
610
+ """
611
+
612
+ tiling_type: SkyTilingType = SkyTilingType.RECTANGULAR
613
+
614
+ def update_az_el_points(self) -> None:
615
+ """
616
+ Update the az_el_points instance variable with new az/el coordinates.
617
+
618
+ The values store in the "hae_longitude" and "hae_latitude" variables
619
+ are used to construct the azimuth and elevation coordinates.
620
+ """
621
+ # Get lon/lat coordinates, squeeze the epoch dimension and stack along
622
+ # the spatial dimensions. xarray.stack() takes possibly multiple spatial
623
+ # dimensions and reshapes those into a single dimension.
624
+ az_stacked = (
625
+ self.data["hae_longitude"]
626
+ .squeeze("epoch")
627
+ .stack({CoordNames.GENERIC_PIXEL.value: self.spatial_coords})
628
+ )
629
+ el_stacked = (
630
+ self.data["hae_latitude"]
631
+ .squeeze("epoch")
632
+ .stack({CoordNames.GENERIC_PIXEL.value: self.spatial_coords})
633
+ )
634
+
635
+ # Stack lon/lat along last axis to create shape (..., 2)
636
+ self.az_el_points = xr.DataArray(
637
+ np.stack([az_stacked.values, el_stacked.values], axis=-1),
638
+ dims=[*az_stacked.dims, CoordNames.AZ_EL_VECTOR.value],
639
+ )
640
+
641
+
642
+ class HiPointingSet(LoHiBasePointingSet):
590
643
  """
591
644
  PointingSet object specific to Hi L1C PSet data.
592
645
 
@@ -602,7 +655,7 @@ class HiPointingSet(PointingSet):
602
655
  super().__init__(dataset, spice_reference_frame=geometry.SpiceFrame.ECLIPJ2000)
603
656
 
604
657
  # Filter out ENAs from non-selected portions of the spin.
605
- if spin_phase not in ["full", "ram", "anti-ram"]:
658
+ if spin_phase not in ["full", "ram", "anti"]:
606
659
  raise ValueError(f"Unrecognized spin_phase value: {spin_phase}.")
607
660
  # ram only includes spin-phase interval [0, 0.5)
608
661
  # which is the first half of the spin_angle_bins
@@ -612,7 +665,7 @@ class HiPointingSet(PointingSet):
612
665
  )
613
666
  # anti-ram includes spin-phase interval [0.5, 1)
614
667
  # which is the second half of the spin_angle_bins
615
- elif spin_phase == "anti-ram":
668
+ elif spin_phase == "anti":
616
669
  self.data = self.data.isel(
617
670
  spin_angle_bin=slice(self.data["spin_angle_bin"].data.size // 2, None)
618
671
  )
@@ -631,16 +684,13 @@ class HiPointingSet(PointingSet):
631
684
  self.data["exposure_factor"], self.data["epoch"].values[0]
632
685
  )
633
686
 
634
- self.az_el_points = np.column_stack(
635
- (
636
- np.squeeze(self.data["hae_longitude"]),
637
- np.squeeze(self.data["hae_latitude"]),
638
- )
639
- )
640
687
  self.spatial_coords = ("spin_angle_bin",)
641
688
 
689
+ # Update az_el_points using the base class method
690
+ self.update_az_el_points()
642
691
 
643
- class LoPointingSet(PointingSet):
692
+
693
+ class LoPointingSet(LoHiBasePointingSet):
644
694
  """
645
695
  PointingSet object specific to Lo L1C PSet data.
646
696
 
@@ -653,15 +703,11 @@ class LoPointingSet(PointingSet):
653
703
  def __init__(self, dataset: xr.Dataset):
654
704
  super().__init__(dataset, spice_reference_frame=geometry.SpiceFrame.IMAP_HAE)
655
705
 
656
- # The HAE centers are stored in the pset as (1, 3600, 40) arrays
657
- self.az_el_points = np.column_stack(
658
- (
659
- np.squeeze(self.data["hae_longitude"]).values.ravel(),
660
- np.squeeze(self.data["hae_latitude"]).values.ravel(),
661
- )
662
- )
663
706
  self.spatial_coords = ("spin_angle", "off_angle")
664
707
 
708
+ # Update az_el_points using the base class method
709
+ self.update_az_el_points()
710
+
665
711
 
666
712
  # Define the Map classes
667
713
  class AbstractSkyMap(ABC):
@@ -694,9 +740,10 @@ class AbstractSkyMap(ABC):
694
740
  max_epoch: int
695
741
 
696
742
  # ======== Attributes required to be set in a subclass ========
697
- # Azimuth and elevation coordinates of each spatial pixel. The ndarray should
698
- # have the shape (n, 2) where n is the number of spatial pixels
699
- az_el_points: np.ndarray
743
+ # Azimuth and elevation coordinates of each spatial pixel. The xarray.DataArray
744
+ # should have the shape (n, 2) where n is the number of spatial pixels.
745
+ # Always a simple numpy array for maps (no need for multi-dimensional coords).
746
+ az_el_points: xr.DataArray
700
747
  # Type of sky tiling
701
748
  tiling_type: SkyTilingType
702
749
  # Dictionary of xr.DataArray objects for each non-spatial coordinate in the SkyMap
@@ -829,19 +876,11 @@ class AbstractSkyMap(ABC):
829
876
  )
830
877
 
831
878
  for value_key in value_keys:
832
- pset_values = pointing_set.data[value_key]
833
-
834
879
  # If multiple spatial axes present
835
880
  # (i.e (az, el) for rectangular coordinate PSET),
836
881
  # flatten them in the values array to match the raveled indices
837
- non_spatial_axes_shape = tuple(
838
- size
839
- for key, size in pset_values.sizes.items()
840
- if key not in pointing_set.spatial_coords
841
- )
842
- raveled_pset_data = pset_values.data.reshape(
843
- *non_spatial_axes_shape,
844
- pointing_set.num_points,
882
+ raveled_pset_data = pointing_set.data[value_key].stack(
883
+ {CoordNames.GENERIC_PIXEL.value: pointing_set.spatial_coords}
845
884
  )
846
885
 
847
886
  if value_key not in self.data_1d.data_vars:
@@ -864,10 +903,16 @@ class AbstractSkyMap(ABC):
864
903
  if index_match_method is IndexMatchMethod.PUSH:
865
904
  # Bin the values at the matched indices. There may be multiple
866
905
  # pointing set pixels that correspond to the same sky map pixel.
906
+ # Broadcast all arrays together using xarray dimension alignment
907
+ data_bc, indices_bc = xr.broadcast(
908
+ raveled_pset_data, matched_indices_push
909
+ )
910
+
911
+ # Extract numpy arrays for bincount operation
867
912
  pointing_projected_values = map_utils.bin_single_array_at_indices(
868
- value_array=raveled_pset_data,
913
+ value_array=data_bc.values,
869
914
  projection_grid_shape=self.binning_grid_shape,
870
- projection_indices=matched_indices_push,
915
+ projection_indices=indices_bc.values,
871
916
  input_valid_mask=pset_valid_mask,
872
917
  )
873
918
  # TODO: we may need to allow for unweighted/weighted means here by
@@ -879,7 +924,7 @@ class AbstractSkyMap(ABC):
879
924
  valid_map_mask = pset_valid_mask[matched_indices_pull]
880
925
  # We know that there will only be one value per sky map pixel,
881
926
  # so we can use the matched indices directly
882
- pointing_projected_values = raveled_pset_data[
927
+ pointing_projected_values = raveled_pset_data.values[
883
928
  ..., matched_indices_pull[valid_map_mask]
884
929
  ]
885
930
  # TODO: we may need to allow for unweighted/weighted means here by
@@ -1120,7 +1165,10 @@ class RectangularSkyMap(AbstractSkyMap):
1120
1165
  el_points = self.sky_grid.el_grid.ravel()
1121
1166
 
1122
1167
  # Stack so axis 0 is different pixels, and axis 1 is (az, el) of the pixel
1123
- self.az_el_points = np.column_stack((az_points, el_points))
1168
+ self.az_el_points = xr.DataArray(
1169
+ np.column_stack((az_points, el_points)),
1170
+ dims=[CoordNames.GENERIC_PIXEL.value, CoordNames.AZ_EL_VECTOR.value],
1171
+ )
1124
1172
 
1125
1173
  # Calculate solid angles of each pixel in the map grid in units of steradians
1126
1174
  self.solid_angle_grid = spatial_utils.build_solid_angle_map(
@@ -1267,7 +1315,7 @@ class RectangularSkyMap(AbstractSkyMap):
1267
1315
  if ("L2" in name)
1268
1316
  ]
1269
1317
  l2_coords.append(CoordNames.TIME.value)
1270
- for map_coord in cdf_ds.dims.keys():
1318
+ for map_coord in cdf_ds.dims:
1271
1319
  if map_coord not in l2_coords:
1272
1320
  cdf_ds = cdf_ds.drop_dims(map_coord)
1273
1321
 
@@ -1431,7 +1479,10 @@ class HealpixSkyMap(AbstractSkyMap):
1431
1479
  nside=nside, ipix=np.arange(hp.nside2npix(nside)), nest=nested, lonlat=True
1432
1480
  )
1433
1481
  # Stack so axis 0 is different pixels, and axis 1 is (az, el) of the pixel
1434
- self.az_el_points = np.column_stack((pixel_az, pixel_el))
1482
+ self.az_el_points = xr.DataArray(
1483
+ np.column_stack((pixel_az, pixel_el)),
1484
+ dims=[CoordNames.GENERIC_PIXEL.value, CoordNames.AZ_EL_VECTOR.value],
1485
+ )
1435
1486
 
1436
1487
  self.spatial_coords = {
1437
1488
  CoordNames.HEALPIX_INDEX.value: xr.DataArray(
@@ -1767,7 +1818,7 @@ class HealpixSkyMap(AbstractSkyMap):
1767
1818
  value_array=healpix_values_array,
1768
1819
  max_subdivision_depth=max_subdivision_depth,
1769
1820
  )
1770
- for lon_lat in rect_map.az_el_points
1821
+ for lon_lat in rect_map.az_el_points.values
1771
1822
  ]
1772
1823
 
1773
1824
  # Separate the best value and the recursion depth for each pixel
@@ -18,3 +18,8 @@ class CoordNames(Enum):
18
18
  ELEVATION_L1C = "latitude"
19
19
  AZIMUTH_L2 = "longitude"
20
20
  ELEVATION_L2 = "latitude"
21
+
22
+ # Common name for dimension along azimuth/elevation vector
23
+ AZ_EL_VECTOR = "az_el"
24
+ # Commoon name for dimension along Cartesian vector
25
+ CARTESIAN_VECTOR = "x_y_z"