feat: multi-mode control (models.py)
Browse files
models.py
CHANGED
|
@@ -15,7 +15,7 @@ These follow the OpenEnv conventions (openenv-core):
|
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
import json
|
| 18 |
-
from typing import Any, Dict, List, Union
|
| 19 |
|
| 20 |
try:
|
| 21 |
from openenv.core.env_server.interfaces import Action, Observation, State
|
|
@@ -36,37 +36,69 @@ class SpacecraftAction(Action):
|
|
| 36 |
"""
|
| 37 |
Control action for the RANS spacecraft.
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
(8 for the default MFP2D layout).
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
"""
|
| 52 |
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
@field_validator("thrusters", mode="before")
|
| 56 |
@classmethod
|
| 57 |
-
def _coerce_thrusters(cls, v: Any) -> List[float]:
|
| 58 |
-
"""Accept
|
|
|
|
|
|
|
| 59 |
if isinstance(v, str):
|
| 60 |
v = v.strip()
|
| 61 |
-
|
|
|
|
| 62 |
if v.startswith("["):
|
| 63 |
try:
|
| 64 |
-
|
|
|
|
| 65 |
except json.JSONDecodeError:
|
| 66 |
pass
|
| 67 |
-
# Comma-separated: "0.5,0.5,..."
|
| 68 |
-
|
| 69 |
-
|
| 70 |
return v
|
| 71 |
|
| 72 |
|
|
|
|
| 15 |
from __future__ import annotations
|
| 16 |
|
| 17 |
import json
|
| 18 |
+
from typing import Any, Dict, List, Optional, Union
|
| 19 |
|
| 20 |
try:
|
| 21 |
from openenv.core.env_server.interfaces import Action, Observation, State
|
|
|
|
| 36 |
"""
|
| 37 |
Control action for the RANS spacecraft.
|
| 38 |
|
| 39 |
+
Three mutually-exclusive control modes are supported. The environment
|
| 40 |
+
picks whichever mode has non-None fields (priority: thrusters > force/torque
|
| 41 |
+
> velocity target).
|
|
|
|
| 42 |
|
| 43 |
+
**Mode 1 β Thruster activations (default)**
|
| 44 |
+
``thrusters``: list of N floats, each in [0, 1]. Length must match the
|
| 45 |
+
platform's thruster count (8 for the default MFP2D layout).
|
| 46 |
+
Accepts a comma-separated string from the web UI form.
|
| 47 |
+
Example::
|
| 48 |
|
| 49 |
+
SpacecraftAction(thrusters=[1, 0, 0, 0, 0, 0, 0, 0])
|
| 50 |
|
| 51 |
+
**Mode 2 β Direct world-frame force / torque**
|
| 52 |
+
``fx``, ``fy``: force components in N (world frame, any sign).
|
| 53 |
+
``torque``: yaw torque in NΒ·m (positive = CCW).
|
| 54 |
+
Bypasses thruster geometry entirely β useful for high-level control
|
| 55 |
+
or when you don't care about actuator layout.
|
| 56 |
+
Example::
|
| 57 |
+
|
| 58 |
+
SpacecraftAction(fx=2.0, fy=0.0, torque=0.5)
|
| 59 |
+
|
| 60 |
+
**Mode 3 β Target velocity (PD controller)**
|
| 61 |
+
``vx_target``, ``vy_target``: desired world-frame linear velocities (m/s).
|
| 62 |
+
``omega_target``: desired yaw rate (rad/s).
|
| 63 |
+
The environment applies a proportional controller each step to drive
|
| 64 |
+
the spacecraft toward the requested velocities.
|
| 65 |
+
Example::
|
| 66 |
+
|
| 67 |
+
SpacecraftAction(vx_target=0.5, vy_target=0.0, omega_target=0.0)
|
| 68 |
"""
|
| 69 |
|
| 70 |
+
# ββ Mode 1: thruster activations βββββββββββββββββββββββββββββββββββββ
|
| 71 |
+
thrusters: Optional[List[float]] = None
|
| 72 |
+
|
| 73 |
+
# ββ Mode 2: direct world-frame force / torque ββββββββββββββββββββββββ
|
| 74 |
+
fx: Optional[float] = None # N
|
| 75 |
+
fy: Optional[float] = None # N
|
| 76 |
+
torque: Optional[float] = None # NΒ·m
|
| 77 |
+
|
| 78 |
+
# ββ Mode 3: velocity targets (PD controller) βββββββββββββββββββββββββ
|
| 79 |
+
vx_target: Optional[float] = None # m/s
|
| 80 |
+
vy_target: Optional[float] = None # m/s
|
| 81 |
+
omega_target: Optional[float] = None # rad/s
|
| 82 |
|
| 83 |
@field_validator("thrusters", mode="before")
|
| 84 |
@classmethod
|
| 85 |
+
def _coerce_thrusters(cls, v: Any) -> Optional[List[float]]:
|
| 86 |
+
"""Accept JSON-array string, comma-separated string, or None."""
|
| 87 |
+
if v is None:
|
| 88 |
+
return None
|
| 89 |
if isinstance(v, str):
|
| 90 |
v = v.strip()
|
| 91 |
+
if not v:
|
| 92 |
+
return None
|
| 93 |
if v.startswith("["):
|
| 94 |
try:
|
| 95 |
+
parsed = json.loads(v)
|
| 96 |
+
return parsed if parsed else None
|
| 97 |
except json.JSONDecodeError:
|
| 98 |
pass
|
| 99 |
+
# Comma-separated: "0.5,0.5,..."
|
| 100 |
+
parsed = [float(x.strip()) for x in v.split(",") if x.strip()]
|
| 101 |
+
return parsed if parsed else None
|
| 102 |
return v
|
| 103 |
|
| 104 |
|