Files
Mujoco_WASM/training_server/mobile_manipulator/kernel.py
T
chenlin f3a8a38acd
web-platform-ci / Standalone decision service (no cloud credentials) (push) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (push) Has been cancelled
web-platform-ci / Playwright E2E (push) Has been cancelled
lekiwi-compatibility / cpu-compatibility (push) Has been cancelled
web-platform-ci / Standalone decision service (no cloud credentials) (pull_request) Has been cancelled
web-platform-ci / TypeScript, lint, unit, build (pull_request) Has been cancelled
web-platform-ci / Playwright E2E (pull_request) Has been cancelled
lekiwi-compatibility / cpu-compatibility (pull_request) Has been cancelled
feat: release v1.0.1 CADWorld 网站与 LeKiwi 智能抓放
集成同源 BYOK 会话隔离、精简模型设置、官方订阅入口和 HTTPS 发布运维;保留本地训练/调参与控制能力。同步 npm 版本及 CHANGELOG,记录公网真实 API 验收仍待用户凭据。
2026-09-24 09:57:41 +08:00

310 lines
12 KiB
Python

"""Math mirror of web_platform/src/mobile/TaskKernel.ts (SI, world, wxyz)."""
import json
import math
from pathlib import Path
import numpy as np
CONTRACTS = Path(__file__).resolve().parents[2] / "contracts"
TASK = json.loads((CONTRACTS / "mobile-manipulator-v2.json").read_text())
ROBOTS = json.loads((CONTRACTS / "mobile-robots-v1.json").read_text())
def clip(value, lo=-1.0, hi=1.0):
return max(lo, min(hi, value))
def rotate(q, v):
w, x, y, z = q
vx, vy, vz = v
tx, ty, tz = 2 * (y * vz - z * vy), 2 * (z * vx - x * vz), 2 * (x * vy - y * vx)
return [
vx + w * tx + y * tz - z * ty,
vy + w * ty + z * tx - x * tz,
vz + w * tz + x * ty - y * tx,
]
def canonical(q):
n = math.hypot(*q)
return np.asarray(q) * ((-1 if q[0] < 0 else 1) / n) if n > 1e-12 else np.array([1, 0, 0, 0])
def relative(parent, child):
w, x, y, z = parent[3:] * np.array([1, -1, -1, -1])
a, b, c, d = child[3:]
q = [
w * a - x * b - y * c - z * d,
w * b + x * a + y * d - z * c,
w * c - x * d + y * a + z * b,
w * d + x * c - y * b + z * a,
]
pos = np.clip(
np.array(rotate([w, x, y, z], child[:3] - parent[:3])) / TASK["positionScale"], -1, 1
)
return np.concatenate((pos, canonical(q)))
def validate_config(c):
primary = [
g for g in c["gripperActuators"] if g.get("joint", c["gripperJoint"]) == c["gripperJoint"]
]
if not primary or any(
g["closed"] != c["gripperClosed"] or g["open"] != c["gripperOpen"] for g in primary
):
raise ValueError("gripper observation/actuator stroke mismatch")
joints = [
*c["baseJoints"],
*(j["name"] for j in c["armJoints"]),
*set(
[c["gripperJoint"], *(g.get("joint", c["gripperJoint"]) for g in c["gripperActuators"])]
),
]
actuators = [
*c["baseActuators"],
*c["armActuators"],
*(g["name"] for g in c["gripperActuators"]),
]
if (
not c["id"]
or c["recipe"] not in ("lekiwi-v1", "lekiwi-bundle", "mjcf")
or not 0 < len(c["armJoints"]) <= TASK["maxArmJoints"]
or len(c["armJoints"]) != len(c["armActuators"])
or not len(c["baseJoints"]) == len(c["baseActuators"]) == len(c["baseMix"])
or not c["baseJoints"]
or not c["gripperActuators"]
or len(set(joints)) != len(joints)
or len(set(actuators)) != len(actuators)
):
raise ValueError("invalid RobotConfig topology")
if (
np.asarray(c["baseMix"]).shape != (len(c["baseJoints"]), 3)
or not np.isfinite(c["baseMix"]).all()
):
raise ValueError("invalid baseMix")
if (
len(c["baseLimits"]) != 3
or not np.isfinite(c["baseLimits"]).all()
or min(c["baseLimits"]) <= 0
or not math.isfinite(c["wheelLimit"])
or c["wheelLimit"] <= 0
or len(c["eefOffset"]) != 3
or not np.isfinite(c["eefOffset"]).all()
):
raise ValueError("invalid scales")
for j in c["armJoints"]:
if (
not np.isfinite([j["min"], j["max"], j["neutral"], j["velocityLimit"]]).all()
or not j["min"] <= j["neutral"] <= j["max"]
or j["min"] >= j["max"]
or j["velocityLimit"] <= 0
or j["mode"] not in ("position", "velocity")
):
raise ValueError("invalid arm limits")
for closed, opened in [
(c["gripperClosed"], c["gripperOpen"]),
*((g["closed"], g["open"]) for g in c["gripperActuators"]),
]:
if not np.isfinite([closed, opened]).all() or closed == opened:
raise ValueError("invalid gripper stroke")
def decode_action(config, action, output=None):
"""Legacy v1 fixture oracle only; v2 environments use SafeActionController."""
action = np.asarray(action, dtype=np.float32)
if action.shape != (TASK["actionSize"],) or not np.isfinite(action).all():
raise ValueError("action must be finite [12]")
nbase, narm = len(config["baseJoints"]), len(config["armJoints"])
if output is None:
output = np.zeros(nbase + narm + len(config["gripperActuators"]), dtype=np.float64)
largest = config["wheelLimit"]
for i, row in enumerate(config["baseMix"]):
output[i] = sum(row[j] * clip(float(action[j])) * config["baseLimits"][j] for j in range(3))
largest = max(largest, abs(output[i]))
output[:nbase] *= config["wheelLimit"] / largest
for i, j in enumerate(config["armJoints"]):
a = clip(float(action[3 + i]))
output[nbase + i] = (
j["min"] + (a + 1) * 0.5 * (j["max"] - j["min"])
if j["mode"] == "position"
else a * j["velocityLimit"]
)
opening = (clip(float(action[11])) + 1) * 0.5
for i, g in enumerate(config["gripperActuators"]):
output[nbase + narm + i] = g["closed"] + opening * (g["open"] - g["closed"])
return output
STAGES = ("navigate", "reach", "pick-place")
def navigation_error(s):
distance = math.hypot(
s[37] - TASK["navigationOffset"][0] - s[0], s[38] - TASK["navigationOffset"][1] - s[1]
)
w, x, y, z = s[3:7]
yaw = math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z))
return distance, yaw
class TaskKernel:
def __init__(self, config, stage="pick-place"):
validate_config(config)
if stage not in STAGES:
raise ValueError("invalid training stage")
self.stage = stage
self.last_action = np.zeros(TASK["actionSize"], dtype=np.float32)
self.targets = np.zeros(TASK["actionSize"], dtype=np.float32)
self.config = config
self.observation = np.zeros(TASK["observationSize"], dtype=np.float32)
self.reset()
def reset(self):
self.steps = self.settle = 0
self.has_lifted = self.terminated = self.truncated = False
self.reward = self.action_rate = 0.0
self.previous_distance = None
self.last_action.fill(0)
self.targets.fill(0)
self.info = {
"reward_components": dict.fromkeys(
[
"reach",
"lift",
"transport",
"success",
"navigation",
"action_rate",
"joint_velocity",
"safety",
],
0.0,
),
"is_success": False,
"stage": "navigate" if self.stage == "navigate" else "reach",
"safety_stop": "",
"navigation_distance": 0.0,
"max_joint_velocity": 0.0,
}
def record_action(self, applied, targets):
self.action_rate = sum(
(float(applied[i]) - float(self.last_action[i])) ** 2 for i in range(TASK["actionSize"])
)
self.last_action[:] = applied
self.targets[:] = targets
def observe(self, state):
s, o, t = state, self.observation, TASK
o.fill(0)
o[:3] = np.clip(s[:3] / t["positionScale"], -1, 1)
o[3:7] = canonical(s[3:7])
o[7:10] = np.clip(s[7:10] / t["linearVelocityScale"], -1, 1)
o[10:13] = np.clip(s[10:13] / t["angularVelocityScale"], -1, 1)
for i, j in enumerate(self.config["armJoints"]):
o[13 + i] = clip(2 * (s[13 + i] - j["min"]) / (j["max"] - j["min"]) - 1)
o[21 + i] = clip(s[21 + i] / t["jointVelocityScale"])
o[29 + i] = 1
o[37] = clip(2 * s[29] - 1)
o[38:41] = np.clip(s[30:33] / t["positionScale"], -1, 1)
o[41:45] = canonical(s[33:37])
o[45:52] = relative(s[30:37], s[37:44])
o[52:59] = relative(s[47:54], s[37:44])
o[59:62] = np.clip(s[47:50] / t["positionScale"], -1, 1)
o[62:66] = canonical(s[50:54])
o[66], o[67] = self.has_lifted, self.settle / t["settleSteps"]
o[68:80] = self.last_action
o[80:92] = self.targets
return o
def evaluate(self, s, safety_stop="", peak_velocity=0.0):
if self.terminated or self.truncated:
raise RuntimeError("episode ended; reset required")
t = TASK
reach = math.hypot(*(s[37:40] - s[30:33]))
goal = math.hypot(*(s[37:40] - s[47:50]))
lift = clip((s[39] - t["objectStart"][2]) / t["liftHeight"], 0, 1)
if self.stage == "pick-place" and lift >= 1 and reach < t["graspDistance"] and s[29] < 0.4:
self.has_lifted = True
distance, yaw = navigation_error(s)
near = distance < t["navigationTolerance"] and abs(yaw) < t["navigationYawTolerance"]
stopped = (
math.hypot(*s[7:10]) < t["navigationSpeedTolerance"] and math.hypot(*s[10:13]) < 0.1
)
settled = (
self.has_lifted
and goal < t["goalTolerance"]
and s[29] > t["releaseOpening"]
and reach > t["graspDistance"]
and math.hypot(*s[44:47]) < t["settleSpeed"]
)
if self.stage == "navigate":
settled = near and stopped
elif self.stage == "reach":
settled = (
near and stopped and reach < t["graspDistance"] and math.hypot(*s[21:29]) < 0.15
)
self.settle = min(t["settleSteps"], self.settle + 1) if settled else 0
success = self.settle == t["settleSteps"]
r = self.info["reward_components"]
r["reach"] = t["controlDt"] * t["reachWeight"] * math.exp(-t["reachGain"] * reach)
r["lift"] = t["controlDt"] * t["liftWeight"] * lift
r["transport"] = (
t["controlDt"] * t["transportWeight"] * math.exp(-t["transportGain"] * goal)
if self.has_lifted
else 0.0
)
if self.stage == "navigate":
r["reach"] = r["lift"] = r["transport"] = 0.0
elif self.stage == "reach":
r["lift"] = r["transport"] = 0.0
progress = 0 if self.previous_distance is None else self.previous_distance - distance
self.previous_distance = distance
r["navigation"] = (
(
t["navigationProgressWeight"] * progress
- t["controlDt"] * (distance + 0.1 * abs(yaw))
)
if not self.has_lifted
else 0.0
)
r["action_rate"] = -t["actionRateWeight"] * self.action_rate
r["joint_velocity"] = (
-t["controlDt"] * t["jointVelocityWeight"] * sum(float(v) ** 2 for v in s[21:29])
)
peak_velocity = max(peak_velocity, max(abs(s[21:29])))
if not safety_stop and peak_velocity > t["jointSpeedStop"]:
safety_stop = "joint_velocity"
if not safety_stop and (
1 - 2 * (s[4] ** 2 + s[5] ** 2) < 0.5 or max(abs(s[:2])) > t["positionScale"]
):
safety_stop = "base_pose"
if safety_stop:
success = False
r["success"] = t["successBonus"] if success else 0.0
r["safety"] = -t["safetyPenalty"] if safety_stop else 0.0
self.reward = sum(r.values())
self.terminated = success or bool(safety_stop)
self.info["safety_stop"] = safety_stop
self.info["navigation_distance"] = distance
self.info["max_joint_velocity"] = max(self.info["max_joint_velocity"], peak_velocity)
self.steps += 1
self.truncated = self.steps >= t["maxSteps"] and not success
self.info["is_success"] = success
self.info["stage"] = (
"safety-stop"
if safety_stop
else "success"
if success
else "navigate"
if self.stage == "navigate" or (not near and not self.has_lifted)
else "transport"
if self.has_lifted
else "lift"
if reach < t["graspDistance"]
else "reach"
)
self.observe(s)
return self.observation, self.reward, self.terminated, self.truncated, self.info