imap-processing 0.19.4__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.
- imap_processing/_version.py +2 -2
- imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml +13 -1
- imap_processing/cdf/config/imap_codice_l1a_variable_attrs.yaml +44 -44
- imap_processing/cdf/config/imap_codice_l1b_variable_attrs.yaml +127 -126
- imap_processing/cdf/config/imap_codice_l2-hi-omni_variable_attrs.yaml +635 -0
- imap_processing/cdf/config/imap_codice_l2-hi-sectored_variable_attrs.yaml +422 -0
- imap_processing/cdf/config/imap_constant_attrs.yaml +1 -1
- imap_processing/cdf/config/imap_enamaps_l2-common_variable_attrs.yaml +61 -55
- imap_processing/cdf/config/imap_enamaps_l2-healpix_variable_attrs.yaml +3 -2
- imap_processing/cdf/config/imap_enamaps_l2-rectangular_variable_attrs.yaml +17 -5
- imap_processing/cli.py +6 -11
- imap_processing/codice/codice_l1a.py +42 -21
- imap_processing/codice/codice_l2.py +640 -127
- imap_processing/codice/constants.py +224 -129
- imap_processing/ena_maps/ena_maps.py +124 -70
- imap_processing/ena_maps/utils/coordinates.py +5 -0
- imap_processing/ena_maps/utils/corrections.py +268 -0
- imap_processing/ena_maps/utils/map_utils.py +143 -42
- imap_processing/hi/hi_l2.py +10 -15
- imap_processing/ialirt/constants.py +7 -1
- imap_processing/ialirt/generate_coverage.py +1 -1
- imap_processing/ialirt/l0/ialirt_spice.py +1 -1
- imap_processing/ialirt/l0/parse_mag.py +33 -0
- imap_processing/ialirt/l0/process_codice.py +66 -0
- imap_processing/ialirt/utils/create_xarray.py +2 -0
- imap_processing/idex/idex_l2a.py +2 -2
- imap_processing/idex/idex_l2b.py +1 -1
- imap_processing/lo/l1c/lo_l1c.py +61 -3
- imap_processing/lo/l2/lo_l2.py +79 -11
- imap_processing/mag/l1a/mag_l1a.py +2 -2
- imap_processing/mag/l1a/mag_l1a_data.py +71 -13
- imap_processing/mag/l1c/interpolation_methods.py +34 -13
- imap_processing/mag/l1c/mag_l1c.py +117 -67
- imap_processing/mag/l1d/mag_l1d_data.py +3 -1
- imap_processing/spice/geometry.py +39 -28
- imap_processing/spice/pointing_frame.py +77 -50
- imap_processing/swapi/l1/swapi_l1.py +12 -4
- imap_processing/swe/utils/swe_constants.py +7 -7
- imap_processing/ultra/l1b/extendedspin.py +1 -1
- imap_processing/ultra/l1b/ultra_l1b_culling.py +2 -2
- imap_processing/ultra/l1b/ultra_l1b_extended.py +1 -1
- imap_processing/ultra/l1c/helio_pset.py +1 -1
- imap_processing/ultra/l1c/spacecraft_pset.py +2 -2
- imap_processing/ultra/l2/ultra_l2.py +3 -3
- imap_processing-1.0.1.dist-info/METADATA +121 -0
- {imap_processing-0.19.4.dist-info → imap_processing-1.0.1.dist-info}/RECORD +49 -47
- imap_processing-0.19.4.dist-info/METADATA +0 -120
- {imap_processing-0.19.4.dist-info → imap_processing-1.0.1.dist-info}/LICENSE +0 -0
- {imap_processing-0.19.4.dist-info → imap_processing-1.0.1.dist-info}/WHEEL +0 -0
- {imap_processing-0.19.4.dist-info → imap_processing-1.0.1.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
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
|
|
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 :
|
|
118
|
-
|
|
119
|
-
the
|
|
120
|
-
|
|
121
|
-
|
|
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[
|
|
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[
|
|
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[
|
|
195
|
-
phi=input_obj_az_el_output_frame[
|
|
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.
|
|
240
|
-
#
|
|
241
|
-
|
|
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
|
-
|
|
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 =
|
|
434
|
-
(
|
|
435
|
-
self.sky_grid.az_grid.ravel(),
|
|
436
|
-
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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()
|
|
691
|
+
|
|
642
692
|
|
|
643
|
-
class LoPointingSet(
|
|
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
|
|
698
|
-
# have the shape (n, 2) where n is the number of spatial pixels
|
|
699
|
-
|
|
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
|
-
|
|
838
|
-
|
|
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=
|
|
913
|
+
value_array=data_bc.values,
|
|
869
914
|
projection_grid_shape=self.binning_grid_shape,
|
|
870
|
-
projection_indices=
|
|
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 =
|
|
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
|
|
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
|
|
|
@@ -1331,25 +1379,28 @@ class RectangularSkyMap(AbstractSkyMap):
|
|
|
1331
1379
|
)
|
|
1332
1380
|
cdf_ds.attrs.update(map_attrs)
|
|
1333
1381
|
|
|
1334
|
-
# Set the variable attributes
|
|
1335
|
-
for
|
|
1382
|
+
# Set the variable and coordinate attributes
|
|
1383
|
+
for name, data_array in {**cdf_ds.data_vars, **cdf_ds.coords}.items():
|
|
1336
1384
|
try:
|
|
1337
|
-
#
|
|
1338
|
-
|
|
1339
|
-
check_schema =
|
|
1340
|
-
False if any(s in var for s in ignore_schema_substrings) else True
|
|
1341
|
-
)
|
|
1385
|
+
# We only check the schema on data variables that include "epoch"
|
|
1386
|
+
# in their list of dimensions (But not epoch itself).
|
|
1387
|
+
check_schema = name != "epoch" and "epoch" in data_array.dims
|
|
1342
1388
|
var_attrs = cdf_attrs.get_variable_attributes(
|
|
1343
|
-
variable_name=
|
|
1389
|
+
variable_name=name,
|
|
1344
1390
|
check_schema=check_schema,
|
|
1345
1391
|
)
|
|
1346
1392
|
except KeyError as e:
|
|
1347
1393
|
raise KeyError(
|
|
1348
|
-
f"Attributes for variable {
|
|
1394
|
+
f"Attributes for variable {name} not found in "
|
|
1349
1395
|
f"loaded variable attributes."
|
|
1350
1396
|
) from e
|
|
1351
1397
|
|
|
1352
|
-
cdf_ds[
|
|
1398
|
+
cdf_ds[name].attrs.update(var_attrs)
|
|
1399
|
+
|
|
1400
|
+
# Manually adjust epoch attributes
|
|
1401
|
+
cdf_ds["epoch"].attrs.update(
|
|
1402
|
+
{"DELTA_PLUS_VAR": "epoch_delta", "BIN_LOCATION": 0}
|
|
1403
|
+
)
|
|
1353
1404
|
|
|
1354
1405
|
return cdf_ds
|
|
1355
1406
|
|
|
@@ -1428,7 +1479,10 @@ class HealpixSkyMap(AbstractSkyMap):
|
|
|
1428
1479
|
nside=nside, ipix=np.arange(hp.nside2npix(nside)), nest=nested, lonlat=True
|
|
1429
1480
|
)
|
|
1430
1481
|
# Stack so axis 0 is different pixels, and axis 1 is (az, el) of the pixel
|
|
1431
|
-
self.az_el_points =
|
|
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
|
+
)
|
|
1432
1486
|
|
|
1433
1487
|
self.spatial_coords = {
|
|
1434
1488
|
CoordNames.HEALPIX_INDEX.value: xr.DataArray(
|
|
@@ -1764,7 +1818,7 @@ class HealpixSkyMap(AbstractSkyMap):
|
|
|
1764
1818
|
value_array=healpix_values_array,
|
|
1765
1819
|
max_subdivision_depth=max_subdivision_depth,
|
|
1766
1820
|
)
|
|
1767
|
-
for lon_lat in rect_map.az_el_points
|
|
1821
|
+
for lon_lat in rect_map.az_el_points.values
|
|
1768
1822
|
]
|
|
1769
1823
|
|
|
1770
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"
|
|
@@ -4,7 +4,23 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
import numpy as np
|
|
6
6
|
import pandas as pd
|
|
7
|
+
import xarray as xr
|
|
7
8
|
from numpy.polynomial import Polynomial
|
|
9
|
+
from scipy.constants import electron_volt, erg, proton_mass
|
|
10
|
+
|
|
11
|
+
from imap_processing.ena_maps.ena_maps import LoHiBasePointingSet
|
|
12
|
+
from imap_processing.ena_maps.utils.coordinates import CoordNames
|
|
13
|
+
from imap_processing.spice import geometry
|
|
14
|
+
from imap_processing.spice.time import ttj2000ns_to_et
|
|
15
|
+
|
|
16
|
+
# Physical constants for Compton-Getting correction
|
|
17
|
+
# Units: electron_volt = [J / eV]
|
|
18
|
+
# erg = [J / erg]
|
|
19
|
+
# To get [erg / eV], => electron_volt [J / eV] / erg [J / erg] = erg_per_ev [erg / eV]
|
|
20
|
+
ERG_PER_EV = electron_volt / erg # erg per eV - unit conversion factor
|
|
21
|
+
# Units: proton_mass = [kg]
|
|
22
|
+
# Here, we convert proton_mass to grams
|
|
23
|
+
PROTON_MASS_GRAMS = proton_mass * 1e3 # proton mass in grams
|
|
8
24
|
|
|
9
25
|
|
|
10
26
|
class PowerLawFluxCorrector:
|
|
@@ -289,3 +305,255 @@ class PowerLawFluxCorrector:
|
|
|
289
305
|
)
|
|
290
306
|
|
|
291
307
|
return corrected_flux, corrected_flux_stat_unc
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _add_spacecraft_velocity_to_pset(pset: LoHiBasePointingSet) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Calculate and add spacecraft velocity data to pointing set.
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
pset : LoHiBasePointingSet
|
|
317
|
+
Pointing set object to be updated.
|
|
318
|
+
|
|
319
|
+
Notes
|
|
320
|
+
-----
|
|
321
|
+
Adds the following DataArrays to pset.data:
|
|
322
|
+
- "sc_velocity": Spacecraft velocity vector (km/s) with dims ["x_y_z"]
|
|
323
|
+
- "sc_direction_vector": Spacecraft velocity unit vector with dims ["x_y_z"]
|
|
324
|
+
"""
|
|
325
|
+
# Compute ephemeris time (J2000 seconds) of PSET midpoint time
|
|
326
|
+
# TODO: Use the Pointing midpoint time. Epoch should be start time
|
|
327
|
+
# but use it until we can make Lo and Hi PSETs have a consistent
|
|
328
|
+
# variable to hold the midpoint time.
|
|
329
|
+
et = ttj2000ns_to_et(pset.data["epoch"].values[0])
|
|
330
|
+
# Get spacecraft state in HAE frame
|
|
331
|
+
sc_state = geometry.imap_state(et, ref_frame=geometry.SpiceFrame.IMAP_HAE)
|
|
332
|
+
sc_velocity_vector = sc_state[3:6]
|
|
333
|
+
|
|
334
|
+
# Store spacecraft velocity as DataArray
|
|
335
|
+
pset.data["sc_velocity"] = xr.DataArray(
|
|
336
|
+
sc_velocity_vector, dims=[CoordNames.CARTESIAN_VECTOR.value]
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
# Calculate spacecraft speed and direction
|
|
340
|
+
sc_velocity_km_per_sec = np.linalg.norm(
|
|
341
|
+
pset.data["sc_velocity"], axis=-1, keepdims=True
|
|
342
|
+
)
|
|
343
|
+
pset.data["sc_direction_vector"] = pset.data["sc_velocity"] / sc_velocity_km_per_sec
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _add_cartesian_look_direction(pset: LoHiBasePointingSet) -> None:
|
|
347
|
+
"""
|
|
348
|
+
Calculate and add look direction vectors to pointing set.
|
|
349
|
+
|
|
350
|
+
Parameters
|
|
351
|
+
----------
|
|
352
|
+
pset : LoHiBasePointingSet
|
|
353
|
+
Pointing set object to be updated.
|
|
354
|
+
|
|
355
|
+
Notes
|
|
356
|
+
-----
|
|
357
|
+
Adds the following DataArray to pset.data:
|
|
358
|
+
- "look_direction": Cartesian unit vectors with dims [...spatial_dims, "x_y_z"]
|
|
359
|
+
"""
|
|
360
|
+
longitudes = pset.data["hae_longitude"]
|
|
361
|
+
latitudes = pset.data["hae_latitude"]
|
|
362
|
+
|
|
363
|
+
# Stack spherical coordinates (r=1 for unit vectors, azimuth, elevation)
|
|
364
|
+
spherical_coords = np.stack(
|
|
365
|
+
[
|
|
366
|
+
np.ones_like(longitudes), # r = 1 for unit vectors
|
|
367
|
+
longitudes, # azimuth = longitude
|
|
368
|
+
latitudes, # elevation = latitude
|
|
369
|
+
],
|
|
370
|
+
axis=-1,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Convert to Cartesian coordinates and store as DataArray
|
|
374
|
+
pset.data["look_direction"] = xr.DataArray(
|
|
375
|
+
geometry.spherical_to_cartesian(spherical_coords),
|
|
376
|
+
dims=[*longitudes.dims, CoordNames.CARTESIAN_VECTOR.value],
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _calculate_compton_getting_transform(
|
|
381
|
+
pset: LoHiBasePointingSet,
|
|
382
|
+
energy_hf: xr.DataArray,
|
|
383
|
+
) -> None:
|
|
384
|
+
"""
|
|
385
|
+
Apply Compton-Getting transformation to compute ENA source directions.
|
|
386
|
+
|
|
387
|
+
This implements the Compton-Getting velocity transformation to correct
|
|
388
|
+
for the motion of the spacecraft through the heliosphere. The transformation
|
|
389
|
+
accounts for the Doppler shift of ENA energies and the aberration of
|
|
390
|
+
arrival directions.
|
|
391
|
+
|
|
392
|
+
All calculations are performed using xarray DataArrays to preserve
|
|
393
|
+
dimension information throughout the computation.
|
|
394
|
+
|
|
395
|
+
Parameters
|
|
396
|
+
----------
|
|
397
|
+
pset : LoHiBasePointingSet
|
|
398
|
+
Pointing set object with sc_velocity, sc_direction_vector, and
|
|
399
|
+
look_direction already added.
|
|
400
|
+
energy_hf : xr.DataArray
|
|
401
|
+
ENA energies in the heliosphere frame in eV.
|
|
402
|
+
|
|
403
|
+
Notes
|
|
404
|
+
-----
|
|
405
|
+
The algorithm is based on the "Appendix A. The IMAP-Lo Mapping Algorithms"
|
|
406
|
+
document.
|
|
407
|
+
Adds the following DataArrays to pset.data:
|
|
408
|
+
- "energy_sc": ENA energies in spacecraft frame (eV)
|
|
409
|
+
- "ena_source_hae_longitude": ENA source longitudes in heliosphere frame (degrees)
|
|
410
|
+
- "ena_source_hae_latitude": ENA source latitudes in heliosphere frame (degrees)
|
|
411
|
+
"""
|
|
412
|
+
# Store heliosphere frame energies
|
|
413
|
+
pset.data["energy_hf"] = energy_hf
|
|
414
|
+
|
|
415
|
+
# Calculate spacecraft speed
|
|
416
|
+
sc_velocity_km_per_sec = np.linalg.norm(
|
|
417
|
+
pset.data["sc_velocity"], axis=-1, keepdims=True
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
# Calculate dot product between look directions and spacecraft direction vector
|
|
421
|
+
# Use Einstein summation for efficient vectorized dot product
|
|
422
|
+
dot_product = xr.DataArray(
|
|
423
|
+
np.einsum(
|
|
424
|
+
"...i,...i->...",
|
|
425
|
+
pset.data["look_direction"],
|
|
426
|
+
pset.data["sc_direction_vector"],
|
|
427
|
+
),
|
|
428
|
+
dims=pset.data["look_direction"].dims[:-1],
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
# Calculate the kinetic energy of a hydrogen ENA traveling at spacecraft velocity
|
|
432
|
+
# E_u = (1/2) * m * U_sc^2 (convert km/s to cm/s with 1.0e5 factor)
|
|
433
|
+
energy_u = (
|
|
434
|
+
0.5 * PROTON_MASS_GRAMS * (sc_velocity_km_per_sec * 1e5) ** 2 / ERG_PER_EV
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Note: Tim thinks that this approach seems backwards. Here, we are assuming
|
|
438
|
+
# that ENAs are observed in the heliosphere frame at the ESA energy levels.
|
|
439
|
+
# We then calculate the velocity that said ENAs would have in the spacecraft
|
|
440
|
+
# frame as well as the CG corrected energy level in the spacecraft frame.
|
|
441
|
+
# We then use this velocity to calculate and the velocity of the spacecraft
|
|
442
|
+
# to do the vector math which determines the ENA source direction in the
|
|
443
|
+
# heliosphere frame.
|
|
444
|
+
# The ENAs are in fact observed in the spacecraft frame at a known energy
|
|
445
|
+
# level in the spacecraft frame. Why don't we use that energy level to
|
|
446
|
+
# calculate the source direction in the spacecraft frame and then do the
|
|
447
|
+
# vector math to find the source direction in the heliosphere frame? We
|
|
448
|
+
# would also need to calculate the CG corrected ENA energy in the heliosphere
|
|
449
|
+
# frame and keep track of that when binning.
|
|
450
|
+
|
|
451
|
+
# Calculate y values for each energy level (Equation 61)
|
|
452
|
+
# y_k = sqrt(E^h_k / E^u)
|
|
453
|
+
y = np.sqrt(pset.data["energy_hf"] / energy_u)
|
|
454
|
+
|
|
455
|
+
# Velocity magnitude factor calculation (Equation 62)
|
|
456
|
+
# x_k = (êₛ · û_sc) + sqrt(y² + (êₛ · û_sc)² - 1)
|
|
457
|
+
x = dot_product + np.sqrt(y**2 + dot_product**2 - 1)
|
|
458
|
+
|
|
459
|
+
# Calculate ENA speed in the spacecraft frame
|
|
460
|
+
# |v⃗_sc| = x_k * U_sc
|
|
461
|
+
velocity_sc = x * sc_velocity_km_per_sec
|
|
462
|
+
|
|
463
|
+
# Calculate the kinetic energy in the spacecraft frame
|
|
464
|
+
# E_sc = (1/2) * M_p * v_sc² (convert km/s to cm/s with 1.0e5 factor)
|
|
465
|
+
pset.data["energy_sc"] = (
|
|
466
|
+
0.5 * PROTON_MASS_GRAMS * (velocity_sc * 1e5) ** 2 / ERG_PER_EV
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
# Calculate the velocity vector in the spacecraft frame
|
|
470
|
+
# v⃗_sc = |v_sc| * êₛ (velocity direction follows look direction)
|
|
471
|
+
velocity_vector_sc = velocity_sc * pset.data["look_direction"]
|
|
472
|
+
|
|
473
|
+
# Calculate the ENA velocity vector in the heliosphere frame
|
|
474
|
+
# v⃗_helio = v⃗_sc - U⃗_sc (simple velocity addition)
|
|
475
|
+
velocity_vector_helio = velocity_vector_sc - pset.data["sc_velocity"]
|
|
476
|
+
|
|
477
|
+
# Convert to spherical coordinates to get ENA source directions
|
|
478
|
+
ena_source_direction_helio = geometry.cartesian_to_spherical(
|
|
479
|
+
velocity_vector_helio.data
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
# Update the PSET hae_longitude and hae_latitude variables with the new
|
|
483
|
+
# energy-dependent values.
|
|
484
|
+
pset.data["hae_longitude"] = (
|
|
485
|
+
pset.data["energy_sc"].dims,
|
|
486
|
+
ena_source_direction_helio[..., 1],
|
|
487
|
+
)
|
|
488
|
+
pset.data["hae_latitude"] = (
|
|
489
|
+
pset.data["energy_sc"].dims,
|
|
490
|
+
ena_source_direction_helio[..., 2],
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# For ram/anti-ram filtering we can use the sign of the scalar projection
|
|
494
|
+
# of the ENA source direction onto the spacecraft velocity vector.
|
|
495
|
+
# ram_mask = (v⃗_helio · û_sc) >= 0
|
|
496
|
+
ram_mask = (
|
|
497
|
+
np.einsum(
|
|
498
|
+
"...i,...i->...", velocity_vector_helio, pset.data["sc_direction_vector"]
|
|
499
|
+
)
|
|
500
|
+
>= 0
|
|
501
|
+
)
|
|
502
|
+
pset.data["ram_mask"] = xr.DataArray(
|
|
503
|
+
ram_mask,
|
|
504
|
+
dims=velocity_vector_helio.dims[:-1],
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def apply_compton_getting_correction(
|
|
509
|
+
pset: LoHiBasePointingSet,
|
|
510
|
+
energy_hf: xr.DataArray,
|
|
511
|
+
) -> None:
|
|
512
|
+
"""
|
|
513
|
+
Apply Compton-Getting correction to a pointing set and update coordinates.
|
|
514
|
+
|
|
515
|
+
This function performs the Compton-Getting velocity transformation to correct
|
|
516
|
+
ENA observations for the motion of the spacecraft through the heliosphere.
|
|
517
|
+
The corrected coordinates represent the true source directions of the ENAs
|
|
518
|
+
in the heliosphere frame.
|
|
519
|
+
|
|
520
|
+
The pointing set is modified in-place: new variables are added to the dataset
|
|
521
|
+
for the corrected coordinates and energies, and the az_el_points attribute
|
|
522
|
+
is updated to use the corrected coordinates for binning.
|
|
523
|
+
|
|
524
|
+
All calculations are performed using xarray DataArrays to preserve dimension
|
|
525
|
+
information throughout the computation.
|
|
526
|
+
|
|
527
|
+
Parameters
|
|
528
|
+
----------
|
|
529
|
+
pset : LoHiBasePointingSet
|
|
530
|
+
Pointing set object containing HAE longitude/latitude coordinates.
|
|
531
|
+
energy_hf : xr.DataArray
|
|
532
|
+
ENA energies in the heliosphere frame in eV. Must be 1D with an
|
|
533
|
+
energy dimension.
|
|
534
|
+
|
|
535
|
+
Notes
|
|
536
|
+
-----
|
|
537
|
+
This function adds the following variables to the pointing set dataset:
|
|
538
|
+
- "sc_velocity": Spacecraft velocity vector (km/s)
|
|
539
|
+
- "sc_direction_vector": Spacecraft velocity unit vector
|
|
540
|
+
- "look_direction": Cartesian unit vectors of observation directions
|
|
541
|
+
- "energy_hf": ENA energies in heliosphere frame (eV)
|
|
542
|
+
- "energy_sc": ENA energies in spacecraft frame (eV)
|
|
543
|
+
- "ena_source_hae_longitude": ENA source longitudes in heliosphere frame (degrees)
|
|
544
|
+
- "ena_source_hae_latitude": ENA source latitudes in heliosphere frame (degrees)
|
|
545
|
+
|
|
546
|
+
The az_el_points attribute is updated to use the corrected coordinates,
|
|
547
|
+
which will be used for subsequent binning operations.
|
|
548
|
+
"""
|
|
549
|
+
# Step 1: Add spacecraft velocity and direction to pset
|
|
550
|
+
_add_spacecraft_velocity_to_pset(pset)
|
|
551
|
+
|
|
552
|
+
# Step 2: Calculate and add look direction vectors to pset
|
|
553
|
+
_add_cartesian_look_direction(pset)
|
|
554
|
+
|
|
555
|
+
# Step 3: Apply Compton-Getting transformation
|
|
556
|
+
_calculate_compton_getting_transform(pset, energy_hf)
|
|
557
|
+
|
|
558
|
+
# Step 4: Update az_el_points to use the corrected coordinates
|
|
559
|
+
pset.update_az_el_points()
|