SAT / models.py
harry19s's picture
UI push
27f6a5e
Raw
History Blame Contribute Delete
7.77 kB
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
"""Typed models for the satellite environment."""
import json
from typing import Any, Dict, List, Literal, Tuple
import pydantic
BaseModel = pydantic.BaseModel
Field = pydantic.Field
model_validator = getattr(pydantic, "model_validator", None)
field_validator = getattr(pydantic, "field_validator", None)
root_validator = getattr(pydantic, "root_validator", None)
validator = getattr(pydantic, "validator", None)
try:
from openenv.core.env_server.types import Action, Observation
except Exception: # pragma: no cover - local simulator fallback
Action = BaseModel
Observation = BaseModel
class SatelliteState(BaseModel):
"""State of an individual satellite in the constellation."""
id: int = Field(..., description="Satellite ID")
position: Tuple[float, float, float] = Field(
..., description="Position (x, y, z) in orbit"
)
battery: float = Field(..., description="Battery level 0-100")
storage: float = Field(..., description="Storage used percentage 0-100")
last_action: str = Field(..., description="Last action taken")
class SatelliteAction(Action):
"""Action for satellite constellation - commands for each satellite."""
@staticmethod
def _normalize_payload(value: Any) -> Any:
"""Accept dicts, JSON strings, and common wrapper shapes from HTTP UIs."""
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
return value
if not isinstance(value, dict):
return value
if "action" in value and "satellite_actions" not in value:
nested_action = value["action"]
if isinstance(nested_action, str):
try:
nested_action = json.loads(nested_action)
except json.JSONDecodeError:
nested_action = value["action"]
if isinstance(nested_action, dict):
value = nested_action
if "satellite_actions" not in value:
return {"satellite_actions": value}
satellite_actions = value.get("satellite_actions")
if isinstance(satellite_actions, str):
try:
satellite_actions = json.loads(satellite_actions)
except json.JSONDecodeError:
return value
if (
isinstance(satellite_actions, dict)
and "satellite_actions" in satellite_actions
and len(satellite_actions) == 1
):
satellite_actions = satellite_actions["satellite_actions"]
if isinstance(satellite_actions, dict):
value = {**value, "satellite_actions": satellite_actions}
return value
@staticmethod
def _normalize_satellite_actions_field(value: Any) -> Any:
"""Handle direct field validation when wrappers are passed to the field itself."""
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError:
return value
if not isinstance(value, dict):
return value
if "action" in value:
nested_action = value["action"]
if isinstance(nested_action, str):
try:
nested_action = json.loads(nested_action)
except json.JSONDecodeError:
nested_action = value["action"]
if isinstance(nested_action, dict):
value = nested_action
if "satellite_actions" in value:
nested_actions = value["satellite_actions"]
if isinstance(nested_actions, str):
try:
nested_actions = json.loads(nested_actions)
except json.JSONDecodeError:
return value
if isinstance(nested_actions, dict):
return nested_actions
return value
if model_validator is not None:
@model_validator(mode="before")
@classmethod
def _coerce_payload(cls, value: Any) -> Any:
return cls._normalize_payload(value)
elif root_validator is not None: # pragma: no cover - pydantic v1 fallback
@root_validator(pre=True)
def _coerce_payload(cls, values: Dict[str, Any]) -> Dict[str, Any]:
return cls._normalize_payload(values)
if field_validator is not None:
@field_validator("satellite_actions", mode="before")
@classmethod
def _coerce_satellite_actions_field(cls, value: Any) -> Any:
return cls._normalize_satellite_actions_field(value)
elif validator is not None: # pragma: no cover - pydantic v1 fallback
@validator("satellite_actions", pre=True)
def _coerce_satellite_actions_field(cls, value: Any) -> Any:
return cls._normalize_satellite_actions_field(value)
satellite_actions: Dict[int, Literal["capture", "downlink", "maintain", "idle"]] = Field(
...,
description=(
"Dictionary mapping satellite_id to action: "
"'capture', 'downlink', 'maintain', or 'idle'"
),
)
class SatelliteObservation(Observation):
"""Observation from the Satellite environment."""
satellites: List[SatelliteState] = Field(..., description="States of all satellites")
time_step: int = Field(..., description="Current time step")
ground_stations: List[Tuple[float, float]] = Field(
..., description="Ground station coordinates (lat, lon)"
)
weather_conditions: Dict[str, float] = Field(..., description="Cloud cover by region")
pending_tasks: List[Dict[str, Any]] = Field(..., description="List of pending tasks")
total_reward: float = Field(default=0.0, description="Cumulative reward")
done: bool = Field(default=False, description="Whether the episode has ended")
reward: float = Field(default=0.0, description="Immediate reward from the latest step")
metadata: Dict[str, Any] = Field(
default_factory=dict, description="Additional step metadata"
)
class SatelliteReward(BaseModel):
"""Typed reward model returned by the canonical environment API."""
value: float = Field(..., description="Scalar reward for the latest step")
components: Dict[str, float] = Field(
default_factory=dict, description="Reward component breakdown"
)
class SatelliteEnvState(BaseModel):
"""Typed state snapshot returned by state()."""
episode_id: str = Field(..., description="Unique episode identifier")
task_name: str = Field(..., description="Active task preset")
step_count: int = Field(..., description="Current environment step count")
max_steps: int = Field(..., description="Maximum steps allowed in the episode")
seed: int = Field(..., description="Deterministic seed for the current task setup")
done: bool = Field(..., description="Whether the episode is finished")
total_reward: float = Field(..., description="Accumulated reward")
metrics: Dict[str, float] = Field(
default_factory=dict, description="Deterministic episode metrics used for grading"
)
satellites: List[SatelliteState] = Field(..., description="Current satellite states")
ground_stations: List[Tuple[float, float]] = Field(
..., description="Ground station coordinates (lat, lon)"
)
weather_conditions: Dict[str, float] = Field(..., description="Cloud cover by region")
pending_tasks: List[Dict[str, Any]] = Field(..., description="Remaining visible tasks")