Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .readthedocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 2
build:
os: "ubuntu-22.04"
tools:
python: "3.10"
python: "3.12"

sphinx:
configuration: docs/source/conf.py
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,30 @@ bg_rate_sys_err:
VAR_NOTES: Systematic error in the background count rate from non-ENA (non-heliospheric) sources.
VAR_TYPE: support_data

bg_rate_sys_err_minus:
<<: *default_float32
DEPEND_0: epoch
DICT_KEY: SPASE>Particle>ParticleType:Atom,ParticleQuantity:NumberFlux,Qualifier:Uncertainty,CoordinateSystemName:HAE,CoordinateRepresentation:Spherical
DISPLAY_TYPE: map_image
FIELDNAM: Background Rate Sys Error Lower
FORMAT: F6.3
LABLAXIS: Rate Error Lower
UNITS: count s-1
VAR_NOTES: Lower bound of the systematic error in the background count rate from non-ENA (non-heliospheric) sources.
VAR_TYPE: support_data

bg_rate_sys_err_plus:
<<: *default_float32
DEPEND_0: epoch
DICT_KEY: SPASE>Particle>ParticleType:Atom,ParticleQuantity:NumberFlux,Qualifier:Uncertainty,CoordinateSystemName:HAE,CoordinateRepresentation:Spherical
DISPLAY_TYPE: map_image
FIELDNAM: Background Rate Sys Error Upper
FORMAT: F6.3
LABLAXIS: Rate Error Upper
UNITS: count s-1
VAR_NOTES: Upper bound of the systematic error in the background count rate from non-ENA (non-heliospheric) sources.
VAR_TYPE: support_data

bg_intensity:
<<: *default_float32
DEPEND_0: epoch
Expand Down Expand Up @@ -275,6 +299,30 @@ bg_intensity_sys_err:
VAR_NOTES: Non-statistical error in the background intensity from non-ENA (non-heliospheric) sources.
VAR_TYPE: support_data

bg_intensity_sys_err_minus:
<<: *default_float32
DEPEND_0: epoch
DICT_KEY: SPASE>Particle>ParticleType:Atom,ParticleQuantity:NumberFlux,Qualifier:Uncertainty,CoordinateSystemName:HAE,CoordinateRepresentation:Spherical
DISPLAY_TYPE: map_image
FIELDNAM: Background Intensity Non-statistical Error Lower
FORMAT: F8.3
LABLAXIS: Intensity Non-statistical Error Lower
UNITS: cm -2 s -1 sr -1 keV -1
VAR_NOTES: Lower bound of the non-statistical error in the background intensity from non-ENA (non-heliospheric) sources.
VAR_TYPE: support_data

bg_intensity_sys_err_plus:
<<: *default_float32
DEPEND_0: epoch
DICT_KEY: SPASE>Particle>ParticleType:Atom,ParticleQuantity:NumberFlux,Qualifier:Uncertainty,CoordinateSystemName:HAE,CoordinateRepresentation:Spherical
DISPLAY_TYPE: map_image
FIELDNAM: Background Intensity Non-statistical Error Upper
FORMAT: F8.3
LABLAXIS: Intensity Non-statistical Error Upper
UNITS: cm -2 s -1 sr -1 keV -1
VAR_NOTES: Upper bound of the non-statistical error in the background intensity from non-ENA (non-heliospheric) sources.
VAR_TYPE: support_data

ena_count:
<<: *default_float32
DEPEND_0: epoch
Expand Down
4 changes: 4 additions & 0 deletions imap_processing/ena_maps/utils/naming.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,9 +203,13 @@ def build_map_var_catdesc(self, support_var_name: str) -> str | None:
"bg_intensity": "Background Inten",
"bg_intensity_stat_uncert": "Background Inten Stat. Unc.",
"bg_intensity_sys_err": "Background Inten Sys. Err.",
"bg_intensity_sys_err_minus": "Background Inten Sys. Err. Lower",
"bg_intensity_sys_err_plus": "Background Inten Sys. Err. Upper",
"bg_rate": "Background Count Rate",
"bg_rate_stat_uncert": "Background Count Rate Stat. Unc.",
"bg_rate_sys_err": "Background Count Rate Sys. Err.",
"bg_rate_sys_err_minus": "Background Count Rate Sys. Err. Lower",
"bg_rate_sys_err_plus": "Background Count Rate Sys. Err. Upper",
"ena_count_rate": "ENA Count Rate",
"ena_count_rate_stat_uncert": "ENA Count Rate Stat. Unc.",
"obs_date": "Mean Observation Date",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
esa_mode,incident_E-Step,Observed_E-Step,Cntr_E,Cntr_E_unc,GF_Trpl_H,GF_Trpl_H_unc_minus,GF_Trpl_H_unc_plus
# [1],[1],[1],[keV],[keV],[cm^2sr keV/keV],[cm^2sr keV/keV],[cm^2sr keV/keV]
0,1,1,0.01633,0.28,4.45E-05,4.03E-05,4.20E-05
0,2,2,0.03047,0.43,5.02E-05,4.53E-05,4.72E-05
0,3,3,0.05576,0.89,6.16E-05,5.58E-05,5.82E-05
0,4,4,0.10626,1.9,7.12E-05,4.51E-05,4.89E-05
0,5,5,0.20004,3.4,8.89E-05,5.85E-05,6.31E-05
0,6,6,0.40496,7.3,1.12E-04,6.58E-05,7.23E-05
0,7,7,0.78729,22,1.43E-04,8.25E-05,9.09E-05
1,1,1,0.01719,0.22,1.05E-04,8.82E-05,9.26E-05
1,2,2,0.03236,0.36,1.22E-04,1.02E-04,1.07E-04
1,3,3,0.05948,0.77,1.44E-04,1.21E-04,1.27E-04
1,4,4,0.11441,1.3,1.73E-04,1.17E-04,1.26E-04
1,5,5,0.2137,3,2.15E-04,1.37E-04,1.49E-04
1,6,6,0.43736,5.3,2.82E-04,1.65E-04,1.81E-04
1,7,7,0.83888,11.7,3.61E-04,2.08E-04,2.29E-04
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
esa_mode,incident_E-Step,Observed_E-Step,Cntr_E,Cntr_E_unc,GF_Trpl_O,GF_Trpl_O_unc_minus,GF_Trpl_O_unc_plus
# [1],[1],[1],[keV],[keV],[cm^2sr keV/keV],[cm^2sr keV/keV],[cm^2sr keV/keV]
0,1,1,0.01919,1.372344105,1.98E-05,1.74E-05,1.81E-05
0,2,2,0.03675,2.537963082,3.18E-05,2.39E-05,2.53E-05
0,3,3,0.07121,6.591339559,5.34E-05,4.05E-05,4.29E-05
0,4,4,0.14141,17.43990907,8.64E-05,6.14E-05,6.56E-05
0,5,5,0.274,35.83900763,1.32E-04,9.07E-05,9.72E-05
0,6,6,0.58503,98.06681395,2.46E-04,1.53E-04,1.67E-04
0,7,7,1.13506,238.1076888,3.81E-04,2.23E-04,2.45E-04
1,1,1,0.02043303552,1.575924599,5.02E-05,4.41E-05,4.61E-05
1,2,2,0.03972,2.953020557,8.13E-05,6.09E-05,6.47E-05
1,3,3,0.07648,7.476494389,1.33E-04,1.01E-04,1.07E-04
1,4,4,0.15353,14.87953071,2.38E-04,1.69E-04,1.80E-04
1,5,5,0.29846,41.48950349,3.87E-04,2.67E-04,2.86E-04
1,6,6,0.61524,83.57629594,6.61E-04,4.11E-04,4.47E-04
1,7,7,1.24282,212.0806857,1.02E-03,6.10E-04,6.67E-04
20 changes: 17 additions & 3 deletions imap_processing/lo/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ class LoConstants:
# The first matching open interval (low < pivot < high) is used; if none matches,
# THRESHOLD_BG_RATE_RAM_DEFAULT / THRESHOLD_BG_RATE_ANTI_RAM_DEFAULT apply.
PIVOT_ANGLE_THRESHOLDS: ClassVar[dict[tuple[float, float], tuple[float, float]]] = {
(88.0, 92.0): (0.014, 0.007),
(73.0, 77.0): (0.0175, 0.00875),
(103.0, 107.0): (0.0112, 0.0056),
(88.0, 92.0): (0.028, 0.014),
(73.0, 77.0): (0.035, 0.0175),
(103.0, 107.0): (0.0224, 0.0112),
}

# Default background-rate thresholds [counts/s] when no pivot range matches.
Expand All @@ -75,3 +75,17 @@ class LoConstants:
# Padding [s] added to begin/end of each goodtime interval to ensure complete
# cycles are covered at interval edges.
GOODTIME_PADDING: float = 2.0

# Star-sensor spin-angle binning offset (fractional bin-index shift used when
# computing sample centers), keyed by the IFB star-sync housekeeping state
# (ifb_ctrl_star_sync). Flight software 4.8 enabled star sync ("EN"),
# switching from binning to the bin center (+0.5) to the left edge (+0.0).
STAR_BIN_OFFSET_BY_SYNC: ClassVar[dict[str | None, float]] = {
"DS": 0.5, # star sync disabled (pre FSW 4.8)
"EN": 0.0, # star sync enabled (FSW 4.8+)
}

# Number of ending bins to exclude from each star-sensor profile average.
STAR_END_BINS_TO_EXCLUDE: int = 2
# Minimum COUNT value for a star-sensor record to be considered valid.
STAR_MIN_COUNT_THRESHOLD: int = 700
90 changes: 73 additions & 17 deletions imap_processing/lo/l1b/lo_l1b.py
Original file line number Diff line number Diff line change
Expand Up @@ -1986,7 +1986,7 @@ def split_rate_dataset(

def filter_valid_star_records(
l1a_star: xr.Dataset,
min_count: int = 700,
min_count: int = c.STAR_MIN_COUNT_THRESHOLD,
time_window_offset: float = 0.0,
time_window_duration: float | None = None,
) -> np.ndarray:
Expand Down Expand Up @@ -2014,7 +2014,7 @@ def filter_valid_star_records(
valid_mask : np.ndarray
Boolean array indicating valid records.
"""
# Section 5: Acceptance Criteria - COUNT >= 700
# Section 5: Acceptance Criteria - COUNT >= min_count
count_mask = l1a_star["count"].values >= min_count

# shcoarse is already in MET seconds
Expand Down Expand Up @@ -2049,7 +2049,7 @@ def filter_valid_star_records(
def calculate_star_sensor_profile_for_group(
data: np.ndarray,
counts: np.ndarray,
end_bins_to_exclude: int = 2,
end_bins_to_exclude: int = c.STAR_END_BINS_TO_EXCLUDE,
) -> tuple[np.ndarray, np.ndarray]:
"""
Calculate averaged star sensor amplitude profile for a group of records.
Expand Down Expand Up @@ -2098,14 +2098,63 @@ def calculate_star_sensor_profile_for_group(
return avg_amplitude, count_array


def get_star_bin_offset(l1b_nhk: xr.Dataset, reference_epoch: int) -> float:
"""
Determine the star-sensor binning offset from the IFB star-sync state.

Reads ``ifb_ctrl_star_sync`` from the NHK housekeeping at the record nearest
on or before ``reference_epoch`` and maps it to a binning offset via
``LoConstants.STAR_BIN_OFFSET_BY_SYNC``.

Parameters
----------
l1b_nhk : xr.Dataset
L1B NHK dataset containing ``ifb_ctrl_star_sync`` and an ``epoch``
coordinate (TT2000 nanoseconds since J2000).
reference_epoch : int
Epoch at which to evaluate the sync state, in TT2000 nanoseconds since
J2000. The NHK record in effect at or before this time is used.

Returns
-------
bin_offset : float
Fractional bin-index offset to use when computing sample spin-angle
centers.

Raises
------
KeyError
If ``ifb_ctrl_star_sync`` is not present in ``l1b_nhk``.
"""
if "ifb_ctrl_star_sync" not in l1b_nhk:
raise KeyError(
"ifb_ctrl_star_sync field not found in L1B NHK dataset. "
"Cannot determine star-sensor binning offset."
)

nhk_epoch = l1b_nhk["epoch"].values
sync_state = l1b_nhk["ifb_ctrl_star_sync"].values

# Use the housekeeping record in effect at the reference epoch (the last
# NHK sample at or before it), clamping to the first sample if the reference
# epoch falls before NHK coverage.
idx = max(int(np.searchsorted(nhk_epoch, reference_epoch, side="right")) - 1, 0)
state = str(sync_state[idx])

offset = c.STAR_BIN_OFFSET_BY_SYNC[state]
logger.info(f"Star sync state '{state}' -> bin offset {offset}")
return offset


def calculate_star_sensor_profiles_by_group(
l1a_star: xr.Dataset,
sampling_cadence: float,
spin_period: float,
group_size: int = 64,
start_angle_offset: float = 62.0,
end_bins_to_exclude: int = 2,
min_count_threshold: int = 700,
end_bins_to_exclude: int = c.STAR_END_BINS_TO_EXCLUDE,
min_count_threshold: int = c.STAR_MIN_COUNT_THRESHOLD,
bin_offset: float = 0.5,
) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
"""
Calculate averaged star sensor amplitude profiles for groups of records.
Expand All @@ -2129,6 +2178,10 @@ def calculate_star_sensor_profiles_by_group(
Number of ending bins to exclude from each average (default: 2).
min_count_threshold : int
Minimum COUNT value for valid record (default: 700).
bin_offset : float
Fractional offset applied to bin indices when computing sample
spin-angle centers (default: 0.5). Use 0.5 to bin to the bin center
and 0.0 to bin to the left edge.

Returns
-------
Expand All @@ -2152,7 +2205,7 @@ def calculate_star_sensor_profiles_by_group(
# Calculate spin angles (same for all groups)
deg_per_bin = 360.0 * (sampling_cadence / 1000.0) / spin_period
bin_indices = np.arange(720)
sample_centers = (bin_indices + 0.5) * deg_per_bin
sample_centers = (bin_indices + bin_offset) * deg_per_bin
spin_angle = (start_angle_offset + sample_centers) % 360.0

if n_valid == 0:
Expand Down Expand Up @@ -2282,13 +2335,20 @@ def l1b_star(
logger.info(f"Using spin duration from spin data: {spin_duration:.6f} s")

# TODO: Read from ancillary config file when available
lo_angle_offset = 2.0
sc_to_inst_angle_offset = (
360 * get_spacecraft_to_instrument_spin_phase_offset(SpiceFrame.IMAP_LO)
+ lo_angle_offset
sc_to_inst_angle_offset = 360 * get_spacecraft_to_instrument_spin_phase_offset(
SpiceFrame.IMAP_LO
)
end_bins_to_exclude = 2
min_count_threshold = 700
end_bins_to_exclude = c.STAR_END_BINS_TO_EXCLUDE
min_count_threshold = c.STAR_MIN_COUNT_THRESHOLD

# Global epoch times from L1A data (used for start_doy/end_doy below).
global_start_epoch = l1a_star["epoch"].values[0]
global_end_epoch = l1a_star["epoch"].values[-1]

# Select the star-sensor binning convention from the IFB star-sync state in
# housekeeping. Evaluate at the earliest star record's epoch so a pointing that
# spans the `EN` event uses the value corresponding to the state at its start.
bin_offset = get_star_bin_offset(l1b_nhk, int(global_start_epoch))

# Calculate profiles for each 64-spin group
(
Expand All @@ -2304,12 +2364,9 @@ def l1b_star(
start_angle_offset=sc_to_inst_angle_offset,
end_bins_to_exclude=end_bins_to_exclude,
min_count_threshold=min_count_threshold,
bin_offset=bin_offset,
)

# Get global epoch times from L1A data for start_doy and end_doy
global_start_epoch = l1a_star["epoch"].values[0]
global_end_epoch = l1a_star["epoch"].values[-1]

# Create dataset with spin_angle as coordinate and multiple epochs
group_epochs = met_to_ttj2000ns(group_mets)
l1b_star_ds = xr.Dataset(
Expand Down Expand Up @@ -2375,7 +2432,6 @@ def l1b_star(
l1b_star_ds.attrs["pointing_mid_met"] = pointing_mid_met
l1b_star_ds.attrs["sampling_cadence_ms"] = sampling_cadence
l1b_star_ds.attrs["spin_duration_sec"] = spin_duration
l1b_star_ds.attrs["lo_angle_offset_deg"] = lo_angle_offset
l1b_star_ds.attrs["end_bins_excluded"] = end_bins_to_exclude
l1b_star_ds.attrs["min_count_threshold"] = min_count_threshold
l1b_star_ds.attrs["group_size"] = group_size
Expand Down
Loading
Loading