Skip to content

Commit f5ebc7b

Browse files
authored
I-ALiRT - Coordinated coverage schedule (IMAP-Science-Operations-Center#2628)
1 parent 2c0abd6 commit f5ebc7b

3 files changed

Lines changed: 133 additions & 8 deletions

File tree

imap_processing/ialirt/constants.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Module for constants and useful shared classes used in I-ALiRT processing."""
22

33
from dataclasses import dataclass
4+
from datetime import time
45
from typing import NamedTuple
56

67
import numpy as np
@@ -55,6 +56,8 @@ class StationProperties(NamedTuple):
5556
latitude: float # latitude in degrees
5657
altitude: float # altitude in kilometers
5758
min_elevation_deg: float # minimum elevation angle in degrees
59+
schedule_start: time | None = None # station schedule start
60+
schedule_end: time | None = None # station schedule end
5861

5962

6063
# Verified by Observatory staff.
@@ -70,23 +73,31 @@ class StationProperties(NamedTuple):
7073
latitude=54.2632, # degrees North
7174
altitude=0.1, # approx 100 meters
7275
min_elevation_deg=5, # 5 degrees is the requirement
76+
schedule_start=None,
77+
schedule_end=None,
7378
),
7479
"Korea": StationProperties(
7580
longitude=126.2958, # degrees East
7681
latitude=33.4273, # degrees North
7782
altitude=0.1, # approx 100 meters
7883
min_elevation_deg=5, # 5 degrees is the requirement
84+
schedule_start=None,
85+
schedule_end=None,
7986
),
8087
"Manaus": StationProperties(
8188
longitude=-59.969319, # degrees East (negative = West)
8289
latitude=-2.891215, # degrees North (negative = South)
8390
altitude=0.9578, # approx 957.8 meters
8491
min_elevation_deg=5, # 5 degrees is the requirement
92+
schedule_start=None,
93+
schedule_end=None,
8594
),
8695
"SANSA": StationProperties(
8796
longitude=27.714, # degrees East (negative = West)
8897
latitude=-25.888, # degrees North (negative = South)
8998
altitude=1.542, # approx 1542 meters
9099
min_elevation_deg=2, # 5 degrees is the requirement
100+
schedule_start=None,
101+
schedule_end=None,
91102
),
92103
}

imap_processing/ialirt/generate_coverage.py

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import numpy as np
66

7-
from imap_processing.ialirt.constants import STATIONS
7+
from imap_processing.ialirt.constants import STATIONS, StationProperties
88
from imap_processing.ialirt.process_ephemeris import calculate_azimuth_and_elevation
99
from imap_processing.spice.time import et_to_utc, str_to_et
1010

@@ -28,6 +28,55 @@
2828
]
2929

3030

31+
def create_schedule_mask(
32+
station: StationProperties, time_range: np.ndarray
33+
) -> np.ndarray:
34+
"""
35+
Create a boolean mask based on the static daily operating schedule.
36+
37+
Parameters
38+
----------
39+
station : StationProperties
40+
Ground station configuration.
41+
time_range : np.ndarray
42+
Array of ephemeris time (ET) values corresponding to the
43+
coverage time.
44+
45+
Returns
46+
-------
47+
schedule_mask : np.ndarray
48+
Boolean array True is operating window.
49+
"""
50+
if station.schedule_start is None and station.schedule_end is None:
51+
return np.ones(time_range.shape, dtype=bool)
52+
53+
utc_times = et_to_utc(time_range, format_str="ISOC")
54+
utc_dt = utc_times.astype("datetime64[s]")
55+
56+
# seconds since midnight (UTC), vectorized
57+
sec_of_day = (utc_dt - utc_dt.astype("datetime64[D]")) / np.timedelta64(1, "s")
58+
59+
schedule_mask = np.ones(time_range.shape, dtype=bool)
60+
61+
if station.schedule_start is not None:
62+
start_sec = (
63+
station.schedule_start.hour * 3600
64+
+ station.schedule_start.minute * 60
65+
+ station.schedule_start.second
66+
)
67+
schedule_mask &= sec_of_day >= start_sec
68+
69+
if station.schedule_end is not None:
70+
end_sec = (
71+
station.schedule_end.hour * 3600
72+
+ station.schedule_end.minute * 60
73+
+ station.schedule_end.second
74+
)
75+
schedule_mask &= sec_of_day <= end_sec
76+
77+
return schedule_mask
78+
79+
3180
def generate_coverage(
3281
start_time: str,
3382
outages: dict | None = None,
@@ -76,11 +125,18 @@ def generate_coverage(
76125
end_et = str_to_et(end)
77126
dsn_outage_mask |= (time_range >= start_et) & (time_range <= end_et)
78127

79-
for station_name, (lon, lat, alt, min_elevation) in stations.items():
128+
for station_name, station in stations.items():
80129
_azimuth, elevation = calculate_azimuth_and_elevation(
81-
lon, lat, alt, time_range, obsref="IAU_EARTH"
130+
station.longitude,
131+
station.latitude,
132+
station.altitude,
133+
time_range,
134+
obsref="IAU_EARTH",
82135
)
83-
visible = elevation > min_elevation
136+
visible = elevation > station.min_elevation_deg
137+
138+
schedule_mask = create_schedule_mask(station, time_range)
139+
visible &= schedule_mask
84140

85141
outage_mask = np.zeros(time_range.shape, dtype=bool)
86142
if outages and station_name in outages:
@@ -133,9 +189,9 @@ def generate_coverage(
133189
coverage_dict["total_coverage_percent"] = total_coverage_percent
134190

135191
# Ensure all stations are present in both dicts
136-
for station in ALL_STATIONS:
137-
coverage_dict.setdefault(station, np.array([], dtype="<U23"))
138-
outage_dict.setdefault(station, np.array([], dtype="<U23"))
192+
for station_name in ALL_STATIONS:
193+
coverage_dict.setdefault(station_name, np.array([], dtype="<U23"))
194+
outage_dict.setdefault(station_name, np.array([], dtype="<U23"))
139195

140196
return coverage_dict, outage_dict
141197

imap_processing/tests/ialirt/unit/test_generate_coverage.py

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
"""Test processEphemeris functions."""
22

3-
from datetime import datetime
3+
from datetime import datetime, time
4+
from types import SimpleNamespace
5+
from unittest.mock import patch
46

57
import numpy as np
68
import pytest
79

810
from imap_processing.ialirt.generate_coverage import (
11+
create_schedule_mask,
912
format_coverage_summary,
1013
generate_coverage,
1114
)
@@ -109,3 +112,58 @@ def test_dsn(furnish_kernels):
109112

110113
assert "I-ALiRT Coverage Summary" in output["summary"]
111114
assert 40.6 == output["total_coverage_percent"]
115+
116+
117+
@patch("imap_processing.ialirt.generate_coverage.et_to_utc")
118+
def test_create_schedule_mask(mock_et_to_utc):
119+
"""
120+
Test create_schedule_mask.
121+
"""
122+
123+
mock_et_to_utc.return_value = np.array(
124+
[
125+
"2026-09-22T11:30:00.000",
126+
"2026-09-22T11:35:00.000",
127+
"2026-09-22T11:40:00.000",
128+
"2026-09-22T11:45:00.000",
129+
"2026-09-22T11:50:00.000",
130+
"2026-09-22T11:55:00.000",
131+
"2026-09-22T12:00:00.000",
132+
"2026-09-22T12:05:00.000",
133+
"2026-09-22T12:10:00.000",
134+
"2026-09-22T12:15:00.000",
135+
"2026-09-22T12:20:00.000",
136+
"2026-09-22T12:25:00.000",
137+
"2026-09-22T12:30:00.000",
138+
]
139+
)
140+
141+
time_range = np.arange(13)
142+
143+
station = SimpleNamespace(
144+
schedule_start=time(12, 0),
145+
schedule_end=None,
146+
)
147+
148+
mask = create_schedule_mask(station, time_range)
149+
150+
expected = np.array(
151+
[
152+
False,
153+
False,
154+
False,
155+
False,
156+
False,
157+
False,
158+
True,
159+
True,
160+
True,
161+
True,
162+
True,
163+
True,
164+
True,
165+
],
166+
dtype=bool,
167+
)
168+
169+
np.testing.assert_array_equal(mask, expected)

0 commit comments

Comments
 (0)