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'",