Skip to content

Commit 6dd63c0

Browse files
authored
Populate map principal data CATDESC from descriptor (IMAP-Science-Operations-Center#2712)
* ENA: Add to_catdesc to create CATDESC from map descriptor * ENA: populate principal data CATDESC from descriptor * ENA: Change CATDESC based on feedback from Dan * Put species before the quantity * Spell out Combined (and space out from instrument name) * Special-case ISN [species] Rate * Address simple PR comments for map CATDESC * ENA: Use descriptor to find principal data variable; populate CATDESC for Ultra * Remove redundant test for Ultra L2 rectangular map from descriptor
1 parent cba3b01 commit 6dd63c0

6 files changed

Lines changed: 211 additions & 6 deletions

File tree

imap_processing/ena_maps/ena_maps.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from imap_processing.cdf.imap_cdf_manager import ImapCdfAttributes
1818
from imap_processing.cdf.utils import load_cdf
19-
from imap_processing.ena_maps.utils import map_utils, spatial_utils
19+
from imap_processing.ena_maps.utils import map_utils, naming, spatial_utils
2020

2121
# The coordinate names can vary between L1C and L2 data (e.g. azimuth vs longitude),
2222
# so we define an enum to handle the coordinate names.
@@ -1421,6 +1421,12 @@ def build_cdf_dataset( # noqa: PLR0912
14211421
{"DELTA_PLUS_VAR": "epoch_delta", "BIN_LOCATION": 0}
14221422
)
14231423

1424+
# And CATDESC for principal data
1425+
md = naming.MapDescriptor.from_string(descriptor)
1426+
principal_data = md.principal_data_var
1427+
if principal_data in cdf_ds:
1428+
cdf_ds[principal_data].attrs["CATDESC"] = md.to_catdesc()
1429+
14241430
return cdf_ds
14251431

14261432
def to_properties_dict(self) -> dict:

imap_processing/ena_maps/utils/naming.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,87 @@ def to_string(self) -> str:
173173
]
174174
)
175175

176+
def to_catdesc(self) -> str:
177+
"""
178+
Convert the MapDescriptor instance to a human-readable CATDESC string.
179+
180+
Returns
181+
-------
182+
str
183+
Information in descriptor converted to SPDF CATDESC attribute. This
184+
is normally used for plot titles and should be under about 80 characters.
185+
"""
186+
instrument = self.instrument.name.split("_")[0]
187+
if instrument not in ("IDEX", "GLOWS"):
188+
instrument = instrument.title()
189+
sensor = " Combined" if self.sensor == "combined" else self.sensor
190+
species = "UV" if self.species == "uv" else self.species.title()
191+
m = re.match(
192+
r"^(drt|ena|int|isn|spx)(?:(?<=spx)\d+)?([^-_\s]*)$", self.principal_data
193+
)
194+
quantity = {
195+
"drt": "Rate",
196+
"ena": "Inten",
197+
"int": "Inten",
198+
"isn": "Rate",
199+
"spx": "Spectral",
200+
}[m.group(1)]
201+
if m.group(1) == "isn":
202+
species = "ISN " + species
203+
extras = m.group(2)
204+
coord = self.coordinate_system.upper()
205+
frame = {
206+
"hf": "Helio",
207+
"hk": "Helio Kin",
208+
"sf": "SC",
209+
}[self.frame_descriptor]
210+
survival = "Surv Corr" if self.survival_corrected == "sp" else "No Surv Corr"
211+
spin_phase = self.spin_phase.title()
212+
if spin_phase == "Full":
213+
spin_phase = "Full Spin"
214+
m = re.match(r"^(\d+)deg|nside(\d+)", self.resolution_str)
215+
resolution = f"{m.group(1)} deg" if m.group(1) else f"NSide {m.group(2)}"
216+
if isinstance(self.duration, int):
217+
duration = f"{self.duration} Day"
218+
else:
219+
m = re.match(r"^(\d+)(.*)$", self.duration)
220+
duration = f"{m.group(1)} {m.group(2).title()}"
221+
if duration.endswith("Mo"):
222+
duration += "n"
223+
catdesc = (
224+
f"IMAP {instrument}{sensor} {species} {quantity}, {coord} "
225+
f"{frame} Frame, {survival}, {spin_phase}, {resolution}, {duration}"
226+
)
227+
possible_extras = [
228+
("nbs", "No sputter/bootstrap"),
229+
("nbkgnd", "No bkgnd sub"),
230+
]
231+
for extra, long_description in possible_extras:
232+
if extras.startswith(extra):
233+
catdesc += f", {long_description}"
234+
break
235+
return catdesc
236+
237+
@property
238+
def principal_data_var(self) -> str:
239+
"""
240+
The name of the variable containing the principal data for the map.
241+
242+
Returns
243+
-------
244+
principal_data_var : str
245+
CDF (dataset) variable name expected to contain the principal data.
246+
"""
247+
if self.principal_data.startswith("isnnbkgnd"):
248+
return "isn_rate"
249+
return {
250+
"drt": "dust_rate",
251+
"ena": "ena_intensity",
252+
"int": "glows_rate",
253+
"isn": "isn_rate_bg_subtracted",
254+
"spx": "ena_spectral_index",
255+
}[self.principal_data[:3]]
256+
176257
# Methods for parsing and building parts of the map descriptor string
177258
@staticmethod
178259
def get_instrument_descriptor(

imap_processing/tests/ena_maps/test_ena_maps.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,11 @@ def test_build_cdf_dataset(self, mock_to_dataset, mock_data_for_build_cdf_datase
921921
skymap.min_epoch = 10
922922
skymap.max_epoch = 15
923923
cdf_dataset = skymap.build_cdf_dataset(
924-
"hi", "l2", "foo_descriptor", sensor="45", drop_vars_with_no_attributes=True
924+
"hi",
925+
"l2",
926+
"h45-ena-h-sf-nsp-ram-hae-6deg-6mo",
927+
sensor="45",
928+
drop_vars_with_no_attributes=True,
925929
)
926930

927931
# Check that expected vars gets removed
@@ -967,6 +971,12 @@ def test_build_cdf_dataset(self, mock_to_dataset, mock_data_for_build_cdf_datase
967971
f"attr '{attr}' should not be in variable attributes for '{var}'"
968972
)
969973

974+
# Check CATDESC made from descriptor
975+
assert (
976+
cdf_dataset["ena_intensity"].attrs["CATDESC"]
977+
== "IMAP Hi45 H Inten, HAE SC Frame, No Surv Corr, Ram, 6 deg, 6 Mon"
978+
)
979+
970980
@mock.patch("imap_processing.ena_maps.ena_maps.RectangularSkyMap.to_dataset")
971981
def test_build_cdf_dataset_external_dataset(
972982
self, mock_to_dataset, mock_data_for_build_cdf_dataset
@@ -979,12 +989,16 @@ def test_build_cdf_dataset_external_dataset(
979989
skymap.min_epoch = 10
980990
skymap.max_epoch = 15
981991
cdf_dataset_standard = skymap.build_cdf_dataset(
982-
"hi", "l2", "foo_descriptor", sensor="45", drop_vars_with_no_attributes=True
992+
"hi",
993+
"l2",
994+
"h45-ena-h-sf-nsp-ram-hae-6deg-6mo",
995+
sensor="45",
996+
drop_vars_with_no_attributes=True,
983997
)
984998
cdf_dataset_external = skymap.build_cdf_dataset(
985999
"hi",
9861000
"l2",
987-
"foo_descriptor",
1001+
"h45-ena-h-sf-nsp-ram-hae-6deg-6mo",
9881002
sensor="45",
9891003
drop_vars_with_no_attributes=True,
9901004
external_map_dataset=mock_data_for_build_cdf_dataset,
@@ -1019,7 +1033,9 @@ def test_build_cdf_dataset_key_error(
10191033
KeyError,
10201034
match="Required variable 'energy_delta_minus' not found in cdf Dataset.",
10211035
):
1022-
_ = skymap.build_cdf_dataset("hi", "l2", "foo_descriptor", sensor="45")
1036+
_ = skymap.build_cdf_dataset(
1037+
"hi", "l2", "h45-ena-h-sf-nsp-ram-hae-6deg-6mo", sensor="45"
1038+
)
10231039

10241040
@mock.patch("imap_processing.ena_maps.ena_maps.RectangularSkyMap.to_dataset")
10251041
def test_keep_vars_with_no_attributes(
@@ -1035,7 +1051,7 @@ def test_keep_vars_with_no_attributes(
10351051
cdf_dataset = skymap.build_cdf_dataset(
10361052
"hi",
10371053
"l2",
1038-
"foo_descriptor",
1054+
"h45-ena-h-sf-nsp-ram-hae-6deg-6mo",
10391055
sensor="45",
10401056
drop_vars_with_no_attributes=False,
10411057
)

imap_processing/tests/ena_maps/test_naming.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,92 @@ def test_to_string(self):
304304
)
305305
descriptor_str_ultra_combined = md_ultra_combined.to_string()
306306
assert descriptor_str_ultra_combined == "ulc-ena-h-sf-nsp-full-hae-nside32-1yr"
307+
308+
@pytest.mark.parametrize(
309+
"descriptor_str, expected_catdesc",
310+
[
311+
(
312+
"h45-spx-h-hf-sp-ram-hae-4deg-3mo",
313+
"IMAP Hi45 H Spectral, HAE Helio Frame, Surv Corr, Ram, 4 deg, 3 Mon",
314+
),
315+
(
316+
"h45-spx0305-h-hf-sp-ram-hae-4deg-3mo",
317+
"IMAP Hi45 H Spectral, HAE Helio Frame, Surv Corr, Ram, 4 deg, 3 Mon",
318+
),
319+
(
320+
"hic-ena-h-hf-sp-ram-hae-4deg-3mo",
321+
"IMAP Hi Combined H Inten, HAE Helio Frame, Surv Corr, Ram,"
322+
" 4 deg, 3 Mon",
323+
),
324+
(
325+
"u45-ena-h-hf-sp-ram-hae-4deg-3mo",
326+
"IMAP Ultra45 H Inten, HAE Helio Frame, Surv Corr, Ram, 4 deg, 3 Mon",
327+
),
328+
(
329+
"u45-ena-h-hf-sp-full-hae-4deg-3mo",
330+
"IMAP Ultra45 H Inten, HAE Helio Frame, Surv Corr, Full Spin,"
331+
" 4 deg, 3 Mon",
332+
),
333+
(
334+
"u45-ena-h-hf-sp-ram-hae-nside128-3mo",
335+
"IMAP Ultra45 H Inten, HAE Helio Frame, Surv Corr, Ram, NSide 128,"
336+
" 3 Mon",
337+
),
338+
(
339+
"u45-enaCUSTOM-h-hf-sp-ram-hae-4deg-3mo",
340+
"IMAP Ultra45 H Inten, HAE Helio Frame, Surv Corr, Ram, 4 deg, 3 Mon",
341+
),
342+
(
343+
"l090-enanbs-h-sf-nsp-ram-hae-6deg-1yr",
344+
"IMAP Lo90 H Inten, HAE SC Frame, No Surv Corr, Ram, 6 deg, 1 Yr,"
345+
" No sputter/bootstrap",
346+
),
347+
(
348+
"t090-ena-o-sf-nsp-ram-hae-6deg-1yr",
349+
"IMAP Lo90 O Inten, HAE SC Frame, No Surv Corr, Ram, 6 deg, 1 Yr",
350+
),
351+
(
352+
"l090-ena-h-hf-nsp-ram-gcs-6deg-1yr",
353+
"IMAP Lo90 H Inten, GCS Helio Frame, No Surv Corr, Ram, 6 deg, 1 Yr",
354+
),
355+
(
356+
"l090-isn-h-sf-nsp-ram-hae-6deg-1yr",
357+
"IMAP Lo90 ISN H Rate, HAE SC Frame, No Surv Corr, Ram, 6 deg, 1 Yr",
358+
),
359+
(
360+
"l090-isnnbkgnd-h-sf-nsp-ram-hae-6deg-1yr",
361+
"IMAP Lo90 ISN H Rate, HAE SC Frame, No Surv Corr, Ram, 6 deg, 1 Yr,"
362+
" No bkgnd sub",
363+
),
364+
(
365+
"glx-int-uv-sf-nsp-full-hae-6deg-1yr",
366+
"IMAP GLOWS UV Inten, HAE SC Frame, No Surv Corr, Full Spin, 6 deg,"
367+
" 1 Yr",
368+
),
369+
(
370+
"idx-drt-dust-sf-nsp-full-hae-6deg-1yr",
371+
"IMAP IDEX Dust Rate, HAE SC Frame, No Surv Corr, Full Spin, 6 deg,"
372+
" 1 Yr",
373+
),
374+
],
375+
)
376+
def test_to_catdesc(self, descriptor_str, expected_catdesc):
377+
# Use case is primarily from descriptor str to CATDESC
378+
md = MapDescriptor.from_string(descriptor_str)
379+
actual_catdesc = md.to_catdesc()
380+
assert actual_catdesc == expected_catdesc
381+
382+
@pytest.mark.parametrize(
383+
"descriptor_str, expected_principal_data_var",
384+
[
385+
("hic-ena-h-hf-sp-ram-hae-4deg-3mo", "ena_intensity"),
386+
("h45-spx0305-h-hf-sp-ram-hae-4deg-3mo", "ena_spectral_index"),
387+
("idx-drt-dust-sf-nsp-full-hae-6deg-1yr", "dust_rate"),
388+
("glx-int-uv-sf-nsp-full-hae-6deg-1yr", "glows_rate"),
389+
("l090-isnnbkgnd-h-sf-nsp-ram-hae-6deg-1yr", "isn_rate"),
390+
("l090-isn-h-sf-nsp-ram-hae-6deg-1yr", "isn_rate_bg_subtracted"),
391+
],
392+
)
393+
def test_principal_data_var(self, descriptor_str, expected_principal_data_var):
394+
md = MapDescriptor.from_string(descriptor_str)
395+
assert md.principal_data_var == expected_principal_data_var

imap_processing/tests/ultra/unit/test_ultra_l2.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,12 @@ def test_ultra_l2_descriptor_rectmap(self, mock_data_dict, furnish_kernels):
712712

713713
assert output_map.attrs["Spice_reference_frame"] == "IMAP_HAE"
714714
assert output_map.attrs["Spacing_degrees"] == "6.0"
715+
# Variable Metadata spot checks
716+
assert (
717+
output_map["ena_intensity"].attrs["CATDESC"]
718+
== "IMAP Ultra90 H Inten, HAE Helio Frame, No Surv Corr, Full Spin,"
719+
" 6 deg, 6 Mon"
720+
)
715721
write_cdf(output_map)
716722

717723
@pytest.mark.usefixtures("_setup_spice_kernels_list")

imap_processing/ultra/l2/ultra_l2.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -850,4 +850,11 @@ def ultra_l2(
850850
map_dataset["obs_date"] = map_dataset["obs_date"].astype(np.int64)
851851
map_dataset["obs_date_range"] = map_dataset["obs_date_range"].astype(np.int64)
852852

853+
# Adjust CATDESC per descriptor
854+
if descriptor is not None:
855+
md = MapDescriptor.from_string(descriptor)
856+
principal_data = md.principal_data_var
857+
if principal_data in map_dataset:
858+
map_dataset[principal_data].attrs["CATDESC"] = md.to_catdesc()
859+
853860
return [map_dataset]

0 commit comments

Comments
 (0)