mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-08 05:23:41 +08:00
129 lines
4.7 KiB
Python
129 lines
4.7 KiB
Python
"""Select an eligible observation without accepting it or authorizing control."""
|
|
from collections.abc import Mapping
|
|
from dataclasses import dataclass
|
|
from enum import StrEnum
|
|
|
|
from openpilot.starpilot.speed_limits.acceptance import Observation, ObservationKind, observation_is_valid
|
|
|
|
|
|
class Source(StrEnum):
|
|
DASHBOARD = "dashboard"
|
|
MAP = "map"
|
|
VISION = "vision"
|
|
ONLINE = "online"
|
|
|
|
|
|
class SelectionMode(StrEnum):
|
|
ORDERED = "ordered"
|
|
HIGHEST = "highest"
|
|
LOWEST = "lowest"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class SelectionPolicy:
|
|
mode: SelectionMode
|
|
slots: tuple[Source | None, Source | None]
|
|
online_fallback: bool
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Selection:
|
|
observation: Observation
|
|
selected_source: Source | None
|
|
reason: str
|
|
considered: tuple[Source, ...] = ()
|
|
unavailable: tuple[tuple[Source, ObservationKind], ...] = ()
|
|
below_minimum: tuple[Source, ...] = ()
|
|
errors: tuple[str, ...] = ()
|
|
|
|
|
|
MINIMUM_LIMIT_MPS = 1.0
|
|
PRIMARY_ORDER = (Source.DASHBOARD, Source.MAP, Source.VISION)
|
|
|
|
|
|
def select_limit(observations: Mapping[Source, Observation], policy: SelectionPolicy) -> Selection:
|
|
"""Use valid configured sources, then an optional online fallback.
|
|
|
|
The caller supplies normalized, eligible, source-specific observations. A
|
|
missing producer is UNKNOWN, not ABSENT. Selection does not infer freshness,
|
|
vision support, map lookahead, road identity or provider request permission.
|
|
"""
|
|
errors = _input_errors(observations, policy)
|
|
if errors:
|
|
return Selection(Observation(ObservationKind.UNKNOWN), None, "invalid_input", errors=tuple(errors))
|
|
|
|
if policy.mode == SelectionMode.ORDERED:
|
|
considered = tuple(dict.fromkeys(source for source in policy.slots if source is not None))
|
|
else:
|
|
considered = PRIMARY_ORDER if Source.VISION in policy.slots else PRIMARY_ORDER[:2]
|
|
|
|
unavailable = []
|
|
below_minimum = []
|
|
eligible = []
|
|
|
|
def consider(source):
|
|
observation = observations[source]
|
|
if observation.kind == ObservationKind.VALID:
|
|
if observation.candidate.speed_mps >= MINIMUM_LIMIT_MPS:
|
|
eligible.append(source)
|
|
return
|
|
below_minimum.append(source)
|
|
unavailable.append((source, ObservationKind.ABSENT))
|
|
else:
|
|
unavailable.append((source, observation.kind))
|
|
|
|
for source in considered:
|
|
consider(source)
|
|
|
|
if eligible:
|
|
if policy.mode == SelectionMode.ORDERED:
|
|
selected = eligible[0]
|
|
else:
|
|
# Stable source ordering makes equal-limit ties independent of input map order.
|
|
choose = max if policy.mode == SelectionMode.HIGHEST else min
|
|
selected = choose(eligible, key=lambda source: observations[source].candidate.speed_mps)
|
|
reason = "primary"
|
|
elif policy.online_fallback:
|
|
considered += (Source.ONLINE,)
|
|
consider(Source.ONLINE)
|
|
selected = eligible[0] if eligible else None
|
|
reason = "online_fallback" if selected is not None else "no_candidate"
|
|
else:
|
|
selected = None
|
|
reason = "no_candidate"
|
|
|
|
if selected is not None:
|
|
observation = observations[selected]
|
|
else:
|
|
kinds = {kind for _, kind in unavailable}
|
|
# Preserve source availability; acceptance independently owns previous-limit fallback.
|
|
kind = (ObservationKind.UNKNOWN if ObservationKind.UNKNOWN in kinds else
|
|
ObservationKind.STALE if ObservationKind.STALE in kinds else ObservationKind.ABSENT)
|
|
observation = Observation(kind)
|
|
return Selection(observation, selected, reason, considered, tuple(unavailable), tuple(below_minimum))
|
|
|
|
|
|
def _input_errors(observations, policy):
|
|
errors = []
|
|
if not isinstance(policy, SelectionPolicy):
|
|
return ["policy must be SelectionPolicy"]
|
|
if not isinstance(policy.mode, SelectionMode):
|
|
errors.append("selection mode must be explicit")
|
|
if (not isinstance(policy.slots, tuple) or len(policy.slots) != 2 or
|
|
any(source is not None and (not isinstance(source, Source) or source == Source.ONLINE) for source in policy.slots)):
|
|
errors.append("two primary source slots are required; online is fallback only")
|
|
if type(policy.online_fallback) is not bool:
|
|
errors.append("online fallback must be Boolean")
|
|
if not isinstance(observations, Mapping):
|
|
return errors + ["observations must be a source mapping"]
|
|
if len(observations) != len(Source) or any(not isinstance(source, Source) for source in observations) or set(observations) != set(Source):
|
|
return errors + ["every source requires an explicit observation"]
|
|
for source, observation in observations.items():
|
|
if not observation_is_valid(observation):
|
|
errors.append(f"{source.value}: invalid observation")
|
|
continue
|
|
candidate = observation.candidate
|
|
if candidate is not None and candidate.source != source.value:
|
|
errors.append(f"{source.value}: candidate source mismatch")
|
|
return errors
|