diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json
index 06a39d376..14f830e03 100644
--- a/.basedpyright/baseline.json
+++ b/.basedpyright/baseline.json
@@ -1155,30 +1155,6 @@
}
],
"./positronic/data_collection.py": [
- {
- "code": "reportOptionalOperand",
- "range": {
- "startColumn": 25,
- "endColumn": 48,
- "lineCount": 1
- }
- },
- {
- "code": "reportOptionalMemberAccess",
- "range": {
- "startColumn": 89,
- "endColumn": 92,
- "lineCount": 1
- }
- },
- {
- "code": "reportPossiblyUnboundVariable",
- "range": {
- "startColumn": 64,
- "endColumn": 80,
- "lineCount": 1
- }
- },
{
"code": "reportReturnType",
"range": {
@@ -1211,14 +1187,6 @@
"lineCount": 1
}
},
- {
- "code": "reportArgumentType",
- "range": {
- "startColumn": 73,
- "endColumn": 99,
- "lineCount": 1
- }
- },
{
"code": "reportAttributeAccessIssue",
"range": {
@@ -1227,14 +1195,6 @@
"lineCount": 1
}
},
- {
- "code": "reportAttributeAccessIssue",
- "range": {
- "startColumn": 66,
- "endColumn": 71,
- "lineCount": 1
- }
- },
{
"code": "reportAssignmentType",
"range": {
@@ -3331,30 +3291,6 @@
"lineCount": 1
}
},
- {
- "code": "reportArgumentType",
- "range": {
- "startColumn": 46,
- "endColumn": 50,
- "lineCount": 1
- }
- },
- {
- "code": "reportAttributeAccessIssue",
- "range": {
- "startColumn": 43,
- "endColumn": 49,
- "lineCount": 1
- }
- },
- {
- "code": "reportOptionalSubscript",
- "range": {
- "startColumn": 32,
- "endColumn": 38,
- "lineCount": 1
- }
- },
{
"code": "reportReturnType",
"range": {
@@ -3997,22 +3933,6 @@
}
],
"./positronic/drivers/webxr.py": [
- {
- "code": "reportArgumentType",
- "range": {
- "startColumn": 12,
- "endColumn": 38,
- "lineCount": 1
- }
- },
- {
- "code": "reportArgumentType",
- "range": {
- "startColumn": 12,
- "endColumn": 30,
- "lineCount": 1
- }
- },
{
"code": "reportArgumentType",
"range": {
@@ -4021,22 +3941,6 @@
"lineCount": 1
}
},
- {
- "code": "reportAttributeAccessIssue",
- "range": {
- "startColumn": 59,
- "endColumn": 70,
- "lineCount": 1
- }
- },
- {
- "code": "reportAttributeAccessIssue",
- "range": {
- "startColumn": 59,
- "endColumn": 70,
- "lineCount": 1
- }
- },
{
"code": "reportArgumentType",
"range": {
@@ -6977,4 +6881,4 @@
}
]
}
-}
+}
\ No newline at end of file
diff --git a/pimm/logging.py b/pimm/logging.py
index 99d628a69..748337053 100644
--- a/pimm/logging.py
+++ b/pimm/logging.py
@@ -39,6 +39,7 @@
'boto3',
's3transfer', # per part of a multipart upload
'asyncio', # per selector event, under its debug mode
+ 'linuxpy', # per ioctl, which is several times a frame for every camera
)
diff --git a/pimm/tests/test_calls.py b/pimm/tests/test_calls.py
index 09187272e..e3583597b 100644
--- a/pimm/tests/test_calls.py
+++ b/pimm/tests/test_calls.py
@@ -175,6 +175,17 @@ def run(self, should_stop, clock):
yield Sleep(0.001)
+class Interrupted(ControlSystem):
+ """Ends on the interrupt an operator sends, which reaches every process of a run at once."""
+
+ def __init__(self):
+ self.add = ControlSystemHandler[tuple[int, int], int](self)
+
+ def run(self, should_stop, clock):
+ yield Sleep(0.001)
+ raise KeyboardInterrupt
+
+
class TestAllOf:
def test_one_answer_stands_for_many(self, bound):
caller, handler = bound
@@ -267,6 +278,18 @@ def test_a_handler_whose_system_ends_answers_the_calls_it_never_reached(self):
world.run([client, deaf])
assert isinstance(client.results[0], HandlerStopped)
+ def test_a_handler_the_operator_interrupts_answers_nothing(self):
+ """An interrupt can land inside a manager call, and the connection then carries half a message.
+
+ Reading it again returns the tail of another, so an interrupted system says nothing at all.
+ """
+ client, interrupted = Client([(1, 2)]), Interrupted()
+ with World(virtual_time=True) as world:
+ world.connect(client.add, interrupted.add)
+ with pytest.raises(KeyboardInterrupt):
+ world.run([client, interrupted])
+ assert client.results == []
+
def test_a_stopped_handler_survives_the_trip_to_another_process(self):
"""A reply crosses a pipe as pickle, and an exception is rebuilt by calling its class with its args."""
assert isinstance(pickle.loads(pickle.dumps(HandlerStopped())), HandlerStopped)
diff --git a/pimm/tests/test_world.py b/pimm/tests/test_world.py
index 9c7dbe92c..c6646f31d 100644
--- a/pimm/tests/test_world.py
+++ b/pimm/tests/test_world.py
@@ -9,6 +9,7 @@
import pytest
+import pimm.world
from pimm.core import (
ControlSystem,
ControlSystemEmitter,
@@ -99,6 +100,24 @@ def read_from_buffer(self, buffer: memoryview | bytes) -> None:
self.value = struct.unpack('d', buffer[:8])[0]
+def test_a_process_that_took_an_interrupt_stops_talking_to_the_manager(monkeypatch):
+ """An interrupt can land inside a call to the manager, and that connection then holds half a message.
+
+ Reading it again returns what another call asked for, so a reader takes a value off a channel it never
+ subscribed to. A process that has taken one neither reads nor sends after it.
+ """
+ with World() as world:
+ emitter, receiver = world.mp_pipes()
+ assert not isinstance(receiver, list) # one receiver, which is what `mp_pipes` makes by default
+ emitter.emit('before', ts=1)
+ assert receiver.read() is not None
+
+ monkeypatch.setattr(pimm.world, '_interrupted', True)
+
+ emitter.emit('after', ts=2) # dropped, not sent
+ assert receiver.read() is None
+
+
class TestQueueEmitter:
"""Test the QueueEmitter class."""
diff --git a/pimm/world.py b/pimm/world.py
index fcd47c495..4fb170951 100644
--- a/pimm/world.py
+++ b/pimm/world.py
@@ -11,6 +11,7 @@
import traceback
from collections import Counter, defaultdict, deque
from collections.abc import Callable, Iterator, Mapping
+from contextlib import contextmanager
from enum import IntEnum
from multiprocessing import resource_tracker
from multiprocessing.managers import ValueProxy
@@ -42,6 +43,7 @@
logger = logging.getLogger(__name__)
T = TypeVar('T')
+U = TypeVar('U')
Req = TypeVar('Req')
Res = TypeVar('Res')
@@ -75,6 +77,28 @@ def emit(self, data: T, ts: int = -1):
pass
+# Set in a process that has taken an interrupt. An interrupt can land inside a call to the manager, and
+# that connection then holds half a message: the next call over it returns what another one asked for, so
+# a reader takes a value from a channel it never subscribed to. Nothing may be sent or read after it.
+_interrupted = False
+
+
+@contextmanager
+def _noting_interrupt() -> Iterator[None]:
+ """Record an interrupt taken inside the block, and let it go on.
+
+ A connection is torn by an interrupt that lands in the middle of a call over it, so every process that
+ reaches a transport records its own -- there is nowhere else the tearing can happen, and no process has
+ to have had a handler installed for it.
+ """
+ global _interrupted
+ try:
+ yield
+ except KeyboardInterrupt:
+ _interrupted = True
+ raise
+
+
class MultiprocessEmitter(SignalEmitter[T]):
"""Signal emitter that transparently bridges processes.
@@ -188,9 +212,12 @@ def _emit_shared_memory(self, data: SMCompliant, ts: int) -> bool:
return True
+ @_noting_interrupt()
def emit(self, data: T, ts: int = -1):
+ if _interrupted:
+ return
ts = ts if ts >= 0 else self._clock.now_ns()
- mode = self._ensure_mode(data)
+ mode = self._ensure_mode(data) # itself a call to the manager, so it sits inside the guard
if mode is TransportMode.SHARED_MEMORY:
if not isinstance(data, SMCompliant):
@@ -275,6 +302,10 @@ def _read_queue(self) -> Message[T] | None:
except Empty:
message = None
else:
+ if not isinstance(message, Message):
+ # An interrupt that lands inside a manager call leaves that connection holding half a
+ # message, and every read after it comes back as whatever another call asked for.
+ raise ConnectionError(f'the queue was read after an interrupt tore its connection: {message!r}')
self._last_queue_message = Message(message.data, message.ts, True)
if self._mode is TransportMode.UNDECIDED:
self._mode = TransportMode.QUEUE
@@ -332,8 +363,11 @@ def _read_shared_memory(self) -> Message[T] | None:
self._up_value.value = False
return Message(data=self._out_value, ts=self._ts_value.value, updated=updated) # instead of True
+ @_noting_interrupt()
def read(self) -> Message[T] | None:
- mode = self.transport_mode
+ if _interrupted:
+ return None
+ mode = self.transport_mode # itself a call to the manager, so it sits inside the guard
if mode is TransportMode.SHARED_MEMORY:
return self._read_shared_memory()
@@ -456,11 +490,19 @@ def __init__(self, cs: ControlSystem):
self.__name__ = f'{type(cs).__name__}.run'
def __call__(self, should_stop: SignalReceiver, clock: Clock) -> Iterator[Command]:
+ interrupted = False
try:
yield from self.cs.run(should_stop, clock)
+ except KeyboardInterrupt:
+ # An interrupt lands in every process at once, and it can land inside a manager call, which
+ # leaves that connection in the middle of a message. Reading it again returns the tail of
+ # somebody else's, so a call is left unanswered here rather than answered from a torn stream.
+ interrupted = True
+ raise
finally:
- for handler in handlers_of(self.cs):
- handler.fail_queued()
+ if not interrupted:
+ for handler in handlers_of(self.cs):
+ handler.fail_queued()
def _bg_wrapper(
@@ -645,10 +687,10 @@ def interleave(self, *loops: ControlLoop) -> Iterator[Command]:
def connect(
self,
source: ControlSystemEmitter[T] | ControlSystemCaller[Req, Res],
- target: ControlSystemReceiver[T] | ControlSystemHandler[Req, Res],
+ target: ControlSystemReceiver[U] | ControlSystemHandler[Req, Res],
*,
emitter_wrapper: Callable[[SignalEmitter[T]], SignalEmitter[T]] = identity,
- receiver_wrapper: Callable[[SignalReceiver[T]], SignalReceiver[T]] = identity,
+ receiver_wrapper: Callable[[SignalReceiver[T]], SignalReceiver[U]] = identity,
) -> None:
"""Declare a logical connection: an Emitter feeding a Receiver, or a Caller invoking a Handler.
diff --git a/positronic/assets/mujoco/trossen_wxai/LICENSE b/positronic/assets/mujoco/trossen_wxai/LICENSE
new file mode 100644
index 000000000..58eba3013
--- /dev/null
+++ b/positronic/assets/mujoco/trossen_wxai/LICENSE
@@ -0,0 +1,29 @@
+BSD 3-Clause License
+
+Copyright (c) 2025, Trossen Robotics
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+ this list of conditions and the following disclaimer in the documentation
+ and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/positronic/assets/mujoco/trossen_wxai/README.md b/positronic/assets/mujoco/trossen_wxai/README.md
new file mode 100644
index 000000000..697c700b4
--- /dev/null
+++ b/positronic/assets/mujoco/trossen_wxai/README.md
@@ -0,0 +1,24 @@
+# Trossen WidowX AI Description
+
+Robot description (MJCF) of the Trossen WidowX AI arm, from
+[trossen_arm_mujoco](https://github.com/TrossenRobotics/trossen_arm_mujoco)
+(`trossen_arm_mujoco/assets/wxai/wxai_follower.xml` at revision
+[`8d9389b`](https://github.com/TrossenRobotics/trossen_arm_mujoco/commit/8d9389b46ba02bdbf6d5e086e09d20717ccfed68)),
+BSD 3-Clause, see [LICENSE](./LICENSE).
+
+`meshdir` names `assets`, the directory beside this file that holds the meshes.
+
+`ee_site` is the frame the arm controller reports its Cartesian position in: the site sits 0.156 m along
+the flange's x axis, which is the `t_flange_tool` offset the `trossen_arm` SDK carries for the standard
+`wxai_v0` end effector. Forward kinematics on this model and the pose the controller reports agree to
+0.13 mm and 0.01 degrees, measured on firmware 1.11.1.
+
+`wxai_follower.urdf` is the same arm as URDF, from
+[trossen_arm_description](https://github.com/TrossenRobotics/trossen_arm_description)
+(`urdf/generated/wxai/wxai_follower.urdf`), BSD 3-Clause, with `meshes/trossen_black.png` beside the STLs
+it already shares with the MJCF. The driver solves against the MJCF and the codecs against the URDF, so
+`ee_gripper_link` of the URDF and `ee_site` of the MJCF must name one place:
+`test_the_trossen_urdf_and_its_mjcf_put_the_control_frame_in_the_same_place` measures that they do.
+
+The URDF names its meshes the way its own ROS package does; the driver shortens each to the file beside
+this README before it publishes the model.
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/base_link.stl b/positronic/assets/mujoco/trossen_wxai/assets/base_link.stl
new file mode 100644
index 000000000..5fb36b36f
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/base_link.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/camera_mount_d405.stl b/positronic/assets/mujoco/trossen_wxai/assets/camera_mount_d405.stl
new file mode 100644
index 000000000..6215e6f99
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/camera_mount_d405.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/carriage_left.stl b/positronic/assets/mujoco/trossen_wxai/assets/carriage_left.stl
new file mode 100644
index 000000000..ac0223b3f
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/carriage_left.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/carriage_right.stl b/positronic/assets/mujoco/trossen_wxai/assets/carriage_right.stl
new file mode 100644
index 000000000..15660bd78
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/carriage_right.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/d405.stl b/positronic/assets/mujoco/trossen_wxai/assets/d405.stl
new file mode 100644
index 000000000..413d260f6
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/d405.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/gripper_left.stl b/positronic/assets/mujoco/trossen_wxai/assets/gripper_left.stl
new file mode 100644
index 000000000..4d7d72fd1
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/gripper_left.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/gripper_right.stl b/positronic/assets/mujoco/trossen_wxai/assets/gripper_right.stl
new file mode 100644
index 000000000..868a198b9
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/gripper_right.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/link_1.stl b/positronic/assets/mujoco/trossen_wxai/assets/link_1.stl
new file mode 100644
index 000000000..05173877d
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/link_1.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/link_2.stl b/positronic/assets/mujoco/trossen_wxai/assets/link_2.stl
new file mode 100644
index 000000000..40ccf3492
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/link_2.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/link_3.stl b/positronic/assets/mujoco/trossen_wxai/assets/link_3.stl
new file mode 100644
index 000000000..ebee403ee
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/link_3.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/link_4.stl b/positronic/assets/mujoco/trossen_wxai/assets/link_4.stl
new file mode 100644
index 000000000..e43e5f2bf
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/link_4.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/link_5.stl b/positronic/assets/mujoco/trossen_wxai/assets/link_5.stl
new file mode 100644
index 000000000..5ff6485e7
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/link_5.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/link_6.stl b/positronic/assets/mujoco/trossen_wxai/assets/link_6.stl
new file mode 100644
index 000000000..886782795
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/link_6.stl differ
diff --git a/positronic/assets/mujoco/trossen_wxai/assets/trossen_black.png b/positronic/assets/mujoco/trossen_wxai/assets/trossen_black.png
new file mode 100644
index 000000000..e7443eb55
Binary files /dev/null and b/positronic/assets/mujoco/trossen_wxai/assets/trossen_black.png differ
diff --git a/positronic/assets/mujoco/trossen_wxai/wxai_follower.urdf b/positronic/assets/mujoco/trossen_wxai/wxai_follower.urdf
new file mode 100644
index 000000000..5e3542348
--- /dev/null
+++ b/positronic/assets/mujoco/trossen_wxai/wxai_follower.urdf
@@ -0,0 +1,405 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positronic/assets/mujoco/trossen_wxai/wxai_follower.xml b/positronic/assets/mujoco/trossen_wxai/wxai_follower.xml
new file mode 100644
index 000000000..b009daef6
--- /dev/null
+++ b/positronic/assets/mujoco/trossen_wxai/wxai_follower.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/positronic/cfg/embodiment.py b/positronic/cfg/embodiment.py
index 4dacebe9f..d8cf8547e 100644
--- a/positronic/cfg/embodiment.py
+++ b/positronic/cfg/embodiment.py
@@ -65,6 +65,34 @@ def yam(robot_arm, cameras):
)
+@cfn.config(
+ robot_arm=positronic.cfg.hardware.roboarm.trossen,
+ cameras=dict(positronic.cfg.hardware.camera.TROSSEN_STATION_VIEWS),
+)
+def trossen(robot_arm, cameras):
+ """Real single-arm Trossen WidowX AI: the arm driver carries the gripper (they share one controller)."""
+ observations = {
+ keys.ROBOT_STATE: Observation(robot_arm.state, Serializers.robot_state),
+ keys.GRIP: Observation(robot_arm.grip, None),
+ **{name: Observation(cam.frame, Serializers.camera_images) for name, cam in cameras.items()},
+ }
+ commands = {
+ keys.ROBOT_COMMAND: Command(robot_arm.commands, Serializers.robot_command),
+ keys.TARGET_GRIP: Command(robot_arm.target_grip, None),
+ }
+ return Embodiment(
+ descriptor='trossen_wxai',
+ observations=observations,
+ commands=commands,
+ # One driver, one handler: the arm's controller carries its fingers
+ prepare_handlers={eval_keys.ARM: robot_arm.sync_move},
+ static_meta=dict(ROBOT_STATIC_META),
+ meta_source=robot_arm.robot_meta,
+ control_systems=(*cameras.values(), robot_arm),
+ simulated=False,
+ )
+
+
@cfn.config(
left_channel='can0',
right_channel='can1',
diff --git a/positronic/cfg/eval/real/trossen.py b/positronic/cfg/eval/real/trossen.py
new file mode 100644
index 000000000..46da2953f
--- /dev/null
+++ b/positronic/cfg/eval/real/trossen.py
@@ -0,0 +1,49 @@
+from collections.abc import Callable
+from functools import partial
+from typing import Any
+
+import configuronic as cfn
+import numpy as np
+
+from positronic.cfg.embodiment import trossen
+from positronic.cfg.hardware.roboarm import TROSSEN_NOMINAL_JOINTS
+from positronic.drivers.roboarm import command
+from positronic.eval import Eval, Task
+from positronic.eval import keys as eval_keys
+
+PARTS_TASK = 'Pick the parts one by one from the table and put them into the transparent box.'
+
+
+def _trossen_trial(instruction: str, timeout: float | None, meta: dict[str, Any] | None = None) -> Task:
+ """One trial on the Trossen station. A person lays the parts out before it, so the trial asks for no scene.
+
+ The arm opens every trial at the pose the operator's demonstrations open at: it rests on the lower limit
+ of two joints, where half the directions out of it have no solution.
+ """
+ return Task(
+ instruction_source=instruction,
+ timeout_sec=timeout,
+ prepare_args={eval_keys.ARM: command.JointPosition(np.asarray(TROSSEN_NOMINAL_JOINTS, dtype=np.float64))},
+ meta=meta or {},
+ )
+
+
+@cfn.config(instruction=PARTS_TASK, timeout=None)
+def attended_trials(instruction: str, timeout: float | None) -> Callable[[], Task]:
+ """The trials an attended Trossen run gets, one per press. Without a ``timeout`` the operator ends the
+ episode."""
+ return partial(_trossen_trial, instruction, timeout)
+
+
+def _planned_trials(instruction: str, timeout: float | None, trial_count: int) -> list[Task]:
+ return [
+ _trossen_trial(instruction, timeout, {eval_keys.TRIAL_INDEX: trial, eval_keys.TRIAL_COUNT: trial_count})
+ for trial in range(trial_count)
+ ]
+
+
+@cfn.config(embodiment=trossen, instruction=PARTS_TASK, timeout=120, trial_count=1)
+def pick_place(embodiment, instruction: str, timeout: float | None, trial_count: int) -> Eval:
+ """A real Trossen pick-and-place eval. The outcome is the operator's annotation: a real rig has no
+ ground truth to compute one from."""
+ return Eval(embodiment, partial(_planned_trials, instruction, timeout, trial_count))
diff --git a/positronic/cfg/hardware/camera.py b/positronic/cfg/hardware/camera.py
index 07acc4b5a..266c237db 100644
--- a/positronic/cfg/hardware/camera.py
+++ b/positronic/cfg/hardware/camera.py
@@ -24,6 +24,35 @@ def linux_video(**kwargs):
)
+# rules-allow: primitive-type — a device path is the `str` `linuxpy.Device` takes and `LinuxVideo` hands
+# it, spelled the way the arducam configurations above spell theirs
+# The four RealSense D405 of the Trossen station. A D405 enumerates as a plain UVC device with six video
+# nodes, of which `-video-index4` carries the colour stream. The serial in the link is the USB one, which
+# is not the serial the RealSense SDK reports for the same camera.
+_D405_DEVICE_PATH_PREFIX = (
+ '/dev/v4l/by-id/usb-Intel_R__RealSense_TM__Depth_Camera_405_Intel_R__RealSense_TM__Depth_Camera_405_'
+)
+
+d405_wrist_left = linux_video.override(
+ device_path=f'{_D405_DEVICE_PATH_PREFIX}251323070021-video-index4',
+ width=640,
+ height=480,
+ fps=30,
+ pixel_format='YUYV',
+)
+
+d405_wrist_right = d405_wrist_left.override(device_path=f'{_D405_DEVICE_PATH_PREFIX}251323070565-video-index4')
+
+d405_scene_top = d405_wrist_left.override(device_path=f'{_D405_DEVICE_PATH_PREFIX}260323072626-video-index4')
+
+d405_scene_bottom = d405_wrist_left.override(device_path=f'{_D405_DEVICE_PATH_PREFIX}260323072970-video-index4')
+
+# The views the station records a demonstration with: the wrist camera of the arm it drives, and the one
+# that looks down on the table. The other two belong to the other arm. Data collection and the embodiment
+# read this one mapping, because a policy is fed what it was trained on or it is fed nothing.
+TROSSEN_STATION_VIEWS = {keys.WRIST_IMAGE: d405_wrist_right, keys.EXTERIOR_IMAGE: d405_scene_top}
+
+
@cfn.config()
def zed(**kwargs):
from positronic.drivers.camera.zed import SLCamera
diff --git a/positronic/cfg/hardware/roboarm/__init__.py b/positronic/cfg/hardware/roboarm/__init__.py
index ec2779737..82e2e6476 100644
--- a/positronic/cfg/hardware/roboarm/__init__.py
+++ b/positronic/cfg/hardware/roboarm/__init__.py
@@ -7,6 +7,12 @@
FRANKA_NOMINAL_JOINTS = [0.0, -0.31, 0.0, -1.65, 0.0, 1.522, 0.0]
YAM_NOMINAL_JOINTS = [0.0, 1.047, 1.047, 0.0, 0.0, 0.0]
SO101_NOMINAL_JOINTS = [0.0, 0.0, 0.0, 0.0, 0.0]
+# The Trossen rests on the lower limit of joints 1 and 2, where half the directions out of it have no
+# solution at all. Its start pose is mid-range on every joint instead, end effector at [0.503, 0, 0.232].
+TROSSEN_NOMINAL_JOINTS = [0.0, 1.571, 1.178, 0.0, 0.0, 0.0]
+# Where the Trossen rests: every joint at zero, which is the lower limit of joints 1 and 2. The arm holds
+# itself there without the controller, so it is where a session leaves it.
+TROSSEN_PARK_JOINTS = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
# How far, per joint, a start pose drawn around the Franka's nominal may sit from it.
FRANKA_JOINTS_SPREAD = [0.03, 0.05, 0.08, 0.08, 0.10, 0.10, 0.10]
# The gains DROID's Franka ran, which its pretrained checkpoints were trained under.
@@ -76,3 +82,23 @@ def yam(channel: str, sim: bool, base_pose):
from positronic.drivers.roboarm.yam import Robot
return Robot(channel, base_pose=base_pose, sim=sim)
+
+
+@cfn.config(ip='192.168.1.4')
+def trossen(ip: str):
+ from positronic.drivers.roboarm.trossen import Robot
+
+ return Robot(ip=ip)
+
+
+# What the controller cancels of the gripper's own friction on a leader the operator holds, in N. Felt at
+# the rig on the leader at 192.168.1.2, which is calibrated at 5.77: at 10.02 the trigger moves under a
+# finger and still holds where it is left. Another arm may want its own.
+TROSSEN_LEADER_GRIPPER_FRICTION = 10.02
+
+
+@cfn.config(ip='192.168.1.2', force_feedback_gain=0.0, gripper_friction_constant=TROSSEN_LEADER_GRIPPER_FRICTION)
+def trossen_leader(ip: str, force_feedback_gain: float, gripper_friction_constant: float | None):
+ from positronic.drivers.roboarm.trossen_leader import Leader
+
+ return Leader(ip=ip, force_feedback_gain=force_feedback_gain, gripper_friction_constant=gripper_friction_constant)
diff --git a/positronic/data_collection.py b/positronic/data_collection.py
index 3319233a4..56b65250d 100644
--- a/positronic/data_collection.py
+++ b/positronic/data_collection.py
@@ -23,6 +23,7 @@
from positronic.dataset.local_dataset import LocalDatasetWriter
from positronic.dataset.serializers import Serializers
from positronic.drivers import roboarm
+from positronic.drivers.keyboard import KeyboardControl
from positronic.drivers.roboarm import State as RoboarmState
from positronic.drivers.webxr import WebXR
from positronic.gui import dpg_ui
@@ -32,11 +33,18 @@
from positronic.utils.buttons import ButtonHandler
-def _parse_buttons(buttons: dict, button_handler: ButtonHandler):
+def _parse_buttons(buttons: dict, button_handler: ButtonHandler) -> set[str]:
+ """Feed the controllers' buttons to ``button_handler``, and answer with the hands that sent any.
+
+ A controller the headset has lost sends nothing at all, and the handler raises when asked for a button
+ it has never seen — so the hands named here are exactly the ones a caller may ask about.
+ """
+ hands = set()
for side in ['left', 'right']:
if buttons[side] is None:
continue
+ hands.add(side)
mapping = {
f'{side}_A': buttons[side][4],
f'{side}_B': buttons[side][5],
@@ -45,10 +53,14 @@ def _parse_buttons(buttons: dict, button_handler: ButtonHandler):
f'{side}_stick': buttons[side][3],
}
button_handler.update_buttons(mapping)
+ return hands
-def _check_error(is_error, was_error):
- return is_error, is_error and not was_error
+# Where the hand ends and its shake begins. Measured over three minutes of teleoperation: 84% of the
+# controller's movement sits below 1 Hz, 2% between 8 and 15 Hz — the tremor a hand always carries — and
+# 8% above 15 Hz, which no hand does and the pose stream brings on its own. A cut here keeps 88% of the
+# movement, and costs the operator 32 ms of lag.
+_HAND_CUTOFF_HZ = 5.0
class _Tracker:
@@ -59,6 +71,8 @@ class _Tracker:
def __init__(self, operator_position: geom.Transform3D | None):
self._operator_position = operator_position
self.on = self.umi_mode
+ self._steady: geom.Transform3D | None = None
+ self._steady_at = 0
@property
def umi_mode(self):
@@ -82,16 +96,92 @@ def turn_off(self):
self.on = False
logging.info('Stopped tracking')
- def update(self, tracker_pos: geom.Transform3D):
- if self.umi_mode:
+ def _steadied(self, pose: geom.Transform3D, ts_ns: int) -> geom.Transform3D:
+ """``pose`` with the hand's own shake taken out, as a first-order lag on both halves.
+
+ A pose that is not a number is dropped rather than filtered: the lag keeps what it is given, so one
+ NaN would sit in it and every pose after it would come back NaN until the run was started again.
+ """
+ if not np.all(np.isfinite(pose.translation)) or not np.all(np.isfinite(pose.rotation.as_quat)):
+ logging.warning('The headset sent a pose that is not a number, and it is dropped')
+ return self._steady if self._steady is not None else pose
+ if self._steady is None:
+ self._steady, self._steady_at = pose, ts_ns
+ return pose
+ step = max(ts_ns - self._steady_at, 0) / 1e9
+ self._steady_at = ts_ns
+ share = step / (step + 1.0 / (2.0 * np.pi * _HAND_CUTOFF_HZ))
+ turn = (self._steady.rotation.inv * pose.rotation).as_rotvec
+ # A quaternion and its negative are the same turn, and `as_rotvec` reads the negative one as
+ # nearly a full turn the other way. The hand went the short way round.
+ if (angle := float(np.linalg.norm(turn))) > np.pi:
+ turn = turn * (1.0 - 2.0 * np.pi / angle)
+ self._steady = geom.Transform3D(
+ self._steady.translation + share * (pose.translation - self._steady.translation),
+ self._steady.rotation * geom.Rotation.from_rotvec(share * turn),
+ )
+ return self._steady
+
+ def update(self, tracker_pos: geom.Transform3D, ts_ns: int):
+ if self._operator_position is None: # `umi_mode`, said so the operator's frame reads as the pose it is
return tracker_pos
- self._teleop_t = self._operator_position * tracker_pos * self._operator_position.inv
+ steady = self._steadied(tracker_pos, ts_ns)
+ self._teleop_t = self._operator_position * steady * self._operator_position.inv
return geom.Transform3D(
self._teleop_t.translation + self._offset.translation, self._teleop_t.rotation * self._offset.rotation
)
+class Teleop(Enum):
+ """What the operator moves to drive the arm."""
+
+ # A hand tracked in space. The arm follows the pose of the controller in that hand, and the operator
+ # starts and stops the tracking with that controller's own `A`.
+ HAND = 'hand'
+ # A leader arm the operator holds. The follower stands at the joints the leader reads — nothing is
+ # solved in between — and starts following once the two arms have met.
+ LEADER = 'leader'
+
+
+# How closely the leader and the follower must stand before the follower starts copying it, per joint. The
+# follower takes up whatever is left as one streamed step, so this is the largest jump engaging can make.
+_MEET_RAD = 0.1
+
+
+class _Follow:
+ """The follower waiting for the leader it copies to come to it.
+
+ Asking the operator to line the arms up and then press something takes their word for it, and a press
+ with the arms apart moves the follower the whole way at once. This engages on the fact instead: the
+ arms are together, or the follower is not following.
+ """
+
+ def __init__(self) -> None:
+ self.on = False
+ self._armed = False
+
+ def arm(self) -> None:
+ """Let the follower take up its leader again, once the session has put both where they stand."""
+ self._armed = True
+
+ def turn_off(self) -> None:
+ if self.on:
+ logging.info('The arm stopped following its leader')
+ self.on = False
+ self._armed = False
+
+ def take_up(self, leader: np.ndarray, follower: np.ndarray) -> None:
+ """Let the follower copy the leader, once the session has armed it and every joint of the two is
+ within ``_MEET_RAD`` of the other's. It goes on copying until something turns it off, and an arm
+ the operator has not asked for holds still however close the two stand."""
+ if not self._armed or self.on:
+ return
+ if leader.shape == follower.shape and np.max(np.abs(leader - follower)) <= _MEET_RAD:
+ self.on = True
+ logging.info('The arm met its leader and follows it now')
+
+
class OperatorPosition(Enum):
# map xyz -> zxy
FRONT = geom.Transform3D(rotation=geom.Rotation.from_quat([0.5, 0.5, 0.5, 0.5]))
@@ -99,27 +189,59 @@ class OperatorPosition(Enum):
BACK = geom.Transform3D(rotation=geom.Rotation.from_quat([-0.5, -0.5, 0.5, 0.5]))
+class SessionEvent(Enum):
+ """What the operator asks of the session itself, by whatever device is to hand."""
+
+ # Start the recording, or stop the one that runs.
+ RECORD = 'record'
+ # Put the arm back at its start pose, and abandon the recording that runs.
+ READY = 'ready'
+ # Put the arm where it rests, and abandon the recording that runs.
+ PARK = 'park'
+
+
+# What a session plays at the operator, from the sounds the package carries.
+_SOUNDS = Path(package_assets_path('assets/sounds'))
+_RECORDING_STARTED = _SOUNDS / 'recording-has-started.wav'
+_RECORDING_STOPPED = _SOUNDS / 'recording-has-stopped.wav'
+_RECORDING_ABANDONED = _SOUNDS / 'recording-has-been-aborted.wav'
+_SOMETHING_WENT_WRONG = _SOUNDS / 'error-occurred.wav'
+
+
class DataCollectionController(pimm.ControlSystem):
def __init__(
self,
operator_position: geom.Transform3D | None,
nominal_joints: Sequence[float] | np.ndarray,
joints_spread: Sequence[float] | np.ndarray = (),
+ park_joints: Sequence[float] | np.ndarray = (),
*,
+ teleop: Teleop = Teleop.HAND,
output_path: Path | None = None,
static_meta: dict | None = None,
metadata_getter: Callable[[], dict] | None = None,
):
self.operator_position = operator_position
+ self.teleop = teleop
self._output_path = output_path
self._nominal_joints = np.asarray(nominal_joints, dtype=np.float64)
# A station that measured no jitter sends the arm exactly to its nominal.
spread = joints_spread if len(joints_spread) else np.zeros_like(self._nominal_joints)
self._joints_spread = np.asarray(spread, dtype=np.float64)
+ self._park_joints = np.asarray(park_joints, dtype=np.float64)
self._static_meta = static_meta or {}
self.metadata_getter = metadata_getter or (lambda: {})
self.controller_positions = pimm.DefaultingReceiver(self, default={})
- self.buttons_receiver = pimm.ControlSystemReceiver(self)
+ # An arm driven by a leader has no controller to press, and the session runs off `session_events`
+ # instead — so no buttons is a rig without them, not a rig whose operator has not pressed yet.
+ self.buttons_receiver = pimm.DefaultingReceiver(self, default={'left': None, 'right': None})
+ # What the arm last read as: it holds still until a reset clears the error, so nothing is sent
+ self.arm_in_error = False
+ # A keyboard carries these on a rig whose operator holds the leader.
+ self.session_events = pimm.ControlSystemReceiver[SessionEvent](self)
+ # What the leader publishes, on a rig driven by one.
+ self.leader_joints = pimm.ControlSystemReceiver[np.ndarray](self)
+ self.leader_grip = pimm.ControlSystemReceiver[float](self)
self.robot_state = pimm.ControlSystemReceiver(self)
self.gripper_state = pimm.FakeReceiver(self) # To make compatible with other "policy" control systems
self.frames = pimm.ReceiverDict(self, fake=True)
@@ -127,90 +249,215 @@ def __init__(
self.robot_commands = pimm.ControlSystemEmitter(self)
self.sync_move = pimm.calls.ControlSystemCaller[roboarm.command.CommandType, None](self)
+ # The leader travels to the poses the follower is sent to, so the two arms stand together when the
+ # operator takes over. A leader left behind hands the follower the whole gap as its first step.
+ self.leader_move = pimm.calls.ControlSystemCaller[roboarm.command.CommandType, None](self)
self.redraw_scene = pimm.calls.ControlSystemCaller[Any, None](self)
self.target_grip = pimm.ControlSystemEmitter(self)
self.ds_agent_commands = pimm.ControlSystemEmitter(self)
self.sound = pimm.ControlSystemEmitter(self)
- def _ready(self, should_stop: pimm.SignalReceiver) -> Iterator[pimm.Sleep]:
- """Redraw the scene and put the arm at a start pose drawn around the nominal joints, yielding until
- both are done.
+ @property
+ def _umi(self) -> bool:
+ """Whether a hand's poses reach the arm as they are, with no tracking to start or stop and no
+ start pose to put anything back at. A leader is never that: it is an arm, and it has one."""
+ return self.teleop is Teleop.HAND and self.operator_position is None
+
+ def _travel(
+ self, target: roboarm.command.CommandType, should_stop: pimm.SignalReceiver, asks: Sequence[Any] = ()
+ ) -> Iterator[pimm.Sleep]:
+ """Take every arm of the rig to ``target``, yielding until it and everything in ``asks`` is done.
Raises whatever a device failed on: a call hands its handler's exception back, so the vocabulary is
the driver's — a move abandoned, a target it cannot hold, a fault from the vendor.
"""
- logging.info('Readying the rig for the next episode')
- asks = []
- if self.redraw_scene.connected: # a real scene is a person's to set up, and nothing here is asked
- asks.append(self.redraw_scene(None))
- if len(self._nominal_joints): # a station with no arm has none to put anywhere
- asks.append(self.sync_move(roboarm.command.sampled_joints(self._nominal_joints, self._joints_spread)))
+ asks = list(asks)
+ for move in (self.sync_move, self.leader_move):
+ if move.connected: # a rig without the arm has none to put anywhere
+ asks.append(move(target))
ready = pimm.calls.all_of(asks)
while not ready.done():
if should_stop.value:
return
+ self._drop_readings()
yield pimm.Sleep(0.001)
ready.result()
- def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]: # noqa: C901
- sounds = Path(package_assets_path('assets/sounds'))
- start_wav_path = sounds / 'recording-has-started.wav'
- end_wav_path = sounds / 'recording-has-stopped.wav'
- abort_wav_path = sounds / 'recording-has-been-aborted.wav'
- error_wav_path = sounds / 'error-occurred.wav'
+ def _drop_readings(self) -> None:
+ """Drop what reached the rig while it travels: those readings are of a rig that has since moved.
+
+ A reading taken before a move says where an arm stood then, and the tick after the move takes what
+ it reads for the present. A follower would meet a leader that has gone, and be sent to the pose the
+ two of them stood at. What the operator asks for in front of a travelling arm goes the same way:
+ the press is impatience with this move, and holding it would run the move again.
+ """
+ for port in (
+ self.session_events,
+ self.buttons_receiver,
+ self.controller_positions,
+ self.robot_state,
+ self.leader_joints,
+ self.leader_grip,
+ ):
+ port.read()
+
+ def _ready(self, source: Any, should_stop: pimm.SignalReceiver) -> Iterator[pimm.Sleep]:
+ """Stop the tracking, redraw the scene, and put every arm at a start pose drawn around the nominal
+ joints, yielding until all of them are done."""
+ source.turn_off()
+ logging.info('Readying the rig for the next episode')
+ # A real scene is a person's to set up, and nothing here is asked.
+ scene = [self.redraw_scene(None)] if self.redraw_scene.connected else []
+ try:
+ yield from self._travel(
+ roboarm.command.sampled_joints(self._nominal_joints, self._joints_spread), should_stop, scene
+ )
+ if isinstance(source, _Follow):
+ source.arm()
+ # rules-allow: swallowed-error — the operator hears it and asks again, session goes on
+ except Exception as e:
+ logging.error(f'The rig was not readied: {e}')
+ self.sound.emit(_SOMETHING_WENT_WRONG)
+
+ def _park(self, source: Any, should_stop: pimm.SignalReceiver) -> Iterator[pimm.Sleep]:
+ """Stop the tracking and put every arm of the rig where it rests, yielding until all of them are
+ there."""
+ source.turn_off()
+ if not len(self._park_joints):
+ logging.warning('This rig names no pose to rest at, so there is nowhere to park it')
+ return
+ logging.info('Taking the rig to rest')
+ try:
+ yield from self._travel(roboarm.command.JointPosition(self._park_joints), should_stop)
+ # rules-allow: swallowed-error — the operator hears it and asks again, session goes on
+ except Exception as e:
+ logging.error(f'The rig was not parked: {e}')
+ self.sound.emit(_SOMETHING_WENT_WRONG)
+
+ def _forget_presses(self, buttons: ButtonHandler) -> None:
+ """Take the buttons as they stand, so a press made in front of a travelling arm is not read as new.
+
+ ``_drop_readings`` lets the messages go, and the handler reports an edge against the last reading it
+ saw: a button released before a travel and held during it would read as pressed the moment it ends.
+ """
+ _parse_buttons(self.buttons_receiver.value, buttons)
- tracker = _Tracker(self.operator_position)
+ def _abandon(self, recording: bool) -> None:
+ """Give up the recording that runs, if one does."""
+ if not recording:
+ return
+ self.ds_agent_commands.emit(DsWriterCommand.ABORT())
+ self.sound.emit(_RECORDING_ABANDONED)
+ logging.info('The recording was abandoned')
+
+ def _record(self, recording: bool) -> None:
+ """Start a recording, or stop the one that runs."""
+ if recording:
+ self.ds_agent_commands.emit(DsWriterCommand.STOP())
+ self.sound.emit(_RECORDING_STOPPED)
+ logging.info('The recording stopped')
+ else:
+ meta = dict(self._static_meta)
+ meta.update(self.robot_meta_in.value)
+ meta.update(self.metadata_getter())
+ self.ds_agent_commands.emit(DsWriterCommand.START(self._output_path, meta))
+ self.sound.emit(_RECORDING_STARTED)
+ logging.info('The recording started')
+
+ def _tracked(self, source: _Tracker, state: pimm.Message[RoboarmState] | None) -> None:
+ """Follow the hand, or stop following it. An arm that has not said where it stands cannot be left."""
+ if source.on:
+ source.turn_off()
+ elif state is None:
+ logging.warning('The arm has not said where it stands, so there is nothing to follow from')
+ else:
+ source.turn_on(state.data.ee_pose)
+ logging.info('Tracking is %s', 'on' if source.on else 'off')
+
+ def _from_hand(self, tracker: _Tracker, hands: set[str], buttons: ButtonHandler) -> tuple[Any, float | None]:
+ """What the hand asks the arm for, and what it holds the grip at.
+
+ The tracker is fed whether or not the arm is tracking: what the hand did while it was not is how
+ far the arm would jump the moment it starts, and the offset is measured off it.
+ """
+ grip = buttons.get_value('right_trigger') if 'right' in hands else None
+ cp_msg = self.controller_positions.read()
+ if cp_msg.updated and cp_msg.data.get('right') is not None:
+ pose = tracker.update(cp_msg.data['right'], cp_msg.ts)
+ return roboarm.command.CartesianPosition(pose), grip
+ return None, grip
+
+ def _from_leader(self, follow: _Follow, state: pimm.Message[RoboarmState] | None) -> tuple[Any, float | None]:
+ """What the leader asks its follower for, and what its trigger holds the grip at.
+
+ The joints go over as they read: a leader and its follower are the same arm, so there is nothing
+ to solve between them.
+ """
+ held = self.leader_grip.read()
+ grip = float(held.data) if held is not None and held.updated else None
+ joints = self.leader_joints.read()
+ # The arms are judged to have met on a reading the leader has just sent. A reading that repeats
+ # says where the leader was when it sent it, and the arm may have been driven somewhere since.
+ if joints is None or state is None or not joints.updated:
+ return None, grip
+ leader = np.asarray(joints.data, dtype=np.float64)
+ follow.take_up(leader, np.asarray(state.data.q, dtype=np.float64))
+ if not follow.on:
+ return None, grip
+ return roboarm.command.JointPosition(leader), grip
+
+ def _note_error(self, state: pimm.Message[RoboarmState]) -> None:
+ """Follow the arm's error state, and tell the operator on the tick it starts.
+
+ The state is ``self.arm_in_error``; an arm in error holds still until a reset clears it.
+ """
+ was_error, self.arm_in_error = self.arm_in_error, state.data.status == roboarm.RobotStatus.ERROR
+ if self.arm_in_error and not was_error:
+ logging.error('The arm is in error. It holds still until a reset clears the error.')
+ self.sound.emit(_SOMETHING_WENT_WRONG)
+
+ def _asked_of_the_arm(
+ self, source: Any, hands: set[str], buttons: ButtonHandler, state: pimm.Message[RoboarmState] | None
+ ) -> tuple[Any, float | None]:
+ """What the operator asks the arm for, through whichever device drives it."""
+ if isinstance(source, _Tracker):
+ return self._from_hand(source, hands, buttons)
+ return self._from_leader(source, state)
+
+ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]:
+ source = _Tracker(self.operator_position) if self.teleop is Teleop.HAND else _Follow()
button_handler = ButtonHandler()
recording = False
- in_error = False
while not should_stop.value:
try:
- _parse_buttons(self.buttons_receiver.value, button_handler)
- if button_handler.just_pressed('right_B'):
- if not recording:
- meta = dict(self._static_meta)
- meta.update(self.robot_meta_in.value)
- meta.update(self.metadata_getter())
- self.ds_agent_commands.emit(DsWriterCommand.START(self._output_path, meta))
- self.sound.emit(start_wav_path)
- else:
- self.ds_agent_commands.emit(DsWriterCommand.STOP())
- self.sound.emit(end_wav_path)
+ hands = _parse_buttons(self.buttons_receiver.value, button_handler)
+ state = self.robot_state.read()
+ asked = pimm.value_updated(self.session_events)
+ if button_handler.just_pressed('right_B') or asked is SessionEvent.RECORD:
+ self._record(recording)
recording = not recording
- elif button_handler.just_pressed('right_A'):
- if tracker.on:
- tracker.turn_off()
- else:
- tracker.turn_on(self.robot_state.value.ee_pose)
- elif button_handler.just_pressed('right_stick') and not tracker.umi_mode:
- if recording:
- self.ds_agent_commands.emit(DsWriterCommand.ABORT())
- self.sound.emit(abort_wav_path)
- tracker.turn_off()
+ elif isinstance(source, _Tracker) and button_handler.just_pressed('right_A'):
+ self._tracked(source, state)
+ elif (button_handler.just_pressed('right_stick') or asked is SessionEvent.READY) and not self._umi:
+ self._abandon(recording)
+ recording = False
+ yield from self._ready(source, should_stop)
+ self._forget_presses(button_handler)
+ elif asked is SessionEvent.PARK:
+ self._abandon(recording)
recording = False
- try:
- yield from self._ready(should_stop)
- # rules-allow: swallowed-error — the operator hears it and asks again, session goes on
- except Exception as e:
- logging.error(f'The rig was not readied: {e}')
- self.sound.emit(error_wav_path)
-
- self.target_grip.emit(button_handler.get_value('right_trigger'))
- cp_msg = self.controller_positions.read()
- if cp_msg.updated:
- target_robot_pos = tracker.update(cp_msg.data['right'])
-
- if tracker.on:
- in_error, entered_error = _check_error(
- self.robot_state.value.status == roboarm.RobotStatus.ERROR, in_error
- )
- if entered_error:
- self.sound.emit(error_wav_path)
- if not in_error and cp_msg.updated:
- cmd = roboarm.command.CartesianPosition(target_robot_pos)
+ yield from self._park(source, should_stop)
+ self._forget_presses(button_handler)
+
+ cmd, grip = self._asked_of_the_arm(source, hands, button_handler, state)
+ if grip is not None:
+ self.target_grip.emit(grip)
+ if source.on and state is not None:
+ self._note_error(state)
+ if not self.arm_in_error and cmd is not None:
self.robot_commands.emit(cmd)
yield pimm.Sleep(0.001)
@@ -220,6 +467,16 @@ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[p
continue
+# The key that asks for each. An operator holding the leader has no hand free for these, so a second
+# person presses them; a pedal under the operator's own foot would ask for the same ones.
+_SESSION_KEYS = {'r': SessionEvent.RECORD, ' ': SessionEvent.READY, 'h': SessionEvent.PARK}
+
+
+def _session_event(key: str) -> SessionEvent | None:
+ """What a keystroke asks of the session, or nothing for a key that asks for nothing."""
+ return _SESSION_KEYS.get(key)
+
+
def controller_positions_serializer(controller_positions: dict[str, geom.Transform3D]) -> dict[str, np.ndarray]:
res = {}
for side, pos in controller_positions.items():
@@ -238,12 +495,26 @@ def _wire(
world: pimm.World,
ds_agent: DsWriterAgent | None,
data_collection: DataCollectionController,
- webxr: WebXR,
+ webxr: WebXR | None,
robot_arm: pimm.ControlSystem | None,
sound: pimm.ControlSystem | None,
+ leader: pimm.ControlSystem | None = None,
+ keyboard: KeyboardControl | None = None,
):
- world.connect(webxr.controller_positions, data_collection.controller_positions)
- world.connect(webxr.buttons, data_collection.buttons_receiver)
+ if webxr is not None:
+ world.connect(webxr.controller_positions, data_collection.controller_positions)
+ world.connect(webxr.buttons, data_collection.buttons_receiver)
+
+ if leader is not None:
+ # A driver's ports are its own: ``pimm.ControlSystem`` declares none for the checker to find.
+ world.connect(leader.joints, data_collection.leader_joints) # pyright: ignore[reportAttributeAccessIssue]
+ world.connect(leader.grip, data_collection.leader_grip) # pyright: ignore[reportAttributeAccessIssue]
+ world.connect(data_collection.leader_move, leader.sync_move) # pyright: ignore[reportAttributeAccessIssue]
+
+ if keyboard is not None:
+ world.connect(
+ keyboard.keyboard_inputs, data_collection.session_events, receiver_wrapper=pimm.map(_session_event)
+ )
if robot_arm is not None:
# A driver's ports are its own: ``pimm.ControlSystem`` declares none for the checker to find.
@@ -255,7 +526,9 @@ def _wire(
world.connect(robot_arm.state, sound.level, receiver_wrapper=pimm.map(_wrench_to_level))
if ds_agent is not None:
- if robot_arm is not None:
+ # A leader's joints are not recorded beside these: the command the follower is given is those very
+ # joints, and it is recorded already.
+ if robot_arm is not None and webxr is not None:
ds_agent.add_signal('controller_positions', controller_positions_serializer)
world.connect(webxr.controller_positions, ds_agent.inputs['controller_positions'])
world.connect(data_collection.ds_agent_commands, ds_agent.command)
@@ -263,15 +536,67 @@ def _wire(
return ds_agent
+def _frame_array(frame: pimm.shared_memory.NumpySMAdapter) -> np.ndarray:
+ return frame.array
+
+
+def _check_rig(
+ robot_arm: pimm.ControlSystem | None,
+ webxr: WebXR | None,
+ leader: pimm.ControlSystem | None,
+ nominal_joints: Sequence[float],
+ joints_spread: Sequence[float],
+ park_joints: Sequence[float],
+) -> None:
+ """Refuse a rig that cannot run, before any of it is started.
+
+ Every one of these reaches the operator as a failed move or an arm that does nothing, long after the
+ run began and with nothing to say which part of the configuration was wrong.
+ """
+ if (robot_arm is not None) != (len(nominal_joints) > 0):
+ raise ValueError(
+ '--robot_arm and --nominal_joints are named together or not at all: the right stick puts the arm '
+ 'a station has at the pose it measured, and either one alone leaves the other with nothing'
+ )
+ if len(park_joints) not in (0, len(nominal_joints)):
+ raise ValueError(
+ f'--park_joints names {len(park_joints)} joints and --nominal_joints names {len(nominal_joints)}: '
+ 'both are poses of the same arm, so they name the same joints'
+ )
+ if len(joints_spread) not in (0, len(nominal_joints)):
+ raise ValueError(
+ f'--joints_spread names {len(joints_spread)} joints and --nominal_joints names {len(nominal_joints)}: '
+ 'the spread is jitter measured per joint, so it carries one value for each, or none at all'
+ )
+ if not np.all(np.isfinite([*nominal_joints, *joints_spread, *park_joints])):
+ raise ValueError(
+ '--nominal_joints, --joints_spread and --park_joints name joint angles: every value has to be '
+ 'finite, or the draw between them raises instead of reaching the arm'
+ )
+ if leader is not None and robot_arm is None:
+ raise ValueError('--leader is held to drive a follower, and one with nothing on the other end drives nothing')
+ if leader is not None and webxr is not None:
+ raise ValueError(
+ 'the arm is driven by a leader or by a hand tracked in space, not by both: --leader and --webxr '
+ 'would leave two things asking one arm to be in two places at once'
+ )
+
+
def main(
robot_arm: pimm.ControlSystem | None,
gripper: pimm.ControlSystem | None,
- webxr: WebXR,
+ webxr: WebXR | None,
sound: pimm.ControlSystem | None,
cameras: dict[str, pimm.ControlSystem] | None,
- # The start pose the right stick puts the arm at: drawn around ``nominal_joints``, within ``joints_spread``.
+ # The start pose the stick and the space key put the arm at: drawn around ``nominal_joints``, within
+ # ``joints_spread``.
nominal_joints: Sequence[float] = (),
joints_spread: Sequence[float] = (),
+ # The pose the `h` key takes every arm to, where it rests between sessions.
+ park_joints: Sequence[float] = (),
+ # The arm the operator holds. Naming it is what makes this a rig driven by a leader rather than by a
+ # hand tracked in space.
+ leader: pimm.ControlSystem | None = None,
output_dir: str | None = None,
stream_video_to_webxr: str | None = None,
operator_position: OperatorPosition = OperatorPosition.FRONT,
@@ -279,21 +604,7 @@ def main(
video_options: dict[str, str] | None = None,
):
"""Runs data collection in real hardware."""
- if (robot_arm is not None) != (len(nominal_joints) > 0):
- raise ValueError(
- '--robot_arm and --nominal_joints are named together or not at all: the right stick puts the arm '
- 'a station has at the pose it measured, and either one alone leaves the other with nothing'
- )
- if len(joints_spread) not in (0, len(nominal_joints)):
- raise ValueError(
- f'--joints_spread names {len(joints_spread)} joints and --nominal_joints names {len(nominal_joints)}: '
- 'the spread is jitter measured per joint, so it carries one value for each, or none at all'
- )
- if not np.all(np.isfinite([*nominal_joints, *joints_spread])):
- raise ValueError(
- '--nominal_joints and --joints_spread name joint angles: every value has to be finite, or the '
- 'draw between them raises instead of reaching the arm'
- )
+ _check_rig(robot_arm, webxr, leader, nominal_joints, joints_spread, park_joints)
camera_instances = cameras or {}
camera_emitters = {name: cam.frame for name, cam in camera_instances.items()}
static_meta = {}
@@ -305,27 +616,37 @@ def main(
if output_dir is not None:
output_path = pos3.sync(output_dir, sync_on_error=True)
utils.save_run_metadata(output_path, patterns=['*.py', '*.toml'])
+ # An operator with a hand on the leader has none free for the session, so the keys carry it.
+ keyboard = KeyboardControl(quit_key='q') if leader is not None else None
data_collection = DataCollectionController(
- operator_position.value, nominal_joints, joints_spread, output_path=output_path, static_meta=static_meta
+ operator_position.value,
+ nominal_joints,
+ joints_spread,
+ park_joints,
+ teleop=Teleop.LEADER if leader is not None else Teleop.HAND,
+ output_path=output_path,
+ static_meta=static_meta,
)
dataset_factory = partial(LocalDatasetWriter, video_options=video_options) if output_path is not None else None
with pimm.World() as world:
ds_agent = wire.wire(world, data_collection, dataset_factory, camera_emitters, robot_arm, gripper, None)
- _wire(world, ds_agent, data_collection, webxr, robot_arm, sound)
+ _wire(world, ds_agent, data_collection, webxr, robot_arm, sound, leader, keyboard)
# SO-101 fills both the arm and gripper slots with one object; a control system runs in exactly one process.
gripper_cs = [] if gripper is robot_arm else [gripper]
- bg_cs = [webxr, *camera_instances.values(), ds_agent, robot_arm, *gripper_cs, sound]
+ bg_cs = [webxr, *camera_instances.values(), ds_agent, robot_arm, *gripper_cs, leader, sound]
- if stream_video_to_webxr is not None:
- world.connect(
- camera_emitters[stream_video_to_webxr],
- webxr.frame,
- receiver_wrapper=pimm.map(lambda adapter: adapter.array),
- )
+ if stream_video_to_webxr is not None and webxr is not None:
+ world.connect(camera_emitters[stream_video_to_webxr], webxr.frame, receiver_wrapper=pimm.map(_frame_array))
- world.run(data_collection, bg_cs)
+ # The keyboard reads the terminal this was started from, which no spawned process holds.
+ try:
+ world.run([data_collection, keyboard], bg_cs)
+ # rules-allow: swallowed-error — an interrupt is how an operator ends a run, and the world has
+ # already stopped every process by the time it arrives here
+ except KeyboardInterrupt:
+ logging.info('The run ended on an interrupt')
@cfn.config(
@@ -442,6 +763,33 @@ def yamcfg(robot_arm, **kwargs):
main(robot_arm=robot_arm, gripper=robot_arm, **kwargs)
+@cfn.config(
+ robot_arm=positronic.cfg.hardware.roboarm.trossen,
+ webxr=positronic.cfg.webxr.oculus,
+ # The operator stands behind the arm, and it reaches away from them over the table. Solved from the
+ # rig: the controller moved away from the base runs along -z of the headset's frame, and only `BACK`
+ # takes that to +x of the arm's.
+ operator_position=OperatorPosition.BACK,
+ # The station has no audio device, so the operator reads the recording state off the terminal.
+ sound=None,
+ cameras=dict(positronic.cfg.hardware.camera.TROSSEN_STATION_VIEWS),
+ stream_video_to_webxr=keys.WRIST_IMAGE,
+ nominal_joints=positronic.cfg.hardware.roboarm.TROSSEN_NOMINAL_JOINTS,
+ park_joints=positronic.cfg.hardware.roboarm.TROSSEN_PARK_JOINTS,
+)
+def trossencfg(robot_arm, **kwargs):
+ """Runs data collection on a real Trossen WidowX AI arm (the arm driver carries the gripper)."""
+ main(robot_arm=robot_arm, gripper=robot_arm, **kwargs)
+
+
+# The same arm, driven by the leader arm beside it instead of from the headset. There is nothing to wear:
+# the operator holds the leader, and the keys carry the session — `r` records, space puts both arms at the
+# start pose, `h` takes them to rest, and `q` ends the run.
+trossen_leader = trossencfg.override(
+ leader=positronic.cfg.hardware.roboarm.trossen_leader, webxr=None, stream_video_to_webxr=None
+)
+
+
droid = cfn.Config(
main,
robot_arm=positronic.cfg.hardware.roboarm.franka_droid,
@@ -478,6 +826,8 @@ def _internal_main():
'real': main_cfg,
'so101': so101cfg,
'yam': yamcfg,
+ 'trossen': trossencfg,
+ 'trossen_leader': trossen_leader,
'sim': main_sim,
'sim_pnp': main_sim.override(loaders=positronic.cfg.simulator.multi_tote_loaders),
'droid': droid,
diff --git a/positronic/drivers/camera/linux_video.py b/positronic/drivers/camera/linux_video.py
index c982f10bc..43cc6cac2 100644
--- a/positronic/drivers/camera/linux_video.py
+++ b/positronic/drivers/camera/linux_video.py
@@ -1,3 +1,4 @@
+import logging
from collections.abc import Iterator
import av
@@ -10,8 +11,20 @@
with vendor_import('linuxpy', 'Linux video capture', platforms=('linux',)):
from linuxpy.video.device import Device, PixelFormat
+logger = logging.getLogger(__name__)
+
class LinuxVideo(pimm.ControlSystem):
+ # The formats a camera may compress in, and what decodes each of them.
+ _CODECS = {
+ PixelFormat.H264: 'h264',
+ PixelFormat.HEVC: 'hevc',
+ PixelFormat.VP8: 'vp8',
+ PixelFormat.VP9: 'vp9',
+ PixelFormat.MPEG4: 'mpeg4',
+ PixelFormat.MJPEG: 'mjpeg',
+ }
+
def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_format: str):
self.device_path = device_path
self.width = width
@@ -19,20 +32,48 @@ def __init__(self, device_path: str, width: int, height: int, fps: int, pixel_fo
self.fps = fps
self.pixel_format = pixel_format
self.fps_counter = pimm.utils.RateCounter(f'LinuxVideo {device_path}')
- self.frame: pimm.SignalEmitter = pimm.ControlSystemEmitter(self)
-
- def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]: # noqa: C901
- codec_mapping = {
- PixelFormat.H264: 'h264',
- PixelFormat.HEVC: 'hevc',
- PixelFormat.VP8: 'vp8',
- PixelFormat.VP9: 'vp9',
- PixelFormat.MPEG4: 'mpeg4',
- PixelFormat.MJPEG: 'mjpeg',
- }
+ self.frame = pimm.ControlSystemEmitter[pimm.shared_memory.NumpySMAdapter](self)
+ self._frame_adapter = None # Lazy init
+
+ @staticmethod
+ def _framed(data: np.ndarray, frame, bytes_per_pixel: int) -> np.ndarray | None:
+ """``data`` shaped as the frame it belongs to, or ``None`` where it is not one frame's worth."""
+ if data.size != frame.height * frame.width * bytes_per_pixel:
+ return None
+ return data.reshape((frame.height, frame.width, bytes_per_pixel))
+
+ def _images(self, frame, codec_context) -> list[np.ndarray] | None:
+ """Every image the buffer ``frame`` carries, as RGB, or ``None`` where it is not one frame's worth.
+
+ A compressed buffer whole enough to read may still carry no image: the parser and the decoder both
+ hold data back until they have a frame to give, so an empty list is a wait, not a loss.
+ """
+ data = np.frombuffer(frame.data, dtype=np.uint8)
+ match frame.pixel_format:
+ case PixelFormat.YUYV:
+ raw = self._framed(data, frame, 2)
+ return None if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_YUYV)]
+ case PixelFormat.UYVY:
+ raw = self._framed(data, frame, 2)
+ return None if raw is None else [cv2.cvtColor(raw, cv2.COLOR_YUV2RGB_UYVY)]
+ case _ if frame.pixel_format in self._CODECS:
+ codec_ctx = codec_context(self._CODECS[frame.pixel_format])
+ # `av` types what it parses as `bytes` and carries `decode` on the subclasses of
+ # `CodecContext`, so the buffer the device hands over and the base class both read wrong.
+ packets = codec_ctx.parse(data) # pyright: ignore[reportArgumentType]
+ return [
+ decoded.to_ndarray(format='rgb24')
+ for packet in packets
+ for decoded in codec_ctx.decode(packet) # pyright: ignore[reportAttributeAccessIssue]
+ ]
+ case _:
+ raw = self._framed(data, frame, 3) # assume 3 bytes per pixel (RGB/BGR)
+ return None if raw is None else [raw]
+
+ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Sleep]:
codec_contexts = {}
- def get_codec_context(codec_name: str) -> av.CodecContext:
+ def codec_context(codec_name: str) -> av.CodecContext:
"""Lazily initialize and return codec context for given codec"""
if codec_name not in codec_contexts:
codec_contexts[codec_name] = av.CodecContext.create(codec_name, 'r')
@@ -44,40 +85,31 @@ def get_codec_context(codec_name: str) -> av.CodecContext:
device.set_format(device.info.buffers[0], self.width, self.height, self.pixel_format)
device.set_fps(device.info.buffers[0], self.fps)
+ misframed = overtaken = 0
for frame in device:
if should_stop.value:
break
- data = np.frombuffer(frame.data, dtype=np.uint8)
- result = None
-
- match frame.pixel_format:
- case PixelFormat.YUYV:
- data = data.reshape((frame.height, frame.width, 2))
- result = {'image': cv2.cvtColor(data, cv2.COLOR_YUV2RGB_YUYV)}
- case PixelFormat.UYVY:
- data = data.reshape((frame.height, frame.width, 2))
- result = {'image': cv2.cvtColor(data, cv2.COLOR_YUV2RGB_UYVY)}
- case _ if frame.pixel_format in codec_mapping:
- codec_name = codec_mapping[frame.pixel_format]
- codec_ctx = get_codec_context(codec_name)
- packets = codec_ctx.parse(data)
- for packet in packets:
- frames = codec_ctx.decode(packet)
- if len(frames) == 1:
- result = {'image': frames[0].to_ndarray(format='rgb24')}
- else:
- for i, decoded_frame in enumerate(frames):
- result[f'image_{i}'] = decoded_frame.to_ndarray(format='rgb24')
- case _:
- # Assume 3 bytes per pixel (RGB/BGR)
- rgb_data = data.reshape((frame.height, frame.width, 3))
- result = {'image': rgb_data}
-
- if result is not None:
- self.frame.emit(result)
+ images = self._images(frame, codec_context)
+ # rules-allow: swallowed-error — a busy bus hands over a buffer that is not a whole frame as
+ # ordinary traffic, roughly one per camera per minute of capture on the station's four D405;
+ # the next buffer is a thirtieth of a second away, and the count says how many went
+ if images is None:
+ misframed += 1
+ if misframed == 1:
+ logger.warning('%s handed over a buffer that is not one frame in size', self.device_path)
+ elif images:
+ # The port holds one image and nothing runs between two emissions of the same tick, so a
+ # buffer decoding to several has only its newest to give; the rest are counted, not sent.
+ overtaken += len(images) - 1
+ self._frame_adapter = pimm.shared_memory.NumpySMAdapter.lazy_init(images[-1], self._frame_adapter)
+ self.frame.emit(self._frame_adapter)
self.fps_counter.tick()
yield pimm.Yield() # Give control back to the world
+ if misframed:
+ logger.warning('%s handed over %d buffers that are not one frame in size', self.device_path, misframed)
+ if overtaken:
+ logger.warning('%s decoded %d images a newer one of the same buffer overtook', self.device_path, overtaken)
device.close()
diff --git a/positronic/drivers/camera/tests/conftest.py b/positronic/drivers/camera/tests/conftest.py
new file mode 100644
index 000000000..b2c8876c8
--- /dev/null
+++ b/positronic/drivers/camera/tests/conftest.py
@@ -0,0 +1,30 @@
+"""A stand-in for the vendor package the Linux video driver imports.
+
+``linuxpy`` ships only in the ``hardware`` extra, so the driver module cannot be imported from a default
+sync. The tests drive a device of their own, so a module carrying the names the driver binds at import is
+enough. Installed here, before any test module imports the driver, and only where the real package is
+absent.
+"""
+
+import importlib.util
+import sys
+import types
+from enum import Enum
+
+VENDOR = 'linuxpy'
+DEVICE_MODULE = f'{VENDOR}.video.device'
+
+if importlib.util.find_spec(VENDOR) is None:
+ # The formats the driver names. The values are the V4L2 four-character codes, as `linuxpy` reports them.
+ PixelFormat = Enum('PixelFormat', ['YUYV', 'UYVY', 'RGB24', 'H264', 'HEVC', 'VP8', 'VP9', 'MPEG4', 'MJPEG'])
+
+ device = types.ModuleType(DEVICE_MODULE)
+ device.__dict__.update(Device=object, PixelFormat=PixelFormat)
+
+ video = types.ModuleType(f'{VENDOR}.video')
+ video.__dict__.update(device=device)
+
+ package = types.ModuleType(VENDOR)
+ package.__dict__.update(video=video)
+
+ sys.modules.update({VENDOR: package, f'{VENDOR}.video': video, DEVICE_MODULE: device})
diff --git a/positronic/drivers/camera/tests/test_linux_video.py b/positronic/drivers/camera/tests/test_linux_video.py
new file mode 100644
index 000000000..16e730b91
--- /dev/null
+++ b/positronic/drivers/camera/tests/test_linux_video.py
@@ -0,0 +1,179 @@
+"""What the Linux video driver puts on its frame port, from the buffers a device hands it."""
+
+import io
+
+import av
+import numpy as np
+import pytest
+
+import pimm
+from positronic.drivers.camera import linux_video
+from positronic.tests.testing_coutils import RecordingEmitter
+
+WIDTH, HEIGHT = 4, 2
+
+
+class StopFlag(pimm.SignalReceiver[bool]):
+ """``should_stop`` under the test's control."""
+
+ def __init__(self):
+ self.stopped = False
+
+ def read(self) -> pimm.Message[bool]:
+ return pimm.Message(self.stopped)
+
+
+class FakeFrame:
+ def __init__(self, data: bytes, pixel_format):
+ self.data = data
+ self.pixel_format = pixel_format
+ self.width, self.height = WIDTH, HEIGHT
+
+
+class FakeDevice:
+ """A device that hands over the frames a test gives it, and records what it was set to."""
+
+ to_serve: list['FakeFrame'] = []
+ opened: 'FakeDevice | None' = None
+
+ def __init__(self, path: str):
+ self.path = path
+ self.info = type('Info', (), {'buffers': ['capture']})()
+ self.frames = list(FakeDevice.to_serve)
+ self.format = None
+ self.fps = None
+ self.closed = False
+
+ def open(self) -> None:
+ FakeDevice.opened = self
+
+ def set_format(self, buffer, width, height, pixel_format) -> None:
+ self.format = (buffer, width, height, pixel_format)
+
+ def set_fps(self, buffer, fps) -> None:
+ self.fps = (buffer, fps)
+
+ def __iter__(self):
+ return iter(self.frames)
+
+ def close(self) -> None:
+ self.closed = True
+
+
+@pytest.fixture
+def device(monkeypatch):
+ """The class the driver opens, replaced by one a test hands frames to. Pass it to ``_driven``."""
+ monkeypatch.setattr(linux_video, 'Device', FakeDevice)
+ FakeDevice.to_serve, FakeDevice.opened = [], None
+ return FakeDevice
+
+
+def _driven(device, frames):
+ """A driver over ``device`` carrying ``frames``, with its port recorded, run to exhaustion."""
+ camera = linux_video.LinuxVideo(device_path='/dev/null', width=WIDTH, height=HEIGHT, fps=30, pixel_format='YUYV')
+ emitted = RecordingEmitter()
+ camera.frame._bind(emitted)
+ device.to_serve = frames
+ list(camera.run(StopFlag(), pimm.world.SystemClock()))
+ opened = device.opened
+ assert opened is not None, 'the driver opened no device'
+ return emitted, opened
+
+
+def _yuyv(luma: int) -> bytes:
+ """One YUYV buffer of a flat grey, which converts to a flat grey RGB image."""
+ return bytes([luma, 128] * (WIDTH * HEIGHT))
+
+
+def test_a_yuyv_buffer_reaches_the_port_as_an_image(device):
+ emitted, _ = _driven(device, [FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV)])
+
+ assert len(emitted.emitted) == 1
+ _, adapter = emitted.emitted[0]
+ assert adapter.array.shape == (HEIGHT, WIDTH, 3)
+ assert adapter.array.dtype == np.uint8
+ assert adapter.array.min() > 150 # the grey survives the conversion
+
+
+def test_every_whole_yuyv_buffer_is_one_frame(device):
+ frames = [FakeFrame(_yuyv(v), linux_video.PixelFormat.YUYV) for v in (50, 120, 200)]
+
+ emitted, _ = _driven(device, frames)
+
+ assert len(emitted.emitted) == 3
+
+
+def test_the_device_is_set_to_what_the_driver_was_asked_for(device):
+ _, opened = _driven(device, [FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)])
+
+ assert opened.format == ('capture', WIDTH, HEIGHT, 'YUYV')
+ assert opened.fps == ('capture', 30)
+
+
+def test_the_device_is_closed_when_the_frames_run_out(device):
+ _, opened = _driven(device, [FakeFrame(_yuyv(100), linux_video.PixelFormat.YUYV)])
+
+ assert opened.closed
+
+
+def test_a_buffer_that_is_not_one_frame_in_size_is_dropped(device, caplog):
+ """A busy bus hands over a buffer that is not a whole frame, and a run outlives it."""
+ misframed = FakeFrame(_yuyv(200)[: WIDTH * HEIGHT], linux_video.PixelFormat.YUYV)
+ whole = FakeFrame(_yuyv(200), linux_video.PixelFormat.YUYV)
+
+ emitted, _ = _driven(device, [misframed, whole, misframed])
+
+ assert len(emitted.emitted) == 1
+ assert 'not one frame in size' in caplog.text
+ assert 'handed over 2 buffers' in caplog.text
+
+
+def test_a_buffer_of_three_bytes_a_pixel_is_taken_as_it_is(device):
+ raw = bytes(range(WIDTH * HEIGHT * 3))
+
+ emitted, _ = _driven(device, [FakeFrame(raw, linux_video.PixelFormat.RGB24)])
+
+ _, adapter = emitted.emitted[0]
+ np.testing.assert_array_equal(adapter.array, np.frombuffer(raw, dtype=np.uint8).reshape(HEIGHT, WIDTH, 3))
+
+
+def _h264(count: int) -> bytes:
+ """An H.264 stream of ``count`` frames, as a camera that compresses would hand it over."""
+ stream = io.BytesIO()
+ container = av.open(stream, 'w', format='h264')
+ encoded = container.add_stream('libx264', rate=30)
+ encoded.width, encoded.height, encoded.pix_fmt = WIDTH * 16, HEIGHT * 16, 'yuv420p'
+ encoded.options = {'tune': 'zerolatency'} # a camera streams frames in order, and so must the fixture
+ for i in range(count):
+ image = np.full((HEIGHT * 16, WIDTH * 16, 3), i * 40, np.uint8)
+ for packet in encoded.encode(av.VideoFrame.from_ndarray(image, format='rgb24')):
+ container.mux(packet)
+ for packet in encoded.encode(None):
+ container.mux(packet)
+ container.close()
+ return stream.getvalue()
+
+
+def test_a_compressed_buffer_the_decoder_holds_back_is_not_counted_misframed(device, caplog):
+ """The decoder gives no image until it has one, and a whole buffer is not a truncated one."""
+ head = FakeFrame(_h264(4)[:64], linux_video.PixelFormat.H264)
+
+ emitted, _ = _driven(device, [head])
+
+ assert emitted.emitted == []
+ assert 'not one frame in size' not in caplog.text
+
+
+def test_a_buffer_of_several_images_gives_the_newest(device, caplog):
+ """The frame port holds one image, so the older images of a buffer have nowhere to go."""
+ buffer = FakeFrame(_h264(4), linux_video.PixelFormat.H264)
+ driver = linux_video.LinuxVideo(device_path='/dev/null', width=WIDTH, height=HEIGHT, fps=30, pixel_format='YUYV')
+ decoded = driver._images(buffer, lambda name: av.CodecContext.create(name, 'r'))
+ assert decoded is not None and len(decoded) > 1, 'the buffer decoded to a single image'
+
+ emitted, _ = _driven(device, [FakeFrame(_h264(4), linux_video.PixelFormat.H264)])
+
+ assert len(emitted.emitted) == 1
+ _, adapter = emitted.emitted[0]
+ assert adapter.array.mean() == pytest.approx(decoded[-1].mean())
+ assert f'{len(decoded) - 1} images a newer one' in caplog.text
diff --git a/positronic/drivers/roboarm/franka.py b/positronic/drivers/roboarm/franka.py
index 7f7370868..ac0a4f47e 100644
--- a/positronic/drivers/roboarm/franka.py
+++ b/positronic/drivers/roboarm/franka.py
@@ -405,7 +405,7 @@ def _build_robot_meta(robot) -> dict:
return {
roboarm_keys.URDF: ET.tostring(root, encoding='unicode'),
roboarm_keys.JOINT_NAMES: _revolute_joint_names(urdf_xml),
- 'meshes': meshes,
+ roboarm_keys.MESHES: meshes,
roboarm_keys.CONTROL_FRAME: DEFAULT_FRAME,
roboarm_keys.GRIPPER: gripper,
}
diff --git a/positronic/drivers/roboarm/keys.py b/positronic/drivers/roboarm/keys.py
index e52d8731e..098bb1d6e 100644
--- a/positronic/drivers/roboarm/keys.py
+++ b/positronic/drivers/roboarm/keys.py
@@ -6,8 +6,12 @@
URDF = 'urdf'
CONTROL_FRAME = 'control_frame'
JOINT_NAMES = 'joint_names'
+# The arm a driver names itself, where its metadata carries one.
+ROBOT = 'robot'
# The gripper spec the viewer drives: the signal it reads, the joints it moves, their travel at full closure.
GRIPPER = 'gripper'
+# The mesh files ``URDF`` names, keyed by the name it gives each one.
+MESHES = 'meshes'
# Where the episode's poses sit relative to ``DEFAULT_FRAME``, as a ``[tx,ty,tz,qw,qx,qy,qz]`` transform.
# Absent means they are in that frame itself; ``ChangeEEFrame`` writes it when it moves them.
diff --git a/positronic/drivers/roboarm/models.py b/positronic/drivers/roboarm/models.py
index e51d665ff..09769edca 100644
--- a/positronic/drivers/roboarm/models.py
+++ b/positronic/drivers/roboarm/models.py
@@ -151,7 +151,7 @@ def _bundled_robotiq_2f85() -> dict:
subtree = ''.join(ET.tostring(el, encoding='unicode') for el in _build_2f85_elements())
return {
'subtree': subtree,
- 'meshes': {f.name: f.read_bytes() for f in sorted(mesh_dir.glob('*.stl'))},
+ roboarm_keys.MESHES: {f.name: f.read_bytes() for f in sorted(mesh_dir.glob('*.stl'))},
roboarm_keys.GRIPPER: {'signal': keys.GRIP, 'joints': _2F85_GRIP_ACTUATED_JOINTS, 'travel': 0.8},
}
@@ -170,7 +170,7 @@ def attach_robotiq_2f85(arm_root: ET.Element, meshes: dict[str, bytes]) -> dict:
merge its meshes into ``meshes``. Returns the ``grip``-driven gripper spec for the viewer."""
gripper = _bundled_robotiq_2f85()
arm_root.extend(ET.fromstring(f'{gripper["subtree"]}'))
- meshes.update(gripper['meshes'])
+ meshes.update(gripper[roboarm_keys.MESHES])
return gripper[roboarm_keys.GRIPPER]
@@ -189,7 +189,7 @@ def bundled_franka_model() -> dict:
add_default_frame(arm_root, EE_LINK)
return {
roboarm_keys.URDF: ET.tostring(arm_root, encoding='unicode'),
- 'meshes': meshes,
+ roboarm_keys.MESHES: meshes,
roboarm_keys.JOINT_NAMES: [f'joint{i}' for i in range(1, 8)],
roboarm_keys.CONTROL_FRAME: DEFAULT_FRAME,
roboarm_keys.GRIPPER: gripper,
@@ -207,11 +207,11 @@ def bundled_panda_model() -> dict:
urdf_path = Path(__file__).resolve().parents[2] / 'assets' / 'mujoco' / 'panda.urdf'
root = ET.fromstring(urdf_path.read_text())
mesh_dir = urdf_path.parent / 'assets'
- mesh_files = {mesh.get('filename') for mesh in root.iter('mesh')}
+ mesh_files = {mesh.get('filename', '') for mesh in root.iter('mesh')}
add_default_frame(root, EE_LINK)
return {
roboarm_keys.URDF: ET.tostring(root, encoding='unicode'),
- 'meshes': {name: (mesh_dir / name).read_bytes() for name in sorted(mesh_files)},
+ roboarm_keys.MESHES: {name: (mesh_dir / name).read_bytes() for name in sorted(mesh_files)},
roboarm_keys.JOINT_NAMES: [f'joint{i}' for i in range(1, 8)],
roboarm_keys.CONTROL_FRAME: DEFAULT_FRAME,
# ``grip`` is recorded in [0, 1] (open→closed); each finger slides 0..0.04 m along its axis.
diff --git a/positronic/drivers/roboarm/tests/conftest.py b/positronic/drivers/roboarm/tests/conftest.py
index 0612414dc..d391db240 100644
--- a/positronic/drivers/roboarm/tests/conftest.py
+++ b/positronic/drivers/roboarm/tests/conftest.py
@@ -1,11 +1,11 @@
"""Stand-ins for the vendor packages the arm drivers import.
-``positronic_franka`` builds against libfranka, ``scservo_sdk`` talks to a serial servo bus, and ``placo``
-solves kinematics; all three ship only in the ``hardware`` extra, so the driver modules cannot be imported
-from a default sync. Their Python-side logic needs no vendor behaviour, so a stub carrying the names each
-module binds at import is enough — a test that needs a vendor to compute something stands in for the class
-that wraps it instead. Installed here, before any test module imports a driver, and only where the real
-package is absent.
+``positronic_franka`` builds against libfranka, ``scservo_sdk`` talks to a serial servo bus, ``placo``
+solves kinematics, and ``trossen_arm`` talks to an arm controller; all four ship only in an extra, so the
+driver modules cannot be imported from a default sync. Their Python-side logic needs no vendor behaviour,
+so a stub carrying the names each module binds at import is enough — a test that needs a vendor to compute
+something stands in for the class that wraps it instead. Installed here, before any test module imports a
+driver, and only where the real package is absent.
"""
import importlib.util
@@ -20,6 +20,7 @@
PACKAGE = 'positronic_franka'
VENDOR = f'{PACKAGE}._franka'
DESK = f'{PACKAGE}.desk'
+TROSSEN = 'trossen_arm'
def _install_vendor_stub() -> None:
@@ -63,12 +64,37 @@ def __init__(
sys.modules.update({PACKAGE: package, VENDOR: vendor, DESK: desk})
+def _install_trossen_stub() -> None:
+ """Bind the names ``trossen_arm`` gives the Trossen driver, which reaches them as ``trossen_arm.*``."""
+
+ class Mode(Enum):
+ idle = 'idle'
+ position = 'position'
+ external_effort = 'external_effort'
+
+ class Model(Enum):
+ wxai_v0 = 'wxai_v0'
+
+ module = types.ModuleType(TROSSEN)
+ module.__dict__.update(
+ Mode=Mode,
+ Model=Model,
+ StandardEndEffector=types.SimpleNamespace(wxai_v0_follower=object(), wxai_v0_leader=object()),
+ TrossenArmDriver=object,
+ RuntimeError=type('RuntimeError', (RuntimeError,), {}),
+ )
+ sys.modules[TROSSEN] = module
+
+
# Both are reached for only inside the functions that use them, so an empty module carries the import
_EMPTY_STUBS = ('scservo_sdk', 'placo')
if importlib.util.find_spec(PACKAGE) is None:
_install_vendor_stub()
+if importlib.util.find_spec(TROSSEN) is None:
+ _install_trossen_stub()
+
for _name in _EMPTY_STUBS:
if importlib.util.find_spec(_name) is None:
sys.modules[_name] = types.ModuleType(_name)
diff --git a/positronic/drivers/roboarm/tests/test_ik.py b/positronic/drivers/roboarm/tests/test_ik.py
index 6ec1eeab5..0879a1d8a 100644
--- a/positronic/drivers/roboarm/tests/test_ik.py
+++ b/positronic/drivers/roboarm/tests/test_ik.py
@@ -9,6 +9,7 @@
from positronic.dataset.episode import EpisodeContainer
from positronic.dataset.tests.utils import DummySignal
from positronic.drivers.roboarm import keys as roboarm_keys
+from positronic.drivers.roboarm import trossen
from positronic.drivers.roboarm.ik import (
DLSIKSolver,
DLSIKSolverWithLimits,
@@ -182,6 +183,38 @@ def test_bundled_model_declares_the_frame_it_reports_in(model):
frame_transform(model[roboarm_keys.URDF], DEFAULT_FRAME, DEFAULT_FRAME)
+def test_the_trossen_model_declares_the_frame_it_reports_in():
+ model = trossen._robot_meta()
+ assert model[roboarm_keys.CONTROL_FRAME] == DEFAULT_FRAME
+ frame_transform(model[roboarm_keys.URDF], DEFAULT_FRAME, DEFAULT_FRAME)
+
+
+def test_the_trossen_urdf_and_its_mjcf_put_the_control_frame_in_the_same_place():
+ """The driver solves against the MJCF and the codecs against the URDF. Two models of one arm that
+ disagree put every recorded pose in a frame nothing else can read."""
+ spec = _prepare_spec(trossen._robot_meta()[roboarm_keys.URDF], DEFAULT_FRAME)
+ urdf_model = spec.compile()
+ urdf_data = mj.MjData(urdf_model)
+ site = mj.mj_name2id(urdf_model, mj.mjtObj.mjOBJ_SITE, DEFAULT_FRAME)
+ kin = trossen._Kinematics()
+
+ rng = np.random.default_rng(0)
+ for _ in range(8):
+ q = rng.uniform([-3.0, 0.1, 0.1, -1.5, -1.5, -3.0], [3.0, 3.0, 2.3, 1.5, 1.5, 3.0])
+ urdf_data.qpos[:] = 0.0
+ urdf_data.qpos[: len(q)] = q
+ mj.mj_forward(urdf_model, urdf_data)
+ from_urdf = geom.Transform3D(
+ urdf_data.site_xpos[site].copy(),
+ geom.Rotation.from_rotation_matrix(urdf_data.site_xmat[site].reshape(3, 3)),
+ )
+ from_mjcf = kin.fk(q)
+ np.testing.assert_allclose(from_urdf.translation, from_mjcf.translation, atol=1e-4)
+ np.testing.assert_allclose(
+ from_urdf.rotation.as_rotation_matrix, from_mjcf.rotation.as_rotation_matrix, atol=1e-4
+ )
+
+
def test_declared_droid_frame_matches_the_model_geometry():
"""The checkpoint states its frame as a constant; this pins it to where the model puts that frame."""
urdf = bundled_franka_model()[roboarm_keys.URDF]
diff --git a/positronic/drivers/roboarm/tests/test_trossen.py b/positronic/drivers/roboarm/tests/test_trossen.py
new file mode 100644
index 000000000..b578bf060
--- /dev/null
+++ b/positronic/drivers/roboarm/tests/test_trossen.py
@@ -0,0 +1,853 @@
+"""What the Trossen driver puts on the link, and what a caller waiting on a move hears."""
+
+import logging
+
+import numpy as np
+import pytest
+
+import pimm
+from pimm.tests.testing import MockClock, wire_call
+from positronic import geom
+from positronic.drivers.roboarm import RobotStatus, command
+from positronic.drivers.roboarm import trossen as trossen_driver
+from positronic.drivers.roboarm.tests.fakes import StopFlag
+from positronic.drivers.utils import MoveAbandoned
+from positronic.tests.testing_coutils import ManualCommandReceiver, RecordingEmitter
+
+GRIP_SPEED_M_S = 0.25 # what the controller reports the finger drive may do
+GRIP_TRAVEL_M = 0.04 # the gripper joint's range, which the arm reports and grip is normalized against
+JOGGED = np.array([0.2, 0.4, 0.3, 0.0, 0.1, 0.0])
+# Mid-range on every joint. The arm rests on the lower limit of joints 1 and 2, where Cartesian targets have
+# no solution in half the directions, so every Cartesian test starts from here instead.
+HOME = np.array([0.0, 1.571, 1.178, 0.0, 0.0, 0.0])
+ARM = trossen_driver._ARM_JOINTS
+# What the wxai_v0 controller reports as the following error it allows, and what caps a streamed solution
+TOLERANCE = np.array([0.2, 0.2, 0.2, 0.4, 0.4, 0.4])
+
+
+class FakeArm(trossen_driver._FakeTrossen):
+ """The driver's fake with the link under the test's control.
+
+ ``blocked`` holds the joints where they stand while the controller keeps streaming; ``frozen`` stops
+ the stream, as a dropped link does. ``raises`` is what reading the arm raises, ``write_raises`` what
+ a write raises, and ``velocities`` what the joints read as running at.
+ """
+
+ def __init__(self, position=None):
+ super().__init__()
+ if position is not None:
+ self._position = np.asarray(position, dtype=np.float64)
+ self.raises: Exception | None = None
+ self.write_raises: Exception | None = None
+ self.configure_raises: Exception | None = None
+ self.velocities: np.ndarray | None = None
+ self.attempts = 0
+ self.blocked = False
+
+ def configure(self, model, end_effector, serv_ip, clear_error, timeout=20.0) -> None:
+ self.attempts += 1
+ if self.configure_raises is not None:
+ raise self.configure_raises
+ super().configure(model, end_effector, serv_ip, clear_error, timeout)
+
+ def get_robot_output(self):
+ if self.raises is not None:
+ raise self.raises
+ out = super().get_robot_output()
+ if self.velocities is not None:
+ out.joint.arm.velocities = np.asarray(self.velocities, dtype=np.float64)
+ return out
+
+ def set_all_modes(self, mode) -> None:
+ if self.write_raises is not None:
+ raise self.write_raises
+ super().set_all_modes(mode)
+
+ def set_all_positions(self, goal_positions, goal_time=2.0, blocking=True) -> None:
+ if self.write_raises is not None:
+ raise self.write_raises
+ super().set_all_positions(goal_positions, goal_time, blocking)
+
+ def set_gripper_position(self, goal_position, goal_time=2.0, blocking=True) -> None:
+ if self.write_raises is not None:
+ raise self.write_raises
+ super().set_gripper_position(goal_position, goal_time, blocking)
+
+ def _servo(self) -> None:
+ if not self.blocked:
+ super()._servo()
+
+
+class SaggingArm(FakeArm):
+ """The arm's fake, holding itself up with a following error, as the real one does.
+
+ The joints settle a fixed distance short of every goal. The values are what the arm at the rig reads
+ against what it was asked for at the nominal pose.
+ """
+
+ DROOP = np.array([0.0, 0.071, -0.064, -0.051, 0.0, 0.0])
+
+ def _servo(self) -> None:
+ super()._servo()
+ if self.goals:
+ settled = np.append(np.asarray(self.goals[-1])[:ARM] + SaggingArm.DROOP, self._position[ARM])
+ self._position = self._position + 0.1 * (settled - self._position)
+
+
+def _driven(arm: FakeArm, clock: MockClock | None = None, stop: StopFlag | None = None):
+ """A driver over ``arm`` with its state recorded, and its loop ready to pump."""
+ driver = trossen_driver.Robot('192.168.1.4', connect=lambda ip: arm)
+ states = RecordingEmitter()
+ driver.state._bind(states)
+ return driver, states, driver.run(stop or StopFlag(), clock or MockClock())
+
+
+def _settle(loop, ticks: int = 400) -> None:
+ """Run the loop long enough for the arm to reach what it was last asked for.
+
+ Every setpoint is held to what the joints may travel in one tick, so arriving takes many of them.
+ """
+ for _ in range(ticks):
+ next(loop)
+
+
+def _held(arm: FakeArm) -> np.ndarray:
+ return np.asarray(arm.goals[-1][:ARM])
+
+
+def _homed(loop, ticks: int = 60) -> None:
+ """Run the loop past the travel the driver makes to the pose a run opens at."""
+ _settle(loop, ticks)
+
+
+def _drain(loop, ticks: int = 1200) -> None:
+ """Run the loop to its end. A run rests the arm before it gives the handle back."""
+ for _ in range(ticks):
+ try:
+ next(loop)
+ except StopIteration:
+ return
+ raise AssertionError('the run did not end')
+
+
+def test_the_link_is_written_only_once_something_has_asked_for_a_setpoint():
+ """An arm already held where it stands is not written to again."""
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ grip = ManualCommandReceiver()
+ driver.target_grip._bind(grip)
+ _homed(loop)
+
+ next(loop)
+ written = len(arm.goals) + len(arm.gripper_goals)
+ for _ in range(3): # and nothing has asked since
+ next(loop)
+ assert len(arm.goals) + len(arm.gripper_goals) == written
+
+ grip.push(0.25)
+ next(loop)
+ assert len(arm.goals) + len(arm.gripper_goals) == written + 1
+
+
+def test_an_open_grip_reaches_the_arm_as_the_joint_at_its_upper_limit():
+ """positronic counts 1 as closed; the joint counts its lower limit as closed."""
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ grip = ManualCommandReceiver()
+ driver.target_grip._bind(grip)
+ _homed(loop)
+
+ grip.push(0.0)
+ _settle(loop, 40) # the fingers are paced by their own velocity limit
+
+ assert arm.gripper_goals[-1] == pytest.approx(GRIP_TRAVEL_M)
+
+
+def test_a_closed_grip_reaches_the_arm_as_the_joint_at_its_lower_limit():
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ grip = ManualCommandReceiver()
+ driver.target_grip._bind(grip)
+ _homed(loop)
+
+ grip.push(1.0)
+ _settle(loop, 40)
+
+ assert arm.gripper_goals[-1] == pytest.approx(0.0)
+
+
+def test_the_fingers_are_never_asked_for_more_than_they_may_travel_in_a_tick():
+ """A trigger let go of asks for the whole range at once, which is metres a second past the joint's own
+ limit -- and a joint past its limit faults the controller."""
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ grip = ManualCommandReceiver()
+ driver.target_grip._bind(grip)
+ _homed(loop)
+ grip.push(1.0) # the fingers start closed and are asked to stay there
+ next(loop)
+ was = arm.gripper_goals[-1]
+
+ grip.push(0.0) # from closed to fully open in one message
+ next(loop)
+
+ travelled = abs(arm.gripper_goals[-1] - was) * trossen_driver._HZ
+ assert travelled < GRIP_SPEED_M_S, travelled
+
+
+def test_the_joint_the_arm_reports_comes_back_as_a_normalized_grip():
+ """The reading crosses the same conversion as the goal, the other way."""
+ arm = FakeArm(position=np.append(np.zeros(6), GRIP_TRAVEL_M / 4))
+ driver, _, loop = _driven(arm)
+ grips = RecordingEmitter()
+ driver.grip._bind(grips)
+
+ next(loop) # the reading goes out before the arm is moved anywhere
+
+ assert grips.emitted[-1][1] == pytest.approx(0.75)
+
+
+def test_a_joint_reading_just_outside_its_range_still_comes_back_a_grip():
+ """A closed gripper reads a shade below its lower limit, and the port carries 0..1."""
+ arm = FakeArm(position=np.append(np.zeros(6), -0.000945))
+ driver, _, loop = _driven(arm)
+ grips = RecordingEmitter()
+ driver.grip._bind(grips)
+
+ next(loop) # the reading goes out before the arm is moved anywhere
+
+ assert grips.emitted[-1][1] == pytest.approx(1.0)
+
+
+def test_a_run_opens_by_taking_the_arm_off_its_rest_pose():
+ """The arm rests on the lower limit of joints 1 and 2, where half the directions out have no solution.
+ A Cartesian setpoint from there is refused, so the run does not begin there."""
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ _homed(loop)
+
+ np.testing.assert_allclose(_held(arm), trossen_driver._HOME_JOINTS, atol=1e-3)
+ assert states.emitted[-1][1].status == RobotStatus.AVAILABLE
+
+
+def test_a_run_that_opens_on_a_moving_arm_waits_for_it_before_it_writes():
+ """Position mode on a joint past its limit faults the controller. A goal sent before the mode change is
+ refused, and that refusal reads as a dead command channel and sends the run through link recovery."""
+ arm = FakeArm()
+ arm.velocities = np.full(ARM, 10.0) # past every joint's limit when the session opens
+ driver, states, loop = _driven(arm)
+
+ for _ in range(20):
+ next(loop)
+ assert arm.goals == [], 'the arm was written to before it was in position mode'
+ assert arm.mode is not trossen_driver.trossen_arm.Mode.position
+
+ arm.velocities = np.zeros(ARM)
+ _homed(loop)
+
+ assert arm.mode is trossen_driver.trossen_arm.Mode.position
+ np.testing.assert_allclose(_held(arm), trossen_driver._HOME_JOINTS, atol=1e-3)
+
+
+def test_a_run_ends_by_putting_the_arm_back_on_it():
+ """The controller holds the arm only at rest: `_opened` sets it idle, and idle anywhere else falls."""
+ arm = FakeArm()
+ stop = StopFlag()
+ _, _, loop = _driven(arm, stop=stop)
+ _homed(loop)
+
+ stop.stopped = True
+ _drain(loop)
+
+ # Within the arrival tolerance, which is the following error the controller holds the arm up with
+ rested = np.asarray(arm.get_robot_output().joint.arm.positions)
+ np.testing.assert_allclose(rested, np.zeros(ARM), atol=0.1)
+ assert arm.mode is trossen_driver.trossen_arm.Mode.idle
+ assert arm.cleaned_up
+
+
+def test_a_streamed_joint_command_reaches_the_arm():
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+
+ commands.push(command.JointPosition(JOGGED))
+ _settle(loop)
+
+ np.testing.assert_allclose(_held(arm), JOGGED, atol=1e-6)
+
+
+def test_a_setpoint_never_asks_a_joint_for_more_than_it_may_travel_in_a_tick():
+ """Past its velocity limit the controller faults and drops the arm, so no goal may ask for that.
+
+ The ramp also comes up to speed rather than starting at it, so the first steps are smaller still.
+ """
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ arm.goals.clear() # the travel a run opens with is its own move, planned by the firmware
+
+ commands.push(command.JointPosition(np.array([3.0, 3.0, 2.0, 1.5, 1.5, 3.0])))
+ for _ in range(40):
+ next(loop)
+
+ per_tick = np.array([limit.velocity_max for limit in arm.get_joint_limits()[:ARM]])
+ per_tick = per_tick * trossen_driver._COMMANDED_SHARE / trossen_driver._HZ
+ steps = np.abs(np.diff(np.array([goal[:ARM] for goal in arm.goals]), axis=0))
+ assert np.all(steps <= per_tick + 1e-9), steps.max(axis=0)
+
+
+def test_a_joint_target_outside_the_range_is_clipped_to_it_and_said_so(caplog):
+ """The second joint has no negative half, and a target below it is held at the limit, not refused. A
+ leader pushed past the follower's range carries it as far as the range goes, and says where it stopped."""
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+
+ with caplog.at_level(logging.WARNING):
+ commands.push(command.JointPosition(np.array([0.0, -1.0, 0.0, 0.0, 0.0, 0.0])))
+ _settle(loop)
+
+ assert _held(arm)[1] == pytest.approx(0.0)
+ assert 'Joint 1' in caplog.text and 'outside' in caplog.text
+ assert [r.levelname for r in caplog.records if 'outside' in r.message] == ['WARNING']
+
+
+def test_a_joint_target_that_is_not_six_finite_joints_moves_nothing(caplog):
+ """One number broadcasts across all six limits, and a NaN clips to itself: neither may reach the arm."""
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ home = _held(arm)
+
+ for target in (np.array([0.5]), np.array([0.5, np.nan, 0.5, 0.0, 0.0, 0.0])):
+ commands.push(command.JointPosition(target))
+ _settle(loop, ticks=5)
+
+ np.testing.assert_allclose(_held(arm), home)
+ assert 'not applied' in caplog.text
+
+
+def test_a_grip_that_is_not_a_number_leaves_the_fingers_alone(caplog):
+ """``np.clip`` carries a NaN through, and the controller takes whatever finger position it is handed."""
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ grip = ManualCommandReceiver()
+ driver.target_grip._bind(grip)
+ _homed(loop)
+ written = len(arm.goals) + len(arm.gripper_goals)
+
+ grip.push(float('nan'))
+ next(loop)
+
+ assert len(arm.goals) + len(arm.gripper_goals) == written
+ assert 'grip at nan' in caplog.text
+
+
+def test_a_streamed_command_the_arm_cannot_be_put_at_leaves_it_where_it_is(monkeypatch):
+ """A command stream cannot end the run: the next command supersedes one that could not be applied.
+
+ Every target is brought within a step of where the arm stands before it is solved, so one the solver
+ cannot reach is rare enough that the solver is what stands in for it here.
+ """
+ arm = FakeArm()
+ driver, _, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ where_it_is = _held(arm)
+
+ monkeypatch.setattr(trossen_driver._Kinematics, 'ik', lambda self, target, current_q: None)
+ commands.push(command.CartesianPosition(geom.Transform3D(np.array([3.0, 0.0, 0.2]))))
+ next(loop)
+
+ np.testing.assert_allclose(_held(arm), where_it_is, atol=1e-6)
+
+
+def _reject_writes(arm: FakeArm, commands: ManualCommandReceiver, loop) -> None:
+ """Break the command half of the link, and give the driver a setpoint that finds out."""
+ arm.write_raises = trossen_driver.trossen_arm.RuntimeError('Broken pipe')
+ commands.push(command.JointPosition(JOGGED))
+ next(loop)
+
+
+def test_a_read_that_raises_reads_error_and_the_run_carries_on():
+ arm = FakeArm()
+ _, states, loop = _driven(arm)
+
+ next(loop)
+ arm.raises = trossen_driver.trossen_arm.RuntimeError('the controller stopped answering')
+ next(loop)
+ assert states.emitted[-1][1].status == RobotStatus.ERROR
+
+ arm.raises = None
+ next(loop)
+ assert states.emitted[-1][1].status == RobotStatus.AVAILABLE
+
+
+def test_an_arm_that_stops_streaming_reads_error_though_the_read_still_answers():
+ """A dropped link does not make the read raise: it hands back the last telemetry, over and over."""
+ arm = FakeArm()
+ clock = MockClock()
+ _, states, loop = _driven(arm, clock)
+ _homed(loop)
+
+ next(loop)
+ assert states.emitted[-1][1].status == RobotStatus.AVAILABLE
+
+ arm.frozen = True
+ clock.advance(trossen_driver._STALE_AFTER_S + 0.1)
+ next(loop)
+ assert states.emitted[-1][1].status == RobotStatus.ERROR
+
+ arm.frozen = False
+ next(loop)
+ assert states.emitted[-1][1].status == RobotStatus.AVAILABLE
+
+
+def test_a_setpoint_the_link_refuses_reads_error_and_stops_the_writes():
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+
+ written = len(arm.goals)
+ _reject_writes(arm, commands, loop)
+ assert states.emitted[-1][1].status == RobotStatus.ERROR
+
+ for _ in range(5): # a refused write is not retried a hundred times a second
+ next(loop)
+ assert len(arm.goals) == written
+
+
+def test_telemetry_alone_does_not_bring_a_refused_command_channel_back():
+ """The controller keeps streaming over a link whose command half is gone, so freshness proves nothing."""
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+
+ _reject_writes(arm, commands, loop)
+ arm.write_raises = None # the wire is back, but the session the controller dropped is not
+ for _ in range(5):
+ next(loop)
+
+ assert states.emitted[-1][1].status == RobotStatus.ERROR
+ assert arm.sessions == 1
+
+
+def test_a_link_that_stays_down_gets_a_new_session_and_the_arm_answers_again():
+ arm = FakeArm()
+ clock = MockClock()
+ driver, states, loop = _driven(arm, clock)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ assert arm.sessions == 1
+
+ _reject_writes(arm, commands, loop)
+ assert states.emitted[-1][1].status == RobotStatus.ERROR
+
+ arm.write_raises = None
+ clock.advance(trossen_driver._RECONNECT_AFTER_S + 0.01)
+ next(loop)
+
+ assert arm.sessions == 2
+ assert states.emitted[-1][1].status == RobotStatus.AVAILABLE
+
+
+def test_a_new_session_holds_the_arm_where_it_finds_it():
+ """The arm ends up wherever the lost session left it, and driving it back to an old target is a jump."""
+ arm = FakeArm()
+ clock = MockClock()
+ driver, _, loop = _driven(arm, clock)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+
+ commands.push(command.JointPosition(JOGGED))
+ _settle(loop)
+ where_it_is = np.asarray(arm.get_robot_output().joint.arm.positions)
+
+ _reject_writes(arm, commands, loop)
+ arm.write_raises = None
+ clock.advance(trossen_driver._RECONNECT_AFTER_S + 0.01)
+ next(loop)
+
+ np.testing.assert_allclose(_held(arm), where_it_is, atol=1e-3)
+
+
+def test_a_move_in_flight_when_the_link_drops_is_failed_by_the_new_session(world):
+ """A new session holds the arm where it reads, so the target of a move in flight is never sent again."""
+ arm = FakeArm()
+ clock = MockClock()
+ driver, states, loop = _driven(arm, clock)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](driver)
+ wire_call(world, caller, driver.sync_move)
+ _homed(loop)
+
+ answer = caller(command.JointPosition(JOGGED))
+ arm.write_raises = trossen_driver.trossen_arm.RuntimeError('Broken pipe')
+ for _ in range(4): # the link goes before the target reaches the controller
+ next(loop)
+ assert not answer.done()
+
+ arm.write_raises = None
+ clock.advance(trossen_driver._RECONNECT_AFTER_S + 0.01)
+ next(loop)
+
+ assert clock.now() < trossen_driver._MOVE_TIMEOUT_S, 'the move waited out its deadline'
+ with pytest.raises(ConnectionError, match='dropped during the move'):
+ answer.result()
+ assert states.emitted[-1][1].status is RobotStatus.ERROR # the arm is not where the driver put it
+
+
+def test_a_new_session_that_fails_is_tried_again_further_and_further_apart():
+ """A fault the controller latches outlives a new session, so retrying at the same pace stalls the loop."""
+ arm = FakeArm()
+ clock = MockClock()
+ driver, _, loop = _driven(arm, clock)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+
+ _reject_writes(arm, commands, loop)
+ arm.configure_raises = trossen_driver.trossen_arm.RuntimeError('Network is unreachable')
+ clock.advance(trossen_driver._RECONNECT_AFTER_S + 0.01)
+ for _ in range(20): # many ticks inside one reconnect interval, one attempt between them
+ next(loop)
+ assert arm.attempts == 1
+
+ clock.advance(trossen_driver._RECONNECT_EVERY_S + 0.01)
+ for _ in range(5):
+ next(loop)
+ assert arm.attempts == 1 # the interval doubled after the first attempt failed
+
+ clock.advance(trossen_driver._RECONNECT_EVERY_S + 0.01)
+ next(loop)
+ assert arm.attempts == 2
+ assert arm.sessions == 1 # none of them took
+
+
+def test_a_sync_move_answers_once_the_arm_reads_back_at_its_target(world):
+ arm = FakeArm()
+ clock = MockClock()
+ driver, states, loop = _driven(arm, clock)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](driver)
+ wire_call(world, caller, driver.sync_move)
+
+ answer = caller(command.JointPosition(JOGGED))
+ for _ in range(400):
+ if answer.done():
+ break
+ next(loop)
+ answer.result()
+
+ np.testing.assert_allclose(states.emitted[-1][1].q, JOGGED, atol=0.1)
+
+
+def test_a_move_the_world_stops_under_is_handed_back_to_its_asker(world):
+ arm = FakeArm()
+ clock = MockClock()
+ stop = StopFlag()
+ driver, _, loop = _driven(arm, clock, stop=stop)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](driver)
+ wire_call(world, caller, driver.sync_move)
+ _homed(loop)
+
+ arm.blocked = True # it never reaches the target, so the move is still in flight when the run ends
+ answer = caller(command.JointPosition(JOGGED))
+ next(loop)
+ stop.stopped = True
+ next(loop) # the run leaves its loop and starts resting the arm
+ clock.advance(trossen_driver._MOVE_TIMEOUT_S + 1) # an arm that cannot move cannot be rested either
+ _drain(loop)
+
+ assert answer.done()
+ with pytest.raises(MoveAbandoned):
+ answer.result()
+
+
+def test_a_run_that_ends_on_a_dead_link_still_gives_the_handle_back():
+ """Setting the arm idle is what the run tries last, and an arm it cannot reach must not end it badly."""
+ arm = FakeArm()
+ stop = StopFlag()
+ _, _, loop = _driven(arm, stop=stop)
+ _homed(loop)
+
+ arm.write_raises = trossen_driver.trossen_arm.RuntimeError('Connection reset by peer')
+ stop.stopped = True
+ _drain(loop)
+
+ assert arm.cleaned_up
+
+
+def test_an_arm_running_past_a_joint_limit_is_left_alone_until_it_slows():
+ """Past its limit the controller faults and drops the arm, so the driver stops driving before that."""
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+
+ arm.velocities = np.array([0.0, 0.0, 0.0, 0.0, 9.0, 0.0]) # joint 4 stops at 9.4248 rad/s
+ commands.push(command.JointPosition(JOGGED))
+ for _ in range(5):
+ next(loop)
+
+ assert states.emitted[-1][1].status == RobotStatus.ERROR
+ np.testing.assert_allclose(_held(arm), states.emitted[-1][1].q, atol=1e-3)
+
+ arm.velocities = np.zeros(6)
+ commands.push(command.JointPosition(JOGGED))
+ _settle(loop)
+ np.testing.assert_allclose(_held(arm), JOGGED, atol=1e-6)
+
+
+# --- Cartesian, which the driver solves itself ---
+
+
+def _ee(states: RecordingEmitter) -> geom.Transform3D:
+ return states.emitted[-1][1].ee_pose
+
+
+def test_the_pose_that_goes_out_is_the_one_the_joints_put_the_end_effector_at():
+ """Measured on the arm at rest: joints all but zero put ``ee_site`` here."""
+ arm = FakeArm()
+ _, states, loop = _driven(arm)
+
+ next(loop)
+
+ np.testing.assert_allclose(_ee(states).translation, [0.2537, 0.0, 0.1635], atol=5e-4)
+
+
+def test_a_streamed_cartesian_command_takes_the_end_effector_there():
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ target = geom.Transform3D(_ee(states).translation + np.array([0.0, 0.04, -0.03]), _ee(states).rotation)
+
+ for _ in range(60): # the target is held, as a teleoperator holds one
+ commands.push(command.CartesianPosition(target))
+ _settle(loop, 10)
+
+ np.testing.assert_allclose(_ee(states).translation, target.translation, atol=2e-3)
+
+
+def test_a_cartesian_delta_composes_onto_the_pose_the_joints_put_the_arm_at():
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ started = _ee(states).translation.copy()
+
+ commands.push(command.CartesianDelta(geom.Transform3D(np.array([0.0, 0.01, 0.0]))))
+ _settle(loop)
+
+ np.testing.assert_allclose(_ee(states).translation, started + np.array([0.0, 0.01, 0.0]), atol=2e-3)
+
+
+def test_a_delta_longer_than_one_step_is_walked_the_whole_way():
+ """A delta is consumed once, and it asks for a travel: paced to a step a tick, the rest of it would
+ have nowhere to live and the arm would stop a step in."""
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ started = _ee(states).translation.copy()
+ travel = np.array([0.0, 0.05, 0.0]) # more than three times `_MAX_STEP_M`
+ assert np.linalg.norm(travel) > trossen_driver._MAX_STEP_M
+
+ commands.push(command.CartesianDelta(geom.Transform3D(travel)))
+ _settle(loop) # nothing else is streamed at the arm
+
+ np.testing.assert_allclose(_ee(states).translation, started + travel, atol=2e-3)
+
+
+def test_a_pose_the_stream_stops_at_holds_the_arm_a_step_in():
+ """A pose says where the arm is wanted now, and the next one supersedes it. An arm whose stream stops
+ holds where it stands rather than finishing a travel nobody is asking for any more."""
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ started = _ee(states).translation.copy()
+
+ commands.push(command.CartesianPosition(geom.Transform3D(started + np.array([0.0, 0.05, 0.0]))))
+ _settle(loop) # the stream stops after that one pose
+
+ assert np.linalg.norm(_ee(states).translation - started) < trossen_driver._MAX_STEP_M + 2e-3
+
+
+def test_a_cartesian_target_out_of_reach_is_solved_one_step_at_a_time():
+ """A teleoperator reaching past the arm asks for a target that runs away from it."""
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ started = _ee(states).translation.copy()
+
+ commands.push(command.CartesianPosition(geom.Transform3D(started + np.array([0.0, 5.0, 0.0]))))
+ next(loop)
+
+ step = np.linalg.norm(_ee(states).translation - started)
+ assert step < trossen_driver._MAX_STEP_M + 1e-6
+
+
+def test_a_pose_streamed_after_a_joint_move_steps_from_where_the_joints_put_the_arm(world):
+ """The rig is readied by a joint move between episodes, and a Cartesian step measured from the pose the
+ stream asked for before it would drive the arm back towards the last episode's target."""
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](driver)
+ wire_call(world, caller, driver.sync_move)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+
+ away = geom.Transform3D(_ee(states).translation + np.array([0.0, 0.06, 0.0]), _ee(states).rotation)
+ for _ in range(40): # a teleoperator drives the arm away from home
+ commands.push(command.CartesianPosition(away))
+ _settle(loop, 10)
+
+ answer = caller(command.JointPosition(HOME))
+ for _ in range(400):
+ if answer.done():
+ break
+ next(loop)
+ answer.result()
+ _settle(loop) # the arm takes up the last of the travel with nothing streamed at it
+ homed = _ee(states).translation.copy()
+
+ commands.push(command.CartesianPosition(_ee(states))) # asked to stand where it now is
+ _settle(loop, 20)
+
+ np.testing.assert_allclose(_ee(states).translation, homed, atol=2e-3)
+
+
+def test_an_arm_that_runs_too_fast_tells_the_move_it_was_making(world):
+ """Standing down holds the arm where it reads, and a move in flight refuses every request that could
+ send its target again -- so nothing would finish it and its asker would wait out the deadline."""
+ arm = FakeArm()
+ clock = MockClock()
+ driver, _, loop = _driven(arm, clock)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](driver)
+ wire_call(world, caller, driver.sync_move)
+ _homed(loop)
+
+ answer = caller(command.JointPosition(JOGGED))
+ next(loop)
+ arm.velocities = np.full(6, 10.0) # past every joint's limit
+ next(loop)
+
+ assert clock.now() < trossen_driver._MOVE_TIMEOUT_S, 'the move waited out its deadline'
+ with pytest.raises(RuntimeError, match='ran too fast'):
+ answer.result()
+
+
+def test_a_streamed_turn_is_walked_the_whole_way_by_an_arm_that_sags():
+ """A teleoperator turns the end effector over many ticks, and the setpoint moves a fraction of a degree
+ in each: the turn arrives however many that takes, on an arm holding itself up with a following error."""
+ arm = SaggingArm(position=np.append(HOME, 0.0))
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ here = _ee(states)
+ asked = geom.Transform3D(
+ here.translation, geom.Rotation.from_rotvec(np.array([0.0, np.radians(30), 0.0])) * here.rotation
+ )
+
+ for _ in range(1500): # a teleoperator's stream: the same target, tick after tick
+ commands.push(command.CartesianPosition(asked))
+ next(loop)
+
+ turned = np.degrees(np.linalg.norm((here.rotation.inv * _ee(states).rotation).as_rotvec))
+ assert turned > 25, turned
+
+
+def test_a_sync_move_to_a_pose_is_answered_like_any_other(world):
+ """A pose says which joints reach it once the driver solves for them, so a caller may wait on one.
+
+ Arrival is judged from the joints, within the following error the controller says it allows. The arm
+ carries that error holding itself up, so the tolerance cannot be tighter — and at half a metre of
+ reach it is centimetres at the end effector, which is why the pose is only checked that closely.
+ """
+ arm = FakeArm()
+ driver, states, loop = _driven(arm)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](driver)
+ wire_call(world, caller, driver.sync_move)
+ # Further than a streamed target is ever paced to, so a move that only stepped once would fall short
+ target = geom.Transform3D(_ee(states).translation + np.array([0.0, 0.05, -0.04]), _ee(states).rotation)
+
+ answer = caller(command.CartesianPosition(target))
+ for _ in range(400):
+ if answer.done():
+ break
+ next(loop)
+ answer.result()
+
+ np.testing.assert_allclose(_ee(states).translation, target.translation, atol=6e-2)
+
+
+def test_a_pose_far_from_where_the_arm_stands_is_solved_only_when_it_may_change_shape():
+ """The same pose is reachable with the arm in more than one shape, and moving between them swings it."""
+ kin = trossen_driver._Kinematics()
+ here = kin.fk(HOME)
+ far = geom.Transform3D(here.translation + np.array([-0.15, 0.15, -0.1]), here.rotation)
+
+ assert kin.ik(far, HOME, max_jump=TOLERANCE) is None
+ assert kin.ik(far, HOME) is not None
+
+
+def test_a_target_the_arm_has_drooped_away_from_is_still_solved():
+ """A teleoperator's target starts at what the arm reads, which stands off from what it was asked for."""
+ kin = trossen_driver._Kinematics()
+ drooped = HOME + np.array([0.0, 0.08, -0.06, -0.05, 0.0, 0.0]) # the following error measured on the arm
+
+ assert kin.ik(kin.fk(drooped), HOME, max_jump=TOLERANCE) is not None
+
+
+def test_a_stream_of_poses_the_arm_cannot_follow_says_so_once(caplog, monkeypatch):
+ """The pose differs every tick and the fault that refuses it does not, so one complaint stands for all."""
+ arm = FakeArm()
+ clock = MockClock()
+ driver, _, loop = _driven(arm, clock)
+ commands = ManualCommandReceiver()
+ driver.commands._bind(commands)
+ _homed(loop)
+
+ monkeypatch.setattr(trossen_driver._Kinematics, 'ik', lambda self, target, current_q, max_jump=None: None)
+ for tick in range(5):
+ commands.push(command.CartesianPosition(geom.Transform3D(np.array([0.4, 0.001 * tick, 0.2]))))
+ next(loop)
+ clock.advance(1.0 / trossen_driver._HZ)
+
+ assert caplog.text.count('not applied') == 1
+
+
+def test_an_arm_reading_outside_a_joint_range_says_so_and_is_driven_anyway(caplog):
+ """The controller takes a margin past what it reports, so only it knows whether this one is too far."""
+ arm = FakeArm(position=np.append(np.zeros(6), -0.0062)) # the gripper past its own zero
+
+ _, states, loop = _driven(arm)
+ next(loop)
+
+ assert 'joint 6 reads' in caplog.text
+ assert states.emitted[-1][1].status == RobotStatus.AVAILABLE
diff --git a/positronic/drivers/roboarm/tests/test_trossen_leader.py b/positronic/drivers/roboarm/tests/test_trossen_leader.py
new file mode 100644
index 000000000..c11f883be
--- /dev/null
+++ b/positronic/drivers/roboarm/tests/test_trossen_leader.py
@@ -0,0 +1,260 @@
+"""What a leader arm publishes while the operator moves it, and what it asks of its controller."""
+
+from typing import Any
+
+import numpy as np
+import pytest
+
+import pimm
+from pimm.tests.testing import MockClock, wire_call
+from positronic import geom
+from positronic.drivers.roboarm import command, trossen_leader
+from positronic.drivers.roboarm.tests.fakes import StopFlag
+from positronic.tests.testing_coutils import ManualCommandReceiver, RecordingEmitter
+
+GRIP_TRAVEL_M = 0.04 # the gripper joint's range, which the arm reports and grip is normalized against
+GRIPPER_FRICTION = 5.77 # the friction constant term the station's leaders carry, in N
+HELD = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, GRIP_TRAVEL_M]
+
+
+class FakeLeader:
+ """The slice of ``TrossenArmDriver`` the leader driver uses, under the test's control.
+
+ ``raises`` is what every call to the arm raises, as a controller that has faulted or a link that has
+ dropped does.
+ """
+
+ class _Limit:
+ def __init__(self, lower: float, upper: float):
+ self.position_min, self.position_max = lower, upper
+
+ class _Characteristic:
+ def __init__(self, friction_constant_term: float):
+ self.friction_constant_term = friction_constant_term
+
+ def __init__(self, positions: list[float] | None = None):
+ self.positions = list(positions if positions is not None else HELD)
+ self.modes: list[Any] = []
+ self.efforts: list[list[float]] = []
+ self.moves: list[tuple[list[float], float, bool]] = []
+ self.characteristics = [self._Characteristic(0.0)] * 6 + [self._Characteristic(GRIPPER_FRICTION)]
+ self.friction_terms: list[float] = []
+ self.raises: Exception | None = None
+ self.cleaned_up = False
+
+ def get_joint_characteristics(self):
+ return self.characteristics
+
+ def set_joint_characteristics(self, characteristics) -> None:
+ if self.raises is not None:
+ raise self.raises
+ self.characteristics = list(characteristics)
+ self.friction_terms.append(characteristics[6].friction_constant_term)
+
+ def get_joint_limits(self):
+ return [self._Limit(-3.14, 3.14)] * 6 + [self._Limit(0.0, GRIP_TRAVEL_M)]
+
+ def set_all_modes(self, mode) -> None:
+ if self.raises is not None:
+ raise self.raises
+ self.modes.append(mode)
+
+ def set_all_external_efforts(self, efforts, goal_time=2.0, blocking=True) -> None:
+ if self.raises is not None:
+ raise self.raises
+ self.efforts.append(list(efforts))
+
+ def set_all_positions(self, goal_positions, goal_time=2.0, blocking=True) -> None:
+ if self.raises is not None:
+ raise self.raises
+ self.moves.append((list(goal_positions), float(goal_time), bool(blocking)))
+ self.positions = list(goal_positions)
+
+ def get_all_positions(self):
+ if self.raises is not None:
+ raise self.raises
+ return list(self.positions)
+
+ def cleanup(self) -> None:
+ self.cleaned_up = True
+
+
+def build(arm: FakeLeader, **kwargs):
+ """The leader over ``arm``, with recorders on what it publishes and the clock under the test."""
+ leader = trossen_leader.Leader('192.168.1.3', connect=lambda _ip: arm, **kwargs)
+ joints, grips = RecordingEmitter(), RecordingEmitter()
+ leader.joints._bind(joints)
+ leader.grip._bind(grips)
+ return leader, joints, grips, MockClock(), StopFlag()
+
+
+def drive(leader, clock: MockClock, stop: StopFlag, *, ticks: int) -> None:
+ """Run ``ticks`` of the leader's loop, then let it finish the way a stopped run does."""
+ run = leader.run(stop, clock)
+ for _ in range(ticks):
+ next(run)
+ clock.advance(0.01)
+ stop.stopped = True
+ for _ in run:
+ pass
+
+
+def test_the_leader_publishes_the_joints_the_operator_moves_it_to():
+ """The arm joints are the whole of what a follower is asked for, so they go out as they read — the
+ gripper is not among them, since it is a grip and travels on its own port."""
+ arm = FakeLeader()
+ leader, joints, _grips, clock, stop = build(arm)
+
+ drive(leader, clock, stop, ticks=3)
+
+ assert len(joints.emitted) == 3
+ for _ts, published in joints.emitted:
+ np.testing.assert_allclose(published, HELD[:6])
+
+
+def test_a_leader_nobody_drives_is_left_for_the_hand_that_holds_it():
+ """The operator moves this arm. A leader held in position mode fights the hand on it, so the driver
+ reads the arm and asks nothing of it."""
+ arm = FakeLeader()
+ leader, _joints, _grips, clock, stop = build(arm)
+
+ drive(leader, clock, stop, ticks=3)
+
+ names = [mode.name for mode in arm.modes]
+ assert names[0] == 'external_effort', 'the arm was not freed for the operator to move'
+ assert 'position' not in names, f'the leader was driven, not followed: {names}'
+ assert not arm.moves, 'the leader was sent somewhere nobody asked for'
+ assert names[-1] == 'idle', 'the run left the arm holding itself up'
+ assert arm.cleaned_up
+
+
+def test_a_move_the_session_asks_for_drives_the_leader_and_gives_it_back(world):
+ """Both arms travel to the pose the session opens on, so the follower has no gap to take up when it
+ copies its leader. The arm is free in the operator's hand again the moment it arrives."""
+ arm = FakeLeader()
+ leader, _joints, _grips, clock, stop = build(arm)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](leader)
+ wire_call(world, caller, leader.sync_move)
+ target = np.array([0.0, 1.571, 1.178, 0.0, 0.0, 0.0])
+
+ answer = caller(command.JointPosition(target))
+ drive(leader, clock, stop, ticks=2)
+
+ answer.result()
+ goal, seconds, blocking = arm.moves[-1]
+ np.testing.assert_allclose(goal[:6], target)
+ assert goal[6] == pytest.approx(HELD[6]), 'the gripper was moved out of the hand that holds it'
+ assert blocking, 'the arm was left travelling with the driver reading it as held'
+ assert seconds == pytest.approx(np.max(np.abs(target - np.array(HELD[:6]))) / trossen_leader._MOVE_SPEED)
+ names = [mode.name for mode in arm.modes]
+ assert names[names.index('position') + 1] == 'external_effort', 'the arm was left holding itself'
+
+
+def test_a_leader_is_not_driven_to_a_pose(world):
+ """A leader has no kinematics: the joints it reads are the whole of what its follower is asked for.
+ A pose would have to be solved for, and the arm goes on being read either way."""
+ arm = FakeLeader()
+ leader, joints, _grips, clock, stop = build(arm)
+ caller = pimm.calls.ControlSystemCaller[command.CommandType, None](leader)
+ wire_call(world, caller, leader.sync_move)
+
+ answer = caller(command.CartesianPosition(geom.Transform3D()))
+ drive(leader, clock, stop, ticks=2)
+
+ with pytest.raises(NotImplementedError):
+ answer.result()
+ assert joints.emitted, 'a target the leader cannot take ended the run'
+ assert not arm.moves
+
+
+@pytest.mark.parametrize(
+ ('position', 'grip'),
+ [(0.0, 1.0), (GRIP_TRAVEL_M, 0.0), (GRIP_TRAVEL_M / 2, 0.5), (-0.001, 1.0), (GRIP_TRAVEL_M + 0.001, 0.0)],
+ ids=['closed', 'open', 'halfway', 'past closed', 'past open'],
+)
+def test_the_trigger_reads_as_the_grip_the_follower_speaks(position: float, grip: float):
+ """positronic speaks a grip where 1 is closed, and the trigger reads a little past its range at either
+ end — which saturates rather than asking the follower for a grip it has no room for."""
+ arm = FakeLeader([*HELD[:6], position])
+ leader, _joints, grips, clock, stop = build(arm)
+
+ drive(leader, clock, stop, ticks=1)
+
+ assert grips.emitted[0][1] == pytest.approx(grip)
+
+
+def test_a_leader_pushes_back_with_nothing_until_the_follower_says_what_it_holds():
+ """Zero external effort is gravity compensation: the controller holds the arm's own weight and the
+ operator moves it freely. That is what a rig without force feedback runs."""
+ arm = FakeLeader()
+ leader, _joints, _grips, clock, stop = build(arm)
+
+ drive(leader, clock, stop, ticks=2)
+
+ assert arm.efforts, 'the arm was never told what to push with'
+ for asked in arm.efforts:
+ np.testing.assert_allclose(asked, np.zeros(7))
+
+
+def test_force_feedback_pushes_back_what_the_follower_is_holding():
+ """What the operator feels is the follower's own effort, reversed: the follower pushes into the world
+ and the leader pushes into the hand."""
+ arm = FakeLeader()
+ leader, _joints, _grips, clock, stop = build(arm, force_feedback_gain=0.1)
+ held = np.array([1.0, -2.0, 3.0, 0.0, 0.0, 0.0, 0.5])
+ efforts = ManualCommandReceiver()
+ efforts.push(held)
+ leader.follower_efforts._bind(efforts)
+
+ drive(leader, clock, stop, ticks=2)
+
+ np.testing.assert_allclose(arm.efforts[0], -0.1 * held)
+
+
+def test_the_gripper_friction_the_operator_asks_for_stands_only_for_the_run():
+ """The term is the arm's configuration, and it outlives the process that set it. A run that leaves it
+ raised hands the next one an arm that is not the arm it was calibrated as."""
+ arm = FakeLeader()
+ leader, _joints, _grips, clock, stop = build(arm, gripper_friction_constant=8.0)
+
+ drive(leader, clock, stop, ticks=2)
+
+ assert arm.friction_terms == [8.0, GRIPPER_FRICTION], 'the run did not hand the gripper back as it took it'
+
+
+def test_an_arm_asked_for_no_gripper_friction_keeps_what_it_was_calibrated_with():
+ """Every arm ships with its own calibration, and a station that names no preference has none to state."""
+ arm = FakeLeader()
+ leader, _joints, _grips, clock, stop = build(arm)
+
+ drive(leader, clock, stop, ticks=2)
+
+ assert not arm.friction_terms, 'the run wrote a characteristic nobody asked for'
+ assert arm.characteristics[6].friction_constant_term == GRIPPER_FRICTION
+
+
+def test_a_leader_that_stops_being_read_does_not_end_the_session():
+ """One arm of a rig going quiet is not a reason to lose the episode the rest of it is recording. The
+ leader complains, holds off, and takes the arm back when it answers again."""
+ arm = FakeLeader()
+ leader, joints, _grips, clock, stop = build(arm)
+ run = leader.run(stop, clock)
+
+ next(run)
+ arm.raises = RuntimeError('link down')
+ for _ in range(5):
+ next(run)
+ clock.advance(0.01)
+ published_while_down = len(joints.emitted)
+
+ arm.raises = None
+ clock.advance(trossen_leader._RECOVER_EVERY_S)
+ for _ in range(3):
+ next(run)
+ clock.advance(0.01)
+
+ assert published_while_down == 1, 'the leader went on publishing an arm it could not read'
+ assert len(joints.emitted) > published_while_down, 'the leader never took the arm back'
+ stop.stopped = True
+ for _ in run:
+ pass
diff --git a/positronic/drivers/roboarm/trossen.py b/positronic/drivers/roboarm/trossen.py
new file mode 100644
index 000000000..436ed797f
--- /dev/null
+++ b/positronic/drivers/roboarm/trossen.py
@@ -0,0 +1,1096 @@
+"""Driver for the Trossen WidowX AI arm — one Ethernet link carrying six joints plus the gripper.
+
+The controller firmware runs the servo loop, and this driver solves FK/IK itself against the vendored MJCF
+(``assets/mujoco/trossen_wxai/wxai_follower.xml``) at ``ee_site`` — the frame the controller reports its own
+Cartesian position in. So every command reaches the arm as joints, which is what lets the driver hold each
+one to its own velocity limit before it goes out: past that limit the controller faults and drops the arm.
+
+The firmware solves Cartesian goals too, but each one on its own, knowing nothing of the last. Near a
+workspace boundary — the arm at rest sits on the lower limit of two joints — successive solutions come from
+different branches and the arm tears itself between them. Solving here, warm-started from where the arm
+stands, is what keeps the joints continuous.
+
+The gripper is the 7th joint, a prismatic finger drive the controller reports in meters. positronic
+speaks a normalized grip where 1 is closed, so grip converts against the travel the arm reports for that
+joint instead of a constant.
+
+A link that drops takes the controller's TCP session with it, and the vendor driver does not open a new
+one. Telemetry and commands travel separately — the controller streams the first over UDP and takes the
+second over TCP — so the arm can be heard from while nothing reaches it. The driver opens a new session
+itself once either half stops working, and resumes from wherever it finds the arm.
+"""
+
+import contextlib
+import logging
+import xml.etree.ElementTree as ET
+from collections.abc import Callable, Iterator
+from pathlib import Path
+from types import SimpleNamespace
+from typing import Any, NamedTuple
+
+import mujoco as mj
+import numpy as np
+
+import pimm
+from positronic import geom
+from positronic.drivers import vendor_import
+from positronic.drivers.roboarm import keys as roboarm_keys
+from positronic.drivers.roboarm.models import DEFAULT_FRAME, add_default_frame
+from positronic.drivers.utils import DriverRun, MoveStatus
+from positronic.utils import package_assets_path
+
+from . import RobotStatus, State, command
+from .ik import qpos_from_site_pose
+
+# trossen_arm lives in the `trossen` extra, which the type-check environment does not install.
+with vendor_import(
+ 'trossen_arm', 'Trossen arm support', hint='Re-run with the trossen extra:\n uv run --locked --extra trossen ...\n'
+):
+ import trossen_arm # pyright: ignore[reportMissingImports]
+
+logger = logging.getLogger(__name__)
+
+_ARM_JOINTS = 6
+_GRIPPER_JOINT = 6
+_HZ = 100
+# Goal time for a streamed setpoint: one tick, so the firmware interpolates linearly across the gap to the
+# next one. At or below 0.001 s it applies the goal as a step instead, and asks the servo for whatever
+# acceleration closes the distance at once.
+_STREAM_GOAL_TIME_S = 1.0 / _HZ
+# What a move somebody waits on travels at. Above 0.2 s of goal time the firmware plans the whole move as a
+# quintic, which starts and stops the arm gently — so a move hands it the time and lets it do that.
+_MOVE_SPEED = 0.6 # rad/s
+_MIN_MOVE_TIME_S = 1.0
+_MOVE_TIMEOUT_S = 15.0 # the whole range of a joint at that speed, and time to settle after it
+# The share of the following error the controller allows within which the arm counts as arrived. It holds
+# itself up with that error, so a tolerance tighter than the droop is one no move ever meets.
+_ARRIVED_SHARE = 0.5
+# How long the controller's clock may stand still before the link counts as down. Telemetry arrives at
+# over 200 Hz, so this is many missed frames, not a scheduling hiccup.
+_STALE_AFTER_S = 0.25
+_CONNECT_TIMEOUT_S = 20.0
+# A reconnect runs on the control loop, so its timeout is what the loop stands still for on a failed attempt.
+_RECONNECT_TIMEOUT_S = 1.0
+_RECONNECT_AFTER_S = 0.5 # how long the link stays down before a new session is worth opening
+_RECONNECT_EVERY_S = 2.0 # and how often another is tried while it stays down
+# How often a failure that stands is worth saying again. A tick rate of complaints buries every other line.
+_COMPLAIN_EVERY_S = 5.0
+# A fault the controller latches is not cleared by opening another session, so the attempts back off rather
+# than stall the loop every couple of seconds for as long as the arm stays down.
+_RECONNECT_MAX_S = 30.0
+# The share of a joint's own velocity limit at which the driver stops driving. Past its limit the controller
+# faults and drops the arm to idle, so the driver stands down before it gets there.
+_VELOCITY_HEADROOM = 0.8
+# The share of a joint's velocity limit a streamed setpoint may ask for. It bounds what one wild target can
+# ask of the arm; the hand the arm follows paces it. Measured over an episode driven by a leader arm: at
+# 0.1 the arm held 1 rad/s where the operator asked for 2 to 3, fell as far as 0.8 rad behind, and took a
+# second to take that up.
+_COMMANDED_SHARE = 0.3
+# Where the driver puts the arm at either end of a run. The arm rests at all joints zero, which is the
+# lower limit of joints 1 and 2 and where half the directions out of it have no solution, so a run opens by
+# taking the arm off it. It closes back on it, because the controller holds the arm only there: set idle
+# anywhere else, the arm falls.
+_HOME_JOINTS = np.array([0.0, 1.571, 1.178, 0.0, 0.0, 0.0])
+_REST_JOINTS = np.zeros(6)
+_MJCF_PATH = 'assets/mujoco/trossen_wxai/wxai_follower.xml'
+_URDF_PATH = 'assets/mujoco/trossen_wxai/wxai_follower.urdf'
+_MESH_DIR = 'assets/mujoco/trossen_wxai/assets'
+_EE_SITE = 'ee_site'
+# The URDF link at the pose the controller reports, 0.156 m along the flange's x axis. It is `ee_site` of
+# the MJCF: the driver solves against the MJCF and the codecs against the URDF, so the two must agree.
+_EE_LINK = 'ee_gripper_link'
+# The carriage joints the fingers ride on. A positive joint value opens them, and the viewer closes a
+# gripper by driving its joints to `grip * travel` -- so the two cannot be reconciled by a travel alone.
+_FINGER_JOINTS = ('right_carriage_joint', 'left_carriage_joint')
+_JOINT_NAMES = ('joint_0', 'joint_1', 'joint_2', 'joint_3', 'joint_4', 'joint_5')
+_IK_POS_TOL = 1e-3 # meters; FK-verify acceptance for an IK solution after clamping
+_IK_ROT_TOL = 1e-2 # radians
+# How far a streamed Cartesian target may move in one tick. At the tick rate this allows 1.5 m/s, which is
+# faster than a hand moves, so it does not shape teleoperation.
+_MAX_STEP_M = 0.015
+_MAX_STEP_RAD = 0.08
+# How far that target may sit from where the arm reads. The arm holds itself up with a following error, so
+# it stands off from every pose it is asked for; this is what keeps the standoff from becoming a leash the
+# target drags. Wide enough for the droop, and narrow enough that a target cannot run away from an arm.
+_MAX_STANDOFF_M = 0.08
+_MAX_STANDOFF_RAD = 0.4
+
+
+class TrossenState(State, pimm.shared_memory.NumpySMAdapter):
+ Q_OFFSET = 0
+ DQ_OFFSET = Q_OFFSET + _ARM_JOINTS
+ EE_POSE_OFFSET = DQ_OFFSET + _ARM_JOINTS
+ STATUS_OFFSET = EE_POSE_OFFSET + 7
+ TOTAL = STATUS_OFFSET + 1
+
+ def __init__(self):
+ super().__init__(shape=(TrossenState.TOTAL,), dtype=np.dtype(np.float32))
+
+ def instantiation_params(self) -> tuple[Any, ...]:
+ return ()
+
+ @property
+ def q(self) -> np.ndarray:
+ return self.array[TrossenState.Q_OFFSET : TrossenState.Q_OFFSET + _ARM_JOINTS].copy()
+
+ @property
+ def dq(self) -> np.ndarray:
+ return self.array[TrossenState.DQ_OFFSET : TrossenState.DQ_OFFSET + _ARM_JOINTS].copy()
+
+ @property
+ def ee_pose(self) -> geom.Transform3D:
+ pose = self.array[TrossenState.EE_POSE_OFFSET : TrossenState.EE_POSE_OFFSET + 7].copy()
+ return geom.Transform3D(pose[:3], geom.Rotation.from_quat(pose[3:7]))
+
+ @property
+ def status(self) -> RobotStatus:
+ return RobotStatus(int(self.array[TrossenState.STATUS_OFFSET]))
+
+ def encode(self, q: np.ndarray, dq: np.ndarray, ee_pose: geom.Transform3D, status: RobotStatus) -> None:
+ self.array[TrossenState.Q_OFFSET : TrossenState.Q_OFFSET + _ARM_JOINTS] = q
+ self.array[TrossenState.DQ_OFFSET : TrossenState.DQ_OFFSET + _ARM_JOINTS] = dq
+ self.array[TrossenState.EE_POSE_OFFSET : TrossenState.EE_POSE_OFFSET + 3] = ee_pose.translation
+ self.array[TrossenState.EE_POSE_OFFSET + 3 : TrossenState.EE_POSE_OFFSET + 7] = ee_pose.rotation.as_quat
+ self.array[TrossenState.STATUS_OFFSET] = status.value
+
+
+class _Kinematics:
+ """FK/IK on the vendored MJCF at ``ee_site``, in the arm base frame.
+
+ ``mujoco`` exports every symbol below from a compiled extension, so a type checker cannot see them.
+ """
+
+ def __init__(self):
+ self._model = mj.MjModel.from_xml_path(package_assets_path(_MJCF_PATH))
+ self._data = mj.MjData(self._model)
+ self._site_id = mj.mj_name2id(self._model, mj.mjtObj.mjOBJ_SITE, _EE_SITE)
+ self._qpos_ids = np.array([self._model.joint(name).qposadr.item() for name in _JOINT_NAMES])
+ self._dof_ids = np.array([self._model.joint(name).dofadr.item() for name in _JOINT_NAMES])
+ ranges = np.array([self._model.joint(name).range for name in _JOINT_NAMES])
+ self.lower, self.upper = ranges[:, 0], ranges[:, 1]
+
+ def fk(self, q: np.ndarray) -> geom.Transform3D:
+ self._data.qpos[self._qpos_ids] = q
+ mj.mj_kinematics(self._model, self._data)
+ quat = np.empty(4)
+ mj.mju_mat2Quat(quat, self._data.site_xmat[self._site_id].copy())
+ return geom.Transform3D(self._data.site_xpos[self._site_id].copy(), geom.Rotation.from_quat(quat))
+
+ @staticmethod
+ def _reach_postures(x: float, y: float) -> list[np.ndarray]:
+ """IK warm-start candidates for reaching toward base-frame point (x, y).
+
+ Joint 0 swung to the target's azimuth, the arm unfolded to two heights. The arm rests on the lower
+ limit of joints 1 and 2, where half the directions have no solution at all, so a seed away from
+ there is what lets IK find one.
+ """
+ azimuth = np.arctan2(y, x)
+ return [np.array([azimuth, 1.571, 1.178, 0.0, 0.0, 0.0]), np.array([azimuth, 1.2, 1.5, 0.0, 0.4, 0.0])]
+
+ def ik(
+ self, target: geom.Transform3D, current_q: np.ndarray, max_jump: float | np.ndarray | None = None
+ ) -> np.ndarray | None:
+ """LM IK for ``target``, warm-started from where the arm stands.
+
+ ``max_jump`` bounds how far the solution may sit from ``current_q``, per joint or over all of them,
+ and searching stops there: the arm keeps the shape it has, and a pose it can only reach in another
+ one comes back as nothing.
+ Without it the reach postures are tried too, so the arm may change shape to get there — which
+ swings the end effector, and is only for a move somebody asked for and waits on.
+
+ Solutions are clamped into joint range and FK-verified before acceptance, so a target the arm cannot
+ reach comes back as nothing rather than as the nearest thing the solver stopped at.
+
+ A target that is reached costs about a quarter of a millisecond, and one that is not costs every
+ seed's full search — more than a tick.
+ """
+ seeds = (current_q,) if max_jump is not None else (current_q, *self._reach_postures(*target.translation[:2]))
+ for start in seeds:
+ self._data.qpos[:] = 0.0
+ self._data.qpos[self._qpos_ids] = start
+ qpos, _, success = qpos_from_site_pose(
+ self._model,
+ self._data,
+ self._site_id,
+ self._dof_ids,
+ target.translation,
+ target.rotation.as_quat,
+ rot_weight=0.5,
+ )
+ if not success:
+ continue
+ q = np.clip(qpos[self._qpos_ids].copy(), self.lower, self.upper)
+ if max_jump is not None and np.any(np.abs(q - current_q) > max_jump):
+ continue
+ reached = self.fk(q)
+ turn = (reached.rotation.inv * target.rotation).as_rotvec
+ angle = float(np.linalg.norm(turn))
+ angle = min(angle, 2 * np.pi - angle)
+ if np.linalg.norm(reached.translation - target.translation) < _IK_POS_TOL and angle < _IK_ROT_TOL:
+ return q
+ return None
+
+
+def _configure(driver: Any, ip: str, timeout_s: float) -> None:
+ """Open a session with the controller, clearing an error a previous one left behind."""
+ end_effector = trossen_arm.StandardEndEffector.wxai_v0_follower
+ driver.configure(trossen_arm.Model.wxai_v0, end_effector, ip, True, timeout_s)
+
+
+class _Arm(DriverRun[command.CommandType]):
+ """The arm the driver drives: the controller handle, the reading it takes each tick, and the setpoint
+ it holds the arm at.
+
+ The controller reports position but not whether it is tracking a goal, so arrival is judged from the
+ reading. A setpoint is written only when something has asked for a new one: a goal time re-sent every
+ tick restarts the trajectory it plans, and the arm would never arrive.
+ """
+
+ def __init__(
+ self,
+ driver: Any,
+ ip: str,
+ sync_move: pimm.calls.ControlSystemHandler[command.CommandType, None],
+ async_move: pimm.SignalReceiver[command.CommandType],
+ out: pimm.SignalEmitter[TrossenState],
+ grip_out: pimm.SignalEmitter[float],
+ should_stop: pimm.SignalReceiver,
+ clock: pimm.Clock,
+ ):
+ super().__init__(sync_move, async_move, should_stop, clock, hz=_HZ)
+ self.driver = driver
+ self.ip = ip
+ self.out = out
+ self.grip_out = grip_out
+ self.state = TrossenState()
+ limits = driver.get_joint_limits()
+ self._q_lower = np.array([limits[i].position_min for i in range(_ARM_JOINTS)])
+ self._q_upper = np.array([limits[i].position_max for i in range(_ARM_JOINTS)])
+ # The gripper joint's own travel, which grip is normalized against
+ self._grip_travel = float(limits[_GRIPPER_JOINT].position_max - limits[_GRIPPER_JOINT].position_min)
+ self._grip_closed = float(limits[_GRIPPER_JOINT].position_min)
+ self._grip_open = float(limits[_GRIPPER_JOINT].position_max)
+ # What the fingers may travel in a tick, as a share of the range grip is normalized against. The
+ # whole range asked for at once is metres a second past the limit, which is what faults a joint.
+ grip_speed = float(limits[_GRIPPER_JOINT].velocity_max) * _VELOCITY_HEADROOM
+ self._grip_step_max = grip_speed / _HZ / self._grip_travel
+ self._dq_limit = np.array([limits[i].velocity_max for i in range(_ARM_JOINTS)])
+ self._dq_max = self._dq_limit * _VELOCITY_HEADROOM
+ self._step_max = self._dq_limit * _COMMANDED_SHARE / _HZ # what a streamed setpoint may move in a tick
+ tolerance = np.array([limits[i].position_tolerance for i in range(_ARM_JOINTS)])
+ self._arrived_tol = float(np.min(tolerance) * _ARRIVED_SHARE)
+ # How far a streamed target's solution may sit from the joints last asked for. Further than this the
+ # same pose is reached with the arm in another shape, and moving into one swings the end effector.
+ # The arm holds itself up with a following error, so what it reads stands off from what it was asked
+ # for by as much as the controller allows — and a teleoperator's target starts at what the arm reads.
+ # A cap tighter than that error refuses every one of those targets, so the error the controller
+ # reports is the cap.
+ self._jump_max = tolerance
+ # What a delta still owes: a delta is consumed once, and its travel takes more than the one step a
+ # streamed pose is paced to
+ self._travel_to: geom.Transform3D | None = None
+ # Whether the arm is in position mode. `set_all_positions` needs it, and the controller refuses a
+ # goal without it -- which reads as a dead command channel and sends the run through recovery.
+ self.controlled = False
+ self._output = driver.get_robot_output()
+ self._kin = _Kinematics()
+ self._target = np.asarray(self._output.joint.arm.positions, dtype=np.float64)
+ self._wanted = self._target.copy()
+ self._goal_time = _STREAM_GOAL_TIME_S
+ self._anchor: geom.Transform3D | None = None # the pose last asked for, which the next steps on from
+ self._grip_target = self._grip_wanted = self._grip_of(self._output)
+ self._arm_unsent, self._grip_unsent = False, False
+ # The two halves of the link, which fail apart. Neither is `Moves.errored`, which says the arm is
+ # not where the driver put it: a link that drops says nothing about the move.
+ self._stream_stale = False # the controller's clock stands still, so its telemetry stopped arriving
+ self._command_dead = False # a write was refused, and only a new session takes another
+ self._down_since: float | None = None
+ self._reconnect_at = -_RECONNECT_EVERY_S
+ self._reconnect_every = _RECONNECT_EVERY_S
+ self._stamp = int(self._output.header.timestamp)
+ self._stamp_at = clock.now()
+ self.overspeed = False
+ self._complaint = ''
+ self._complained_at = -_COMPLAIN_EVERY_S
+
+ # TODO(#686): a rate limit on a log line belongs in the logging layer, as `log_every_n_sec`, where every
+ # driver reaches it.
+ def complain(self, message: str, key: str | None = None, level: int = logging.ERROR) -> None:
+ """Say what is wrong, but not on every tick of a fault that stands.
+
+ ``key`` names the fault where the message alone cannot: a refused setpoint carries the pose, which
+ differs every tick, and the fault that refuses it does not.
+ """
+ now = self.clock.now()
+ key = key if key is not None else message
+ if key == self._complaint and now - self._complained_at < _COMPLAIN_EVERY_S:
+ return
+ self._complaint, self._complained_at = key, now
+ logger.log(level, message)
+
+ @property
+ def link_down(self) -> bool:
+ """Whether the arm is out of reach, either way round."""
+ return self._stream_stale or self._command_dead
+
+ def _note_link(self, now: float) -> None:
+ """Keep when the link went down, which is what a reconnect waits on."""
+ if not self.link_down:
+ self._down_since = None
+ elif self._down_since is None:
+ self._down_since = now
+
+ def _grip_of(self, output: Any) -> float:
+ """How closed the fingers are, from the joint position the controller reports.
+
+ The reading sits a little outside the joint range at either end, so it saturates to the 0..1 the
+ ``grip`` port carries.
+ """
+ travelled = (float(output.joint.gripper.position) - self._grip_closed) / self._grip_travel
+ return float(np.clip(1.0 - travelled, 0.0, 1.0))
+
+ def limit_violation(self) -> str:
+ """What reads outside the range the controller reports for it, if anything.
+
+ The controller takes a margin past what it reports — a gripper reading a millimetre below its zero
+ is driven without complaint — but far enough past, and entering position mode faults it and drops
+ the arm, whatever it is then told to do. How much further is not something the SDK says, so this
+ only says what it sees, next to the fault it may explain.
+ """
+ readings = [*self._output.joint.arm.positions, self._output.joint.gripper.position]
+ limits = [*zip(self._q_lower, self._q_upper, strict=True), (self._grip_closed, self._grip_open)]
+ for i, (value, (lower, upper)) in enumerate(zip(readings, limits, strict=True)):
+ if not lower <= value <= upper:
+ return f'joint {i} reads {value:.4f}, outside [{lower:.4f}, {upper:.4f}]'
+ return ''
+
+ def take_control(self) -> None:
+ """Put the arm in position mode holding where it reads, and say so through ``controlled``.
+
+ The mode change comes first and the setpoint immediately after, so the servo has a goal from the
+ tick it starts servoing. Reading first is what makes a session opened mid-run resume without a jump:
+ the arm is wherever it ended up, not where the last session was driving it.
+ """
+ self._output = self.driver.get_robot_output()
+ dq = np.abs(np.asarray(self._output.joint.arm.velocities, dtype=np.float64))
+ self.overspeed = bool(np.any(dq > self._dq_max))
+ if self.overspeed:
+ # Position mode on a joint already past its limit is what faults the controller and drops the
+ # arm. `controlled` stays false, so nothing is written and `run` asks again once it has slowed.
+ self.complain(f'The arm at {self.ip} runs too fast to take control of; waiting for it to slow')
+ return
+ if outside := self.limit_violation():
+ self.complain(f'The arm at {self.ip} may refuse position mode: {outside}')
+ self._target = self._wanted = self.q
+ self._goal_time = _STREAM_GOAL_TIME_S
+ self._grip_target = self._grip_wanted = self._grip_of(self._output)
+ self.driver.set_all_modes(trossen_arm.Mode.position)
+ self.controlled = True
+ self._anchor = None # wherever the arm is now is what a Cartesian target steps on from
+ self._travel_to = None # and nothing is owed the rest of a travel across a new session
+ self._arm_unsent, self._grip_unsent = True, True
+ self.write()
+
+ def __enter__(self) -> '_Arm':
+ self.take_control()
+ return self
+
+ def __exit__(self, exc_type, exc: BaseException | None, tb) -> None:
+ """Answer whatever was waiting on a move; ``_opened`` takes the arm back to idle."""
+ self.moves.abandon(exc)
+
+ def read(self) -> None:
+ """Take the whole arm off the link, once a tick.
+
+ The controller streams its telemetry and the read hands back the last of it, so a link that drops
+ does not raise — it repeats itself. The controller's own clock is what says the stream stopped.
+ """
+ now = self.clock.now()
+ try:
+ self._output = self.driver.get_robot_output()
+ # rules-allow: swallowed-error — a link that drops reads ERROR; the run outlives it, and a new
+ # session clears it
+ except trossen_arm.RuntimeError as exc:
+ self.complain(f'The arm at {self.ip} did not answer: {exc}')
+ self._stream_stale = True
+ self._note_link(now)
+ return
+ stamp = int(self._output.header.timestamp)
+ if stamp != self._stamp:
+ self._stamp, self._stamp_at = stamp, now
+ self._stream_stale = now - self._stamp_at > _STALE_AFTER_S
+ self._note_link(now)
+ dq = np.abs(np.asarray(self._output.joint.arm.velocities, dtype=np.float64))
+ was_overspeed, self.overspeed = self.overspeed, bool(np.any(dq > self._dq_max))
+ if self.overspeed and not was_overspeed:
+ fastest = int(np.argmax(dq / self._dq_max))
+ self.complain(f'Joint {fastest} of the arm at {self.ip} runs at {dq[fastest]:.2f} rad/s; standing down')
+
+ @property
+ def q(self) -> np.ndarray:
+ return np.asarray(self._output.joint.arm.positions, dtype=np.float64)
+
+ @property
+ def ee_pose(self) -> geom.Transform3D:
+ """Where the joints put the end effector, in the arm base frame.
+
+ Solved here rather than read from the controller so that the pose that goes out and the pose a
+ command is solved against are the same frame by construction. The two agree to 0.13 mm anyway.
+ """
+ return self._kin.fk(self.q)
+
+ def settle(self) -> None:
+ """Judge a move in flight against what the controller reports."""
+ if not self.moves.active:
+ return
+ if self.moves.settle(self.q, self.clock.now()) is MoveStatus.GAVE_UP:
+ # Holding the target the arm stopped short of would resume the move once whatever blocked it
+ # goes away, long after its asker was told it failed.
+ self._target = self._wanted = self.q
+ self._goal_time, self._arm_unsent, self._travel_to = _STREAM_GOAL_TIME_S, True, None
+
+ def advance(self) -> None:
+ """Move the setpoint one tick's travel towards what was last asked for, fingers included.
+
+ A move the firmware is planning owns the setpoint until it arrives: it is making its own trajectory
+ there, and a setpoint moved under it would be a new move every tick.
+ """
+ grip_step = float(np.clip(self._grip_wanted - self._grip_target, -self._grip_step_max, self._grip_step_max))
+ if grip_step:
+ self._grip_target, self._grip_unsent = self._grip_target + grip_step, True
+ if self._goal_time != _STREAM_GOAL_TIME_S:
+ return
+ step = np.clip(self._wanted - self._target, -self._step_max, self._step_max)
+ if np.any(step):
+ self._target, self._arm_unsent = self._target + step, True
+
+ def hold_grip(self, grip: float) -> None:
+ """Hold the fingers at ``grip``, and refuse one that is not a number.
+
+ ``np.clip`` carries a NaN through, and the controller takes whatever finger position it is handed.
+ ``advance`` walks the setpoint there, by what the finger drive may travel in a tick.
+ """
+ if not np.isfinite(grip):
+ self.complain(f'The arm at {self.ip} was asked to grip at {grip}', key='grip refused')
+ return
+ self._grip_wanted, self._grip_unsent = float(np.clip(grip, 0.0, 1.0)), True
+
+ @property
+ def asked_pose(self) -> geom.Transform3D:
+ """The pose the arm was last asked to hold.
+
+ Not the pose it reads: it stands off from what it is given. A step measured from the reading and
+ solved from the joints last asked for spans that standoff, and asks for a jump a step never needs.
+ """
+ return self._anchor if self._anchor is not None else self._kin.fk(self._target)
+
+ @staticmethod
+ def _short_way(frm: geom.Rotation, to: geom.Rotation) -> np.ndarray:
+ """The rotation vector from ``frm`` to ``to``, the way round that turns least.
+
+ A quaternion and its negative are the same turn, and `as_rotvec` reads the negative one as nearly a
+ full turn the other way.
+ """
+ turn = (frm.inv * to).as_rotvec
+ angle = float(np.linalg.norm(turn))
+ return turn * (1.0 - 2.0 * np.pi / angle) if angle > np.pi else turn
+
+ @staticmethod
+ def _apart(frm: geom.Transform3D, to: geom.Transform3D) -> tuple[float, float]:
+ """How far ``to`` stands from ``frm``: metres, and radians the short way round."""
+ return float(np.linalg.norm(to.translation - frm.translation)), float(
+ np.linalg.norm(_Arm._short_way(frm.rotation, to.rotation))
+ )
+
+ @staticmethod
+ def _towards(frm: geom.Transform3D, to: geom.Transform3D, max_m: float, max_rad: float) -> geom.Transform3D:
+ """``to``, brought within ``max_m`` and ``max_rad`` of ``frm``."""
+ step, turn = to.translation - frm.translation, _Arm._short_way(frm.rotation, to.rotation)
+ distance, angle = _Arm._apart(frm, to)
+ if distance > max_m:
+ step = step * (max_m / distance)
+ if angle > max_rad:
+ turn = turn * (max_rad / angle)
+ return geom.Transform3D(frm.translation + step, frm.rotation * geom.Rotation.from_rotvec(turn))
+
+ def _stepped(self, target: geom.Transform3D) -> geom.Transform3D:
+ """``target``, one tick's travel on from the pose the arm was last asked for.
+
+ Stepping on from the last pose asked for, rather than from the pose the arm reads, is what keeps
+ this from being a loop. The arm stands off from every pose it is given, by the following error it
+ holds itself up with, and that standoff grows as the arm reaches further out — so a target measured
+ from the reading walks itself outwards, further every tick.
+
+ The standoff is bounded instead: the pose stepped on from is first pulled back to within reach of
+ the arm, so a target still cannot run away from one that is held up.
+ """
+ anchor = self._towards(self.ee_pose, self.asked_pose, _MAX_STANDOFF_M, _MAX_STANDOFF_RAD)
+ return self._towards(anchor, target, _MAX_STEP_M, _MAX_STEP_RAD)
+
+ # TODO(#685): take which kinematics to use as a constructor argument. The controller solves Cartesian
+ # goals itself, through `set_cartesian_positions`, and a station may want that path.
+ def _ik(self, pose: geom.Transform3D, *, streamed: bool) -> np.ndarray:
+ """The joints that reach ``pose``; raises what the arm cannot reach.
+
+ A streamed pose is paced to a step at a time and solved without letting the arm change shape. One
+ somebody waits on is solved as it stands, and may change shape to get there.
+ """
+ if not streamed:
+ solution = self._kin.ik(pose, self.q)
+ if solution is None:
+ raise ValueError(f'{pose} is out of reach')
+ return solution
+ # Solved from the joints last asked for, not from the ones read back: the arm stands off from what
+ # it is given, and measuring against the reading would make the room for a step have to cover that
+ # standoff too — which is room enough to change the arm's shape in.
+ stepped = self._stepped(pose)
+ solution = self._kin.ik(stepped, self._target, max_jump=self._jump_max)
+ if solution is None:
+ raise ValueError(f'{pose} is out of reach')
+ # Only a pose that solved may be stepped on from: an anchor that walked on past one out of reach
+ # would leave every pose after it further out of reach than the last.
+ self._anchor = stepped
+ return solution
+
+ def _target_of(self, cmd: command.CommandType, *, streamed: bool = True) -> np.ndarray:
+ """The joints ``cmd`` asks the arm to hold, clipped to the range the controller reports."""
+ # TODO: accept the modes the arm can run instead of leaving them to what a command omits. Its joints
+ # are position-servoed, so `PositionControl` names the law already running.
+ command.require_native_mode(cmd, 'Trossen')
+ match cmd:
+ # A joint-space target moves the arm off whatever pose a Cartesian stream last asked for, so
+ # the anchor the next Cartesian step measures from is where those joints put the end effector.
+ case command.JointPosition(positions):
+ target, self._anchor = np.asarray(positions, dtype=np.float64), None
+ case command.JointDelta(velocities=delta):
+ target = (self._target if streamed else self.q) + np.asarray(delta, dtype=np.float64)
+ self._anchor = None
+ case command.CartesianPosition(pose):
+ target = self._ik(pose, streamed=streamed)
+ case command.CartesianDelta() as delta_cmd:
+ from_pose = self.asked_pose if streamed else self.ee_pose
+ target = self._ik(delta_cmd.apply(from_pose), streamed=streamed)
+ case other:
+ raise NotImplementedError(f'Unsupported command {other}')
+ # A shorter target broadcasts across the limits and moves every joint; a NaN clips to itself.
+ if target.shape != (len(_JOINT_NAMES),) or not np.all(np.isfinite(target)):
+ raise ValueError(f'{cmd} asks the arm for {target}, and not for {len(_JOINT_NAMES)} finite joints')
+ clipped = np.clip(target, self._q_lower, self._q_upper)
+ # The nearest joints the arm can hold are still worth moving to -- a leader pushed past the
+ # follower's range should carry it as far as the range goes -- but the operator hears about it.
+ if (outside := np.flatnonzero(clipped != target)).size:
+ i = int(outside[0])
+ self.complain(
+ f'Joint {i} of the arm at {self.ip} was asked for {target[i]:.4f}, outside '
+ f'[{self._q_lower[i]:.4f}, {self._q_upper[i]:.4f}]; it holds at {clipped[i]:.4f}',
+ key='target clipped',
+ level=logging.WARNING,
+ )
+ return clipped
+
+ def track(self, cmd: command.CommandType) -> None:
+ """Hold the arm at the setpoint ``cmd`` asks for, with nobody waiting on the arrival.
+
+ Held to what a joint may travel in a tick. Teleoperation is paced by the hand it follows, so this
+ only bounds what one wild target can ask the arm for.
+ """
+ # Composed before `_target_of`, which steps the anchor a delta is measured from, and kept only
+ # once that has taken the command: one it refuses asks the arm for no travel.
+ goal = cmd.apply(self.asked_pose) if isinstance(cmd, command.CartesianDelta) else None
+ self._wanted = self._target_of(cmd)
+ self._travel_to = goal
+ self._goal_time = _STREAM_GOAL_TIME_S
+
+ def walk(self) -> None:
+ """Take one more step of the travel a delta asked for, where one is still owed.
+
+ A streamed pose is paced to a step a tick and a delta is consumed once, so a delta longer than that
+ step would arrive as that step and lose the rest. A pose asks for no travel: the next one supersedes
+ it, and an arm whose stream stops holds where it stands.
+ """
+ if self._travel_to is None or self._goal_time != _STREAM_GOAL_TIME_S:
+ return
+ distance, angle = self._apart(self.asked_pose, self._travel_to)
+ goal, last = self._travel_to, distance <= _MAX_STEP_M and angle <= _MAX_STEP_RAD
+ if last:
+ self._travel_to = None
+ try:
+ self._wanted = self._target_of(command.CartesianPosition(goal))
+ # rules-allow: swallowed-error — a travel the arm cannot finish ends here, and the run outlives it
+ except ValueError as exc:
+ self._travel_to = None
+ self.complain(f'The arm at {self.ip} stopped short of {goal}: {exc}', key='travel refused')
+
+ def taking_control(self) -> Iterator[pimm.Command]:
+ """Yield until the arm is in position mode, asking again each tick it is not.
+
+ An arm still running too fast to take is left alone: a goal sent before the mode change is refused,
+ and the refusal reads as a dead command channel and sends the run through link recovery.
+ """
+ while not self.controlled and not self.should_stop.value:
+ self.publish()
+ yield self.limiter.wait()
+ self.read()
+ if not self.link_down:
+ self.take_control()
+
+ def travel_to(self, joints: np.ndarray, what: str, *, at_teardown: bool = False) -> Iterator[pimm.Command]:
+ """Put the arm at ``joints`` and yield until it reads back there.
+
+ Nobody waits on this, so a travel that does not arrive is logged and the run goes on. ``at_teardown``
+ is for the travel the driver makes on its way out: the stop is set by then, and heeding it would
+ abandon the travel before it began.
+ """
+ target = np.clip(np.asarray(joints, dtype=np.float64), self._q_lower, self._q_upper)
+ self._target = self._wanted = target
+ self._anchor = self._travel_to = None
+ self._goal_time = max(_MIN_MOVE_TIME_S, float(np.max(np.abs(target - self.q))) / _MOVE_SPEED)
+ self._arm_unsent = True
+ logger.info(f'The arm at {self.ip} travels to {what}, in {self._goal_time:.1f} s')
+ deadline = self.clock.now() + _MOVE_TIMEOUT_S
+ try:
+ while at_teardown or not self.should_stop.value:
+ self.publish() # a run says where the arm stands from its first tick, travel or no travel
+ self.write()
+ yield self.limiter.wait()
+ self.read()
+ if bool(np.all(np.abs(self.q - target) < self._arrived_tol)):
+ return
+ if self.link_down:
+ logger.error(f'The arm at {self.ip} stopped answering on its way to {what}')
+ return
+ if self.clock.now() >= deadline:
+ logger.error(f'The arm at {self.ip} stopped at {np.round(self.q, 3)}, short of {what}')
+ return
+ finally:
+ self._goal_time = _STREAM_GOAL_TIME_S # what a streamed setpoint is paced by
+
+ def sync_move(self, call: pimm.calls.Call[command.CommandType, None]) -> None:
+ """Hold the arm where ``call`` asks; ``settle`` answers it once the controller reads back there.
+
+ The whole move goes to the firmware with the time to make it in, so the firmware plans it: above
+ 0.2 s of goal time that is a quintic, which starts and stops the arm gently. Nothing this driver
+ does itself is as smooth, and a move somebody waits on is exactly where that shows.
+ """
+ with pimm.calls.raise_to(call):
+ target = self._target_of(call.request, streamed=False)
+ travel = float(np.max(np.abs(target - self.q)))
+ self._target = self._wanted = target
+ # The arm ends this move where the plan puts it, so that is what the next Cartesian step
+ # measures from, and nothing is owed the rest of a travel the move interrupted.
+ self._anchor = self._travel_to = None
+ self._goal_time = max(_MIN_MOVE_TIME_S, travel / _MOVE_SPEED)
+ self._arm_unsent = True
+ self.moves.accept(call, target, self._arrived_tol, self.clock.now(), _MOVE_TIMEOUT_S)
+
+ def _grip_metres(self, grip: float) -> float:
+ """The gripper joint position that holds the fingers at ``grip``."""
+ return self._grip_closed + (1.0 - grip) * self._grip_travel
+
+ def _put_goal(self, setpoint: np.ndarray, move_arm: bool) -> None:
+ """Hand the controller this tick's setpoint, fingers included where one call carries both.
+
+ All seven joints are in position mode, which ``set_all_positions`` requires; the fingers take their
+ own call where the arm is already being held where it is asked to be.
+ """
+ grip_m = self._grip_metres(self._grip_target)
+ if move_arm:
+ self.driver.set_all_positions([*setpoint, grip_m], self._goal_time, False)
+ else:
+ self.driver.set_gripper_position(grip_m, _STREAM_GOAL_TIME_S, False)
+
+ def write(self) -> None:
+ """Put the setpoint on the link, if anything has asked for one since it was last written."""
+ if not self.controlled or not (self._arm_unsent or self._grip_unsent):
+ return
+ try:
+ self._put_goal(self._target, self._arm_unsent)
+ # rules-allow: swallowed-error — a link that refuses a write reads ERROR; the setpoint stays unsent
+ # and goes out again on the next session
+ except trossen_arm.RuntimeError as exc:
+ self.complain(f'The arm at {self.ip} did not take the setpoint: {exc}')
+ self._command_dead = True
+ self._note_link(self.clock.now())
+ return
+ self._arm_unsent, self._grip_unsent = False, False
+ self._command_dead = False
+ self._note_link(self.clock.now())
+
+ def recover(self) -> None:
+ """Open a new session once the link has been down long enough, and no more often than that again.
+
+ A session does not survive the link dropping and the vendor driver does not open another, so the
+ arm stays out of reach until this does it. The attempt runs on the control loop, which stands still
+ for as long as the connection takes to fail.
+ """
+ now = self.clock.now()
+ if self._down_since is None or now - self._down_since < _RECONNECT_AFTER_S:
+ return
+ if now - self._reconnect_at < self._reconnect_every:
+ return
+ self._reconnect_at = now
+ logger.info(f'Opening a new session with the arm at {self.ip}')
+ try:
+ try:
+ self.driver.cleanup()
+ # rules-allow: swallowed-error — the session being closed is the one that failed, and the
+ # replacement still has to be opened
+ except trossen_arm.RuntimeError as exc:
+ logger.error(f'The old session with the arm at {self.ip} did not close: {exc}')
+ _configure(self.driver, self.ip, _RECONNECT_TIMEOUT_S)
+ if self.moves.active:
+ # The new session holds the arm where it reads, and a move in flight refuses every request
+ # that would resend its target, so waiting out its deadline is all it could do.
+ self.moves.fail(ConnectionError(f'the link to the arm at {self.ip} dropped during the move'))
+ self.take_control()
+ # rules-allow: swallowed-error — an arm still out of reach reads ERROR; the next attempt tries again
+ except trossen_arm.RuntimeError as exc:
+ self.complain(f'The arm at {self.ip} did not take a new session: {exc}')
+ self._reconnect_every = min(self._reconnect_every * 2, _RECONNECT_MAX_S)
+ return
+ self._reconnect_every = _RECONNECT_EVERY_S
+ self._stamp, self._stamp_at = int(self._output.header.timestamp), now
+ self._stream_stale = False
+ self._note_link(now)
+ logger.info(f'The arm at {self.ip} answers again')
+
+ def stand_down(self) -> None:
+ """Hold the arm where it reads, so nothing is driving it while it runs too fast."""
+ if self.moves.active:
+ # Nothing sends the target again: a move in flight refuses every request, and the setpoint now
+ # holds where the arm reads.
+ self.moves.fail(RuntimeError(f'the arm at {self.ip} ran too fast to finish the move'))
+ self._target = self._wanted = self.q
+ # Where the arm reads is what the next Cartesian step measures from: it is not where it was asked
+ # to be, and nothing is owed the rest of a travel it was stopped in.
+ self._anchor = self._travel_to = None
+ self._goal_time, self._arm_unsent = _STREAM_GOAL_TIME_S, True
+ self.write()
+
+ def publish(self) -> None:
+ """Ship the arm as the controller last reported it, arm and fingers."""
+ if self.link_down or self.overspeed or self.moves.errored: # out of reach, running away, or not
+ # where the driver put it
+ status = RobotStatus.ERROR
+ elif self.moves.active: # the driver owns the arm until the move settles
+ status = RobotStatus.BUSY
+ else:
+ status = RobotStatus.AVAILABLE
+ velocities = np.asarray(self._output.joint.arm.velocities, dtype=np.float64)
+ self.state.encode(self.q, velocities, self.ee_pose, status)
+ self.out.emit(self.state)
+ self.grip_out.emit(self._grip_of(self._output))
+
+
+def _connect(ip: str) -> Any:
+ """Open the arm controller and take ownership of it.
+
+ A configuration that fails leaves a driver holding whatever it opened, and nobody else has the handle
+ yet, so it goes back here.
+ """
+ driver = trossen_arm.TrossenArmDriver()
+ try:
+ _configure(driver, ip, _CONNECT_TIMEOUT_S)
+ except BaseException:
+ # rules-allow: swallowed-error — the failure to configure is the one to report, and a session that
+ # never opened has nothing to say about closing
+ with contextlib.suppress(Exception):
+ driver.cleanup()
+ raise
+ return driver
+
+
+@contextlib.contextmanager
+def _opened(connect: Callable[[str], Any], ip: str) -> Iterator[Any]:
+ """The arm, left idle and its handle given back however the run ends — including one that never starts."""
+ driver = connect(ip)
+ try:
+ yield driver
+ finally:
+ try:
+ driver.set_all_modes(trossen_arm.Mode.idle)
+ # rules-allow: swallowed-error — an arm that cannot be reached cannot be set idle either, and the
+ # handle still has to go back
+ except trossen_arm.RuntimeError as exc:
+ logger.error(f'The arm at {ip} was not set idle: {exc}')
+ finally: # an arm that will not go idle still has a handle to give back
+ try:
+ driver.cleanup()
+ # rules-allow: swallowed-error — this runs in a `finally`, and a session that cannot be closed
+ # must not replace the reason the run is ending
+ except trossen_arm.RuntimeError as exc:
+ logger.error(f'The session with the arm at {ip} did not close: {exc}')
+
+
+def _robot_meta() -> dict[str, Any]:
+ """The model an episode carries: the arm the viewer draws and the codecs solve against.
+
+ The vendored URDF names its meshes the way its own package does; the viewer looks each one up by the
+ name the URDF gives, so both are shortened to the file beside this model.
+
+ # TODO(#gripper-spec): no `gripper`. The viewer drives a gripper's joints to `grip * travel`, and
+ # `grip` is 1 when the fingers are closed -- but this arm's carriage joints open at their positive end
+ # and close at zero, which no single `travel` expresses.
+ """
+ urdf = ET.fromstring(Path(package_assets_path(_URDF_PATH)).read_text())
+ for mesh in urdf.iter('mesh'):
+ mesh.set('filename', Path(mesh.get('filename', '')).name)
+ add_default_frame(urdf, _EE_LINK)
+ mesh_dir = Path(package_assets_path(_MESH_DIR))
+ return {
+ roboarm_keys.ROBOT: 'trossen_wxai',
+ roboarm_keys.URDF: ET.tostring(urdf, encoding='unicode'),
+ roboarm_keys.MESHES: {
+ name: (mesh_dir / name).read_bytes() for name in sorted({m.get('filename', '') for m in urdf.iter('mesh')})
+ },
+ roboarm_keys.JOINT_NAMES: list(_JOINT_NAMES),
+ roboarm_keys.CONTROL_FRAME: DEFAULT_FRAME,
+ }
+
+
+class Robot(pimm.ControlSystem):
+ """Drives one Trossen WidowX AI arm over Ethernet, in the arm base frame.
+
+ The gripper shares the arm's controller, so this driver carries the ``grip``/``target_grip`` ports
+ (SO-101 precedent).
+ """
+
+ def __init__(self, ip: str = '192.168.1.4', *, connect: Callable[[str], Any] = _connect) -> None:
+ """
+ :param ip: Address of the arm controller.
+ :param connect: ``ip -> TrossenArmDriver`` factory; the fake-mode smoke injects ``_FakeTrossen``.
+ """
+ self._ip = ip
+ self._connect = connect
+
+ self.commands = pimm.ControlSystemReceiver[command.CommandType](self)
+ self.sync_move = pimm.calls.ControlSystemHandler[command.CommandType, None](self)
+ self.target_grip = pimm.ControlSystemReceiver[float](self)
+ self.state = pimm.ControlSystemEmitter[TrossenState](self)
+ self.grip = pimm.ControlSystemEmitter[float](self)
+ self.robot_meta = pimm.ControlSystemEmitter[dict[str, Any]](self)
+
+ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Command]:
+ with _opened(self._connect, self._ip) as driver:
+ arm = _Arm(driver, self._ip, self.sync_move, self.commands, self.state, self.grip, should_stop, clock)
+ with arm:
+ self.robot_meta.emit(_robot_meta())
+ yield from arm.taking_control()
+ yield from arm.travel_to(_HOME_JOINTS, 'the pose it opens at')
+
+ while not should_stop.value:
+ arm.read()
+ if arm.link_down:
+ arm.recover() # get the arm back first, so the state that goes out says where it is
+ arm.settle() # a move runs out its deadline on the last reading; nobody waits forever
+ arm.publish()
+ arm.moves.answer()
+ yield arm.limiter.wait()
+ continue
+
+ if not arm.controlled: # the arm was too fast to take when the session opened
+ arm.take_control()
+ arm.publish()
+ yield arm.limiter.wait()
+ continue
+
+ if arm.overspeed: # a joint past its limit faults the controller and drops the arm
+ arm.stand_down()
+ arm.settle()
+ arm.publish()
+ arm.moves.answer()
+ yield arm.limiter.wait()
+ continue
+
+ if (grip := pimm.value_updated(self.target_grip)) is not None:
+ arm.hold_grip(grip)
+ arm.settle()
+ asked = arm.moves.next_request()
+ if isinstance(asked, pimm.calls.Call):
+ arm.sync_move(asked)
+ elif asked is not None:
+ try:
+ arm.track(asked)
+ # rules-allow: swallowed-error — a command stream cannot end the run; the next
+ # setpoint supersedes this one
+ except Exception as exc:
+ arm.complain(f'{asked} not applied: {exc}', key='setpoint refused')
+ else:
+ arm.walk() # a delta asked for a travel, and nothing has superseded it
+
+ arm.advance()
+ arm.write()
+ arm.publish()
+ arm.moves.answer() # the state a settled move is answered with is out
+
+ yield arm.limiter.wait()
+
+ # The controller holds the arm only at rest; `_opened` sets it idle, and idle anywhere else
+ # is an arm that falls.
+ yield from arm.travel_to(_REST_JOINTS, 'rest', at_teardown=True)
+
+
+class _FakeTrossen:
+ """First-order-lag echo of the 7-joint arm, so the ``--fake`` smoke runs without hardware.
+
+ Duck-types the slice of ``TrossenArmDriver`` the driver uses. It models the link and the servo; the
+ kinematics are the driver's own, so the joints it reports are the whole of what it says.
+ """
+
+ class _Limit(NamedTuple):
+ position_min: float
+ position_max: float
+ velocity_max: float
+ position_tolerance: float
+
+ # What the arm reports for itself, read off a wxai_v0 controller on firmware 1.11.1
+ _LIMITS = [
+ _Limit(-3.141593, 3.141593, 6.2832, 0.2),
+ _Limit(0.0, 3.141593, 6.2832, 0.2),
+ _Limit(0.0, 2.356194, 6.2832, 0.2),
+ _Limit(-1.570796, 1.570796, 9.4248, 0.4),
+ _Limit(-1.570796, 1.570796, 9.4248, 0.4),
+ _Limit(-3.141593, 3.141593, 9.4248, 0.4),
+ _Limit(0.0, 0.04, 0.25, 0.004),
+ ]
+
+ _TICK_US = 5000 # the controller streams faster than the driver reads, so its clock moves every read
+
+ def __init__(self, alpha: float = 1.0):
+ self._alpha = alpha
+ self._position = np.zeros(7) # the arm boots with the fingers closed
+ self._velocity = np.zeros(7)
+ self._stamp = 0
+ self.frozen = False # the controller stops being heard from, as a dropped link leaves it
+ self.sessions = 1
+ self.mode: Any = None
+ self.goals: list[list[float]] = []
+ self.goal_times: list[float] = []
+ self.gripper_goals: list[float] = []
+ self._gripper_goal: float | None = None
+ self.cleaned_up = False
+
+ def configure(self, model: Any, end_effector: Any, serv_ip: str, clear_error: bool, timeout: float = 20.0):
+ self.mode = None
+ self.cleaned_up = False
+ self.sessions += 1
+
+ def get_joint_limits(self) -> list['_FakeTrossen._Limit']:
+ return list(_FakeTrossen._LIMITS)
+
+ def get_robot_output(self) -> Any:
+ if not self.frozen:
+ self._servo()
+ self._stamp += _FakeTrossen._TICK_US
+ arm = SimpleNamespace(positions=self._position[:_ARM_JOINTS].copy(), velocities=self._velocity[:6].copy())
+ gripper = SimpleNamespace(position=float(self._position[_GRIPPER_JOINT]))
+ return SimpleNamespace(
+ joint=SimpleNamespace(arm=arm, gripper=gripper), header=SimpleNamespace(timestamp=self._stamp)
+ )
+
+ def set_all_modes(self, mode: Any) -> None:
+ self.mode = mode
+
+ def set_gripper_position(self, goal_position, goal_time=2.0, blocking=True) -> None:
+ if self.mode is not trossen_arm.Mode.position:
+ raise trossen_arm.RuntimeError(f'a gripper goal needs the joint in position mode, not {self.mode}')
+ self._gripper_goal = float(goal_position)
+ self.gripper_goals.append(self._gripper_goal)
+
+ def set_all_positions(self, goal_positions, goal_time=2.0, blocking=True) -> None:
+ if self.mode is not trossen_arm.Mode.position:
+ raise trossen_arm.RuntimeError(f'set_all_positions needs every joint in position mode, not {self.mode}')
+ self.goals.append([float(v) for v in goal_positions])
+ self.goal_times.append(float(goal_time))
+
+ def _servo(self) -> None:
+ """Advance the joints towards the goal they were last given, as the controller's own loop does.
+
+ A Cartesian goal needs kinematics to follow, which this fake does not have; the fingers still move.
+ """
+ if not self.goals:
+ return
+ goal = np.asarray(self.goals[-1])
+ if self._gripper_goal is not None:
+ goal = np.append(goal[:_ARM_JOINTS], self._gripper_goal)
+ step = self._alpha * (goal - self._position)
+ # Half of what each joint may do, which is the headroom a servo keeps when it is not faulting.
+ per_tick = np.array([limit.velocity_max for limit in _FakeTrossen._LIMITS]) / (2 * _HZ)
+ step = np.clip(step, -per_tick, per_tick)
+ self._velocity = step * _HZ
+ self._position = self._position + step
+
+ def cleanup(self, reboot_controller: bool = False) -> None:
+ self.cleaned_up = True
+
+
+if __name__ == '__main__':
+ import argparse
+ import time
+
+ parser = argparse.ArgumentParser(description='Trossen driver smoke: joints and gripper round-trip.')
+ parser.add_argument('--ip', default='192.168.1.4')
+ parser.add_argument('--fake', action='store_true', help='in-process first-order-lag echo; needs no hardware')
+ args = parser.parse_args()
+
+ _ARRIVED_SLACK = 0.1 # what the checks below allow, being the tolerance the wxai_v0 controller reports
+ fake = _FakeTrossen() if args.fake else None
+ robot = Robot(args.ip, connect=(lambda ip: fake) if args.fake else _connect)
+
+ with pimm.World() as world:
+ # `World.pair` cannot express that it returns the counterpart of the port it is given, so the four
+ # payload types are named here.
+ commands = world.pair(robot.commands)
+ sync_move = world.pair(robot.sync_move)
+ target_grip = world.pair(robot.target_grip)
+ state = world.pair(robot.state)
+ grip = world.pair(robot.grip)
+
+ loop = world.start([robot])
+
+ def pump(seconds: float):
+ deadline = time.monotonic() + seconds
+ while time.monotonic() < deadline and not world.should_stop:
+ cmd = next(loop)
+ time.sleep(cmd.seconds if isinstance(cmd, pimm.Sleep) else 0)
+
+ pump(0.2)
+ assert state.read() is not None, 'the driver published no state'
+ assert state.value.status == RobotStatus.AVAILABLE, state.value.status
+
+ # Grip round-trip: polarity inverted on the way out (goal) and on the way back (reading).
+ target_grip.emit(0.0)
+ pump(0.5)
+ if fake is not None: # grip 0 open -> the joint at the far end of its travel
+ assert abs(fake.gripper_goals[-1] - 0.04) < 1e-6, fake.gripper_goals[-1]
+ assert abs(grip.value) < 0.02, grip.value
+ target_grip.emit(1.0)
+ pump(0.5)
+ if fake is not None:
+ assert abs(fake.gripper_goals[-1]) < 1e-6, fake.gripper_goals[-1]
+ assert abs(grip.value - 1.0) < 0.02, grip.value
+
+ # A streamed joint setpoint nobody waits on.
+ jog = np.array([0.2, 0.4, 0.3, 0.0, 0.1, 0.0])
+ commands.emit(command.JointPosition(jog))
+ pump(0.5)
+ assert np.allclose(state.value.q, jog, atol=_ARRIVED_SLACK), state.value.q
+
+ # A target outside the joint range is clipped, not refused: joint 1 has no negative half.
+ commands.emit(command.JointPosition(np.array([0.0, -1.0, 0.0, 0.0, 0.0, 0.0])))
+ pump(0.5)
+ assert state.value.q[1] > -_ARRIVED_SLACK, state.value.q
+
+ # A synchronous move the firmware plans, answered once the arm reads back at the target.
+ home = np.zeros(_ARM_JOINTS)
+ answer = sync_move(command.JointPosition(home))
+ for _ in range(100):
+ if answer.done():
+ break
+ pump(0.1)
+ answer.result()
+ assert np.allclose(state.value.q, home, atol=_ARRIVED_SLACK), state.value.q
+ assert state.value.status == RobotStatus.AVAILABLE, state.value.status
+
+ print(f'ee_pose {state.value.ee_pose}')
+ print('Trossen driver smoke passed')
diff --git a/positronic/drivers/roboarm/trossen_leader.py b/positronic/drivers/roboarm/trossen_leader.py
new file mode 100644
index 000000000..38e60e5d7
--- /dev/null
+++ b/positronic/drivers/roboarm/trossen_leader.py
@@ -0,0 +1,242 @@
+"""Driver for a Trossen WidowX AI arm the operator holds, read as a teleoperation source.
+
+The operator moves the arm and the controller does nothing but hold its weight: it runs in
+``external_effort`` mode, where every joint is back-drivable. What it reads is what a follower is asked to
+stand at, joint for joint, with no kinematics in between — which is the whole reason to prefer a leader to
+a hand tracked in space. A pose has to be solved for, and near the workspace boundary a solution may not
+exist at all; joints always do.
+
+The arm carries the leader end effector, whose fingers are shorter than the follower's. The gripper's
+position is computed from the motor angle through that geometry, so a leader read as a follower reports
+5.9 mm less than it holds. That is past the 4 mm the controller tolerates below the gripper's range, and
+it then refuses position mode outright with ``Joint limit exceeded ... Setting to idle``. Measured on both
+leaders of the station: the same arm reads -0.0004 m as a leader and -0.0063 m as a follower.
+
+Force feedback is what the follower is holding, pushed back into the operator's hand. It stays off until
+the follower driver publishes its external efforts, which it does not yet; ``force_feedback_gain`` is 0
+and ``follower_efforts`` goes unconnected, which leaves plain gravity compensation.
+
+The trigger takes the operator's hand to move, because the gripper joint carries the most friction of any
+joint on the arm. What the controller cancels of it is that joint's ``friction_constant_term``, which
+``gripper_friction_constant`` stands in for.
+"""
+
+import contextlib
+import logging
+from collections.abc import Callable, Iterator
+from typing import Any
+
+import numpy as np
+
+import pimm
+from positronic.drivers import vendor_import
+
+from . import command
+
+# trossen_arm lives in the `trossen` extra, which the type-check environment does not install.
+with vendor_import(
+ 'trossen_arm', 'Trossen arm support', hint='Re-run with the trossen extra:\n uv run --locked --extra trossen ...\n'
+):
+ import trossen_arm # pyright: ignore[reportMissingImports]
+
+logger = logging.getLogger(__name__)
+
+_ARM_JOINTS = 6
+_GRIPPER_JOINT = 6
+# The leader is read at the rate the follower is driven at: a reading the follower never uses is latency
+# the operator paid for and got nothing back.
+_HZ = 100
+_CONNECT_TIMEOUT_S = 20.0
+# How often a failure that stands is worth saying again. A tick rate of complaints buries every other line.
+_COMPLAIN_EVERY_S = 5.0
+# How often the arm is asked back into the mode it should be in, once something has knocked it out.
+_RECOVER_EVERY_S = 1.0
+# The pace a leader is driven at, and the least time any such move takes. The operator has a hand on the
+# arm while it travels, so it goes no faster than the follower does.
+_MOVE_SPEED = 0.6 # rad/s
+_MIN_MOVE_TIME_S = 1.0
+
+
+def _connect(ip: str) -> Any:
+ """Open the controller of an arm held as a leader, and take ownership of it."""
+ driver = trossen_arm.TrossenArmDriver()
+ driver.configure(
+ trossen_arm.Model.wxai_v0, trossen_arm.StandardEndEffector.wxai_v0_leader, ip, True, _CONNECT_TIMEOUT_S
+ )
+ return driver
+
+
+@contextlib.contextmanager
+def _gripper_freed(driver: Any, constant: float | None) -> Iterator[None]:
+ """The gripper with ``constant`` as its friction constant term, and the arm's own value back after.
+
+ The controller adds this term to the effort it puts on the joint, in the direction the joint moves, so
+ a larger one leaves less of the gripper's own friction for the operator's hand to overcome.
+ """
+ if constant is None:
+ yield
+ return
+ characteristics = driver.get_joint_characteristics()
+ stood_at = characteristics[_GRIPPER_JOINT].friction_constant_term
+ characteristics[_GRIPPER_JOINT].friction_constant_term = constant
+ driver.set_joint_characteristics(characteristics)
+ logger.info(f'The gripper friction constant is {constant}, where the arm carries {stood_at}')
+ try:
+ yield
+ finally:
+ characteristics[_GRIPPER_JOINT].friction_constant_term = stood_at
+ try:
+ driver.set_joint_characteristics(characteristics)
+ # rules-allow: swallowed-error — an arm that cannot be reached keeps the term until it is
+ # configured again, and the handle still has to go back
+ except Exception as exc:
+ logger.error(f'The gripper keeps the friction constant {constant} until it is configured again: {exc}')
+
+
+@contextlib.contextmanager
+def _opened(connect: Callable[[str], Any], ip: str) -> Iterator[Any]:
+ """The arm, left idle and its handle given back however the run ends — including one that never starts."""
+ driver = connect(ip)
+ try:
+ yield driver
+ finally:
+ try:
+ driver.set_all_modes(trossen_arm.Mode.idle)
+ # rules-allow: swallowed-error — an arm that cannot be reached cannot be set idle either, and the
+ # handle still has to go back
+ except trossen_arm.RuntimeError as exc:
+ logger.error(f'The leader at {ip} was not set idle: {exc}')
+ finally: # an arm that will not go idle still has a handle to give back
+ try:
+ driver.cleanup()
+ # rules-allow: swallowed-error — this runs in a `finally`, and a session that cannot be closed
+ # must not replace the reason the run is ending
+ except trossen_arm.RuntimeError as exc:
+ logger.error(f'The session with the leader at {ip} did not close: {exc}')
+
+
+class Leader(pimm.ControlSystem):
+ """One WidowX AI arm the operator holds, publishing the joints and the grip it is being moved to.
+
+ The arm is left free the whole time it runs: there is no tracking to switch on, because nothing is
+ driven from here. Whoever reads these ports decides when they reach a follower.
+ """
+
+ def __init__(
+ self,
+ ip: str,
+ *,
+ force_feedback_gain: float = 0.0,
+ gripper_friction_constant: float | None = None,
+ connect: Callable[[str], Any] = _connect,
+ ) -> None:
+ """
+ :param ip: Address of the leader arm's controller.
+ :param force_feedback_gain: Share of the follower's external effort pushed back into the
+ operator's hand. 0 leaves the arm in plain gravity compensation.
+ :param gripper_friction_constant: What the controller cancels of the gripper's own friction, in N.
+ None keeps the value the arm was calibrated with. A larger one takes the trigger's stiffness
+ off the operator's hand; too large and the trigger moves on its own.
+ :param connect: ``ip -> TrossenArmDriver`` factory; a test injects its own.
+ """
+ self._ip = ip
+ self._gain = force_feedback_gain
+ self._gripper_friction = gripper_friction_constant
+ self._connect = connect
+
+ self.joints = pimm.ControlSystemEmitter[np.ndarray](self)
+ self.grip = pimm.ControlSystemEmitter[float](self)
+ self.follower_efforts = pimm.ControlSystemReceiver[np.ndarray](self)
+ self.sync_move = pimm.calls.ControlSystemHandler[command.CommandType, None](self)
+
+ def _pushed_back(self) -> list[float]:
+ """What the leader pushes back with: the follower's own external effort, reversed and scaled, so
+ the operator feels what the follower is holding.
+
+ Zero while nothing reports it, which is gravity compensation and nothing else.
+ """
+ zero = [0.0] * (_ARM_JOINTS + 1)
+ if not self._gain:
+ return zero
+ felt = self.follower_efforts.read()
+ if felt is None:
+ return zero
+ return (-self._gain * np.asarray(felt.data, dtype=np.float64)).tolist()
+
+ def _go_to(self, driver: Any, asked: command.CommandType, closed: float, travel: float) -> None:
+ """Drive the arm to the joints ``asked`` names, then give it back to the operator's hand.
+
+ The controller plans the whole move, at the pace the follower travels at. The gripper stays where
+ the operator holds it: ``set_all_positions`` takes every joint, and this move is the arm's.
+ """
+ if not isinstance(asked, command.JointPosition):
+ raise NotImplementedError(f'A leader is driven to joints, and {asked} names none')
+ target = np.asarray(asked.positions, dtype=np.float64)
+ # Checked before the mode change: a shorter target broadcasts across the reading and moves every
+ # joint, and a NaN makes the travel time itself NaN, both with the arm already servoing.
+ if target.shape != (_ARM_JOINTS,) or not np.all(np.isfinite(target)):
+ raise ValueError(f'A leader takes {_ARM_JOINTS} finite joint positions, and this names {target}')
+ # Clipped to what this arm reports, as the follower clips the same pose to what its own controller
+ # reports: a leader sent past its limit is refused, and then the two cannot meet.
+ limits = driver.get_joint_limits()
+ lower = np.array([limits[i].position_min for i in range(_ARM_JOINTS)])
+ upper = np.array([limits[i].position_max for i in range(_ARM_JOINTS)])
+ if np.any(target < lower) or np.any(target > upper):
+ logger.warning(f'The leader at {self._ip} was asked for {target}, outside [{lower}, {upper}]')
+ target = np.clip(target, lower, upper)
+ positions = np.asarray(driver.get_all_positions(), dtype=np.float64)
+ seconds = max(_MIN_MOVE_TIME_S, float(np.max(np.abs(target - positions[:_ARM_JOINTS]))) / _MOVE_SPEED)
+ held = float(np.clip(positions[_GRIPPER_JOINT], closed, closed + travel))
+ logger.info(f'The leader at {self._ip} travels to the pose the session asked for, in {seconds:.1f} s')
+ driver.set_all_modes(trossen_arm.Mode.position)
+ try:
+ driver.set_all_positions([*target, held], seconds, True)
+ finally:
+ driver.set_all_modes(trossen_arm.Mode.external_effort)
+
+ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[pimm.Command]:
+ with _opened(self._connect, self._ip) as driver, _gripper_freed(driver, self._gripper_friction):
+ limit = driver.get_joint_limits()[_GRIPPER_JOINT]
+ closed, travel = limit.position_min, limit.position_max - limit.position_min
+ driver.set_all_modes(trossen_arm.Mode.external_effort)
+ logger.info(f'The leader at {self._ip} is free to move; the controller holds its weight')
+
+ limiter = pimm.RateLimiter(clock, hz=_HZ)
+ complained_at: float = -_COMPLAIN_EVERY_S
+ failed_at: float | None = None
+ while not should_stop.value:
+ now = clock.now()
+ for call in self.sync_move.incoming():
+ with pimm.calls.raise_to(call):
+ self._go_to(driver, call.request, closed, travel)
+ call.set_result(None)
+ if failed_at is None or now - failed_at >= _RECOVER_EVERY_S:
+ try:
+ if failed_at is not None:
+ # TODO: a fault the controller latches is cleared by a new session, not by
+ # asking for the mode again; reopen one, the way the follower's `recover` does.
+ driver.set_all_modes(trossen_arm.Mode.external_effort)
+ failed_at = None
+ logger.info(f'The leader at {self._ip} is free to move again')
+ driver.set_all_external_efforts(self._pushed_back(), 0.0, False)
+ positions = np.asarray(driver.get_all_positions(), dtype=np.float64)
+ ts = clock.now_ns()
+ self.joints.emit(positions[:_ARM_JOINTS], ts)
+ self.grip.emit(self._grip_of(positions[_GRIPPER_JOINT], closed, travel), ts)
+ # rules-allow: swallowed-error — a leader that stops being heard from is one arm of a
+ # session, and the rest of the rig goes on recording without it
+ except Exception as exc:
+ failed_at = now
+ if now - complained_at >= _COMPLAIN_EVERY_S:
+ complained_at = now
+ logger.error(f'The leader at {self._ip} is not being read: {exc}')
+
+ yield limiter.wait()
+
+ @staticmethod
+ def _grip_of(position: float, closed: float, travel: float) -> float:
+ """How closed the operator is holding the trigger, as the 1-is-closed grip positronic speaks.
+
+ The reading sits a little outside the joint range at either end, so it saturates to 0..1.
+ """
+ return float(np.clip(1.0 - (position - closed) / travel, 0.0, 1.0))
diff --git a/positronic/drivers/roboarm/yam.py b/positronic/drivers/roboarm/yam.py
index 6611068ee..b2719a0ec 100644
--- a/positronic/drivers/roboarm/yam.py
+++ b/positronic/drivers/roboarm/yam.py
@@ -362,7 +362,7 @@ def run(self, should_stop: pimm.SignalReceiver, clock: pimm.Clock) -> Iterator[p
with _opened(self._connect, self._channel, self._sim) as vendor:
chain = self._chain(vendor, should_stop, clock)
meta = {
- 'robot': 'i2rt_yam',
+ roboarm_keys.ROBOT: 'i2rt_yam',
roboarm_keys.JOINT_NAMES: list(_JOINT_NAMES),
roboarm_keys.CONTROL_FRAME: DEFAULT_FRAME,
}
diff --git a/positronic/drivers/tests/test_utils.py b/positronic/drivers/tests/test_utils.py
index 26873f2bd..522783f8e 100644
--- a/positronic/drivers/tests/test_utils.py
+++ b/positronic/drivers/tests/test_utils.py
@@ -174,8 +174,10 @@ def test_a_grip_asked_for_past_the_range_is_tracked_against_a_width_the_fingers_
assert answer.result() is None
-def test_a_streamed_grip_waits_for_the_call_queue_to_be_empty(asking):
- """A signal holds only its latest value, so a stream read in the same tick as a call would be lost."""
+def test_a_setpoint_the_device_was_moved_away_from_is_let_go(asking):
+ """A setpoint says where the device is wanted now, and the move taken after it puts the device
+ somewhere else. Applying the setpoint once the move lands takes the device back off the pose it was
+ asked for, in one step and with nobody asking."""
ask, moves, stream = asking
stream.push(0.25)
ask(0.9)
@@ -183,8 +185,34 @@ def test_a_streamed_grip_waits_for_the_call_queue_to_be_empty(asking):
assert grip_setpoint(moves, grip=0.0, now=0.0) == 0.9
assert grip_setpoint(moves, grip=0.9, now=0.1) is None # the call arrives
moves.answer()
- assert grip_setpoint(moves, grip=0.9, now=0.2) == 0.25 # the stream, still waiting
- assert grip_setpoint(moves, grip=0.25, now=0.3) is None
+ assert grip_setpoint(moves, grip=0.9, now=0.2) is None, 'the setpoint the move superseded reached the device'
+
+
+def test_the_newest_setpoint_is_what_the_device_is_asked_for(asking):
+ """A transport that queues setpoints hands the oldest over first. A device asked for one a tick is
+ driven through what its asker has already done, and falls further behind the longer it runs."""
+ _ask, moves, stream = asking
+ for width in (0.1, 0.2, 0.3):
+ stream.push(width)
+
+ assert grip_setpoint(moves, grip=0.0, now=0.0) == 0.3
+ assert grip_setpoint(moves, grip=0.3, now=0.1) is None, 'the device was asked for a width it had passed'
+
+
+def test_setpoints_streamed_at_a_travelling_device_do_not_reach_it_when_the_move_lands(asking):
+ """Nothing reads the stream for as long as a move owns the device, so what arrives in that time is
+ where the asker wanted the device before the move — every setpoint of it, oldest first."""
+ ask, moves, stream = asking
+ ask(0.9)
+ assert grip_setpoint(moves, grip=0.0, now=0.0) == 0.9
+
+ stream.push(0.25)
+ stream.push(0.30)
+ assert grip_setpoint(moves, grip=0.0, now=0.1) is None # the move still travels
+ assert grip_setpoint(moves, grip=0.9, now=0.2) is None # ... and lands
+ moves.answer()
+
+ assert grip_setpoint(moves, grip=0.9, now=0.3) is None, 'the device was driven back through the stream'
def test_how_long_a_move_gets_is_the_driver_s_to_say():
diff --git a/positronic/drivers/tests/test_webxr.py b/positronic/drivers/tests/test_webxr.py
new file mode 100644
index 000000000..16ceb6faa
--- /dev/null
+++ b/positronic/drivers/tests/test_webxr.py
@@ -0,0 +1,41 @@
+"""What the WebXR driver makes of the payload from the headset."""
+
+import numpy as np
+import pytest
+
+from positronic.drivers.webxr import BUTTONS, CONTROLLERS, ORIENTATION, POSITION, SIDES, _parse_controller_data
+
+TOUCH_BUTTONS = [0.4, 0.0, 0.0, 0.0, 1.0, 0.0] # trigger, squeeze, unused, stick, A, B
+
+
+def _payload(**controllers):
+ return {CONTROLLERS: {**dict.fromkeys(SIDES), **controllers}}
+
+
+def _controller(buttons):
+ return {POSITION: [0.1, 0.2, 0.3], ORIENTATION: [1.0, 0.0, 0.0, 0.0], BUTTONS: buttons}
+
+
+def test_gamepad_buttons_reach_the_caller():
+ _, buttons = _parse_controller_data(_payload(right=_controller(TOUCH_BUTTONS)))
+
+ right = buttons['right']
+ assert right is not None
+ np.testing.assert_allclose(right, TOUCH_BUTTONS)
+
+
+def test_an_input_source_that_cannot_drive_the_arm_is_refused():
+ """Every teleoperation control is a button, so a source with none of them controls nothing."""
+ with pytest.raises(ValueError, match=r'shaped \(0,\)'):
+ _parse_controller_data(_payload(right=_controller([])))
+
+
+def test_a_controller_short_of_a_button_is_refused():
+ with pytest.raises(ValueError, match=r'shaped \(5,\)'):
+ _parse_controller_data(_payload(right=_controller(TOUCH_BUTTONS[:5])))
+
+
+def test_buttons_that_are_not_one_row_are_refused():
+ """Six buttons in a nested list count six, and the reads that follow take one of the rows for a value."""
+ with pytest.raises(ValueError, match=r'shaped \(1, 6\)'):
+ _parse_controller_data(_payload(right=_controller([TOUCH_BUTTONS])))
diff --git a/positronic/drivers/utils.py b/positronic/drivers/utils.py
index aead06e36..41971df96 100644
--- a/positronic/drivers/utils.py
+++ b/positronic/drivers/utils.py
@@ -78,18 +78,32 @@ def target(self) -> np.ndarray | float:
assert self._call is not None, 'no move is in flight'
return self._target
+ def streamed(self) -> T | None:
+ """The newest setpoint streamed at the device; reading it lets go of every setpoint older than it.
+
+ A setpoint says where the device is wanted now. A transport that queues them hands the oldest over
+ first, so a device asked for one a tick is driven through what its asker has already done — seconds
+ of it where a move owned the device and nothing was read at all.
+ """
+ latest = None
+ while (message := self._async_move.read()) is not None and message.updated:
+ latest = message.data
+ return latest
+
def next_request(self) -> pimm.calls.Call[T, None] | T | None:
"""What the device is asked for now: a call whose asker waits to hear it arrive, a streamed setpoint
nobody waits on, or nothing.
- A call comes first, because a signal holds only its latest value and a setpoint read in the same tick
- would be lost. A device a move already owns is asked for nothing.
+ A call comes first: a setpoint says where the device is wanted now, and the move that follows it
+ puts the device somewhere else. A device a move already owns is asked for nothing, and the setpoints
+ streamed at it while it travels are let go for the same reason.
"""
+ streamed = self.streamed()
if self.busy:
return None
if (call := next(self._sync_move.incoming(), None)) is not None:
return call
- return pimm.value_updated(self._async_move)
+ return streamed
def accept(
self, call: pimm.calls.Call[T, None], target: np.ndarray | float, tol: float, now: float, timeout_s: float
@@ -191,6 +205,8 @@ def grip_setpoint(moves: Moves[float], grip: float, now: float) -> float | None:
driver writes before calling ``Moves.answer``.
"""
if moves.active:
+ # A width streamed at fingers a move owns is older than where the move puts them.
+ moves.streamed()
return grip if moves.settle(grip, now) is MoveStatus.GAVE_UP else None
asked = moves.next_request()
if isinstance(asked, pimm.calls.Call):
diff --git a/positronic/drivers/webxr.py b/positronic/drivers/webxr.py
index 269914df0..ccffd70a1 100644
--- a/positronic/drivers/webxr.py
+++ b/positronic/drivers/webxr.py
@@ -30,15 +30,29 @@
}
+# Every teleoperation control is a button, and the highest index one of them reads is the B button's.
+_TELEOP_BUTTONS = 6
+
+
+# The fields of the payload the headset sends, and the hands it sends one for.
+CONTROLLERS, POSITION, ORIENTATION, BUTTONS = 'controllers', 'position', 'orientation', 'buttons'
+SIDES = ('right', 'left')
+
+
def _parse_controller_data(data: dict):
- controller_positions = {'left': None, 'right': None}
- buttons_dict = {'left': None, 'right': None}
- for side in ['right', 'left']:
- if data['controllers'][side] is not None:
- translation = np.array(data['controllers'][side]['position'], dtype=np.float64)
- rotation = np.array(data['controllers'][side]['orientation'], dtype=np.float64)
- buttons = np.array(data['controllers'][side]['buttons'], dtype=np.float64)
+ controller_positions: dict[str, geom.Transform3D | None] = dict.fromkeys(SIDES)
+ buttons_dict: dict[str, np.ndarray | None] = dict.fromkeys(SIDES)
+ for side in SIDES:
+ if data[CONTROLLERS][side] is not None:
+ translation = np.array(data[CONTROLLERS][side][POSITION], dtype=np.float64)
+ rotation = np.array(data[CONTROLLERS][side][ORIENTATION], dtype=np.float64)
+ buttons = np.array(data[CONTROLLERS][side][BUTTONS], dtype=np.float64)
controller_positions[side] = geom.Transform3D(translation, geom.Rotation.from_quat(rotation))
+ if buttons.ndim != 1 or buttons.size < _TELEOP_BUTTONS:
+ raise ValueError(
+ f'The {side} controller sends buttons shaped {buttons.shape}; teleoperation reads '
+ f'{_TELEOP_BUTTONS} of them, one after another'
+ )
buttons_dict[side] = buttons
return controller_positions, buttons_dict
@@ -213,9 +227,9 @@ async def websocket_endpoint(websocket: WebSocket):
controller_positions, buttons = _parse_controller_data(data)
# apply scaling
- for side, transform in controller_positions.items():
+ for transform in controller_positions.values():
if transform is not None:
- controller_positions[side].translation *= self.sensitivity
+ transform.translation *= self.sensitivity
ts = clock.now_ns()
if controller_positions['left'] is not None or controller_positions['right'] is not None:
diff --git a/positronic/inference.py b/positronic/inference.py
index f32877c4a..0e311c724 100644
--- a/positronic/inference.py
+++ b/positronic/inference.py
@@ -13,6 +13,7 @@
import pimm
import positronic.cfg.embodiment
import positronic.cfg.eval.real.droid
+import positronic.cfg.eval.real.trossen
import positronic.cfg.policy as policy_cfg
from pimm.logging import init_logging
from positronic.cfg.eval.sim.positronic import stack_cubes
@@ -109,6 +110,13 @@ def real(policy, embodiment: Embodiment, next_task: Callable[[], Task], output_d
)
+# The Trossen station, whose arm and cameras are the ones its demonstrations were recorded with. Every
+# trial opens at the same start pose the operator's own trials open at.
+trossen_cfg = real_cfg.override(
+ embodiment=positronic.cfg.embodiment.trossen, next_task=positronic.cfg.eval.real.trossen.attended_trials
+)
+
+
# Console entry point for [project.scripts].
@pos3.with_mirror()
def _internal_main():
@@ -116,6 +124,7 @@ def _internal_main():
cfn.cli({
'run': real_cfg,
'real': real_cfg, # `real` is the documented name for the hardware path
+ 'trossen': trossen_cfg,
'sim': run.override(eval=stack_cubes),
'stats': stats,
})
diff --git a/positronic/server/dataset_utils.py b/positronic/server/dataset_utils.py
index f47ec1d5e..ebfe6a6d4 100644
--- a/positronic/server/dataset_utils.py
+++ b/positronic/server/dataset_utils.py
@@ -555,7 +555,7 @@ def _log_urdf_robot(
"""Log the episode's robot model, its joints animated by `joint_sig`."""
joint_names = ep.static.get(roboarm_keys.JOINT_NAMES)
urdf_str = ep.static.get(roboarm_keys.URDF)
- meshes = ep.static.get('meshes')
+ meshes = ep.static.get(roboarm_keys.MESHES)
if not (joint_names and urdf_str and meshes):
return
ts_arr, q_vals = numeric_data[joint_sig]
diff --git a/positronic/tests/test_components.py b/positronic/tests/test_components.py
index 2f9d0a645..5bb438da2 100644
--- a/positronic/tests/test_components.py
+++ b/positronic/tests/test_components.py
@@ -1,3 +1,4 @@
+import logging
import pickle
from collections.abc import Callable
from importlib import import_module
@@ -8,11 +9,24 @@
import pimm
from pimm.core import ControlSystem
+logger = logging.getLogger(__name__)
+
+_OURS = ('positronic', 'pimm')
+
def _optional_import(module: str, symbol: str) -> Any | None:
+ """``symbol``, or ``None`` where the vendor package it needs is not usable on this machine.
+
+ A vendor that is installed but cannot load — its own native library is missing — leaves this station
+ without the component just as an absent package does. A project module that fails to import is a broken
+ build rather than a missing vendor, and is not one of those.
+ """
try:
return getattr(import_module(module), symbol)
- except ModuleNotFoundError: # pragma: no cover - optional dependency
+ except ImportError as e: # pragma: no cover - optional dependency
+ if (e.name or '').split('.')[0] in _OURS:
+ raise
+ logger.error('%s is not available: %s', module, e)
return None
diff --git a/positronic/tests/test_data_collection.py b/positronic/tests/test_data_collection.py
index 77b64e809..80c1a36f5 100644
--- a/positronic/tests/test_data_collection.py
+++ b/positronic/tests/test_data_collection.py
@@ -1,16 +1,18 @@
from collections.abc import Callable, Sequence
+from dataclasses import dataclass, field
from pathlib import Path
import numpy as np
import pytest
import pimm
-from positronic import data_collection, keys, wire
+from positronic import data_collection, geom, keys, wire
from positronic.data_collection import DataCollectionController, OperatorPosition, controller_positions_serializer
-from positronic.dataset.ds_writer_agent import DsWriterAgent, DsWriterCommand
+from positronic.dataset.ds_writer_agent import DsWriterAgent, DsWriterCommand, DsWriterCommandType
from positronic.dataset.episode import Episode
from positronic.dataset.local_dataset import LocalDataset, LocalDatasetWriter
from positronic.dataset.serializers import Serializers
+from positronic.drivers.roboarm import RobotStatus
from positronic.drivers.roboarm import command as roboarm_command
from positronic.drivers.webxr import WebXR
from positronic.geom import Rotation, Transform3D
@@ -67,6 +69,58 @@ def build_collection(world, out_dir: Path, *, metadata_getter: Callable[[], dict
return dc, ds_agent, ctrl_em_dc, ctrl_em_agent, buttons_em, robot
+def test_the_tracker_takes_the_shake_out_of_a_hand_that_holds_still():
+ """A hand at rest still shakes, and the arm shows every bit of it unless the tracker holds it back."""
+ tracker = data_collection._Tracker(data_collection.OperatorPosition.BACK.value)
+ tracker.turn_on(geom.Transform3D())
+ shake, seen = 0.004, []
+ for tick in range(400): # four seconds of a hand shaking 10 Hz about one spot
+ at = geom.Transform3D(np.array([shake * np.sin(2 * np.pi * 10 * tick / 100), 0.0, 0.0]))
+ seen.append(tracker.update(at, tick * 10_000_000).translation)
+ left = np.ptp(np.asarray(seen)[100:], axis=0).max()
+ assert left < shake, f"the arm still swings {left * 1000:.1f} mm of the hand's {shake * 2000:.1f} mm"
+
+
+def test_the_tracker_follows_a_hand_that_means_it():
+ """What the filter holds back is the shake, not the movement: a hand that goes somewhere arrives."""
+ tracker = data_collection._Tracker(data_collection.OperatorPosition.BACK.value)
+ tracker.turn_on(geom.Transform3D())
+ where = geom.Transform3D()
+ for tick in range(100): # one second of holding the hand 20 cm away
+ where = tracker.update(geom.Transform3D(np.array([0.2, 0.0, 0.0])), tick * 10_000_000)
+ assert np.linalg.norm(where.translation) > 0.19
+
+
+def test_a_pose_that_is_not_a_number_does_not_settle_in_the_filter():
+ """The lag keeps what it is given, so one NaN would sit in it and every pose after it would come back
+ NaN -- the arm would take no target again until the run was started afresh."""
+ tracker = data_collection._Tracker(data_collection.OperatorPosition.BACK.value)
+ tracker.turn_on(geom.Transform3D())
+ good = tracker.update(geom.Transform3D(np.array([0.1, 0.0, 0.0])), 10_000_000)
+
+ tracker.update(geom.Transform3D(np.array([np.nan, 0.0, 0.0])), 20_000_000)
+ after = tracker.update(geom.Transform3D(np.array([0.1, 0.0, 0.0])), 30_000_000)
+
+ assert np.all(np.isfinite(good.translation))
+ assert np.all(np.isfinite(after.translation))
+
+
+def test_the_tracker_reads_a_negated_quaternion_as_the_turn_it_is():
+ """A rotation and its negated quaternion are the same turn, and a hand holding still sends either."""
+ tracker = data_collection._Tracker(data_collection.OperatorPosition.BACK.value)
+ tracker.turn_on(geom.Transform3D())
+ held = geom.Rotation.from_rotvec(np.array([0.01, 0.0, 0.0]))
+ barely_moved = geom.Rotation.from_rotvec(np.array([0.02, 0.0, 0.0]))
+ negated = geom.Rotation.from_quat(-barely_moved.as_quat) # the same turn, written the other way
+
+ first = tracker.update(geom.Transform3D(rotation=held), 0)
+ second = tracker.update(geom.Transform3D(rotation=negated), 10_000_000)
+
+ # A rotation matrix reads the same for a quaternion and its negative, so it is what the two are alike in
+ swing = float(np.abs(first.rotation.as_rotation_matrix - second.rotation.as_rotation_matrix).max())
+ assert swing < 0.02, f'the arm turned where the hand barely moved, by {swing:.3f} of a rotation matrix'
+
+
def test_data_collection_records_task_metadata(tmp_path, world):
call_count = 0
@@ -306,6 +360,261 @@ def test_every_start_pose_is_a_fresh_per_joint_draw_around_the_nominal():
assert np.all(offsets.std(axis=1) > 0) # each joint drawn on its own, not one offset for the whole vector
+@dataclass
+class _StandingStill:
+ """The least an arm reports for a controller to follow it: where it stands, and that it takes commands."""
+
+ ee_pose: Transform3D = field(default_factory=Transform3D)
+ status: RobotStatus = RobotStatus.AVAILABLE
+ q: np.ndarray = field(default_factory=lambda: np.zeros(6))
+ dq: np.ndarray = field(default_factory=lambda: np.zeros(6))
+
+
+@dataclass
+class _LeaderRig:
+ """A controller over an arm the operator drives with the leader beside it, with its ports paired."""
+
+ dc: DataCollectionController
+ commands: RecordingEmitter
+ grips: RecordingEmitter
+ state: pimm.ControlSystemEmitter
+ joints: pimm.ControlSystemEmitter
+ grip: pimm.ControlSystemEmitter
+ events: pimm.ControlSystemEmitter
+ move: pimm.calls.ControlSystemHandler
+ leader_move: pimm.calls.ControlSystemHandler
+
+ def answer_moves(self) -> list:
+ """Answer every move both arms have been asked for, as arms that arrive do."""
+ asked = [*self.move.incoming(), *self.leader_move.incoming()]
+ for call in asked:
+ call.set_result(None)
+ return asked
+
+
+PARK_JOINTS = np.zeros(len(NOMINAL_JOINTS))
+
+
+def build_leader_rig(world) -> _LeaderRig:
+ dc = DataCollectionController(
+ OperatorPosition.FRONT.value, NOMINAL_JOINTS, park_joints=PARK_JOINTS, teleop=data_collection.Teleop.LEADER
+ )
+ commands, grips = RecordingEmitter(), RecordingEmitter()
+ dc.robot_commands._bind(commands)
+ dc.target_grip._bind(grips)
+ return _LeaderRig(
+ dc=dc,
+ commands=commands,
+ grips=grips,
+ state=world.pair(dc.robot_state),
+ joints=world.pair(dc.leader_joints),
+ grip=world.pair(dc.leader_grip),
+ events=world.pair(dc.session_events),
+ move=world.pair(dc.sync_move),
+ leader_move=world.pair(dc.leader_move),
+ )
+
+
+def test_the_keys_the_session_answers_to():
+ """The names a key asks by are the session's, not the keyboard's: a pedal would ask by the same ones."""
+ assert data_collection._session_event('r') is data_collection.SessionEvent.RECORD
+ assert data_collection._session_event(' ') is data_collection.SessionEvent.READY
+ assert data_collection._session_event('h') is data_collection.SessionEvent.PARK
+ assert data_collection._session_event('x') is None
+
+
+def test_a_follower_waits_until_its_leader_has_come_to_it(world):
+ """A follower that starts copying a leader standing somewhere else travels the whole way there at once.
+ Rather than take the operator's word that the arms are lined up, this waits until they are."""
+ rig = build_leader_rig(world)
+ apart, together = np.full(6, 0.5), np.full(6, 0.05)
+ marks = {}
+
+ driver = ManualDriver([
+ (lambda: rig.events.emit(data_collection.SessionEvent.READY), 0.01),
+ (rig.answer_moves, 0.01),
+ (lambda: rig.state.emit(_StandingStill(q=np.zeros(6))), 0.01),
+ (lambda: rig.grip.emit(0.0), 0.01),
+ (lambda: rig.joints.emit(apart), 0.01),
+ (lambda: rig.joints.emit(apart), 0.01),
+ (lambda: marks.update(apart=len(rig.commands.emitted)), 0.01),
+ (lambda: rig.joints.emit(together), 0.01),
+ (lambda: rig.joints.emit(together), 0.01),
+ (None, 0.01),
+ ])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ assert marks['apart'] == 0, 'the follower was sent to where the leader stood, all at once'
+ asked = rig.commands.emitted
+ assert asked, 'the follower never took up the leader it had met'
+ assert isinstance(asked[-1][1], roboarm_command.JointPosition), 'the leader was solved for, not copied'
+ np.testing.assert_allclose(asked[-1][1].positions, together)
+
+
+def test_a_follower_holds_still_until_the_session_puts_both_arms_where_they_start(world):
+ """The arms stand close the moment a run starts, and the operator has asked for nothing yet. A follower
+ that took its leader up there moves on the first hand laid on the leader."""
+ rig = build_leader_rig(world)
+ together = np.full(6, 0.05)
+
+ driver = ManualDriver([
+ (lambda: rig.state.emit(_StandingStill(q=np.zeros(6))), 0.01),
+ (lambda: rig.joints.emit(together), 0.01),
+ (lambda: rig.joints.emit(together), 0.01),
+ (None, 0.01),
+ ])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ assert not rig.commands.emitted, 'the follower took up its leader before the session asked for anything'
+
+
+def test_the_start_pose_takes_the_leader_with_the_follower(world):
+ """A leader left where it stands is the gap the follower jumps the moment it takes it up, so both arms
+ travel to the same start pose and stand together when the operator takes over."""
+ rig = build_leader_rig(world)
+ asked = []
+
+ driver = ManualDriver([
+ (lambda: rig.events.emit(data_collection.SessionEvent.READY), 0.01),
+ (lambda: asked.extend(rig.answer_moves()), 0.01),
+ (None, 0.01),
+ ])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ assert len(asked) == 2, f'the start pose reached {len(asked)} of the two arms'
+ poses = [call.request.positions for call in asked]
+ np.testing.assert_allclose(poses[0], poses[1], err_msg='the arms were sent to poses of their own')
+
+
+def test_the_park_key_takes_both_arms_to_rest(world):
+ """The pose the arms rest at is measured, not drawn: an arm that rests where it is asked to holds
+ itself there with the controller off."""
+ rig = build_leader_rig(world)
+ asked = []
+
+ driver = ManualDriver([
+ (lambda: rig.events.emit(data_collection.SessionEvent.PARK), 0.01),
+ (lambda: asked.extend(rig.answer_moves()), 0.01),
+ (None, 0.01),
+ ])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ assert len(asked) == 2, f'the rest pose reached {len(asked)} of the two arms'
+ for call in asked:
+ np.testing.assert_array_equal(call.request.positions, PARK_JOINTS)
+
+
+def test_a_reading_taken_before_the_rig_travelled_never_reaches_the_follower(world):
+ """The arms stood together before the move, so what they reported then passes for a meeting. A
+ follower that took it up would leave the leader standing and go back to the pose it was moved from."""
+ rig = build_leader_rig(world)
+ before = np.full(6, 0.5)
+
+ driver = ManualDriver([
+ (lambda: rig.state.emit(_StandingStill(q=before)), 0.01),
+ (lambda: rig.joints.emit(before), 0.01),
+ (lambda: rig.events.emit(data_collection.SessionEvent.READY), 0.01),
+ (lambda: rig.joints.emit(before), 0.01), # both arms report where they stood as they travel
+ (lambda: rig.state.emit(_StandingStill(q=before)), 0.01),
+ (rig.answer_moves, 0.01),
+ (lambda: rig.state.emit(_StandingStill(q=np.zeros(6))), 0.01),
+ (None, 0.01),
+ ])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ assert not rig.commands.emitted, 'the follower was sent to the pose the arms stood at before the move'
+
+
+def test_the_trigger_of_a_leader_holds_the_follower_grip(world):
+ """The grip crosses even before the arms meet: closing the hand is not moving the arm."""
+ rig = build_leader_rig(world)
+
+ driver = ManualDriver([(lambda: rig.grip.emit(0.75), 0.01), (None, 0.01)])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ assert rig.grips.emitted[-1][1] == pytest.approx(0.75)
+
+
+def test_the_keys_carry_the_session_a_leader_rig_has_no_buttons_for(world):
+ """The operator's hand is on the arm, so the recording and the start pose are asked for by name from
+ somewhere else — the same names whatever presses them."""
+ rig = build_leader_rig(world)
+ written = RecordingEmitter()
+ rig.dc.ds_agent_commands._bind(written)
+ asked = []
+
+ driver = ManualDriver([
+ (lambda: rig.events.emit(data_collection.SessionEvent.RECORD), 0.01),
+ (lambda: rig.events.emit(data_collection.SessionEvent.READY), 0.01),
+ (lambda: asked.extend(rig.move.incoming()), 0.01),
+ (None, 0.01),
+ ])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ started = [command for _ts, command in written.emitted]
+ assert started and started[0].type is DsWriterCommandType.START_EPISODE, 'the keys started no episode'
+ assert len(asked) == 1, 'the start pose did not reach the arm'
+
+
+def test_a_press_in_front_of_a_travelling_arm_does_not_run_the_move_again(world):
+ """An operator watching an arm cross the table presses again because it is slow, not because they want
+ a second start pose. The arm that arrives is the one the first press asked for."""
+ rig = build_leader_rig(world)
+ asked, marks = [], {}
+
+ driver = ManualDriver([
+ (lambda: rig.events.emit(data_collection.SessionEvent.READY), 0.01),
+ (lambda: asked.extend(rig.move.incoming()), 0.01),
+ (lambda: rig.events.emit(data_collection.SessionEvent.READY), 0.01), # ... while the arm travels
+ (lambda: rig.events.emit(data_collection.SessionEvent.READY), 0.01),
+ (lambda: asked.extend(rig.move.incoming()), 0.01),
+ (lambda: marks.update(travelling=len(asked)), 0.01),
+ (lambda: [call.set_result(None) for call in asked], 0.01),
+ (lambda: asked.extend(rig.move.incoming()), 0.01),
+ (None, 0.01),
+ ])
+ drive_scheduler(world.start([rig.dc, driver]), steps=400)
+
+ assert marks['travelling'] == 1, 'a press in front of the travelling arm asked for the move again'
+ assert len(asked) == 1, 'the presses the arm outran reached it after it landed'
+
+
+def test_an_arm_driven_by_both_a_leader_and_a_headset_is_refused():
+ """Two things asking one arm to be in two places at once is not a rig; it is a fight, and the arm loses
+ it somewhere over the table."""
+ with pytest.raises(ValueError, match='leader'):
+ data_collection.main(
+ robot_arm=DummyRobot(),
+ gripper=None,
+ webxr=WebXR(port=0),
+ sound=None,
+ cameras=None,
+ nominal_joints=NOMINAL_JOINTS.tolist(),
+ leader=DummyRobot(),
+ )
+
+
+def test_a_rest_pose_that_does_not_cover_every_joint_of_the_start_pose_is_refused():
+ """Both poses belong to one arm. A rest pose of another length reaches the arm as a failed move, with
+ nothing to say which of the two the station named wrong."""
+ with pytest.raises(ValueError, match='park_joints'):
+ data_collection.main(
+ robot_arm=DummyRobot(),
+ gripper=None,
+ webxr=WebXR(port=0),
+ sound=None,
+ cameras=None,
+ nominal_joints=NOMINAL_JOINTS.tolist(),
+ park_joints=NOMINAL_JOINTS[:-1].tolist(),
+ )
+
+
+def test_a_leader_with_no_follower_is_refused():
+ """A leader is held to drive a follower. One with nothing on the other end moves nothing at all."""
+ with pytest.raises(ValueError, match='leader'):
+ data_collection.main(robot_arm=None, gripper=None, webxr=None, sound=None, cameras=None, leader=DummyRobot())
+
+
def test_data_collection_with_mujoco_robot_gripper(tmp_path):
sim = MujocoSim('positronic/assets/mujoco/franka_table.xml', loaders=())
diff --git a/positronic/vendors/lerobot_0_3_3/codecs.py b/positronic/vendors/lerobot_0_3_3/codecs.py
index d46fc0d2b..6587c3978 100644
--- a/positronic/vendors/lerobot_0_3_3/codecs.py
+++ b/positronic/vendors/lerobot_0_3_3/codecs.py
@@ -18,6 +18,17 @@
horizon=1.0,
)
+# The Trossen WidowX AI carries six arm joints where the codecs above assume seven, and a demonstration
+# driven by a leader arm records the joints the leader asked for. Those are what the follower's own driver
+# takes back, with no pose to solve for on the way in or out.
+trossen_joints = codecs.compose.override(
+ obs=codecs.joints_obs.override(state_features={keys.JOINTS: 6, keys.GRIP: 1}),
+ action=codecs.absolute_joints_action.override(
+ tgt_joints_key=keys.TARGET_JOINTS, tgt_grip_key=keys.TARGET_GRIP, num_joints=6
+ ),
+ horizon=1.0,
+)
+
# IK variants: reconstruct joint targets from recorded EE targets via IK
joints_ik = ee.override(obs=codecs.joints_obs, action=codecs.ik_joints_action)
joints_ik_sim = joints_ik.override(**{'action.solver': 'lm'})
diff --git a/positronic/vendors/lerobot_0_3_3/server.py b/positronic/vendors/lerobot_0_3_3/server.py
index 7b6e9e74a..661bd43ba 100644
--- a/positronic/vendors/lerobot_0_3_3/server.py
+++ b/positronic/vendors/lerobot_0_3_3/server.py
@@ -93,6 +93,7 @@ def pipeline(codec: Codec, source: ModelSource):
joints_traj = pipeline.override(codec=lerobot_codecs.joints_traj)
joints_ik = pipeline.override(codec=lerobot_codecs.joints_ik)
joints_ik_sim = pipeline.override(codec=lerobot_codecs.joints_ik_sim)
+trossen_joints = pipeline.override(codec=lerobot_codecs.trossen_joints)
# For checkpoints trained on inverted-grip (1 = open) sim data, which speak the flipped convention.
ee_flip = pipeline.override(codec=lerobot_codecs.ee.override(flip_grip=True))
@@ -107,6 +108,7 @@ def pipeline(codec: Codec, source: ModelSource):
'joints_traj': serve.override(pipeline=joints_traj),
'joints_ik': serve.override(pipeline=joints_ik),
'joints_ik_sim': serve.override(pipeline=joints_ik_sim),
+ 'trossen_joints': serve.override(pipeline=trossen_joints),
'ee_flip': serve.override(pipeline=ee_flip),
'phail': serve.override(
pipeline=ee.override(
diff --git a/pyproject.toml b/pyproject.toml
index f3c790f4a..005450dd5 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -106,6 +106,10 @@ lance = [
yam = [
"i2rt", # i2rt YAM arm: joint-space position-PD + gravity-comp over CAN; not on PyPI, see [tool.uv.sources]
]
+trossen = [
+ # Trossen WidowX AI arm, over Ethernet. The SDK version must match the arm firmware version.
+ "trossen-arm>=1.11,<1.12",
+]
telemetry = [
# `positronic eval run --timing` only: the OTel SDK + exporter that write the span sidecars, and the
# machine-load sampler's probes.
diff --git a/uv.lock b/uv.lock
index c025bf7c0..e5e94309c 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,52 +3,52 @@ revision = 3
requires-python = ">=3.11, <3.14"
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
conflicts = [[
@@ -319,18 +319,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -367,34 +367,34 @@ version = "16.1.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
sdist = { url = "https://files.pythonhosted.org/packages/78/cd/3a83ffbc3cc25b39721d174487fb0d51a76582f4a1703f98e46170ce83d4/av-16.1.0.tar.gz", hash = "sha256:a094b4fd87a3721dacf02794d3d2c82b8d712c85b9534437e82a8a978c175ffd", size = 4285203, upload-time = "2026-01-11T07:31:33.772Z" }
@@ -637,10 +637,10 @@ version = "8.1.8"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -657,46 +657,46 @@ version = "8.4.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -723,18 +723,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -768,14 +768,14 @@ version = "4.2.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
@@ -817,10 +817,10 @@ version = "5.4.3.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -845,46 +845,46 @@ version = "6.0.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -906,10 +906,10 @@ version = "1.87.0.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -941,46 +941,46 @@ version = "1.89.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -1481,14 +1481,14 @@ version = "3.6.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
@@ -1520,18 +1520,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -1631,18 +1631,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -1669,14 +1669,14 @@ version = "0.36.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
@@ -1787,10 +1787,10 @@ version = "3.10.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -1819,46 +1819,46 @@ version = "3.12.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -1908,10 +1908,10 @@ version = "1.2.9"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -1934,46 +1934,46 @@ version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -2270,14 +2270,14 @@ version = "0.29.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
@@ -2300,18 +2300,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -2456,25 +2456,25 @@ version = "0.35.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
@@ -2510,28 +2510,28 @@ version = "1.19.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -3129,14 +3129,14 @@ version = "0.3.3"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
@@ -3177,18 +3177,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -3859,10 +3859,10 @@ version = "2.2.6"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
@@ -3915,46 +3915,46 @@ version = "2.3.5"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
@@ -4213,18 +4213,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -4249,34 +4249,34 @@ version = "4.13.0.92"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -4407,18 +4407,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -4435,34 +4435,34 @@ version = "26.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
@@ -4621,10 +4621,10 @@ version = "3.4.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -4655,46 +4655,46 @@ version = "3.8.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -4726,10 +4726,10 @@ version = "0.9.16"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -4762,46 +4762,46 @@ version = "0.9.20"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -4974,6 +4974,9 @@ telemetry = [
{ name = "opentelemetry-exporter-otlp-json-file" },
{ name = "opentelemetry-sdk" },
]
+trossen = [
+ { name = "trossen-arm" },
+]
yam = [
{ name = "i2rt" },
]
@@ -5057,11 +5060,12 @@ requires-dist = [
{ name = "torch", marker = "extra == 'molmoact2'" },
{ name = "torchvision", marker = "extra == 'molmoact2'" },
{ name = "transformers", marker = "extra == 'molmoact2'", specifier = ">=5.3,<6" },
+ { name = "trossen-arm", marker = "extra == 'trossen'", specifier = ">=1.11,<1.12" },
{ name = "uvicorn", extras = ["standard"] },
{ name = "websockets", specifier = ">=15.0.1" },
{ name = "zmq" },
]
-provides-extras = ["openpi", "hardware", "lerobot-0-3-3", "lerobot", "molmoact2", "dreamzero", "lance", "yam", "telemetry"]
+provides-extras = ["openpi", "hardware", "lerobot-0-3-3", "lerobot", "molmoact2", "dreamzero", "lance", "yam", "trossen", "telemetry"]
[package.metadata.requires-dev]
dev = [
@@ -5943,10 +5947,10 @@ version = "4.7.1"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -5972,46 +5976,46 @@ version = "4.8.2"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'darwin' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra == 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
dependencies = [
@@ -6552,18 +6556,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -6580,34 +6584,34 @@ version = "82.0.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
+ "python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'emscripten' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32' and extra != 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform == 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version >= '3.13' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
- "python_full_version == '3.12.*' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
"python_full_version < '3.12' and sys_platform != 'linux' and extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra != 'extra-10-positronic-molmoact2' and extra != 'extra-10-positronic-yam'",
]
sdist = { url = "https://files.pythonhosted.org/packages/82/f3/748f4d6f65d1756b9ae577f329c951cda23fb900e4de9f70900ced962085/setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb", size = 1144893, upload-time = "2026-02-08T15:08:40.206Z" }
@@ -6967,18 +6971,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -7007,10 +7011,10 @@ version = "5.11.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform != 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform != 'linux'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform != 'linux'",
]
dependencies = [
@@ -7070,6 +7074,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/28/71/bd20ffcb7a64c753dc2463489a61bf69d531f308e390ad06390268c4ea04/triton-3.3.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42", size = 155735832, upload-time = "2025-05-29T23:40:10.522Z" },
]
+[[package]]
+name = "trossen-arm"
+version = "1.11.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(extra == 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-lerobot-0-3-3') or (extra == 'extra-10-positronic-lerobot' and extra == 'extra-10-positronic-molmoact2') or (extra == 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-molmoact2') or (extra != 'extra-10-positronic-molmoact2' and extra == 'extra-10-positronic-yam') or (extra != 'extra-10-positronic-lerobot' and extra != 'extra-10-positronic-lerobot-0-3-3' and extra == 'extra-10-positronic-yam')" },
+ { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "extra == 'extra-10-positronic-lerobot' or extra == 'extra-10-positronic-lerobot-0-3-3' or extra == 'extra-10-positronic-molmoact2' or extra != 'extra-10-positronic-yam'" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/1e/ea1b83640f0561a1f1b4671b8acdd2d0fa4d1eb95ec30dd0810565f65535/trossen_arm-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cc208d6fa733eeb8642b33940b238742f96a217faf405e3363a4bceb9011b99f", size = 747814, upload-time = "2026-07-01T23:47:19.201Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/83/82088d9e747b5d44a922d551309740162299f2b29b47c3516fc27a2083fa/trossen_arm-1.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e70d0436e6eeb4a4d094282d7118ff562da7ed528339531d0caa77c906716b18", size = 1156918, upload-time = "2026-07-01T23:47:20.363Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/a8/1a09f3372d4a2d88bf2e977ff3d4e2afd74cd25d63128fa48548960a69de/trossen_arm-1.11.0-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a2f169c3ff55dd17e5c150b5e48c8f58079a6386a6f91bc19c19843966b040b", size = 980217, upload-time = "2026-07-01T23:47:21.842Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/37/1a61b635b944a619726c614aa146fdd67ac8096fc36bd9c7428dc366f2db/trossen_arm-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:835c2e547d5239e15963df3659f4f26ae772e25df524e9f99849dda23ca7b4a9", size = 753053, upload-time = "2026-07-01T23:47:22.99Z" },
+ { url = "https://files.pythonhosted.org/packages/17/16/c7d4d318eee4632bbe84bc7928c792635be7e5186a94aa8954e864739248/trossen_arm-1.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654b03ea7055f8bca38be0fc03d625cd2fc11dc7290ec09ca00075e4d25744f8", size = 1159854, upload-time = "2026-07-01T23:47:24.184Z" },
+ { url = "https://files.pythonhosted.org/packages/72/c9/2d6b5c0b345ee7728a8570027745c3675383342bd236d425bbd85e41c30c/trossen_arm-1.11.0-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96549d0a266a1104e0648e1278dae85e5f743e372ef378c22d66612da09c0a52", size = 981086, upload-time = "2026-07-01T23:47:25.655Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/bc/65acd5d3c0ff4381b09553f4618cfef65c71ba3357b18d112e9174ffa58c/trossen_arm-1.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:74f4fbf64147667e0a10795708d840b64b5ba615267c8cfac27dd7f1d876a372", size = 714975, upload-time = "2026-07-01T23:47:27.365Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/69/f2bea2c1b9eb15d1658d94533984a6ca1f1ef864c5038af5519b84901860/trossen_arm-1.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:698b639c5152bd2b4cba135778cd2c36b17fdb9f2c89c3fb11d73657f293c3f7", size = 1159772, upload-time = "2026-07-01T23:47:28.552Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/9f/f1abdebc7195be7fc6aaa2fbeea8c4fca923ad90da2651ff48b2cb05b2fb/trossen_arm-1.11.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68d51aa028f5b1c8e81aa2168747b9febee72aea523b1db35364b14a342bda5b", size = 981254, upload-time = "2026-07-01T23:47:29.943Z" },
+]
+
[[package]]
name = "typeguard"
version = "4.5.2"
@@ -7260,18 +7284,18 @@ source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
- "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'darwin'",
- "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux'",
+ "python_full_version == '3.12.*' and sys_platform == 'darwin'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'darwin'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
@@ -7308,14 +7332,14 @@ version = "0.25.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
"python_full_version >= '3.13' and sys_platform == 'linux'",
- "python_full_version == '3.12.*' and sys_platform == 'linux'",
- "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version >= '3.13' and sys_platform == 'win32'",
"python_full_version >= '3.13' and sys_platform == 'emscripten'",
"python_full_version >= '3.13' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version == '3.12.*' and sys_platform == 'linux'",
"python_full_version == '3.12.*' and sys_platform == 'win32'",
"python_full_version == '3.12.*' and sys_platform == 'emscripten'",
"python_full_version == '3.12.*' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",
+ "python_full_version < '3.12' and sys_platform == 'linux'",
"python_full_version < '3.12' and sys_platform == 'win32'",
"python_full_version < '3.12' and sys_platform == 'emscripten'",
"python_full_version < '3.12' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32'",