Skip to content

Commit b5d6ea3

Browse files
authored
updates to swapi ialirt fitting algorithm (IMAP-Science-Operations-Center#2632)
1 parent 03f796a commit b5d6ea3

3 files changed

Lines changed: 189 additions & 17 deletions

File tree

imap_processing/ialirt/constants.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ class IalirtSwapiConstants:
4040
e_charge = 1.602176634e-19 # electronic charge, [C]
4141
speed_coeff = np.sqrt(2 * e_charge / prot_mass) / 1e3
4242

43+
# temporary correction factor based on WIND data available
44+
# overlapping with the first ~month of SWAPI data.
45+
# to be replaced once SWAPI's L3 processing pipeline is finalized
46+
# this will increase the model count by a factor of e^1,
47+
# changing the output density by a factor of e^-1.
48+
temporary_density_factor = np.exp(1)
49+
4350

4451
class StationProperties(NamedTuple):
4552
"""Class that represents properties of ground stations."""

imap_processing/ialirt/l0/process_swapi.py

Lines changed: 59 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
logger = logging.getLogger(__name__)
2323

2424
NUM_IALIRT_ENERGY_STEPS = 63
25+
FILLVAL_FLOAT32 = -1.0e31
2526

2627

2728
def count_rate(
@@ -57,6 +58,9 @@ def count_rate(
5758
speed = speed * 1000 # convert km/s to m/s
5859
density = density * 1e6 # convert 1/cm**3 to 1/m**3
5960

61+
# see comment on Consts.temporary_density_factor
62+
density = density * Consts.temporary_density_factor
63+
6064
return (
6165
(density * Consts.eff_area * (beta / np.pi) ** (3 / 2))
6266
* (np.exp(-beta * (center_speed**2 + speed**2 - 2 * center_speed * speed)))
@@ -104,15 +108,43 @@ def optimize_pseudo_parameters(
104108
60000 * (initial_speed_guess / 400) ** 2,
105109
]
106110
)
107-
sol = curve_fit(
108-
f=count_rate,
109-
xdata=energy_passbands.take(range(max_index - 3, max_index + 3), mode="clip"),
110-
ydata=count_rates.take(range(max_index - 3, max_index + 3), mode="clip"),
111-
sigma=count_rate_error.take(range(max_index - 3, max_index + 3), mode="clip"),
112-
p0=initial_param_guess,
113-
)
114111

115-
return sol[0]
112+
sol = None
113+
114+
try:
115+
five_point_range = range(max_index - 2, max_index + 2 + 1)
116+
xdata = energy_passbands.take(five_point_range, mode="clip")
117+
ydata = count_rates.take(five_point_range, mode="clip")
118+
sigma = count_rate_error.take(five_point_range, mode="clip")
119+
curve_fit_output = curve_fit(
120+
f=count_rate,
121+
xdata=xdata,
122+
ydata=ydata,
123+
sigma=sigma,
124+
p0=initial_param_guess,
125+
)
126+
127+
# If covariance matrix is not finite, scipy failed to converge to a
128+
# solution and could just be reporting the initial guess
129+
covariance_matrix_is_finite = np.all(np.isfinite(curve_fit_output[1]))
130+
131+
# fit has failed if R^2 < 0.7
132+
yfit = count_rate(xdata, *curve_fit_output[0])
133+
r2 = 1 - np.sum((ydata - yfit) ** 2) / np.sum((ydata - ydata.mean()) ** 2)
134+
r2_is_acceptable = r2 >= 0.7
135+
136+
if covariance_matrix_is_finite and r2_is_acceptable:
137+
sol = curve_fit_output[0]
138+
except RuntimeError:
139+
logger.error("curve_fit failed")
140+
sol = None
141+
142+
# report speed only if fit fails
143+
if sol is None:
144+
sol = initial_param_guess.copy()
145+
sol[1:] = FILLVAL_FLOAT32
146+
147+
return sol
116148

117149

118150
def geometric_mean(
@@ -237,8 +269,10 @@ def process_swapi_ialirt(
237269
grouped_subset = grouped_dataset.sel(epoch=grouped_dataset.group == group)
238270

239271
raw_coin_count = process_sweep_data(grouped_subset, "swapi_coin_cnt")
240-
# I-ALiRT packets are 16 times less than the regular science packets.
241-
raw_coin_count = raw_coin_count * 16
272+
# I-ALiRT packets have counts compressed by a factor of 16.
273+
# Add 8 to avoid having counts truncated to 0 and to avoid
274+
# counts being systematically too low
275+
raw_coin_count = raw_coin_count * 16 + 8
242276
# Subset to only the relevant I-ALiRT energy steps
243277
raw_coin_count = raw_coin_count[:, :NUM_IALIRT_ENERGY_STEPS]
244278
raw_coin_rate = raw_coin_count / SWAPI_LIVETIME
@@ -290,6 +324,21 @@ def process_swapi_ialirt(
290324
pseudo_proton_temperature_list[-5:],
291325
)
292326

327+
# replace nans (resulting from geometric means that
328+
# include fill values) with fill values
329+
(
330+
avg_pseudo_proton_speed,
331+
avg_pseudo_proton_density,
332+
avg_pseudo_proton_temperature,
333+
) = np.nan_to_num(
334+
(
335+
avg_pseudo_proton_speed,
336+
avg_pseudo_proton_density,
337+
avg_pseudo_proton_temperature,
338+
),
339+
nan=FILLVAL_FLOAT32,
340+
)
341+
293342
swapi_data.append(
294343
_populate_instrument_header_items(met)
295344
| {

imap_processing/tests/ialirt/unit/test_process_swapi.py

Lines changed: 123 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
from imap_processing import imap_module_directory
99
from imap_processing.ialirt.l0.process_swapi import (
10+
FILLVAL_FLOAT32,
11+
Consts,
1012
count_rate,
1113
geometric_mean,
1214
optimize_pseudo_parameters,
@@ -184,8 +186,8 @@ def test_count_rate():
184186
"""Use random realistic values to test for expected output of count_rate()."""
185187

186188
actual_result = count_rate(1370, *[550, 5.27, 1e5])
187-
expected_result = 3073.023325893161
188-
assert actual_result == expected_result, (
189+
expected_result = 3073.023325893161 * Consts.temporary_density_factor
190+
assert np.isclose(actual_result, expected_result), (
189191
f"The actual result of count_rate()"
190192
f" {actual_result} does not "
191193
f"match the expected result "
@@ -202,24 +204,24 @@ def test_optimize_parameters():
202204
"file_name": "ialirt_test_data_u_sw_550_n_sw_5_T_sw_100000_v2.csv",
203205
"expected_values": { # expected output and acceptable tolerance
204206
"pseudo_speed": (550, 0.01),
205-
"pseudo_density": (5, 0.14),
206-
"pseudo_temperature": (1e5, 0.2),
207+
"pseudo_density": (5 / Consts.temporary_density_factor, 0.14),
208+
"pseudo_temperature": (1e5, 0.25),
207209
},
208210
},
209211
"test_set_2": {
210212
"file_name": "ialirt_test_data_u_sw_650_n_sw_3.0_T_sw_120000_v2.csv",
211213
"expected_values": { # expected output and acceptable tolerance
212214
"pseudo_speed": (650, 0.01),
213-
"pseudo_density": (3, 0.3),
215+
"pseudo_density": (3 / Consts.temporary_density_factor, 0.3),
214216
"pseudo_temperature": (1.2e5, 0.28),
215217
},
216218
},
217219
"test_set_3": {
218220
"file_name": "ialirt_test_data_u_sw_400_n_sw_6.0_T_sw_80000_v2.csv",
219221
"expected_values": { # expected output and acceptable tolerance
220222
"pseudo_speed": (400, 0.01),
221-
"pseudo_density": (6, 0.39),
222-
"pseudo_temperature": (8e4, 0.15),
223+
"pseudo_density": (6 / Consts.temporary_density_factor, 0.39),
224+
"pseudo_temperature": (8e4, 0.2),
223225
},
224226
},
225227
}
@@ -259,6 +261,120 @@ def test_optimize_parameters():
259261
)
260262

261263

264+
def test_optimize_parameters_exception_handling():
265+
"""Test that the optimize_pseudo_parameters() function reports
266+
speed only when given data that causes curve_fit to fail."""
267+
268+
expected_speed = 557.279273 # peak passband speed
269+
file_name = "ialirt_test_data_u_sw_550_n_sw_5_T_sw_100000_v2.csv"
270+
271+
calibration_test_file = pd.read_csv(
272+
f"{imap_module_directory}/tests/ialirt/data/l0/swapi_ialirt_energy_steps.csv"
273+
)
274+
energy_passbands = calibration_test_file["Energy"][0:63].to_numpy().astype(float)
275+
276+
energy_data = pd.read_csv(
277+
f"{imap_module_directory}/tests/ialirt/data/l0/{file_name}"
278+
)
279+
count_rates = energy_data["Count Rates [Hz]"].to_numpy()
280+
count_rates[0] = 0.0
281+
count_rates = np.tile(count_rates, (2, 1))
282+
count_rates_errors = energy_data["Count Rates Error [Hz]"].to_numpy()
283+
284+
"""
285+
code to select the random seed:
286+
for i in range(100):
287+
np.random.seed(i)
288+
result = optimize_pseudo_parameters(count_rates *
289+
np.abs(np.random.standard_normal(size=count_rates.shape)),
290+
count_rates_errors, energy_passbands)
291+
if np.isclose(result['pseudo_speed'][0], expected_speed,
292+
rtol=1e-6) and np.isnan(result['pseudo_density'][0]):
293+
print(i)
294+
"""
295+
np.random.seed(14)
296+
speed, density, temperature = optimize_pseudo_parameters(
297+
count_rates * np.abs(np.random.standard_normal(size=count_rates.shape)),
298+
count_rates_errors,
299+
energy_passbands,
300+
)
301+
302+
np.testing.assert_allclose(speed, expected_speed, rtol=1e-6)
303+
np.testing.assert_allclose(density, FILLVAL_FLOAT32)
304+
np.testing.assert_allclose(temperature, FILLVAL_FLOAT32)
305+
306+
307+
def test_optimize_parameters_bad_fit_handling():
308+
"""Test that the optimize_pseudo_parameters() function
309+
reports speed only when the fit is too poor."""
310+
311+
file_name = "ialirt_test_data_u_sw_550_n_sw_5_T_sw_100000_v2.csv"
312+
313+
calibration_test_file = pd.read_csv(
314+
f"{imap_module_directory}/tests/ialirt/data/l0/swapi_ialirt_energy_steps.csv"
315+
)
316+
energy_passbands = calibration_test_file["Energy"][0:63].to_numpy().astype(float)
317+
318+
energy_data = pd.read_csv(
319+
f"{imap_module_directory}/tests/ialirt/data/l0/{file_name}"
320+
)
321+
count_rates = energy_data["Count Rates [Hz]"].to_numpy()
322+
count_rates[0] = 0.0
323+
count_rates_errors = energy_data["Count Rates Error [Hz]"].to_numpy()
324+
325+
# add high-amplitude randomness to the count rates to make the fit poor
326+
np.random.seed(0)
327+
count_rates = count_rates + np.abs(
328+
np.random.standard_normal(size=count_rates.shape) * count_rates.max()
329+
)
330+
331+
speed, density, temperature = optimize_pseudo_parameters(
332+
count_rates, count_rates_errors, energy_passbands
333+
)
334+
335+
expected_speed = (
336+
np.sqrt(energy_passbands[count_rates.argmax(axis=-1)]) * Consts.speed_coeff
337+
)
338+
339+
np.testing.assert_allclose(speed, expected_speed, rtol=1e-6)
340+
np.testing.assert_allclose(density, FILLVAL_FLOAT32)
341+
np.testing.assert_allclose(temperature, FILLVAL_FLOAT32)
342+
343+
344+
def test_optimize_parameters_bad_covariance_handling():
345+
"""Test that the optimize_pseudo_parameters() function
346+
reports speed only when output covariance is nonsensical."""
347+
348+
file_name = "ialirt_test_data_u_sw_550_n_sw_5_T_sw_100000_v2.csv"
349+
350+
calibration_test_file = pd.read_csv(
351+
f"{imap_module_directory}/tests/ialirt/data/l0/swapi_ialirt_energy_steps.csv"
352+
)
353+
energy_passbands = calibration_test_file["Energy"][0:63].to_numpy().astype(float)
354+
355+
energy_data = pd.read_csv(
356+
f"{imap_module_directory}/tests/ialirt/data/l0/{file_name}"
357+
)
358+
count_rates = energy_data["Count Rates [Hz]"].to_numpy()
359+
count_rates[0] = 0.0
360+
count_rates_errors = energy_data["Count Rates Error [Hz]"].to_numpy()
361+
362+
# setting errors to 0 results in infinite covariance
363+
count_rates_errors *= 0
364+
365+
speed, density, temperature = optimize_pseudo_parameters(
366+
count_rates, count_rates_errors, energy_passbands
367+
)
368+
369+
expected_speed = (
370+
np.sqrt(energy_passbands[count_rates.argmax(axis=-1)]) * Consts.speed_coeff
371+
)
372+
373+
np.testing.assert_allclose(speed, expected_speed, rtol=1e-6)
374+
np.testing.assert_allclose(density, FILLVAL_FLOAT32)
375+
np.testing.assert_allclose(temperature, FILLVAL_FLOAT32)
376+
377+
262378
def test_geometric_mean():
263379
"""Test geometric_mean function."""
264380

0 commit comments

Comments
 (0)