diff --git a/motrix_env_core/src/motrix_env_core/sim/__init__.py b/motrix_env_core/src/motrix_env_core/sim/__init__.py index 3db80e16..5743303c 100644 --- a/motrix_env_core/src/motrix_env_core/sim/__init__.py +++ b/motrix_env_core/src/motrix_env_core/sim/__init__.py @@ -58,8 +58,6 @@ BodyLinearVelocityWrite, BodyPositionWrite, BodyRotationWrite, - DofPositionWrite, - DofVelocityWrite, JointPositionWrite, JointVelocityWrite, ) @@ -89,9 +87,7 @@ "BodyMassQuery", "DofPositionLimitsQuery", "DofPositionQuery", - "DofPositionWrite", "DofVelocityQuery", - "DofVelocityWrite", "GeomFrictionQuery", "GeomLinearVelocityQuery", "GeomSpec", diff --git a/motrix_env_core/src/motrix_env_core/sim/write.py b/motrix_env_core/src/motrix_env_core/sim/write.py index 053eb1b7..defeb45b 100644 --- a/motrix_env_core/src/motrix_env_core/sim/write.py +++ b/motrix_env_core/src/motrix_env_core/sim/write.py @@ -31,22 +31,6 @@ def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: """Record this declaration on the compiler through its typed hook.""" -@dataclass(frozen=True) -class DofPositionWrite(SimWrite): - """Complete canonical DOF position write.""" - - def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: - compiler.compile_dof_position(name, self) - - -@dataclass(frozen=True) -class DofVelocityWrite(SimWrite): - """Complete canonical DOF velocity write.""" - - def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: - compiler.compile_dof_velocity(name, self) - - @dataclass(frozen=True) class BodyJointPositionWrite(SimWrite): """One body's articulated-joint position write.""" @@ -87,6 +71,29 @@ def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: compiler.compile_joint_velocity(name, self) +@dataclass(frozen=True) +class JointQuaternionWrite(SimWrite): + """Local orientation quaternions (xyzw) of declared ball joints: ``(N, J, 4)``. + + The backend normalizes each quaternion on write. + """ + + joints: tuple[str, ...] + + def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: + compiler.compile_joint_quaternion(name, self) + + +@dataclass(frozen=True) +class JointAngularVelocityWrite(SimWrite): + """Local angular velocities of declared ball joints: ``(N, J, 3)``.""" + + joints: tuple[str, ...] + + def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: + compiler.compile_joint_angular_velocity(name, self) + + @dataclass(frozen=True) class CtrlTargetsWrite(SimWrite): """Actuator ctrl targets in declared name order. @@ -143,13 +150,23 @@ def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: @dataclass(frozen=True) -class MocapPoseWrite(SimWrite): - """Mocap body poses in declared order: ``(N, B, 7)`` float32.""" +class KinematicBodyPositionWrite(SimWrite): + """Kinematic body world positions in declared order: ``(N, B, 3)`` float32.""" + + bodies: tuple[str, ...] + + def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: + compiler.compile_kinematic_body_position(name, self) + + +@dataclass(frozen=True) +class KinematicBodyRotationWrite(SimWrite): + """Kinematic body world quaternions in declared order: ``(N, B, 4)`` float32.""" bodies: tuple[str, ...] def compile_with(self, compiler: SimWriteCompiler, name: str) -> None: - compiler.compile_mocap_pose(name, self) + compiler.compile_kinematic_body_rotation(name, self) @dataclass(frozen=True) @@ -239,14 +256,6 @@ def _begin_compile(self) -> None: def _build_program(self, *, reset: bool, forward_kinematics: bool) -> WriteProgram: """Assemble the program from the ops recorded during dispatch.""" - @abc.abstractmethod - def compile_dof_position(self, name: str, write: DofPositionWrite) -> None: - """Record a complete canonical DOF position write.""" - - @abc.abstractmethod - def compile_dof_velocity(self, name: str, write: DofVelocityWrite) -> None: - """Record a complete canonical DOF velocity write.""" - @abc.abstractmethod def compile_body_joint_position(self, name: str, write: BodyJointPositionWrite) -> None: """Record one body's articulated DOF position write.""" @@ -263,6 +272,14 @@ def compile_joint_position(self, name: str, write: JointPositionWrite) -> None: def compile_joint_velocity(self, name: str, write: JointVelocityWrite) -> None: """Record named one-DOF joint velocity writes.""" + @abc.abstractmethod + def compile_joint_quaternion(self, name: str, write: JointQuaternionWrite) -> None: + """Record ball-joint local orientation writes.""" + + @abc.abstractmethod + def compile_joint_angular_velocity(self, name: str, write: JointAngularVelocityWrite) -> None: + """Record ball-joint local angular velocity writes.""" + @abc.abstractmethod def compile_ctrl_targets(self, name: str, write: CtrlTargetsWrite) -> None: """Record actuator control targets.""" @@ -284,8 +301,12 @@ def compile_body_angular_velocity(self, name: str, write: BodyAngularVelocityWri """Record floating-body world angular velocity writes.""" @abc.abstractmethod - def compile_mocap_pose(self, name: str, write: MocapPoseWrite) -> None: - """Record mocap-body pose writes.""" + def compile_kinematic_body_position(self, name: str, write: KinematicBodyPositionWrite) -> None: + """Record kinematic-body world position writes.""" + + @abc.abstractmethod + def compile_kinematic_body_rotation(self, name: str, write: KinematicBodyRotationWrite) -> None: + """Record kinematic-body world rotation writes.""" @abc.abstractmethod def compile_actuator_kp(self, name: str, write: ActuatorKpWrite) -> None: diff --git a/motrix_env_core/tests/test_direct_env_sim_backend.py b/motrix_env_core/tests/test_direct_env_sim_backend.py index c7d431da..dcf09c77 100644 --- a/motrix_env_core/tests/test_direct_env_sim_backend.py +++ b/motrix_env_core/tests/test_direct_env_sim_backend.py @@ -17,7 +17,6 @@ from motrix_env_core.sim import ( ActuatorCtrlQuery, DofPositionQuery, - DofPositionWrite, DofVelocityQuery, ModelQuery, PhysicsReadProgram, @@ -25,7 +24,7 @@ from motrix_env_core.sim.backend import SimBackend from motrix_env_core.sim.model import ActuatorSpec, ActuatorType, SimModel from motrix_env_core.sim.registry import register_sim_backend -from motrix_env_core.sim.write import CtrlTargetsWrite, DofVelocityWrite, WriteProgram +from motrix_env_core.sim.write import CtrlTargetsWrite, JointPositionWrite, JointVelocityWrite, WriteProgram def _core_model() -> SimModel: @@ -47,9 +46,9 @@ def __init__(self, backend: "_FakeBackend", writes, reset: bool) -> None: for name, write in writes.items(): if isinstance(write, CtrlTargetsWrite): self._buffers[name] = np.zeros((backend.num_envs, backend.num_actuators), dtype=np.float32) - elif isinstance(write, DofPositionWrite): + elif isinstance(write, JointPositionWrite): self._buffers[name] = np.zeros_like(backend.dof_pos) - elif isinstance(write, DofVelocityWrite): + elif isinstance(write, JointVelocityWrite): self._buffers[name] = np.zeros_like(backend.dof_vel) def buffer(self, name: str) -> np.ndarray: @@ -189,7 +188,11 @@ def __init__(self, cfg: _FakeDirectCfg, num_envs: int, backend: str | None = Non ) self._ctrl_writes = self.sim.compile_writes({"ctrl": CtrlTargetsWrite()}) self._reset_program = self.sim.compile_writes( - {"state_position": DofPositionWrite(), "state_velocity": DofVelocityWrite()}, reset=True + { + "state_position": JointPositionWrite(("j0", "j1")), + "state_velocity": JointVelocityWrite(("j0", "j1")), + }, + reset=True, ) self._action_space = gym.spaces.Box(-1.0, 1.0, (2,), dtype=np.float32) self._observation_space = gym.spaces.Box(-np.inf, np.inf, (6,), dtype=np.float32) diff --git a/motrix_env_core/tests/test_manager_sim_backend.py b/motrix_env_core/tests/test_manager_sim_backend.py index 5dff96a0..b5bcc2e4 100644 --- a/motrix_env_core/tests/test_manager_sim_backend.py +++ b/motrix_env_core/tests/test_manager_sim_backend.py @@ -38,7 +38,7 @@ from motrix_env_core.sim.backend import SimBackend from motrix_env_core.sim.model import ActuatorSpec, ActuatorType, SimModel from motrix_env_core.sim.registry import register_sim_backend -from motrix_env_core.sim.write import CtrlTargetsWrite, DofPositionWrite, DofVelocityWrite, WriteProgram +from motrix_env_core.sim.write import CtrlTargetsWrite, WriteProgram _ACTUATORS = ( ActuatorSpec( @@ -82,10 +82,6 @@ def __init__(self, backend: "_FakeBackend", writes, reset: bool) -> None: ) self._routes[name] = route self._buffers[name] = np.zeros((backend.num_envs, len(route)), dtype=np.float32) - elif isinstance(write, DofPositionWrite): - self._buffers[name] = np.zeros_like(backend.dof_pos) - elif isinstance(write, DofVelocityWrite): - self._buffers[name] = np.zeros_like(backend.dof_vel) def buffer(self, name: str) -> np.ndarray: return self._buffers[name] diff --git a/motrix_env_core/tests/test_numba_manager.py b/motrix_env_core/tests/test_numba_manager.py index 4df2b0d5..b18d5607 100644 --- a/motrix_env_core/tests/test_numba_manager.py +++ b/motrix_env_core/tests/test_numba_manager.py @@ -50,7 +50,7 @@ from motrix_env_core.numba.manager.observations import create_observation_groups from motrix_env_core.numba.manager.rewards import create_reward_terms from motrix_env_core.numba.manager.terminations import TerminationManager -from motrix_env_core.sim import DofPositionWrite +from motrix_env_core.sim.write import CtrlTargetsWrite @kernel_data @@ -352,14 +352,13 @@ def physics_step(self) -> None: @dispatch def _recording_reset(ctx: ManagerContext, sim_writes: Map[np.ndarray]) -> None: - dof_pos = sim_writes["dof_pos"] - dof_pos[:] = 0.0 + sim_writes["ctrl"][:] = 0.0 @dispatch def _noop_reset(ctx: ManagerContext, sim_writes: Map[np.ndarray]) -> None: _ = ctx - sim_writes["dof_pos"][:] = 0.0 + sim_writes["ctrl"][:] = 0.0 @configclass(kw_only=True) @@ -368,14 +367,14 @@ class _DescriptorResetTermCfg(ResetTermCfg): def __call__(self, env: ManagerEnv) -> ResetTerm: del env - return ResetTerm(_noop_reset, writes={"dof_pos": DofPositionWrite()}) + return ResetTerm(_noop_reset, writes={"ctrl": CtrlTargetsWrite()}) @configclass class _RecordingResetTermCfg(ResetTermCfg): def __call__(self, env: ManagerEnv) -> ResetTerm: del env - return ResetTerm(_recording_reset, writes={"dof_pos": DofPositionWrite()}) + return ResetTerm(_recording_reset, writes={"ctrl": CtrlTargetsWrite()}) @configclass diff --git a/motrix_env_core/tests/test_sim_write_dispatch.py b/motrix_env_core/tests/test_sim_write_dispatch.py index 74ddb0db..969afa79 100644 --- a/motrix_env_core/tests/test_sim_write_dispatch.py +++ b/motrix_env_core/tests/test_sim_write_dispatch.py @@ -17,12 +17,13 @@ BodyPositionWrite, BodyRotationWrite, CtrlTargetsWrite, - DofPositionWrite, - DofVelocityWrite, GeomFrictionWrite, + JointAngularVelocityWrite, JointPositionWrite, + JointQuaternionWrite, JointVelocityWrite, - MocapPoseWrite, + KinematicBodyPositionWrite, + KinematicBodyRotationWrite, SimWriteCompiler, WriteProgram, ) @@ -49,14 +50,6 @@ def _build_program(self, *, reset: bool, forward_kinematics: bool) -> WriteProgr del reset, forward_kinematics return _RecordingProgram() - def compile_dof_position(self, name, write) -> None: - del name, write - self.dispatched.append("dof_position") - - def compile_dof_velocity(self, name, write) -> None: - del name, write - self.dispatched.append("dof_velocity") - def compile_body_joint_position(self, name, write) -> None: del name, write self.dispatched.append("body_dof_position") @@ -73,6 +66,14 @@ def compile_joint_velocity(self, name, write) -> None: del name, write self.dispatched.append("joint_velocity") + def compile_joint_quaternion(self, name, write) -> None: + del name, write + self.dispatched.append("joint_quaternion") + + def compile_joint_angular_velocity(self, name, write) -> None: + del name, write + self.dispatched.append("joint_angular_velocity") + def compile_ctrl_targets(self, name, write) -> None: del name, write self.dispatched.append("ctrl") @@ -93,9 +94,13 @@ def compile_body_angular_velocity(self, name, write) -> None: del name, write self.dispatched.append("body_angular_velocity") - def compile_mocap_pose(self, name, write) -> None: + def compile_kinematic_body_position(self, name, write) -> None: + del name, write + self.dispatched.append("mocap_position") + + def compile_kinematic_body_rotation(self, name, write) -> None: del name, write - self.dispatched.append("mocap") + self.dispatched.append("mocap_rotation") def compile_actuator_kp(self, name, write) -> None: del name, write @@ -121,18 +126,19 @@ def compile_geom_friction(self, name, write) -> None: def test_sim_write_compiler_dispatches_each_write_to_its_typed_compiler() -> None: compiler = _DispatchCompiler() - DofPositionWrite().compile_with(compiler, "write") - DofVelocityWrite().compile_with(compiler, "write") BodyJointPositionWrite("body").compile_with(compiler, "write") BodyJointVelocityWrite("body").compile_with(compiler, "write") JointPositionWrite(("joint",)).compile_with(compiler, "write") JointVelocityWrite(("joint",)).compile_with(compiler, "write") + JointQuaternionWrite(("joint",)).compile_with(compiler, "write") + JointAngularVelocityWrite(("joint",)).compile_with(compiler, "write") CtrlTargetsWrite().compile_with(compiler, "write") BodyPositionWrite(("body",)).compile_with(compiler, "write") BodyRotationWrite(("body",)).compile_with(compiler, "write") BodyLinearVelocityWrite(("body",)).compile_with(compiler, "write") BodyAngularVelocityWrite(("body",)).compile_with(compiler, "write") - MocapPoseWrite(("body",)).compile_with(compiler, "write") + KinematicBodyPositionWrite(("body",)).compile_with(compiler, "write") + KinematicBodyRotationWrite(("body",)).compile_with(compiler, "write") ActuatorKpWrite(("actuator",)).compile_with(compiler, "write") ActuatorDampingWrite(("actuator",)).compile_with(compiler, "write") BodyMassWrite(("link",)).compile_with(compiler, "write") @@ -140,18 +146,19 @@ def test_sim_write_compiler_dispatches_each_write_to_its_typed_compiler() -> Non GeomFrictionWrite(("geom",)).compile_with(compiler, "write") assert compiler.dispatched == [ - "dof_position", - "dof_velocity", "body_dof_position", "body_dof_velocity", "joint_position", "joint_velocity", + "joint_quaternion", + "joint_angular_velocity", "ctrl", "body_position", "body_rotation", "body_linear_velocity", "body_angular_velocity", - "mocap", + "mocap_position", + "mocap_rotation", "kp", "damping", "mass", @@ -164,10 +171,10 @@ def test_compile_dispatches_every_write_in_order_and_builds_one_program() -> Non compiler = _DispatchCompiler() program = compiler.compile( - {"a": DofPositionWrite(), "b": CtrlTargetsWrite(), "c": MocapPoseWrite(("body",))}, + {"a": JointQuaternionWrite(("joint",)), "b": CtrlTargetsWrite(), "c": KinematicBodyRotationWrite(("body",))}, reset=True, forward_kinematics=False, ) assert isinstance(program, WriteProgram) - assert compiler.dispatched == ["dof_position", "ctrl", "mocap"] + assert compiler.dispatched == ["joint_quaternion", "ctrl", "mocap_rotation"] diff --git a/motrix_env_motrixsim/pyproject.toml b/motrix_env_motrixsim/pyproject.toml index 427106c0..51b41b5a 100644 --- a/motrix_env_motrixsim/pyproject.toml +++ b/motrix_env_motrixsim/pyproject.toml @@ -12,7 +12,7 @@ readme = "README.md" license = "Apache-2.0" dependencies = [ "motrix-env-core", - "motrixsim==0.10.1.dev123478", + "motrixsim==0.10.1", "numpy>=1.26", ] diff --git a/motrix_env_motrixsim/src/motrix_env_motrixsim/runtime.py b/motrix_env_motrixsim/src/motrix_env_motrixsim/runtime.py index 3e26138a..e390d01e 100644 --- a/motrix_env_motrixsim/src/motrix_env_motrixsim/runtime.py +++ b/motrix_env_motrixsim/src/motrix_env_motrixsim/runtime.py @@ -314,7 +314,7 @@ def __init__(self, scene: SceneCfg, sim: SimCfg, num_envs: int) -> None: self._data: mtx.SceneData = mtx.SceneData(self._model, batch=[num_envs]) self._num_envs = num_envs self._model_compiler = MotrixSimModelCompiler(self._model) - self._write_compiler = MotrixSimWriteCompiler(self._model, self._data, self._masked_rows) + self._write_compiler = MotrixSimWriteCompiler(self._model, self._data) @property def model_compiler(self) -> SimModelCompiler: diff --git a/motrix_env_motrixsim/src/motrix_env_motrixsim/write_compiler.py b/motrix_env_motrixsim/src/motrix_env_motrixsim/write_compiler.py index 603598e9..a97aaa4b 100644 --- a/motrix_env_motrixsim/src/motrix_env_motrixsim/write_compiler.py +++ b/motrix_env_motrixsim/src/motrix_env_motrixsim/write_compiler.py @@ -3,12 +3,11 @@ """MotrixSim compiler and executable program for declarative sim writes.""" -from collections.abc import Callable from dataclasses import dataclass -from typing import Protocol import motrixsim as mtx import numpy as np +from motrixsim import write as mtx_write from motrix_env_core.sim.write import ( ActuatorDampingWrite, @@ -22,116 +21,142 @@ BodyPositionWrite, BodyRotationWrite, CtrlTargetsWrite, - DofPositionWrite, - DofVelocityWrite, GeomFrictionWrite, + JointAngularVelocityWrite, JointPositionWrite, + JointQuaternionWrite, JointVelocityWrite, - MocapPoseWrite, + KinematicBodyPositionWrite, + KinematicBodyRotationWrite, SimWriteCompiler, WriteProgram, ) -class _WriteOp(Protocol): - def alloc(self, num_envs: int) -> np.ndarray: ... - - def __call__(self, buffers, idx: np.ndarray | slice, rows: mtx.SceneData) -> None: ... - - -class _ResetPatchOp(Protocol): - def apply(self, dof_pos, dof_vel, buffers, env_ids) -> None: ... - - @dataclass(frozen=True) class _CompiledWrite: - op: _WriteOp - reset_op: _ResetPatchOp | None = None pos_indices: np.ndarray | None = None vel_indices: np.ndarray | None = None refresh_kinematics: bool = False + native: mtx_write.WriteSource | None = None + ctrl_indices: np.ndarray | None = None class MotrixSimWriteCompiler(SimWriteCompiler): - """Compile neutral write declarations against one MotrixSim model and data batch.""" + """Compile neutral write declarations against one MotrixSim model and data batch. - def __init__( - self, - model: mtx.SceneModel, - data: mtx.SceneData, - masked_rows: Callable[[np.ndarray], mtx.SceneData], - ) -> None: + Every write compiles into one native write plan; reset and + forward-kinematics behavior are baked into the plan at compile time. + """ + + def __init__(self, model: mtx.SceneModel, data: mtx.SceneData) -> None: self._model = model self._data = data - self._masked_rows = masked_rows self._pending: list[tuple[str, _CompiledWrite]] = [] def _begin_compile(self) -> None: self._pending = [] def _build_program(self, *, reset: bool, forward_kinematics: bool) -> WriteProgram: - buffers: dict[str, np.ndarray] = {} - ops: list[tuple[_CompiledWrite, np.ndarray]] = [] + native_fields: dict[str, mtx_write.WriteSource] = {} ctrl_owners: dict[int, str] = {} claimed_pos: dict[int, str] = {} claimed_vel: dict[int, str] = {} refresh_kinematics = False for name, compiled in self._pending: - if isinstance(compiled.op, _CtrlOp): - self._claim_ctrl_targets(name, compiled.op.indices, ctrl_owners) + if compiled.ctrl_indices is not None: + self._claim_ctrl_targets(name, compiled.ctrl_indices, ctrl_owners) if compiled.pos_indices is not None: self._claim(name, compiled.pos_indices, claimed_pos, "position") if compiled.vel_indices is not None: self._claim(name, compiled.vel_indices, claimed_vel, "velocity") - sub = compiled.op.alloc(self._data.shape[0]) - ops.append((compiled, sub)) - buffers[name] = sub + native_fields[name] = compiled.native refresh_kinematics |= compiled.refresh_kinematics - return _MotrixSimWriteProgram( - self._model, - self._data, - self._masked_rows, - buffers, - ops, - reset=reset, - refresh_kinematics=forward_kinematics and (reset or refresh_kinematics), - ) - - def compile_dof_position(self, name: str, write: DofPositionWrite) -> None: - del write - indices = np.arange(self._model.num_dof_pos, dtype=np.int64) - op = _DofChannelOp(indices) - self._pending.append((name, _CompiledWrite(op, op, pos_indices=indices, refresh_kinematics=True))) - - def compile_dof_velocity(self, name: str, write: DofVelocityWrite) -> None: - del write - indices = np.arange(self._model.num_dof_vel, dtype=np.int64) - op = _DofChannelOp(indices, velocity=True) - self._pending.append((name, _CompiledWrite(op, op, vel_indices=indices))) + buffers: dict[str, np.ndarray] = {} + native_program = None + if native_fields: + # Reset (restore defaults, then apply the writes) and the FK pass + # are baked into the native plan at compile time. + refresh = forward_kinematics and (reset or refresh_kinematics) + native_program = self._model.compile_write(native_fields, reset=reset, forward_kinematic=refresh).allocate( + self._data + ) + for name in native_fields: + buffers[name] = native_program[name] + return _MotrixSimWriteProgram(self._data, buffers, native_program) def compile_body_joint_position(self, name: str, write: BodyJointPositionWrite) -> None: - indices = np.asarray(_named_body(self._model, write.body).get_dof_pos_indices(False), dtype=np.int64) - op = _DofChannelOp(indices) - self._pending.append((name, _CompiledWrite(op, op, pos_indices=indices, refresh_kinematics=True))) + body = _named_body(self._model, write.body) + joints = list(body.joints) + indices = np.asarray(body.get_dof_pos_indices(False), dtype=np.int64) + self._require_single_dof_body(name, write.body, joints, "position") + self._pending.append( + ( + name, + _CompiledWrite( + native=mtx_write.BodyJointPosition([joint.name for joint in joints]), + pos_indices=indices, + refresh_kinematics=True, + ), + ) + ) def compile_body_joint_velocity(self, name: str, write: BodyJointVelocityWrite) -> None: - indices = np.asarray(_named_body(self._model, write.body).get_dof_vel_indices(False), dtype=np.int64) - op = _DofChannelOp(indices, velocity=True) - self._pending.append((name, _CompiledWrite(op, op, vel_indices=indices))) + body = _named_body(self._model, write.body) + joints = list(body.joints) + indices = np.asarray(body.get_dof_vel_indices(False), dtype=np.int64) + self._require_single_dof_body(name, write.body, joints, "velocity") + self._pending.append( + ( + name, + _CompiledWrite( + native=mtx_write.BodyJointVelocity([joint.name for joint in joints]), + vel_indices=indices, + ), + ) + ) def compile_joint_position(self, name: str, write: JointPositionWrite) -> None: - indices = np.asarray([joint.dof_pos_index for joint in self._joints(name, write.joints)], dtype=np.int64) - op = _DofChannelOp(indices) - self._pending.append((name, _CompiledWrite(op, op, pos_indices=indices, refresh_kinematics=True))) + joints = self._joints(name, write.joints) + indices = np.asarray([joint.dof_pos_index for joint in joints], dtype=np.int64) + self._pending.append( + ( + name, + _CompiledWrite( + native=mtx_write.BodyJointPosition([joint.name for joint in joints]), + pos_indices=indices, + refresh_kinematics=True, + ), + ) + ) def compile_joint_velocity(self, name: str, write: JointVelocityWrite) -> None: - indices = np.asarray([joint.dof_vel_index for joint in self._joints(name, write.joints)], dtype=np.int64) - op = _DofChannelOp(indices, velocity=True) - self._pending.append((name, _CompiledWrite(op, op, vel_indices=indices))) + joints = self._joints(name, write.joints) + indices = np.asarray([joint.dof_vel_index for joint in joints], dtype=np.int64) + self._pending.append( + ( + name, + _CompiledWrite( + native=mtx_write.BodyJointVelocity([joint.name for joint in joints]), + vel_indices=indices, + ), + ) + ) + + def compile_joint_quaternion(self, name: str, write: JointQuaternionWrite) -> None: + self._ball_joints(name, write.joints) + self._pending.append( + (name, _CompiledWrite(native=mtx_write.JointQuaternion(list(write.joints)), refresh_kinematics=True)) + ) + + def compile_joint_angular_velocity(self, name: str, write: JointAngularVelocityWrite) -> None: + self._ball_joints(name, write.joints) + self._pending.append((name, _CompiledWrite(native=mtx_write.JointAngularVelocity(list(write.joints))))) def compile_ctrl_targets(self, name: str, write: CtrlTargetsWrite) -> None: if write.actuators is None: indices = np.arange(self._model.num_actuators, dtype=np.int64) + selection = None else: if not write.actuators: raise ValueError(f"CtrlTargetsWrite {name!r} actuator names must not be empty.") @@ -140,74 +165,94 @@ def compile_ctrl_targets(self, name: str, write: CtrlTargetsWrite) -> None: indices = np.asarray( [_named_actuator(self._model, actuator).index for actuator in write.actuators], dtype=np.int64 ) - self._pending.append((name, _CompiledWrite(_CtrlOp(indices)))) + selection = list(write.actuators) + self._pending.append((name, _CompiledWrite(native=mtx_write.ActuatorCtrls(selection), ctrl_indices=indices))) def compile_body_position(self, name: str, write: BodyPositionWrite) -> None: bases = self._floating_bases(name, write.bodies, type(write).__name__) indices = np.asarray([base.dof_pos_indices[:3] for base in bases], dtype=np.int64) - op = _MultiTargetOp(bases, "set_translation", 3) self._pending.append( ( name, - _CompiledWrite(op, _DofComponentPatchOp(indices), pos_indices=indices.ravel(), refresh_kinematics=True), + _CompiledWrite( + pos_indices=indices.ravel(), + refresh_kinematics=True, + native=mtx_write.BodyPosition(list(write.bodies)), + ), ) ) def compile_body_rotation(self, name: str, write: BodyRotationWrite) -> None: bases = self._floating_bases(name, write.bodies, type(write).__name__) indices = np.asarray([base.dof_pos_indices[3:] for base in bases], dtype=np.int64) - op = _MultiTargetOp(bases, "set_rotation", 4, contiguous=True) self._pending.append( ( name, - _CompiledWrite(op, _DofComponentPatchOp(indices), pos_indices=indices.ravel(), refresh_kinematics=True), + _CompiledWrite( + pos_indices=indices.ravel(), + refresh_kinematics=True, + native=mtx_write.BodyRotation(list(write.bodies)), + ), ) ) def compile_body_linear_velocity(self, name: str, write: BodyLinearVelocityWrite) -> None: bases = self._floating_bases(name, write.bodies, type(write).__name__) indices = np.asarray([base.dof_vel_indices[:3] for base in bases], dtype=np.int64) - op = _MultiTargetOp(bases, "set_global_linear_velocity", 3) self._pending.append( - (name, _CompiledWrite(op, _DofComponentPatchOp(indices, velocity=True), vel_indices=indices.ravel())) + ( + name, + _CompiledWrite( + vel_indices=indices.ravel(), + native=mtx_write.BodyLinearVelocity(list(write.bodies)), + ), + ) ) def compile_body_angular_velocity(self, name: str, write: BodyAngularVelocityWrite) -> None: bases = self._floating_bases(name, write.bodies, type(write).__name__) indices = np.asarray([base.dof_vel_indices[3:] for base in bases], dtype=np.int64) - op = _MultiTargetOp(bases, "set_global_angular_velocity", 3) self._pending.append( - (name, _CompiledWrite(op, _DofComponentPatchOp(indices, velocity=True), vel_indices=indices.ravel())) + ( + name, + _CompiledWrite( + vel_indices=indices.ravel(), + native=mtx_write.BodyAngularVelocity(list(write.bodies)), + ), + ) + ) + + def compile_kinematic_body_position(self, name: str, write: KinematicBodyPositionWrite) -> None: + self._mocaps(name, write.bodies) + self._pending.append( + (name, _CompiledWrite(native=mtx_write.BodyPosition(list(write.bodies)), refresh_kinematics=True)) ) - def compile_mocap_pose(self, name: str, write: MocapPoseWrite) -> None: - bodies = self._targets(name, write.bodies, "body", _named_body) - mocaps = [] - for body_name, body in zip(write.bodies, bodies): - if body.mocap is None: - raise ValueError(f"MocapPoseWrite body {body_name!r} is not a mocap body.") - mocaps.append(body.mocap) - self._pending.append((name, _CompiledWrite(_MultiTargetOp(mocaps, "set_pose", 7), refresh_kinematics=True))) + def compile_kinematic_body_rotation(self, name: str, write: KinematicBodyRotationWrite) -> None: + self._mocaps(name, write.bodies) + self._pending.append( + (name, _CompiledWrite(native=mtx_write.BodyRotation(list(write.bodies)), refresh_kinematics=True)) + ) def compile_actuator_kp(self, name: str, write: ActuatorKpWrite) -> None: - targets = self._targets(name, write.actuators, "actuator", _named_actuator) - self._pending.append((name, _CompiledWrite(_MultiTargetOp(targets, "set_kp_override", 1)))) + self._targets(name, write.actuators, "actuator", _named_actuator) + self._pending.append((name, _CompiledWrite(native=mtx_write.ActuatorKpOverride(list(write.actuators))))) def compile_actuator_damping(self, name: str, write: ActuatorDampingWrite) -> None: - targets = self._targets(name, write.actuators, "actuator", _named_actuator) - self._pending.append((name, _CompiledWrite(_MultiTargetOp(targets, "set_damping_override", 1)))) + self._targets(name, write.actuators, "actuator", _named_actuator) + self._pending.append((name, _CompiledWrite(native=mtx_write.ActuatorDampingOverride(list(write.actuators))))) def compile_body_mass(self, name: str, write: BodyMassWrite) -> None: - targets = self._targets(name, write.links, "link", _named_link) - self._pending.append((name, _CompiledWrite(_MultiTargetOp(targets, "set_mass_override", 1)))) + self._targets(name, write.links, "link", _named_link) + self._pending.append((name, _CompiledWrite(native=mtx_write.LinkMassOverride(list(write.links))))) def compile_body_com(self, name: str, write: BodyComWrite) -> None: - targets = self._targets(name, write.links, "link", _named_link) - self._pending.append((name, _CompiledWrite(_MultiTargetOp(targets, "set_center_of_mass_override", 3)))) + self._targets(name, write.links, "link", _named_link) + self._pending.append((name, _CompiledWrite(native=mtx_write.LinkCenterOfMassOverride(list(write.links))))) def compile_geom_friction(self, name: str, write: GeomFrictionWrite) -> None: - targets = self._targets(name, write.geoms, "geom", _named_geom) - self._pending.append((name, _CompiledWrite(_MultiTargetOp(targets, "set_friction_override", 3)))) + self._targets(name, write.geoms, "geom", _named_geom) + self._pending.append((name, _CompiledWrite(native=mtx_write.GeomFrictionOverride(list(write.geoms))))) def _joints(self, name: str, joint_names: tuple[str, ...]): if not joint_names: @@ -224,6 +269,29 @@ def _joints(self, name: str, joint_names: tuple[str, ...]): joints.append(joint) return joints + def _ball_joints(self, name: str, joint_names: tuple[str, ...]) -> None: + if not joint_names: + raise ValueError(f"Simulator write {name!r} must declare at least one joint.") + if len(set(joint_names)) != len(joint_names): + raise ValueError(f"Simulator write {name!r} contains duplicate joint names.") + for joint_name in joint_names: + joint = self._model.get_joint(joint_name) + if joint is None: + raise KeyError(f"Unknown joint {joint_name!r} in write {name!r}.") + if joint.num_dof_pos != 4 or joint.num_dof_vel != 3: + raise ValueError(f"Simulator write joint {joint_name!r} in {name!r} must be a ball joint.") + + def _require_single_dof_body(self, name: str, body_name: str, joints, channel: str) -> None: + if joints and all(joint.num_dof_pos == 1 and joint.num_dof_vel == 1 for joint in joints): + return + multi = [joint.name for joint in joints if joint.num_dof_pos != 1 or joint.num_dof_vel != 1] + orientation = "JointQuaternionWrite" if channel == "position" else "JointAngularVelocityWrite" + raise ValueError( + f"Simulator write {name!r} body {body_name!r} has multi-DoF joints {multi}; declare per-quantity " + f"writes instead (JointPositionWrite/{orientation} for position, " + "JointVelocityWrite/JointAngularVelocityWrite for velocity)." + ) + def _floating_bases(self, name: str, body_names: tuple[str, ...], write_type: str): bodies = self._targets(name, body_names, "body", _named_body) bases = [] @@ -233,6 +301,11 @@ def _floating_bases(self, name: str, body_names: tuple[str, ...], write_type: st bases.append(body.floatingbase) return bases + def _mocaps(self, name: str, body_names: tuple[str, ...]) -> None: + for body_name in body_names: + if _named_body(self._model, body_name).mocap is None: + raise ValueError(f"Kinematic body write target {body_name!r} in {name!r} is not a mocap body.") + def _targets(self, name: str, names: tuple[str, ...], target_type: str, resolver): if not names: raise ValueError(f"Simulator write {name!r} must declare at least one {target_type}.") @@ -264,22 +337,13 @@ class _MotrixSimWriteProgram(WriteProgram): def __init__( self, - model: mtx.SceneModel, data: mtx.SceneData, - masked_rows: Callable[[np.ndarray], mtx.SceneData], buffers: dict[str, np.ndarray], - ops: list[tuple[_CompiledWrite, np.ndarray]], - *, - reset: bool, - refresh_kinematics: bool, + native_program: mtx_write.WriteProgram | None = None, ) -> None: - self._model = model self._data = data - self._masked_rows = masked_rows self._buffers = buffers - self._ops = ops - self._reset = reset - self._refresh_kinematics = refresh_kinematics + self._native_program = native_program def buffer(self, name: str) -> np.ndarray: return self._buffers[name] @@ -294,106 +358,8 @@ def execute(self, env_ids: np.ndarray | None = None) -> None: raise ValueError("Simulator write env_ids must not contain duplicates.") if env_ids.size == 0: return - selected_ids = np.arange(self._data.shape[0], dtype=np.int64) if env_ids is None else np.sort(env_ids) - rows = self._data if env_ids is None else self._masked_rows(selected_ids) - idx = slice(None) if env_ids is None else selected_ids - if self._reset: - self._execute_reset(rows, selected_ids, idx) - return - for compiled, sub_buffers in self._ops: - compiled.op(sub_buffers, idx, rows) - if self._refresh_kinematics: - self._model.forward_kinematic(rows) - - def _execute_reset(self, rows: mtx.SceneData, env_ids: np.ndarray, idx: np.ndarray | slice) -> None: - default_dof_pos = np.asarray(self._model.compute_init_dof_pos(), dtype=np.float32) - dof_pos = np.broadcast_to(default_dof_pos, (env_ids.size, self._model.num_dof_pos)).copy() - dof_vel = np.zeros((env_ids.size, self._model.num_dof_vel), dtype=np.float32) - post_reset_ops = [] - for compiled, buffers in self._ops: - if compiled.reset_op is None: - post_reset_ops.append((compiled.op, buffers)) - else: - compiled.reset_op.apply(dof_pos, dof_vel, buffers, env_ids) - kwargs = {"forward_kinematic": self._refresh_kinematics and not post_reset_ops} - if self._model.num_dof_pos: - kwargs["dof_pos"] = np.ascontiguousarray(dof_pos) - if self._model.num_dof_vel: - kwargs["dof_vel"] = np.ascontiguousarray(dof_vel) - rows.reset(self._model, **kwargs) - for op, buffers in post_reset_ops: - op(buffers, idx, rows) - if post_reset_ops and self._refresh_kinematics: - self._model.forward_kinematic(rows) - - -class _DofChannelOp: - def __init__(self, indices: np.ndarray, *, velocity: bool = False) -> None: - self._indices = indices - self._velocity = velocity - - def alloc(self, num_envs: int) -> np.ndarray: - return np.zeros((num_envs, self._indices.size), dtype=np.float32) - - def __call__(self, buffers, idx: np.ndarray | slice, rows: mtx.SceneData) -> None: - target = rows.dof_vel if self._velocity else rows.dof_pos - target[:, self._indices] = buffers[idx] - - def apply(self, dof_pos, dof_vel, buffers, env_ids) -> None: - target = dof_vel if self._velocity else dof_pos - target[:, self._indices] = buffers[env_ids] - - -class _DofComponentPatchOp: - def __init__(self, indices: np.ndarray, *, velocity: bool = False) -> None: - self._indices = indices - self._velocity = velocity - - def apply(self, dof_pos, dof_vel, buffers, env_ids) -> None: - target = dof_vel if self._velocity else dof_pos - values = buffers[env_ids] - for target_index, indices in enumerate(self._indices): - target[:, indices] = values[:, target_index] - - -class _CtrlOp: - """Ctrl targets routed to fixed native actuator columns.""" - - def __init__(self, indices: np.ndarray) -> None: - self.indices = indices - - def alloc(self, num_envs: int) -> np.ndarray: - return np.zeros((num_envs, self.indices.size), dtype=np.float32) - - def __call__(self, buffers, idx: np.ndarray | slice, rows: mtx.SceneData) -> None: - values = buffers[idx] - if not values.shape[1]: - return - if self.indices.size == rows.actuator_ctrls.shape[1]: - rows.actuator_ctrls = values - else: - rows.actuator_ctrls[:, self.indices] = values - - -class _MultiTargetOp: - """Apply one fixed-width property to targets in declared order.""" - - def __init__(self, targets, setter_name: str, width: int, *, contiguous: bool = False) -> None: - self._setters = [getattr(target, setter_name) for target in targets] - self._width = width - self._contiguous = contiguous - - def alloc(self, num_envs: int) -> np.ndarray: - shape = (num_envs, len(self._setters)) if self._width == 1 else (num_envs, len(self._setters), self._width) - return np.zeros(shape, dtype=np.float32) - - def __call__(self, buffers, idx: np.ndarray | slice, rows: mtx.SceneData) -> None: - values = buffers[idx] - for target_index, setter in enumerate(self._setters): - target_values = values[:, target_index] - if self._contiguous: - target_values = np.ascontiguousarray(target_values) - setter(rows, target_values) + if self._native_program is not None: + self._native_program.execute(self._data, env_ids=None if env_ids is None else np.sort(env_ids)) def _named_body(model: mtx.SceneModel, body_name: str): diff --git a/motrix_env_motrixsim/tests/test_motrixsim_backend.py b/motrix_env_motrixsim/tests/test_motrixsim_backend.py index 9de9e039..f7f085fc 100644 --- a/motrix_env_motrixsim/tests/test_motrixsim_backend.py +++ b/motrix_env_motrixsim/tests/test_motrixsim_backend.py @@ -10,6 +10,7 @@ from motrix_env_core.base import SimCfg from motrix_env_core.config.scene import SceneCfg, SceneCompiler from motrix_env_core.sim import ( + ActuatorCtrlQuery, ActuatorKdQuery, ActuatorKpQuery, BodyAngularVelocityWrite, @@ -22,7 +23,6 @@ BodyRotationWrite, DofPositionLimitsQuery, DofPositionQuery, - DofPositionWrite, DofVelocityQuery, GeomSpecsQuery, JointPositionQuery, @@ -35,7 +35,7 @@ ) from motrix_env_core.sim.model import SimModel from motrix_env_core.sim.registry import create_sim_backend, list_sim_backends -from motrix_env_core.sim.write import BodyJointVelocityWrite, DofVelocityWrite, JointVelocityWrite +from motrix_env_core.sim.write import BodyJointVelocityWrite, CtrlTargetsWrite, JointVelocityWrite from motrix_env_motrixsim.compiler import MotrixSimSceneCompiler from motrix_env_motrixsim.runtime import MotrixSimBackend @@ -100,9 +100,7 @@ def test_motrixsim_runtime_reset_restores_default_state(): assert model.actuators == () # A scene without DOF resets to backend defaults without any value channels. - backend.write_compiler.compile( - {"state_position": DofPositionWrite(), "state_velocity": DofVelocityWrite()}, reset=True - ).execute(np.asarray([0, 1], dtype=np.int64)) + backend.write_compiler.compile({"ctrl": CtrlTargetsWrite()}, reset=True).execute(np.asarray([0, 1], dtype=np.int64)) def test_body_joint_position_limits_follow_body_joint_dof_order(): @@ -288,18 +286,20 @@ def test_full_dof_reset_preserves_unsorted_environment_id_values(): cfg = registry.make_env_config("dm-finger-spin", mode="play") backend = MotrixSimBackend(cfg.scene, cfg.sim, 3) reset = backend.write_compiler.compile( - {"state_position": DofPositionWrite(), "state_velocity": DofVelocityWrite()}, reset=True + {"joint_position": JointPositionWrite(("hinge",)), "joint_velocity": JointVelocityWrite(("hinge",))}, + reset=True, ) read = backend.compile_reads({"position": DofPositionQuery()}) - position = reset.buffer("state_position") - position[2] = [0.2, 0.3, 0.4] - position[0] = [-0.2, -0.3, -0.4] + hinge_dof = backend._model.get_joint("hinge").dof_pos_index + position = reset.buffer("joint_position") + position[2] = [0.4] + position[0] = [-0.4] reset.execute(np.asarray([2, 0], dtype=np.int64)) read.execute() - np.testing.assert_allclose(read["position"][2], [0.2, 0.3, 0.4]) - np.testing.assert_allclose(read["position"][0], [-0.2, -0.3, -0.4]) + np.testing.assert_allclose(read["position"][2, hinge_dof], [0.4]) + np.testing.assert_allclose(read["position"][0, hinge_dof], [-0.4]) def test_body_dof_reset_matches_body_query_layout(): @@ -339,10 +339,8 @@ def test_reset_compilation_rejects_conflicting_targets(): with pytest.raises(ValueError, match="conflict"): backend.write_compiler.compile( { - "all_position": DofPositionWrite(), - "all_velocity": DofVelocityWrite(), "joint_position": JointPositionWrite(("hinge",)), - "joint_velocity": JointVelocityWrite(("hinge",)), + "joint_position_again": JointPositionWrite(("hinge",)), }, reset=True, ) @@ -400,9 +398,7 @@ def test_named_joint_queries_reject_unknown_joints(): def test_motrixsim_reset_program_validates_environment_ids(): backend = _make_backend(SceneCfg(), SimCfg(), num_envs=2) - program = backend.write_compiler.compile( - {"state_position": DofPositionWrite(), "state_velocity": DofVelocityWrite()}, reset=True - ) + program = backend.write_compiler.compile({"ctrl": CtrlTargetsWrite()}, reset=True) with pytest.raises(TypeError, match="int64 ndarray"): program.execute(np.asarray([0], dtype=np.int32)) @@ -422,3 +418,96 @@ def test_sim_cfg_preserves_unspecified_solver_options(): assert options.max_iterations == 7 assert options.solver_tolerance == pytest.approx(2e-4) + + +def test_ctrl_targets_write_routes_named_actuators_through_native_plan(): + import motrix_envs # noqa: F401 + from motrix_env_core import registry + + cfg = registry.make_env_config("go2-walk-flat", mode="play") + backend = MotrixSimBackend(cfg.scene, cfg.sim, 3) + actuator_names = tuple(actuator.name for actuator in backend._model.actuators) + reordered = (actuator_names[-1], actuator_names[0]) + ctrl = backend.write_compiler.compile({"ctrl": CtrlTargetsWrite(reordered)}) + + assert ctrl.buffer("ctrl").shape == (3, 2) + ctrl.buffer("ctrl")[:] = [[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]] + ctrl.execute(np.asarray([1], dtype=np.int64)) + + read = backend.compile_reads({"ctrl_values": ActuatorCtrlQuery()}) + read.execute() + # The declared order defines the buffer layout: column 0 goes to the last + # declared actuator, column 1 to the first. Full model order is read back. + expected = np.zeros(len(actuator_names), dtype=np.float32) + expected[[len(actuator_names) - 1, 0]] = [3.0, 4.0] + np.testing.assert_allclose(read["ctrl_values"][1], expected) + np.testing.assert_allclose(read["ctrl_values"][0], np.zeros(len(actuator_names), dtype=np.float32)) + + +def test_partial_env_ids_leave_other_rows_untouched_with_native_writes(): + import motrix_envs # noqa: F401 + from motrix_env_core import registry + + cfg = registry.make_env_config("dm-humanoid-walk", mode="play") + body = "torso" + backend = MotrixSimBackend(cfg.scene, cfg.sim, 2) + program = backend.write_compiler.compile( + { + "body_position": BodyPositionWrite((body,)), + "body_rotation": BodyRotationWrite((body,)), + } + ) + read = backend.compile_reads({"position": LinkPositionQuery(link=body), "rotation": LinkQuaternionQuery(link=body)}) + program.buffer("body_position")[1, 0] = [1.0, 2.0, 3.0] + program.buffer("body_rotation")[1, 0] = [0.0, 0.0, 0.0, 1.0] + + program.execute(np.asarray([1], dtype=np.int64)) + read.execute() + + np.testing.assert_allclose(read["position"][1], [1.0, 2.0, 3.0]) + np.testing.assert_allclose(read["rotation"][1], [0.0, 0.0, 0.0, 1.0]) + assert not np.allclose(read["position"][0], [1.0, 2.0, 3.0]) + + +def test_ctrl_targets_reject_duplicated_actuators_with_stable_message(): + import motrix_envs # noqa: F401 + from motrix_env_core import registry + + cfg = registry.make_env_config("go2-walk-flat", mode="play") + backend = MotrixSimBackend(cfg.scene, cfg.sim, 1) + name = backend._model.actuators[0].name + with pytest.raises(ValueError, match="both target actuator"): + backend.write_compiler.compile( + { + "first": CtrlTargetsWrite((name,)), + "second": CtrlTargetsWrite((name,)), + } + ) + + +def test_mixed_dof_and_native_writes_execute_in_one_program(): + import motrix_envs # noqa: F401 + from motrix_env_core import registry + + cfg = registry.make_env_config("dm-humanoid-walk", mode="play") + body = "torso" + backend = MotrixSimBackend(cfg.scene, cfg.sim, 2) + program = backend.write_compiler.compile( + { + "joint_position": BodyJointPositionWrite(body), + "base_position": BodyPositionWrite((body,)), + } + ) + read = backend.compile_reads( + {"joint_pos": BodyJointPositionQuery(body=body), "position": LinkPositionQuery(link=body)} + ) + program.buffer("joint_position")[0] = 0.0 + program.buffer("base_position")[0, 0] = [0.5, -0.5, 1.5] + + program.execute(np.asarray([0], dtype=np.int64)) + read.execute(np.asarray([0], dtype=np.int64)) + + # Legacy DOF-channel scatter and the native body write land in the same + # step, and forward kinematics still refreshes link poses once. + np.testing.assert_allclose(read["position"][0], [0.5, -0.5, 1.5]) + np.testing.assert_allclose(read["joint_pos"][0], program.buffer("joint_position")[0]) diff --git a/motrix_env_motrixsim/tests/test_multi_target_writes.py b/motrix_env_motrixsim/tests/test_multi_target_writes.py deleted file mode 100644 index 5f5cc128..00000000 --- a/motrix_env_motrixsim/tests/test_multi_target_writes.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright Motphys Technology Co., Ltd. 2025, 2026 -# SPDX-License-Identifier: Apache-2.0 - -"""Structured target-axis behavior of fixed-width MotrixSim write ops.""" - -import numpy as np - -from motrix_env_motrixsim.write_compiler import _MultiTargetOp - - -class _Target: - def __init__(self) -> None: - self.values = [] - - def set_scalar(self, rows, values) -> None: - self.values.append((rows, values.copy())) - - def set_vector(self, rows, values) -> None: - self.values.append((rows, values.copy())) - - -def test_multi_target_scalar_op_routes_declared_target_axis() -> None: - first = _Target() - second = _Target() - op = _MultiTargetOp((first, second), "set_scalar", 1) - buffers = op.alloc(3) - assert buffers.shape == (3, 2) - buffers[:] = [[1.0, 10.0], [2.0, 20.0], [3.0, 30.0]] - - op(buffers, np.asarray([2, 0], dtype=np.int64), "rows") - - np.testing.assert_array_equal(first.values[0][1], [3.0, 1.0]) - np.testing.assert_array_equal(second.values[0][1], [30.0, 10.0]) - - -def test_multi_target_vector_op_routes_declared_target_axis() -> None: - first = _Target() - second = _Target() - op = _MultiTargetOp((first, second), "set_vector", 3) - buffers = op.alloc(2) - assert buffers.shape == (2, 2, 3) - buffers[:] = [[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]] - - op(buffers, slice(None), "rows") - - np.testing.assert_array_equal(first.values[0][1], [[1, 2, 3], [7, 8, 9]]) - np.testing.assert_array_equal(second.values[0][1], [[4, 5, 6], [10, 11, 12]]) diff --git a/motrix_env_motrixsim/tests/test_write_compiler.py b/motrix_env_motrixsim/tests/test_write_compiler.py index 38fe7f90..4cac31e4 100644 --- a/motrix_env_motrixsim/tests/test_write_compiler.py +++ b/motrix_env_motrixsim/tests/test_write_compiler.py @@ -3,107 +3,79 @@ """Execution-level behavior of compiled MotrixSim write programs.""" -import numpy as np +import numpy +import pytest -from motrix_env_motrixsim.write_compiler import _CompiledWrite, _MotrixSimWriteProgram +from motrix_env_motrixsim.write_compiler import _MotrixSimWriteProgram -class _Model: - num_dof_pos = 0 - num_dof_vel = 0 +class _Data: + shape = (3,) + +class _NativeProgram: def __init__(self) -> None: - self.forward_kinematic_rows = [] + self.execute_calls = [] - def compute_init_dof_pos(self) -> np.ndarray: - return np.zeros((0,), dtype=np.float32) + def execute(self, data, env_ids=None) -> None: + self.execute_calls.append((data, None if env_ids is None else tuple(env_ids))) - def forward_kinematic(self, rows) -> None: - self.forward_kinematic_rows.append(rows) +def _program(native: _NativeProgram | None = None, data: _Data | None = None) -> _MotrixSimWriteProgram: + return _MotrixSimWriteProgram(data if data is not None else _Data(), {}, native) -class _Data: - shape = (3,) +def test_write_program_executes_native_program_with_selected_ids() -> None: + native = _NativeProgram() + data = _Data() + program = _program(native, data) -class _ResetData(_Data): - def __init__(self) -> None: - self.reset_calls = [] + program.execute(numpy.asarray([2, 0], dtype=numpy.int64)) - def reset(self, model, **kwargs) -> None: - self.reset_calls.append((model, kwargs)) + assert native.execute_calls == [(data, (0, 2))] -class _Op: - def __init__(self) -> None: - self.rows = [] +def test_write_program_executes_full_batch_without_env_ids() -> None: + native = _NativeProgram() + data = _Data() + program = _program(native, data) - def __call__(self, buffers, idx, rows) -> None: - del buffers, idx - self.rows.append(rows) + program.execute() + assert native.execute_calls == [(data, None)] -def test_write_program_refreshes_kinematics_once_after_all_ops() -> None: - model = _Model() - data = _Data() - first = _Op() - second = _Op() - program = _MotrixSimWriteProgram( - model, - data, - lambda env_ids: ("rows", tuple(env_ids)), - {}, - [(_CompiledWrite(first), {}), (_CompiledWrite(second), {})], - reset=False, - refresh_kinematics=True, - ) - - program.execute(np.asarray([2, 0], dtype=np.int64)) - - expected_rows = ("rows", (0, 2)) - assert first.rows == [expected_rows] - assert second.rows == [expected_rows] - assert model.forward_kinematic_rows == [expected_rows] - - -def test_reset_program_passes_compile_time_kinematics_flag_to_native_reset() -> None: - model = _Model() - data = _ResetData() - program = _MotrixSimWriteProgram(model, data, lambda env_ids: env_ids, {}, [], reset=True, refresh_kinematics=False) - program.execute() +def test_write_program_without_native_plan_is_a_no_op() -> None: + program = _program() - assert data.reset_calls == [(model, {"forward_kinematic": False})] - assert model.forward_kinematic_rows == [] + program.execute(numpy.asarray([0, 1], dtype=numpy.int64)) -def test_reset_program_applies_non_fused_writes_after_native_reset_and_refreshes_once() -> None: - model = _Model() - data = _ResetData() - op = _Op() - program = _MotrixSimWriteProgram( - model, - data, - lambda env_ids: env_ids, - {}, - [(_CompiledWrite(op), {})], - reset=True, - refresh_kinematics=True, - ) +@pytest.mark.parametrize( + "env_ids", + [ + numpy.asarray([0], dtype=numpy.int32), + numpy.asarray([[0, 1]], dtype=numpy.int64), + [0, 1], + ], +) +def test_write_program_rejects_non_int64_1d_ids(env_ids) -> None: + with pytest.raises(TypeError, match="int64 ndarray"): + _program().execute(env_ids) - program.execute() - assert data.reset_calls == [(model, {"forward_kinematic": False})] - assert op.rows == [data] - assert model.forward_kinematic_rows == [data] +def test_write_program_rejects_out_of_range_ids() -> None: + with pytest.raises(IndexError, match="out of range"): + _program().execute(numpy.asarray([3], dtype=numpy.int64)) -def test_write_program_skips_kinematic_refresh_when_no_op_requires_it() -> None: - model = _Model() - program = _MotrixSimWriteProgram( - model, _Data(), lambda env_ids: env_ids, {}, [], reset=False, refresh_kinematics=False - ) +def test_write_program_rejects_duplicate_ids() -> None: + with pytest.raises(ValueError, match="duplicates"): + _program().execute(numpy.asarray([0, 0], dtype=numpy.int64)) - program.execute() - assert model.forward_kinematic_rows == [] +def test_write_program_skips_empty_selection() -> None: + native = _NativeProgram() + _program(native).execute(numpy.asarray([], dtype=numpy.int64)) + + assert native.execute_calls == [] diff --git a/motrix_envs/src/motrix_envs/basic/bounce_ball/bounce_ball_np.py b/motrix_envs/src/motrix_envs/basic/bounce_ball/bounce_ball_np.py index c6ed0e89..dcd99997 100644 --- a/motrix_envs/src/motrix_envs/basic/bounce_ball/bounce_ball_np.py +++ b/motrix_envs/src/motrix_envs/basic/bounce_ball/bounce_ball_np.py @@ -18,7 +18,12 @@ GeomSpecsQuery, JointPositionWrite, ) -from motrix_env_core.sim.write import CtrlTargetsWrite, JointVelocityWrite, MocapPoseWrite +from motrix_env_core.sim.write import ( + CtrlTargetsWrite, + JointVelocityWrite, + KinematicBodyPositionWrite, + KinematicBodyRotationWrite, +) from .cfg import BounceBallEnvCfg @@ -40,8 +45,10 @@ def __init__(self, cfg: BounceBallEnvCfg, num_envs=1, backend: str | None = None self.sim_data = self.sim.compile_reads(_SIM_DATA_QUERIES) self._marker_writes = self.sim.write_compiler.compile( { - "height": MocapPoseWrite(("target_height_marker",)), - "paddle": MocapPoseWrite(("paddle_home_marker",)), + "height_pos": KinematicBodyPositionWrite(("target_height_marker",)), + "height_rot": KinematicBodyRotationWrite(("target_height_marker",)), + "paddle_pos": KinematicBodyPositionWrite(("paddle_home_marker",)), + "paddle_rot": KinematicBodyRotationWrite(("paddle_home_marker",)), }, ) self._ctrl_writes = self.sim.write_compiler.compile({"ctrl": CtrlTargetsWrite()}) @@ -580,14 +587,20 @@ def reset(self, env_ids: np.ndarray) -> None: target_marker_poses = np.tile(self._target_marker_base_pose, (num_reset, 1)) target_marker_poses[:, 2] = new_target_heights + self._ball_radius # Set z position - self._marker_writes.buffer("height")[env_ids, 0] = np.ascontiguousarray(target_marker_poses, dtype=np.float32) - - # Update paddle home marker position - paddle_home_marker_poses = np.tile(self._paddle_home_marker_pose, (num_reset, 1)) + self._marker_writes.buffer("height_pos")[env_ids, 0] = np.ascontiguousarray( + target_marker_poses[:, :3], dtype=np.float32 + ) + self._marker_writes.buffer("height_rot")[env_ids, 0] = np.ascontiguousarray( + target_marker_poses[:, 3:7], dtype=np.float32 + ) # Set paddle home marker mocap body pose - self._marker_writes.buffer("paddle")[env_ids, 0] = np.ascontiguousarray( - paddle_home_marker_poses, dtype=np.float32 + paddle_home_marker_poses = np.tile(self._paddle_home_marker_pose, (num_reset, 1)) + self._marker_writes.buffer("paddle_pos")[env_ids, 0] = np.ascontiguousarray( + paddle_home_marker_poses[:, :3], dtype=np.float32 + ) + self._marker_writes.buffer("paddle_rot")[env_ids, 0] = np.ascontiguousarray( + paddle_home_marker_poses[:, 3:7], dtype=np.float32 ) # Both markers go to the backend in one crossing. self._marker_writes.execute(env_ids) diff --git a/motrix_envs/src/motrix_envs/basic/manipulator/manipulator_np.py b/motrix_envs/src/motrix_envs/basic/manipulator/manipulator_np.py index 67c1be4e..f7719a0a 100644 --- a/motrix_envs/src/motrix_envs/basic/manipulator/manipulator_np.py +++ b/motrix_envs/src/motrix_envs/basic/manipulator/manipulator_np.py @@ -19,7 +19,7 @@ SitePositionQuery, SiteQuaternionQuery, ) -from motrix_env_core.sim.write import CtrlTargetsWrite, JointVelocityWrite, MocapPoseWrite +from motrix_env_core.sim.write import CtrlTargetsWrite, JointVelocityWrite, KinematicBodyPositionWrite, KinematicBodyRotationWrite from motrix_envs.basic.manipulator.cfg import BringBallCfg _ARM_JOINTS = ( @@ -106,7 +106,12 @@ def __init__(self, cfg: BringBallCfg, num_envs=1, backend: str | None = None): super().__init__(cfg, num_envs, backend=backend) self.model = self.sim.compile_model(_SIM_MODEL_QUERIES) self.sim_data = self.sim.compile_reads(_SIM_DATA_QUERIES) - self._target_writes = self.sim.write_compiler.compile({"target": MocapPoseWrite(("target_ball",))}) + self._target_writes = self.sim.write_compiler.compile( + { + "target_pos": KinematicBodyPositionWrite(("target_ball",)), + "target_rot": KinematicBodyRotationWrite(("target_ball",)), + } + ) self._ctrl_writes = self.sim.write_compiler.compile({"ctrl": CtrlTargetsWrite()}) object_joints = ("ball_x", "ball_z", "ball_y") self._reset_program = self.sim.write_compiler.compile( @@ -229,7 +234,8 @@ def _set_target_mocap( pose[:, 2] = target_z pose[:, 3:7] = _quat_from_y_angle(target_angle) env_ids = np.asarray(env_ids, dtype=np.int64) - self._target_writes.buffer("target")[env_ids, 0] = pose + self._target_writes.buffer("target_pos")[env_ids, 0] = pose[:, :3] + self._target_writes.buffer("target_rot")[env_ids, 0] = pose[:, 3:7] self._target_writes.execute(env_ids) def _set_object_state( diff --git a/motrix_envs/src/motrix_envs/locomotion/anymal_c/anymal_c_np.py b/motrix_envs/src/motrix_envs/locomotion/anymal_c/anymal_c_np.py index 25f8a1d0..f9baa748 100644 --- a/motrix_envs/src/motrix_envs/locomotion/anymal_c/anymal_c_np.py +++ b/motrix_envs/src/motrix_envs/locomotion/anymal_c/anymal_c_np.py @@ -23,7 +23,12 @@ LinkQuaternionQuery, SensorValuesQuery, ) -from motrix_env_core.sim.write import BodyJointVelocityWrite, CtrlTargetsWrite, MocapPoseWrite +from motrix_env_core.sim.write import ( + BodyJointVelocityWrite, + CtrlTargetsWrite, + KinematicBodyPositionWrite, + KinematicBodyRotationWrite, +) from .cfg import AnymalCEnvCfg @@ -59,11 +64,18 @@ def __init__(self, cfg: AnymalCEnvCfg, num_envs=1, backend: str | None = None): self.sim_data = self.sim.compile_reads(_sim_data_queries(cfg)) self._heading_writes = self.sim.write_compiler.compile( { - "robot": MocapPoseWrite(("robot_heading_arrow",)), - "desired": MocapPoseWrite(("desired_heading_arrow",)), + "robot_pos": KinematicBodyPositionWrite(("robot_heading_arrow",)), + "robot_rot": KinematicBodyRotationWrite(("robot_heading_arrow",)), + "desired_pos": KinematicBodyPositionWrite(("desired_heading_arrow",)), + "desired_rot": KinematicBodyRotationWrite(("desired_heading_arrow",)), }, ) - self._target_writes = self.sim.write_compiler.compile({"target": MocapPoseWrite(("target_marker",))}) + self._target_writes = self.sim.write_compiler.compile( + { + "target_pos": KinematicBodyPositionWrite(("target_marker",)), + "target_rot": KinematicBodyRotationWrite(("target_marker",)), + } + ) self._ctrl_writes = self.sim.write_compiler.compile({"ctrl": CtrlTargetsWrite()}) self._reset_program = self.sim.write_compiler.compile( { @@ -330,18 +342,16 @@ def _update_heading_arrows( ) robot_arrow_pos = robot_pos.copy() robot_arrow_pos[:, 2] = arrow_height - robot_arrow_quat = quaternion.from_euler(0, 0, cur_yaw) - self._heading_writes.buffer("robot")[env_ids, 0] = np.concatenate( - [robot_arrow_pos, robot_arrow_quat], axis=1 - ).astype(np.float32) + robot_arrow_quat = quaternion.from_euler(0, 0, cur_yaw).astype(np.float32) + self._heading_writes.buffer("robot_pos")[env_ids, 0] = robot_arrow_pos.astype(np.float32) + self._heading_writes.buffer("robot_rot")[env_ids, 0] = robot_arrow_quat des_yaw = np.where( np.linalg.norm(desired_vel_xy, axis=1) > 1e-6, np.arctan2(desired_vel_xy[:, 1], desired_vel_xy[:, 0]), 0.0 ) - desired_arrow_quat = quaternion.from_euler(0, 0, des_yaw) - self._heading_writes.buffer("desired")[env_ids, 0] = np.concatenate( - [robot_arrow_pos, desired_arrow_quat], axis=1 - ).astype(np.float32) + desired_arrow_quat = quaternion.from_euler(0, 0, des_yaw).astype(np.float32) + self._heading_writes.buffer("desired_pos")[env_ids, 0] = robot_arrow_pos.astype(np.float32) + self._heading_writes.buffer("desired_rot")[env_ids, 0] = desired_arrow_quat # Both heading markers go to the backend in one crossing. self._heading_writes.execute(env_ids) @@ -474,10 +484,9 @@ def _update_target_marker(self, env_ids: np.ndarray, pose_commands: np.ndarray): arrow_pos = pose_commands.copy() arrow_pos[:, 2] = 0.05 arrow_pos = np.column_stack([pose_commands[:, 0], pose_commands[:, 1], np.full((num_envs, 1), 0.5)]) - arrow_quat = quaternion.from_euler(0, 0, pose_commands[:, 2]) - self._target_writes.buffer("target")[env_ids, 0] = np.concatenate([arrow_pos, arrow_quat], axis=1).astype( - np.float32 - ) + arrow_quat = quaternion.from_euler(0, 0, pose_commands[:, 2]).astype(np.float32) + self._target_writes.buffer("target_pos")[env_ids, 0] = arrow_pos.astype(np.float32) + self._target_writes.buffer("target_rot")[env_ids, 0] = arrow_quat self._target_writes.execute(env_ids) def _compute_terminated(self, state: ArrayEnvState) -> ArrayEnvState: diff --git a/motrix_envs/src/motrix_envs/manipulation/rm65_insert_peg/insert_peg_np.py b/motrix_envs/src/motrix_envs/manipulation/rm65_insert_peg/insert_peg_np.py index 9ae014ed..89517fff 100644 --- a/motrix_envs/src/motrix_envs/manipulation/rm65_insert_peg/insert_peg_np.py +++ b/motrix_envs/src/motrix_envs/manipulation/rm65_insert_peg/insert_peg_np.py @@ -12,13 +12,20 @@ BodyJointPositionQuery, BodyJointPositionWrite, BodyJointVelocityQuery, + BodyJointVelocityWrite, DofPositionQuery, GeomLinearVelocityQuery, GeomPositionQuery, GeomQuaternionQuery, SitePositionQuery, ) -from motrix_env_core.sim.write import BodyJointVelocityWrite, CtrlTargetsWrite +from motrix_env_core.sim.write import ( + CtrlTargetsWrite, + JointAngularVelocityWrite, + JointPositionWrite, + JointQuaternionWrite, + JointVelocityWrite, +) from .cfg import PegInsertEnvCfg @@ -52,11 +59,15 @@ def __init__(self, cfg: PegInsertEnvCfg, num_envs=1, backend: str | None = None) { "robot_position": BodyJointPositionWrite("base_link"), "robot_velocity": BodyJointVelocityWrite("base_link"), - "peg_position": BodyJointPositionWrite("free_peg"), - "peg_velocity": BodyJointVelocityWrite("free_peg"), + "peg_translation": JointPositionWrite(("peg_x", "peg_y", "peg_z")), + "peg_translation_velocity": JointVelocityWrite(("peg_x", "peg_y", "peg_z")), + "peg_orientation": JointQuaternionWrite(("peg_rot",)), + "peg_angular_velocity": JointAngularVelocityWrite(("peg_rot",)), }, reset=True, ) + self._peg_reset_translation = self._reset_program.buffer("peg_translation") + self._peg_reset_orientation = self._reset_program.buffer("peg_orientation")[:, 0] self.default_joint_pos = self._cfg.init_state.default_joint_pos self._action_dim = 7 @@ -353,10 +364,9 @@ def reset(self, env_ids) -> None: robot_pos[row_ids] = 0.0 robot_pos[row_ids, : self._num_dof_pos] = self._init_dof_pos self._reset_program.buffer("robot_velocity")[row_ids] = 0.0 - peg_pos = self._reset_program.buffer("peg_position") - peg_pos[row_ids] = 0.0 - peg_pos[row_ids, -1] = 1.0 - self._reset_program.buffer("peg_velocity")[row_ids] = 0.0 + self._peg_reset_translation[row_ids] = 0.0 + self._peg_reset_orientation[row_ids] = 0.0 + self._peg_reset_orientation[row_ids, 3] = 1.0 # identity quaternion (xyzw) self._reset_program.execute(row_ids) self.sim_data.execute(row_ids) @@ -374,10 +384,9 @@ def reset(self, env_ids) -> None: robot_pos[row_ids] = 0.0 robot_pos[row_ids, : self._num_dof_pos] = np.asarray(robot_dof_pos, dtype=np.float32) self._reset_program.buffer("robot_velocity")[row_ids] = 0.0 - peg_pos[row_ids, :3] = np.stack([peg_x, peg_y, peg_z], axis=-1) - peg_pos[row_ids, 3:6] = 0.0 - peg_pos[row_ids, 6] = 1.0 - self._reset_program.buffer("peg_velocity")[row_ids] = 0.0 + self._peg_reset_translation[row_ids] = np.stack([peg_x, peg_y, peg_z], axis=-1) + self._peg_reset_orientation[row_ids] = 0.0 + self._peg_reset_orientation[row_ids, 3] = 1.0 # identity quaternion (xyzw) self._reset_program.execute(row_ids) self.sim_data.execute(row_ids) diff --git a/motrix_envs/src/motrix_envs/manipulation/shadow_hand/shadow_hand_np.py b/motrix_envs/src/motrix_envs/manipulation/shadow_hand/shadow_hand_np.py index 8f2c6dae..587f0cf7 100644 --- a/motrix_envs/src/motrix_envs/manipulation/shadow_hand/shadow_hand_np.py +++ b/motrix_envs/src/motrix_envs/manipulation/shadow_hand/shadow_hand_np.py @@ -24,7 +24,12 @@ LinkPositionQuery, LinkQuaternionQuery, ) -from motrix_env_core.sim.write import BodyJointVelocityWrite, CtrlTargetsWrite, MocapPoseWrite +from motrix_env_core.sim.write import ( + BodyJointVelocityWrite, + CtrlTargetsWrite, + KinematicBodyPositionWrite, + KinematicBodyRotationWrite, +) from .cfg import ShadowHandReposeEnvCfg @@ -83,7 +88,12 @@ def __init__(self, cfg: ShadowHandReposeEnvCfg, num_envs=1, backend: str | None super().__init__(cfg, num_envs, backend=backend) self.model = self.sim.compile_model(_SIM_MODEL_QUERIES) self.sim_data = self.sim.compile_reads(_SIM_DATA_QUERIES) - self._target_writes = self.sim.write_compiler.compile({"target": MocapPoseWrite(("target",))}) + self._target_writes = self.sim.write_compiler.compile( + { + "target_pos": KinematicBodyPositionWrite(("target",)), + "target_rot": KinematicBodyRotationWrite(("target",)), + } + ) self._ctrl_writes = self.sim.write_compiler.compile({"ctrl": CtrlTargetsWrite()}) self._reset_program = self.sim.write_compiler.compile( { @@ -345,12 +355,10 @@ def _update_target_visualization(self): # Compute visualization position (offset from goal position) viz_pos = self._goal_pos + np.array(cfg.viz_target_offset, dtype=np.float32) - # Combine into pose array: [x, y, z, qx, qy, qz, qw] - viz_pose = np.concatenate([viz_pos, self._goal_rot], axis=-1) - # Update mocap body pose all_ids = np.arange(self._num_envs, dtype=np.int64) - self._target_writes.buffer("target")[all_ids, 0] = np.asarray(viz_pose, dtype=np.float32) + self._target_writes.buffer("target_pos")[all_ids, 0] = np.asarray(viz_pos, dtype=np.float32) + self._target_writes.buffer("target_rot")[all_ids, 0] = np.asarray(self._goal_rot, dtype=np.float32) self._target_writes.execute(all_ids) def reset(self, env_ids: np.ndarray) -> None: diff --git a/pyproject.toml b/pyproject.toml index 93472142..8e7e0416 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -99,8 +99,8 @@ url = "https://download.pytorch.org/whl/rocm7.2" explicit = true [[tool.uv.index]] -name = "motphys-dev" -url = "https://pypi.motphys.com/simple" +name = "pypi" +url = "https://pypi.org/simple" [tool.uv.sources] motrix-env-core = { workspace = true } diff --git a/uv.lock b/uv.lock index b1e5c941..0028fd9d 100644 --- a/uv.lock +++ b/uv.lock @@ -1023,7 +1023,7 @@ dependencies = [ [package.metadata] requires-dist = [ { name = "motrix-env-core", editable = "motrix_env_core" }, - { name = "motrixsim", specifier = "==0.10.1.dev123478" }, + { name = "motrixsim", specifier = "==0.10.1" }, { name = "numpy", specifier = ">=1.26" }, ] @@ -1244,28 +1244,28 @@ provides-extras = ["skrl-jax", "skrl-torch", "rslrl"] [[package]] name = "motrixsim" -version = "0.10.1.dev123478" -source = { registry = "https://pypi.motphys.com/simple" } +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "motrixsim-core" }, ] wheels = [ - { url = "https://pypi.motphys.com/packages/motrixsim-0.10.1.dev123478-py3-none-any.whl", hash = "sha256:53a86b45f9dbd7b3dc89c70e97740d5a582383ae137ecffbd7704696e25788ea" }, + { url = "https://files.pythonhosted.org/packages/cd/cd/0f2ca088cea1721d9a0cca058e9b18a41b5645ce8dcf8a857086e7667fec/motrixsim-0.10.1-py3-none-any.whl", hash = "sha256:9c27362c40b9a72c3e7ac80f556cc0effd4c30ab3afb93764c81e1f3745402b0", size = 2018, upload-time = "2026-09-18T16:25:05.939Z" }, ] [[package]] name = "motrixsim-core" -version = "0.10.1.dev123478+pro" -source = { registry = "https://pypi.motphys.com/simple" } +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "absl-py" }, { name = "numpy" }, ] wheels = [ - { url = "https://pypi.motphys.com/packages/motrixsim_core-0.10.1.dev123478+pro-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fe585358b7c1dc9a81953e6820873e20e45a80bb226ca4ec287c5cdf72d75109" }, - { url = "https://pypi.motphys.com/packages/motrixsim_core-0.10.1.dev123478+pro-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7946cc8c2dd0699c9d6760276263593045008a5e6496a105a244f16c5d6f1846" }, - { url = "https://pypi.motphys.com/packages/motrixsim_core-0.10.1.dev123478+pro-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9135536195f7139274ada6507cf1d23e0216ce0627867925bbefa06c706fca7" }, - { url = "https://pypi.motphys.com/packages/motrixsim_core-0.10.1.dev123478+pro-cp310-cp310-win_amd64.whl", hash = "sha256:be99be2772c5ef8947c9f4b171b0cc4b68204dcd1fdbe2ab000a963ee46a4252" }, + { url = "https://files.pythonhosted.org/packages/4f/13/5aacc370c2aa951a34587549625d4776c84d30d0ef1f1da555ef6a51923c/motrixsim_core-0.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d12fba015f1ce890e0a07daa6c9d24730805dfb27916f3603779186294358150", size = 58730585, upload-time = "2026-09-18T16:26:14.933Z" }, + { url = "https://files.pythonhosted.org/packages/07/3d/c4d41be2de374a73aa1c1cde2a8df5b1225b204a83e4ab7f52fe095a69c7/motrixsim_core-0.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:acce59249576d724160756383632bc8af522beac337d6b41ccea0b521f972841", size = 69500031, upload-time = "2026-09-18T16:29:59.37Z" }, + { url = "https://files.pythonhosted.org/packages/fc/b4/186e83b3d89b3818aaccefd62690b77d6914370097fd5645dbc1f4d3d92a/motrixsim_core-0.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a16c78a2a9a5a574de38af0cf3831b12c3f754eba2be8474046434884a14b34", size = 66696962, upload-time = "2026-09-18T16:29:41.349Z" }, + { url = "https://files.pythonhosted.org/packages/17/be/95c65ea4484597c7575f4d44f6cd006bd6212d079c09a9335675749aa2c3/motrixsim_core-0.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:da609700990970bd304e07cb8e9072ed4e98c986e07da103aac6b8f3507bba43", size = 50656529, upload-time = "2026-09-18T16:28:49.685Z" }, ] [[package]]