Skip to content
Open
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
Binary file added src/layup/data/ObsCodes.json.gz
Binary file not shown.
41 changes: 40 additions & 1 deletion src/layup/utilities/data_processing_utilities.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import gzip
import logging
import os
import tempfile
from concurrent.futures import ProcessPoolExecutor
from importlib.resources import files

import numpy as np
import requests
from sorcha.ephemeris.simulation_geometry import barycentricObservatoryRates
from sorcha.ephemeris.simulation_parsing import Observatory as SorchaObservatory
from sorcha.ephemeris.simulation_setup import furnish_spiceypy
Expand All @@ -17,6 +22,25 @@
logger = logging.getLogger(__name__)


def write_fallback_obscodes():
"""Decompress the observatory-codes file bundled with layup to a plain JSON
file and return its path.

The MPC observatory-codes file is normally downloaded from
minorplanetcenter.net on first use. When that download fails (e.g. the MPC
server is unreachable, as has happened on CI runners), we fall back to the
copy shipped in ``layup/data/ObsCodes.json.gz`` instead of failing the run.
Observatory codes change rarely, so a slightly stale fallback is far better
than a hard failure. Returns a path suitable for ``Observatory``'s
``oc_file`` argument (which reads the decompressed JSON directly).
"""
compressed = files("layup.data").joinpath("ObsCodes.json.gz").read_bytes()
dest = os.path.join(tempfile.gettempdir(), "layup_obscodes_fallback.json")
with open(dest, "wb") as f:
f.write(gzip.decompress(compressed))
return dest


def process_data(data, n_workers, func, **kwargs):
"""
Process a structured numpy array in parallel for a given function and keyword arguments
Expand Down Expand Up @@ -315,7 +339,22 @@ def __init__(self, cache_dir=None):
# Furnish the spiceypy kernels
layup_furnish_spiceypy(cache_dir)

super().__init__(FakeSorchaArgs(cache_dir), config.auxiliary)
try:
super().__init__(FakeSorchaArgs(cache_dir), config.auxiliary)
except requests.exceptions.RequestException as exc:
# The observatory-codes download from the MPC failed (server
# unreachable, timeout, etc.). Fall back to the copy bundled with
# layup so we don't fail the run over a transient network outage.
logger.warning(
"Could not fetch observatory codes from the MPC (%s); "
"falling back to the copy bundled with layup.",
exc,
)
super().__init__(
FakeSorchaArgs(cache_dir),
config.auxiliary,
oc_file=write_fallback_obscodes(),
)

# A cache of barycentric positions for observatories of the form {obscode: {et: (x, y, z)}}
self.cached_obs = {}
Expand Down
41 changes: 41 additions & 0 deletions tests/layup/test_data_processing_utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
has_cov_columns,
parse_fit_result,
process_data,
write_fallback_obscodes,
)
from layup.utilities.data_utilities_for_tests import get_test_filepath
from layup.utilities.file_io.CSVReader import CSVDataReader
Expand Down Expand Up @@ -560,3 +561,43 @@ def test_parse_fit_result_missing_some_cov_columns():
assert len(fit_result.cov) == 36
assert np.all(np.array(fit_result.cov[:-1]) != 0.0)
assert fit_result.cov[-1] == 0.0


def test_write_fallback_obscodes():
"""The obscodes copy bundled with layup decompresses to valid JSON that
sorcha's Observatory can read directly (network-outage fallback)."""
import json

path = write_fallback_obscodes()
with open(path) as f:
obs = json.load(f)
assert len(obs) > 2000
assert "X05" in obs and "500" in obs # Rubin + geocenter


def test_layup_observatory_falls_back_on_mpc_failure(monkeypatch):
"""If the MPC obscodes download fails (e.g. server unreachable), the
LayupObservatory should fall back to the bundled copy instead of raising."""
import requests

import layup.utilities.data_processing_utilities as dpu

orig_init = dpu.SorchaObservatory.__init__
calls = []

def fake_init(self, args, auxconfigs, oc_file=None):
calls.append(oc_file)
if oc_file is None:
# Simulate the MPC download failing.
raise requests.exceptions.ConnectTimeout("simulated MPC outage")
# The fallback path supplies a local oc_file; let the real init read it.
orig_init(self, args, auxconfigs, oc_file=oc_file)

monkeypatch.setattr(dpu.SorchaObservatory, "__init__", fake_init)

observatory = LayupObservatory()

# First attempt (download) raised; the second used the bundled fallback.
assert calls[0] is None and calls[1] is not None
assert "X05" in observatory.ObservatoryXYZ
assert len(observatory.ObservatoryXYZ) > 2000
Loading