"""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", }