dpang commited on
Commit
1f11274
Β·
verified Β·
1 Parent(s): ef64cc0

feat: multi-mode control (models.py)

Browse files
Files changed (1) hide show
  1. models.py +50 -18
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
- ``thrusters`` is a list of activations, one per thruster, each in [0, 1].
40
- For binary (on/off) control pass values of 0.0 or 1.0.
41
- The list length should match the thruster count of the configured platform
42
- (8 for the default MFP2D layout).
43
 
44
- Example (8-thruster, fire thruster 0 only)::
 
 
 
 
45
 
46
- SpacecraftAction(thrusters=[1, 0, 0, 0, 0, 0, 0, 0])
47
 
48
- The web interface sends thrusters as a comma-separated string
49
- (e.g. "0.5,0.5,0.5,0.5,0.5,0.5,0.5,0.5") or a JSON array string
50
- (e.g. "[0.5,0.5,...]"). The validator below handles both forms.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  """
52
 
53
- thrusters: List[float]
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  @field_validator("thrusters", mode="before")
56
  @classmethod
57
- def _coerce_thrusters(cls, v: Any) -> List[float]:
58
- """Accept a JSON-array string or comma-separated string in addition to a list."""
 
 
59
  if isinstance(v, str):
60
  v = v.strip()
61
- # Try JSON array first: "[0.5, 0.5, ...]"
 
62
  if v.startswith("["):
63
  try:
64
- v = json.loads(v)
 
65
  except json.JSONDecodeError:
66
  pass
67
- # Comma-separated: "0.5,0.5,..." or a single value "0.5"
68
- if isinstance(v, str):
69
- v = [float(x.strip()) for x in v.split(",") if x.strip()]
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