Skip to content

Commit f22cada

Browse files
author
Menlo Innovations - CAVA Project
committed
Harrison 2382 - SWYN/PSCH - Rotate temperature and velocity in moments calculation
1 parent 20b0b3a commit f22cada

7 files changed

Lines changed: 144 additions & 42 deletions

File tree

imap_processing/swe/l3/science/moment_calculations.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import math
22
from dataclasses import dataclass
3+
from datetime import datetime
34

45
import numpy as np
6+
from spiceypy import spiceypy
57

68
from imap_processing.constants import ELECTRON_MASS_KG, \
79
BOLTZMANN_CONSTANT_JOULES_PER_KELVIN
@@ -109,7 +111,6 @@ class Moments:
109111
ao: float
110112

111113

112-
# TODO figure out what the moments are and return a dataclass with them
113114
def calculate_fit_temperature_density_velocity(parameters: np.ndarray[float]):
114115
moments = Moments(alpha=0,
115116
beta=0,
@@ -226,3 +227,23 @@ def filter_and_flatten_regress_parameters(corrected_energy_bins: np.ndarray,
226227
yreg[filtered_phase_space_density <= 1e-35] = -80.6
227228

228229
return velocity_vectors[valid_mask], weights[valid_mask], yreg
230+
231+
232+
def rotate_dps_vector_to_rtn(epoch: datetime, vector: np.ndarray[float]) -> np.ndarray[float]:
233+
et_time = spiceypy.datetime2et(epoch)
234+
rotation_matrix = spiceypy.pxform("IMAP_DPS", "IMAP_RTN", et_time)
235+
return rotation_matrix @ vector
236+
237+
238+
def rotate_temperature(epoch: datetime, alpha: float, beta: float):
239+
sin_dec = np.sin(beta)
240+
x = sin_dec * np.cos(alpha)
241+
y = sin_dec * np.sin(alpha)
242+
z = np.cos(beta)
243+
244+
rtn_temperature = rotate_dps_vector_to_rtn(epoch, np.array([x, y, z]))
245+
246+
phi = np.asin(rtn_temperature[2])
247+
theta = np.atan2(rtn_temperature[1], rtn_temperature[0])
248+
249+
return theta, phi

imap_processing/swe/swe_processor.py

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
import numpy as np
44
from imap_data_access import upload
55

6+
from imap_processing import spice_wrapper
67
from imap_processing.data_utils import find_closest_neighbor
78
from imap_processing.processor import Processor
89
from imap_processing.swapi.l3a.science.calculate_pickup_ion import calculate_solar_wind_velocity_vector
910
from imap_processing.swe.l3.models import SweL3Data
1011
from imap_processing.swe.l3.science.moment_calculations import compute_maxwellian_weight_factors, \
11-
filter_and_flatten_regress_parameters, regress, calculate_fit_temperature_density_velocity
12+
filter_and_flatten_regress_parameters, regress, calculate_fit_temperature_density_velocity, rotate_temperature, \
13+
rotate_dps_vector_to_rtn
1214
from imap_processing.swe.l3.science.pitch_calculations import average_over_look_directions, find_breakpoints, \
1315
correct_and_rebin, \
1416
integrate_distribution_to_get_1d_spectrum, integrate_distribution_to_get_inbound_and_outbound_1d_spectrum, \
@@ -26,22 +28,21 @@ def process(self):
2628
upload(output_cdf)
2729

2830
def calculate_moment_products(self, dependencies: SweL3Dependencies):
31+
spice_wrapper.furnish()
2932
swe_l2_data = dependencies.swe_l2_data
3033
swe_epoch = swe_l2_data.epoch
3134
config = dependencies.configuration
3235

3336
spacecraft_potential_history = [config["spacecraft_potential_initial_guess"] for _ in
34-
range(4)]
35-
halo_core_history = [config["core_halo_breakpoint_initial_guess"] for _ in range(4)]
37+
range(3)]
38+
halo_core_history = [config["core_halo_breakpoint_initial_guess"] for _ in range(3)]
3639

3740
for i in range(len(swe_epoch)):
38-
averaged_phase_space_density = average_over_look_directions(swe_l2_data.phase_space_density[i],
39-
np.array(config["geometric_fractions"]))
40-
41-
spacecraft_potential, halo_core = find_breakpoints(swe_l2_data.energy, averaged_phase_space_density,
42-
np.average(spacecraft_potential_history[:3]),
43-
np.average(halo_core_history[:3]),
44-
spacecraft_potential_history[-1], halo_core_history[-1],
41+
averaged_psd = average_over_look_directions(swe_l2_data.phase_space_density[i],
42+
np.array(config["geometric_fractions"]))
43+
spacecraft_potential, halo_core = find_breakpoints(swe_l2_data.energy, averaged_psd,
44+
spacecraft_potential_history,
45+
halo_core_history,
4546
config)
4647

4748
spacecraft_potential_history = [*spacecraft_potential_history[1:], spacecraft_potential]
@@ -53,7 +54,7 @@ def calculate_moment_products(self, dependencies: SweL3Dependencies):
5354
swe_l2_data.inst_az_spin_sector[i])
5455

5556
# TODO: read these from L1 dataset
56-
ccounts = np.reshape(np.arange(24 * 30 * 7), (24, 30, 7)) * 1000
57+
ccounts = np.reshape(np.arange(20 * 30 * 7), (20, 30, 7)) * 1000
5758

5859
weights: np.ndarray[float] = compute_maxwellian_weight_factors(ccounts)
5960

@@ -80,6 +81,11 @@ def calculate_moment_products(self, dependencies: SweL3Dependencies):
8081
else:
8182
halo_core_breakpoint_index -= 1
8283

84+
rtn_velocity = rotate_dps_vector_to_rtn(swe_epoch[i],
85+
np.array(
86+
[moments.velocity_x, moments.velocity_y, moments.velocity_z]))
87+
rotate_temperature(swe_epoch[i], moments.alpha, moments.beta)
88+
8389
def calculate_pitch_angle_products(self, dependencies: SweL3Dependencies) -> SweL3Data:
8490
swe_l2_data = dependencies.swe_l2_data
8591
swe_epoch = swe_l2_data.epoch

run_local.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,9 +298,9 @@ def create_hit_direct_event_cdf():
298298

299299
if "swe_moments" in sys.argv:
300300
dependencies = SweL3Dependencies.from_file_paths(
301-
get_test_data_path("swe/imap_swe_l2_sci-with-ace-data_20250101_v002.cdf"),
302-
get_test_data_path("mag/imap_mag_l1d_mago-normal_20250101_v001.cdf"),
303-
get_test_data_path("swe/imap_swapi_l3a_proton-sw_20250101_v001.cdf"),
301+
get_test_data_path("swe/imap_swe_l2_sci-with-ace-data_19990609_v002.cdf"),
302+
get_test_data_path("swe/imap_mag_l1d_mago-normal_19990609_v001.cdf"),
303+
get_test_data_path("swe/imap_swapi_l3a_proton-sw_19990609_v001.cdf"),
304304
get_test_data_path("swe/example_swe_config.json"),
305305
)
306306
print(create_swe_moments_cdf(dependencies))

tests/hit/l3/test_hit_processor.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from copy import deepcopy
2-
from dataclasses import fields
32
from datetime import datetime
4-
from typing import Type, TypeVar
3+
from typing import TypeVar
54
from unittest import TestCase
65
from unittest.mock import sentinel, patch, call, Mock
76

@@ -18,7 +17,7 @@
1817
from imap_processing.hit.l3.sectored_products.models import HitPitchAngleDataProduct
1918
from imap_processing.models import MagL1dData, InputMetadata
2019
from imap_processing.processor import Processor
21-
from tests.test_helpers import NumpyArrayMatcher
20+
from tests.test_helpers import NumpyArrayMatcher, create_dataclass_mock
2221

2322

2423
class TestHitProcessor(TestCase):
@@ -57,11 +56,11 @@ def test_process_pitch_angle_product(self, mock_rebin_by_pitch_angle_and_gyropha
5756
averaged_mag_vectors = [sentinel.mag_vector1, sentinel.mag_vector2]
5857

5958
mock_dependencies = Mock(spec=HITL3SectoredDependencies)
60-
mock_mag_data = self.create_dataclass_mock(MagL1dData)
59+
mock_mag_data = create_dataclass_mock(MagL1dData)
6160
mock_mag_data.rebin_to = Mock()
6261
mock_mag_data.rebin_to.return_value = averaged_mag_vectors
6362
mock_dependencies.mag_l1d_data = mock_mag_data
64-
mock_hit_data = self.create_dataclass_mock(HitL2Data)
63+
mock_hit_data = create_dataclass_mock(HitL2Data)
6564
mock_hit_data.epoch = epochs
6665
mock_hit_data.epoch_delta = epoch_deltas
6766

@@ -588,6 +587,3 @@ def test_raise_error_if_descriptor_doesnt_match(self):
588587
f"Don't know how to generate 'spectral-index' /n Known HIT l3 data products: 'pitch-angle', 'direct-event'.")
589588

590589
T = TypeVar("T")
591-
592-
def create_dataclass_mock(self, obj: Type[T]) -> T:
593-
return Mock(spec=[field.name for field in fields(obj)])

tests/swe/l3/science/test_moments_calculation.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import math
12
import unittest
3+
from datetime import datetime
4+
from unittest.mock import patch
25

36
import numpy as np
47

58
from imap_processing.swe.l3.science.moment_calculations import compute_maxwellian_weight_factors, \
6-
filter_and_flatten_regress_parameters, regress, calculate_fit_temperature_density_velocity
9+
filter_and_flatten_regress_parameters, regress, calculate_fit_temperature_density_velocity, rotate_temperature, \
10+
rotate_dps_vector_to_rtn
711
from tests.test_helpers import get_test_data_path
812

913

@@ -139,3 +143,44 @@ def test_filter_and_flatten_regress_parameters(self):
139143
np.testing.assert_array_equal(vectors, [[3, 0, 0], [4, 0, 0], [10, 0, 0], [10, 0, 0], [0, 0, 0]])
140144
np.testing.assert_array_equal(actual_weights, [3, 1e-36, 10, 11, 12])
141145
np.testing.assert_array_equal(yreg, [np.log(3), -80.6, np.log(10), np.log(11), np.log(12)])
146+
147+
@patch('spiceypy.spiceypy.pxform')
148+
@patch('spiceypy.spiceypy.datetime2et')
149+
def test_rotate_dps_vector_to_rtn(self, mock_datetime2et, mock_pxform):
150+
epoch = datetime(year=2020, month=3, day=10)
151+
dsp_vector = np.array([0, 1, 0])
152+
rotation_matrix = np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
153+
mock_pxform.return_value = rotation_matrix
154+
155+
rtn_vector = rotate_dps_vector_to_rtn(epoch, dsp_vector)
156+
mock_datetime2et.assert_called_once_with(epoch)
157+
158+
mock_pxform.assert_called_once_with("IMAP_DPS", "IMAP_RTN", mock_datetime2et.return_value)
159+
160+
np.testing.assert_array_equal(rtn_vector, rotation_matrix @ dsp_vector)
161+
162+
@patch('spiceypy.spiceypy.pxform')
163+
@patch('spiceypy.spiceypy.datetime2et')
164+
def test_rotate_temperature(self, mock_datetime2et, mock_pxform):
165+
epoch = datetime(year=2020, month=3, day=11)
166+
temperature_alpha = math.pi / 4
167+
temperature_beta = math.pi / 8
168+
169+
rotation_matrix = np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]])
170+
mock_pxform.return_value = rotation_matrix
171+
172+
theta, phi = rotate_temperature(epoch, temperature_alpha, temperature_beta)
173+
mock_datetime2et.assert_called_once_with(epoch)
174+
175+
mock_pxform.assert_called_once_with("IMAP_DPS", "IMAP_RTN", mock_datetime2et.return_value)
176+
177+
sin_dec = np.sin(temperature_beta)
178+
x = sin_dec * np.cos(temperature_alpha)
179+
y = sin_dec * np.sin(temperature_alpha)
180+
z = np.cos(temperature_beta)
181+
182+
expected_rtn_temperature = rotation_matrix @ np.array([x, y, z])
183+
expected_rtn_temperature /= np.linalg.norm(expected_rtn_temperature)
184+
185+
self.assertEqual(np.asin(expected_rtn_temperature[2]), phi)
186+
self.assertEqual(np.atan2(expected_rtn_temperature[1], expected_rtn_temperature[0]), theta)

0 commit comments

Comments
 (0)