diff --git a/apriltag/src/main/python/pyproject.toml b/apriltag/src/main/python/pyproject.toml index 83e4728f68..d59a934d35 100644 --- a/apriltag/src/main/python/pyproject.toml +++ b/apriltag/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", diff --git a/apriltag/src/main/python/semiwrap/AprilTagDetector.yml b/apriltag/src/main/python/semiwrap/AprilTagDetector.yml index df9e1d970d..c614eb3e47 100644 --- a/apriltag/src/main/python/semiwrap/AprilTagDetector.yml +++ b/apriltag/src/main/python/semiwrap/AprilTagDetector.yml @@ -59,6 +59,9 @@ classes: :return: list of results )doc" ) + .def("close", [](AprilTagDetector &self) { + self = AprilTagDetector(); + }, py::doc("Frees all resources associated with this detector")) wpi::apriltag::AprilTagDetector::Config: attributes: numThreads: diff --git a/commandsv2/src/main/python/LICENSE b/commandsv2/src/main/python/LICENSE new file mode 100644 index 0000000000..3d5a824cad --- /dev/null +++ b/commandsv2/src/main/python/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2009-2021 FIRST and other WPILib contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of FIRST, WPILib, nor the names of other WPILib + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY FIRST AND OTHER WPILIB CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY NONINFRINGEMENT AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FIRST 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/commandsv2/src/main/python/README.md b/commandsv2/src/main/python/README.md new file mode 100644 index 0000000000..4af1fccb1d --- /dev/null +++ b/commandsv2/src/main/python/README.md @@ -0,0 +1,7 @@ +robotpy-commands-v2 +=================== + +Python wrappers around a modified version of the new WPILib commands library. + +* Documentation @ https://robotpy.readthedocs.io/projects/commands-v2 +* Examples @ https://github.com/robotpy/examples/tree/main/commands-v2 diff --git a/commandsv2/src/main/python/commands2/button/commandgenerichid.py b/commandsv2/src/main/python/commands2/button/commandgenerichid.py index 61561de955..eb0014e3cf 100644 --- a/commandsv2/src/main/python/commands2/button/commandgenerichid.py +++ b/commandsv2/src/main/python/commands2/button/commandgenerichid.py @@ -9,7 +9,7 @@ from .trigger import Trigger class CommandGenericHID: """ - A version of :class:`wpilib.interfaces.GenericHID` with :class:`.Trigger` factories for command-based. + A version of :class:`wpilib.GenericHID` with :class:`.Trigger` factories for command-based. """ def __init__(self, port: int): diff --git a/commandsv2/src/main/python/commands2/button/joystickbutton.py b/commandsv2/src/main/python/commands2/button/joystickbutton.py index 4edd68d59f..3476959855 100644 --- a/commandsv2/src/main/python/commands2/button/joystickbutton.py +++ b/commandsv2/src/main/python/commands2/button/joystickbutton.py @@ -6,7 +6,7 @@ from .trigger import Trigger class JoystickButton(Trigger): """ - A Button that gets its state from a :class:`wpilib.interfaces.GenericHID`. + A Button that gets its state from a :class:`wpilib.GenericHID`. """ def __init__(self, joystick: GenericHID, buttonNumber: int): @@ -14,6 +14,6 @@ class JoystickButton(Trigger): Creates a joystick button for triggering commands. :param joystick: The GenericHID object that has the button (e.g. Joystick, KinectStick, etc) - :param buttonNumber: The button number (see :func:`wpilib.interfaces.GenericHID.getRawButton` + :param buttonNumber: The button number (see :func:`wpilib.GenericHID.getRawButton` """ super().__init__(lambda: joystick.getRawButton(buttonNumber)) diff --git a/commandsv2/src/main/python/commands2/button/networkbutton.py b/commandsv2/src/main/python/commands2/button/networkbutton.py index 34fe28aaac..e54660ccf5 100644 --- a/commandsv2/src/main/python/commands2/button/networkbutton.py +++ b/commandsv2/src/main/python/commands2/button/networkbutton.py @@ -112,8 +112,7 @@ class NetworkButton(Trigger): if inst is not None and table is not None and field is not None: return init_inst_table_field(inst, table, field) - raise TypeError( - f""" + raise TypeError(f""" TypeError: NetworkButton(): incompatible function arguments. The following argument types are supported: 1. (self: NetworkButton, topic: BooleanTopic) 2. (self: NetworkButton, sub: BooleanSubscriber) @@ -122,5 +121,4 @@ TypeError: NetworkButton(): incompatible function arguments. The following argum 5. (self: NetworkButton, inst: NetworkTableInstance, table: str, field: str) Invoked with: {format_args_kwargs(self, *args, **kwargs)} -""" - ) +""") diff --git a/commandsv2/src/main/python/commands2/button/povbutton.py b/commandsv2/src/main/python/commands2/button/povbutton.py index 5696dcdf36..28b2b2d765 100644 --- a/commandsv2/src/main/python/commands2/button/povbutton.py +++ b/commandsv2/src/main/python/commands2/button/povbutton.py @@ -6,7 +6,7 @@ from .trigger import Trigger class POVButton(Trigger): """ - A Button that gets its state from a POV on a :class:`wpilib.interfaces.GenericHID`. + A Button that gets its state from a POV on a :class:`wpilib.GenericHID`. """ def __init__(self, joystick: GenericHID, angle: int, povNumber: int = 0): @@ -15,6 +15,6 @@ class POVButton(Trigger): :param joystick: The GenericHID object that has the POV :param angle: The desired angle in degrees (e.g. 90, 270) - :param povNumber: The POV number (see :func:`wpilib.interfaces.GenericHID.getPOV`) + :param povNumber: The POV number (see :func:`wpilib.GenericHID.getPOV`) """ super().__init__(lambda: joystick.getPOV(povNumber) == angle) diff --git a/commandsv2/src/main/python/commands2/button/trigger.py b/commandsv2/src/main/python/commands2/button/trigger.py index 85f65f3052..26356064ee 100644 --- a/commandsv2/src/main/python/commands2/button/trigger.py +++ b/commandsv2/src/main/python/commands2/button/trigger.py @@ -73,16 +73,29 @@ class Trigger: if loop is not None and condition is not None: return init_loop_condition(loop, condition) - raise TypeError( - f""" + raise TypeError(f""" TypeError: Trigger(): incompatible function arguments. The following argument types are supported: 1. (self: Trigger) 2. (self: Trigger, condition: () -> bool) 3. (self: Trigger, loop: EventLoop, condition: () -> bool) Invoked with: {format_args_kwargs(self, *args, **kwargs)} -""" - ) +""") + + def _add_binding(self, body: Callable[[bool, bool], None]) -> None: + """ + Adds a binding to the EventLoop. + + :param body: The body of the binding to add. + """ + + state = SimpleNamespace(previous=self._condition()) + + @self._loop.bind + def _(): + current = self._condition() + body(state.previous, current) + state.previous = current def onTrue(self, command: Command) -> Self: """ @@ -92,14 +105,10 @@ Invoked with: {format_args_kwargs(self, *args, **kwargs)} :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) - - @self._loop.bind - def _(): - pressed = self._condition() - if not state.pressed_last and pressed: + @self._add_binding + def _(previous, current): + if not previous and current: command.schedule() - state.pressed_last = pressed return self @@ -111,14 +120,10 @@ Invoked with: {format_args_kwargs(self, *args, **kwargs)} :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) - - @self._loop.bind - def _(): - pressed = self._condition() - if state.pressed_last and not pressed: + @self._add_binding + def _(previous, current): + if previous and not current: command.schedule() - state.pressed_last = pressed return self @@ -130,17 +135,11 @@ Invoked with: {format_args_kwargs(self, *args, **kwargs)} :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) - - @self._loop.bind - def _(): - pressed = self._condition() - - if state.pressed_last != pressed: + @self._add_binding + def _(previous, current): + if previous != current: command.schedule() - state.pressed_last = pressed - return self def whileTrue(self, command: Command) -> Self: @@ -155,16 +154,12 @@ Invoked with: {format_args_kwargs(self, *args, **kwargs)} :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) - - @self._loop.bind - def _(): - pressed = self._condition() - if not state.pressed_last and pressed: + @self._add_binding + def _(previous, current): + if not previous and current: command.schedule() - elif state.pressed_last and not pressed: + elif previous and not current: command.cancel() - state.pressed_last = pressed return self @@ -180,16 +175,12 @@ Invoked with: {format_args_kwargs(self, *args, **kwargs)} :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) - - @self._loop.bind - def _(): - pressed = self._condition() - if state.pressed_last and not pressed: + @self._add_binding + def _(previous, current): + if previous and not current: command.schedule() - elif not state.pressed_last and pressed: + elif not previous and current: command.cancel() - state.pressed_last = pressed return self @@ -201,17 +192,13 @@ Invoked with: {format_args_kwargs(self, *args, **kwargs)} :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) - - @self._loop.bind - def _(): - pressed = self._condition() - if not state.pressed_last and pressed: + @self._add_binding + def _(previous, current): + if not previous and current: if command.isScheduled(): command.cancel() else: command.schedule() - state.pressed_last = pressed return self @@ -223,17 +210,13 @@ Invoked with: {format_args_kwargs(self, *args, **kwargs)} :returns: this trigger, so calls can be chained """ - state = SimpleNamespace(pressed_last=self._condition()) - - @self._loop.bind - def _(): - pressed = self._condition() - if state.pressed_last and not pressed: + @self._add_binding + def _(previous, current): + if previous and not current: if command.isScheduled(): command.cancel() else: command.schedule() - state.pressed_last = pressed return self diff --git a/commandsv2/src/main/python/commands2/commandscheduler.py b/commandsv2/src/main/python/commands2/commandscheduler.py index d1203c8a4d..45d6985a4c 100644 --- a/commandsv2/src/main/python/commands2/commandscheduler.py +++ b/commandsv2/src/main/python/commands2/commandscheduler.py @@ -9,13 +9,14 @@ from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Union import hal from typing_extensions import Self from wpilib import ( - RobotBase, DriverStation, EventLoop, + RobotBase, TimedRobot, Watchdog, reportWarning, ) + from wpiutil import Sendable, SendableBuilder, SendableRegistry from .command import Command, InterruptionBehavior diff --git a/commandsv2/src/main/python/commands2/proxycommand.py b/commandsv2/src/main/python/commands2/proxycommand.py index 21d1cb1442..564f34b39f 100644 --- a/commandsv2/src/main/python/commands2/proxycommand.py +++ b/commandsv2/src/main/python/commands2/proxycommand.py @@ -79,15 +79,13 @@ class ProxyCommand(Command): elif callable(args[0]): return init_supplier(args[0]) - raise TypeError( - f""" + raise TypeError(f""" TypeError: ProxyCommand(): incompatible function arguments. The following argument types are supported: 1. (self: ProxyCommand, supplier: () -> Command) 2. (self: ProxyCommand, command: Command) Invoked with: {format_args_kwargs(self, *args, **kwargs)} -""" - ) +""") def initialize(self): self._command = self._supplier() diff --git a/commandsv2/src/main/python/commands2/sysid/__init__.py b/commandsv2/src/main/python/commands2/sysid/__init__.py index 6a0719126c..8b4874df0d 100644 --- a/commandsv2/src/main/python/commands2/sysid/__init__.py +++ b/commandsv2/src/main/python/commands2/sysid/__init__.py @@ -1,4 +1,3 @@ from .sysidroutine import SysIdRoutine - __all__ = ["SysIdRoutine"] diff --git a/commandsv2/src/main/python/commands2/sysid/sysidroutine.py b/commandsv2/src/main/python/commands2/sysid/sysidroutine.py index 6b1b2b3bda..ac3f3436db 100644 --- a/commandsv2/src/main/python/commands2/sysid/sysidroutine.py +++ b/commandsv2/src/main/python/commands2/sysid/sysidroutine.py @@ -11,7 +11,6 @@ from wpimath.units import seconds, volts from typing import Callable, Optional - volts_per_second = float diff --git a/commandsv2/src/main/python/commands2/waituntilcommand.py b/commandsv2/src/main/python/commands2/waituntilcommand.py index d97526773c..c195a550f7 100644 --- a/commandsv2/src/main/python/commands2/waituntilcommand.py +++ b/commandsv2/src/main/python/commands2/waituntilcommand.py @@ -63,15 +63,13 @@ class WaitUntilCommand(Command): elif callable(args[0]): return init_condition(args[0]) - raise TypeError( - f""" + raise TypeError(f""" TypeError: WaitUntilCommand(): incompatible function arguments. The following argument types are supported: 1. (self: WaitUntilCommand, condition: () -> bool) 2. (self: WaitUntilCommand, time: wpimath.units.seconds) Invoked with: {format_args_kwargs(self, *args, **kwargs)} -""" - ) +""") def isFinished(self) -> bool: return self._condition() diff --git a/commandsv2/src/main/python/pyproject.toml b/commandsv2/src/main/python/pyproject.toml new file mode 100644 index 0000000000..737405e705 --- /dev/null +++ b/commandsv2/src/main/python/pyproject.toml @@ -0,0 +1,36 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "robotpy-commands-v2" +version = "0.0.0" +description = "WPILib command framework v2" +readme = "README.md" +requires-python = ">=3.10" +license = "BSD-3-Clause" +license-files = ["LICENSE"] +dependencies = [ + "wpilib==0.0.0", + "typing_extensions>=4.1.0,<5", +] + +[[project.authors]] +name = "RobotPy Development Team" +email = "robotpy@googlegroups.com" + +[[project.maintainers]] +name = "RobotPy Development Team" +email = "robotpy@googlegroups.com" + +[project.urls] +"Source code" = "https://github.com/robotpy/robotpy-commands-v2" + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.targets.sdist] +packages = ["commands2"] + +[tool.hatch.build.targets.wheel] +packages = ["commands2"] \ No newline at end of file diff --git a/commandsv2/src/test/python/util.py b/commandsv2/src/test/python/util.py index 0331840394..b90be4683a 100644 --- a/commandsv2/src/test/python/util.py +++ b/commandsv2/src/test/python/util.py @@ -5,7 +5,6 @@ import inspect import commands2 from wpilib.simulation import DriverStationSim, pauseTiming, resumeTiming, stepTiming - Y = TypeVar("Y") diff --git a/datalog/src/main/python/pyproject.toml b/datalog/src/main/python/pyproject.toml index 4bda639fd3..bb4edd68c5 100644 --- a/datalog/src/main/python/pyproject.toml +++ b/datalog/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatchling", "robotpy-native-datalog==0.0.0", diff --git a/hal/src/main/python/hal/simulation/__init__.py b/hal/src/main/python/hal/simulation/__init__.py index 9617a1f11c..3a624cee41 100644 --- a/hal/src/main/python/hal/simulation/__init__.py +++ b/hal/src/main/python/hal/simulation/__init__.py @@ -2,3 +2,5 @@ from . import _init__simulation from ._simulation import * del _init__simulation + +from .mockhooks import waitForProgramStart diff --git a/hal/src/main/python/hal/simulation/mockhooks.py b/hal/src/main/python/hal/simulation/mockhooks.py new file mode 100644 index 0000000000..fc4ef11b22 --- /dev/null +++ b/hal/src/main/python/hal/simulation/mockhooks.py @@ -0,0 +1,24 @@ +import time +import typing as T + +from ._simulation import getProgramStarted + + +def waitForProgramStart(timeout: T.Optional[float] = None, delta: float = 0.001): + """ + Polls robot program and returns when it has reported that it started + + :param timeout: Amount of time to wait + :param delta: Amount of time to sleep between checks + """ + + # This is basically the same thing that the C version of this function + # does. Implemented in python so that CTRL-C works. + + until = None + if timeout and timeout > 0: + until = time.monotonic() + timeout + while not getProgramStarted(): + if until is not None and time.monotonic() > until: + raise TimeoutError("Program did not start") + time.sleep(delta) diff --git a/hal/src/main/python/pyproject.toml b/hal/src/main/python/pyproject.toml index 41c57f8a19..f46cb3a83b 100644 --- a/hal/src/main/python/pyproject.toml +++ b/hal/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatchling", "pyntcore==0.0.0", diff --git a/hal/src/main/python/semiwrap/simulation/MockHooks.yml b/hal/src/main/python/semiwrap/simulation/MockHooks.yml index 94aafdcf71..47d45a8475 100644 --- a/hal/src/main/python/semiwrap/simulation/MockHooks.yml +++ b/hal/src/main/python/semiwrap/simulation/MockHooks.yml @@ -8,6 +8,7 @@ strip_prefixes: functions: HALSIM_SetRuntimeType: HALSIM_WaitForProgramStart: + ignore: true HALSIM_SetProgramStarted: HALSIM_GetProgramStarted: HALSIM_SetProgramState: diff --git a/ntcore/src/main/python/devtools/gen-pubsub.py b/ntcore/src/main/python/devtools/gen-pubsub.py index 5a2ccaa62f..3a52551ef3 100755 --- a/ntcore/src/main/python/devtools/gen-pubsub.py +++ b/ntcore/src/main/python/devtools/gen-pubsub.py @@ -43,8 +43,7 @@ if __name__ == "__main__": sphinxify.process_raw("\n".join(docs)).splitlines() ) - print( - f""" + print(f""" // autogenerated by gen-pubsub.py .def(py::init([]( {paramstr} @@ -60,5 +59,4 @@ if __name__ == "__main__": )" ) - """ - ) + """) diff --git a/ntcore/src/main/python/pyproject.toml b/ntcore/src/main/python/pyproject.toml index 1401c7c148..9a9331ef3f 100644 --- a/ntcore/src/main/python/pyproject.toml +++ b/ntcore/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", diff --git a/ntcore/src/test/python/test_util.py b/ntcore/src/test/python/test_util.py index fc0d35ec6c..1d74ffe6e1 100644 --- a/ntcore/src/test/python/test_util.py +++ b/ntcore/src/test/python/test_util.py @@ -3,7 +3,6 @@ import pytest from ntcore import NetworkTableInstance, NetworkTableType from ntcore.util import ntproperty, ChooserControl - # def test_autoupdatevalue(nt): # # tricksy: make sure that this works *before* initialization diff --git a/requirements.txt b/requirements.txt index a363a2a4ec..9007c4be32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ jinja2==3.1.6 protobuf==5.28.3 grpcio-tools==1.68.0 -semiwrap==0.2.4 +semiwrap==0.2.6 pytest>=3.9 pytest-reraise numpy diff --git a/requirements_lock.txt b/requirements_lock.txt index 1992b1a582..7b298aed41 100644 --- a/requirements_lock.txt +++ b/requirements_lock.txt @@ -37,9 +37,9 @@ click==8.3.1 \ --hash=sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a \ --hash=sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6 # via black -cxxheaderparser[pcpp]==1.6.2 \ - --hash=sha256:4574f09908609a7bf5d93841fae6e677b803e9708f564ae599382c1f0e18c7ed \ - --hash=sha256:92aae2b45cb96e4bb5b4be392cca73afef439f06d612c5da3a9cab6d7a13e4ae +cxxheaderparser[pcpp]==1.7.0 \ + --hash=sha256:2d0282e6f3cc25484acb2d737b3b5a646b947ae48cc0060242c50925d402f2ed \ + --hash=sha256:3c8ade60cc0c102bdd3eeb26fbc9ae30c3ae9b74ce881fab91e8fe8647b1a5f4 # via semiwrap dictdiffer==0.9.0 \ --hash=sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578 \ @@ -566,9 +566,9 @@ ruamel-yaml-clib==0.2.15 \ --hash=sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51 \ --hash=sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f # via ruamel-yaml -semiwrap==0.2.4 \ - --hash=sha256:6f972a103271c3e751b71cfb07ddc032ee4451ef97e830ec466a7ccfb2f6318d \ - --hash=sha256:b057280146b120bbad6d651a55e26aedcab5675e13aa52c32ff9c883dca37f56 +semiwrap==0.2.6 \ + --hash=sha256:cc260ac824af2020f87cc95bc25e41d6486783fe886b43d846b801c034a8a0fb \ + --hash=sha256:d6ce64193abe8db5743ec27873d65bc53a2a7f35d3c16ce91aa8e9ea21d2d05e # via -r requirements.txt sphinxify==0.12 \ --hash=sha256:3ec299e78babac7d3457f47bf263411b48e10b9c8add18d7159fa0327cc4a061 \ diff --git a/requirements_windows_lock.txt b/requirements_windows_lock.txt index ae79d03342..3c86f82470 100644 --- a/requirements_windows_lock.txt +++ b/requirements_windows_lock.txt @@ -44,9 +44,9 @@ colorama==0.4.6 \ # click # pkgconf # pytest -cxxheaderparser[pcpp]==1.6.2 \ - --hash=sha256:4574f09908609a7bf5d93841fae6e677b803e9708f564ae599382c1f0e18c7ed \ - --hash=sha256:92aae2b45cb96e4bb5b4be392cca73afef439f06d612c5da3a9cab6d7a13e4ae +cxxheaderparser[pcpp]==1.7.0 \ + --hash=sha256:2d0282e6f3cc25484acb2d737b3b5a646b947ae48cc0060242c50925d402f2ed \ + --hash=sha256:3c8ade60cc0c102bdd3eeb26fbc9ae30c3ae9b74ce881fab91e8fe8647b1a5f4 # via semiwrap dictdiffer==0.9.0 \ --hash=sha256:17bacf5fbfe613ccf1b6d512bd766e6b21fb798822a133aa86098b8ac9997578 \ @@ -570,9 +570,9 @@ ruamel-yaml-clib==0.2.15 \ --hash=sha256:fd4c928ddf6bce586285daa6d90680b9c291cfd045fc40aad34e445d57b1bf51 \ --hash=sha256:fe239bdfdae2302e93bd6e8264bd9b71290218fff7084a9db250b55caaccf43f # via ruamel-yaml -semiwrap==0.2.4 \ - --hash=sha256:6f972a103271c3e751b71cfb07ddc032ee4451ef97e830ec466a7ccfb2f6318d \ - --hash=sha256:b057280146b120bbad6d651a55e26aedcab5675e13aa52c32ff9c883dca37f56 +semiwrap==0.2.6 \ + --hash=sha256:cc260ac824af2020f87cc95bc25e41d6486783fe886b43d846b801c034a8a0fb \ + --hash=sha256:d6ce64193abe8db5743ec27873d65bc53a2a7f35d3c16ce91aa8e9ea21d2d05e # via -r requirements.txt sphinxify==0.12 \ --hash=sha256:3ec299e78babac7d3457f47bf263411b48e10b9c8add18d7159fa0327cc4a061 \ diff --git a/romiVendordep/src/main/python/pyproject.toml b/romiVendordep/src/main/python/pyproject.toml index a1cf3be978..e1d92c7502 100644 --- a/romiVendordep/src/main/python/pyproject.toml +++ b/romiVendordep/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", diff --git a/romiVendordep/src/main/python/romi/cli.py b/romiVendordep/src/main/python/romi/cli.py index 04753e73bb..00a434d7fb 100644 --- a/romiVendordep/src/main/python/romi/cli.py +++ b/romiVendordep/src/main/python/romi/cli.py @@ -6,7 +6,6 @@ import typing import wpilib - if sys.version_info < (3, 10): def entry_points(group): diff --git a/simulation/halsim_gui/src/main/python/pyproject.toml b/simulation/halsim_gui/src/main/python/pyproject.toml index 714efe734c..f2f1b537dd 100644 --- a/simulation/halsim_gui/src/main/python/pyproject.toml +++ b/simulation/halsim_gui/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", diff --git a/wpilibc/src/main/python/pyproject.toml b/wpilibc/src/main/python/pyproject.toml index 539838d2c5..44fdc4435e 100644 --- a/wpilibc/src/main/python/pyproject.toml +++ b/wpilibc/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", diff --git a/wpilibc/src/main/python/semiwrap/BooleanEvent.yml b/wpilibc/src/main/python/semiwrap/BooleanEvent.yml index 69dc7fd6f0..a93873dee6 100644 --- a/wpilibc/src/main/python/semiwrap/BooleanEvent.yml +++ b/wpilibc/src/main/python/semiwrap/BooleanEvent.yml @@ -19,3 +19,32 @@ classes: Rising: Falling: Debounce: + inline_code: | + .def("negate", [](BooleanEvent &self) { + return !self; + }, + py::doc("Returns a BooleanEvent that is active when this event is inactive.")) + .def("and_", [](BooleanEvent &self, BooleanEvent &other) { + return self && static_cast>(other); + }, + py::arg("other"), + py::doc("Compose this event with another event") + ) + .def("and_", [](BooleanEvent &self, std::function other) { + return self && other; + }, + py::arg("other"), + py::doc("Compose this event with a callable") + ) + .def("or_", [](BooleanEvent &self, BooleanEvent &other) { + return self || static_cast>(other); + }, + py::arg("other"), + py::doc("Compose this event with another event") + ) + .def("or_", [](BooleanEvent &self, std::function other) { + return self || other; + }, + py::arg("other"), + py::doc("Compose this event with a callable") + ) diff --git a/wpilibc/src/main/python/semiwrap/simulation/SimHooks.yml b/wpilibc/src/main/python/semiwrap/simulation/SimHooks.yml index 95a4ff2f5d..45568e89a4 100644 --- a/wpilibc/src/main/python/semiwrap/simulation/SimHooks.yml +++ b/wpilibc/src/main/python/semiwrap/simulation/SimHooks.yml @@ -1,6 +1,7 @@ functions: SetRuntimeType: WaitForProgramStart: + ignore: true SetProgramStarted: GetProgramStarted: SetProgramState: diff --git a/wpilibc/src/main/python/wpilib/_impl/cli_sim.py b/wpilibc/src/main/python/wpilib/_impl/cli_sim.py index eca00373cf..c42d782a1d 100644 --- a/wpilibc/src/main/python/wpilib/_impl/cli_sim.py +++ b/wpilibc/src/main/python/wpilib/_impl/cli_sim.py @@ -7,7 +7,6 @@ import typing import wpilib - logger = logging.getLogger("robot.sim") diff --git a/wpilibc/src/main/python/wpilib/simulation/__init__.py b/wpilibc/src/main/python/wpilib/simulation/__init__.py index 2d62011546..131d4729d7 100644 --- a/wpilibc/src/main/python/wpilib/simulation/__init__.py +++ b/wpilibc/src/main/python/wpilib/simulation/__init__.py @@ -56,7 +56,6 @@ from ._simulation import ( setRuntimeType, stepTiming, stepTimingAsync, - waitForProgramStart, ) __all__ = [ @@ -114,7 +113,10 @@ __all__ = [ "setRuntimeType", "stepTiming", "stepTimingAsync", - "waitForProgramStart", ] del _init__simulation + +from hal.simulation import waitForProgramStart + +__all__ += ["waitForProgramStart"] diff --git a/wpilibc/src/test/python/test_boolean_event.py b/wpilibc/src/test/python/test_boolean_event.py new file mode 100644 index 0000000000..90592ad33f --- /dev/null +++ b/wpilibc/src/test/python/test_boolean_event.py @@ -0,0 +1,476 @@ +from wpilib import BooleanEvent, EventLoop + + +def test_binary_compositions(): + loop = EventLoop() + and_counter = {"value": 0} + or_counter = {"value": 0} + + def inc_and(): + and_counter["value"] += 1 + + def inc_or(): + or_counter["value"] += 1 + + assert and_counter["value"] == 0 + assert or_counter["value"] == 0 + + BooleanEvent(loop, lambda: True).and_(lambda: False).ifHigh(inc_and) + BooleanEvent(loop, lambda: True).or_(lambda: False).ifHigh(inc_or) + + loop.poll() + + assert and_counter["value"] == 0 + assert or_counter["value"] == 1 + + +def test_binary_compositions_with_edge_decorators(): + loop = EventLoop() + bool1 = {"value": False} + bool2 = {"value": False} + bool3 = {"value": False} + bool4 = {"value": False} + counter = {"value": 0} + + def inc(): + counter["value"] += 1 + + event1 = BooleanEvent(loop, lambda: bool1["value"]).rising() + event2 = BooleanEvent(loop, lambda: bool2["value"]).rising() + event3 = BooleanEvent(loop, lambda: bool3["value"]).rising() + event4 = BooleanEvent(loop, lambda: bool4["value"]).rising() + event1.and_(event2).ifHigh(inc) + event3.or_(event4).ifHigh(inc) + assert counter["value"] == 0 + + bool1["value"] = True + bool2["value"] = True + bool3["value"] = True + bool4["value"] = True + loop.poll() + + assert counter["value"] == 2 + + loop.poll() + + assert counter["value"] == 2 + + bool1["value"] = False + bool2["value"] = False + bool3["value"] = False + bool4["value"] = False + loop.poll() + + assert counter["value"] == 2 + + bool1["value"] = True + loop.poll() + + assert counter["value"] == 2 + + bool2["value"] = True + loop.poll() + + assert counter["value"] == 2 + + bool1["value"] = False + bool2["value"] = False + loop.poll() + + assert counter["value"] == 2 + + bool1["value"] = True + bool2["value"] = True + loop.poll() + + assert counter["value"] == 3 + + bool3["value"] = True + loop.poll() + + assert counter["value"] == 4 + + loop.poll() + + assert counter["value"] == 4 + + bool4["value"] = True + loop.poll() + + assert counter["value"] == 5 + + loop.poll() + + assert counter["value"] == 5 + + +def test_binary_composition_loop_semantics(): + loop1 = EventLoop() + loop2 = EventLoop() + bool1 = {"value": True} + bool2 = {"value": True} + counter1 = {"value": 0} + counter2 = {"value": 0} + + def inc1(): + counter1["value"] += 1 + + def inc2(): + counter2["value"] += 1 + + BooleanEvent(loop1, lambda: bool1["value"]).and_( + BooleanEvent(loop2, lambda: bool2["value"]) + ).ifHigh(inc1) + + BooleanEvent(loop2, lambda: bool2["value"]).and_( + BooleanEvent(loop1, lambda: bool1["value"]) + ).ifHigh(inc2) + + assert counter1["value"] == 0 + assert counter2["value"] == 0 + + loop1.poll() + + assert counter1["value"] == 1 + assert counter2["value"] == 0 + + loop2.poll() + + assert counter1["value"] == 1 + assert counter2["value"] == 1 + + bool2["value"] = False + loop1.poll() + + assert counter1["value"] == 2 + assert counter2["value"] == 1 + + loop2.poll() + + assert counter1["value"] == 2 + assert counter2["value"] == 1 + + loop1.poll() + + assert counter1["value"] == 2 + assert counter2["value"] == 1 + + bool2["value"] = True + loop2.poll() + + assert counter1["value"] == 2 + assert counter2["value"] == 2 + + loop1.poll() + + assert counter1["value"] == 3 + assert counter2["value"] == 2 + + bool1["value"] = False + loop2.poll() + + assert counter1["value"] == 3 + assert counter2["value"] == 3 + + loop1.poll() + + assert counter1["value"] == 3 + assert counter2["value"] == 3 + + loop2.poll() + + assert counter1["value"] == 3 + assert counter2["value"] == 3 + + +def test_poll_ordering(): + loop = EventLoop() + bool1 = {"value": True} + bool2 = {"value": True} + enable_assert = {"value": False} + counter = {"value": 0} + + def action1(): + if enable_assert["value"]: + counter["value"] += 1 + assert counter["value"] % 3 == 1 + return bool1["value"] + + def action2(): + if enable_assert["value"]: + counter["value"] += 1 + assert counter["value"] % 3 == 2 + return bool2["value"] + + def action3(): + if enable_assert["value"]: + counter["value"] += 1 + assert counter["value"] % 3 == 0 + + BooleanEvent(loop, action1).and_(BooleanEvent(loop, action2)).ifHigh(action3) + enable_assert["value"] = True + loop.poll() + loop.poll() + loop.poll() + loop.poll() + + +def test_edge_decorators(): + loop = EventLoop() + flag = {"value": False} + counter = {"value": 0} + + def inc(): + counter["value"] += 1 + + def dec(): + counter["value"] -= 1 + + BooleanEvent(loop, lambda: flag["value"]).falling().ifHigh(dec) + BooleanEvent(loop, lambda: flag["value"]).rising().ifHigh(inc) + + assert counter["value"] == 0 + + flag["value"] = False + loop.poll() + + assert counter["value"] == 0 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 1 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 1 + + flag["value"] = False + loop.poll() + + assert counter["value"] == 0 + + +def test_edge_reuse(): + loop = EventLoop() + flag = {"value": False} + counter = {"value": 0} + + def inc(): + counter["value"] += 1 + + event = BooleanEvent(loop, lambda: flag["value"]).rising() + event.ifHigh(inc) + event.ifHigh(inc) + + assert counter["value"] == 0 + + loop.poll() + + assert counter["value"] == 0 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 2 + + loop.poll() + + assert counter["value"] == 2 + + flag["value"] = False + loop.poll() + + assert counter["value"] == 2 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 4 + + +def test_edge_reconstruct(): + loop = EventLoop() + flag = {"value": False} + counter = {"value": 0} + + def inc(): + counter["value"] += 1 + + event = BooleanEvent(loop, lambda: flag["value"]) + event.rising().ifHigh(inc) + event.rising().ifHigh(inc) + + assert counter["value"] == 0 + + loop.poll() + + assert counter["value"] == 0 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 2 + + loop.poll() + + assert counter["value"] == 2 + + flag["value"] = False + loop.poll() + + assert counter["value"] == 2 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 4 + + +def test_mid_loop_boolean_change(): + loop = EventLoop() + flag = {"value": False} + counter = {"value": 0} + + def inc(): + counter["value"] += 1 + + event = BooleanEvent(loop, lambda: flag["value"]).rising() + + def first_action(): + flag["value"] = False + counter["value"] += 1 + + event.ifHigh(first_action) + event.ifHigh(inc) + + assert counter["value"] == 0 + + loop.poll() + + assert counter["value"] == 0 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 2 + + loop.poll() + + assert counter["value"] == 2 + + flag["value"] = False + loop.poll() + + assert counter["value"] == 2 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 4 + + +def test_mid_loop_boolean_change_with_composed_events(): + loop = EventLoop() + bool1 = {"value": False} + bool2 = {"value": False} + bool3 = {"value": False} + bool4 = {"value": False} + counter = {"value": 0} + + def inc(): + counter["value"] += 1 + + event1 = BooleanEvent(loop, lambda: bool1["value"]) + event2 = BooleanEvent(loop, lambda: bool2["value"]) + event3 = BooleanEvent(loop, lambda: bool3["value"]) + event4 = BooleanEvent(loop, lambda: bool4["value"]) + + def action1(): + bool2["value"] = False + bool3["value"] = False + counter["value"] += 1 + + event1.ifHigh(action1) + + def action2(): + bool1["value"] = False + counter["value"] += 1 + + event3.or_(event4).ifHigh(action2) + + def action3(): + bool4["value"] = False + counter["value"] += 1 + + event1.and_(event2).ifHigh(action3) + + assert counter["value"] == 0 + + bool1["value"] = True + bool2["value"] = True + bool3["value"] = True + bool4["value"] = True + loop.poll() + + assert counter["value"] == 3 + + loop.poll() + + assert counter["value"] == 3 + + bool1["value"] = True + bool2["value"] = True + loop.poll() + + assert counter["value"] == 5 + + bool1["value"] = False + loop.poll() + + assert counter["value"] == 5 + + bool1["value"] = True + bool3["value"] = True + loop.poll() + + assert counter["value"] == 7 + + bool1["value"] = False + bool4["value"] = True + loop.poll() + + assert counter["value"] == 8 + + +def test_negation(): + loop = EventLoop() + flag = {"value": False} + counter = {"value": 0} + + def inc(): + counter["value"] += 1 + + BooleanEvent(loop, lambda: flag["value"]).negate().ifHigh(inc) + + assert counter["value"] == 0 + + loop.poll() + + assert counter["value"] == 1 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 1 + + flag["value"] = False + loop.poll() + + assert counter["value"] == 2 + + flag["value"] = True + loop.poll() + + assert counter["value"] == 2 diff --git a/wpilibc/src/test/python/test_opmode_robot.py b/wpilibc/src/test/python/test_opmode_robot.py index abc04214d2..956f025415 100644 --- a/wpilibc/src/test/python/test_opmode_robot.py +++ b/wpilibc/src/test/python/test_opmode_robot.py @@ -135,6 +135,7 @@ def test_remove_op_mode(): assert options[0].name == "OneArgOpMode" +# @pytest.mark.xfail(reason="wpilib bug") def test_none_periodic(): class MyMockRobot(MockRobot): def __init__(self): diff --git a/wpilibc/src/test/python/test_pytest_plugins.py b/wpilibc/src/test/python/test_pytest_plugins.py index d485c29cd8..9c6d99355a 100644 --- a/wpilibc/src/test/python/test_pytest_plugins.py +++ b/wpilibc/src/test/python/test_pytest_plugins.py @@ -5,8 +5,7 @@ import pytest def _make_robot_module(pytester): - pytester.makepyfile( - robot_module=""" + pytester.makepyfile(robot_module=""" import wpilib @@ -51,13 +50,11 @@ class IterativeStateRobot(wpilib.TimedRobot): def teleopPeriodic(self): self.did_teleop_periodic = True -""" - ) +""") def _configure_robot_testing_plugin(pytester, robot_class="DummyRobot"): - pytester.makeconftest( - f""" + pytester.makeconftest(f""" import pathlib from wpilib.testing.pytest_plugin import RobotTestingPlugin @@ -68,13 +65,11 @@ from robot_module import {robot_class} def pytest_configure(config): robot_file = pathlib.Path(__file__).resolve() config.pluginmanager.register(RobotTestingPlugin({robot_class}, robot_file, False)) -""" - ) +""") def _configure_isolated_plugin(pytester, parallelism=1, robot_class="DummyRobot"): - pytester.makeconftest( - f""" + pytester.makeconftest(f""" import pathlib from wpilib.testing.pytest_isolated_tests_plugin import IsolatedTestsPlugin @@ -88,19 +83,16 @@ def pytest_configure(config): config.pluginmanager.register( IsolatedTestsPlugin({robot_class}, robot_file, False, False, {parallelism}) ) -""" - ) +""") def test_robot_testing_plugin_success(pytester): _make_robot_module(pytester) _configure_robot_testing_plugin(pytester) - pytester.makepyfile( - test_success=""" + pytester.makepyfile(test_success=""" def test_robot_fixture(robot): assert robot.did_init -""" - ) +""") result = pytester.runpytest("-vv") @@ -110,13 +102,11 @@ def test_robot_fixture(robot): def test_robot_testing_plugin_failure_shows_output(pytester): _make_robot_module(pytester) _configure_robot_testing_plugin(pytester) - pytester.makepyfile( - test_failure=""" + pytester.makepyfile(test_failure=""" def test_robot_failure(robot): print("checked failure output") assert False -""" - ) +""") result = pytester.runpytest("-vv") @@ -132,8 +122,7 @@ def test_robot_failure(robot): def test_isolated_plugin_process_and_output(pytester): _make_robot_module(pytester) _configure_isolated_plugin(pytester) - pytester.makepyfile( - test_isolated=""" + pytester.makepyfile(test_isolated=""" import os @@ -155,8 +144,7 @@ def test_robot_pid_two(robot): def test_robot_failure_output(robot): print("isolated failure output") assert False -""" - ) +""") result = pytester.runpytest_subprocess("-vv") @@ -181,8 +169,7 @@ def test_robot_failure_output(robot): def test_isolated_plugin_no_duplicate_verbose_output(pytester): _make_robot_module(pytester) _configure_isolated_plugin(pytester) - pytester.makepyfile( - test_isolated=""" + pytester.makepyfile(test_isolated=""" def test_non_robot(): assert True @@ -193,8 +180,7 @@ def test_robot_one(robot): def test_robot_two(robot): assert robot is not None -""" - ) +""") result = pytester.runpytest_subprocess("-v") @@ -216,16 +202,14 @@ def test_robot_two(robot): def test_isolated_plugin_reports_signal_exit(pytester): _make_robot_module(pytester) _configure_isolated_plugin(pytester) - pytester.makepyfile( - test_isolated=""" + pytester.makepyfile(test_isolated=""" import os import signal def test_robot_signal_exit(robot): os.kill(os.getpid(), signal.SIGTERM) -""" - ) +""") result = pytester.runpytest_subprocess("-vv") @@ -241,8 +225,7 @@ def test_robot_signal_exit(robot): def test_isolated_plugin_shows_file_in_non_verbose_output(pytester): _make_robot_module(pytester) _configure_isolated_plugin(pytester) - pytester.makepyfile( - test_isolated=""" + pytester.makepyfile(test_isolated=""" def test_non_robot(): assert True @@ -253,8 +236,7 @@ def test_robot_one(robot): def test_robot_two(robot): assert robot is not None -""" - ) +""") result = pytester.runpytest_subprocess() @@ -267,16 +249,14 @@ def test_robot_two(robot): def test_isolated_plugin_maxfail_stops_early(pytester): _make_robot_module(pytester) _configure_isolated_plugin(pytester) - pytester.makepyfile( - test_isolated=""" + pytester.makepyfile(test_isolated=""" def test_robot_first(robot): assert False def test_robot_second(robot): assert False -""" - ) +""") result = pytester.runpytest_subprocess("-v", "-x") diff --git a/wpimath/robotpy_pybind_build_info.bzl b/wpimath/robotpy_pybind_build_info.bzl index f370dfdc89..1f61fa8ebc 100644 --- a/wpimath/robotpy_pybind_build_info.bzl +++ b/wpimath/robotpy_pybind_build_info.bzl @@ -948,6 +948,22 @@ def wpimath_extension(srcs = [], header_to_dat_deps = [], extra_hdrs = [], inclu ("wpi::math::Models", "wpi__math__Models.hpp"), ], ), + struct( + class_name = "NumericalIntegration", + yml_file = "semiwrap/NumericalIntegration.yml", + header_root = "$(execpath :robotpy-native-wpimath.copy_headers)", + header_file = "$(execpath :robotpy-native-wpimath.copy_headers)/wpi/math/system/NumericalIntegration.hpp", + tmpl_class_names = [], + trampolines = [], + ), + struct( + class_name = "NumericalJacobian", + yml_file = "semiwrap/NumericalJacobian.yml", + header_root = "$(execpath :robotpy-native-wpimath.copy_headers)", + header_file = "$(execpath :robotpy-native-wpimath.copy_headers)/wpi/math/system/NumericalJacobian.hpp", + tmpl_class_names = [], + trampolines = [], + ), struct( class_name = "ExponentialProfile", yml_file = "semiwrap/ExponentialProfile.yml", diff --git a/wpimath/src/main/python/pyproject.toml b/wpimath/src/main/python/pyproject.toml index b905cb09b0..5913a208b3 100644 --- a/wpimath/src/main/python/pyproject.toml +++ b/wpimath/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", @@ -69,8 +69,6 @@ scan_headers_ignore = [ "wpi/math/random/Normal.hpp", "wpi/math/system/Discretization.hpp", - "wpi/math/system/NumericalIntegration.hpp", - "wpi/math/system/NumericalJacobian.hpp", "wpi/math/proto/*", "wpi/math/*/proto/*", @@ -1566,8 +1564,8 @@ LinearSystem = "wpi/math/system/LinearSystem.hpp" LinearSystemLoop = "wpi/math/system/LinearSystemLoop.hpp" # LinearSystemUtil = "wpi/math/system/LinearSystemUtil.hpp" Models = "wpi/math/system/Models.hpp" -# NumericalIntegration = "wpi/math/system/NumericalIntegration.hpp" -# NumericalJacobian = "wpi/math/system/NumericalJacobian.hpp" +NumericalIntegration = "wpi/math/system/NumericalIntegration.hpp" +NumericalJacobian = "wpi/math/system/NumericalJacobian.hpp" # wpi/math/trajectory ExponentialProfile = "wpi/math/trajectory/ExponentialProfile.hpp" diff --git a/wpimath/src/main/python/semiwrap/LinearSystem.yml b/wpimath/src/main/python/semiwrap/LinearSystem.yml index 242381ba4e..39c033445b 100644 --- a/wpimath/src/main/python/semiwrap/LinearSystem.yml +++ b/wpimath/src/main/python/semiwrap/LinearSystem.yml @@ -25,9 +25,31 @@ classes: CalculateX: CalculateY: Slice: - # TODO? ignore: true + template_inline_code: | + cls_LinearSystem.def("slice", [](wpi::math::LinearSystem &self, int idx0) { + return self.Slice(idx0); + }, py::arg("outputIndex"), + py::doc("Returns the LinearSystem with the outputs listed in outputIndices.") + ); + + if constexpr (Outputs > 1) { + cls_LinearSystem.def("slice", [](wpi::math::LinearSystem &self, int idx0, int idx1) { + return self.Slice(idx0, idx1); + }, py::arg("outputIndex0"), py::arg("outputIndex1"), + py::doc("Returns the LinearSystem with the outputs listed in outputIndices.") + ); + } + if constexpr (Outputs > 2) { + cls_LinearSystem.def("slice", [](wpi::math::LinearSystem &self, int idx0, int idx1, int idx2) { + return self.Slice(idx0, idx1, idx2); + }, py::arg("outputIndex0"), py::arg("outputIndex1"), py::arg("outputIndex2"), + py::doc("Returns the LinearSystem with the outputs listed in outputIndices.") + ); + } + + templates: LinearSystem_1_1_1: qualname: wpi::math::LinearSystem diff --git a/wpimath/src/main/python/semiwrap/NumericalIntegration.yml b/wpimath/src/main/python/semiwrap/NumericalIntegration.yml new file mode 100644 index 0000000000..d607f204c8 --- /dev/null +++ b/wpimath/src/main/python/semiwrap/NumericalIntegration.yml @@ -0,0 +1,25 @@ +extra_includes: +- frc_eigen.h +- wpi/math/linalg/EigenCore.hpp +- pybind11/functional.h + +functions: + RK4: + overloads: + F&&, T, wpi::units::second_t: + template_impls: + - [std::function, Eigen::MatrixXd] + F&&, T, U, wpi::units::second_t: + template_impls: + - ["std::function", Eigen::MatrixXd, Eigen::MatrixXd] + F&&, wpi::units::second_t, T, wpi::units::second_t: + template_impls: + - ["std::function", Eigen::MatrixXd] + RKDP: + overloads: + F&&, T, U, wpi::units::second_t, double: + template_impls: + - ["std::function", Eigen::MatrixXd, Eigen::MatrixXd] + F&&, wpi::units::second_t, T, wpi::units::second_t, double: + template_impls: + - ["std::function", Eigen::MatrixXd] diff --git a/wpimath/src/main/python/semiwrap/NumericalJacobian.yml b/wpimath/src/main/python/semiwrap/NumericalJacobian.yml new file mode 100644 index 0000000000..a214282e65 --- /dev/null +++ b/wpimath/src/main/python/semiwrap/NumericalJacobian.yml @@ -0,0 +1,52 @@ +extra_includes: +- frc_eigen.h +- wpi/math/linalg/EigenCore.hpp +- pybind11/functional.h +- pybind11/typing.h + +functions: + NumericalJacobian: + overloads: + F&&, const Vectord&: + ignore: true + F&&, const Eigen::VectorXd&: + template_impls: + - [std::function] + NumericalJacobianX: + overloads: + F&&, const Vectord&, const Vectord&, Args&&...: + ignore: true + F&&, const Eigen::VectorXd&, const Eigen::VectorXd&, Args&&...: + # template_impls: + # - ["std::function", py::args, Eigen::MatrixXd] + param_override: + args: + ignore: true + no_release_gil: true + cpp_code: | + [](py::typing::Callable fn, + const Eigen::VectorXd& x, const Eigen::VectorXd& u, py::args args) { + return wpi::math::NumericalJacobianX([=](const Eigen::VectorXd &ix, const Eigen::VectorXd &iu) { + py::object r = fn(ix, iu, *args); + return r.cast(); + }, x, u); + } + NumericalJacobianU: + overloads: + F&&, const Vectord&, const Vectord&, Args&&...: + ignore: true + F&&, const Eigen::VectorXd&, const Eigen::VectorXd&, Args&&...: + # template_impls: + # - ["std::function", py::args, Eigen::MatrixXd] + param_override: + args: + ignore: true + no_release_gil: true + cpp_code: |- + [](py::typing::Callable fn, + const Eigen::VectorXd& x, const Eigen::VectorXd& u, py::args args) { + return wpi::math::NumericalJacobianU([=](const Eigen::VectorXd &ix, const Eigen::VectorXd &iu) { + py::object r = fn(ix, iu, *args); + return r.cast(); + }, x, u); + } diff --git a/wpimath/src/main/python/tools/create_units.py b/wpimath/src/main/python/tools/create_units.py index 1f902719db..8ab8771ad4 100755 --- a/wpimath/src/main/python/tools/create_units.py +++ b/wpimath/src/main/python/tools/create_units.py @@ -51,9 +51,7 @@ for f in sorted(pathlib.Path(sys.argv[1]).glob("*.h")): ofp.write("\nnamespace pybind11 { namespace detail {\n") for single, double in names: - ofp.write( - inspect.cleandoc( - f""" + ofp.write(inspect.cleandoc(f""" template <> struct handle_type_name {{ static constexpr auto name = _("{double}"); @@ -63,9 +61,7 @@ for f in sorted(pathlib.Path(sys.argv[1]).glob("*.h")): static constexpr auto name = _("{double}"); }}; - """ - ) - ) + """)) ofp.write("\n\n") ofp.write("\n}\n}\n\n") diff --git a/wpimath/src/main/python/wpimath/__init__.py b/wpimath/src/main/python/wpimath/__init__.py index 3e3d27b222..319289bfbf 100644 --- a/wpimath/src/main/python/wpimath/__init__.py +++ b/wpimath/src/main/python/wpimath/__init__.py @@ -101,6 +101,8 @@ from ._wpimath import ( ProfiledPIDControllerRadians, Quaternion, QuinticHermiteSpline, + RK4, + RKDP, Rectangle2d, RectangularRegionConstraint, Rotation2d, @@ -184,6 +186,9 @@ from ._wpimath import ( angleModulus, applyDeadband, inputModulus, + numericalJacobian, + numericalJacobianU, + numericalJacobianX, objectToRobotPose, slewRateLimit, ) @@ -288,6 +293,8 @@ __all__ = [ "ProfiledPIDControllerRadians", "Quaternion", "QuinticHermiteSpline", + "RK4", + "RKDP", "Rectangle2d", "RectangularRegionConstraint", "Rotation2d", @@ -371,6 +378,9 @@ __all__ = [ "angleModulus", "applyDeadband", "inputModulus", + "numericalJacobian", + "numericalJacobianU", + "numericalJacobianX", "objectToRobotPose", "slewRateLimit", ] diff --git a/wpimath/src/test/python/geometry/test_rotation3d.py b/wpimath/src/test/python/geometry/test_rotation3d.py index 53641f5097..c0da59e1f0 100644 --- a/wpimath/src/test/python/geometry/test_rotation3d.py +++ b/wpimath/src/test/python/geometry/test_rotation3d.py @@ -73,7 +73,13 @@ def test_init_rotation_matrix(): assert expected2 == rot2 # Matrix that isn't orthogonal - R3 = np.array([[1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 0.0]]) + R3 = np.array( + [ + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + ] + ) with pytest.raises(ValueError): Rotation3d(R3) diff --git a/wpimath/src/test/python/test_system.py b/wpimath/src/test/python/test_system.py new file mode 100644 index 0000000000..ee70e4fd54 --- /dev/null +++ b/wpimath/src/test/python/test_system.py @@ -0,0 +1,202 @@ +import math + +import wpimath + +import pytest +import numpy as np + + +def test_rk4_exponential(): + """Test that integrating dx/dt = eˣ works""" + y0 = np.array([[0.0]]) + + y1 = wpimath.RK4(lambda x: np.array([[math.exp(x[0, 0])]]), y0, 0.1) + + assert math.isclose(y1[0, 0], math.exp(0.1) - math.exp(0.0), abs_tol=1e-3) + + +def test_rk4_exponential_with_u(): + """Test that integrating dx/dt = eˣ works when we provide a u""" + y0 = np.array([[0.0]]) + + y1 = wpimath.RK4( + lambda x, u: np.array([[math.exp(u[0, 0] * x[0, 0])]]), + y0, + np.array([[1.0]]), + 0.1, + ) + + assert math.isclose(y1[0, 0], math.exp(0.1) - math.exp(0.0), abs_tol=1e-3) + + +def test_rk4_time_varying(): + """ + Tests RK4 with a time varying solution. From + http://www2.hawaii.edu/~jmcfatri/math407/RungeKuttaTest.html: + + dx/dt = x (2 / (eᵗ + 1) - 1) + + The true (analytical) solution is: + + x(t) = 12eᵗ/(eᵗ + 1)² + """ + y0 = np.array([[12.0 * math.exp(5.0) / math.pow(math.exp(5.0) + 1.0, 2.0)]]) + + y1 = wpimath.RK4( + lambda t, x: np.array([[x[0, 0] * (2.0 / (math.exp(t) + 1.0) - 1.0)]]), + 5.0, + y0, + 1.0, + ) + + expected = 12.0 * math.exp(6.0) / math.pow(math.exp(6.0) + 1.0, 2.0) + assert math.isclose(y1[0, 0], expected, abs_tol=1e-3) + + +def test_rkdp_zero(): + """Tests that integrating dx/dt = 0 works with RKDP""" + y1 = wpimath.RKDP( + lambda x, u: np.zeros((1, 1)), + np.array([[0.0]]), + np.array([[0.0]]), + 0.1, + ) + + assert math.isclose(y1[0, 0], 0.0, abs_tol=1e-3) + + +def test_rkdp_exponential(): + """Tests that integrating dx/dt = eˣ works with RKDP""" + y0 = np.array([[0.0]]) + + y1 = wpimath.RKDP( + lambda x, u: np.array([[math.exp(x[0, 0])]]), + y0, + np.array([[0.0]]), + 0.1, + ) + + assert math.isclose(y1[0, 0], math.exp(0.1) - math.exp(0.0), abs_tol=1e-3) + + +def test_rkdp_time_varying(): + """ + Tests RKDP with a time varying solution. From + http://www2.hawaii.edu/~jmcfatri/math407/RungeKuttaTest.html: + + dx/dt = x(2/(eᵗ + 1) - 1) + + The true (analytical) solution is: + + x(t) = 12eᵗ/(eᵗ + 1)² + """ + y0 = np.array([[12.0 * math.exp(5.0) / math.pow(math.exp(5.0) + 1.0, 2.0)]]) + + y1 = wpimath.RKDP( + lambda t, x: np.array([[x[0, 0] * (2.0 / (math.exp(t) + 1.0) - 1.0)]]), + 5.0, + y0, + 1.0, + 1e-12, + ) + + expected = 12.0 * math.exp(6.0) / math.pow(math.exp(6.0) + 1.0, 2.0) + assert math.isclose(y1[0, 0], expected, abs_tol=1e-3) + + +def test_numerical_jacobian(): + """Test that we can recover A from ax_fn() pretty accurately""" + a = np.array( + [ + [1.0, 2.0, 4.0, 1.0], + [5.0, 2.0, 3.0, 4.0], + [5.0, 1.0, 3.0, 2.0], + [1.0, 1.0, 3.0, 7.0], + ] + ) + + def ax_fn(x): + return a @ x + + new_a = wpimath.numericalJacobian(ax_fn, np.zeros((4, 1))) + np.testing.assert_allclose(new_a, a, rtol=1e-6, atol=1e-5) + + +def test_numerical_jacobian_x_u_square(): + """Test that we can recover B from axbu_fn() pretty accurately""" + a = np.array( + [ + [1.0, 2.0, 4.0, 1.0], + [5.0, 2.0, 3.0, 4.0], + [5.0, 1.0, 3.0, 2.0], + [1.0, 1.0, 3.0, 7.0], + ] + ) + b = np.array([[1.0, 1.0], [2.0, 1.0], [3.0, 2.0], [3.0, 7.0]]) + + def axbu_fn(x, u): + return a @ x + b @ u + + x0 = np.zeros((4, 1)) + u0 = np.zeros((2, 1)) + new_a = wpimath.numericalJacobianX(axbu_fn, x0, u0) + new_b = wpimath.numericalJacobianU(axbu_fn, x0, u0) + np.testing.assert_allclose(new_a, a, rtol=1e-6, atol=1e-5) + np.testing.assert_allclose(new_b, b, rtol=1e-6, atol=1e-5) + + +def test_numerical_jacobian_x_u_rectangular(): + c = np.array( + [ + [1.0, 2.0, 4.0, 1.0], + [5.0, 2.0, 3.0, 4.0], + [5.0, 1.0, 3.0, 2.0], + ] + ) + d = np.array([[1.0, 1.0], [2.0, 1.0], [3.0, 2.0]]) + + def cxdu_fn(x, u): + return c @ x + d @ u + + x0 = np.zeros((4, 1)) + u0 = np.zeros((2, 1)) + new_c = wpimath.numericalJacobianX(cxdu_fn, x0, u0) + new_d = wpimath.numericalJacobianU(cxdu_fn, x0, u0) + np.testing.assert_allclose(new_c, c, rtol=1e-6, atol=1e-5) + np.testing.assert_allclose(new_d, d, rtol=1e-6, atol=1e-5) + + +def test_numerical_jacobian_x_passes_extra_args(): + a = np.array([[2.0, -1.0], [0.5, 3.0]]) + b = np.array([[1.0], [4.0]]) + x0 = np.zeros((2, 1)) + u0 = np.zeros((1, 1)) + + seen = {} + + def axbu_fn(x, u, scale, bias): + seen["args"] = (scale, bias) + return scale * (a @ x) + bias * (b @ u) + + new_a = wpimath.numericalJacobianX(axbu_fn, x0, u0, 2.5, -3.0) + + assert seen["args"] == (2.5, -3.0) + np.testing.assert_allclose(new_a, 2.5 * a, rtol=1e-6, atol=1e-5) + + +def test_numerical_jacobian_u_passes_extra_args(): + a = np.array([[1.0, 0.0], [0.0, -2.0]]) + b = np.array([[1.5], [-0.5]]) + x0 = np.zeros((2, 1)) + u0 = np.zeros((1, 1)) + + seen = {} + + def axbu_fn(x, u, scale, bias): + seen["args"] = (scale, bias) + return scale * (a @ x) + bias * (b @ u) + + new_b = wpimath.numericalJacobianU(axbu_fn, x0, u0, 4.0, 0.25) + + assert seen["args"] == (4.0, 0.25) + np.testing.assert_allclose(new_b, 0.25 * b, rtol=1e-6, atol=1e-5) diff --git a/wpinet/src/main/python/pyproject.toml b/wpinet/src/main/python/pyproject.toml index 1ebeca60ad..c06d5b8bfb 100644 --- a/wpinet/src/main/python/pyproject.toml +++ b/wpinet/src/main/python/pyproject.toml @@ -1,9 +1,9 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatchling", "robotpy-native-wpinet==0.0.0", diff --git a/wpinet/src/main/python/wpinet/__init__.py b/wpinet/src/main/python/wpinet/__init__.py index b3261c177a..0adc59f445 100644 --- a/wpinet/src/main/python/wpinet/__init__.py +++ b/wpinet/src/main/python/wpinet/__init__.py @@ -1,6 +1,5 @@ from . import _init__wpinet - # autogenerated by 'semiwrap create-imports wpinet wpinet._wpinet' from ._wpinet import PortForwarder, WebServer diff --git a/wpiutil/src/main/python/pyproject.toml b/wpiutil/src/main/python/pyproject.toml index e90ce9c031..53f4b7d6ed 100644 --- a/wpiutil/src/main/python/pyproject.toml +++ b/wpiutil/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", diff --git a/wpiutil/src/main/python/wpiutil/wpistruct/dataclass.py b/wpiutil/src/main/python/wpiutil/wpistruct/dataclass.py index 160db3e7e1..a37bf0734b 100644 --- a/wpiutil/src/main/python/wpiutil/wpistruct/dataclass.py +++ b/wpiutil/src/main/python/wpiutil/wpistruct/dataclass.py @@ -176,8 +176,7 @@ def _process_class(cls, struct_name: typing.Optional[str]): ctx["_s"] = s # Construct the serialization functions using the same hack NamedTuple uses - fnsrc = inspect.cleandoc( - f""" + fnsrc = inspect.cleandoc(f""" from wpiutil import wpistruct def _pack(v): @@ -210,8 +209,7 @@ def _process_class(cls, struct_name: typing.Optional[str]): # raise ValueError(f"{err_name}: error unpacking data") from e {forEachNested_stmt} - """ - ) + """) exec(fnsrc, ctx, ctx) diff --git a/xrpVendordep/src/main/python/pyproject.toml b/xrpVendordep/src/main/python/pyproject.toml index 3f200b321a..3a49610691 100644 --- a/xrpVendordep/src/main/python/pyproject.toml +++ b/xrpVendordep/src/main/python/pyproject.toml @@ -1,7 +1,7 @@ [build-system] build-backend = "hatchling.build" requires = [ - "semiwrap~=0.2.1", + "semiwrap~=0.2.6", "hatch-meson~=0.1.0", "hatch-robotpy~=0.2.1", "hatchling", diff --git a/xrpVendordep/src/main/python/xrp/cli.py b/xrpVendordep/src/main/python/xrp/cli.py index 0549380260..99e6acdbbc 100644 --- a/xrpVendordep/src/main/python/xrp/cli.py +++ b/xrpVendordep/src/main/python/xrp/cli.py @@ -6,7 +6,6 @@ import typing import wpilib - if sys.version_info < (3, 10): def entry_points(group):