yksa-orbital/yksa_orbital/model_config.py
2026-08-18 22:01:53 +08:00

108 lines
4.2 KiB
Python

"""What simulation model the sidecar is flying, asked rather than assumed.
The model -- force model, propagator, decay altitude, drag-fit thresholds,
space-weather sources -- is defined once, in the sidecar's ``config.py``, because
more than one service flies it and two forecasts are only comparable if the model
behind them is the same. This is how a Django service reads that definition
instead of keeping a second copy that drifts.
Use it for the values a page or a plot genuinely needs locally: the ensemble's
display floor, the drag-fit window count, the decay altitude in a caption. Do
*not* use it to build a request -- omitting a parameter already gets the
sidecar's value, and echoing it back would only add a way for the two to
disagree.
Cached, because it changes on deploy and not otherwise, and because a page that
wants one number out of it must not pay a round trip. Falls back to a vendored
copy of the defaults when the sidecar is unreachable, so a caption never takes a
page down; the fallback is flagged so a caller can tell.
"""
from __future__ import annotations
import logging
from django.conf import settings
from django.core.cache import cache
from odm import PropagationError
from .registry import get_backend
logger = logging.getLogger(__name__)
CACHE_KEY = "yksa_orbital:model_config"
CACHE_TTL_S = 3600
#: A failure is cached too, briefly. Without it a sidecar that is down costs a
#: fresh connection attempt on every page render -- and against an unresolvable
#: hostname that is a DNS timeout each time, which is how a caption took a page
#: down rather than the other way round.
FAILURE_TTL_S = 60
#: The sidecar's defaults as of this release. Only ever used when the sidecar
#: cannot be reached, and only for values that are read for display -- so a stale
#: entry here shows a slightly wrong caption rather than flying a wrong model.
#: Keep it in step with ``services/orekit/config.py`` when a default changes.
FALLBACK = {
"force_model": {
"gravity_degree": 4, "gravity_order": 4,
"srp": True, "third_bodies": True, "tesseral": False,
"atmosphere": "NRLMSISE00",
},
"decay": {
"method": "auto", "strengths": ["AVERAGE"], "altitude_km": 105.0,
"max_years": 25.0, "dsst_handover_altitude_km": 180.0,
},
"drag_fit": {"window_days": 90, "min_elements": 30, "windows": 5},
"ensemble": {
"realizations": 30, "percentiles": [10, 25, 50, 75, 90],
"display_floor_km": 150.0,
},
"unavailable": True,
}
def model_config(*, refresh: bool = False) -> dict:
"""The sidecar's resolved model. Never raises."""
if not refresh:
cached = cache.get(CACHE_KEY)
if cached is not None:
return cached
try:
config = get_backend("orekit").model_config()
except (PropagationError, ValueError) as exc:
logger.warning("could not read the sidecar's model config: %s", exc)
cache.set(CACHE_KEY, FALLBACK, FAILURE_TTL_S)
return FALLBACK
cache.set(CACHE_KEY, config, CACHE_TTL_S)
return config
def setting(section: str, key: str, default=None):
"""One value out of the model, with the local override applied if there is one.
Django's ``DECAY_*`` settings are overrides: when one is set, this deployment
really is flying something other than the sidecar's model, and a caption
reading the sidecar's value would be describing a different run than the one
on screen.
"""
override = getattr(settings, _OVERRIDES.get((section, key), ""), None)
if override not in (None, ""):
return override
return model_config().get(section, {}).get(key, default)
#: (section, key) -> the Django setting that overrides it. Mirrors
#: ``yksa_tle.predictions.service._DECAY_OVERRIDES``.
_OVERRIDES = {
("decay", "altitude_km"): "DECAY_ALTITUDE_KM",
("decay", "strengths"): "DECAY_STRENGTHS",
("decay", "method"): "DECAY_METHOD",
("decay", "max_years"): "DECAY_MAX_YEARS",
("force_model", "gravity_degree"): "DECAY_GRAVITY_DEGREE",
("force_model", "gravity_order"): "DECAY_GRAVITY_ORDER",
("force_model", "srp"): "DECAY_SRP",
("force_model", "third_bodies"): "DECAY_THIRD_BODIES",
("drag_fit", "windows"): "DECAY_DRAG_FIT_WINDOWS",
("ensemble", "realizations"): "DECAY_ENSEMBLE_REALIZATIONS",
}