From 60e549302d50e4899235c0575cebb4e65afe9503 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 27 Jan 2026 12:12:10 -0700 Subject: [PATCH 01/10] block duplicate stations --- imap_processing/ialirt/generate_coverage.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/imap_processing/ialirt/generate_coverage.py b/imap_processing/ialirt/generate_coverage.py index 49cfecf22..57604d0d1 100644 --- a/imap_processing/ialirt/generate_coverage.py +++ b/imap_processing/ialirt/generate_coverage.py @@ -55,6 +55,7 @@ def generate_coverage( duration_seconds = 24 * 60 * 60 # 86400 seconds in 24 hours time_step = 5 * 60 # 5 min in seconds + # Non-DSN stations must be listed in order of priority. stations = { "Kiel": STATIONS["Kiel"], } @@ -67,20 +68,22 @@ def generate_coverage( time_range = np.arange(start_et_input, stop_et_input, time_step) total_visible_mask = np.zeros(time_range.shape, dtype=bool) - # Precompute DSN outage mask for non-DSN stations - dsn_outage_mask = np.zeros(time_range.shape, dtype=bool) + # Precompute DSN occupied mask for non-DSN stations + dsn_occupied_mask = np.zeros(time_range.shape, dtype=bool) if dsn: for dsn_contacts in dsn.values(): for start, end in dsn_contacts: start_et = str_to_et(start) end_et = str_to_et(end) - dsn_outage_mask |= (time_range >= start_et) & (time_range <= end_et) + dsn_occupied_mask |= (time_range >= start_et) & (time_range <= end_et) + # Blocks later stations. + non_dsn_occupied_mask = np.zeros(time_range.shape, dtype=bool) for station_name, (lon, lat, alt, min_elevation) in stations.items(): _azimuth, elevation = calculate_azimuth_and_elevation( lon, lat, alt, time_range, obsref="IAU_EARTH" ) - visible = elevation > min_elevation + visible_unblocked = elevation > min_elevation outage_mask = np.zeros(time_range.shape, dtype=bool) if outages and station_name in outages: @@ -89,9 +92,13 @@ def generate_coverage( end_et = str_to_et(end) outage_mask |= (time_range >= start_et) & (time_range <= end_et) - visible[outage_mask] = False - # DSN contacts block other stations - visible[dsn_outage_mask] = False + # Block this station if DSN is active OR already-occupied by earlier stations + unavailable_mask = outage_mask | dsn_occupied_mask | non_dsn_occupied_mask + visible = visible_unblocked & ~unavailable_mask + + # This station now occupies these times and will block later stations + non_dsn_occupied_mask |= visible + total_visible_mask |= visible coverage_dict[station_name] = et_to_utc(time_range[visible], format_str="ISOC") From 56578ee75a5e6eae2365e6260e04a639f0f09725 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 27 Jan 2026 13:08:44 -0700 Subject: [PATCH 02/10] remove coverage duplicates --- imap_processing/ialirt/generate_coverage.py | 10 ++-- .../ialirt/unit/test_generate_coverage.py | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/imap_processing/ialirt/generate_coverage.py b/imap_processing/ialirt/generate_coverage.py index 57604d0d1..4a2b66840 100644 --- a/imap_processing/ialirt/generate_coverage.py +++ b/imap_processing/ialirt/generate_coverage.py @@ -26,6 +26,10 @@ "DSS-74", "DSS-75", ] +# Non-DSN stations must be listed in order of priority. +NON_DSN_STATIONS = { + "Kiel": STATIONS["Kiel"], +} def generate_coverage( @@ -55,10 +59,6 @@ def generate_coverage( duration_seconds = 24 * 60 * 60 # 86400 seconds in 24 hours time_step = 5 * 60 # 5 min in seconds - # Non-DSN stations must be listed in order of priority. - stations = { - "Kiel": STATIONS["Kiel"], - } coverage_dict = {} outage_dict = {} @@ -79,7 +79,7 @@ def generate_coverage( # Blocks later stations. non_dsn_occupied_mask = np.zeros(time_range.shape, dtype=bool) - for station_name, (lon, lat, alt, min_elevation) in stations.items(): + for station_name, (lon, lat, alt, min_elevation) in NON_DSN_STATIONS.items(): _azimuth, elevation = calculate_azimuth_and_elevation( lon, lat, alt, time_range, obsref="IAU_EARTH" ) diff --git a/imap_processing/tests/ialirt/unit/test_generate_coverage.py b/imap_processing/tests/ialirt/unit/test_generate_coverage.py index c2ccc0caf..962d6e6a2 100644 --- a/imap_processing/tests/ialirt/unit/test_generate_coverage.py +++ b/imap_processing/tests/ialirt/unit/test_generate_coverage.py @@ -1,10 +1,12 @@ """Test processEphemeris functions.""" from datetime import datetime +from unittest.mock import patch import numpy as np import pytest +from imap_processing.ialirt.constants import STATIONS from imap_processing.ialirt.generate_coverage import ( format_coverage_summary, generate_coverage, @@ -109,3 +111,50 @@ def test_dsn(furnish_kernels): assert "I-ALiRT Coverage Summary" in output["summary"] assert 40.6 == output["total_coverage_percent"] + + +@pytest.mark.external_kernel +def test_non_dsn_priority_blocking_with_kernels(furnish_kernels): + "Test that non-dsn station block other non-dsn stations." + kernels = ["naif0012.tls", "pck00011.tpc", "de440s.bsp", "imap_spk_demo.bsp"] + start_time = "2026-09-22T00:00:00Z" + + with furnish_kernels(kernels): + # Kiel-only coverage + with patch( + "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", + new={"Kiel": STATIONS["Kiel"]}, + ): + cov_kiel, _ = generate_coverage(start_time) + + kiel_times = cov_kiel["Kiel"] + + # Manaus-only coverage + with patch( + "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", + new={"Manaus": STATIONS["Manaus"]}, + ): + cov_manaus_only, _ = generate_coverage(start_time) + + manaus_only_times = cov_manaus_only["Manaus"] + + overlap = np.intersect1d(kiel_times, manaus_only_times) + # Assert the times overlap. + assert overlap.size > 0 + + # Kiel first, then Manaus + with patch( + "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", + new={ + "Kiel": STATIONS["Kiel"], + "Manaus": STATIONS["Manaus"], + }, + ): + coverage, _ = generate_coverage(start_time) + + manaus_coverage = coverage["Manaus"] + + # Manaus should have no overlap with Kiel. + blocked_overlap = np.intersect1d(kiel_times, manaus_coverage) + assert blocked_overlap.size == 0 + assert manaus_coverage[0] > kiel_times[-1] From 81ca84cc02b7706d34fe59c6d330426222cee6e8 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 27 Jan 2026 14:04:38 -0700 Subject: [PATCH 03/10] added outages for dsn --- imap_processing/ialirt/generate_coverage.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/imap_processing/ialirt/generate_coverage.py b/imap_processing/ialirt/generate_coverage.py index 4a2b66840..0b3e03660 100644 --- a/imap_processing/ialirt/generate_coverage.py +++ b/imap_processing/ialirt/generate_coverage.py @@ -69,13 +69,22 @@ def generate_coverage( total_visible_mask = np.zeros(time_range.shape, dtype=bool) # Precompute DSN occupied mask for non-DSN stations - dsn_occupied_mask = np.zeros(time_range.shape, dtype=bool) + dsn_contact_mask = np.zeros(time_range.shape, dtype=bool) + dsn_outage_mask = np.zeros(time_range.shape, dtype=bool) if dsn: - for dsn_contacts in dsn.values(): + for dsn_station, dsn_contacts in dsn.items(): for start, end in dsn_contacts: start_et = str_to_et(start) end_et = str_to_et(end) - dsn_occupied_mask |= (time_range >= start_et) & (time_range <= end_et) + dsn_contact_mask |= (time_range >= start_et) & (time_range <= end_et) + + if outages and dsn_station in outages: + for start, end in outages[dsn_station]: + dsn_outage_mask |= (time_range >= str_to_et(start)) & ( + time_range <= str_to_et(end) + ) + + dsn_occupied_mask = dsn_contact_mask & ~dsn_outage_mask # Blocks later stations. non_dsn_occupied_mask = np.zeros(time_range.shape, dtype=bool) From 342300287a8db991d89260e9f6c6cde424054346 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 27 Jan 2026 15:40:47 -0700 Subject: [PATCH 04/10] added test --- .../ialirt/unit/test_generate_coverage.py | 64 +++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/imap_processing/tests/ialirt/unit/test_generate_coverage.py b/imap_processing/tests/ialirt/unit/test_generate_coverage.py index 962d6e6a2..f20b734d0 100644 --- a/imap_processing/tests/ialirt/unit/test_generate_coverage.py +++ b/imap_processing/tests/ialirt/unit/test_generate_coverage.py @@ -125,18 +125,18 @@ def test_non_dsn_priority_blocking_with_kernels(furnish_kernels): "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", new={"Kiel": STATIONS["Kiel"]}, ): - cov_kiel, _ = generate_coverage(start_time) + coverage_kiel, _ = generate_coverage(start_time) - kiel_times = cov_kiel["Kiel"] + kiel_times = coverage_kiel["Kiel"] # Manaus-only coverage with patch( "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", new={"Manaus": STATIONS["Manaus"]}, ): - cov_manaus_only, _ = generate_coverage(start_time) + coverage_manaus_only, _ = generate_coverage(start_time) - manaus_only_times = cov_manaus_only["Manaus"] + manaus_only_times = coverage_manaus_only["Manaus"] overlap = np.intersect1d(kiel_times, manaus_only_times) # Assert the times overlap. @@ -158,3 +158,59 @@ def test_non_dsn_priority_blocking_with_kernels(furnish_kernels): blocked_overlap = np.intersect1d(kiel_times, manaus_coverage) assert blocked_overlap.size == 0 assert manaus_coverage[0] > kiel_times[-1] + + +@pytest.mark.external_kernel +def test_dsn_outage_allows_ground_station_coverage(furnish_kernels): + """ + DSN contacts block non-DSN stations, but DSN outages remove blocking. + """ + kernels = ["naif0012.tls", "pck00011.tpc", "de440s.bsp", "imap_spk_demo.bsp"] + start_time = "2026-09-22T00:00:00Z" + + with furnish_kernels(kernels): + # Baseline Kiel-only coverage (no DSN) + with patch( + "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", + new={"Kiel": STATIONS["Kiel"]}, + ): + coverage_base, _ = generate_coverage(start_time) + + kiel_times = coverage_base["Kiel"] + + contact_start = kiel_times[10] # inside Kiel coverage + contact_end = kiel_times[16] # 30 min later (inclusive logic in your code) + + # Outage inside the DSN contact + outage_start = kiel_times[12] + outage_end = kiel_times[14] + + dsn = {"DSS-75": [(contact_start, contact_end)]} + + # DSN contact, no DSN outage + # Kiel should be blocked during the full contact window + with patch( + "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", + new={"Kiel": STATIONS["Kiel"]}, + ): + coverage_blocked, _ = generate_coverage(start_time, dsn=dsn, outages=None) + + kiel_blocked = coverage_blocked["Kiel"] + + assert not np.any(np.isin(kiel_times[10:16], kiel_blocked)) + + # DSN contact + DSN outage + # Kiel allowed during outage sub-window + outages = {"DSS-75": [(outage_start, outage_end)]} + + with patch( + "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", + new={"Kiel": STATIONS["Kiel"]}, + ): + coverage_punched, _ = generate_coverage( + start_time, dsn=dsn, outages=outages + ) + + kiel_punched = coverage_punched["Kiel"] + + assert np.all(np.isin(kiel_times[12:14], kiel_punched)) From 14f3c7a1e797d5e794790083c2b00c2fadb5b44c Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 27 Jan 2026 15:45:20 -0700 Subject: [PATCH 05/10] update test --- imap_processing/tests/ialirt/unit/test_generate_coverage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/imap_processing/tests/ialirt/unit/test_generate_coverage.py b/imap_processing/tests/ialirt/unit/test_generate_coverage.py index f20b734d0..e874c2f80 100644 --- a/imap_processing/tests/ialirt/unit/test_generate_coverage.py +++ b/imap_processing/tests/ialirt/unit/test_generate_coverage.py @@ -110,7 +110,7 @@ def test_dsn(furnish_kernels): ) assert "I-ALiRT Coverage Summary" in output["summary"] - assert 40.6 == output["total_coverage_percent"] + assert 42.0 == output["total_coverage_percent"] @pytest.mark.external_kernel From f605c1f289d258b8ee85f394589f9c076087936e Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Tue, 27 Jan 2026 16:46:02 -0700 Subject: [PATCH 06/10] quick fix --- imap_processing/ialirt/generate_coverage.py | 22 +++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/imap_processing/ialirt/generate_coverage.py b/imap_processing/ialirt/generate_coverage.py index 0b3e03660..db8400949 100644 --- a/imap_processing/ialirt/generate_coverage.py +++ b/imap_processing/ialirt/generate_coverage.py @@ -32,7 +32,7 @@ } -def generate_coverage( +def generate_coverage( # noqa: PLR0912 start_time: str, outages: dict | None = None, dsn: dict | None = None, @@ -73,16 +73,18 @@ def generate_coverage( dsn_outage_mask = np.zeros(time_range.shape, dtype=bool) if dsn: for dsn_station, dsn_contacts in dsn.items(): - for start, end in dsn_contacts: - start_et = str_to_et(start) - end_et = str_to_et(end) - dsn_contact_mask |= (time_range >= start_et) & (time_range <= end_et) + for contact_start, contact_end in dsn_contacts: + contact_start_et = str_to_et(contact_start) + contact_end_et = str_to_et(contact_end) + dsn_contact_mask |= (time_range >= contact_start_et) & ( + time_range <= contact_end_et + ) - if outages and dsn_station in outages: - for start, end in outages[dsn_station]: - dsn_outage_mask |= (time_range >= str_to_et(start)) & ( - time_range <= str_to_et(end) - ) + if outages and dsn_station in outages: + for outage_start, outage_end in outages[dsn_station]: + dsn_outage_mask |= (time_range >= str_to_et(outage_start)) & ( + time_range <= str_to_et(outage_end) + ) dsn_occupied_mask = dsn_contact_mask & ~dsn_outage_mask From 28ce347bc4487f76938af08b184fa46fed68ce0d Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Thu, 12 Feb 2026 15:04:16 -0700 Subject: [PATCH 07/10] revert --- imap_processing/ialirt/generate_coverage.py | 48 +++++++-------------- 1 file changed, 15 insertions(+), 33 deletions(-) diff --git a/imap_processing/ialirt/generate_coverage.py b/imap_processing/ialirt/generate_coverage.py index db8400949..49cfecf22 100644 --- a/imap_processing/ialirt/generate_coverage.py +++ b/imap_processing/ialirt/generate_coverage.py @@ -26,13 +26,9 @@ "DSS-74", "DSS-75", ] -# Non-DSN stations must be listed in order of priority. -NON_DSN_STATIONS = { - "Kiel": STATIONS["Kiel"], -} -def generate_coverage( # noqa: PLR0912 +def generate_coverage( start_time: str, outages: dict | None = None, dsn: dict | None = None, @@ -59,6 +55,9 @@ def generate_coverage( # noqa: PLR0912 duration_seconds = 24 * 60 * 60 # 86400 seconds in 24 hours time_step = 5 * 60 # 5 min in seconds + stations = { + "Kiel": STATIONS["Kiel"], + } coverage_dict = {} outage_dict = {} @@ -68,33 +67,20 @@ def generate_coverage( # noqa: PLR0912 time_range = np.arange(start_et_input, stop_et_input, time_step) total_visible_mask = np.zeros(time_range.shape, dtype=bool) - # Precompute DSN occupied mask for non-DSN stations - dsn_contact_mask = np.zeros(time_range.shape, dtype=bool) + # Precompute DSN outage mask for non-DSN stations dsn_outage_mask = np.zeros(time_range.shape, dtype=bool) if dsn: - for dsn_station, dsn_contacts in dsn.items(): - for contact_start, contact_end in dsn_contacts: - contact_start_et = str_to_et(contact_start) - contact_end_et = str_to_et(contact_end) - dsn_contact_mask |= (time_range >= contact_start_et) & ( - time_range <= contact_end_et - ) - - if outages and dsn_station in outages: - for outage_start, outage_end in outages[dsn_station]: - dsn_outage_mask |= (time_range >= str_to_et(outage_start)) & ( - time_range <= str_to_et(outage_end) - ) - - dsn_occupied_mask = dsn_contact_mask & ~dsn_outage_mask + for dsn_contacts in dsn.values(): + for start, end in dsn_contacts: + start_et = str_to_et(start) + end_et = str_to_et(end) + dsn_outage_mask |= (time_range >= start_et) & (time_range <= end_et) - # Blocks later stations. - non_dsn_occupied_mask = np.zeros(time_range.shape, dtype=bool) - for station_name, (lon, lat, alt, min_elevation) in NON_DSN_STATIONS.items(): + for station_name, (lon, lat, alt, min_elevation) in stations.items(): _azimuth, elevation = calculate_azimuth_and_elevation( lon, lat, alt, time_range, obsref="IAU_EARTH" ) - visible_unblocked = elevation > min_elevation + visible = elevation > min_elevation outage_mask = np.zeros(time_range.shape, dtype=bool) if outages and station_name in outages: @@ -103,13 +89,9 @@ def generate_coverage( # noqa: PLR0912 end_et = str_to_et(end) outage_mask |= (time_range >= start_et) & (time_range <= end_et) - # Block this station if DSN is active OR already-occupied by earlier stations - unavailable_mask = outage_mask | dsn_occupied_mask | non_dsn_occupied_mask - visible = visible_unblocked & ~unavailable_mask - - # This station now occupies these times and will block later stations - non_dsn_occupied_mask |= visible - + visible[outage_mask] = False + # DSN contacts block other stations + visible[dsn_outage_mask] = False total_visible_mask |= visible coverage_dict[station_name] = et_to_utc(time_range[visible], format_str="ISOC") From 24a10623305693f51c39be75322f506640695f01 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Thu, 12 Feb 2026 15:44:41 -0700 Subject: [PATCH 08/10] rollback --- .../ialirt/unit/test_generate_coverage.py | 107 +----------------- 1 file changed, 1 insertion(+), 106 deletions(-) diff --git a/imap_processing/tests/ialirt/unit/test_generate_coverage.py b/imap_processing/tests/ialirt/unit/test_generate_coverage.py index e874c2f80..c2ccc0caf 100644 --- a/imap_processing/tests/ialirt/unit/test_generate_coverage.py +++ b/imap_processing/tests/ialirt/unit/test_generate_coverage.py @@ -1,12 +1,10 @@ """Test processEphemeris functions.""" from datetime import datetime -from unittest.mock import patch import numpy as np import pytest -from imap_processing.ialirt.constants import STATIONS from imap_processing.ialirt.generate_coverage import ( format_coverage_summary, generate_coverage, @@ -110,107 +108,4 @@ def test_dsn(furnish_kernels): ) assert "I-ALiRT Coverage Summary" in output["summary"] - assert 42.0 == output["total_coverage_percent"] - - -@pytest.mark.external_kernel -def test_non_dsn_priority_blocking_with_kernels(furnish_kernels): - "Test that non-dsn station block other non-dsn stations." - kernels = ["naif0012.tls", "pck00011.tpc", "de440s.bsp", "imap_spk_demo.bsp"] - start_time = "2026-09-22T00:00:00Z" - - with furnish_kernels(kernels): - # Kiel-only coverage - with patch( - "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", - new={"Kiel": STATIONS["Kiel"]}, - ): - coverage_kiel, _ = generate_coverage(start_time) - - kiel_times = coverage_kiel["Kiel"] - - # Manaus-only coverage - with patch( - "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", - new={"Manaus": STATIONS["Manaus"]}, - ): - coverage_manaus_only, _ = generate_coverage(start_time) - - manaus_only_times = coverage_manaus_only["Manaus"] - - overlap = np.intersect1d(kiel_times, manaus_only_times) - # Assert the times overlap. - assert overlap.size > 0 - - # Kiel first, then Manaus - with patch( - "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", - new={ - "Kiel": STATIONS["Kiel"], - "Manaus": STATIONS["Manaus"], - }, - ): - coverage, _ = generate_coverage(start_time) - - manaus_coverage = coverage["Manaus"] - - # Manaus should have no overlap with Kiel. - blocked_overlap = np.intersect1d(kiel_times, manaus_coverage) - assert blocked_overlap.size == 0 - assert manaus_coverage[0] > kiel_times[-1] - - -@pytest.mark.external_kernel -def test_dsn_outage_allows_ground_station_coverage(furnish_kernels): - """ - DSN contacts block non-DSN stations, but DSN outages remove blocking. - """ - kernels = ["naif0012.tls", "pck00011.tpc", "de440s.bsp", "imap_spk_demo.bsp"] - start_time = "2026-09-22T00:00:00Z" - - with furnish_kernels(kernels): - # Baseline Kiel-only coverage (no DSN) - with patch( - "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", - new={"Kiel": STATIONS["Kiel"]}, - ): - coverage_base, _ = generate_coverage(start_time) - - kiel_times = coverage_base["Kiel"] - - contact_start = kiel_times[10] # inside Kiel coverage - contact_end = kiel_times[16] # 30 min later (inclusive logic in your code) - - # Outage inside the DSN contact - outage_start = kiel_times[12] - outage_end = kiel_times[14] - - dsn = {"DSS-75": [(contact_start, contact_end)]} - - # DSN contact, no DSN outage - # Kiel should be blocked during the full contact window - with patch( - "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", - new={"Kiel": STATIONS["Kiel"]}, - ): - coverage_blocked, _ = generate_coverage(start_time, dsn=dsn, outages=None) - - kiel_blocked = coverage_blocked["Kiel"] - - assert not np.any(np.isin(kiel_times[10:16], kiel_blocked)) - - # DSN contact + DSN outage - # Kiel allowed during outage sub-window - outages = {"DSS-75": [(outage_start, outage_end)]} - - with patch( - "imap_processing.ialirt.generate_coverage.NON_DSN_STATIONS", - new={"Kiel": STATIONS["Kiel"]}, - ): - coverage_punched, _ = generate_coverage( - start_time, dsn=dsn, outages=outages - ) - - kiel_punched = coverage_punched["Kiel"] - - assert np.all(np.isin(kiel_times[12:14], kiel_punched)) + assert 40.6 == output["total_coverage_percent"] From a0365addd35586cc24d0c2780b72623c5879ed50 Mon Sep 17 00:00:00 2001 From: Laura Sandoval Date: Thu, 12 Feb 2026 16:22:40 -0700 Subject: [PATCH 09/10] add static start and stop --- imap_processing/ialirt/constants.py | 11 +++ imap_processing/ialirt/generate_coverage.py | 70 +++++++++++++++++-- .../ialirt/unit/test_generate_coverage.py | 60 +++++++++++++++- 3 files changed, 133 insertions(+), 8 deletions(-) diff --git a/imap_processing/ialirt/constants.py b/imap_processing/ialirt/constants.py index 51705a37a..12c543b98 100644 --- a/imap_processing/ialirt/constants.py +++ b/imap_processing/ialirt/constants.py @@ -1,6 +1,7 @@ """Module for constants and useful shared classes used in I-ALiRT processing.""" from dataclasses import dataclass +from datetime import time from typing import NamedTuple import numpy as np @@ -48,6 +49,8 @@ class StationProperties(NamedTuple): latitude: float # latitude in degrees altitude: float # altitude in kilometers min_elevation_deg: float # minimum elevation angle in degrees + schedule_start: time | None # station schedule start + schedule_end: time | None # station schedule end # Verified by Kiel and KSWC Observatory staff. @@ -59,23 +62,31 @@ class StationProperties(NamedTuple): latitude=54.2632, # degrees North altitude=0.1, # approx 100 meters min_elevation_deg=5, # 5 degrees is the requirement + schedule_start=None, + schedule_end=None, ), "Korea": StationProperties( longitude=126.2958, # degrees East latitude=33.4273, # degrees North altitude=0.1, # approx 100 meters min_elevation_deg=5, # 5 degrees is the requirement + schedule_start=None, + schedule_end=None, ), "Manaus": StationProperties( longitude=-59.969334, # degrees East (negative = West) latitude=-2.891257, # degrees North (negative = South) altitude=0.1, # approx 100 meters min_elevation_deg=5, # 5 degrees is the requirement + schedule_start=None, + schedule_end=None, ), "SANSA": StationProperties( longitude=27.714, # degrees East (negative = West) latitude=-25.888, # degrees North (negative = South) altitude=1.542, # approx 1542 meters min_elevation_deg=2, # 5 degrees is the requirement + schedule_start=None, + schedule_end=None, ), } diff --git a/imap_processing/ialirt/generate_coverage.py b/imap_processing/ialirt/generate_coverage.py index 49cfecf22..0427ccba1 100644 --- a/imap_processing/ialirt/generate_coverage.py +++ b/imap_processing/ialirt/generate_coverage.py @@ -4,7 +4,7 @@ import numpy as np -from imap_processing.ialirt.constants import STATIONS +from imap_processing.ialirt.constants import STATIONS, StationProperties from imap_processing.ialirt.process_ephemeris import calculate_azimuth_and_elevation from imap_processing.spice.time import et_to_utc, str_to_et @@ -28,6 +28,55 @@ ] +def create_schedule_mask( + station: StationProperties, time_range: np.ndarray +) -> np.ndarray: + """ + Create a boolean mask based on the static daily operating schedule. + + Parameters + ---------- + station : StationProperties + Ground station configuration. + time_range : np.ndarray + Array of ephemeris time (ET) values corresponding to the + coverage time. + + Returns + ------- + schedule_mask : np.ndarray + Boolean array True is operating window. + """ + if station.schedule_start is None and station.schedule_end is None: + return np.ones(time_range.shape, dtype=bool) + + utc_times = et_to_utc(time_range, format_str="ISOC") + utc_dt = utc_times.astype("datetime64[s]") + + # seconds since midnight (UTC), vectorized + sec_of_day = (utc_dt - utc_dt.astype("datetime64[D]")) / np.timedelta64(1, "s") + + schedule_mask = np.ones(time_range.shape, dtype=bool) + + if station.schedule_start is not None: + start_sec = ( + station.schedule_start.hour * 3600 + + station.schedule_start.minute * 60 + + station.schedule_start.second + ) + schedule_mask &= sec_of_day >= start_sec + + if station.schedule_end is not None: + end_sec = ( + station.schedule_end.hour * 3600 + + station.schedule_end.minute * 60 + + station.schedule_end.second + ) + schedule_mask &= sec_of_day <= end_sec + + return schedule_mask + + def generate_coverage( start_time: str, outages: dict | None = None, @@ -76,11 +125,18 @@ def generate_coverage( end_et = str_to_et(end) dsn_outage_mask |= (time_range >= start_et) & (time_range <= end_et) - for station_name, (lon, lat, alt, min_elevation) in stations.items(): + for station_name, station in stations.items(): _azimuth, elevation = calculate_azimuth_and_elevation( - lon, lat, alt, time_range, obsref="IAU_EARTH" + station.longitude, + station.latitude, + station.altitude, + time_range, + obsref="IAU_EARTH", ) - visible = elevation > min_elevation + visible = elevation > station.min_elevation_deg + + schedule_mask = create_schedule_mask(station, time_range) + visible &= schedule_mask outage_mask = np.zeros(time_range.shape, dtype=bool) if outages and station_name in outages: @@ -133,9 +189,9 @@ def generate_coverage( coverage_dict["total_coverage_percent"] = total_coverage_percent # Ensure all stations are present in both dicts - for station in ALL_STATIONS: - coverage_dict.setdefault(station, np.array([], dtype=" Date: Thu, 12 Feb 2026 16:28:28 -0700 Subject: [PATCH 10/10] add static start and stop --- imap_processing/ialirt/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/imap_processing/ialirt/constants.py b/imap_processing/ialirt/constants.py index 12c543b98..91e787cb8 100644 --- a/imap_processing/ialirt/constants.py +++ b/imap_processing/ialirt/constants.py @@ -49,8 +49,8 @@ class StationProperties(NamedTuple): latitude: float # latitude in degrees altitude: float # altitude in kilometers min_elevation_deg: float # minimum elevation angle in degrees - schedule_start: time | None # station schedule start - schedule_end: time | None # station schedule end + schedule_start: time | None = None # station schedule start + schedule_end: time | None = None # station schedule end # Verified by Kiel and KSWC Observatory staff.