File size: 9,339 Bytes
d8290eb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
"""Typed loading and validation for the standalone H2 deployment config."""

from __future__ import annotations

from dataclasses import dataclass
import math
from pathlib import Path

import yaml

from .h2_contract import COMMAND_JOINT_MARGIN_RAD
from .deployment_versions import deployment_bundle


PROJECT_ROOT = Path(__file__).resolve().parents[1]


def _resolve(value: str) -> Path:
    path = Path(str(value)).expanduser()
    return path.resolve() if path.is_absolute() else (PROJECT_ROOT / path).resolve()


def _section(data: dict, key: str) -> dict:
    value = data.get(key, {})
    if not isinstance(value, dict):
        raise ValueError(f"config section {key!r} must be a mapping")
    return value


def _bounded_float(name: str, value: object, lower: float, upper: float) -> float:
    result = float(value)
    if not math.isfinite(result) or not lower <= result <= upper:
        raise ValueError(f"{name} must be finite and in [{lower},{upper}], got {value!r}")
    return result


@dataclass(frozen=True)
class DeploySettings:
    source: Path
    deployment_version: str
    policy_path: Path
    policy_device: str
    generator_device: str | None
    generator_sampling_steps: int | None
    generator_warmup_steps: int
    seed: int
    hardware_joint_map_revision: str
    landing_xy: tuple[float, float]
    active_latency_skip_s: float
    hit_plane_tolerance_m: float
    joint_limit_tolerance_rad: float
    command_joint_margin_rad: float
    stop_hold_torque_fraction: float
    max_joint_velocity_rad_s: float
    max_tilt_rad: float
    max_abs_incoming_velocity_mps: float
    robot_mocap_timeout_s: float
    ball_mocap_timeout_s: float
    max_control_gap_s: float
    ready_position_tolerance_rad: float
    ready_arm_position_tolerance_rad: float
    ready_velocity_tolerance_rad_s: float
    ready_settle_duration_s: float
    ready_post_interpolation_timeout_s: float
    max_initial_root_xy_error_m: float
    max_mocap_root_jump_m: float
    max_mocap_root_speed_mps: float
    max_mocap_root_xy_m: float
    max_mocap_heading_jump_rad: float
    max_mocap_heading_rate_rad_s: float


def load_settings(path: str | Path, *, deployment_version: str | None = None) -> DeploySettings:
    source = Path(path).expanduser().resolve()
    with source.open("r", encoding="utf-8") as stream:
        data = yaml.safe_load(stream) or {}
    if not isinstance(data, dict):
        raise ValueError("top-level config must be a mapping")
    if str(data.get("robot", "")).lower() != "h2":
        raise ValueError("this deployment package only accepts robot: h2")

    policy = _section(data, "policy")
    version = deployment_version if deployment_version is not None else str(data.get("deployment_version", "original"))
    bundle = deployment_bundle(version)
    if "path" in policy and _resolve(str(policy["path"])) != bundle.policy_path:
        raise ValueError(
            f"policy.path must resolve exactly to the {version} bundle policy: {bundle.policy_path}; "
            "remove policy.path and select --deployment-version original|v1|v2"
        )
    generator = _section(data, "generator")
    command = _section(data, "command")
    safety = _section(data, "safety")
    hardware = _section(data, "hardware")
    policy_device = str(policy.get("device", "cpu")).lower()
    if policy_device not in ("cpu", "cuda"):
        raise ValueError("policy.device must be cpu or cuda")
    generator_device = generator.get("device")
    if generator_device is not None:
        generator_device = str(generator_device).lower()
        if generator_device not in ("cpu", "cuda"):
            raise ValueError("generator.device must be cpu, cuda or null")
    sampling_steps = generator.get("sampling_steps")
    sampling_steps = None if sampling_steps is None else int(sampling_steps)
    landing = tuple(float(x) for x in command.get("target_landing_xy", [2.87, 0.0]))
    if len(landing) != 2 or not all(math.isfinite(value) for value in landing):
        raise ValueError("command.target_landing_xy must have two finite values")

    settings = DeploySettings(
        source=source,
        deployment_version=version,
        policy_path=bundle.policy_path,
        policy_device=policy_device,
        generator_device=generator_device,
        generator_sampling_steps=sampling_steps,
        generator_warmup_steps=max(0, int(generator.get("warmup_steps", 1))),
        seed=int(data.get("seed", 0)),
        hardware_joint_map_revision=str(hardware.get("joint_map_revision", "")),
        landing_xy=(landing[0], landing[1]),
        active_latency_skip_s=_bounded_float(
            "command.active_latency_skip_s", command.get("active_latency_skip_s", 0.0), 0.0, 1.0
        ),
        hit_plane_tolerance_m=_bounded_float(
            "safety.hit_plane_tolerance_m", safety.get("hit_plane_tolerance_m", 0.06), 0.0, 0.20
        ),
        joint_limit_tolerance_rad=_bounded_float(
            "safety.joint_limit_tolerance_rad",
            safety.get("joint_limit_tolerance_rad", 0.05), 0.0, 0.20,
        ),
        command_joint_margin_rad=_bounded_float(
            "safety.command_joint_margin_rad",
            safety.get("command_joint_margin_rad", COMMAND_JOINT_MARGIN_RAD),
            COMMAND_JOINT_MARGIN_RAD,
            0.10,
        ),
        stop_hold_torque_fraction=_bounded_float(
            "safety.stop_hold_torque_fraction",
            safety.get("stop_hold_torque_fraction", 0.90), 0.25, 0.90,
        ),
        max_joint_velocity_rad_s=_bounded_float(
            "safety.max_joint_velocity_rad_s",
            safety.get("max_joint_velocity_rad_s", 40.0), 0.1, 60.0,
        ),
        max_tilt_rad=_bounded_float(
            "safety.max_tilt_rad", safety.get("max_tilt_rad", 0.80), 0.1, 1.2
        ),
        max_abs_incoming_velocity_mps=_bounded_float(
            "safety.max_abs_incoming_velocity_mps",
            safety.get("max_abs_incoming_velocity_mps", 15.0), 0.1, 30.0,
        ),
        robot_mocap_timeout_s=_bounded_float(
            "safety.robot_mocap_timeout_s",
            safety.get("robot_mocap_timeout_s", 0.10), 0.02, 0.25,
        ),
        ball_mocap_timeout_s=_bounded_float(
            "safety.ball_mocap_timeout_s",
            safety.get("ball_mocap_timeout_s", 0.12), 0.02, 0.25,
        ),
        max_control_gap_s=_bounded_float(
            "safety.max_control_gap_s", safety.get("max_control_gap_s", 0.06), 0.02, 0.099
        ),
        ready_position_tolerance_rad=_bounded_float(
            "safety.ready_position_tolerance_rad",
            safety.get("ready_position_tolerance_rad", 0.10), 0.001, 0.30,
        ),
        ready_arm_position_tolerance_rad=_bounded_float(
            "safety.ready_arm_position_tolerance_rad",
            safety.get("ready_arm_position_tolerance_rad", 0.55), 0.30, 0.60,
        ),
        ready_velocity_tolerance_rad_s=_bounded_float(
            "safety.ready_velocity_tolerance_rad_s",
            safety.get("ready_velocity_tolerance_rad_s", 1.0), 0.01, 3.0,
        ),
        ready_settle_duration_s=_bounded_float(
            "safety.ready_settle_duration_s",
            safety.get("ready_settle_duration_s", 0.50), 0.10, 3.0,
        ),
        ready_post_interpolation_timeout_s=_bounded_float(
            "safety.ready_post_interpolation_timeout_s",
            safety.get("ready_post_interpolation_timeout_s", 10.0), 2.0, 30.0,
        ),
        max_initial_root_xy_error_m=_bounded_float(
            "safety.max_initial_root_xy_error_m",
            safety.get("max_initial_root_xy_error_m", 0.30), 0.01, 0.50,
        ),
        max_mocap_root_jump_m=_bounded_float(
            "safety.max_mocap_root_jump_m",
            safety.get("max_mocap_root_jump_m", 0.10), 0.01, 0.50,
        ),
        max_mocap_root_speed_mps=_bounded_float(
            "safety.max_mocap_root_speed_mps",
            safety.get("max_mocap_root_speed_mps", 2.0), 0.1, 5.0,
        ),
        max_mocap_root_xy_m=_bounded_float(
            "safety.max_mocap_root_xy_m",
            safety.get("max_mocap_root_xy_m", 0.75), 0.10, 2.0,
        ),
        max_mocap_heading_jump_rad=_bounded_float(
            "safety.max_mocap_heading_jump_rad",
            safety.get("max_mocap_heading_jump_rad", 0.25), 0.01, 1.0,
        ),
        max_mocap_heading_rate_rad_s=_bounded_float(
            "safety.max_mocap_heading_rate_rad_s",
            safety.get("max_mocap_heading_rate_rad_s", 4.0), 0.1, 10.0,
        ),
    )
    if not settings.hardware_joint_map_revision:
        raise ValueError("hardware.joint_map_revision is required for the H2 real-robot contract")
    if settings.max_control_gap_s >= 0.10:
        raise ValueError("safety.max_control_gap_s must stay below the 0.10s bridge watchdog")
    if settings.max_initial_root_xy_error_m > settings.max_mocap_root_xy_m:
        raise ValueError("initial root gate cannot exceed the runtime root-domain gate")
    if settings.ready_arm_position_tolerance_rad < settings.ready_position_tolerance_rad:
        raise ValueError("ready arm tolerance cannot be smaller than the common ready tolerance")
    if not settings.policy_path.is_file():
        raise FileNotFoundError(f"H2 policy not found: {settings.policy_path}")
    return settings


__all__ = [
    "DeploySettings",
    "PROJECT_ROOT",
    "load_settings",
]