Skip to content

Commit 4fed5a9

Browse files
authored
CoDICE l2 angular intensity validation (#2331)
* add todos to remove pytest workarounds
1 parent cbe72eb commit 4fed5a9

File tree

3 files changed

+159
-13
lines changed

3 files changed

+159
-13
lines changed

imap_processing/cdf/config/imap_codice_global_cdf_attrs.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,24 @@ imap_codice_l2_lo-sw-species:
241241
Logical_source: imap_codice_l2_lo-sw-species
242242
Logical_source_description: IMAP Mission CoDICE Lo Level-2 Sunward Species Intensity Data.
243243

244+
imap_codice_l2_lo-nsw-species:
245+
<<: *instrument_base
246+
Data_type: L2_lo-nsw-species>Level-2 Lo Non-Sunward Species Intensities Data
247+
Logical_source: imap_codice_l2_lo-nsw-species
248+
Logical_source_description: IMAP Mission CoDICE Lo Level-2 Non-Sunward Species Intensity Data.
249+
250+
imap_codice_l2_lo-sw-angular:
251+
<<: *instrument_base
252+
Data_type: L2_lo-sw-species>Level-2 Lo Sunward Angular Intensities Data
253+
Logical_source: imap_codice_l2_lo-sw-angular
254+
Logical_source_description: IMAP Mission CoDICE Lo Level-2 Sunward Angular Intensity Data.
255+
256+
imap_codice_l2_lo-nsw-angular:
257+
<<: *instrument_base
258+
Data_type: L2_lo-nsw-species>Level-2 Lo Non-Sunward Angular Intensities Data
259+
Logical_source: imap_codice_l2_lo-nsw-angular
260+
Logical_source_description: IMAP Mission CoDICE Lo Level-2 Non-Sunward Angular Intensity Data.
261+
244262
imap_codice_l2_hi-omni:
245263
<<: *instrument_base
246264
Data_type: L2_hi-omni>Level-2 Hi Omnidirectional Species Intensities Data

imap_processing/codice/codice_l2.py

Lines changed: 61 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
LO_POSITION_TO_ELEVATION_ANGLE,
3232
LO_SW_ANGULAR_VARIABLE_NAMES,
3333
LO_SW_PICKUP_ION_SPECIES_VARIABLE_NAMES,
34-
LO_SW_SPECIES_VARIABLE_NAMES,
34+
LO_SW_SOLAR_WIND_SPECIES_VARIABLE_NAMES,
3535
NSW_POSITIONS,
36+
PIXEL_ORIENTATIONS,
3637
PUI_POSITIONS,
3738
SOLAR_WIND_POSITIONS,
3839
SW_POSITIONS,
@@ -354,11 +355,14 @@ def process_lo_angular_intensity(
354355
positions,
355356
average_across_positions=False,
356357
)
358+
357359
# transform positions to elevation angles
358360
if positions == SW_POSITIONS:
359361
pos_to_el = LO_POSITION_TO_ELEVATION_ANGLE["sw"]
362+
position_index_to_adjust = 0
360363
elif positions == NSW_POSITIONS:
361364
pos_to_el = LO_POSITION_TO_ELEVATION_ANGLE["nsw"]
365+
position_index_to_adjust = 9
362366
else:
363367
raise ValueError("Unknown positions for elevation angle mapping.")
364368

@@ -369,6 +373,8 @@ def process_lo_angular_intensity(
369373
[pos_to_el[pos] for pos in dataset["inst_az"].data],
370374
)
371375
)
376+
# add uncertainties to species list
377+
species_list = species_list + [f"unc_{var}" for var in species_list]
372378
# Take the mean across elevation angles and restore the original dimension order
373379
dataset_converted = (
374380
dataset[species_list]
@@ -383,8 +389,41 @@ def process_lo_angular_intensity(
383389
dataset = dataset.assign_coords(
384390
spin_angle=("spin_sector", dataset["spin_sector"].data * 15.0 + 7.5)
385391
)
386-
387392
dataset = dataset.drop_vars(species_list).merge(dataset_converted)
393+
# Positions 0 and 10 only observe half of the 24 spins for each esa step.
394+
# To account for this, we replicate the counts observed in position 0 and 10 for
395+
# each esa step to either spin angles 0-11 or 12-23, depending on the pixel
396+
# orientation (A/B). See section 11.2.2 of the CoDICE algorithm document
397+
a_inds = np.array(
398+
[pos for pos, orientation in PIXEL_ORIENTATIONS.items() if orientation == "A"]
399+
)
400+
b_inds = np.array(
401+
[pos for pos, orientation in PIXEL_ORIENTATIONS.items() if orientation == "B"]
402+
)
403+
404+
position_index = position_index_to_adjust
405+
for species in species_list:
406+
# Determine the correct spin indices based on the position
407+
spin_sectors = dataset["spin_sector"].data
408+
spin_inds_1 = np.where(spin_sectors >= 12)[0]
409+
spin_inds_2 = np.where(spin_sectors < 12)[0]
410+
# if position_index is 9, swap the spin indices
411+
if position_index == 9:
412+
spin_inds_1, spin_inds_2 = spin_inds_2, spin_inds_1
413+
414+
# Assign the values to the correct positions and spin sectors
415+
dataset[species].values[
416+
:, a_inds[:, np.newaxis], spin_inds_1, position_index
417+
] = dataset[species].values[
418+
:, a_inds[:, np.newaxis], spin_inds_2, position_index
419+
]
420+
421+
dataset[species].values[
422+
:, b_inds[:, np.newaxis], spin_inds_2, position_index
423+
] = dataset[species].values[
424+
:, b_inds[:, np.newaxis], spin_inds_1, position_index
425+
]
426+
388427
return dataset
389428

390429

@@ -777,7 +816,6 @@ def process_codice_l2(
777816
# This should get science files since ancillary or spice doesn't have data_type
778817
# as data level.
779818
file_path = dependencies.get_file_paths(descriptor=descriptor)[0]
780-
781819
# Now form product name from descriptor
782820
descriptor = ScienceFilePath(file_path).descriptor
783821
dataset_name = f"imap_codice_l2_{descriptor}"
@@ -790,19 +828,23 @@ def process_codice_l2(
790828
"imap_codice_l2_lo-nsw-angular",
791829
"imap_codice_l2_lo-sw-angular",
792830
]:
831+
cdf_attrs = ImapCdfAttributes()
832+
cdf_attrs.add_instrument_global_attrs("codice")
833+
793834
l2_dataset = load_cdf(file_path).copy()
794835

795836
geometric_factor_lookup = get_geometric_factor_lut(dependencies)
796837
efficiency_lookup = get_efficiency_lut(dependencies)
797838
geometric_factors = compute_geometric_factors(
798839
l2_dataset, geometric_factor_lookup
799840
)
841+
800842
if dataset_name == "imap_codice_l2_lo-sw-species":
801843
# Filter the efficiency lookup table for solar wind efficiencies
802844
efficiencies = efficiency_lookup[efficiency_lookup["product"] == "sw"]
803845
# Calculate the pickup ion sunward solar wind intensities using equation
804846
# described in section 11.2.3 of algorithm document.
805-
process_lo_species_intensity(
847+
l2_dataset = process_lo_species_intensity(
806848
l2_dataset,
807849
LO_SW_PICKUP_ION_SPECIES_VARIABLE_NAMES,
808850
geometric_factors,
@@ -811,25 +853,31 @@ def process_codice_l2(
811853
)
812854
# Calculate the sunward solar wind species intensities using equation
813855
# described in section 11.2.3 of algorithm document.
814-
process_lo_species_intensity(
856+
l2_dataset = process_lo_species_intensity(
815857
l2_dataset,
816-
LO_SW_SPECIES_VARIABLE_NAMES,
858+
LO_SW_SOLAR_WIND_SPECIES_VARIABLE_NAMES,
817859
geometric_factors,
818860
efficiencies,
819861
SOLAR_WIND_POSITIONS,
820862
)
863+
l2_dataset.attrs.update(
864+
cdf_attrs.get_global_attributes("imap_codice_l2_lo-sw-species")
865+
)
821866
elif dataset_name == "imap_codice_l2_lo-nsw-species":
822867
# Filter the efficiency lookup table for non-solar wind efficiencies
823868
efficiencies = efficiency_lookup[efficiency_lookup["product"] == "nsw"]
824869
# Calculate the non-sunward species intensities using equation
825870
# described in section 11.2.3 of algorithm document.
826-
process_lo_species_intensity(
871+
l2_dataset = process_lo_species_intensity(
827872
l2_dataset,
828873
LO_NSW_SPECIES_VARIABLE_NAMES,
829874
geometric_factors,
830875
efficiencies,
831876
NSW_POSITIONS,
832877
)
878+
l2_dataset.attrs.update(
879+
cdf_attrs.get_global_attributes("imap_codice_l2_lo-nsw-species")
880+
)
833881
elif dataset_name == "imap_codice_l2_lo-sw-angular":
834882
efficiencies = efficiency_lookup[efficiency_lookup["product"] == "sw"]
835883
# Calculate the sunward solar wind angular intensities using equation
@@ -841,6 +889,9 @@ def process_codice_l2(
841889
efficiencies,
842890
SW_POSITIONS,
843891
)
892+
l2_dataset.attrs.update(
893+
cdf_attrs.get_global_attributes("imap_codice_l2_lo-sw-angular")
894+
)
844895
if dataset_name == "imap_codice_l2_lo-nsw-angular":
845896
# Calculate the non sunward angular intensities
846897
efficiencies = efficiency_lookup[efficiency_lookup["product"] == "nsw"]
@@ -851,7 +902,9 @@ def process_codice_l2(
851902
efficiencies,
852903
NSW_POSITIONS,
853904
)
854-
905+
l2_dataset.attrs.update(
906+
cdf_attrs.get_global_attributes("imap_codice_l2_lo-nsw-angular")
907+
)
855908
if dataset_name in [
856909
"imap_codice_l2_hi-counters-singles",
857910
"imap_codice_l2_hi-counters-aggregated",

imap_processing/tests/codice/test_codice_l2.py

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
process_lo_species_intensity,
2222
)
2323
from imap_processing.codice.constants import (
24+
LO_NSW_ANGULAR_VARIABLE_NAMES,
2425
LO_SW_ANGULAR_VARIABLE_NAMES,
2526
LO_SW_SOLAR_WIND_SPECIES_VARIABLE_NAMES,
2627
SW_POSITIONS,
@@ -307,8 +308,12 @@ def test_process_lo_angular_intensity():
307308
.groupby("group")
308309
.sum()
309310
)
311+
# Skip checking the first elevations. Those get reassigned and will be
312+
# validated below.
310313
np.testing.assert_allclose(
311-
l1b_val_data_processed[var].values, expected_intensity.values, rtol=1e-5
314+
l1b_val_data_processed[var].values[:, :, :, 1:],
315+
expected_intensity.values[:, :, :, 1:],
316+
rtol=1e-5,
312317
)
313318
# Check coords
314319
np.testing.assert_allclose(l1b_val_data_processed["elevation_angle"], [0, 15, 30])
@@ -320,30 +325,100 @@ def test_process_lo_angular_intensity():
320325
def test_codice_l2_sw_species_intensity(processing_dependencies, mock_get_file_paths):
321326
sci_input = ScienceInput("imap_codice_l1b_lo-sw-species_20250814_v007.cdf")
322327
processing_dependencies.add(sci_input)
328+
329+
l2_val_data = (
330+
imap_module_directory
331+
/ "tests"
332+
/ "codice"
333+
/ "data"
334+
/ "l2_validation"
335+
/ "imap_codice_l2_lo-sw-species_20250814_v007.cdf"
336+
)
337+
l2_val_data = load_cdf(l2_val_data)
323338
ds = process_codice_l2("lo-sw-species", processing_dependencies)
339+
for variable in l2_val_data.data_vars:
340+
processed_val = ds[variable].values
341+
np.testing.assert_allclose(
342+
processed_val,
343+
l2_val_data[variable].values,
344+
rtol=1e-5,
345+
err_msg=f"Mismatch in variable '{variable}'",
346+
)
324347
ds.attrs["Data_version"] = "001"
325-
write_cdf(ds)
348+
write_cdf(ds, istp=False)
326349

327350

328351
def test_codice_l2_nsw_species_intensity(processing_dependencies, mock_get_file_paths):
329352
sci_input = ScienceInput("imap_codice_l1b_lo-nsw-species_20250814_v007.cdf")
330353
processing_dependencies.add(sci_input)
354+
l2_val_data = (
355+
imap_module_directory
356+
/ "tests"
357+
/ "codice"
358+
/ "data"
359+
/ "l2_validation"
360+
/ "imap_codice_l2_lo-nsw-species_20250814_v007.cdf"
361+
)
362+
l2_val_data = load_cdf(l2_val_data)
331363
ds = process_codice_l2("lo-nsw-species", processing_dependencies)
364+
for variable in l2_val_data.data_vars:
365+
np.testing.assert_allclose(
366+
ds[variable].values,
367+
l2_val_data[variable].values,
368+
rtol=1e-5,
369+
err_msg=f"Mismatch in variable '{variable}'",
370+
)
332371
ds.attrs["Data_version"] = "001"
333-
write_cdf(ds)
372+
write_cdf(ds, istp=False)
334373

335374

336375
def test_codice_l2_nsw_angular_intensity(processing_dependencies, mock_get_file_paths):
337376
sci_input = ScienceInput("imap_codice_l1b_lo-nsw-angular_20250814_v007.cdf")
338377
processing_dependencies.add(sci_input)
378+
l2_val_data = (
379+
imap_module_directory
380+
/ "tests"
381+
/ "codice"
382+
/ "data"
383+
/ "l2_validation"
384+
/ "imap_codice_l2_lo-nsw-angular_20250814_v007.cdf"
385+
)
386+
l2_val_data = load_cdf(l2_val_data)
339387
ds = process_codice_l2("lo-nsw-angular", processing_dependencies)
388+
for variable in LO_NSW_ANGULAR_VARIABLE_NAMES:
389+
np.testing.assert_allclose(
390+
ds[variable].values,
391+
l2_val_data[variable].values,
392+
rtol=1e-5,
393+
err_msg=f"Mismatch in variable '{variable}'",
394+
)
340395
ds.attrs["Data_version"] = "001"
341-
write_cdf(ds)
396+
# TODO fix attrs
397+
write_cdf(ds, istp=False)
342398

343399

344400
def test_codice_l2_sw_angular_intensity(processing_dependencies, mock_get_file_paths):
345401
sci_input = ScienceInput("imap_codice_l1b_lo-sw-angular_20250814_v007.cdf")
346402
processing_dependencies.add(sci_input)
403+
l2_val_data = (
404+
imap_module_directory
405+
/ "tests"
406+
/ "codice"
407+
/ "data"
408+
/ "l2_validation"
409+
/ "imap_codice_l2_lo-sw-angular_20250814_v007.cdf"
410+
)
411+
l2_val_data = load_cdf(l2_val_data)
347412
ds = process_codice_l2("lo-sw-angular", processing_dependencies)
413+
for variable in LO_SW_ANGULAR_VARIABLE_NAMES:
414+
np.testing.assert_allclose(
415+
ds[variable].values,
416+
l2_val_data[variable].values,
417+
# TODO is 1e-4 ok?
418+
rtol=1e-4,
419+
err_msg=f"Mismatch in variable '{variable}'",
420+
)
421+
348422
ds.attrs["Data_version"] = "001"
349-
write_cdf(ds)
423+
# TODO fix attrs
424+
write_cdf(ds, istp=False)

0 commit comments

Comments
 (0)