diff --git a/.bumpversion.cfg b/.bumpversion.cfg index f0245844f0..86534655b4 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 1.0.2 +current_version = 1.1.0 message = Bump version to {new_version} commit = True tag = True @@ -21,6 +21,6 @@ replace = __version__ = "{new_version}" search = Unreleased replace = [{new_version}] {now:%Y-%m-%d} -[bumpversion:glob:src/compas_fab/ghpython/components/**/code.py] +[bumpversion:glob:src/compas_fab/ghpython/components_cpython/**/code.py] search = v{current_version} replace = v{new_version} diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 94f47e8364..0000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,14 +0,0 @@ -version = 1 - -test_patterns = ["tests/**"] - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" - -[[analyzers]] -name = "test-coverage" -enabled = true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 46923068c0..4a7edfe875 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,20 +4,45 @@ on: push: branches: - main - - wip pull_request: + branches: + - main jobs: build: if: "!contains(github.event.pull_request.labels.*.name, 'docs-only')" runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.8', '3.9', '3.10'] + python: ['3.9', '3.13'] steps: - - uses: compas-dev/compas-actions.build@v3 + - uses: compas-dev/compas-actions.build@v5 + env: + CFLAGS: ${{ startsWith(matrix.os, 'macos') && '-fno-define-target-os-macros' || '' }} with: python: ${{ matrix.python }} invoke_lint: true + + build-cpython-components: + runs-on: windows-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e .[dev] + + - name: Create CPython Grasshopper user objects + run: | + invoke build-cpython-ghuser-components + + - uses: actions/upload-artifact@v5 + with: + name: compas_fab_components + path: src/compas_fab/ghpython/components_cpython/ghuser diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 803a09b602..5687389aff 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - LTS-main-1.x tags: - 'v*' pull_request_review: @@ -14,7 +15,11 @@ jobs: if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'pull_request_review' && github.event.review.state == 'approved') runs-on: ubuntu-latest steps: - - uses: compas-dev/compas-actions.docs@v3 + - uses: compas-dev/compas-actions.docs@v5 with: github_token: ${{ secrets.GITHUB_TOKEN }} doc_url: https://compas.dev/compas_fab + # Site is built with MkDocs (via `invoke docs`); `v5` of the action + # added the `generator` switch so it can deploy MkDocs sites in + # addition to the original Sphinx flow. + generator: mkdocs diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 53f609e3bc..1450bb2e08 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -4,34 +4,55 @@ on: push: branches: - main - - wip pull_request: + branches: + - main + - LTS-main-1.x jobs: build: - name: ubuntu-py38-integration + name: ubuntu-py39-integration runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 + - uses: actions/checkout@v6 + - name: Set up Python 3.9 + uses: actions/setup-python@v5 with: - python-version: '3.8' - - name: Set up docker containers + python-version: '3.9' + + - name: Bring up both ROS stacks + # Defaults: ROS 1 rosbridge on 9090, ROS 2 rosbridge on 9091, ROS 2 + # HTTP assets on 9092. See `tests/integration_setup/README.md`. run: | - docker-compose -f "tests/integration_setup/docker-compose.yml" up -d --build + docker compose -f tests/integration_setup/docker-compose.yml up -d --build + docker compose -f tests/integration_setup/docker-compose-ros2.yml up -d --build docker ps -a + - name: Install dependencies + # `pip install -r requirements-dev.txt` alone installs the dev tools + # but NOT the package itself nor its runtime deps. The top-level + # `conftest.py` then crashes on `from twisted.internet import ...` + # because twisted only arrives transitively via `roslibpy` (a runtime + # dep). Installing the package with the `[dev]` extra pulls both. run: | - python -m pip install --upgrade pip - python -m pip install wheel - - name: Install - run: | - python -m pip install --no-cache-dir -r requirements-dev.txt - - name: Run integration tests + python -m pip install --upgrade pip wheel + python -m pip install --no-cache-dir -e ".[dev]" + + - name: Run tests + doctests + # `COMPAS_FAB_RUN_ROS_INTEGRATION_TESTS=1` opts the `ros1_client` / + # `ros2_client` fixtures into actually connecting (otherwise every + # integration test is skipped). `--doctest-modules` picks up + # doctests embedded in `src/compas_fab/` modules. + env: + COMPAS_FAB_RUN_ROS_INTEGRATION_TESTS: "1" + # Opt the whole suite into roslibpy's new asyncio transport + # (websockets-based) instead of the historical twisted reactor. + ROSLIBPY_TRANSPORT: asyncio run: | pytest --doctest-modules - pytest docs + - name: Tear down docker containers + if: always() run: | - docker-compose -f "tests/integration_setup/docker-compose.yml" down + docker compose -f tests/integration_setup/docker-compose.yml down + docker compose -f tests/integration_setup/docker-compose-ros2.yml down diff --git a/.github/workflows/ironpython.yml b/.github/workflows/ironpython.yml deleted file mode 100644 index 2624d7cb8f..0000000000 --- a/.github/workflows/ironpython.yml +++ /dev/null @@ -1,63 +0,0 @@ -name: ironpython - -on: - push: - branches: - - main - - wip - pull_request: - -jobs: - build: - name: windows-ironpython - runs-on: windows-latest - steps: - - uses: actions/checkout@v2 - - name: Install dependencies - run: | - echo "Installing IronPython..." - choco install ironpython --version=2.7.8.1 - - echo "Downloading ironpython-pytest..." - curl -o ironpython-pytest.tar.gz -LJO https://pypi.debian.net/ironpython-pytest/latest - - echo "Downloading COMPAS..." - curl -o compas.tar.gz -LJO https://pypi.debian.net/COMPAS/COMPAS-2.1.0.tar.gz - - echo "Downloading roslibpy..." - curl -o roslibpy.tar.gz -LJO https://pypi.debian.net/roslibpy/latest - - echo "Downloading compas_robots..." - curl -o compas_robots.tar.gz -LJO https://pypi.debian.net/compas_robots/latest - - echo "Setting up IronPython environment..." - ipy -X:Frames -m ensurepip - - echo "Installing ironpython-pytest..." - ipy -X:Frames -m pip install --no-deps ironpython-pytest.tar.gz - - echo "Installing COMPAS..." - ipy -X:Frames -m pip install --no-deps compas.tar.gz - - echo "Installing roslibpy..." - ipy -X:Frames -m pip install --no-deps roslibpy.tar.gz - - echo "Installing compas_robots..." - ipy -X:Frames -m pip install --no-deps compas_robots.tar.gz - - uses: NuGet/setup-nuget@v1.0.5 - - uses: compas-dev/compas-actions.ghpython_components@v5 - with: - source: src/compas_fab/ghpython/components - target: src/compas_fab/ghpython/components/ghuser - - name: Test import - run: | - echo "Testing import of compas_fab..." - ipy -m compas_fab - env: - IRONPYTHONPATH: ./src - - name: Run tests - run: | - echo "Running tests..." - ipy tests/ipy_test_runner.py - env: - IRONPYTHONPATH: ./src diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index a61aab5bdc..7e9ce2b212 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -4,13 +4,14 @@ on: types: [assigned, opened, synchronize, reopened, labeled, unlabeled] branches: - main + - LTS-main-1.x jobs: build: name: Check Actions runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v6 - name: Changelog check uses: Zomzog/changelog-checker@v1.1.0 with: diff --git a/.github/workflows/publish_yak.yml b/.github/workflows/publish_yak.yml index 8c8ded40e3..4058be3c21 100644 --- a/.github/workflows/publish_yak.yml +++ b/.github/workflows/publish_yak.yml @@ -29,41 +29,17 @@ jobs: } - name: Checkout repo - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements-dev.txt + pip install -e .[dev] - name: Create CPython Grasshopper user objects run: | invoke build-cpython-ghuser-components - - name: Create IronPython Grasshopper user objects - run: | - choco install ironpython --version=2.7.8.1 - invoke clean - invoke build-ghuser-components - - - name: Create Rhino7 Yak package - shell: pwsh - run: | - invoke yakerize -m $Env:YAK_TEMPLATE\manifest.yml -l $Env:YAK_TEMPLATE\icon.png -g $Env:USER_OBJECTS -t rh7 - env: - USER_OBJECTS: src\compas_fab\ghpython\components\ghuser - YAK_TEMPLATE: src\compas_fab\ghpython\yak_template - - - name: Publish to Yak server (Rhino 7) - shell: pwsh - run: | - $test_flag = if ($Env:TEST_FLAG) { $Env:TEST_FLAG } else { "" } - $file = Get-ChildItem -Path dist\yak_package\*rh7*.yak -File | Select-Object -ExpandProperty Name - $command = "invoke publish-yak -y dist\yak_package\$file $test_flag".Trim() - Invoke-Expression $command - env: - YAK_TOKEN: ${{ secrets.YAK_DF_TOKEN }} - - name: Create Rhino8 Yak package shell: pwsh run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5aacf3c3e5..e9e24e214d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python: ['3.8', '3.9', '3.10'] + python: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - uses: compas-dev/compas-actions.build@v3 @@ -29,8 +29,8 @@ jobs: pypi_token: ${{ secrets.PYPI_PASSWORD }} github_token: ${{ secrets.GITHUB_TOKEN }} build_ghpython_components: true - gh_source: src/compas_fab/ghpython/components - gh_target: src/compas_fab/ghpython/components/ghuser + gh_source: src/compas_fab/ghpython/components_cpython + gh_target: src/compas_fab/ghpython/components_cpython/ghuser gh_prefix: "COMPAS FAB: " - gh_interpreter: "ironpython" + gh_interpreter: "cpython" release_name_prefix: COMPAS FAB diff --git a/.gitignore b/.gitignore index 71c538a6e8..f53daa5442 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ docs/generated/* temp/** !temp/.gitkeep +.DS_Store # Grasshopper generated objects -src/compas_fab/ghpython/components/ghuser/*.ghuser +src/compas_fab/ghpython/components_cpython/ghuser/*.ghuser diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000000..29b1952b62 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,19 @@ +# Authors + +- Gramazio Kohler Research [@gramaziokohler](https://github.com/gramaziokohler) +- Romana Rust <> [@romanarust](https://github.com/romanarust) +- Gonzalo Casas <> [@gonzalocasas](https://github.com/gonzalocasas) +- Stefana Parascho <> [@stefanaparascho](https://github.com/stefanaparascho) +- David Jenny <> [@DavidJenny](https://github.com/DavidJenny) +- Kathrin Dörfler <> [@kathrindoerfler](https://github.com/kathrindoerfler) +- Matthias Helmreich <> [@mhelmrei](https://github.com/mhelmrei) +- Augusto Gandia <> [@augustogandia](https://github.com/augustogandia) +- Zhao Ma <> [@xarthurx](https://github.com/xarthurx) +- Inés Ariza <> [@inesariza](https://github.com/inesariza) +- Matteo Pacher <> [@matteo-pacher](https://github.com/matteo-pacher) +- Beverly Lytle <> [@beverlylytle](https://github.com/beverlylytle) +- Yijiang Huang <> [@yijiangh](https://github.com/yijiangh) +- Chen Kasirer <> [@chenkasirer](https://github.com/chenkasirer) +- Edvard Bruun <> [@ebruun](https://github.com/ebruun) +- Victor Pok Yin Leung <> [@yck011522](https://github.com/yck011522) +- Begüm Saral <> [@begums](https://github.com/begums) diff --git a/AUTHORS.rst b/AUTHORS.rst deleted file mode 100644 index 38bc55aecf..0000000000 --- a/AUTHORS.rst +++ /dev/null @@ -1,53 +0,0 @@ -******************************************************************************** -Citing -******************************************************************************** - -If you use COMPAS FAB in a project, please use the following citation: - -.. code-block:: none - - @misc{compas-fab, - title={{COMPAS~FAB}: Robotic fabrication package for the COMPAS Framework}, - author={ - Rust, R. and - Casas, G. and - Parascho, S. and - Jenny, D. and - D\"{o}rfler, K. and - Helmreich, M. and - Gandia, A. and - Ma, Z. and - Ariza, I. and - Pacher, M. and - Lytle, B. and - Huang, Y. and - Kasirer, C. and - Bruun, E. and - Leung, P.Y.V. - }, - howpublished={https://github.com/compas-dev/compas\_fab/}, - note={Gramazio Kohler Research, ETH Z\"{u}rich}, - year={2018}, - doi={10.5281/zenodo.3469478}, - url={https://doi.org/10.5281/zenodo.3469478}, - } - -Authors -======= - -* Gramazio Kohler Research `@gramaziokohler `_ -* Romana Rust `@romanarust `_ -* Gonzalo Casas `@gonzalocasas `_ -* Stefana Parascho `@stefanaparascho `_ -* David Jenny `@DavidJenny `_ -* Kathrin Dörfler `@kathrindoerfler `_ -* Matthias Helmreich `@mhelmrei `_ -* Augusto Gandia `@augustogandia `_ -* Zhao Ma `@xarthurx `_ -* Inés Ariza `@inesariza `_ -* Matteo Pacher `@matteo-pacher `_ -* Beverly Lytle `@beverlylytle `_ -* Yijiang Huang `@yijiangh `_ -* Chen Kasirer `@chenkasirer `_ -* Edvard Bruun `@ebruun `_ -* Victor Pok Yin Leung `@yck011522 `_ diff --git a/CHANGELOG.md b/CHANGELOG.md index 63e20b9126..2d9acfc4a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,35 +9,333 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -* Added `compas_fab.robots.Waypoints` class to represent a sequence of targets. It has two child classes: `FrameWaypoints` and `PointAxisWaypoints`. -* Added `compas_fab.robots.Target` class to represent a motion planning target. -* Added also child classes `FrameTarget`, `PointAxisTarget`, `ConfigurationTarget`, `ConstraintSetTarget` -* Unlike previous constraints, `Targets` do not contain `group` as parameter. Instead, group parameter is passed to the planning call. -* Target scaling function is now embeded in the code for Targets. `scaled()` should be called by the user before passing the target to the `plan_motion` function. - -### Changed - - -* Renamed `PybulletClient.get_cached_robot` to `PybulletClient.get_cached_robot_model` to avoid confusion between the `RobotModel` and `Robot` class. -* Renamed `PybulletClient.ensure_cached_robot` to `PybulletClient.ensure_cached_robot_model`. -* Renamed `PybulletClient.ensure_cached_robot_geometry` to `PybulletClient.ensure_cached_robot_model_geometry`. -* Renamed `PybulletClient.cache_robot` to `PybulletClient.cache_robot_model`. -* Backend planners now use multi-inherence instead of `__call__` to include the backend functions. This allows for better generated documentation. -* `Robot.plan_carteisan_motion()` now accepts `Waypoints` as target. Implementation for `FrameWaypoints` is supported with same functionality as before. Simply wrap `Frame` objects using `FrameWaypoints(frames)`. -* Changed `BoundingVolume`, `Constraint`, `JointConstraint`, `OrientationConstraint`, `PositionConstraint` to inherit from `compas.data.Data` class. -* Change the signature of `plan_motion()` to use `target` (`Target` class) instead of `goal_constraints`. Only one target is accepted. Users who wish to compose their own constraint sets can still use `ConstraintSetTarget`. -* Moved `Robot.orientation_constraint_from_frame()` to `OrientationConstraint.from_frame()`, as constraints are no longer intended for users to use directly. -* Moved `Robot.position_constraint_from_frame()` to `PositionConstraint.from_frame()`, as constraints are no longer intended for users to use directly. -* Moved `Robot.constraints_from_frame()` to ros.backend_features and is handled by `convert_target_to_goal_constraints()`. Users who wish to use a frame as target should use a `FrameTarget` instead. -* Changed the behavior of Duration class when accepting both seconds (float) and nanoseconds (int) where the decimal point of seconds and the nanoseconds add up to more than 1 second. +* New backend page [Analytical IK + PyBullet](backends/analytical_pybullet.md) for `AnalyticalPyBulletPlanner`, the hybrid planner that pairs closed-form analytical IK with PyBullet collision checking. Previously this backend was undocumented despite being shipped in `compas_fab.backends`. +* `docs/backends/pybullet.md` now flags that free-space `plan_motion` is not yet implemented for `PyBulletPlanner` (Cartesian motion via `plan_cartesian_motion` works). +* New `ros2-ur10e-demo` docker reference backend (ROS 2 Jazzy + MoveIt 2 + UR10e) with a `ur-sim` service running the official `universalrobots/ursim_e-series` Polyscope simulator, a `zenoh-router` service running `rmw_zenohd` as the RMW transport (replaces DDS, no more multicast discovery issues on Docker Desktop), a `ur-driver` service running the real `ur_robot_driver` against the simulator (with the ros2_control + RTDE update rate lowered from the default 500 Hz to 100 Hz via a baked-in `update_rate.yaml`, to stop the controller manager from spamming "Overrun detected!" warnings under Docker virtualisation), `moveit-demo` running `ur_moveit.launch.py`, rosbridge on `9090`, an HTTP file server on `9091` for serving meshes from `/opt/ros/jazzy/share/`, and a `theasp/novnc` web GUI on `8080` for viewing RViz. +* New `compas_fab.backends.HttpFileServerLoader` that mirrors `RosFileServerLoader`'s interface but fetches meshes over plain HTTP and reads URDF/SRDF from rosbridge topics (the ROS 2 convention) instead of ROS parameters. +* Migrated documentation from Sphinx to MkDocs Material, matching the structure used in `compas_robots`. New `mkdocs.yml` at the repository root; documentation sources are now Markdown (`docs/*.md`) with API pages driven by `mkdocstrings`. Backlinks are disabled. `inventories` includes `compas`, `compas_robots`, and `compas_viewer`. Sphinx config (`docs/conf.py`) and Sphinx-only `docs/requirements.txt` have been removed; `tasks.py` now invokes `compas_invocations2.mkdocs.docs` so `invoke docs` builds the MkDocs site. +* New developer guide section: [Backend architecture](developer/architecture.md) ported from the old `docs/developer/backends.rst`, plus [Grasshopper components](developer/grasshopper.md) ported from `docs/developer/grasshopper.rst`. +* New [Icon system](developer/icons/index.md) page documenting the Grasshopper component icon design system (24×24 artboard, 1.8 px monoline keyline, shared primitive kit — cube=cell, square=body, cone=tool, triad=frame, ring=goal, polyline=motion, brackets=library, chip=backend, teal accent for the semantic subject). Ships the editable SVG source (`icons.js`) and a standalone catalog/spec sheet (`icon_system.html`) showing all glyphs grouped by subcategory with a Grasshopper-style preview. +* Re-rendered every `Cf_*` component `icon.png` from the new design system — consistent stroke weight, shared primitive kit, teal accent for the semantic subject of each icon. Affects every `src/compas_fab/ghpython/components_cpython/*/icon.png`. +* New API pages explicitly exposing `compas_fab.backends.interfaces`, `compas_fab.backends.ros.backend_features` and `compas_fab.backends.pybullet.backend_features` via `mkdocstrings`, replacing the Sphinx `autosummary` tables that previously generated stub `.rst` files per symbol. +* New `Target` and `Waypoints` classes to represent inputs of planning functions. + * New `PointAxisTarget` and `PointAxisWaypoint` classes for processes that have a cylindrical tool (e.g. drilling, milling, 3D printing). +* New `TargetMode` enum to specify how planning functions interpret a target frame. + * New `TargetMode.Tool` mode allow users to specify the target location of the attached tool. + * New `TargetMode.WORKPIECE` mode allow the same for the attached workpiece. + * Forward kinematics functions also support target mode to return Robot, attached Tool or attached Workpiece frame. +* New `RobotCell` class to represent all objects in a a robot cells, namely the RobotModel, ToolModel(s) and RigidBody(s). RobotSemantics is also here. Replaced `Robot` class. + * New `PlannerInterface.set_robot_cell` function for the user to pass the robot cell to the planner. + * New `RigidBody` class support separated Visual and Collision geometries. +* New `RobotCellState` class to represent state of all objects in the robot cell, including robot Configuration, robot base frame (new), ToolState(s) and RigidBodyState(s). + * New `RobotCellState._robot_base_frame` (relative to WCF) can now be changed by user. + * Note that all RigidBody frames and Target frames are still relative to WCF (conceptually the same as before). + * New `ToolState` class includes tool configuration (for kinematic tools) and attachment state (group and attachment frame) of the tool to the robot. + * New `ToolState.attachment_frame` allows users to change the attachment location of a tool relative to the end-effector link. + * New `RigidBodyState` class can model both options of attaching the object to a robot link (e.g. cable dress-pack) and attaching to the tool (which is now referred to as a Workpiece). +* Planning backends are redesigned to have stateless planning functions (e.g. `collision_check`, `inverse_kinematics`, `plan_motion`). + * Stateless planning functions requires `RobotCellState` input, allowing different object states between planning calls. + * It avoids the need for the user to keep track of the state in the planner between planning calls and even sessions. + * It allows cleaner implementation when batch planning multiple motions with different attachment relationships. +* New functions for `PyBulletPlanner`. + * New `PlanCartesianMotion` function for `PyBulletPlanner`. Compatible with `FrameWaypoints` and `PointAxisWaypoints`. + * New `InverseKinematics` support for random restarts. + * New `CollisionCheck` function for `PyBulletPlanner`. +* New support for `ToolModels` with kinematic joints. + * Supported by both `PyBulletPlanner` and `MoveItPlanner`. + * Allows users to model tools with moving parts (e.g. grippers jaws) for more accurate collision checking. +* New `RobotCellLibrary`, `ToolLibrary` and `RigidBodyLibrary` classes to quickly load pre-defined objects for tests, examples and demos. + * New `RobotCellLibrary` contains pre-defined robots (e.g. UR5, UR10e, Panda, ABB IRB120, ABB IRB4600 and RFL) and also cells with tools and rigid bodies. + * New `ToolLibrary` contain examples of static and kinematic tools. + * New script to extract robot packages (URDF, SRDF and meshes) from the ROS docker to create new `RobotCellLibrary` objects. +* New `SceneObject` classes for visualization + * Users should use the `RobotCellObject` class, which can visualize the entire robot cell. + * Users should first call `.update()` with the state class, then call `.draw()` to draw native CAD geometries. + * A temporary `RobotModelObject` class is provided to avoid changing the existing one in `compas_robots` +* Redesigned the mechanism for dealing with non-meter scale input and output. + * Input: User created `RigidBody` can contain meshes that are not in meters by setting the `.native_scale` attribute. + * `RigidBody.visual_meshes_in_meters()` and `RigidBody.collision_meshes_in_meters()` functions can return the meshes in meters. + * Input: User created `ToolModel` must be in meters to align with `compas_robot.RobotModel` that is always in meters. + * Input: User created `Targets` and `Waypoints` can have a `.native_scale` attribute to specify the scale. + * Both classes have `.normalize_to_meters()` and `.normalized_to_meters()` functions to convert the target to meters. + * Output: Forward kinematics functions can return frames at other scales by setting the optional `native_scale` parameter. + * Output: `SceneObject` classes for visualization have a `native_scale` attribute to specify the drawing scale. + * Input / Output: `Configuration` and `Trajectory` objects always uses the Meter-Radian units, in accordance with ROS convention. + * Conversion of joint values to other units for execution should be done by the user based on robot controller requirements. + * All the `native_scale` attribute has the same meaning where `user_object_value * native_scale = meter_object_value`. + * In typical use, all `native_scale` attribute can be set to the same value, regardless of input or output. + * For example, if the user wants to work with millimeters, set all `native_scale` to `'0.001'`. +* Added `compas_fab.robots.RobotCell` class. +* Added `compas_fab.robots.RobotCellLibrary` class. +* Added `compas_fab.backends.BackendTargetNotSupportedError`. +* Added `compas_fab.backends.TargetModeMismatchError`. +* Added `compas_fab.backends.PlanningGroupNotExistsError`. +* Added `compas_fab.backends.CollisionCheckError`. +* Added `compas_fab.backends.MotionPlanningError`. +* Added `compas_fab.backends.MPStartStateInCollisionError`. +* Added `compas_fab.backends.MPTargetInCollisionError`. +* Added `compas_fab.backends.MPInterpolationInCollisionError`. +* Added `compas_fab.backends.MPSearchTimeOutError`. +* Added `compas_fab.backends.MPNoIKSolutionError`. +* Added `compas_fab.backends.MPNoPlanFoundError`. +* Added `compas_fab.backends.MPMaxJumpError`. +* Added `compas_fab.backends.AnalyticalKinematicsClient`. +* Added `compas_fab.backends.AnalyticalKinematicsPlanner`. +* Added `compas_fab.backends.AnalyticalPyBulletPlanner`. +* Added `compas_fab.backends.CollisionCheckError`. +* Added `compas_fab.backends.PlanningGroupNotSupported`. +* Added `message` and `target_pcf` attributes to `InverseKinematicsError`. +* Added `compas_fab.backends.CheckCollision` to backend features. +* Added `compas_fab.backends.SetRobotCell` to backend features. +* Added `compas_fab.backends.SetRobotCellState` to backend features. +* Added `compas_fab.backends.ClientInterface.robot_cell` as read-only property. +* Added `compas_fab.backends.ClientInterface.robot_cell_state` as read-only property. +* Added `compas_fab.backends.ClientInterface.robot_model` as read-only property. +* Added `compas_fab.backends.ClientInterface.robot_semantics` as read-only property. +* Added `compas_fab.backends.PlannerInterface.set_robot_cell()` as a possible mix-in method. +* Added `compas_fab.backends.PlannerInterface.set_robot_cell_state()` as a possible mix-in method. +* Added `compas_fab.backends.PlannerInterface.check_collisions()` as a possible mix-in method. +* Added `compas_fab.backends.PlannerInterface.iter_inverse_kinematics()` as a possible mix-in method. +* Added `compas_fab.backends.kinematics.backend_features.AnalyticalPlanCartesianMotion` for `AnalyticalKinematicsPlanner`. +* Added `compas_fab.backends.kinematics.backend_features.AnalyticalForwardKinematics` for `AnalyticalKinematicsPlanner`. +* Added `compas_fab.backends.kinematics.backend_features.AnalyticalInverseKinematics` for `AnalyticalKinematicsPlanner`. +* Added `compas_fab.backends.kinematics.backend_features.AnalyticalSetRobotCell` for `AnalyticalKinematicsPlanner`. +* Added `compas_fab.backends.kinematics.backend_features.AnalyticalSetRobotCellState` for `AnalyticalKinematicsPlanner`. +* Added `compas_fab.backends.kinematics.backend_features.AnalyticalPybulletInverseKinematics` for `AnalyticalPyBulletPlanner`. +* Added `compas_fab.backends.pybullet.backend_features.PyBulletCheckCollision` for `PyBulletPlanner`. +* Added `compas_fab.backends.pybullet.backend_features.PyBulletPlanCartesianMotion` for `PyBulletPlanner`. +* Added `compas_fab.backends.pybullet.backend_features.PyBulletSetRobotCell` for `PyBulletPlanner`. +* Added `compas_fab.backends.pybullet.backend_features.PyBulletSetRobotCellState` for `PyBulletPlanner`. +* Added `compas_fab.backends.pybullet.backend_features.MoveItSetRobotCell` for `MoveItPlanner`. +* Added `compas_fab.backends.pybullet.backend_features.MoveItSetRobotCellState` for `MoveItPlanner`. +* Added `compas_fab.backends.ForwardKinematics.forward_kinematics_to_link()`. +* Added `compas_fab.robots.ToolLibrary` class. +* Added `compas_fab.robots.ToolLibrary.cone()`. +* Added `compas_fab.robots.ToolLibrary.printing_tool()`. +* Added `compas_fab.robots.ToolLibrary.static_gripper()`. +* Added `compas_fab.robots.ToolLibrary.static_gripper_small()`. +* Added `compas_fab.robots.ToolLibrary.kinematic_gripper()`. (example of a kinematic tool) +* Added `compas_fab.robots.RigidBodyLibrary` class. +* Added `compas_fab.robots.RigidBodyLibrary.target_marker()`. +* Added `compas_fab.robots.RobotCellLibrary` class. +* Added `compas_fab.robots.RobotCellLibrary.rfl()`. +* Added `compas_fab.robots.RobotCellLibrary.ur5()`. +* Added `compas_fab.robots.RobotCellLibrary.ur10e()`. +* Added `compas_fab.robots.RobotCellLibrary.abb_irb4600_40_255()`. +* Added `compas_fab.robots.RobotCellLibrary.abb_irb120_3_58()`. +* Added `compas_fab.robots.RobotCellLibrary.panda()`. +* Added `compas_fab.robots.RobotCellLibrary.ur5_cone_tool()`. +* Added `compas_fab.robots.RobotCellLibrary.abb_irb4600_40_255_gripper_one_beam()`. +* Added `compas_fab.robots.RobotCellLibrary.ur10e_gripper_one_beam()`. +* Added `compas_fab.robots.RobotCellLibrary.abb_irb4600_40_255_printing_tool()`. +* Added `compas_fab.robots.RigidBody` class. +* Added `compas_fab.robots.RigidBodyState` class. +* Added `compas_fab.robots.RobotCell` class. +* Added `compas_fab.robots.RobotCellState` class. +* Added `compas_fab.robots.ToolState` class. +* Added `compas_fab.robots.RobotCell.from_urdf_and_srdf()`. Absolute paths passed for `local_package_mesh_folder` are now used as-is; relative paths still resolve against `compas_fab.DATA` (the existing shorthand for the bundled robot library, e.g. `"robot_library/ur5_robot"`, keeps working). Previously, routing absolute paths through `compas_fab.get()` stripped their leading slash and produced a malformed `/` join that never resolved. +* Added `compas_fab.robots.Target` class. +* Added `compas_fab.robots.ConfigurationTarget` class. +* Added `compas_fab.robots.ConstraintSetTarget` class. +* Added `compas_fab.robots.FrameTarget` class. +* Added `compas_fab.robots.PointAxisTarget` class. +* Added `compas_fab.robots.TargetMode` class. +* Added `compas_fab.robots.Waypoints` class. +* Added `compas_fab.robots.FrameWaypoints` class. +* Added `compas_fab.robots.PointAxisWaypoints` class. +* Added `compas_fab.robots.FrameWaypoints` class. +* Added docker file for UR5-demo with GUI turned on by default. +* Added various example files (detailed list to be added later). +* Added `pragma: no cover` to all type-checking imports for better coverage report accuracy. +* Added support for python '3.11'. +* Added tests for `PyBulletClient` and `PyBulletPlanner` backend features, including Ik and FK agreement tests. +* Redesigned the Grasshopper component library (CPython / Rhino 8) for the new stateless planning model. The legacy IronPython `components/` set was retired; all components now live under `src/compas_fab/ghpython/components_cpython/` and target Rhino 8 CPython exclusively. New categories under "COMPAS FAB": + * *Backends* — `Cf_RosClient` (with a `transport` input — `twisted`/`asyncio`/`cli` — wired through to a new `RosClient(transport=…)` constructor kwarg; falls back to roslibpy's process-wide default when empty), `Cf_MoveItPlanner`, `Cf_AnalyticalKinematicsPlanner`. + * *Robot Cell* — `Cf_LoadRobotCellFromLibrary` (with an auto-created dropdown listing every `RobotCellLibrary` entry), `Cf_LoadRobotCellFromRos`, `Cf_LoadRobotCellFromUrdfSrdf`, `Cf_AddToolToCell` (defaults `tool_id` to `tool.name` when unwired so a tool can be added without naming it twice), `Cf_AddRigidBodyToCell`, `Cf_RigidBodyFromMesh`, `Cf_ToolFromLibrary`, `Cf_RigidBodyFromLibrary`. + * *Cell State* — `Cf_DefaultCellState`, `Cf_SetRobotConfiguration`, `Cf_AttachToolToRobot` (auto-creates a dropdown of every tool in the wired cell when `tool_id` is unwired, refreshed when the cell's tool set changes; emits a remark naming the actual link the tool ends up attached to, e.g. `tool0` for UR groups — `attachment_plane` corrects any per-robot EE-axis convention quirks) and `Cf_AttachRigidBodyToLink` (both auto-default `touch_links` from the cell when unwired — `AttachToolToRobot` picks the group's end-effector link plus its parent when the EE link is geometry-less, e.g. UR `tool0` → `flange`; `AttachRigidBodyToLink` picks the link the body is attached to. A warning surfaces the auto-pick so it isn't silent), `Cf_AddAndAttachTool` (shortcut combining `AddToolToCell` + `DefaultCellState` + `AttachToolToRobot` for the common case of adding one tool — outputs the modified cell plus a fresh state with the tool attached, so a single component replaces the usual three), `Cf_AttachRigidBodyToTool`, `Cf_SetRigidBodyFrame`, `Cf_SetTouchLinks`. + * *Targets* — `Cf_FrameTarget`, `Cf_PointAxisTarget`, `Cf_ConfigurationTarget`, `Cf_FrameWaypoints`, `Cf_PointAxisWaypoints` (tightened `points` input to `list[Rhino.Geometry.Point3d]` so the script harness marshals each item as a real `Point3d` rather than the per-coord float flattening produced by the generic `List[object]` typing), `Cf_RobotConfiguration` (auto-creates one `GH_NumberSlider` per configurable joint with the joint's limits, all wired to a single `joints` list input — drop and wire a `robot_cell` and the slider bank appears; values flow through the standard GH solve path). + * *Planning* — `Cf_ForwardKinematics`, `Cf_InverseKinematics` (accepts a bare `compas.geometry.Frame` or Rhino `Plane` as `target` and auto-wraps it as `FrameTarget(target_mode=ROBOT)`; defaults `start_state` to a zero-config state derived from `planner.robot_cell` when unwired; warns when `planner` or `target` are wired but received None; emits both the `configuration` *and* an updated `cell_state` so `Visualize` / `ForwardKinematics` / the next planner step can wire straight in without a `Cf_SetRobotConfiguration` step in between), `Cf_PlanMotion` and `Cf_PlanCartesianMotion` (with `planes` and `polyline` outputs — Rhino planes at the planning group's EE per trajectory point and a polyline through their origins — via `compas_fab.ghpython.trajectory_to_planes_and_polyline`, computed locally through `RobotCell.forward_kinematics_target_frame` so no round trip per point. Planning failures flag the component red with the backend error message; no separate `error` output), `Cf_DeconstructTrajectory` (expands a `JointTrajectory` into a list of `Configurations` plus, when `start_state` is wired, a list of `RobotCellStates` — pipe an index slider into the second output to scrub through `VisualizeRobotCell`). + * *Display* — `Cf_VisualizeRobotCell` (uses the `RobotCellObject` scene object with native-geometry caching for smooth slider scrubbing). + * *ROS* — `Cf_RosTopicPublish`, `Cf_RosTopicSubscribe` (ported to the new `RosClient`). + * Planner calls take a `RobotCell` + `RobotCellState` + `Target` / `Waypoints` triple. Long-lived clients/planners use `scriptcontext.sticky`; user messages flow through `compas_ghpython.error`/`warning`/`remark`. Loader errors are caught and surfaced via clean error messages rather than letting tracebacks reach the GH canvas. +* Added `compas_fab.ghpython.ensure_value_list`, a reusable helper for components that want to auto-create a Grasshopper `Value List` on an unconnected input (used by `Cf_LoadRobotCellFromLibrary` and intended for follow-on components like `Cf_FrameTarget`'s `target_mode` and solver-name pickers). + +### Changed + +* Changed the default HTTP file server port used by `RosClient.load_robot_cell` (and `HttpFileServerLoader`) from `9091` to `9190`. The previous default collided with the rosbridge port `9091` commonly used when remapping a ROS 2 stack to coexist with a ROS 1 stack (rosbridge `9090` + ROS 2 rosbridge `9091`), causing HTTP 404s when the loader hit the bridge instead of the file server. `9190` stays clear of the 909x cluster while remaining mnemonic. The `docker-compose-ros2.yml` test stack and the `ros2-ur10e-demo` reference stack both default `ROS2_HTTP_PORT` and the container-internal port to `9190` to match. +* Standardized every backend page (`docs/backends/{analytical,analytical_pybullet,pybullet,ros,ros2}.md`) to a common structure: *When to use → Trade-offs → Setup → First example (embedded) → More examples (linked) → API reference*. Each page now embeds one runnable example from `docs/backends/*/files/` via `pymdownx.snippets` and links the remaining ~50 examples to GitHub. +* Rewrote `docs/backends/index.md` as a real decision guide: a "by intent" table keyed by what the user wants to do (rather than backend name), plus a "by capability" matrix and explicit setup-cost summary. Linked from both Home and Installation. +* Split `docs/installation.md` into a focused library-install page (uv/pip/conda + verify) and a new `docs/frontends.md` covering CAD environment setup (Rhino 8, Grasshopper, Blender, COMPAS Viewer, headless). The new front-ends page also documents which CAD environments work with which backends, surfacing the limitation that PyBullet cannot run inside Rhino's interpreter. +* Rebranded the old `tutorial.md` as `concepts.md`: a backend-agnostic walkthrough of the data model (`RobotCell`, `RobotCellState`, `Target`/`Waypoints`, `TargetMode`). No planner calls, the concepts page now reads as "this is the data; backends are how you execute it" and links out to the backends section for the actual planning calls. +* Rewrote `docs/index.md` as a real landing page (was a 1-paragraph blurb): leads with the five-backend table, "I want to..." quick links, and a four-step what's-next list. Updated MkDocs nav to surface the new structure (Home → Installation → CAD front-ends → Concepts → Backends → API → Developer guide). +* Bumped `compas_robots` requirement from `>=0.6,<1` to `>=1,<2`. +* Migrated packaging to `pyproject.toml`. +* Cleaned up `requirements-dev.txt`. +* Python classifiers trimmed to 3.9 (minimum, required for Rhino 8) and 3.13 (latest). +* Unpinned `roslibpy` from `1.8.1` to `>=2.0,<3` in preparation for ROS 2 support. +* Updated `ActionClient` and `Goal` imports from `roslibpy.actionlib` to `roslibpy.ros1.actionlib` following the breaking namespace change in `roslibpy` 2.x. +* Changed CI workflow for IronPython. +* Updated compas requirement to > 2.3 +* Changed `client` parameter in `PlannerInterface(client)` to default to `None`. +* Changed `PlannerInterface.client` to a read-only property. +* Moved `PlannerInterface` from `backends/interfaces/client.py` to `backends/interfaces/planner.py` +* Moved `AnalyticalInverseKinematics` to be a backend feature of `AnalyticalKinematicsPlanner`. +* Moved `AnalyticalPlanCartesianMotion` to be a backend feature of `AnalyticalKinematicsPlanner`. +* Change backend features to use multi-inherence instead of `__call__` to include the backend functions. +* Changed parameters of `compas_fab.backends.SetRobotCell.set_robot_cell()` to accept `RobotCell` and `RobotCellState`. +* Changed parameters of `compas_fab.backends.SetRobotCellState.set_robot_cell_state()` to accept `RobotCellState`. +* Changed parameters of `compas_fab.backends.ForwardKinematics.forward_kinematics()` to accept `RobotCellState`, `TargetMode` and `scale`. +* Changed parameters of `compas_fab.backends.InverseKinematics.inverse_kinematics()` to accept `Target` and `RobotCellState`. +* Changed parameters of `compas_fab.backends.InverseKinematics.iter_inverse_kinematics()` to accept `Target` and `RobotCellState`. +* Changed parameters of `compas_fab.backends.PlanMotion.plan_motion()` to accept `Target` and `RobotCellState`. +* Changed parameters of `compas_fab.backends.PlanCartesianMotion.plan_cartesian_motion()` to accept `Waypoints` and `RobotCellState`. +* Changed `compas_fab.robots.BoundingVolume` to inherit from `compas.data.Data`. +* Changed `compas_fab.robots.Constraint` to inherit from `compas.data.Data`. +* Changed `compas_fab.robots.JointConstraint` to inherit from `compas.data.Data`. +* Changed `compas_fab.robots.OrientationConstraint` to inherit from `compas.data.Data`. +* Changed `compas_fab.robots.PositionConstraint` to inherit from `compas.data.Data`. +* Moved `Robot.orientation_constraint_from_frame()` to `OrientationConstraint.from_frame()`. +* Moved `Robot.position_constraint_from_frame()` to `PositionConstraint.from_frame()`. +* Moved `Robot.constraints_from_frame()` to non-exposed `compas_fab.backends.ros.backend_features.convert_target_to_goal_constraints()`. +* Changed `avoid_collisions` parameter in `plan_motion` to `allow_collisions` parameter. +* Fixed error in `PyBulletForwardKinematics.forward_kinematics` where function would crash if `options` was not passed. +* Fixed error in `PyBulletInverseKinematics._accurate_inverse_kinematics` where threshold was not squared for comparison. +* Changed the behavior of `Duration` class when accepting both seconds (float) and nanoseconds (int) where the decimal point of seconds and the nanoseconds add up to more than 1 second. * Changed GH Component `ConstraintsFromPlane` to `FrameTargetFromPlane`. * Changed GH Component `ConstraintsFromTargetConfiguration` to `ConfigurationTarget`. * Changed `RosClient` constructor to take a type for planner backend instead of a string. This also changes the name of the argument from `planner_backend` to `planner_type`. +* Changed type-hints from comment-style to standard Python 3.x type-hints +* Changed `RosDistro` to be an enum instead of string. +* Changed `TargetMode` to be an enum instead of string. +* Changed default values for number of planning attempts in MoveIt to 5, and the allowed planning time to 1 second. +* Changed cartesian motion planner of MoveIt to raise an exception (including the partial trajectory) if fraction is below 1.0 (ie. the trajectory is not possible). +* Changed `PyBulletClient.connect()` and `PyBulletClient.disconnect()` to take a `verbose` parameter. +* Changed `PyBulletClient._handle_concavity()` to ignore the `redirect_stdout` context manager to avoid the mysterious `WinError 6: The handle is invalid.` error. + +### Fixed + +* `AnalyticalPybulletInverseKinematics._iter_inverse_kinematics_frame_target` was silently dropping every collision-free IK candidate. The `try`/`except CollisionCheckError` block had no `else` branch — when the collision check passed (no exception), the configuration was never yielded. Added the missing `else: yield configuration`. Verified end-to-end with the `03_iter_ik_pybullet.py` example, which now returns 6 valid + 2 colliding configurations (was: 0 valid + all 8 reported as colliding). +* `AnalyticalInverseKinematics.iter_inverse_kinematics` and its `_iter_inverse_kinematics_frame_target` helper had `group: Optional[str]` declared without a default, while the parent `InverseKinematics` interface and all example usages treat `group` as optional. Added `= None` so calls like `planner.iter_inverse_kinematics(target, start_state)` work as documented. +* Cleared stale docstring parameters across `compas_fab.backends` interfaces and backend feature implementations (`robot`, `client`, `solver`, `planner_type`) that no longer matched their signatures, plus a confusingly-indented continuation line in `RigidBodyState.attached_to_link`. `invoke docs` now builds with zero warnings. +* Fixed three analytical/PyBullet examples that crashed at startup due to cell-state desync after objects were added to the cell: + * `docs/backends/analytical_kinematics/files/02_inverse_kinematics with_tools.py`: now refreshes the state from the cell after adding the cone tool. + * `docs/backends/analytical_kinematics/files/03_analytical_pybullet_planner.py`: rewritten to use the pre-configured `RobotCellLibrary.abb_irb4600_40_255_printing_tool()` cell (which has the correct `touch_links` semantics) and derives its target frame via FK from a chosen seed pose. Was previously crashing on `KeyError: 'cone'` and then yielding 0 IK solutions even after that. + * `docs/backends/pybullet/files/04_ik_semi_constrained.py`: now creates the `target_marker` rigid body state explicitly (was crashing on `KeyError: 'target_marker'`). +* `docs/backends/analytical_kinematics/files/03_iter_ik_pybullet.py` (already covered above) now derives its target via FK for clarity. +* `docs/backends/analytical_kinematics/files/04_cartesian_path_analytic_pybullet.py` now has a comment noting that `matplotlib` is an optional dependency that must be installed separately. +* All analytical-backend examples now import solver classes (`UR5Kinematics`, `ABB_IRB4600_40_255Kinematics`) from `compas_fab.backends` (the public top-level path), not the private `compas_fab.backends.kinematics.solvers`. +* `MoveItPlanMotion.plan_motion` and `MoveItPlanCartesianMotion.plan_cartesian_motion` were re-raising every typed planner error (`MPNoPlanFoundError`, `MPStartStateInCollisionError`, …) wrapped in `RosValidationError`, so callers catching `MotionPlanningError` as documented were missing every real failure. Both methods now unwrap `RosValidationError` and re-raise `e.original_exception`, matching `MoveItInverseKinematics.iter_inverse_kinematics`. +* `JointTrajectoryPoint.joint_names` was empty on points produced by the MoveIt and analytical Cartesian planners — the ROS message spec only carries names on the parent `JointTrajectory`, and the analytical path simply forgot. Both backends now attach the trajectory's `joint_names` per point at construction, matching PyBullet. Any consumer that walks points by name (the GH viz helper, `Cf_DeconstructTrajectory`) was silently FK-ing the start state for every point. +* `ToolLibrary.cone()` was not initializing a link on the underlying `ToolModel` when `load_geometry=False`, which left the tool's robot tree malformed and made any downstream `iter_joints()` / `get_configurable_joints()` call raise `AttributeError: 'NoneType' object has no attribute 'joints'`. As a result `RobotCellLibrary.ur5_cone_tool(load_geometry=False)` always crashed. The factory now calls `add_link("cone_link", visual_meshes=[...], collision_meshes=[...])` (mirroring `ToolLibrary.printing_tool()`) so the model is structurally valid in both geometry modes. +* Eliminated intermittent CI failures in `integration.yml` on the `RosClient` doctest under `pytest --doctest-modules` that raised `RosTimeoutError('Failed to connect to ROS')`. Both `>>> with RosClient() as client:` examples in `compas_fab.backends.ros.client` (the `RosClient` class docstring at line 162 and `RosClient.load_robot_cell` at line 257) are now marked `# doctest: +SKIP` — they remain as readable illustrations of the API but no longer run as live integration tests. The actual code path is exercised by `tests/backends/ros/test_ros_client.py` (9 cases against a module-scoped `ros1_client` fixture, never observed to flake). Earlier hypotheses — cold rosbridge startup (reverted in `2ffc37fac`) and a roslibpy 2.x reactor lifecycle issue (validated against [gramaziokohler/roslibpy@lifecycle-fixes](https://github.com/gramaziokohler/roslibpy/tree/lifecycle-fixes) via a temporary `requirements.txt` pin) — both failed to eliminate the flake, falsifying them; the root cause appears to be an interaction between doctest's evaluation context and roslibpy's twisted reactor that only the first `>>> with RosClient()` after the test-fixture suite triggers (the second such doctest always succeeds). ### Removed +* All Sphinx-era documentation sources: every `docs/**/*.rst`, the `docs/conf.py` Sphinx config, the `docs/_static/` and `docs/_images/` asset folders, the `docs/developer/generated/` autosummary output, `docs/spelling_wordlist.txt` and `docs/doc_versions.txt` (the latter superseded by `mike`'s gh-pages versioning). +* `.. autosummary::`, `.. toctree::` and `.. currentmodule::` directives from all module-level `__init__.py` docstrings (replaced with prose + intra-doc Markdown links resolved by `mkdocstrings`). +* Removed `DirectUrActionClient` and associated `direct_ur` messages module since this UR-specific action client was outdated and unused. +* Removed support for Python '3.8'. +* Removed `compas_fab.backends.CollisionError`. +* Removed `compas_fab.backends.AddCollisionMesh` from backend features. +* Removed `compas_fab.backends.AppendCollisionMesh` from backend features. +* Removed `compas_fab.backends.RemoveCollisionMesh` from backend features. +* Removed `compas_fab.backends.AddAttachedCollisionMesh` from backend features. +* Removed `compas_fab.backends.RemoveAttachedCollisionMesh` from backend features. +* Removed `compas_fab.backends.ClientInterface.planner` attribute. +* Removed `compas_fab.backends.ClientInterface.inverse_kinematics()`. +* Removed `compas_fab.backends.ClientInterface.forward_kinematics()`. +* Removed `compas_fab.backends.ClientInterface.plan_cartesian_motion()`. +* Removed `compas_fab.backends.ClientInterface.plan_motion()`. +* Removed `compas_fab.backends.ClientInterface.get_planning_scene()`. +* Removed `compas_fab.backends.ClientInterface.reset_planning_scene()`. +* Removed `compas_fab.backends.ClientInterface.add_collision_mesh()`. +* Removed `compas_fab.backends.ClientInterface.remove_collision_mesh()`. +* Removed `compas_fab.backends.ClientInterface.append_collision_mesh()`. +* Removed `compas_fab.backends.ClientInterface.add_attached_collision_mesh()`. +* Removed `compas_fab.backends.ClientInterface.remove_attached_collision_mesh()`. +* Removed `compas_fab.backends.pybullet.backend_features.PyBulletAddAttachedCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.PyBulletAddCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.PyBulletAppendCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.PyBulletRemoveAttachedCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.PyBulletRemoveCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.MoveItAddAttachedCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.MoveItAddCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.MoveItAppendCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.MoveItRemoveAttachedCollisionMesh`. +* Removed `compas_fab.backends.pybullet.backend_features.MoveItRemoveCollisionMesh`. +* Removed `compas_fab.backends.PyBulletClient.collision_objects` attribute. +* Removed `compas_fab.backends.PyBulletClient.attached_collision_objects` attribute. +* Removed `compas_fab.backends.PyBulletClient.load_ur5()`. +* Removed `compas_fab.backends.PyBulletClient.load_robot()`. +* Removed `compas_fab.robots.AttachedCollisionMesh`. +* Removed `compas_fab.robots.CollisionMesh`. +* Removed `compas_fab.robots.PlanningScene`. +* Removed `compas_fab.robots.Robot`. (Use `RobotCell` instead) +* Removed `compas_fab.robots.RobotLibrary`. (Use `RobotCellLibrary` instead) +* Removed `compas_fab.robots.Tool`. (Use `ToolModel` directly in `RobotCell` instead) +* Removed `compas_fab.robots.Robot.transformed_frames`. (use the one in `RobotModel` instead) +* Removed `compas_fab.robots.Robot.transformed_axes`. (use the one in `RobotModel` instead) +* Removed `compas_fab.robots.Robot.merge_group_with_full_configuration` as it can be covered by `Configuration.merged`. +* Removed `compas_fab.robots.Robot.get_position_by_joint_name` as Configuration class can now be directly accessed by joint name. +* Removed `compas_fab.robots.Robot.get_group_names_from_link_name` as it is too oddly specific. +* Removed `compas_fab.robots.JointTrajectory.attached_collision_meshes` attribute from `` class. +* Removed `inverse_kinematics`, `plan_cartesian_motion`, and `plan_motion` methods from Robot, access them using the planner instead. * Removed `plan_cartesian_motion_deprecated` and `plan_motion_deprecated` methods from `Robot` class * Removed `forward_kinematics_deprecated` and `inverse_kinematics_deprecated` method from `Robot` class +* Removed `compas_fab.sensors` module. +* Removed support for IronPython. + +## [1.1.0] 2025-04-17 + +### Added + +### Changed + +* Made `pybullet` entirely optional. To install `pybullet`, use `pip install compas_fab.[pybullet]` or install `pybullet` manually. + +### Removed + + +## [1.0.5] 2025-04-17 + +### Added + +### Changed + +* Rhino CPython support: change `pybullet` to be optional requirements inside Rhino. + +### Removed + + +## [1.0.4] 2025-04-15 + +### Added + +### Changed + +### Removed + + +## [1.0.3] 2025-04-15 + +### Added + +* Added helper function `message` to `compas_fab.ghpython.components`. +* Added helper function `error` to `compas_fab.ghpython.components`. +* Added helper function `remark` to `compas_fab.ghpython.components`. +* Added helper function `warning` to `compas_fab.ghpython.components`. +* Added GH component definitions compatible with CPython in Rhino8. + +### Changed + +* Updated dev dependency to `compas_invocations2`. +* Fixed `AttributeError` in `inverse_kinematics_spherical_wrist()`. +* Fixed `AttributeError` in VisualizeRobot GH component. + +### Removed + +* Removed `create_id` from `compas_fab.ghpython.components`, using `compas_ghpython.create_id` instead. + ## [1.0.2] 2024-02-22 diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000000..c04c9f9052 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,112 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +title: "COMPAS FAB: Robotic fabrication package for the COMPAS Framework" +abstract: "Robotic fabrication package for the COMPAS Framework." +type: software +authors: + - name: "Gramazio Kohler Research" + affiliation: "ETH Zürich" + - family-names: "Rust" + given-names: "Romana" + affiliation: "ETH Zürich" + - family-names: "Casas" + given-names: "Gonzalo" + affiliation: "ETH Zürich" + - family-names: "Parascho" + given-names: "Stefana" + affiliation: "ETH Zürich" + - family-names: "Jenny" + given-names: "David" + affiliation: "ETH Zürich" + - family-names: "Dörfler" + given-names: "Kathrin" + affiliation: "ETH Zürich" + - family-names: "Helmreich" + given-names: "Matthias" + affiliation: "ETH Zürich" + - family-names: "Gandia" + given-names: "Augusto" + affiliation: "ETH Zürich" + - family-names: "Ma" + given-names: "Zhao" + affiliation: "ETH Zürich" + - family-names: "Ariza" + given-names: "Inés" + affiliation: "ETH Zürich" + - family-names: "Pacher" + given-names: "Matteo" + affiliation: "ETH Zürich" + - family-names: "Lytle" + given-names: "Beverly" + affiliation: "ETH Zürich" + - family-names: "Huang" + given-names: "Yijiang" + affiliation: "MIT" + - family-names: "Kasirer" + given-names: "Chen" + affiliation: "ETH Zürich" + - family-names: "Bruun" + given-names: "Edvard" + affiliation: "Princeton University" + - family-names: "Leung" + given-names: "Victor Pok Yin" + affiliation: "ETH Zürich" + - family-names: "Saral" + given-names: "Begüm" + affiliation: "Technical University of Munich" +url: "https://github.com/compas-dev/compas_fab" +repository-code: "https://github.com/compas-dev/compas_fab" +license: MIT +version: "1.1.0" +date-released: 2018-09-01 +doi: 10.5281/zenodo.3469478 +identifiers: + - type: doi + value: 10.5281/zenodo.3469478 + description: "Concept DOI (always resolves to the latest release)" +keywords: + - COMPAS + - robotic fabrication + - robotics + - motion planning + - architecture +preferred-citation: + type: software + title: "COMPAS FAB: Robotic fabrication package for the COMPAS Framework" + year: 2018 + doi: 10.5281/zenodo.3469478 + url: "https://doi.org/10.5281/zenodo.3469478" + notes: "Gramazio Kohler Research, ETH Zürich" + authors: + - family-names: "Rust" + given-names: "Romana" + - family-names: "Casas" + given-names: "Gonzalo" + - family-names: "Parascho" + given-names: "Stefana" + - family-names: "Jenny" + given-names: "David" + - family-names: "Dörfler" + given-names: "Kathrin" + - family-names: "Helmreich" + given-names: "Matthias" + - family-names: "Gandia" + given-names: "Augusto" + - family-names: "Ma" + given-names: "Zhao" + - family-names: "Ariza" + given-names: "Inés" + - family-names: "Pacher" + given-names: "Matteo" + - family-names: "Lytle" + given-names: "Beverly" + - family-names: "Huang" + given-names: "Yijiang" + - family-names: "Kasirer" + given-names: "Chen" + - family-names: "Bruun" + given-names: "Edvard" + - family-names: "Leung" + given-names: "Victor Pok Yin" + - family-names: "Saral" + given-names: "Begüm" diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index d6a79b9b71..0fac6aee01 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -22,7 +22,7 @@ We love pull requests from everyone! Here's a quick guide to improve the code: :: - docker-compose -f "tests/integration_setup/docker-compose.yml" up -d --build + docker compose -f "tests/integration_setup/docker-compose.yml" up -d --build 5. Make sure all tests pass: @@ -41,7 +41,7 @@ We love pull requests from everyone! Here's a quick guide to improve the code: :: - docker-compose -f "tests/integration_setup/docker-compose.yml"" down + docker compose -f "tests/integration_setup/docker-compose.yml" down 9. Check there are no linter errors: diff --git a/LICENSE b/LICENSE index 5fce846d5e..1639c388c0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 Gramazio Kohler Research +Copyright (c) 2018-2026 Gramazio Kohler Research Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index def84412f5..a9d9320624 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [![DOI](https://zenodo.org/badge/107952684.svg)](https://zenodo.org/badge/latestdoi/107952684) [![Twitter Follow](https://img.shields.io/twitter/follow/compas_dev?style=social)](https://twitter.com/compas_dev) -*Robotic fabrication package for the COMPAS Framework** that facilitates the +*Robotic fabrication package for the COMPAS Framework* that facilitates the planning and execution of robotic fabrication processes. It provides interfaces to existing software libraries and tools available in the field of robotics (e.g. OMPL, ROS) and makes them accessible from within the parametric design @@ -26,7 +26,7 @@ architecture, engineering and digital fabrication. * Planning tools: kinematic solvers, path planning, etc. * Execution tools: feedback loops, robot control, etc. -**COMPAS FAB** runs on Python 3.x and IronPython 2.7. +**COMPAS FAB** runs on Python 3.x. ## Getting Started @@ -42,7 +42,6 @@ It can also be installed using `pip`: > On Windows, you may need to install [Microsoft Visual C++ 14.0](https://www.scivision.co/python-windows-visual-c++-14-required/). - Once the installation is completed, you can verify your setup. Start Python from the command prompt and run the following: @@ -92,4 +91,4 @@ Ready to release a new version of **COMPAS FAB**? Here's how to do it: ## Credits This package is maintained by Gramazio Kohler Research [`@gramaziokohler`](https://github.com/gramaziokohler) -and a long list of [contributors](https://github.com/compas-dev/compas_fab/blob/main/AUTHORS.rst). +and a long list of [contributors](https://github.com/compas-dev/compas_fab/blob/main/AUTHORS.md). diff --git a/conftest.py b/conftest.py index 5e030a2125..1999454a4e 100644 --- a/conftest.py +++ b/conftest.py @@ -1,5 +1,6 @@ def pytest_configure(config): from twisted.internet import selectreactor + selectreactor.install() diff --git a/docs/_images/compas_fab.png b/docs/_images/compas_fab.png deleted file mode 100644 index 436d5ca1e7..0000000000 Binary files a/docs/_images/compas_fab.png and /dev/null differ diff --git a/docs/_logo/compas_logo_white_transparent.png b/docs/_logo/compas_logo_white_transparent.png new file mode 100644 index 0000000000..5dd158c84d Binary files /dev/null and b/docs/_logo/compas_logo_white_transparent.png differ diff --git a/docs/_logo/favicon.ico b/docs/_logo/favicon.ico new file mode 100644 index 0000000000..381bd9414f Binary files /dev/null and b/docs/_logo/favicon.ico differ diff --git a/docs/_static/compas_icon_white.png b/docs/_static/compas_icon_white.png deleted file mode 100644 index 2c905bff94..0000000000 Binary files a/docs/_static/compas_icon_white.png and /dev/null differ diff --git a/docs/examples/07_reachability_map/files/00_robot_halfsphere.jpg b/docs/analysis/reachability_map/files/00_robot_halfsphere.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/00_robot_halfsphere.jpg rename to docs/analysis/reachability_map/files/00_robot_halfsphere.jpg diff --git a/docs/examples/07_reachability_map/files/01_example_1D.py b/docs/analysis/reachability_map/files/01_example_1D.py similarity index 68% rename from docs/examples/07_reachability_map/files/01_example_1D.py rename to docs/analysis/reachability_map/files/01_example_1D.py index 1cfd02f72d..21183092e4 100644 --- a/docs/examples/07_reachability_map/files/01_example_1D.py +++ b/docs/analysis/reachability_map/files/01_example_1D.py @@ -1,14 +1,18 @@ -import os import math -from compas.geometry import Point -from compas.geometry import Plane +import os + from compas.geometry import Frame +from compas.geometry import Plane +from compas.geometry import Point from compas.geometry import Sphere -from compas_fab.backends import AnalyticalInverseKinematics -from compas_fab.backends import PyBulletClient -from compas_fab.robots import ReachabilityMap from compas_fab import DATA +from compas_fab.backends import AnalyticalPyBulletClient +from compas_fab.backends import AnalyticalPyBulletPlanner +from compas_fab.backends import UR5Kinematics +from compas_fab.robots import ReachabilityMap +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode # 1. Define frames on a sphere sphere = Sphere((0.4, 0, 0), 0.15) @@ -32,14 +36,15 @@ def points_on_sphere_generator(sphere): # 3. Create reachability map 1D -with PyBulletClient(connection_type='direct') as client: +with AnalyticalPyBulletClient(connection_type="direct") as client: # load robot and define settings - robot = client.load_ur5(load_geometry=True) - ik = AnalyticalInverseKinematics(client) - client.inverse_kinematics = ik.inverse_kinematics + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + planner = AnalyticalPyBulletPlanner(client, UR5Kinematics()) + planner.set_robot_cell(robot_cell, robot_cell_state) + options = {"solver": "ur5", "check_collision": True, "keep_order": True} # calculate reachability map map = ReachabilityMap() - map.calculate(points_on_sphere_generator(sphere), robot, options) + map.calculate(points_on_sphere_generator(sphere), planner, robot_cell_state, TargetMode.ROBOT, options) # save to json map.to_json(os.path.join(DATA, "reachability", "map1D.json")) diff --git a/docs/examples/07_reachability_map/files/01_robot_map1D.jpg b/docs/analysis/reachability_map/files/01_robot_map1D.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/01_robot_map1D.jpg rename to docs/analysis/reachability_map/files/01_robot_map1D.jpg diff --git a/docs/examples/07_reachability_map/files/02_example_2D_deviation_vectors.py b/docs/analysis/reachability_map/files/02_example_2D_deviation_vectors.py similarity index 74% rename from docs/examples/07_reachability_map/files/02_example_2D_deviation_vectors.py rename to docs/analysis/reachability_map/files/02_example_2D_deviation_vectors.py index f6c7ccdbfa..d94cc12e54 100644 --- a/docs/examples/07_reachability_map/files/02_example_2D_deviation_vectors.py +++ b/docs/analysis/reachability_map/files/02_example_2D_deviation_vectors.py @@ -1,16 +1,19 @@ -import os import math -from compas.geometry import Point -from compas.geometry import Plane +import os + from compas.geometry import Frame +from compas.geometry import Plane +from compas.geometry import Point from compas.geometry import Sphere -from compas_fab.backends import AnalyticalInverseKinematics -from compas_fab.backends import PyBulletClient +from compas_fab import DATA +from compas_fab.backends import AnalyticalPyBulletClient +from compas_fab.backends import AnalyticalPyBulletPlanner +from compas_fab.backends import UR5Kinematics from compas_fab.robots import ReachabilityMap +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode from compas_fab.robots.reachability_map import DeviationVectorsGenerator -from compas_fab import DATA - # 1. Define generators @@ -38,6 +41,7 @@ def deviation_vector_generator(frame): yaxis = frame.zaxis.cross(xaxis) yield Frame(frame.point, xaxis, yaxis) + # 2. Create 2D generator @@ -45,17 +49,18 @@ def generator(): for frame in points_on_sphere_generator(sphere): yield deviation_vector_generator(frame) -# 3. Create reachability map 2D +# 3. Create reachability map 2D -with PyBulletClient(connection_type='direct') as client: +with AnalyticalPyBulletClient(connection_type="direct") as client: # load robot and define settings - robot = client.load_ur5(load_geometry=True) - ik = AnalyticalInverseKinematics(client) - client.inverse_kinematics = ik.inverse_kinematics + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + planner = AnalyticalPyBulletPlanner(client, UR5Kinematics()) + planner.set_robot_cell(robot_cell, robot_cell_state) + options = {"solver": "ur5", "check_collision": True, "keep_order": True} # calculate reachability map map = ReachabilityMap() - map.calculate(generator(), robot, options) + map.calculate(generator(), planner, robot_cell_state, TargetMode.ROBOT, options) # save to json map.to_json(os.path.join(DATA, "reachability", "map2D_deviation.json")) diff --git a/docs/examples/07_reachability_map/files/02_robot_frames_at_ik.jpg b/docs/analysis/reachability_map/files/02_robot_frames_at_ik.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/02_robot_frames_at_ik.jpg rename to docs/analysis/reachability_map/files/02_robot_frames_at_ik.jpg diff --git a/docs/examples/07_reachability_map/files/03_example_2D_sphere_points.py b/docs/analysis/reachability_map/files/03_example_2D_sphere_points.py similarity index 73% rename from docs/examples/07_reachability_map/files/03_example_2D_sphere_points.py rename to docs/analysis/reachability_map/files/03_example_2D_sphere_points.py index 2268a7899f..00cdc485f1 100644 --- a/docs/examples/07_reachability_map/files/03_example_2D_sphere_points.py +++ b/docs/analysis/reachability_map/files/03_example_2D_sphere_points.py @@ -1,15 +1,19 @@ -import os import math -from compas.geometry import Point -from compas.geometry import Vector -from compas.geometry import Plane +import os + from compas.geometry import Frame +from compas.geometry import Plane +from compas.geometry import Point from compas.geometry import Sphere +from compas.geometry import Vector -from compas_fab.backends import AnalyticalInverseKinematics -from compas_fab.backends import PyBulletClient -from compas_fab.robots import ReachabilityMap from compas_fab import DATA +from compas_fab.backends import AnalyticalPyBulletClient +from compas_fab.backends import AnalyticalPyBulletPlanner +from compas_fab.backends import UR5Kinematics +from compas_fab.robots import ReachabilityMap +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode # 1. Define generators @@ -37,6 +41,7 @@ def sphere_generator(): center = sphere.point + Vector(x, 0, z) * 0.05 yield Sphere(center, sphere.radius) + # 2. Create 2D generator @@ -44,17 +49,18 @@ def generator(): for sphere in sphere_generator(): yield points_on_sphere_generator(sphere) -# 3. Create reachability map 2D +# 3. Create reachability map 2D -with PyBulletClient(connection_type='direct') as client: +with AnalyticalPyBulletClient(connection_type="direct") as client: # load robot and define settings - robot = client.load_ur5(load_geometry=True) - ik = AnalyticalInverseKinematics(client) - client.inverse_kinematics = ik.inverse_kinematics + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + planner = AnalyticalPyBulletPlanner(client, UR5Kinematics()) + planner.set_robot_cell(robot_cell, robot_cell_state) + options = {"solver": "ur5", "check_collision": True, "keep_order": True} # calculate reachability map map = ReachabilityMap() - map.calculate(generator(), robot, options) + map.calculate(generator(), planner, robot_cell_state, TargetMode.ROBOT, options) # save to json map.to_json(os.path.join(DATA, "reachability", "map2D_spheres.json")) diff --git a/docs/examples/07_reachability_map/files/03_ortho_vectors.jpg b/docs/analysis/reachability_map/files/03_ortho_vectors.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/03_ortho_vectors.jpg rename to docs/analysis/reachability_map/files/03_ortho_vectors.jpg diff --git a/docs/examples/07_reachability_map/files/04_devi_vectors.jpg b/docs/analysis/reachability_map/files/04_devi_vectors.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/04_devi_vectors.jpg rename to docs/analysis/reachability_map/files/04_devi_vectors.jpg diff --git a/docs/examples/07_reachability_map/files/05_devi_vectors_2.jpg b/docs/analysis/reachability_map/files/05_devi_vectors_2.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/05_devi_vectors_2.jpg rename to docs/analysis/reachability_map/files/05_devi_vectors_2.jpg diff --git a/docs/examples/07_reachability_map/files/06_devi_frames.jpg b/docs/analysis/reachability_map/files/06_devi_frames.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/06_devi_frames.jpg rename to docs/analysis/reachability_map/files/06_devi_frames.jpg diff --git a/docs/examples/07_reachability_map/files/07_example_02.jpg b/docs/analysis/reachability_map/files/07_example_02.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/07_example_02.jpg rename to docs/analysis/reachability_map/files/07_example_02.jpg diff --git a/docs/examples/07_reachability_map/files/08_example_03_spheres.jpg b/docs/analysis/reachability_map/files/08_example_03_spheres.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/08_example_03_spheres.jpg rename to docs/analysis/reachability_map/files/08_example_03_spheres.jpg diff --git a/docs/examples/07_reachability_map/files/09_example_03_frames.jpg b/docs/analysis/reachability_map/files/09_example_03_frames.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/09_example_03_frames.jpg rename to docs/analysis/reachability_map/files/09_example_03_frames.jpg diff --git a/docs/examples/07_reachability_map/files/09_example_03_result.jpg b/docs/analysis/reachability_map/files/09_example_03_result.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/09_example_03_result.jpg rename to docs/analysis/reachability_map/files/09_example_03_result.jpg diff --git a/docs/examples/07_reachability_map/files/10_example_03_result_override.jpg b/docs/analysis/reachability_map/files/10_example_03_result_override.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/10_example_03_result_override.jpg rename to docs/analysis/reachability_map/files/10_example_03_result_override.jpg diff --git a/docs/examples/07_reachability_map/files/adaptive_detailing.png b/docs/analysis/reachability_map/files/adaptive_detailing.png similarity index 100% rename from docs/examples/07_reachability_map/files/adaptive_detailing.png rename to docs/analysis/reachability_map/files/adaptive_detailing.png diff --git a/docs/examples/07_reachability_map/files/workshop_sjsu_1.png b/docs/analysis/reachability_map/files/workshop_sjsu_1.png similarity index 100% rename from docs/examples/07_reachability_map/files/workshop_sjsu_1.png rename to docs/analysis/reachability_map/files/workshop_sjsu_1.png diff --git a/docs/examples/07_reachability_map/files/workshop_sjsu_2.jpg b/docs/analysis/reachability_map/files/workshop_sjsu_2.jpg similarity index 100% rename from docs/examples/07_reachability_map/files/workshop_sjsu_2.jpg rename to docs/analysis/reachability_map/files/workshop_sjsu_2.jpg diff --git a/docs/api.rst b/docs/api.rst deleted file mode 100644 index c02cdb2ece..0000000000 --- a/docs/api.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _reference: - -************* -API Reference -************* - -This reference manual details functions, modules, and objects included in COMPAS FAB, -describing what they are and what they do. - -To learn how to use COMPAS FAB, see the :ref:`examples ` and -how to :ref:`work with backends `. - -.. toctree:: - :maxdepth: 3 - - api/compas_fab diff --git a/docs/api/compas_fab.backends.interfaces.md b/docs/api/compas_fab.backends.interfaces.md new file mode 100644 index 0000000000..a2a57440b7 --- /dev/null +++ b/docs/api/compas_fab.backends.interfaces.md @@ -0,0 +1 @@ +# ::: compas_fab.backends.interfaces diff --git a/docs/api/compas_fab.backends.md b/docs/api/compas_fab.backends.md new file mode 100644 index 0000000000..1b9d45701d --- /dev/null +++ b/docs/api/compas_fab.backends.md @@ -0,0 +1 @@ +# ::: compas_fab.backends diff --git a/docs/api/compas_fab.backends.pybullet.backend_features.md b/docs/api/compas_fab.backends.pybullet.backend_features.md new file mode 100644 index 0000000000..5e85ac67d4 --- /dev/null +++ b/docs/api/compas_fab.backends.pybullet.backend_features.md @@ -0,0 +1 @@ +# ::: compas_fab.backends.pybullet.backend_features diff --git a/docs/api/compas_fab.backends.pybullet.md b/docs/api/compas_fab.backends.pybullet.md new file mode 100644 index 0000000000..9edf8dd2f3 --- /dev/null +++ b/docs/api/compas_fab.backends.pybullet.md @@ -0,0 +1 @@ +# ::: compas_fab.backends.pybullet diff --git a/docs/api/compas_fab.backends.ros.backend_features.md b/docs/api/compas_fab.backends.ros.backend_features.md new file mode 100644 index 0000000000..c8150ae0ee --- /dev/null +++ b/docs/api/compas_fab.backends.ros.backend_features.md @@ -0,0 +1 @@ +# ::: compas_fab.backends.ros.backend_features diff --git a/docs/api/compas_fab.backends.ros.md b/docs/api/compas_fab.backends.ros.md new file mode 100644 index 0000000000..4409d6fffb --- /dev/null +++ b/docs/api/compas_fab.backends.ros.md @@ -0,0 +1 @@ +# ::: compas_fab.backends.ros diff --git a/docs/api/compas_fab.backends.rst b/docs/api/compas_fab.backends.rst deleted file mode 100644 index 63a31c5305..0000000000 --- a/docs/api/compas_fab.backends.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab.backends \ No newline at end of file diff --git a/docs/api/compas_fab.blender.md b/docs/api/compas_fab.blender.md new file mode 100644 index 0000000000..580a81817b --- /dev/null +++ b/docs/api/compas_fab.blender.md @@ -0,0 +1 @@ +# ::: compas_fab.blender diff --git a/docs/api/compas_fab.blender.rst b/docs/api/compas_fab.blender.rst deleted file mode 100644 index 528c1a86fc..0000000000 --- a/docs/api/compas_fab.blender.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab.blender diff --git a/docs/api/compas_fab.ghpython.md b/docs/api/compas_fab.ghpython.md new file mode 100644 index 0000000000..148be660ae --- /dev/null +++ b/docs/api/compas_fab.ghpython.md @@ -0,0 +1 @@ +# ::: compas_fab.ghpython diff --git a/docs/api/compas_fab.ghpython.rst b/docs/api/compas_fab.ghpython.rst deleted file mode 100644 index 1369cf0d6d..0000000000 --- a/docs/api/compas_fab.ghpython.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab.ghpython diff --git a/docs/api/compas_fab.md b/docs/api/compas_fab.md new file mode 100644 index 0000000000..50a152ad17 --- /dev/null +++ b/docs/api/compas_fab.md @@ -0,0 +1 @@ +# ::: compas_fab diff --git a/docs/api/compas_fab.rhino.md b/docs/api/compas_fab.rhino.md new file mode 100644 index 0000000000..d8cdb4fb92 --- /dev/null +++ b/docs/api/compas_fab.rhino.md @@ -0,0 +1 @@ +# ::: compas_fab.rhino diff --git a/docs/api/compas_fab.rhino.rst b/docs/api/compas_fab.rhino.rst deleted file mode 100644 index 7389530f5f..0000000000 --- a/docs/api/compas_fab.rhino.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab.rhino diff --git a/docs/api/compas_fab.robots.md b/docs/api/compas_fab.robots.md new file mode 100644 index 0000000000..846885d516 --- /dev/null +++ b/docs/api/compas_fab.robots.md @@ -0,0 +1 @@ +# ::: compas_fab.robots diff --git a/docs/api/compas_fab.robots.rst b/docs/api/compas_fab.robots.rst deleted file mode 100644 index c994bd56b1..0000000000 --- a/docs/api/compas_fab.robots.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab.robots \ No newline at end of file diff --git a/docs/api/compas_fab.rst b/docs/api/compas_fab.rst deleted file mode 100644 index 6233080c19..0000000000 --- a/docs/api/compas_fab.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab \ No newline at end of file diff --git a/docs/api/compas_fab.sensors.rst b/docs/api/compas_fab.sensors.rst deleted file mode 100644 index 7b4c1ecc4f..0000000000 --- a/docs/api/compas_fab.sensors.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab.sensors \ No newline at end of file diff --git a/docs/api/compas_fab.utilities.md b/docs/api/compas_fab.utilities.md new file mode 100644 index 0000000000..8f31b14301 --- /dev/null +++ b/docs/api/compas_fab.utilities.md @@ -0,0 +1 @@ +# ::: compas_fab.utilities diff --git a/docs/api/compas_fab.utilities.rst b/docs/api/compas_fab.utilities.rst deleted file mode 100644 index f5dbfa0bfe..0000000000 --- a/docs/api/compas_fab.utilities.rst +++ /dev/null @@ -1,2 +0,0 @@ - -.. automodule:: compas_fab.utilities \ No newline at end of file diff --git a/docs/api/core/index.md b/docs/api/core/index.md new file mode 100644 index 0000000000..0f85a8992f --- /dev/null +++ b/docs/api/core/index.md @@ -0,0 +1,16 @@ +# Core + +Platform-independent modules. These work everywhere `compas_fab` can be +imported and form the foundation that the integrations build on top of. + +- **[compas_fab](../compas_fab.md)**: top-level package exports. +- **[compas_fab.robots](../compas_fab.robots.md)**: robot cells, tools, + rigid bodies, targets, waypoints, and the robot library. +- **[compas_fab.backends](../compas_fab.backends.md)**: backend client + and planner classes (analytical, PyBullet, ROS/MoveIt). +- **[compas_fab.backends.ros](../compas_fab.backends.ros.md)**: the ROS + rosbridge client, MoveIt planner, and ROS-specific message helpers. +- **[compas_fab.backends.pybullet](../compas_fab.backends.pybullet.md)**: + the PyBullet client and planner. +- **[compas_fab.utilities](../compas_fab.utilities.md)**: assorted + helpers (file I/O, geometry, math). diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000000..f1129a98ad --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,12 @@ +# API Reference + +The `compas_fab` API is split into two groups: + +- **[Core](core/index.md)**: the platform-independent modules: robots, + robot cells, targets, waypoints, the backend interfaces and built-in + back-end implementations. +- **[Integrations](integrations/index.md)**: CAD-specific bindings for + Blender, Rhino and Grasshopper. These depend on the host environment + and are only imported when running inside it. + +If you are getting started, head to **Core** first. diff --git a/docs/api/integrations/index.md b/docs/api/integrations/index.md new file mode 100644 index 0000000000..45b70c30cf --- /dev/null +++ b/docs/api/integrations/index.md @@ -0,0 +1,12 @@ +# Integrations + +Host-specific bindings for CAD environments. Each module is only importable +when its host is available; pick the one that matches where your code runs. + +- **[compas_fab.blender](../compas_fab.blender.md)**: Blender scene objects. +- **[compas_fab.ghpython](../compas_fab.ghpython.md)**: Grasshopper components. +- **[compas_fab.rhino](../compas_fab.rhino.md)**: Rhino scene objects. + +For the COMPAS viewer, robot scene objects come from +[compas_robots.viewer](https://compas.dev/compas_robots/latest/api/compas_robots.viewer/); +`compas_fab` itself does not ship a separate viewer integration. diff --git a/docs/assets/stylesheets/custom.css b/docs/assets/stylesheets/custom.css new file mode 100644 index 0000000000..d341f692e8 --- /dev/null +++ b/docs/assets/stylesheets/custom.css @@ -0,0 +1,5 @@ +:root>* { + --md-primary-fg-color: #0092d2; + --md-primary-fg-color--light: #0092d2; + --md-primary-fg-color--dark: #0092d2; +} \ No newline at end of file diff --git a/docs/authors.rst b/docs/authors.rst deleted file mode 100644 index e122f914a8..0000000000 --- a/docs/authors.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../AUTHORS.rst diff --git a/docs/backends.rst b/docs/backends.rst deleted file mode 100644 index ce6c34929a..0000000000 --- a/docs/backends.rst +++ /dev/null @@ -1,85 +0,0 @@ -.. _backends: - -******************************************************************************** -Working with backends -******************************************************************************** - -.. highlight:: bash - -One of the driving principles of **COMPAS** framework is to create an ecosystem -and serve as an interface between different front-ends (e.g. CAD software) and -back-ends. - -In the case of **COMPAS FAB**, it provides access to multiple robotic platforms -as backends. This chapter highlights the available ones and explains how to -obtain them and set them up. - - -Installing backends -=================== - -Backends can be installed in different ways. Some backends are very simple to -install, while others are very complex. - -In order to simplify working with these tools and have a consistent way -to use and test different backends, **COMPAS FAB** provides them as -`Docker containers`_. Docker containers are a way to bundle systems into -isolated, standarized software units with full reproducibility. It greatly -reduces the time it takes to get a backend up and running. - -However, all of them can be installed *without* Docker as well. Please refer -to the documentation of the respective tool for standard installation -instructions. - -Installing Docker ------------------ - -Download and install `Docker Desktop`_: - -* `Docker for Windows`_ -* `Docker for Mac`_ - -.. note:: - - Make sure you have enabled virtualization in your BIOS. - Docker will complain if not. - -.. note:: - - If you're a Windows user, you will need at least Windows 10 Pro. - - After installation, make sure Docker runs in Linux containers mode: right-click - the docker icon on the tray bar; if there is an option to ``Switch to Linux containers``, - **select it** and wait for Docker to switch before moving forward. - -Working with containers ------------------------ - -For Visual Studio Code users, we recommend installing the ``Docker`` extension. - - -Developing new backends -======================= - -If you are interested in developing/integrating backends to the framework, check -the :ref:`backend architecture document ` and the -:ref:`contributors_guide`. - -Next steps -========== - -Check documentation for your backend of choice: - -.. toctree:: - :maxdepth: 2 - :titlesonly: - :glob: - - backends/* - - -.. _Docker: https://www.docker.com/ -.. _Docker Desktop: https://www.docker.com/get-started -.. _Docker containers: https://www.docker.com/resources/what-container -.. _Docker for Windows: https://hub.docker.com/editions/community/docker-ce-desktop-windows -.. _Docker for Mac: https://hub.docker.com/editions/community/docker-ce-desktop-mac diff --git a/docs/backends/analytical.md b/docs/backends/analytical.md new file mode 100644 index 0000000000..231df59c01 --- /dev/null +++ b/docs/backends/analytical.md @@ -0,0 +1,55 @@ +# Analytical IK + +Closed-form inverse kinematics for known industrial robots. Runs entirely +in-process — no Docker, no PyBullet, no ROS — and is the fastest IK option +in `compas_fab`. + +## When to use + +- Prototyping reachability and inverse kinematics on a supported robot. +- IK from inside Rhino, Grasshopper or any other Python host. +- Generating IK seeds for a downstream planner. +- You don't need collision checking. (If you do, see + [Analytical IK + PyBullet](analytical_pybullet.md).) + +## Trade-offs + +| What you get | What you give up | +|---|---| +| Closed-form IK in microseconds | Only the supported analytical robot families | +| Zero setup beyond `pip install compas_fab` | No collision checking | +| Works from any Python host (CAD, headless, CI) | No motion planning — IK / FK only | + +## Setup + +Nothing to install beyond `compas_fab` itself. Analytical solvers are part +of the package. + +## Supported robots + +- Universal Robots: UR3, UR3e, UR5, UR5e, UR10, UR10e, UR16e +- Staubli: TX2-60L +- ABB: IRB 4600 40/255 + +See [compas_fab.backends][] for the full list of solver classes. + +## First example + +Forward kinematics on a UR5: + +```python +--8<-- "docs/backends/analytical_kinematics/files/01_forward_kinematics.py" +``` + +## More examples + +- [`02_inverse_kinematics.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/analytical_kinematics/files/02_inverse_kinematics.py) — IK returning 8 solutions for a UR5 +- [`02_inverse_kinematics with_tools.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/analytical_kinematics/files/02_inverse_kinematics%20with_tools.py) — IK with a tool attached to the flange + +For collision-aware IK and Cartesian planning, see +[Analytical IK + PyBullet](analytical_pybullet.md). + +## API reference + +- [compas_fab.backends.AnalyticalKinematicsPlanner][] +- [compas_fab.backends][] — all available solvers diff --git a/docs/backends/analytical_kinematics/files/01_forward_kinematics.py b/docs/backends/analytical_kinematics/files/01_forward_kinematics.py new file mode 100644 index 0000000000..9dc5f32ff2 --- /dev/null +++ b/docs/backends/analytical_kinematics/files/01_forward_kinematics.py @@ -0,0 +1,26 @@ +from compas_robots import Configuration + +from compas_fab.backends import AnalyticalKinematicsPlanner +from compas_fab.backends import UR5Kinematics +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +# Not loading the robot's geometry because AnalyticalKinematicsPlanner does not use it for collision checking +robot_cell, robot_cell_state = RobotCellLibrary.ur5(load_geometry=False) + +# The kinematics_solver must match the robot's kinematics +planner = AnalyticalKinematicsPlanner(UR5Kinematics()) + +# This is a simple robot cell with only the robot +planner.set_robot_cell(robot_cell) + +# Configuration for FK calculation +configuration = Configuration.from_revolute_values([0.0, 4.8, 1.5, 1.1, 1.9, 3.1]) +robot_cell_state.robot_configuration = configuration + +# AnalyticalKinematicsPlanner.forward_kinematics(), do not support `planning_group` parameter, it must be left as None. +# The `link` option is also not supported and cannot be used. +frame_WCF = planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT) + +print("Robot flange frame of the default planning group in the world coordinate system:") +print(frame_WCF) diff --git a/docs/backends/analytical_kinematics/files/02_inverse_kinematics with_tools.py b/docs/backends/analytical_kinematics/files/02_inverse_kinematics with_tools.py new file mode 100644 index 0000000000..d4f5d96815 --- /dev/null +++ b/docs/backends/analytical_kinematics/files/02_inverse_kinematics with_tools.py @@ -0,0 +1,66 @@ +from compas.datastructures import Mesh +from compas.geometry import Frame +from compas_robots import ToolModel + +import compas_fab +from compas_fab.backends import AnalyticalKinematicsPlanner +from compas_fab.backends import UR5Kinematics +from compas_fab.robots import FrameTarget +from compas_fab.robots import RigidBody +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +# Not loading the robot's geometry because AnalyticalKinematicsPlanner does not use it for collision checking +robot_cell, robot_cell_state = RobotCellLibrary.ur5(load_geometry=False) + +# The kinematics_solver must match the robot's kinematics +planner = AnalyticalKinematicsPlanner(UR5Kinematics()) + +# --------------------------------------------------------------------- +# Create a robot cell and add objects to it +# --------------------------------------------------------------------- + +# Add Static Collision Geometry +floor_mesh = Mesh.from_stl(compas_fab.get("planning_scene/floor.stl")) +robot_cell.rigid_body_models["floor"] = RigidBody.from_mesh(floor_mesh) + +# Add Tool +tool_mesh = Mesh.from_stl(compas_fab.get("planning_scene/cone.stl")) +tool_frame = Frame([0, 0, 0.14], [1, 0, 0], [0, 1, 0]) +robot_cell.tool_models["cone"] = ToolModel(tool_mesh, tool_frame) + +# The cell state was created by RobotCellLibrary before the floor and tool were +# added, so it doesn't know about them. Refresh it from the (now complete) cell. +robot_cell_state = robot_cell.default_cell_state() + +# The robot cell is passed to the planner +planner.set_robot_cell(robot_cell) + +# ----------------- +# Define the target +# ----------------- + +# Create a Frame object: Frame(point, xaxis, yaxis) +target_frame = Frame((0.381, 0.093, 0.382), (0.371, -0.292, -0.882), (0.113, 0.956, -0.269)) + +# -------------------------------------- +# First demonstrate the IK without tools +# -------------------------------------- +# TODO: Confirm the following demo works after implementing AnalyticalInverseKinematics with TargetMode + +# Create a target with TargetMode.ROBOT +target = FrameTarget(target_frame, TargetMode.ROBOT) +config = planner.inverse_kinematics(target, robot_cell_state) +print("IK Result (Configuration) without tools and the target represent T0CF:") +print(config) + +# ------------------------------------ +# Second demonstrate the IK with tools +# ------------------------------------ +# Modify the cell state to attach the tool to the robot +robot_cell_state.set_tool_attached_to_group("cone", robot_cell.main_group_name) +# Create a target with TargetMode.TOOL +target = FrameTarget(target_frame, TargetMode.TOOL) +config = planner.inverse_kinematics(target, robot_cell_state) +print("IK Result (Configuration) with a tool attached and the target represent TCF of tool:") +print(config) diff --git a/docs/backends/analytical_kinematics/files/02_inverse_kinematics.py b/docs/backends/analytical_kinematics/files/02_inverse_kinematics.py new file mode 100644 index 0000000000..bd60905f07 --- /dev/null +++ b/docs/backends/analytical_kinematics/files/02_inverse_kinematics.py @@ -0,0 +1,39 @@ +from compas.geometry import Frame + +from compas_fab.backends import AnalyticalKinematicsPlanner +from compas_fab.backends import UR5Kinematics +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +# Not loading the robot's geometry because AnalyticalKinematicsPlanner does not use it for collision checking +robot_cell, robot_cell_state = RobotCellLibrary.ur5(load_geometry=False) + +# The kinematics_solver must match the robot's kinematics +planner = AnalyticalKinematicsPlanner(UR5Kinematics()) + +# This is a simple robot cell with only the robot +planner.set_robot_cell(robot_cell) + +# IK Target +frame_WCF = Frame((0.381, 0.093, 0.382), (0.371, -0.292, -0.882), (0.113, 0.956, -0.269)) # Frame(point, xaxis, yaxis) +target = FrameTarget(frame_WCF, TargetMode.ROBOT) + +# The start state is not important here because there is no tools involved +start_state = robot_cell.default_cell_state() + +# iter_inverse_kinematics() will return a generator that would yield possible IK solutions +print("\nResults of iter_inverse_kinematics():") +for config in planner.iter_inverse_kinematics(target, start_state): + # Note that although eight configurations are returned. + # Some of the configurations may be in self-collision + print(config) + +# inverse_kinematics() will return each possible IK solutions one at a time. +print("\nResults of inverse_kinematics():") +for i in range(9): + # Note that the last configuration is the same as the first one + # because the robot has 8 possible solutions for this target + # and calling the function repeatedly will return the same cycle of results + config = planner.inverse_kinematics(target, start_state) + print(config) diff --git a/docs/backends/analytical_kinematics/files/03_analytical_pybullet_planner.py b/docs/backends/analytical_kinematics/files/03_analytical_pybullet_planner.py new file mode 100644 index 0000000000..0044b9fc68 --- /dev/null +++ b/docs/backends/analytical_kinematics/files/03_analytical_pybullet_planner.py @@ -0,0 +1,55 @@ +# Demonstrates the AnalyticalPyBulletPlanner: closed-form analytical IK plus +# PyBullet collision checking, against a pre-configured robot cell with a tool +# attached and a stationary obstacle in the scene. + +from compas.geometry import Box +from compas.geometry import Frame +from compas_robots import Configuration + +from compas_fab.backends import ABB_IRB4600_40_255Kinematics +from compas_fab.backends import AnalyticalPyBulletClient +from compas_fab.backends import AnalyticalPyBulletPlanner +from compas_fab.robots import FrameTarget +from compas_fab.robots import RigidBody +from compas_fab.robots import RigidBodyState +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with AnalyticalPyBulletClient(connection_type="gui") as client: + # The library cell ships with the printing tool already attached and the + # appropriate touch_links configured between the tool and the wrist. + robot_cell, robot_cell_state = RobotCellLibrary.abb_irb4600_40_255_printing_tool() + planner = AnalyticalPyBulletPlanner(client, ABB_IRB4600_40_255Kinematics()) + + # Add a static obstacle to the cell. + obstacle_mesh = Box(0.2, 0.2, 1.0).to_mesh(triangulated=True) + robot_cell.rigid_body_models["obstacle"] = RigidBody.from_mesh(obstacle_mesh) + robot_cell_state.rigid_body_states["obstacle"] = RigidBodyState( + Frame([1.0, -0.5, 0.5], [1, 0, 0], [0, 1, 0]) + ) + + planner.set_robot_cell(robot_cell) + + # Derive a reachable target via forward kinematics from a chosen seed pose, + # so we know at least some IK candidates exist near a known-good pose. + seed = Configuration.from_revolute_values( + [0.0, 0.3, 0.5, 0.0, 0.5, 0.0], + joint_names=("joint_1", "joint_2", "joint_3", "joint_4", "joint_5", "joint_6"), + ) + robot_cell_state.robot_configuration.merge(seed) + target_frame = planner.forward_kinematics(robot_cell_state, TargetMode.TOOL) + print("Target frame (TCF, derived via FK):", target_frame) + + target = FrameTarget(target_frame, TargetMode.TOOL) + + # `keep_order=False` filters out any analytical candidate that would put + # the robot, tool or workpiece in collision; only collision-free + # configurations are yielded. + options = {"check_collision": True, "keep_order": False} + for i, config in enumerate(planner.iter_inverse_kinematics(target, robot_cell_state, options=options)): + print(f"[{i}] {config}") + # Visualize this configuration in the PyBullet GUI + result_state = robot_cell_state.copy() # type: RobotCellState + result_state.robot_configuration = config + planner.set_robot_cell_state(result_state) + input("Press Enter to continue...") diff --git a/docs/backends/analytical_kinematics/files/03_iter_ik_pybullet.py b/docs/backends/analytical_kinematics/files/03_iter_ik_pybullet.py new file mode 100644 index 0000000000..630b1c7d2b --- /dev/null +++ b/docs/backends/analytical_kinematics/files/03_iter_ik_pybullet.py @@ -0,0 +1,40 @@ +from compas_robots import Configuration + +from compas_fab.backends import AnalyticalPyBulletPlanner +from compas_fab.backends import PyBulletClient +from compas_fab.backends import UR5Kinematics +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient(connection_type="direct") as client: + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + + planner = AnalyticalPyBulletPlanner(client, UR5Kinematics()) + planner.set_robot_cell(robot_cell) + planner.set_robot_cell_state(robot_cell_state) + + # Pick a reachable, collision-free starting pose and use forward kinematics + # to derive a target frame from it. This guarantees the target is reachable + # by at least one configuration of this robot. + seed = Configuration.from_revolute_values( + [0.0, -1.5708, 1.5708, -1.5708, -1.5708, 0.0], + joint_names=("shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint", "wrist_1_joint", "wrist_2_joint", "wrist_3_joint"), + ) + robot_cell_state.robot_configuration.merge(seed) + frame_WCF = planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT) + print("Target frame (derived via FK):", frame_WCF) + + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + + # `keep_order=True` returns one entry per analytical IK candidate, with + # configurations that fail collision checking returned as `None` rather + # than removed. The indices stay stable across calls, which is useful when + # feeding adjacent IK calls into a Cartesian path solver. + options = {"check_collision": True, "keep_order": True} + + for i, config in enumerate(planner.iter_inverse_kinematics(target, robot_cell_state, options=options)): + if config is None: + print(f"[{i}] (in collision)") + continue + print(f"[{i}] {config}") diff --git a/docs/backends/analytical_kinematics/files/04_cartesian_path_analytic_pybullet.py b/docs/backends/analytical_kinematics/files/04_cartesian_path_analytic_pybullet.py new file mode 100644 index 0000000000..c365411a7e --- /dev/null +++ b/docs/backends/analytical_kinematics/files/04_cartesian_path_analytic_pybullet.py @@ -0,0 +1,39 @@ +# This example plots the planned trajectory to a window using matplotlib. +# matplotlib is not a dependency of compas_fab and must be installed separately: +# uv pip install matplotlib + +import matplotlib.pyplot as plt +from compas.geometry import Frame + +from compas_fab.backends import AnalyticalPyBulletClient +from compas_fab.backends import AnalyticalPyBulletPlanner +from compas_fab.backends import UR5Kinematics +from compas_fab.robots import FrameTarget +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +frames_WCF = [ + Frame((0.407, 0.073, 0.320), (0.922, 0.000, 0.388), (0.113, 0.956, -0.269)), + Frame((0.404, 0.057, 0.324), (0.919, 0.000, 0.394), (0.090, 0.974, -0.210)), + Frame((0.390, 0.064, 0.315), (0.891, 0.000, 0.454), (0.116, 0.967, -0.228)), + Frame((0.388, 0.079, 0.309), (0.881, 0.000, 0.473), (0.149, 0.949, -0.278)), + Frame((0.376, 0.087, 0.299), (0.850, 0.000, 0.528), (0.184, 0.937, -0.296)), +] + +with AnalyticalPyBulletClient(connection_type="direct") as client: + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + + kinematics = UR5Kinematics() + planner = AnalyticalPyBulletPlanner(client, kinematics) + + start_configuration = planner.inverse_kinematics(FrameTarget(frames_WCF[0], TargetMode.ROBOT), robot_cell_state) + robot_cell_state.robot_configuration = start_configuration + + waypoints = FrameWaypoints(frames_WCF, TargetMode.ROBOT) + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state) + assert trajectory.fraction == 1.0 + + j = [c.joint_values for c in trajectory.points] + plt.plot(j) + plt.show() diff --git a/docs/backends/analytical_pybullet.md b/docs/backends/analytical_pybullet.md new file mode 100644 index 0000000000..180a6fb844 --- /dev/null +++ b/docs/backends/analytical_pybullet.md @@ -0,0 +1,59 @@ +# Analytical IK + PyBullet + +The hybrid back-end uses closed-form analytical inverse kinematics for speed +and PyBullet underneath for collision checking. It is implemented by +[AnalyticalPyBulletPlanner][compas_fab.backends.AnalyticalPyBulletPlanner] +paired with [AnalyticalPyBulletClient][compas_fab.backends.AnalyticalPyBulletClient]. + +## When to use + +- You need **fast IK** *and* **collision checking** without standing up ROS. +- Your robot is in the analytical solver's supported set (Universal Robots, + Staubli TX2, ABB IRB4600 40/255). +- You want to filter or rank analytical IK solutions against a real + collision scene (workpieces, tools, fixtures). + +## Trade-offs + +| What you get | What you give up | +|---|---| +| Closed-form IK (8 solutions per pose, fast) | Only the supported analytical robot families | +| PyBullet collision checking against tools / workpieces / fixtures | No motion planning — IK only (use `PyBulletPlanner` for that) | +| Runs in-process; no Docker | PyBullet still has to be installed (see [PyBullet setup](pybullet.md#setup)) | + +## Setup + +```bash +uv pip install pybullet +``` + +On macOS, PyBullet may need an extra build flag: + +```bash +CFLAGS="-fno-define-target-os-macros" uv pip install pybullet +``` + +The analytical solvers ship with `compas_fab` itself — nothing else to install. + +## First example + +Compute collision-aware IK for a UR5 against an empty cell: + +```python +--8<-- "docs/backends/analytical_kinematics/files/03_iter_ik_pybullet.py" +``` + +With `keep_order=True`, configurations that fail collision checking are +returned as `None` rather than removed, so the indices stay stable across +calls — useful when feeding adjacent IK calls into a Cartesian path solver. + +## More examples + +- [`03_analytical_pybullet_planner.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/analytical_kinematics/files/03_analytical_pybullet_planner.py) — full cell with tool attached, workpiece grasp, GUI visualisation +- [`04_cartesian_path_analytic_pybullet.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/analytical_kinematics/files/04_cartesian_path_analytic_pybullet.py) — Cartesian path planning with collision-aware analytical IK + +## API reference + +- [compas_fab.backends.AnalyticalPyBulletPlanner][] +- [compas_fab.backends.AnalyticalPyBulletClient][] +- [compas_fab.backends][] — all available analytical solvers diff --git a/docs/backends/index.md b/docs/backends/index.md new file mode 100644 index 0000000000..ab59cd6066 --- /dev/null +++ b/docs/backends/index.md @@ -0,0 +1,53 @@ +# Choosing a backend + +**COMPAS FAB** is one library that drives five different planning back-ends. +Pick the one that matches what you want to do. + +## By intent + +| I want to… | Use | Why | +|---|---|---| +| Just compute IK on a robot (e.g., UR, Staubli, ABB, etc) | [Analytical IK](analytical.md) | Closed-form, microsecond IK, no Docker, no PyBullet | +| Compute IK *and* check collisions | [Analytical IK + PyBullet](analytical_pybullet.md) | Analytical-fast IK filtered against a real collision scene | +| Plan motion (collisions + trajectory smoothing) without ROS | [PyBullet](pybullet.md) | In-process motion planning; runs anywhere Python runs | +| Plan motion via ROS 2 / MoveIt 2 | [ROS 2 / MoveIt 2](ros2.md) | Current ROS LTS; the recommended starting point for new ROS work | +| Drive an existing ROS 1 / MoveIt 1 setup | [ROS 1 / MoveIt 1](ros.md) | Legacy stack; only use if you must | +| Just model / visualize a robot cell in a CAD environment | *no backend* | The core data model works without any planner. See [Concepts](../concepts.md) | + +## By capability + +| Capability | Analytical | Analytical + PyBullet | PyBullet | ROS 1 / MoveIt 1 | ROS 2 / MoveIt 2 | +|---|:-:|:-:|:-:|:-:|:-:| +| Forward kinematics | ✓ | ✓ | ✓ | ✓ | ✓ | +| Inverse kinematics | ✓ (closed-form) | ✓ (closed-form) | ✓ (numerical) | ✓ | ✓ | +| Collision checking | — | ✓ | ✓ | ✓ | ✓ | +| Point-to-point motion planning | — | — | ✓ | ✓ | ✓ | +| Cartesian motion planning | — | ✓ (partial) | ✓ | ✓ | ✓ | +| Visualisation | — | PyBullet GUI | PyBullet GUI | RViz | RViz | +| Setup cost | none | `pip install pybullet` | `pip install pybullet` | Docker | Docker | +| Usable from inside Rhino 7/8 | ✓ | — | — | ✓ (over WebSocket) | ✓ (over WebSocket) | + +## Setup cost in plain words + +- **Analytical IK**: nothing to install beyond `compas_fab` itself. +- **PyBullet / Analytical + PyBullet**: one `pip install pybullet` (with a + small workaround on macOS, see the per-backend pages). +- **ROS 1 / ROS 2**: Docker Desktop + the per-robot compose stack in + [`docs/installation/docker_files/`](https://github.com/compas-dev/compas_fab/tree/main/docs/installation/docker_files). + +## Without a backend + +Even without a planning back-end, the core of `compas_fab` is useful for: + +- Building [`RobotCell`][compas_fab.robots.RobotCell] instances with tools + and rigid bodies +- Modelling cell state via [`RobotCellState`][compas_fab.robots.RobotCellState] +- Authoring [`Target`s][compas_fab.robots.Target] and + [`Waypoints`][compas_fab.robots.Waypoints] and serializing them for + planning elsewhere +- Visualising in a CAD front-end +- Forward kinematics via + [`compas_robots.RobotModel.forward_kinematics`][] + +For a backend-agnostic walkthrough of the data model, see +[Concepts](../concepts.md). diff --git a/docs/backends/pybullet.md b/docs/backends/pybullet.md new file mode 100644 index 0000000000..e46c3432bd --- /dev/null +++ b/docs/backends/pybullet.md @@ -0,0 +1,84 @@ +# PyBullet + +[PyBullet](https://pybullet.org/) wraps the Bullet physics engine. It runs +in-process — no Docker, no server — and gives you forward kinematics, +inverse kinematics (numerical), collision checking, and **Cartesian** motion +planning. + +!!! note "Free-space `plan_motion` not yet implemented" + `PyBulletPlanner` does not currently implement free-space + `plan_motion`. Cartesian motion via `plan_cartesian_motion` works. + For free-space planning, use one of the ROS backends. + +## When to use + +- You want fast IK + collision checking without standing up ROS. +- You want to simulate multiple robots or kinematic tools in one scene. +- You are running headless (CI, batch planning) or from VS Code. +- You want a PyBullet GUI to visualize the planning scene. + +PyBullet is difficult to install inside Rhino 8+. + +## Trade-offs + +| What you get | What you give up | +|---|---| +| Numerical IK + collision checking + motion planning | Numerical IK is slower than analytical IK | +| Runs anywhere Python runs (no Docker) | PyBullet has no pre-built wheels for newest Python versions | +| Optional GUI showing the planning scene | Not usable from inside Rhino | + +## Setup + +PyBullet is an optional dependency of `compas_fab`. It is not installed by +default because upstream does not ship wheels for the newest Python versions. + +```bash +uv pip install pybullet +``` + +On macOS, PyBullet sometimes needs an extra build flag: + +```bash +CFLAGS="-fno-define-target-os-macros" uv pip install pybullet +``` + +`PyBulletClient` supports three connection types: + +- `direct` — headless, no GUI (default) +- `gui` — opens a PyBullet OpenGL window +- `shared_memory` — connect to an external PyBullet server + +## First example + +Inverse + forward kinematics on a UR5 with a GUI: + +```python +--8<-- "docs/backends/pybullet/files/04_ik.py" +``` + +## More examples + +Setup: + +- [`01_set_robot_cell.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/01_set_robot_cell.py) — building a cell with floor + obstacles +- [`01_set_robot_cell_state.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/01_set_robot_cell_state.py) — populating cell state +- [`02_check_collision.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/02_check_collision.py) — collision checking against rigid bodies + +Kinematics: + +- [`03_fk.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/03_fk.py), [`03_fk_target_mode.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/03_fk_target_mode.py), [`03_fk_to_link.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/03_fk_to_link.py) — forward kinematics variants +- [`03_iter_ik.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/03_iter_ik.py) — iterating over IK solutions +- [`04_ik_point_axis_target.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/04_ik_point_axis_target.py) — IK with a free rotation axis (drilling, milling) +- [`04_ik_semi_constrained.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/04_ik_semi_constrained.py), [`04_ik_tool_target_mode.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/04_ik_tool_target_mode.py) — constrained / tool-frame variants +- [`04_ik_errors.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/04_ik_errors.py), [`04_ik_multiple_solutions.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/04_ik_multiple_solutions.py) — error handling and multi-solution patterns + +Motion planning: + +- [`06_cartesian_motion_frame_waypoints.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/06_cartesian_motion_frame_waypoints.py) — Cartesian path with `FrameWaypoints` +- [`06_cartesian_motion_point_axis_waypoints.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/pybullet/files/06_cartesian_motion_point_axis_waypoints.py) — Cartesian path with `PointAxisWaypoints` + +## API reference + +- [compas_fab.backends.PyBulletClient][] +- [compas_fab.backends.PyBulletPlanner][] +- [compas_fab.backends.pybullet.backend_features][] — the underlying feature implementations diff --git a/docs/backends/pybullet.rst b/docs/backends/pybullet.rst deleted file mode 100644 index 24dc9001bc..0000000000 --- a/docs/backends/pybullet.rst +++ /dev/null @@ -1,27 +0,0 @@ -.. _pybullet_backend: - -**************** -PyBullet -**************** - -.. highlight:: bash - -`PyBullet `_ is a Python module extending Bullet, an open -source collision detection and rigid dynamics library written in C++. PyBullet -was written with the intention of being a "fast and easy to use Python module for -robotics simulation and machine learning." It also provides bindings for rendering -and visualization, and support for virtual reality headsets. While PyBullet -is based on a client-server architecture, there is no need to spin up any Docker -containers to run the server. This, along with its speed, may make PyBullet a -preferable backend for COMPAS_FAB. However, it, alone, does not provide motion -planning functionality. PyBullet is also not compatible with IronPython. Hence to use -it with Rhinoceros and Grasshopper it must be invoked through the -:mod:`compas.rpc` module. - -Next Steps -========== - -* :doc:`Tutorial: COMPAS Robots ` -* :ref:`Examples: Description models ` -* :ref:`Examples: PyBullet Backend ` -* :ref:`COMPAS FAB API Reference ` diff --git a/docs/backends/pybullet/files/01_set_robot_cell.py b/docs/backends/pybullet/files/01_set_robot_cell.py new file mode 100644 index 0000000000..940b22ddec --- /dev/null +++ b/docs/backends/pybullet/files/01_set_robot_cell.py @@ -0,0 +1,30 @@ +from compas.datastructures import Mesh + +import compas_fab +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import RigidBody +from compas_fab.robots import RobotCell + +with PyBulletClient() as client: + + # Create a robot cell + robot_cell = RobotCell() # Typically RobotCell is initialed with a Robot, but here it is empty for simplicity + + # Add some RigidBodies as stationary obstacles + floor_mesh = Mesh.from_stl(compas_fab.get("planning_scene/floor.stl")) + robot_cell.rigid_body_models["floor"] = RigidBody.from_mesh(floor_mesh) + cone = Mesh.from_stl(compas_fab.get("planning_scene/cone.stl")) + robot_cell.rigid_body_models["cone"] = RigidBody.from_mesh(cone) + + # Add a target marker as a RigidBody with visual mesh but no collision mesh + target_marker = Mesh.from_obj(compas_fab.get("planning_scene/target_marker.obj")) + robot_cell.rigid_body_models["target_marker"] = RigidBody(target_marker, None) + + # The planner object is needed to pass the robot cell into the PyBullet client + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + # No RobotCellState is passed in this example, so the rigid bodies are added to the PyBullet world's origin + # The floor and cone should appear in the PyBullet's GUI + input("Press Enter to continue...") diff --git a/docs/backends/pybullet/files/01_set_robot_cell_state.py b/docs/backends/pybullet/files/01_set_robot_cell_state.py new file mode 100644 index 0000000000..c198dffd73 --- /dev/null +++ b/docs/backends/pybullet/files/01_set_robot_cell_state.py @@ -0,0 +1,71 @@ +from compas.datastructures import Mesh +from compas.geometry import Box +from compas.geometry import Frame +from compas_robots import ToolModel + +import compas_fab +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import RigidBody +from compas_fab.robots import RobotCellLibrary + +with PyBulletClient() as client: + # --------------------------------------------------------------------- + # Create a robot cell and add objects to it + # --------------------------------------------------------------------- + robot_cell, robot_cell_state = RobotCellLibrary.abb_irb4600_40_255() + + # Add Static Collision Geometry + floor_mesh = Mesh.from_stl(compas_fab.get("planning_scene/floor.stl")) + robot_cell.rigid_body_models["floor"] = RigidBody.from_mesh(floor_mesh) + + # Add Tool + tool_mesh = Mesh.from_stl(compas_fab.get("planning_scene/cone.stl")) + tool_frame = Frame([0, 0, 0.14], [1, 0, 0], [0, 1, 0]) + robot_cell.tool_models["cone"] = ToolModel(tool_mesh, tool_frame) + + # Add workpiece at tool tip + workpiece_mesh = Box(1.0, 0.1, 0.2).to_mesh(triangulated=True) + robot_cell.rigid_body_models["workpiece"] = RigidBody.from_mesh(workpiece_mesh) + + # ------------------------------------------------------------------------ + # Create a RobotCellState to represent the current state of the robot cell + # ------------------------------------------------------------------------ + robot_cell_state = robot_cell.default_cell_state() + + # Change the robot's configuration for demonstration purposes + robot_cell_state.robot_configuration.joint_values[1] = 0.5 # Change the second joint angle to 0.5 [rad] + + # Alternatively, the robot's configuration can be reset to entirely new values + configuration = robot_cell.zero_configuration() + configuration.joint_values[1] = 0.5 # Change the second joint angle to 0.5 [rad] + robot_cell_state.robot_configuration = configuration + + # Attach the tool to the robot's main group + robot_cell_state.set_tool_attached_to_group("cone", robot_cell.main_group_name) + + # Attach the workpiece to the tool + workpiece_grasp_frame = Frame([0, 0, 0.1], [1, 0, 0], [0, 1, 0]) + robot_cell_state.set_rigid_body_attached_to_tool("workpiece", "cone", workpiece_grasp_frame) + + # The planner is used for passing the robot cell into the PyBullet client + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) # or planner.set_robot_cell(robot_cell, robot_cell_state) + planner.set_robot_cell_state(robot_cell_state) + + # ------------------------------------------------------------------------ + # Change the robot_cell_state and observe the effect in the PyBullet GUI + # ------------------------------------------------------------------------ + + # Typically the robot_cell_state is passed to the + # planning functions such as planner.plan_motion(start_state, target), or + # visualization functions such as robot_cell_scene_object.update(robot_cell_state). + # In this example, we are directly calling set_robot_cell_state() to see the effect, + # which can be seen in the PyBullet GUI. + + input("Observe the PyBullet GUI, Press Enter to continue...") + + for i in range(10): + robot_cell_state.robot_configuration.joint_values[1] += 0.1 + planner.set_robot_cell_state(robot_cell_state) + input("Observe the PyBullet GUI, Press Enter to continue...") diff --git a/docs/backends/pybullet/files/02_check_collision.py b/docs/backends/pybullet/files/02_check_collision.py new file mode 100644 index 0000000000..449fab2abf --- /dev/null +++ b/docs/backends/pybullet/files/02_check_collision.py @@ -0,0 +1,70 @@ +import time + +from compas_fab.backends import CollisionCheckError +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import RobotCellLibrary + +with PyBulletClient("gui") as client: + planner = PyBulletPlanner(client) + print("Observe the Pybullet GUI window to see the robot cell state being checked") + + # The robot cell is loaded from RobotCellLibrary + robot_cell, robot_cell_state = RobotCellLibrary.ur10e_gripper_one_beam() + planner.set_robot_cell(robot_cell) + + # --------------------------------------------- + # Example 1 - No collision + # --------------------------------------------- + + # This configuration is not in collision + robot_cell_state.robot_configuration.joint_values = [0, -1.5, 2.0, 0, 0, 0] + + # The following check_collision should pass without raising an exception + start = time.time() + planner.check_collision(robot_cell_state) + print("Time taken for collision check: {}".format(time.time() - start)) + input("Press Enter to continue...\n") + + # --------------------------------------------- + # Example 2 - Collision between beam and floor + # --------------------------------------------- + + robot_cell_state.robot_configuration.joint_values = [0, -1.0, 2.0, 0, 0, 0] + + # The following check_collision should raise an exception + try: + planner.check_collision(robot_cell_state) + except CollisionCheckError as e: + print("\nCollision detected: \n{}".format(e)) + input("Press Enter to continue...\n") + + # --------------------------------------------- + # Example 3 - Multiple Collisions + # --------------------------------------------- + + robot_cell_state.robot_configuration.joint_values = [0, -0.8, 2.0, 0, 0, 0] + + # The following check_collision should raise an exception + # The `full_report` option will return all failed collision pairs in the exception message + try: + planner.check_collision(robot_cell_state, options={"full_report": True}) + except CollisionCheckError as e: + print("\nCollision detected: \n{}".format(e)) + input("Press Enter to continue...\n") + + # --------------------------------------------- + # Example 4 - Verbose Mode + # --------------------------------------------- + + robot_cell_state.robot_configuration.joint_values = [0, -0.7, 2.0, 0, 0, 0] + + # The following check_collision should raise an exception + # The `full_report` option will print all failed collision pairs` + try: + planner.check_collision(robot_cell_state, options={"verbose": True, "full_report": True}) + except CollisionCheckError as e: + print("\nCollision detected: \n{}".format(e)) + input("Press Enter to continue...\n") + + # The verbose action will print all tested collision pairs diff --git a/docs/backends/pybullet/files/03_fk.py b/docs/backends/pybullet/files/03_fk.py new file mode 100644 index 0000000000..198fa351e5 --- /dev/null +++ b/docs/backends/pybullet/files/03_fk.py @@ -0,0 +1,35 @@ +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +# Starting the PyBulletClient with the "direct" mode means that the GUI is not shown +with PyBulletClient("direct") as client: + + # The robot in this example is loaded from a URDF file + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + + # The planner object is needed to call the forward kinematics function + planner = PyBulletPlanner(client) + + # ---------------- + # FK without tools + # ---------------- + # This is a simple robot cell with only the robot + planner.set_robot_cell(robot_cell) + + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + + # In this demo, the default planning group is used for the forward kinematics + frame_WCF = planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT) + + print("Robot flange frame of the default planning group in the world coordinate system:") + print(frame_WCF) + print(" ") + + # --------------------------------- + # FK for all the links in the robot + # --------------------------------- + for link_name in robot_cell.get_link_names(): + frame_WCF = planner.forward_kinematics_to_link(robot_cell_state, link_name) + print("Frame of link '{}' : {}".format(link_name, frame_WCF)) diff --git a/docs/backends/pybullet/files/03_fk_target_mode.py b/docs/backends/pybullet/files/03_fk_target_mode.py new file mode 100644 index 0000000000..e69963496e --- /dev/null +++ b/docs/backends/pybullet/files/03_fk_target_mode.py @@ -0,0 +1,32 @@ +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +# Starting the PyBulletClient with the "direct" mode means that the GUI is not shown +with PyBulletClient("direct") as client: + + # Load robot cell from library with a gripper and a beam + robot_cell, robot_cell_state = RobotCellLibrary.ur5_gripper_one_beam() + + # The planner is used for passing the robot cell into the PyBullet client + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + # --------------------- + # Compute FK with tools + # --------------------- + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + + # To retrieve the tool frame, the TargetMode.TOOL is used + print("Frame of the attached tool TCF in World Coordinate Frame:") + print(planner.forward_kinematics(robot_cell_state, TargetMode.TOOL)) + + # To retrieve the Planner Coordinate Frame, the TargetMode.ROBOT is used + print("Frame of the Planner Coordinate Frame PCF in World Coordinate Frame:") + print(planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT)) + + # It is also possible to retrieve the PCF by requesting with the end effector link name + ee_link_name = robot_cell.get_end_effector_link_name() + print("Frame of the T0CF (name='{}') in World Coordinate Frame:".format(ee_link_name)) + print(planner.forward_kinematics_to_link(robot_cell_state, ee_link_name)) diff --git a/docs/backends/pybullet/files/03_fk_to_link.py b/docs/backends/pybullet/files/03_fk_to_link.py new file mode 100644 index 0000000000..9d1e85d451 --- /dev/null +++ b/docs/backends/pybullet/files/03_fk_to_link.py @@ -0,0 +1,21 @@ +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import RobotCellLibrary + +# Starting the PyBulletClient with the "direct" mode means that the GUI is not shown +with PyBulletClient("direct") as client: + + # The robot in this example is loaded from a URDF file + # This is a simple robot cell with only the robot + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + # Change the robot configuration to a specific one + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + + # FK for all the links in the robot + for link_name in robot_cell.get_link_names(): + frame_WCF = planner.forward_kinematics_to_link(robot_cell_state, link_name) + print("Frame of link '{}' : {}".format(link_name, frame_WCF)) diff --git a/docs/backends/pybullet/files/03_iter_ik.py b/docs/backends/pybullet/files/03_iter_ik.py new file mode 100644 index 0000000000..4ce8f20638 --- /dev/null +++ b/docs/backends/pybullet/files/03_iter_ik.py @@ -0,0 +1,22 @@ +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient(connection_type="direct") as client: + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + + options = {"max_results": 20} + result_count = 0 + for config in planner.iter_inverse_kinematics(target, robot_cell_state, options=options): + print("Found configuration", config) + result_count += 1 + print("Found %d configurations" % result_count) diff --git a/docs/backends/pybullet/files/04_ik.py b/docs/backends/pybullet/files/04_ik.py new file mode 100644 index 0000000000..f59866337e --- /dev/null +++ b/docs/backends/pybullet/files/04_ik.py @@ -0,0 +1,37 @@ +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient() as client: + # Create a robot cell with a UR5 robot + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + # The FrameTarget represents the robot's planner coordinate frame (PCF) + # For the UR5 robot, the PCF is equal to the frame of the 'tool0' link + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + config = planner.inverse_kinematics(target, robot_cell_state) + + print("Inverse kinematics result: ", config) + + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # To verify the IK result, we can compute the FK with the obtained joint positions + robot_cell_state.robot_configuration.merge(config) + frame_WCF = planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT) + print("Forward kinematics result (main group): \n ", frame_WCF) + + # The result is the same as the 'tool0' link's frame + frame_WCF = planner.forward_kinematics_to_link(robot_cell_state, "tool0") + print("Forward kinematics result: (tool0 link): \n ", frame_WCF) + + # However, note that the 'flange' link's frame has a different orientation + frame_WCF = planner.forward_kinematics_to_link(robot_cell_state, "flange") + print("Forward kinematics result: (flange link): \n ", frame_WCF) diff --git a/docs/backends/pybullet/files/04_ik_errors.py b/docs/backends/pybullet/files/04_ik_errors.py new file mode 100644 index 0000000000..6b16d8d0af --- /dev/null +++ b/docs/backends/pybullet/files/04_ik_errors.py @@ -0,0 +1,52 @@ +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.backends.exceptions import InverseKinematicsError +from compas_fab.robots import FrameTarget +from compas_fab.robots import RigidBodyLibrary +from compas_fab.robots import RigidBodyState +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient() as client: + + # Load a pre-made robot cell with one tool from the RobotCellLibrary + robot_cell, robot_cell_state = RobotCellLibrary.ur10e_gripper_one_beam() + planner = PyBulletPlanner(client) + + # Load a target marker for illustration + target_marker = RigidBodyLibrary.target_marker(0.5) + robot_cell.rigid_body_models["target_marker"] = target_marker + planner.set_robot_cell(robot_cell) + + # Choose TargetMode.WORKPIECE for the FrameTarget to directly specify the beam's location + beam_target_point = [0.0, 0.5, 0.01] + frame_WCF = Frame(beam_target_point, [0, 0, -1], [-1, 0, 0]) + robot_cell_state.rigid_body_states["target_marker"] = RigidBodyState(frame_WCF) + target = FrameTarget(frame_WCF, TargetMode.WORKPIECE) + + # ---------------------------------------------- + # Example 1: IK without collision checking + # ---------------------------------------------- + + # The target is causing the attached beam to collide with the floor. + # However, if the planner does not check for collisions, it will returns a solution. + options = {"check_collision": False} + config = planner.inverse_kinematics(target, robot_cell_state, options=options) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # ---------------------------------------------- + # Example 2: Enable collision checking in the IK + # ---------------------------------------------- + try: + # Enable the check_collision mode via options + options = {"check_collision": True, "max_results": 1000} # Default is True + config = planner.inverse_kinematics(target, robot_cell_state, options=options) + except InverseKinematicsError as e: + # The planner will try many times but still unable to find a solution + # after "max_results", it will return InverseKinematicsError. + print(e) + print(e.message) + print(e.target_pcf) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") diff --git a/docs/backends/pybullet/files/04_ik_multiple_solutions.py b/docs/backends/pybullet/files/04_ik_multiple_solutions.py new file mode 100644 index 0000000000..3836eb54cf --- /dev/null +++ b/docs/backends/pybullet/files/04_ik_multiple_solutions.py @@ -0,0 +1,33 @@ +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient() as client: + # This example uses the panda robot, which has 7 joints. + # When given a FrameTarget, the robot will have multiple inverse kinematics solutions. + robot_cell, robot_cell_state = RobotCellLibrary.panda() + + # Set RobotCell using the planner function + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + # Create target + frame_WCF = Frame([0.5, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + + # The following demonstration shows that calling inverse_kinematics() + # multiple times with exactly the same input will return different solutions + # This can also be achieved by calling the function iter_inverse_kinematics() + while True: + # Verbose is set to True to show the collision checking process + config = planner.inverse_kinematics(target, robot_cell_state, options={"verbose": True}) + if config is None: + break + # Note that the result are all unique because a uniqueness filter is built-in to the planner + # The uniqueness tolerance can be adjusted with the options parameter + print("Inverse kinematics result: ", config.joint_values) + input("Observe the IK result in PyBullet's GUI, Press Enter to find next solution...") diff --git a/docs/backends/pybullet/files/04_ik_point_axis_target.py b/docs/backends/pybullet/files/04_ik_point_axis_target.py new file mode 100644 index 0000000000..178b144115 --- /dev/null +++ b/docs/backends/pybullet/files/04_ik_point_axis_target.py @@ -0,0 +1,42 @@ +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import PointAxisTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient() as client: + + # Load a pre-made robot cell with one tool from the RobotCellLibrary + robot_cell, robot_cell_state = RobotCellLibrary.abb_irb4600_40_255_printing_tool() + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + planner.set_robot_cell_state(robot_cell_state) + # Observe the starting configuration in PyBullet's GUI + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # Create a PointAxisTarget to represents the tool's coordinate frame (TCF) + target_center_point = [0.5, 1.5, 0.2] + target_z_axis = [0.2, 0.2, -1] # Slightly tilted + target = PointAxisTarget(target_center_point, target_z_axis, TargetMode.TOOL) + + # Options for planning with PointAxisTarget + # See documentation of PyBulletInverseKinematics._iter_inverse_kinematics_point_axis_target() for more options + options = {"num_rotation_steps": 20, "max_random_restart": 5} + config = planner.inverse_kinematics(target, robot_cell_state, options=options) + print("Inverse kinematics result: ", config) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # The planner automatically plans such that the tool's tip is at the target's center point + # The following `iter_` method will return all possible configurations that can reach the target + # In PyBullet's GUI, notice the tool's tip stays at the same location, only the orientation changes + total_results = 0 + for config in planner.iter_inverse_kinematics(target, robot_cell_state): + total_results += 1 + print("Inverse kinematics result: ", config) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # The total number of results is affected by many factors. + # Firstly, the degree of freedom of the robot. + # Secondly, the reachability of the target and the presence of collision objects limits the number of results. + # Finally, planning options such as the number of random restarts, the number of rotation steps, etc. + print("Total results: ", total_results) diff --git a/docs/backends/pybullet/files/04_ik_point_axis_target_obstacle.py b/docs/backends/pybullet/files/04_ik_point_axis_target_obstacle.py new file mode 100644 index 0000000000..4b0e1493e6 --- /dev/null +++ b/docs/backends/pybullet/files/04_ik_point_axis_target_obstacle.py @@ -0,0 +1,52 @@ +# This example demonstrates the ability of PointAxisTarget to search a valid +# configuration around collision objects + +from compas.geometry import Box +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import PointAxisTarget +from compas_fab.robots import RigidBody +from compas_fab.robots import RigidBodyState +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient() as client: + + # Load a pre-made robot cell with one tool from the RobotCellLibrary + robot_cell, robot_cell_state = RobotCellLibrary.abb_irb4600_40_255_printing_tool() + planner = PyBulletPlanner(client) + + # Add a collision object to the robot cell + box = Box.from_corner_corner_height([0.0, 0.0, 0.0], [0.1, 0.2, 0.0], 2.0) + rigid_body = RigidBody.from_mesh(box.to_mesh()) + robot_cell.rigid_body_models["box"] = rigid_body + + planner.set_robot_cell(robot_cell) + + # Create a PointAxisTarget to represents the tool's coordinate frame (TCF) + target_center_point = [0.5, 2.0, 0.2] + target_z_axis = [0, 1.0, -1.0] # Tilted Axis + target = PointAxisTarget(target_center_point, target_z_axis, TargetMode.TOOL) + + # Set the position of the box near the target + robot_cell_state.rigid_body_states["box"] = RigidBodyState( + Frame([0.15, 1.5, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]) + ) + + # Run the IK function without collision checking, note the the robot's wrist is in collision with the box + options = {"check_collision": False} + config = planner.inverse_kinematics(target, robot_cell_state, options=options) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # Run the IK function with collision checking, the planner will search for a different pose around the box + options = {"check_collision": True} + config = planner.inverse_kinematics(target, robot_cell_state, options=options) + print("IK result with collision checking:", config) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # Using iter_inverse_kinematics to explore all possible configurations around the box + for config in planner.iter_inverse_kinematics(target, robot_cell_state, options=options): + print("IK result:", config) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") diff --git a/docs/backends/pybullet/files/04_ik_semi_constrained.py b/docs/backends/pybullet/files/04_ik_semi_constrained.py new file mode 100644 index 0000000000..e5aecf5d84 --- /dev/null +++ b/docs/backends/pybullet/files/04_ik_semi_constrained.py @@ -0,0 +1,85 @@ +# This example demonstrates the semi-constrained mode of the PyBullet Inverse Kinematics (IK) solver. +# The semi-constrained mode will cause the IK solver to only the target's point position and ignore the target's orientation. +# It is only possible to use a Target with TargetMode.ROBOT in this mode. + +from compas.geometry import Frame +from compas.geometry import distance_point_point + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import FrameTarget +from compas_fab.robots import RigidBodyLibrary +from compas_fab.robots import RigidBodyState +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +# NOTE: The semi-constrained IK mode cannot be used with tools + +with PyBulletClient() as client: + # Create a robot cell with a UR5 robot + robot_cell, robot_cell_state = RobotCellLibrary.ur5() + + # Add a target marker to visualize the target + robot_cell.rigid_body_models["target_marker"] = RigidBodyLibrary.target_marker(size=0.15) + + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + # The initial configuration changes the IK result + start_configuration = robot_cell.zero_configuration() + + # Note that the semi-constrained IK mode only accepts ROBOT mode. + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 1, 0], [0, 0, 1]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + + # The library state was created before the target_marker rigid body was + # added to the cell, so we attach a state entry for it here. + robot_cell_state.rigid_body_states["target_marker"] = RigidBodyState(frame_WCF) + + print(" ") + + # ============================= + # IK with semi-constrained mode + # ============================= + + config = planner.inverse_kinematics(target, robot_cell_state, options={"semi-constrained": True}) + + print("Inverse kinematics result: ", config) + + result_state = robot_cell_state.copy() # type: RobotCellState + result_state.robot_configuration = config + + # Perform forward kinematics to verify the result + fk_frame = planner.forward_kinematics(result_state, TargetMode.ROBOT) + print("Forward kinematics frame: \n", fk_frame) + distance_to_target = distance_point_point(fk_frame.point, target.target_frame.point) + assert distance_to_target < PyBulletPlanner.DEFAULT_TARGET_TOLERANCE_POSITION + print( + "Distance to target: {} is smaller than DEFAULT_TARGET_TOLERANCE_POSITION({})".format( + distance_to_target, PyBulletPlanner.DEFAULT_TARGET_TOLERANCE_POSITION + ) + ) + + # The FK result above shows that only the position of the frame matches with the target at Point(x=0.300, y=0.100, z=0.500) + # The orientation of the frame is arbitrary + + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + print(" ") + + # ============================= + # IK in normal operation + # ============================= + + config = planner.inverse_kinematics(target, robot_cell_state) + + print("Inverse kinematics result: ", config) + + result_state = robot_cell_state.copy() # type: RobotCellState + result_state.robot_configuration = config + + # Perform forward kinematics to verify the result + fk_frame = planner.forward_kinematics(result_state, TargetMode.ROBOT) + print("Forward kinematics frame: \n", fk_frame) + + # The FK result above shows that the position and orientation of the frame matches with the target + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") diff --git a/docs/backends/pybullet/files/04_ik_tool_target_mode.py b/docs/backends/pybullet/files/04_ik_tool_target_mode.py new file mode 100644 index 0000000000..a45ea04113 --- /dev/null +++ b/docs/backends/pybullet/files/04_ik_tool_target_mode.py @@ -0,0 +1,42 @@ +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient() as client: + + # Load a pre-made robot cell with one tool from the RobotCellLibrary + robot_cell, robot_cell_state = RobotCellLibrary.ur5_cone_tool() + planner = PyBulletPlanner(client) + planner.set_robot_cell(robot_cell) + + # Create a FrameTarget to represents the tool's coordinate frame (TCF) + target_center_point = [0.0, 0.5, 0.5] + frame_WCF = Frame(target_center_point, [1, 0, 0], [0, 0, -1]) + target = FrameTarget(frame_WCF, TargetMode.TOOL) + + config = planner.inverse_kinematics(target, robot_cell_state) + + print("Inverse kinematics result: ", config) + + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") + + # The planner automatically plans such that the tool tip point stays on the target frame + # In PyBullet's GUI, notice the tool's tip stays at the same location, only the orientation changes + + frame_WCFs = [] + frame_WCFs.append(Frame(target_center_point, [1, 1, 0], [0, 0, -1])) + frame_WCFs.append(Frame(target_center_point, [0, 1, 0], [0, 0, -1])) + frame_WCFs.append(Frame(target_center_point, [-1, 1, 0], [0, 0, -1])) + frame_WCFs.append(Frame(target_center_point, [-1, 0, 0], [0, 0, -1])) + frame_WCFs.append(Frame(target_center_point, [-1, -1, 0], [0, 0, -1])) + frame_WCFs.append(Frame(target_center_point, [0, -1, 0], [0, 0, -1])) + + for frame_WCF in frame_WCFs: + target = FrameTarget(frame_WCF, TargetMode.TOOL) + config = planner.inverse_kinematics(target, robot_cell_state) + print("Inverse kinematics result: ", config) + input("Observe the IK result in PyBullet's GUI, Press Enter to continue...") diff --git a/docs/backends/pybullet/files/06_cartesian_motion_frame_waypoints.py b/docs/backends/pybullet/files/06_cartesian_motion_frame_waypoints.py new file mode 100644 index 0000000000..0276335494 --- /dev/null +++ b/docs/backends/pybullet/files/06_cartesian_motion_frame_waypoints.py @@ -0,0 +1,46 @@ +from _pybullet_demo_helper import trajectory_replay +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient("gui") as client: + planner = PyBulletPlanner(client) + + # The robot cell in this example is loaded from RobotCellLibrary + robot_cell, robot_cell_state = RobotCellLibrary.ur5_cone_tool() + planner.set_robot_cell(robot_cell) + + # --------------------------------------------- + # Plan Cartesian Motion with FrameWaypoints + # --------------------------------------------- + + # The starting robot configuration is set in the robot cell state + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + # FrameWaypoints can hold more than one target frame + target_frames = [] + # Move to X direction and move back + target_frames.append(Frame([0.6, 0.1, 0.5], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0])) + target_frames.append(Frame([0.4, 0.1, 0.5], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0])) + # Move to Y direction and move back + target_frames.append(Frame([0.4, 0.3, 0.5], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0])) + target_frames.append(Frame([0.4, 0.1, 0.5], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0])) + # Move to Z direction and move back + target_frames.append(Frame([0.4, 0.1, 0.7], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0])) + target_frames.append(Frame([0.4, 0.1, 0.5], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0])) + waypoints = FrameWaypoints(target_frames, target_mode=TargetMode.TOOL) + + # In this demo, the default planning group is used for the forward kinematics + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state) + + print("Planned trajectory has {} points.".format(len(trajectory.points))) + for i, point in enumerate(trajectory.points): + print("- JointTrajectoryPoint {}, joint_values: {}".format(i, point.joint_values)) + + # ------------------------------------------------ + # Replay the trajectory in the PyBullet simulation + # ------------------------------------------------ + trajectory_replay(planner, robot_cell_state, trajectory) diff --git a/docs/backends/pybullet/files/06_cartesian_motion_interpolation_settings.py b/docs/backends/pybullet/files/06_cartesian_motion_interpolation_settings.py new file mode 100644 index 0000000000..6fec928694 --- /dev/null +++ b/docs/backends/pybullet/files/06_cartesian_motion_interpolation_settings.py @@ -0,0 +1,62 @@ +from _pybullet_demo_helper import trajectory_replay +from compas.geometry import Frame + +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient("gui") as client: + planner = PyBulletPlanner(client) + + # The robot cell in this example is loaded from RobotCellLibrary + robot_cell, robot_cell_state = RobotCellLibrary.ur5_cone_tool() + planner.set_robot_cell(robot_cell) + + # The starting robot configuration is set in the robot cell state + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + + # The following waypoint will cause the tool tip to rotate for 45 degrees and rotate back + target_frames = [] + target_frames.append(Frame([0.4, 0.1, 0.5], [1.0, 1.0, 0.0], [1.0, -1.0, 0.0])) + target_frames.append(Frame([0.4, 0.1, 0.5], [1.0, 0.0, 0.0], [0.0, -1.0, 0.0])) + waypoints = FrameWaypoints(target_frames, target_mode=TargetMode.TOOL) + + # ------------------------------------------------------- + # Example 1 - Plan Cartesian Motion with default settings + # ------------------------------------------------------- + # The following trajectory will have very few points because rotation around Z axis has virtually no distance + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state) + + print("Example 1: Planned trajectory with default settings has {} points.".format(len(trajectory.points))) + + # -------------------------------------------------------- + # Example 2 - Controlling max_step_angle + # -------------------------------------------------------- + + # Increasing the `max_step_angle` will cause the planner to have finer steps in the interpolation + options = {"max_step_angle": 0.017} # Roughly 1 degree + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state, options=options) + # The frame is rotated by 90 degrees in total, so the trajectory should have roughly 90 points + print("Example 2: Planned trajectory with max_step_angle=0.017 has {} points.".format(len(trajectory.points))) + + # -------------------------------------------------------- + # Example 3 - Controlling max_jump_revolute + # -------------------------------------------------------- + + # An alternative is to increase the `max_jump_revolute` parameter + # This will cause the planner to insert more points in the interpolation because the planned + # configuration has too much distance to the next point + + options = {"max_jump_revolute": 0.087} # Roughly 5 degrees + # This means between each point, no revolute joint will move more than 5 degrees + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state, options=options) + print("Example 3: Planned trajectory with max_jump_revolute=0.1 has {} points.".format(len(trajectory.points))) + + # ------------------------------------------------ + # Replay the trajectory in the PyBullet simulation + # ------------------------------------------------ + # The following code only serves demonstration purposes + # User can step through the trajectory points by pressing 'Enter' key + trajectory_replay(planner, robot_cell_state, trajectory) diff --git a/docs/backends/pybullet/files/06_cartesian_motion_point_axis_waypoints.py b/docs/backends/pybullet/files/06_cartesian_motion_point_axis_waypoints.py new file mode 100644 index 0000000000..a03c60fb4e --- /dev/null +++ b/docs/backends/pybullet/files/06_cartesian_motion_point_axis_waypoints.py @@ -0,0 +1,81 @@ +from _pybullet_demo_helper import trajectory_replay +from compas.geometry import Box +from compas.geometry import Frame +from compas.geometry import Point +from compas.geometry import Vector + +from compas_fab.backends import MPNoIKSolutionError +from compas_fab.backends import MPNoPlanFoundError +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import PointAxisTarget +from compas_fab.robots import PointAxisWaypoints +from compas_fab.robots import RigidBody +from compas_fab.robots import RigidBodyState +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with PyBulletClient("gui") as client: + planner = PyBulletPlanner(client) + + # The robot cell in this example is loaded from RobotCellLibrary + # The printing tool TCP is defined with its Z axis pointing out of nozzle + robot_cell, robot_cell_state = RobotCellLibrary.abb_irb4600_40_255_printing_tool() + + # Add a box (without collision geometry) for demonstration visualization + # Printing tool will trace a square around this box + # box_size = 0.6 will demonstrate a successful planning + # box_size = 0.8 will demonstrate a failure because it is out of reach + box_size = 0.6 + box = Box.from_corner_corner_height([1.0, 1.0, 0], [1.0 + box_size, 1.0 + box_size, 0], 0.50) + rigidbody = RigidBody(box.to_mesh(True), None) + robot_cell.rigid_body_models["box"] = rigidbody + robot_cell_state.rigid_body_states["box"] = RigidBodyState(Frame.worldXY()) + + planner.set_robot_cell(robot_cell) + # -------------------------------------- + # ------- + # Plan Cartesian Motion with FrameWaypoints + # --------------------------------------------- + + # Perform IK to get the initial configuration - first corner of box + first_target = PointAxisTarget(Point(1.0, 1.0, 0.5), Vector(0.5, 0.5, -1.0), TargetMode.TOOL) + initial_configuration = planner.inverse_kinematics(first_target, robot_cell_state) + + # PointAxisWaypoints accepts a list of tuples, each containing a point and an axis + points_and_axes = [] + # Move around in a square with some axis inclinations + points_and_axes.append((Point(1.0 + box_size, 1.0, 0.5), Vector(-0.5, 0.5, -1.0))) + points_and_axes.append((Point(1.0 + box_size, 1.0 + box_size, 0.5), Vector(-0.5, -0.5, -1.0))) + points_and_axes.append((Point(1.0, 1.0 + box_size, 0.5), Vector(0.5, -0.5, -1.0))) + points_and_axes.append((Point(1.0, 1.0, 0.5), Vector(0.5, 0.5, -1.0))) + + waypoints = PointAxisWaypoints(points_and_axes, target_mode=TargetMode.TOOL) + + print("initial_target:", first_target) + print("initial_configuration:", initial_configuration) + input("Press Enter to plan the trajectory...") + + # In this demo, the default planning group is used for the forward kinematics + robot_cell_state.robot_configuration = initial_configuration + try: + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state) + print("Planned trajectory has {} points.".format(len(trajectory.points))) + except MPNoIKSolutionError as e: + # This exception is raised when part of the trajectory has no IK solution, + # either due to collision or it is not reachable. + print("No IK solution found. Reason:", e.message) + print("Target that could not be reached:", e.target) + trajectory = e.partial_trajectory + print("Partial trajectory returned has {} points.".format(len(trajectory.points))) + except MPNoPlanFoundError as e: + # This exception is raised when no plan could be found, the IK solutions along the + # trajectory are valid, but the planner could not find a continuous path between them. + print("No plan found. Reason:", e.message) + trajectory = e.partial_trajectory + print("Partial trajectory returned has {} points.".format(len(trajectory.points))) + + # ------------------------------------------------ + # Replay the trajectory in the PyBullet simulation + # ------------------------------------------------ + trajectory_replay(planner, robot_cell_state, trajectory) diff --git a/docs/backends/pybullet/files/07_plan_motion_config_target.py b/docs/backends/pybullet/files/07_plan_motion_config_target.py new file mode 100644 index 0000000000..05130e19bf --- /dev/null +++ b/docs/backends/pybullet/files/07_plan_motion_config_target.py @@ -0,0 +1,74 @@ +from _pybullet_demo_helper import trajectory_replay +from compas.geometry import Box +from compas.geometry import Frame + +from compas_fab.backends import MPNoIKSolutionError +from compas_fab.backends import MPNoPlanFoundError +from compas_fab.backends import PyBulletClient +from compas_fab.backends import PyBulletPlanner +from compas_fab.robots import ConfigurationTarget +from compas_fab.robots import RigidBody +from compas_fab.robots import RigidBodyState +from compas_fab.robots import RobotCellLibrary + +with PyBulletClient("gui") as client: + planner = PyBulletPlanner(client) + + # The robot cell in this example is loaded from RobotCellLibrary + # The printing tool TCP is defined with its Z axis pointing out of nozzle + robot_cell, robot_cell_state = RobotCellLibrary.abb_irb4600_40_255_printing_tool() + + # Add a box (without collision geometry) for demonstration visualization + # Printing tool will trace a square around this box + # box_size = 0.6 will demonstrate a successful planning + # box_size = 0.8 will demonstrate a failure because it is out of reach + box_size = 0.4 + box = Box.from_corner_corner_height([1.0, 0.0, 0], [1.0 + box_size, 0.0 + box_size, 0], 1.50) + rigidbody = RigidBody(box.to_mesh(True), None) + robot_cell.rigid_body_models["box"] = rigidbody + robot_cell_state.rigid_body_states["box"] = RigidBodyState(Frame.worldXY()) + + planner.set_robot_cell(robot_cell) + + # Set the robot's initial configuration + start_state = robot_cell_state.copy() # type: RobotCellState + start_state.robot_configuration.joint_values = [-0.5, 0.5, 0.0, 1.0, -0.7, 0.0] + end_state = robot_cell_state.copy() # type: RobotCellState + end_state.robot_configuration.joint_values = [0.5, 0.5, 0.0, 1.0, -0.7, 0.0] + + planner.set_robot_cell_state(start_state) + planner.check_collision(start_state) + input("Observe the start state in PyBullet's GUI, Press Enter to continue...") + + planner.set_robot_cell_state(end_state) + planner.check_collision(end_state) + input("Observe the end state in PyBullet's GUI, Press Enter to continue...") + + + # -------------------------------------- + # Plan Free Motion with ConfigurationTarget + # --------------------------------------------- + + target = ConfigurationTarget(end_state.robot_configuration) + try: + trajectory = planner.plan_motion(target, start_state) + print("Planned trajectory has {} points.".format(len(trajectory.points))) + except MPNoIKSolutionError as e: + # This exception is raised when part of the trajectory has no IK solution, + # either due to collision or it is not reachable. + print("No IK solution found. Reason:", e.message) + print("Target that could not be reached:", e.target) + trajectory = e.partial_trajectory + print("Partial trajectory returned has {} points.".format(len(trajectory.points))) + except MPNoPlanFoundError as e: + # This exception is raised when no plan could be found, the IK solutions along the + # trajectory are valid, but the planner could not find a continuous path between them. + print("No plan found. Reason:", e.message) + trajectory = e.partial_trajectory + print("Partial trajectory returned has {} points.".format(len(trajectory.points))) + + # # ------------------------------------------------ + # # Replay the trajectory in the PyBullet simulation + # # ------------------------------------------------ + + trajectory_replay(planner, robot_cell_state, trajectory) diff --git a/docs/backends/pybullet/files/_pybullet_demo_helper.py b/docs/backends/pybullet/files/_pybullet_demo_helper.py new file mode 100644 index 0000000000..f68bb94660 --- /dev/null +++ b/docs/backends/pybullet/files/_pybullet_demo_helper.py @@ -0,0 +1,22 @@ +def trajectory_replay(planner, robot_cell_state, trajectory): + """The function only helps with demonstration using PyBullet's GUI mode. + User can step through the trajectory points by pressing 'Enter' key + + In a real application, the trajectory should be visualized in + a frontend such as Rhino or compas_viewer. + The use of PyBullet's GUI mode is discouraged in production code. + + """ + print("Replaying trajectory with {} points. Auto loop enabled.".format(len(trajectory.points))) + step = 0 + intermediate_robot_cell_state = robot_cell_state.copy() # type: RobotCellState + while True: + if step >= len(trajectory.points): + step = 0 + + print("Step: {} - joint_values = {}".format(step, trajectory.points[step].joint_values)) + intermediate_robot_cell_state.robot_configuration = trajectory.points[step] + planner.set_robot_cell_state(intermediate_robot_cell_state) + + input("Press Enter to continue...") + step += 1 diff --git a/docs/backends/ros.md b/docs/backends/ros.md new file mode 100644 index 0000000000..301125002d --- /dev/null +++ b/docs/backends/ros.md @@ -0,0 +1,81 @@ +# ROS 1 / MoveIt 1 + +The ROS 1 / MoveIt 1 back-end is the legacy planning stack for `compas_fab`. +For new projects we recommend the [ROS 2 / MoveIt 2](ros2.md) back-end — +ROS 1 reached end of life on Noetic and receives no further upstream updates. + +## When to use + +- You already have a ROS 1 workspace or a robot that only ships ROS 1 drivers. +- You are reproducing a legacy `compas_fab` example or paper. +- For anything new, prefer [ROS 2 / MoveIt 2](ros2.md). + +## Trade-offs + +| What you get | What you give up | +|---|---| +| Full motion planning via MoveIt (with collision checking, trajectory smoothing) | Requires Docker + a per-robot compose stack | +| Per-robot demo stacks shipped with the repo | ROS 1 is EOL — no new features, only bug fixes | +| Works from any host OS (Docker Desktop on Mac/Windows is enough) | Slower iteration than in-process backends like PyBullet | + +## Architecture + +`compas_fab` talks to ROS over +[`rosbridge`](https://github.com/RobotWebTools/rosbridge_suite), a +WebSocket bridge that translates JSON messages to ROS topics, services and +actions. The back-end runs inside Docker; your Python code (in any +front-end) connects to it as a client. + +- [RosClient][compas_fab.backends.RosClient]: low-level rosbridge connection +- [MoveItPlanner][compas_fab.backends.MoveItPlanner]: planning service wrapper + +## Setup + +Per-robot Docker compose stacks live under +[`docs/installation/docker_files/`](https://github.com/compas-dev/compas_fab/tree/main/docs/installation/docker_files). +Each demo bundles ROS 1, MoveIt 1, a robot driver and `rosbridge_server`. + +Start, for example, the UR10e demo: + +```bash +cd docs/installation/docker_files/ur10e-demo +docker compose up +``` + +## First example + +Connect from Python and verify the bridge: + +```python +--8<-- "docs/backends/ros/files/01_ros_connection.py" +``` + +## More examples + +Setup: + +- [`02_load_robot_cell.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/02_load_robot_cell.py) — load the robot cell from ROS +- [`02_set_robot_cell.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/02_set_robot_cell.py), [`02_set_robot_cell_state_attach_objects.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/02_set_robot_cell_state_attach_objects.py), [`02_set_robot_cell_state_with_kinematic_tools.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/02_set_robot_cell_state_with_kinematic_tools.py) — populating cells and states + +Kinematics: + +- [`03_forward_kinematics.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/03_forward_kinematics.py), [`03_forward_kinematics_target_mode.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/03_forward_kinematics_target_mode.py), [`03_forward_kinematics_to_link.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/03_forward_kinematics_to_link.py) — FK variants +- [`04_ik.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_ik.py), [`04_ik_target_mode.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_ik_target_mode.py), [`04_ik_allow_collision.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_ik_allow_collision.py), [`04_ik_full_config.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_ik_full_config.py), [`04_ik_unreachable.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_ik_unreachable.py), [`04_iter_ik.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_iter_ik.py) — IK variants +- [`04_ik_cache.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_ik_cache.py), [`04_iter_ik_unique.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/04_iter_ik_unique.py) — IK caching and unique-solution filtering + +Motion planning: + +- [`05_plan_motion.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/05_plan_motion.py), [`05_plan_motion_configuration.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/05_plan_motion_configuration.py), [`05_plan_motion_tolerance.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/05_plan_motion_tolerance.py) — point-to-point motion planning +- [`05_plan_motion_with_attachment.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/05_plan_motion_with_attachment.py), [`05_plan_motion_with_obstacle.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/05_plan_motion_with_obstacle.py) — planning with attached tools or obstacles +- [`06_cartesian_motion.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/06_cartesian_motion.py), [`06_cartesian_motion_partial.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/06_cartesian_motion_partial.py), [`06_cartesian_motion_step_size.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/06_cartesian_motion_step_size.py), [`06_cartesian_motion_target_mode.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/06_cartesian_motion_target_mode.py) — Cartesian motion variants + +Topics / pubsub: + +- [`10_ros_hello_world_talker.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/10_ros_hello_world_talker.py), [`10_ros_hello_world_listener.py`](https://github.com/compas-dev/compas_fab/blob/main/docs/backends/ros/files/10_ros_hello_world_listener.py) — raw ROS pub/sub via rosbridge + +## API reference + +- [compas_fab.backends.RosClient][] +- [compas_fab.backends.MoveItPlanner][] +- [compas_fab.backends.RosFileServerLoader][] +- [compas_fab.backends.ros.backend_features][] — the underlying feature implementations diff --git a/docs/backends/ros.rst b/docs/backends/ros.rst deleted file mode 100644 index db777a0ca3..0000000000 --- a/docs/backends/ros.rst +++ /dev/null @@ -1,160 +0,0 @@ -.. _ros_backend: - -**************** -ROS -**************** - -.. highlight:: bash - -The `Robot Operating System `_ (ROS) is a flexible framework -for writing robot software. It is a collection of tools, libraries, and -conventions that aim to simplify the task of creating complex and robust -robot behavior across a wide variety of robotic platforms. - -Running a ROS system usually involves multiple nodes (i.e. computers, real or -virtual), interconnected through a master controller. - -There are at least 3 different ways to run ROS: using Docker, using Linux, and -using WSL on Windows. In recent times, it became possible to install ROS using -Conda on Windows as well. - - -ROS on Docker -============= - -To massively simplify the use of these tools, we package complete ROS systems -into bundles of `Docker`_ containers. Each of these bundles runs in a -virtualized network within your computer. - -Besides easing deployment, containers have the added benefit of ensuring -repeatability. - -Once you made sure `Docker`_ is running, you can run ROS nodes as containers. -Gramazio Kohler Research publishes ROS images on `Docker Hub`_ but there are -many more to be found online. - -You can start a minimally functional ROS system, containing a ROS master and -the `ROS Bridge`_ with the following command:: - - docker run -p 9090:9090 -t gramaziokohler/ros-noetic-base roslaunch rosbridge_server rosbridge_websocket.launch - -Complete ROS systems --------------------- - -It is usually not enough to run single ROS nodes. ROS systems are networks of -multiple interconnected nodes. Docker provides a way to compose virtualized -networks using the ``docker-compose`` command. These commands take one simple -configuration file as input, and handle all tasks required to run and connect -all the nodes. - -As an example, download :download:`this file `, -open the command prompt, go to the folder where the file was downloaded, -and run the following command:: - - docker-compose up -d - -You now have a ROS system with two nodes running: a ROS master and -the `ROS Bridge`_ which adds a web socket channel to communicate with ROS. - -Creating new ROS bundles using containers is usually only a matter of combining -them into a new ``docker-compose.yml`` file, which is relatively simple but we -prepared some very common ones as examples. - -.. _ros_bundles_list: - -**Complete ROS system examples** - -* ROS Noetic Base setup: :download:`Link ` -* ABB IRB120: :download:`Link ` -* ABB IRB120T: :download:`Link ` -* ABB IRB1600: :download:`Link ` -* ABB IRB4600 40/255: :download:`Link ` -* ABB IRB4600 60/205: :download:`Link ` -* Panda: :download:`Link ` -* RFL: :download:`Link ` -* UR3: :download:`Link ` -* UR3e: :download:`Link ` -* UR5: :download:`Link ` -* UR5e: :download:`Link ` -* UR10: :download:`Link ` -* UR10e: :download:`Link ` - -Once the containers are running, it is possible to access the graphic user interface. -Check :ref:`the following page ` for more details. - -ROS on Linux -============ - -The usual but most involved way to install ROS is on a Linux machine, -either virtual or real. The machine should have an IP address reachable -from your computer. - -Follow the `ROS installation instructions`_ for all the details, or -alternatively, use the following commands as a brief outline of the steps -required to install ROS on **Ubuntu 20.04**: - -:: - - sudo sh -c 'echo "deb http://packages.ros.org/ros/ubuntu $(lsb_release -sc) main" > /etc/apt/sources.list.d/ros-latest.list' - curl -s https://raw.githubusercontent.com/ros/rosdistro/master/ros.asc | sudo apt-key add - - sudo apt update - sudo apt install ros-noetic-desktop-full ros-noetic-rosbridge-server python3-rosdep python3-rosinstall python3-rosinstall-generator python3-wstool build-essential - - sudo rosdep init && rosdep update - echo "source /opt/ros/noetic/setup.bash" >> ~/.bashrc - source ~/.bashrc - - mkdir -p ~/catkin_ws/src - cd ~/catkin_ws/ - catkin_make - - echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc - source ~/.bashrc - -Once ROS is installed, you can start a minimally functional ROS system, -containing a ROS master and the `ROS Bridge`_ with the following command:: - - roslaunch rosbridge_server rosbridge_websocket.launch - - -ROS on WSL -========== - -For Windows 10 users, an alternative is to install the -`Windows Subsystem for Linux`_ (WSL). WSL allows to run Linux within -Windows without the need for an additional virtual machine. - -To install WSL, open PowerShell as administrator and run: - -:: - - wsl --install - -This command will enable the required optional components, download the latest Linux kernel, -set WSL 2 as your default, and install a Linux distribution for you. -Once the installation is completed, run ``bash`` and follow the instructions -above to install ROS on Linux. - -After installation, it is possible to access the graphic user interface. -Check :ref:`the following page ` for more details. - -.. seealso:: - - For additional details, see `Microsoft WSL documentation`_. - - - -.. _ROS installation instructions: https://wiki.ros.org/ROS/Installation -.. _Windows Subsystem for Linux: https://docs.microsoft.com/en-us/windows/wsl/about -.. _Microsoft WSL documentation: https://docs.microsoft.com/en-us/windows/wsl/install-win10 -.. _Docker: https://www.docker.com/ -.. _Docker Hub: https://hub.docker.com/u/gramaziokohler/ -.. _ROS Bridge: https://wiki.ros.org/rosbridge_suite - -Next Steps -========== - -* :doc:`Tutorial: COMPAS Robots ` -* :ref:`Examples: Description models ` -* :ref:`Examples: ROS Backend ` -* :ref:`COMPAS FAB API Reference ` diff --git a/docs/backends/ros/files/01_ros_connection.py b/docs/backends/ros/files/01_ros_connection.py new file mode 100644 index 0000000000..ea77578c8d --- /dev/null +++ b/docs/backends/ros/files/01_ros_connection.py @@ -0,0 +1,4 @@ +from compas_fab.backends import RosClient + +with RosClient() as client: + print("Connected: ", client.is_connected) diff --git a/docs/backends/ros/files/02_load_robot_cell.py b/docs/backends/ros/files/02_load_robot_cell.py new file mode 100644 index 0000000000..66c24a8f6b --- /dev/null +++ b/docs/backends/ros/files/02_load_robot_cell.py @@ -0,0 +1,33 @@ +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + robot_cell.print_info() + +""" +Output: +>>> The robot's name is 'ur5_robot'. +>>> The robot's joints are: +>>> * 'base_link-base_link_inertia' is of type 'fixed' (Fixed) +>>> * 'base_link-base_fixed_joint' is of type 'fixed' (Fixed) +>>> * 'shoulder_pan_joint' is of type 'revolute' and has limits [6.283, -6.283] (Configurable) +>>> * 'shoulder_lift_joint' is of type 'revolute' and has limits [6.283, -6.283] (Configurable) +>>> * 'elbow_joint' is of type 'revolute' and has limits [3.142, -3.142] (Configurable) +>>> * 'wrist_1_joint' is of type 'revolute' and has limits [6.283, -6.283] (Configurable) +>>> * 'wrist_2_joint' is of type 'revolute' and has limits [6.283, -6.283] (Configurable) +>>> * 'wrist_3_joint' is of type 'revolute' and has limits [6.283, -6.283] (Configurable) +>>> * 'wrist_3-flange' is of type 'fixed' (Fixed) +>>> * 'flange-tool0' is of type 'fixed' (Fixed) +>>> The robot's links are: +>>> ['base_link', 'base_link_inertia', 'shoulder_link', 'upper_arm_link', 'forearm_link', 'wrist_1_link', 'wrist_2_link', 'wrist_3_link', 'base', 'flange', 'tool0'] +>>> The planning groups are: ['manipulator', 'endeffector'] +>>> The main planning group is 'manipulator'. +>>> - base link's name is 'base_link' +>>> - end-effector's link name is 'tool0'.>>> The following tools are present: +>>> The following tools are present: +>>> [] +>>> The following rigid bodies are present: +>>> [] +""" diff --git a/docs/backends/ros/files/02_set_robot_cell.py b/docs/backends/ros/files/02_set_robot_cell.py new file mode 100644 index 0000000000..f1bfd9f14b --- /dev/null +++ b/docs/backends/ros/files/02_set_robot_cell.py @@ -0,0 +1,30 @@ +from compas.geometry import Box + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import RigidBody +from compas_fab.robots import RigidBodyLibrary +from compas_fab.robots import ToolLibrary + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + + # Add a gripper to the robot cell + gripper = ToolLibrary.static_gripper_small() + robot_cell.tool_models["my_gripper"] = gripper + + # Add a beam to the robot cell + # Use triangulated mesh + beam_mesh = Box(1, 0.1, 0.1).to_mesh(triangulated=True) + beam = RigidBody.from_mesh(beam_mesh) + robot_cell.rigid_body_models["my_beam"] = beam + + # Add a floor to the robot cell + floor = RigidBodyLibrary.floor() + robot_cell.rigid_body_models["my_floor"] = floor + + # Set the robot cell back to the client + planner.set_robot_cell(robot_cell) + # If you are running ROS with UI, you should see the objects in the PyBullet world + input("Press Enter to terminate...") diff --git a/docs/backends/ros/files/02_set_robot_cell_state_attach_objects.py b/docs/backends/ros/files/02_set_robot_cell_state_attach_objects.py new file mode 100644 index 0000000000..62428b5239 --- /dev/null +++ b/docs/backends/ros/files/02_set_robot_cell_state_attach_objects.py @@ -0,0 +1,68 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import RobotCellLibrary + +with RosClient() as client: + planner = MoveItPlanner(client) + + # ========= + # Step 1 + # ========= + + # Load a robot cell that contains a gripper and a beam + robot_cell, _ = RobotCellLibrary.ur5_gripper_one_beam() + gripper = robot_cell.tool_models["gripper"] + beam = robot_cell.rigid_body_models["beam"] + + # The objects are simply added to the backend, their positions are not set + planner.set_robot_cell(robot_cell) + + # If you are running ROS with UI, you should see the added object + # positioned around the world origin + input("Press Enter to continue...") + + # ========= + # Step 2 + # ========= + # Set the position of the beam + robot_cell_state = robot_cell.default_cell_state() + robot_cell_state.rigid_body_states["beam"].frame = Frame([1.2, 0, 0], [0, 0, 1], [1, 0, 0]) + + # Attach the gripper to the robot + robot_cell_state.tool_states["gripper"].attached_to_group = robot_cell.main_group_name + robot_cell_state.tool_states["gripper"].attachment_frame = Frame([0, 0, 0], [0, 0, 1], [1, 0, 0]) + + # Set the robot cell state in the planner + planner.set_robot_cell_state(robot_cell_state) + + # If you are running ROS with UI, you should see the gripper attached to the robot + input("Press Enter to continue...") + + # ========= + # Step 3 + # ========= + + # Attach the beam to the robot + robot_cell_state.rigid_body_states["beam"].attached_to_tool = "gripper" + robot_cell_state.rigid_body_states["beam"].attachment_frame = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]) + + # Set the robot cell state in the planner + planner.set_robot_cell_state(robot_cell_state) + + # If you are running ROS with UI, you should see the beam attached to the gripper + input("Press Enter to continue...") + + # ========= + # Step 4 + # ========= + + # Move the robot to a specific configuration + robot_cell_state.robot_configuration._joint_values[1] = -1.0 + robot_cell_state.robot_configuration._joint_values[2] = 0.5 + planner.set_robot_cell_state(robot_cell_state) + + # If you are running ROS with UI, you should see the robot in a different configuration + # where the gripper and the beam are still attached to the robot. + input("Press Enter to terminate...") diff --git a/docs/backends/ros/files/02_set_robot_cell_state_with_kinematic_tools.py b/docs/backends/ros/files/02_set_robot_cell_state_with_kinematic_tools.py new file mode 100644 index 0000000000..320009bd58 --- /dev/null +++ b/docs/backends/ros/files/02_set_robot_cell_state_with_kinematic_tools.py @@ -0,0 +1,55 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import ToolLibrary + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + + # ========= + # Step 1 + # ========= + + # Add a kinematic gripper tool to the robot cell + gripper = ToolLibrary.kinematic_gripper() + robot_cell.tool_models[gripper.name] = gripper + # Attach the gripper to the robot + robot_cell_state = robot_cell.default_cell_state() + robot_cell_state.set_tool_attached_to_group( + gripper.name, + robot_cell.main_group_name, + attachment_frame=Frame([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]), + touch_links=["wrist_3_link"], # This is the link that the tool is attached to + ) + + # Move the robot to a specific configuration + robot_cell_state.robot_configuration._joint_values[1] = -1.0 + robot_cell_state.robot_configuration._joint_values[2] = 0.5 + result = planner.set_robot_cell(robot_cell, robot_cell_state) + + # The gripper tool's default configuration is a closed gripper state at [0, 0] + print(gripper.zero_configuration()) + + # If you are running ROS with UI, you should see the gripper attached to the robot. + input("Press Enter to continue...") + + # ========= + # Step 2 + # ========= + + # Kinematic tools can be moved with it's Configuration + # It can be convenient to predefine the open and closed states of a gripper + opened_gripper_configuration = gripper.zero_configuration() + opened_gripper_configuration.joint_values = [0.025, 0.025] + closed_gripper_configuration = gripper.zero_configuration() + closed_gripper_configuration.joint_values = [0.0, 0.0] + + robot_cell_state.tool_states[gripper.name].configuration = opened_gripper_configuration + + # Calling `set_robot_cell_state` updates the robot cell in the planner + result = planner.set_robot_cell_state(robot_cell_state) + + # If you are running ROS with UI, you should see the gripper fingers in an opened state. + input("Press Enter to terminate...") diff --git a/docs/backends/ros/files/03_forward_kinematics.py b/docs/backends/ros/files/03_forward_kinematics.py new file mode 100644 index 0000000000..7b90b0564d --- /dev/null +++ b/docs/backends/ros/files/03_forward_kinematics.py @@ -0,0 +1,24 @@ +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import TargetMode + +with RosClient() as client: + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + planner = MoveItPlanner(client) + + robot_cell_state = robot_cell.default_cell_state() + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + + # When using the forward_kinematics() method with TargetMode.ROBOT, the last link of the robot's main group is used. + frame_pcf_wcf = planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT) + + print("Last link name: {}".format(robot_cell.get_end_effector_link_name())) + print("PCF Frame relative to the WCF: {}".format(frame_pcf_wcf)) + +""" +Output: + +>>> Last link name: tool0 +>>> PCF Frame relative to the WCF: Frame(point=Point(x=0.300, y=0.100, z=0.500), xaxis=Vector(x=-0.000, y=-1.000, z=0.000), yaxis=Vector(x=-0.000, y=-0.000, z=-1.000)) +""" diff --git a/docs/backends/ros/files/03_forward_kinematics_target_mode.py b/docs/backends/ros/files/03_forward_kinematics_target_mode.py new file mode 100644 index 0000000000..0406e0c40d --- /dev/null +++ b/docs/backends/ros/files/03_forward_kinematics_target_mode.py @@ -0,0 +1,50 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Loading RobotCell from RobotCellLibrary with tool and workpiece + robot_cell, robot_cell_state = RobotCellLibrary.ur5_gripper_one_beam(client) + + # Load RobotCell from ROS MoveIt backend and assert that the robot model is the same + client.load_robot_cell(False) + assert client.robot_cell.root_name == robot_cell.root_name + + # Set the tool and workpiece in the ROS MoveIt backend + planner.set_robot_cell(robot_cell) + + # The default robot cell state already have the gripper and beam attached + # We modify the robot's configuration here to a specific joint configuration + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + robot_cell_state.rigid_body_states["beam"].attachment_frame = Frame( + [0.0, 0.0, -0.1], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0] + ) + + # Note that the values for these frames are not the same. + print("Robot Planner Coordinate Frame (PCF) relative to the world coordinate system (WCF):") + frame = planner.forward_kinematics(robot_cell_state, TargetMode.ROBOT) + print("- {}".format(frame)) + + print("Tool Coordinate Frame (TCF) relative to the world coordinate system (WCF):") + frame = planner.forward_kinematics(robot_cell_state, TargetMode.TOOL) + print("- {}".format(frame)) + + print("Workpiece's Object Coordinate Frame (OCF) relative to the world coordinate system (WCF):") + frame = planner.forward_kinematics(robot_cell_state, TargetMode.WORKPIECE) + print("- {}".format(frame)) + +""" +Output: + +>>> Robot Planner Coordinate Frame (PCF) relative to the world coordinate system (WCF): +>>> - Frame(point=Point(x=0.300, y=0.100, z=0.500), xaxis=Vector(x=-0.000, y=-1.000, z=0.000), yaxis=Vector(x=-0.000, y=-0.000, z=-1.000)) +>>> Tool Coordinate Frame (TCF) relative to the world coordinate system (WCF): +>>> - Frame(point=Point(x=0.350, y=0.100, z=0.500), xaxis=Vector(x=1.000, y=-0.000, z=-0.000), yaxis=Vector(x=-0.000, y=-1.000, z=0.000)) +>>> Workpiece's Object Coordinate Frame (OCF) relative to the world coordinate system (WCF): +>>> - Frame(point=Point(x=0.350, y=0.100, z=0.600), xaxis=Vector(x=1.000, y=-0.000, z=-0.000), yaxis=Vector(x=-0.000, y=-1.000, z=0.000)) +""" diff --git a/docs/backends/ros/files/03_forward_kinematics_to_link.py b/docs/backends/ros/files/03_forward_kinematics_to_link.py new file mode 100644 index 0000000000..18e3f3e6db --- /dev/null +++ b/docs/backends/ros/files/03_forward_kinematics_to_link.py @@ -0,0 +1,14 @@ +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient + +with RosClient() as client: + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + planner = MoveItPlanner(client) + + robot_cell_state = robot_cell.default_cell_state() + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + + for link_name in robot_cell.get_link_names(): + frame_lcf_wcf = planner.forward_kinematics_to_link(robot_cell_state, link_name) + print("Link ({}) LCF relative to the WCF: {}".format(link_name, frame_lcf_wcf)) diff --git a/docs/backends/ros/files/04_ik.py b/docs/backends/ros/files/04_ik.py new file mode 100644 index 0000000000..f38475c735 --- /dev/null +++ b/docs/backends/ros/files/04_ik.py @@ -0,0 +1,39 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Create a default RobotCellState from the RobotCell as the starting state + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + start_state = robot_cell.default_cell_state() + + # Create a target frame with the ROBOT mode + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + print("Target frame with Robot Mode: {}".format(target)) + + # Calculate inverse kinematics + configuration = planner.inverse_kinematics(target, start_state) + print("Configuration 1: ", configuration) + + # Try a slightly different target frame + target.target_frame.point.z += 0.1 + # Use the found configuration as the starting point for the second IK calculation + start_state.robot_configuration.merge(configuration) + configuration = planner.inverse_kinematics(target, start_state) + print("Configuration 2: ", configuration) + +""" +Output: +>>> Target frame with Robot Mode: FrameTarget(Frame(point=Point(x=0.300, y=0.100, z=0.500), xaxis=Vector(x=1.000, y=0.000, z=0.000), yaxis=Vector(x=0.000, y=1.000, z=0.000)), ROBOT) +>>> Configuration 1: Configuration((-0.031, -2.025, 2.161, 4.576, -4.712, -1.540), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', +>>> 'wrist_2_joint', 'wrist_3_joint')) +>>> Configuration 2: Configuration((-0.031, -2.027, 1.907, 4.833, -4.712, -1.540), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', +>>> 'wrist_2_joint', 'wrist_3_joint')) +""" diff --git a/docs/backends/ros/files/04_ik_allow_collision.py b/docs/backends/ros/files/04_ik_allow_collision.py new file mode 100644 index 0000000000..b5de465f32 --- /dev/null +++ b/docs/backends/ros/files/04_ik_allow_collision.py @@ -0,0 +1,52 @@ +from compas.geometry import Box +from compas.geometry import Frame + +from compas_fab.backends import InverseKinematicsError +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import RigidBody +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Create a default RobotCellState from the RobotCell as the starting state + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + + # Add a collision geometry to the robot cell + box_mesh = Box(0.1, 0.1, 0.6).to_mesh(triangulated=True) + robot_cell.rigid_body_models["box"] = RigidBody.from_mesh(box_mesh) + start_state = robot_cell.default_cell_state() + start_state.rigid_body_states["box"].frame.point = [0.3, 0.1, 0.3] + planner.set_robot_cell(robot_cell, start_state) + + # Create a target frame with the ROBOT mode + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + print("Target frame with Robot Mode: {}".format(target)) + + # Calculate inverse kinematics + def calculate(options): + try: + configuration = planner.inverse_kinematics(target, start_state, options=options) + print(" Found configuration: ", configuration) + except InverseKinematicsError as e: + print(" Failed to find a configuration: ", e) + + print("Inverse kinematics with collision check enabled: (by default)") + calculate({}) + + print("Inverse kinematics with collision check disabled:") + options = {"allow_collision": True} + calculate(options) + +""" +Output: +>>> Target frame with Robot Mode: FrameTarget(Frame(point=Point(x=0.300, y=0.100, z=0.500), xaxis=Vector(x=1.000, y=0.000, z=0.000), yaxis=Vector(x=0.000, y=1.000, z=0.000)), ROBOT) +>>> Inverse kinematics with collision check enabled: (by default) +>>> Failed to find a configuration: No inverse kinematics solution found. +>>> Inverse kinematics with collision check disabled: +>>> Found configuration: Configuration((-0.031, -2.025, 2.161, 4.576, -4.712, -1.540), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +""" diff --git a/docs/backends/ros/files/04_ik_cache.py b/docs/backends/ros/files/04_ik_cache.py new file mode 100644 index 0000000000..2e9ff51276 --- /dev/null +++ b/docs/backends/ros/files/04_ik_cache.py @@ -0,0 +1,35 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + start_state = robot_cell.default_cell_state() + + # Create a target frame with the ROBOT mode + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + options = {"max_results": 100} + + # First Call to Inverse Kinematics + configuration = planner.inverse_kinematics(target, start_state, options=options) + print("Configuration 1: ", configuration) + + # Subsequent Call to Inverse Kinematics with exactly the same input + configuration = planner.inverse_kinematics(target, start_state, options=options) + print("Configuration 2: ", configuration) + configuration = planner.inverse_kinematics(target, start_state, options=options) + print("Configuration 3: ", configuration) + +""" +Output: (Second and third configuration is random) +>>> Configuration 1: Configuration((-0.031, -2.025, 2.161, 4.576, -4.712, -1.540), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> Configuration 2: Configuration((3.816, -1.117, -2.161, -1.434, 4.712, 4.038), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> Configuration 3: Configuration((-0.031, -0.013, -2.161, 0.604, 1.571, 4.743), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +""" diff --git a/docs/backends/ros/files/04_ik_full_config.py b/docs/backends/ros/files/04_ik_full_config.py new file mode 100644 index 0000000000..d9c54d9656 --- /dev/null +++ b/docs/backends/ros/files/04_ik_full_config.py @@ -0,0 +1,27 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Create a default RobotCellState from the RobotCell as the starting state + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + start_state = robot_cell.default_cell_state() + + # Create a target frame with the ROBOT mode + target = FrameTarget(Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]), TargetMode.ROBOT) + print("Target frame with Robot Mode: {}".format(target)) + + # Calculate inverse kinematics + configuration = planner.inverse_kinematics(target, start_state) + print("Found configuration 1: ", configuration) + + # Calculate inverse kinematics with full configuration + options = {"return_full_configuration": True} + configuration = planner.inverse_kinematics(target, start_state, options=options) + print("Found configuration 2: ", configuration) diff --git a/docs/backends/ros/files/04_ik_target_mode.py b/docs/backends/ros/files/04_ik_target_mode.py new file mode 100644 index 0000000000..a93e7cde25 --- /dev/null +++ b/docs/backends/ros/files/04_ik_target_mode.py @@ -0,0 +1,41 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Load robot from moveit to check the robot model name + native_robot_cell = client.load_robot_cell() + + # Load existing robot cell from the library + robot_cell, start_state = RobotCellLibrary.ur5_gripper_one_beam() + assert robot_cell.robot_model.name == native_robot_cell.robot_model.name == "ur5_robot" + planner.set_robot_cell(robot_cell) + + # Target using Planner Coordinate Frame (PCF) + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + print("\nTarget: {}".format(target)) + configuration = planner.inverse_kinematics(target, start_state) + print("Configuration: {} ".format(configuration)) + + # Target using Tool Coordinate Frame (TCF) + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.TOOL) + print("\nTarget: {}".format(target)) + configuration = planner.inverse_kinematics(target, start_state) + print("Configuration: {} ".format(configuration)) + + # Target using Workpiece Object Coordinate Frame (OCF) + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.WORKPIECE) + print("\nTarget: {}".format(target)) + configuration = planner.inverse_kinematics(target, start_state) + print("Configuration: {} ".format(configuration)) + + # Notice the difference in the resulting configurations diff --git a/docs/backends/ros/files/04_ik_unreachable.py b/docs/backends/ros/files/04_ik_unreachable.py new file mode 100644 index 0000000000..b0ced54791 --- /dev/null +++ b/docs/backends/ros/files/04_ik_unreachable.py @@ -0,0 +1,30 @@ +from compas.geometry import Frame + +from compas_fab.backends import InverseKinematicsError +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Create a default RobotCellState from the RobotCell as the starting state + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + start_state = robot_cell.default_cell_state() + + # Create a target frame far away that is out of reach + frame_WCF = Frame([2.0, 1.0, 1.0], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + + # Calculate inverse kinematics + try: + configuration = planner.inverse_kinematics(target, start_state) + except InverseKinematicsError as e: + print("Failed to find a configuration: ", e) + +""" +Output: +>>> Failed to find a configuration: No inverse kinematics solution found. +""" diff --git a/docs/backends/ros/files/04_iter_ik.py b/docs/backends/ros/files/04_iter_ik.py new file mode 100644 index 0000000000..7c73d1eee0 --- /dev/null +++ b/docs/backends/ros/files/04_iter_ik.py @@ -0,0 +1,40 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Create a default RobotCellState from the RobotCell as the starting state + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + start_state = robot_cell.default_cell_state() + + # Create a target frame with the ROBOT mode + frame_WCF = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) + target = FrameTarget(frame_WCF, TargetMode.ROBOT) + print("Target frame with Robot Mode: {}".format(target)) + + # User iter inverse kinematics to find multiple solutions + configuration_generator = planner.iter_inverse_kinematics(target, start_state) + for i in range(10): + configuration = next(configuration_generator) + print("IK Solution {}: {}".format(i, configuration)) + +""" +Output: +>>> Target frame with Robot Mode: FrameTarget(Frame(point=Point(x=0.300, y=0.100, z=0.500), xaxis=Vector(x=1.000, y=0.000, z=0.000), yaxis=Vector(x=0.000, y=1.000, z=0.000)), ROBOT) +>>> IK Solution 0: Configuration((-0.031, -2.025, 2.161, 4.576, -4.712, -1.540), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 1: Configuration((3.816, 3.155, 2.161, -3.746, -1.571, -2.245), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 2: Configuration((-2.467, -1.117, -2.161, -1.434, -1.571, -2.245), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 3: Configuration((-0.031, -2.025, 2.161, -1.707, -4.712, -1.540), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 4: Configuration((-0.031, -6.135, -1.793, -3.068, -1.571, -4.682), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 5: Configuration((6.253, 0.148, -1.793, -3.068, 4.712, -4.682), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 6: Configuration((-0.031, -0.013, -2.161, 0.604, -4.712, -1.540), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 7: Configuration((-2.467, 5.167, -2.161, 4.849, 4.712, -2.245), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 8: Configuration((-0.031, 0.148, -1.793, 3.215, -1.571, 1.601), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +>>> IK Solution 9: Configuration((3.816, -1.117, -2.161, 4.849, -1.571, -2.245), (0, 0, 0, 0, 0, 0), ('shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint')) +""" diff --git a/docs/backends/ros/files/04_iter_ik_unique.py b/docs/backends/ros/files/04_iter_ik_unique.py new file mode 100644 index 0000000000..9c558e3a9a --- /dev/null +++ b/docs/backends/ros/files/04_iter_ik_unique.py @@ -0,0 +1,23 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Create a default RobotCellState from the RobotCell as the starting state + robot_cell = client.load_robot_cell() + assert robot_cell.robot_model.name == "ur5_robot" + start_state = robot_cell.default_cell_state() + + # Create a target frame with the ROBOT mode + target = FrameTarget(Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]), TargetMode.ROBOT) + options = {"max_results": 200} + + # Showing all unique solutions + configuration_generator = planner.iter_inverse_kinematics(target, start_state) + for i, configuration in enumerate(configuration_generator): + print("Unique configuration {}: {}".format(i, configuration)) diff --git a/docs/backends/ros/files/05_plan_motion.py b/docs/backends/ros/files/05_plan_motion.py new file mode 100644 index 0000000000..fd624ec90d --- /dev/null +++ b/docs/backends/ros/files/05_plan_motion.py @@ -0,0 +1,42 @@ + +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + assert robot_cell.robot_model.name == "ur5_robot" + + # Define Starting State + start_state = robot_cell.default_cell_state() + start_state.robot_configuration.joint_values = [-3.53, 3.83, -0.58, -3.33, 4.76, 0.00] + + # Create FrameTarget + frame = Frame([0.4, 0.3, 0.4], [0, 1, 0], [0, 0, 1]) + target = FrameTarget(frame, TargetMode.ROBOT) + + trajectory = planner.plan_motion(target, start_state) + + print("Trajectory contains %d configurations (JointTrajectoryPoint)." % len(trajectory.points)) + print("Executing this path at full speed would take approx. %.3f seconds." % trajectory.time_from_start) + + # Print the JointTrajectoryPoint objects in the trajectory + for point in trajectory.points: + print(point) +""" +Output: (may vary) +>>> Trajectory contains 8 configurations (JointTrajectoryPoint). +>>> Executing this path at full speed would take approx. 2.377 seconds. +>>> JointTrajectoryPoint((-3.530, 3.830, -0.580, -3.330, 4.760, 0.000), (0, 0, 0, 0, 0, 0), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), (0.995, 0.000, 0.000, 0.000, 0.000, 0.000), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(0, 0)) +>>> JointTrajectoryPoint((-3.330, 3.946, -0.723, -3.269, 4.903, -0.007), (0, 0, 0, 0, 0, 0), (0.531, 0.310, -0.381, 0.162, 0.379, -0.019), (0.957, 0.558, -0.687, 0.293, 0.684, -0.034), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(0, 633561529)) +>>> JointTrajectoryPoint((-3.131, 4.063, -0.866, -3.208, 5.045, -0.014), (0, 0, 0, 0, 0, 0), (0.858, 0.500, -0.616, 0.262, 0.613, -0.031), (0.945, 0.551, -0.678, 0.289, 0.675, -0.034), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(0, 901041171)) +>>> JointTrajectoryPoint((-2.931, 4.179, -1.010, -3.147, 5.188, -0.021), (0, 0, 0, 0, 0, 0), (1.054, 0.615, -0.756, 0.322, 0.753, -0.037), (0.884, 0.515, -0.634, 0.270, 0.631, -0.031), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 106861057)) +>>> JointTrajectoryPoint((-2.731, 4.296, -1.153, -3.086, 5.330, -0.028), (0, 0, 0, 0, 0, 0), (1.059, 0.617, -0.759, 0.324, 0.756, -0.038), (-0.839, -0.489, 0.602, -0.256, -0.599, 0.030), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 282238794)) +>>> JointTrajectoryPoint((-2.532, 4.412, -1.296, -3.025, 5.473, -0.035), (0, 0, 0, 0, 0, 0), (0.872, 0.508, -0.625, 0.266, 0.622, -0.031), (-0.925, -0.539, 0.663, -0.283, -0.660, 0.033), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 486109291)) +>>> JointTrajectoryPoint((-2.332, 4.529, -1.439, -2.964, 5.615, -0.043), (0, 0, 0, 0, 0, 0), (0.541, 0.315, -0.388, 0.165, 0.386, -0.019), (-1.004, -0.585, 0.720, -0.307, -0.717, 0.036), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 747366937)) +>>> JointTrajectoryPoint((-2.132, 4.645, -1.582, -2.903, 5.758, -0.050), (0, 0, 0, 0, 0, 0), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), (-1.008, -0.588, 0.723, -0.308, -0.719, 0.036), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(2, 376883245)) +""" diff --git a/docs/backends/ros/files/05_plan_motion_configuration.py b/docs/backends/ros/files/05_plan_motion_configuration.py new file mode 100644 index 0000000000..6367291bc7 --- /dev/null +++ b/docs/backends/ros/files/05_plan_motion_configuration.py @@ -0,0 +1,41 @@ + + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import ConfigurationTarget + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + assert robot_cell.robot_model.name == "ur5_robot" + + # Define Starting State + start_state = robot_cell.default_cell_state() + start_state.robot_configuration.joint_values = [-3.53, 3.83, -0.58, -3.33, 4.76, 0.00] + + # Create FrameTarget + target_configuration = robot_cell.zero_full_configuration() + target_configuration.joint_values = [-2.132, 4.645, -1.582, -2.903, 5.758, -0.050] + target = ConfigurationTarget(target_configuration, tolerance_above=0.001, tolerance_below=0.001) + + trajectory = planner.plan_motion(target, start_state) + + print("Trajectory contains %d configurations (JointTrajectoryPoint)." % len(trajectory.points)) + print("Executing this path at full speed would take approx. %.3f seconds." % trajectory.time_from_start) + + # Print the JointTrajectoryPoint objects in the trajectory + for point in trajectory.points: + print(point) +""" +Output: (may vary) +>>> Trajectory contains 8 configurations (JointTrajectoryPoint). +>>> Executing this path at full speed would take approx. 2.377 seconds. +>>> JointTrajectoryPoint((-3.530, 3.830, -0.580, -3.330, 4.760, 0.000), (0, 0, 0, 0, 0, 0), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), (0.995, 0.000, 0.000, 0.000, 0.000, 0.000), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(0, 0)) +>>> JointTrajectoryPoint((-3.330, 3.946, -0.723, -3.269, 4.902, -0.007), (0, 0, 0, 0, 0, 0), (0.531, 0.310, -0.381, 0.162, 0.379, -0.019), (0.958, 0.558, -0.686, 0.292, 0.683, -0.035), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(0, 633592153)) +>>> JointTrajectoryPoint((-3.130, 4.063, -0.866, -3.208, 5.045, -0.014), (0, 0, 0, 0, 0, 0), (0.859, 0.501, -0.615, 0.262, 0.612, -0.031), (0.944, 0.551, -0.677, 0.288, 0.674, -0.034), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(0, 901111462)) +>>> JointTrajectoryPoint((-2.931, 4.179, -1.010, -3.147, 5.187, -0.022), (0, 0, 0, 0, 0, 0), (1.054, 0.615, -0.756, 0.322, 0.752, -0.038), (0.882, 0.514, -0.633, 0.269, 0.629, -0.032), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 106995939)) +>>> JointTrajectoryPoint((-2.731, 4.296, -1.153, -3.086, 5.330, -0.029), (0, 0, 0, 0, 0, 0), (1.059, 0.618, -0.759, 0.323, 0.755, -0.038), (-0.838, -0.488, 0.600, -0.256, -0.597, 0.030), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 282458174)) +>>> JointTrajectoryPoint((-2.531, 4.412, -1.296, -3.025, 5.472, -0.036), (0, 0, 0, 0, 0, 0), (0.872, 0.508, -0.625, 0.266, 0.622, -0.032), (-0.925, -0.539, 0.663, -0.282, -0.660, 0.034), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 486375649)) +>>> JointTrajectoryPoint((-2.331, 4.529, -1.439, -2.964, 5.615, -0.043), (0, 0, 0, 0, 0, 0), (0.541, 0.315, -0.388, 0.165, 0.386, -0.020), (-1.004, -0.585, 0.720, -0.307, -0.716, 0.036), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(1, 747682667)) +>>> JointTrajectoryPoint((-2.132, 4.645, -1.582, -2.903, 5.757, -0.051), (0, 0, 0, 0, 0, 0), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), (-1.008, -0.588, 0.723, -0.308, -0.719, 0.037), (0.000, 0.000, 0.000, 0.000, 0.000, 0.000), Duration(2, 377232469)) +""" diff --git a/docs/backends/ros/files/05_plan_motion_tolerance.py b/docs/backends/ros/files/05_plan_motion_tolerance.py new file mode 100644 index 0000000000..03da72c025 --- /dev/null +++ b/docs/backends/ros/files/05_plan_motion_tolerance.py @@ -0,0 +1,54 @@ + +from compas.geometry import Frame +from compas.geometry import axis_angle_from_quaternion + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import TargetMode + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + assert robot_cell.robot_model.name == "ur5_robot" + + # Define Starting State + start_state = robot_cell.default_cell_state() + start_state.robot_configuration.joint_values = [-3.53, 3.83, -0.58, -3.33, 4.76, 0.00] + + # Plan motion and validate the final position error + def plan_and_validate(target): + # type: (FrameTarget) -> None + trajectory = planner.plan_motion(target, start_state) + print("Trajectory contains %d configurations (JointTrajectoryPoint)." % len(trajectory.points)) + end_state = start_state.copy() + end_state.robot_configuration.joint_values = trajectory.points[-1].joint_values + robot_frame = planner.forward_kinematics(end_state, TargetMode.ROBOT) + print(" Final Pose: {}".format(robot_frame)) + position_error = target.target_frame.point.distance_to_point(robot_frame.point) + print(" Distance error: {} (meter)".format(position_error)) + delta_frame = robot_frame.to_local_coordinates(target.target_frame) # type: Frame + _, orientation_error = axis_angle_from_quaternion(delta_frame.quaternion) + print(" Orientation error: {} (radians)".format(orientation_error)) + + # Create FrameTarget with default tolerance and plan + target_frame = Frame([0.4, 0.3, 0.4], [0, 1, 0], [0, 0, 1]) + target = FrameTarget(target_frame, TargetMode.ROBOT) + plan_and_validate(target) + + # Set target tolerance to tighter values and plan again + target.tolerance_position = 0.0001 # Unit is meters (default is 0.001) + target.tolerance_orientation = 0.001 # Unit is radians (default is 0.01) + plan_and_validate(target) + +""" +Output: (may vary) +>>> Trajectory contains 8 configurations (JointTrajectoryPoint). +>>> Final Pose: Frame(point=Point(x=0.400, y=0.300, z=0.400), xaxis=Vector(x=-0.001, y=1.000, z=0.007), yaxis=Vector(x=-0.004, y=-0.007, z=1.000)) +>>> Distance error: 0.0005173194587181946 (meter) +>>> Orientation error: 0.00808542020121989 (radians) +>>> Trajectory contains 8 configurations (JointTrajectoryPoint). +>>> Final Pose: Frame(point=Point(x=0.400, y=0.300, z=0.400), xaxis=Vector(x=-0.000, y=1.000, z=0.001), yaxis=Vector(x=0.001, y=-0.001, z=1.000)) +>>> Distance error: 4.790417904127647e-05 (meter) +>>> Orientation error: 0 (radians) +""" diff --git a/docs/backends/ros/files/05_plan_motion_with_attachment.py b/docs/backends/ros/files/05_plan_motion_with_attachment.py new file mode 100644 index 0000000000..761f883df0 --- /dev/null +++ b/docs/backends/ros/files/05_plan_motion_with_attachment.py @@ -0,0 +1,44 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + + # Use a predefined robot cell with a gripper and a single beam + robot_cell, start_state = RobotCellLibrary.ur5_gripper_one_beam() + planner.set_robot_cell(robot_cell) + + # Define Starting Configuration + start_state.robot_configuration.joint_values = [-3.53, 3.83, -0.58, -3.33, 4.76, 0.00] + + # Create FrameTarget for the workpiece + target_frame = Frame([0.4, 0.1, 0.6], [1, 0, 0], [0, -1, 0]) + target = FrameTarget(target_frame, TargetMode.WORKPIECE) + + trajectory = planner.plan_motion(target, start_state) + + print("Trajectory contains %d configurations (JointTrajectoryPoint)." % len(trajectory.points)) + final_joint_values = trajectory.points[-1].joint_values + print(" Joint values of the final trajectory point: {}".format(final_joint_values)) + + # Validate the pose of the workpiece after planning + end_state = start_state.copy() + end_state.robot_configuration.joint_values = final_joint_values + workpiece_frame = planner.forward_kinematics(end_state, TargetMode.WORKPIECE) + + print("Workpiece's Object Coordinate Frame (OCF): {}".format(workpiece_frame)) + planning_error = workpiece_frame.point.distance_to_point(target_frame.point) + print(" Target distance error: {} (meter)".format(planning_error)) + +""" +Output: (may vary) +>>> Trajectory contains 11 configurations (JointTrajectoryPoint). +>>> Joint values of the final trajectory point: [-2.4166840511621435, 4.937602986182445, -1.317040437371621, -3.5540095964662965, 5.338763175705421, -3.149330570899709] +>>> Workpiece's Object Coordinate Frame (OCF): Frame(point=Point(x=0.398, y=0.103, z=0.597), xaxis=Vector(x=0.994, y=0.097, z=0.054), yaxis=Vector(x=0.099, y=-0.995, z=-0.031)) +>>> Target distance error: 0.004860285596193909 (meter) +""" diff --git a/docs/backends/ros/files/05_plan_motion_with_obstacle.py b/docs/backends/ros/files/05_plan_motion_with_obstacle.py new file mode 100644 index 0000000000..72a5336685 --- /dev/null +++ b/docs/backends/ros/files/05_plan_motion_with_obstacle.py @@ -0,0 +1,39 @@ +from compas.geometry import Box +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import RigidBody +from compas_fab.robots import TargetMode + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + assert robot_cell.robot_model.name == "ur5_robot" + + # Add an obstacle + box_mesh = Box(0.1, 0.1, 1.2).to_mesh(triangulated=True) + robot_cell.rigid_body_models["box"] = RigidBody.from_mesh(box_mesh) + planner.set_robot_cell(robot_cell) + + # Define Starting State + start_state = robot_cell.default_cell_state() + start_state.robot_configuration.joint_values = [-3.53, 3.83, -0.58, -3.33, 4.76, 0.00] + # Set the obstacle's position + start_state.rigid_body_states["box"].frame.point = [0.3, 0.0, 0.0] + + # Create FrameTarget + frame = Frame([0.4, 0.3, 0.4], [0, 1, 0], [0, 0, 1]) + target = FrameTarget(frame, TargetMode.ROBOT) + + trajectory = planner.plan_motion(target, start_state) + + print("Trajectory contains %d configurations (JointTrajectoryPoint)." % len(trajectory.points)) + print("Executing this path at full speed would take approx. %.3f seconds." % trajectory.time_from_start) + +""" +Output: (may vary) +>>> Trajectory contains 79 configurations (JointTrajectoryPoint). +>>> Executing this path at full speed would take approx. 19.415 seconds. +""" diff --git a/docs/backends/ros/files/06_cartesian_motion.py b/docs/backends/ros/files/06_cartesian_motion.py new file mode 100644 index 0000000000..cc21b551fc --- /dev/null +++ b/docs/backends/ros/files/06_cartesian_motion.py @@ -0,0 +1,32 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + robot_cell = client.load_robot_cell() + robot_cell_state = robot_cell.default_cell_state() + + assert robot_cell.robot_model.name == "ur5_robot" + + frames = [] + frames.append(Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0])) + frames.append(Frame([0.5, 0.1, 0.6], [1, 0, 0], [0, 1, 0])) + waypoints = FrameWaypoints(frames, TargetMode.ROBOT) + + robot_cell_state.robot_configuration.joint_values = (-0.042, 0.033, -2.174, 5.282, -1.528, 0.000) + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state) + + print("Computed cartesian path with %d configurations, " % len(trajectory.points)) + print("following %d%% of requested trajectory." % (trajectory.fraction * 100)) + print("Executing this path at full speed would take approx. %.3f seconds." % trajectory.time_from_start) + +""" +Output: (may vary) +>>> Computed cartesian path with 84 configurations, +>>> following 100% of requested trajectory. +>>> Executing this path at full speed would take approx. 5.472 seconds. +""" diff --git a/docs/backends/ros/files/06_cartesian_motion_partial.py b/docs/backends/ros/files/06_cartesian_motion_partial.py new file mode 100644 index 0000000000..f5faad6c56 --- /dev/null +++ b/docs/backends/ros/files/06_cartesian_motion_partial.py @@ -0,0 +1,49 @@ +from compas.geometry import Box +from compas.geometry import Frame + +from compas_fab.backends import MotionPlanningError +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import RigidBody +from compas_fab.robots import TargetMode + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + assert robot_cell.robot_model.name == "ur5_robot" + + # Add an obstacle + box_mesh = Box(0.1, 0.1, 1.2).to_mesh(triangulated=True) + robot_cell.rigid_body_models["box"] = RigidBody.from_mesh(box_mesh) + planner.set_robot_cell(robot_cell) + + # Define Starting State + start_state = robot_cell.default_cell_state() + start_state.robot_configuration.joint_values = [-3.53, 3.83, -0.58, -3.33, 4.76, 0.00] + # Set the obstacle's position + start_state.rigid_body_states["box"].frame.point = [0.3, 0.0, 0.0] + + # Create waypoints + frame = Frame([0.4, 0.3, 0.4], [0, 1, 0], [0, 0, 1]) + waypoints = FrameWaypoints([frame], TargetMode.ROBOT) + + # This planning is expected to fail because it's impossible to follow the trajectory + # in a linear motion, so the planner will fail but return a partial trajectory. + try: + trajectory = planner.plan_cartesian_motion(waypoints, start_state) + print(f"Trajectory fraction: {trajectory.fraction}") + print(f"Trajectory contains {len(trajectory.points)} configurations (JointTrajectoryPoint).") + print(f"Executing this path at full speed would take approx. {trajectory.time_from_start} seconds.") + except MotionPlanningError as e: + print(f"Motion planning failed: {e}") + print(f"Trajectory fraction: {e.partial_trajectory.fraction}") + print(f"Trajectory contains {len(e.partial_trajectory.points)} configurations (JointTrajectoryPoint).") + + +""" +Output: (may vary) +>>> Motion planning failed: Motion planning failed +>>> Trajectory fraction: 0.14285714285714285 +>>> Trajectory contains 12 configurations (JointTrajectoryPoint). +""" diff --git a/docs/backends/ros/files/06_cartesian_motion_step_size.py b/docs/backends/ros/files/06_cartesian_motion_step_size.py new file mode 100644 index 0000000000..897896d67e --- /dev/null +++ b/docs/backends/ros/files/06_cartesian_motion_step_size.py @@ -0,0 +1,35 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + robot_cell = client.load_robot_cell() + robot_cell_state = robot_cell.default_cell_state() + + assert robot_cell.robot_model.name == "ur5_robot" + + # One straight segment + frames = [] + frames.append(Frame([0.3, 0.3, 0.5], [0, -1, 0], [0, 0, -1])) + waypoints = FrameWaypoints(frames, TargetMode.ROBOT) + + robot_cell_state.robot_configuration.joint_values = (-0.042, 0.033, -2.174, 5.282, -1.528, 0.000) + + options = {"max_step": 0.01} # Units in meters + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state, options=options) + print("Path (max_step=0.01) has %d configurations. " % len(trajectory.points)) + + options = {"max_step": 0.001} # Units in meters + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state, options=options) + print("Path (max_step=0.001) has %d configurations. " % len(trajectory.points)) + + +""" +Output: (may vary) +>>> Path (max_step=0.01) has 21 configurations. +>>> Path (max_step=0.001) has 201 configurations. +""" diff --git a/docs/backends/ros/files/06_cartesian_motion_target_mode.py b/docs/backends/ros/files/06_cartesian_motion_target_mode.py new file mode 100644 index 0000000000..2b6a84e241 --- /dev/null +++ b/docs/backends/ros/files/06_cartesian_motion_target_mode.py @@ -0,0 +1,50 @@ +from compas.geometry import Frame + +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import RobotCellLibrary +from compas_fab.robots import TargetMode + +with RosClient() as client: + planner = MoveItPlanner(client) + # Loading RobotCell from RobotCellLibrary with tool and workpiece + robot_cell, robot_cell_state = RobotCellLibrary.ur5_gripper_one_beam(client) + + # Load RobotCell from ROS MoveIt backend and assert that the robot model is the same + client.load_robot_cell(False) + assert client.robot_cell.root_name == robot_cell.root_name + + # Set the tool and workpiece in the ROS MoveIt backend + planner.set_robot_cell(robot_cell) + + # The default robot cell state already have the gripper and beam attached + # We modify the robot's configuration here to a specific joint configuration + # The OCF is = Frame([0.35, 0.1, 0.5], [1, 0, 0], [0, -1, 0]) + robot_cell_state.robot_configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] + + start_frame = planner.forward_kinematics(robot_cell_state, TargetMode.WORKPIECE) + print("Workpiece's OCF at the starting state:\n {}".format(start_frame)) + + # The following waypoints will make the object move in a square + frames = [] + frames.append(Frame([0.35, 0.3, 0.5], [1, 0, 0], [0, -1, 0])) + frames.append(Frame([0.35, 0.3, 0.7], [1, 0, 0], [0, -1, 0])) + frames.append(Frame([0.35, 0.1, 0.7], [1, 0, 0], [0, -1, 0])) + frames.append(start_frame) # Goes back to the starting position + waypoints = FrameWaypoints(frames, TargetMode.WORKPIECE) + + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state) + + print("Computed cartesian path with %d configurations, " % len(trajectory.points)) + print("following %d%% of requested trajectory." % (trajectory.fraction * 100)) + print("Executing this path at full speed would take approx. %.3f seconds." % trajectory.time_from_start) + +""" +Output: (may vary) +>>> Workpiece's OCF at the starting state: +>>> Frame(point=Point(x=0.350, y=0.100, z=0.500), xaxis=Vector(x=1.000, y=-0.000, z=-0.000), yaxis=Vector(x=-0.000, y=-1.000, z=0.000)) +>>> Computed cartesian path with 84 configurations, +>>> following 100% of requested trajectory. +>>> Executing this path at full speed would take approx. 6.012 seconds. +""" diff --git a/docs/examples/03_backends_ros/files/01_ros_hello_world_listener.py b/docs/backends/ros/files/10_ros_hello_world_listener.py similarity index 99% rename from docs/examples/03_backends_ros/files/01_ros_hello_world_listener.py rename to docs/backends/ros/files/10_ros_hello_world_listener.py index b5bef09032..1ed3d11129 100644 --- a/docs/examples/03_backends_ros/files/01_ros_hello_world_listener.py +++ b/docs/backends/ros/files/10_ros_hello_world_listener.py @@ -1,6 +1,7 @@ import time from roslibpy import Topic + from compas_fab.backends import RosClient diff --git a/docs/examples/03_backends_ros/files/01_ros_hello_world_talker.py b/docs/backends/ros/files/10_ros_hello_world_talker.py similarity index 100% rename from docs/examples/03_backends_ros/files/01_ros_hello_world_talker.py rename to docs/backends/ros/files/10_ros_hello_world_talker.py diff --git a/docs/backends/ros/files/Construct Robot Cell.py b/docs/backends/ros/files/Construct Robot Cell.py new file mode 100644 index 0000000000..c332570665 --- /dev/null +++ b/docs/backends/ros/files/Construct Robot Cell.py @@ -0,0 +1,105 @@ + +from compas.datastructures import Mesh +from compas.geometry import Box +from compas.geometry import Frame + +import compas_fab +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import FrameTarget +from compas_fab.robots import FrameWaypoints +from compas_fab.robots import RigidBody +from compas_fab.robots import RigidBodyLibrary +from compas_fab.robots import TargetMode +from compas_fab.robots import ToolLibrary + +target_frames = [] +target_frames.append(Frame([0.4, 0.3, 0.45], [0, 1, 0], [-1, 0, 0])) +target_frames.append(Frame([0.4, 0.0, 0.45], [0, 1, 0], [-1, 0, 0])) +target_frames.append(Frame([0.4, -0.4, 0.45], [1, 0, 0], [0, 1, 0])) +target_frames.append(Frame([0.0, -0.4, 0.45], [1, 0, 0], [0, 1, 0])) + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + + # ===================================== + # Step 1: Creation of Robot Cell + # ===================================== + + # Add a rigid body for the floor + floor_mesh = Mesh.from_stl(compas_fab.get("planning_scene/floor.stl")) + robot_cell.rigid_body_models["floor"] = RigidBody.from_mesh(floor_mesh) + # Add a gripper tool + gripper = ToolLibrary.kinematic_gripper() + robot_cell.tool_models[gripper.name] = gripper + # Add a long bar material as workpiece + bar_mesh = Box(0.05, 0.05, 0.6).to_mesh(triangulated=True) + robot_cell.rigid_body_models["bar"] = RigidBody.from_mesh(bar_mesh) + # Add a target marker + target_marker = RigidBodyLibrary.target_marker(0.2) + # MoveIt RViz GUI typically does not show objects without collision meshes, so + # we set the collision meshes to be the same as the visual meshes + target_marker.collision_meshes = target_marker.visual_meshes + for i, frame in enumerate(target_frames): + robot_cell.rigid_body_models["target_%d" % i] = target_marker.copy() + + # ===================================== + # Step 2: Specify the Robot Cell State + # ===================================== + + # Attach the gripper to the robot + robot_cell_state = robot_cell.default_cell_state() + robot_cell_state.set_tool_attached_to_group( + gripper.name, + robot_cell.main_group_name, + attachment_frame=Frame([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]), + touch_links=["wrist_3_link"], # This is the link that the tool is attached to + ) + # Attach the workpiece to the tool + robot_cell_state.set_rigid_body_attached_to_tool( + "bar", + gripper.name, + attachment_frame=Frame([-0.025, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]), + ) + # Specify the base of the robot is allowed to collide with the floor + robot_cell_state.rigid_body_states["floor"].touch_links = ["base_link_inertia"] + # Place target marker at location + for i, frame in enumerate(target_frames): + robot_cell_state.rigid_body_states["target_%d" % i].frame = frame + result = planner.set_robot_cell(robot_cell, robot_cell_state) + + # =============================================== + # Step 3: Perform IK to get initial configuration + # =============================================== + + # robot_cell_state.robot_configuration.joint_values = [0, pi / -2, pi / 2, 0, 0, 0] + + # fk_frame = planner.forward_kinematics(robot_cell_state, TargetMode.WORKPIECE) + # print(fk_frame) + # input("Press Enter to continue...") + + # It is a common pattern to find the robot's starting configuration by solving the inverse kinematics + # for the first target frame. + configuration = planner.inverse_kinematics( + FrameTarget(target_frames[0], TargetMode.WORKPIECE), robot_cell_state, options=dict(avoid_collisions=False) + ) + robot_cell_state.robot_configuration = configuration + print("Initial configuration: ", configuration) + + # ===================================== + # Step 3: Plan Cartesian Motion + # ===================================== + + # frames.append(Frame([0.6, -0.2, 0.45], [1, 0, 0], [0, 1, 0])) + waypoints = FrameWaypoints(target_frames, TargetMode.WORKPIECE) + # Avoid collision is set to False to avoid colliding with the target markers + options = { + "max_step": 0.01, + "avoid_collisions": False, + } + + trajectory = planner.plan_cartesian_motion(waypoints, robot_cell_state, options=options) + print("Computed cartesian path with %d configurations, " % len(trajectory.points)) + print("following %d%% of requested trajectory." % (trajectory.fraction * 100)) + print("Executing this path at full speed would take approx. %.3f seconds." % trajectory.time_from_start) diff --git a/docs/examples/03_backends_ros/files/gh_forward_kinematics.py b/docs/backends/ros/files/gh_forward_kinematics.py similarity index 100% rename from docs/examples/03_backends_ros/files/gh_forward_kinematics.py rename to docs/backends/ros/files/gh_forward_kinematics.py diff --git a/docs/backends/ros/files/gh_inverse_kinematics.py b/docs/backends/ros/files/gh_inverse_kinematics.py new file mode 100644 index 0000000000..d554d2874e --- /dev/null +++ b/docs/backends/ros/files/gh_inverse_kinematics.py @@ -0,0 +1,31 @@ +"""Calculate the robot's inverse kinematic for a given target. + Inputs: + planner: PlannerInterface + A planning backend with inverse kinematics capability. + target: The target to calculate the inverse kinematics for. + robot_cell_state: The robot cell state + group: str, optional + The planning group used for calculation. Defaults to the robot's + main planning group. + start_configuration: :class:`compas_fab.robots.Configuration`, optional + If passed, the inverse will be calculated such that the calculated + joint positions differ the least from the start_configuration. + Defaults to the zero configuration. + avoid_collisions: bool, optional + Whether or not to avoid collisions. Defaults to True. + Output: + group_configuration: The group configuration + full_configuration: The full configuration +""" + +from __future__ import print_function + +if planner: + robot_cell = planner.robot_cell + if robot_cell and planner.client.is_connected: + options = {"avoid_collisions": avoid_collisions} + group_configuration = planner.inverse_kinematics(target, robot_cell_state, group=group, options=options) + full_configuration = robot_cell.zero_full_configuration().merged(group_configuration) + print(group_configuration) + else: + print("planner.client is not connected") diff --git a/docs/examples/03_backends_ros/files/gh_plan_cartesian_motion.py b/docs/backends/ros/files/gh_plan_cartesian_motion.py similarity index 99% rename from docs/examples/03_backends_ros/files/gh_plan_cartesian_motion.py rename to docs/backends/ros/files/gh_plan_cartesian_motion.py index 89446644e5..47ebe59678 100644 --- a/docs/examples/03_backends_ros/files/gh_plan_cartesian_motion.py +++ b/docs/backends/ros/files/gh_plan_cartesian_motion.py @@ -24,9 +24,10 @@ """ from __future__ import print_function -import scriptcontext as sc +import scriptcontext as sc from compas.geometry import Frame + from compas_fab.robots import FrameWaypoints guid = str(ghenv.Component.InstanceGuid) diff --git a/docs/examples/03_backends_ros/files/gh_plan_motion.py b/docs/backends/ros/files/gh_plan_motion.py similarity index 99% rename from docs/examples/03_backends_ros/files/gh_plan_motion.py rename to docs/backends/ros/files/gh_plan_motion.py index 94715d0d7f..b8ebdda949 100644 --- a/docs/examples/03_backends_ros/files/gh_plan_motion.py +++ b/docs/backends/ros/files/gh_plan_motion.py @@ -26,6 +26,7 @@ The calculated trajectory. """ from __future__ import print_function + import scriptcontext as sc guid = str(ghenv.Component.InstanceGuid) diff --git a/docs/examples/03_backends_ros/files/gh_robot_visualisation.py b/docs/backends/ros/files/gh_robot_visualisation.py similarity index 84% rename from docs/examples/03_backends_ros/files/gh_robot_visualisation.py rename to docs/backends/ros/files/gh_robot_visualisation.py index e8ddee5e5b..8d4d4c227b 100644 --- a/docs/examples/03_backends_ros/files/gh_robot_visualisation.py +++ b/docs/backends/ros/files/gh_robot_visualisation.py @@ -11,8 +11,6 @@ Whether or not to show the robot's collision meshes. show_frames: bool Whether or not to show the robot's joint frames. - show_base_frame: bool - Whether or not to show the robot's base frame. show_end_effector_frame: bool Whether or not to show the robot's end-effector frame. Output: @@ -22,6 +20,7 @@ base_frame_WCF: The robot's base frame. ee_frame_WCF: The robot's end-effector frame. """ + from __future__ import print_function if robot and full_configuration: @@ -40,9 +39,6 @@ axes = robot.transformed_axes(full_configuration, group) frames = robot.transformed_frames(full_configuration, group) - if show_base_frame: - base_frame_WCF = robot.get_base_frame(group, full_configuration) - if show_end_effector_frame: - ee_frame_WCF = robot.get_end_effector_frame(group, full_configuration) + ee_frame_WCF = robot.model.forward_kinematics(full_configuration) print("End-effector", ee_frame_WCF) diff --git a/docs/examples/03_backends_ros/files/robot-playground.ghx b/docs/backends/ros/files/robot-playground.ghx similarity index 100% rename from docs/examples/03_backends_ros/files/robot-playground.ghx rename to docs/backends/ros/files/robot-playground.ghx diff --git a/docs/examples/03_backends_ros/files/06_ros_in_grasshopper.jpg b/docs/backends/ros/images/06_ros_in_grasshopper.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/06_ros_in_grasshopper.jpg rename to docs/backends/ros/images/06_ros_in_grasshopper.jpg diff --git a/docs/examples/03_backends_ros/files/07_urdf_tool_00.jpg b/docs/backends/ros/images/07_urdf_tool_00.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/07_urdf_tool_00.jpg rename to docs/backends/ros/images/07_urdf_tool_00.jpg diff --git a/docs/examples/03_backends_ros/files/07_urdf_tool_01.jpg b/docs/backends/ros/images/07_urdf_tool_01.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/07_urdf_tool_01.jpg rename to docs/backends/ros/images/07_urdf_tool_01.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_00.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_00.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_00.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_00.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_01.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_01.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_01.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_01.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_02.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_02.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_02.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_02.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_03.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_03.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_03.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_03.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_04.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_04.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_04.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_04.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_05.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_05.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_05.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_05.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_06.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_06.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_06.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_06.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_07.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_07.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_07.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_07.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_08.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_08.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_08.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_08.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_09.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_09.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_09.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_09.jpg diff --git a/docs/examples/03_backends_ros/files/08_ros_create_moveit_package_10.jpg b/docs/backends/ros/images/08_ros_create_moveit_package_10.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/08_ros_create_moveit_package_10.jpg rename to docs/backends/ros/images/08_ros_create_moveit_package_10.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_00-1.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_00-1.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_00-1.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_00-1.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_00-2.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_00-2.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_00-2.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_00-2.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_00.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_00.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_00.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_00.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_01.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_01.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_01.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_01.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_02.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_02.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_02.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_02.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_03.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_03.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_03.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_03.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_04.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_04.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_04.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_04.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_06.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_06.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_06.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_06.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_07.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_07.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_07.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_07.jpg diff --git a/docs/examples/03_backends_ros/files/09_ur10_tower_urdf_08.jpg b/docs/backends/ros/images/09_ur10_tower_urdf_08.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/09_ur10_tower_urdf_08.jpg rename to docs/backends/ros/images/09_ur10_tower_urdf_08.jpg diff --git a/docs/backends/ros2.md b/docs/backends/ros2.md new file mode 100644 index 0000000000..b01a563ef7 --- /dev/null +++ b/docs/backends/ros2.md @@ -0,0 +1,153 @@ +# ROS 2 / MoveIt 2 + +The current ROS-based planning back-end for `compas_fab`. Uses ROS 2 Jazzy +(LTS, 2024–2029), MoveIt 2, and `rmw_zenoh_cpp` for transport (replacing +DDS multicast discovery with a Zenoh router). + +## When to use + +- You want full motion planning (collision checking, trajectory smoothing) + against a real or simulated robot. +- You are starting a new project — ROS 2 Jazzy is the supported, current + ROS LTS. +- You need to drive the robot from any host OS (Docker Desktop on Mac / + Windows is enough). + +## Trade-offs + +| What you get | What you give up | +|---|---| +| Full MoveIt 2 motion planning + URSim simulator | Docker required (heavier startup than in-process backends) | +| Zenoh transport avoids DDS multicast issues on Docker bridges | Per-robot compose stacks need maintenance | +| Web-based RViz viewport + simulated teach pendant | First-time setup is involved (see below) | + +## Architecture + +``` +┌─────────────────┐ rosbridge ┌─────────────────┐ +│ compas_fab │ ◄────WebSocket───► │ ros-bridge │ +│ (your script) │ (port 9090) │ │ +└─────────────────┘ └────────┬────────┘ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + ┌────▼─────┐ ┌──────▼─────┐ ┌──────▼──────┐ + │ ur-driver│ │ moveit-demo│ │ ur-sim │ + │ │ │ (RViz) │ │ (URSim) │ + └────┬─────┘ └──────┬─────┘ └──────┬──────┘ + │ │ │ + └─────────┬─────────┴───────────────────┘ + │ + ┌───────▼────────┐ + │ zenoh-router │ + └────────────────┘ +``` + +All ROS 2 nodes are Zenoh clients pointing at a single `zenoh-router` service +that federates traffic between containers. This eliminates the need for DDS +multicast discovery, which is unreliable on Docker bridges and across +host-OS boundaries. + +## Setup + +Two compose files ship with the project, sharing the same Dockerfile and +image (`compas-fab/ros-jazzy-moveit2`): + +### Lightweight test stack + +For integration tests and headless service calls: just MoveIt + rosbridge ++ file server, no robot driver, no simulator, no GUI. + +```bash +docker compose -f tests/integration_setup/docker-compose-ros2.yml up +``` + +| Service | Purpose | Port | +|---|---|---| +| `zenoh-router` | Zenoh router federating ROS 2 traffic | — | +| `moveit2-demo` | MoveIt 2 planning node | — | +| `ros2-bridge` | rosbridge WebSocket | 9091 | +| `file-server` | HTTP server for URDF/mesh assets | 9190 | + +### Full demo stack (URSim + GUI) + +For end-to-end demos. It adds URSim, the real UR ROS 2 driver, and a +noVNC RViz viewport on top of the test stack. + +```bash +docker compose -f docs/installation/docker_files/ros2-ur10e-demo/docker-compose.yml up +``` + +This adds: + +| Service | Purpose | Port | +|---|---|---| +| `ur-sim` | URSim simulator (UR5) | 5900 (VNC), 6080 (noVNC pendant) | +| `ur-driver` | UR ROS 2 driver | — | +| `gui` | noVNC web X11 server for RViz | 8080 | + +Open the **simulated teach pendant** at +and power on the robot (Initialise → Brake release → Play) before issuing +motion commands. + +Open the **RViz viewport** at to see the +planning scene rendered from the `moveit2-demo` container. + +## First example + +Connect from Python and verify the bridge: + +```python +--8<-- "docs/backends/ros/files/01_ros_connection.py" +``` + +## Loading robot descriptions over HTTP + +For ROS 2 we recommend +[HttpFileServerLoader][compas_fab.backends.HttpFileServerLoader] to load +URDFs and meshes from the `file-server` container, instead of the legacy +`RosFileServerLoader` (which depends on a ROS 1 service): + +```python +from compas_fab.backends import HttpFileServerLoader + +loader = HttpFileServerLoader(base_url="http://localhost:9190") +urdf = loader.load_urdf("/robot_description", "std_msgs/String") +``` + +## More examples + +The example scripts in `docs/backends/ros/files/` target both ROS 1 and ROS 2 +back-ends because the `RosClient` API is unchanged — see the +[ROS / MoveIt 1 examples](ros.md#more-examples) for the full list (kinematics, +motion planning, Cartesian motion, pub/sub). + +## Notes on Zenoh + +- All ROS 2 services in this stack run as Zenoh clients (not the default DDS). +- The session config lives at + [`zenoh_session.json5`](https://github.com/compas-dev/compas_fab/tree/main/docs/installation/docker_files/ros2-ur10e-demo/zenoh_session.json5). +- POSIX shared-memory transport is **disabled** in that config because it + fails under Rosetta 2 on Apple Silicon and is unnecessary for TCP-only + inter-container traffic. +- `ZENOH_SESSION_CONFIG_URI` takes a **plain filesystem path** despite the name, + not a `file://` URI. + +## Notes on controller rate + +The UR driver defaults to a 500 Hz control loop. Inside Docker this can cause +overrun warnings. The demo lowers this to 100 Hz via `update_rate.yaml` +mounted at `/etc/compas_fab/update_rate.yaml` and passed to the launch file +with `update_rate_config_file:=...`. + +## Architecture: amd64 only + +URSim and the noVNC X11 server are amd64-only. On Apple Silicon they run +through Rosetta 2; `platform: linux/amd64` is set on those services. + +## API reference + +- [compas_fab.backends.RosClient][] +- [compas_fab.backends.MoveItPlanner][] +- [compas_fab.backends.HttpFileServerLoader][] +- [compas_fab.backends.ros.backend_features][] — the underlying feature implementations diff --git a/docs/backends/web_x11_gui.rst b/docs/backends/web_x11_gui.rst deleted file mode 100644 index 878a9a7b6e..0000000000 --- a/docs/backends/web_x11_gui.rst +++ /dev/null @@ -1,65 +0,0 @@ -.. _backends_gui: - -******************************************************************************** -Access backend GUI -******************************************************************************** - -.. highlight:: bash - -When using Docker or WSL to run backends, there is no visible graphical -user interface (GUI). **COMPAS FAB** aims at making this unnecessary, but -if desired, there are two possibilities to open the graphical user interface -of a backend. - -Visualization over web browser -============================== - -This is the easiest option, and it is normally only available for backends -based on Docker. The display of the backend is forwarded to a container that -serves it over a web connection, making it available from your browser. - -Not all Docker setups will include this ability, but if they do, it can be -accessed easily. Start your browser and go to the following address: - -:: - - http://localhost:8080/vnc.html?resize=scale&autoconnect=true - - -This feature is possible thanks to `NoVNC `_. - - -Visualization forwarding display -================================ - -This option allows to forward a display directly to your operating system. -However, it is important to note this is not entirely supported in all platforms. - -First install an ``X11`` server: - -* `XMing For Windows `_ -* `XQuartz For Mac `_ (see here for `more details `_). - -Configure ``X11`` security: - -* On Windows, add your IP address to the file - ``%ProgramFiles(x86)%\XMing\X0.hosts``. It needs to be opened - as administrator. -* On Mac, run ``xhost +local:root``. Remember to disable later with - ``xhost -local:root``. - -Finally, set the ``DISPLAY`` variable to point to your ``X11`` server as follows -replacing ``YOUR_IP_ADDRESS`` with your current IP. - -For usage from WSL, set it using the following command: - -:: - - export DISPLAY=YOUR_IP_ADDRESS:0.0 - -For usage from Docker, set it adding the following environment -variable to the ``moveit`` service in the ``docker-compose.yml`` file: - -:: - - - DISPLAY=host.docker.internal:0.0 diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000000..786b75d5a3 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1 @@ +--8<-- "CHANGELOG.md" diff --git a/docs/changelog.rst b/docs/changelog.rst deleted file mode 100644 index 7a233a5551..0000000000 --- a/docs/changelog.rst +++ /dev/null @@ -1,5 +0,0 @@ -******************************************************************************* -CHANGELOG -******************************************************************************* - -.. literalinclude:: ../CHANGELOG.md diff --git a/docs/citing.md b/docs/citing.md new file mode 100644 index 0000000000..a3f09409e2 --- /dev/null +++ b/docs/citing.md @@ -0,0 +1,37 @@ +# Citing + +If you use **COMPAS FAB** in a project, please cite it as: + +```bibtex +@misc{compas-fab, + title={{COMPAS~FAB}: Robotic fabrication package for the COMPAS Framework}, + author={ + Rust, R. and + Casas, G. and + Parascho, S. and + Jenny, D. and + D\"{o}rfler, K. and + Helmreich, M. and + Gandia, A. and + Ma, Z. and + Ariza, I. and + Pacher, M. and + Lytle, B. and + Huang, Y. and + Kasirer, C. and + Bruun, E. and + Leung, P.Y.V. + }, + howpublished={https://github.com/compas-dev/compas\_fab/}, + note={Gramazio Kohler Research, ETH Z\"{u}rich}, + year={2018}, + doi={10.5281/zenodo.3469478}, + url={https://doi.org/10.5281/zenodo.3469478}, +} +``` + +Machine-readable citation metadata is available in +[`CITATION.cff`](https://github.com/compas-dev/compas_fab/blob/main/CITATION.cff) +at the repository root (used by GitHub's *Cite this repository* button). + +--8<-- "AUTHORS.md" diff --git a/docs/concepts.md b/docs/concepts.md new file mode 100644 index 0000000000..ea2c88c3a4 --- /dev/null +++ b/docs/concepts.md @@ -0,0 +1,114 @@ +# Concepts + +**COMPAS FAB** is built on a small data model that is shared across every +backend: you describe **what** you want, then any backend can execute it. +This page walks through the core types. It is backend-agnostic. None of +the code on this page calls a planner. + +For planning, see [Choosing a backend](backends/index.md). + +## The robot cell + +A [`RobotCell`][compas_fab.robots.RobotCell] is the workspace: a robot +model, any tools attached to its flanges, and any rigid bodies (workpieces, +fixtures) that participate in planning. + +The [`RobotCellLibrary`][compas_fab.robots.RobotCellLibrary] ships ready-to-use +cells for prototyping: + +```pycon +>>> from compas_fab.robots import RobotCellLibrary +>>> cell, state = RobotCellLibrary.ur10e() +>>> print(cell.robot_model) +Robot name=ur10e, Links=11, Joints=10 (6 configurable) +``` + +Each library entry returns the cell **and** a default state — the second +core type. + +## Robot cell state + +A [`RobotCellState`][compas_fab.robots.RobotCellState] is a snapshot of the +cell at one moment: the robot's joint +[`Configuration`][compas_robots.Configuration], which tools and bodies are +attached where, and their poses. + +A cell tells you *what exists*. A state tells you *where everything is*. +You plan **from** a state and **to** a state (or to a target → next section). + +You can mutate state freely between planning calls: + +```pycon +>>> state.set_tool_attached_to_group("cone", cell.main_group_name) +>>> state.robot_configuration # the joints at this moment +``` + +## Targets and waypoints + +A [`Target`][compas_fab.robots.Target] specifies a single goal pose. The +common variants: + +- [`FrameTarget`][compas_fab.robots.FrameTarget]: a full Cartesian pose + for the tool or robot flange. +- [`PointAxisTarget`][compas_fab.robots.PointAxisTarget]: a point plus an + axis, leaving the rotation around the axis free. Useful for drilling, + milling, 3D printing, anything with a cylindrical tool. +- [`ConfigurationTarget`][compas_fab.robots.ConfigurationTarget]: joint + values directly. +- [`ConstraintSetTarget`][compas_fab.robots.ConstraintSetTarget]: for + region/constraint based targets. + +A [`Waypoints`][compas_fab.robots.Waypoints] is a sequence of targets to +follow for Cartesian motion: + +- [`FrameWaypoints`][compas_fab.robots.FrameWaypoints] +- [`PointAxisWaypoints`][compas_fab.robots.PointAxisWaypoints] + +## Target modes + +A target says "go here", but **where** is "here" measured? At the robot's +flange? At the tip of the attached tool? At the workpiece grasped by the +tool? [`TargetMode`][compas_fab.robots.TargetMode] makes that explicit: + +- `TargetMode.ROBOT`: the target frame is at the planner coordinate frame + (PCF) of the robot (typically the `tool0` link). +- `TargetMode.TOOL`: the target is the tool's coordinate frame (TCF). + The planner does the math to figure out the corresponding PCF. +- `TargetMode.WORKPIECE`: the target is on the attached workpiece. The + planner accounts for both the tool and the workpiece grasp frame. + +```pycon +>>> from compas_fab.robots import FrameTarget, TargetMode +>>> from compas.geometry import Frame +>>> frame = Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]) +>>> target = FrameTarget(frame, TargetMode.TOOL) +``` + +## Putting it together + +A typical planning call is just these four types: a cell, a state, a +target, a planner. + +```pycon +>>> # 1. Define what exists +>>> cell, state = RobotCellLibrary.ur5() +>>> +>>> # 2. Define where you want to go +>>> target = FrameTarget(Frame([0.3, 0.1, 0.5], [1, 0, 0], [0, 1, 0]), +... TargetMode.ROBOT) +>>> +>>> # 3. Pick a backend and ask it +>>> # (see Choosing a backend for which line to write here) +>>> # config = planner.inverse_kinematics(target, state) +``` + +The next step is to [choose a backend](backends/index.md) and run the call. + +## Where things live + +- [`compas_fab.robots`][compas_fab.robots]: every type on this page lives + here. +- [`compas_fab.backends`][compas_fab.backends]: the planners. +- [`compas_robots`](https://compas.dev/compas_robots/latest/): the + underlying `RobotModel`, `Configuration`, `Joint`, `Link` types come + from this lower-level package. diff --git a/docs/concepts/01_frames/files/01_frame_constructor.py b/docs/concepts/01_frames/files/01_frame_constructor.py new file mode 100644 index 0000000000..a789995dc2 --- /dev/null +++ b/docs/concepts/01_frames/files/01_frame_constructor.py @@ -0,0 +1,18 @@ +from compas.geometry import Frame +from compas.geometry import Point +from compas.geometry import Vector + +point = Point(10.0, 20.0, 30.0) +xaxis = Vector(1, 0, 0) +yaxis = Vector(0, 1, 0) + +F = Frame(point, xaxis, yaxis) +print(F) + +# Alternative constructor without importing Point and Vector +F = Frame([10.0, 20.0, 30.0], [1, 0, 0], [0, 1, 0]) +print(F) + +# Default frames +F = Frame.worldXY() +print(F) diff --git a/docs/concepts/01_frames/files/01_frame_conversion.py b/docs/concepts/01_frames/files/01_frame_conversion.py new file mode 100644 index 0000000000..0f724cb05a --- /dev/null +++ b/docs/concepts/01_frames/files/01_frame_conversion.py @@ -0,0 +1,12 @@ +from compas.geometry import Frame +from compas.geometry import Transformation + +# Convert Frame to Transformation +F = Frame.worldXY() +T = Transformation.from_frame(F) +print(T) + +# Conversion to Frame +T = Transformation([[1, 0, 0, 1], [0, 1, 0, 2], [0, 0, 1, 3], [0, 0, 0, 1]]) +F = Frame.from_transformation(T) +print(F) diff --git a/docs/concepts/01_frames/files/01_frame_transform_object.py b/docs/concepts/01_frames/files/01_frame_transform_object.py new file mode 100644 index 0000000000..afa34facac --- /dev/null +++ b/docs/concepts/01_frames/files/01_frame_transform_object.py @@ -0,0 +1,23 @@ +from compas.geometry import Frame +from compas.geometry import Point +from compas.geometry import Transformation +from compas.geometry import Vector + +point = Point(146.00, 150.00, 161.50) +xaxis = Vector(0.9767, 0.0010, -0.214) +yaxis = Vector(0.1002, 0.8818, 0.4609) + +# Coordinate system represented by Frame F +F = Frame(point, xaxis, yaxis) + +# A point in F's local coordinate system +P = Point(35.0, 35.0, 35.0) + +# Computing the point's position in the global (world) coordinates +P_ = F.to_world_coordinates(P) +print(P_) # >>> Point(x=190.314, y=164.389, z=200.285) + +# This is equivalent to the following transformation +T_WCF_LCF = Transformation.from_frame(F) +P_ = P.transformed(T_WCF_LCF) +print(P_) # >>> Point(x=190.314, y=164.389, z=200.285) diff --git a/docs/examples/01_fundamentals/files/02_coordinate_frames.jpg b/docs/concepts/01_frames/files/02_coordinate_frames.jpg similarity index 100% rename from docs/examples/01_fundamentals/files/02_coordinate_frames.jpg rename to docs/concepts/01_frames/files/02_coordinate_frames.jpg diff --git a/docs/examples/02_description_models/files/01_robot_links_and_joints.jpg b/docs/concepts/02_robot_model/files/01_robot_links_and_joints.jpg similarity index 100% rename from docs/examples/02_description_models/files/01_robot_links_and_joints.jpg rename to docs/concepts/02_robot_model/files/01_robot_links_and_joints.jpg diff --git a/docs/examples/02_description_models/files/01_robot_model.jpg b/docs/concepts/02_robot_model/files/01_robot_model.jpg similarity index 100% rename from docs/examples/02_description_models/files/01_robot_model.jpg rename to docs/concepts/02_robot_model/files/01_robot_model.jpg diff --git a/docs/examples/02_description_models/files/02_robot_from_disk.py b/docs/concepts/02_robot_model/files/02_robot_from_disk.py similarity index 100% rename from docs/examples/02_description_models/files/02_robot_from_disk.py rename to docs/concepts/02_robot_model/files/02_robot_from_disk.py diff --git a/docs/examples/02_description_models/files/02_robot_from_github.py b/docs/concepts/02_robot_model/files/02_robot_from_github.py similarity index 100% rename from docs/examples/02_description_models/files/02_robot_from_github.py rename to docs/concepts/02_robot_model/files/02_robot_from_github.py index 6339cefd6c..55afc27cdc 100644 --- a/docs/examples/02_description_models/files/02_robot_from_github.py +++ b/docs/concepts/02_robot_model/files/02_robot_from_github.py @@ -1,5 +1,5 @@ -from compas_robots.resources import GithubPackageMeshLoader from compas_robots import RobotModel +from compas_robots.resources import GithubPackageMeshLoader # Select Github repository, package and branch where the model is stored repository = "ros-industrial/abb" diff --git a/docs/concepts/02_robot_model/files/02_robot_from_library.py b/docs/concepts/02_robot_model/files/02_robot_from_library.py new file mode 100644 index 0000000000..3d20ad4019 --- /dev/null +++ b/docs/concepts/02_robot_model/files/02_robot_from_library.py @@ -0,0 +1,9 @@ +from compas_fab.robots import RobotCellLibrary + +# Load robot from RobotCellLibrary +# RobotCellLibrary also contains .ur5(), .ur10e(), abb_irb120_3_58(), abb_irb4600_40_255(), .rfl(), .panda() +robot_cell, robot_cell_state = RobotCellLibrary.ur5() +robot_model = robot_cell.robot_model +robot_semantics = robot_cell.robot_semantics + +print(robot_model) diff --git a/docs/concepts/02_robot_model/files/02_robot_from_ros.py b/docs/concepts/02_robot_model/files/02_robot_from_ros.py new file mode 100644 index 0000000000..ea23b69cc3 --- /dev/null +++ b/docs/concepts/02_robot_model/files/02_robot_from_ros.py @@ -0,0 +1,6 @@ +from compas_fab.backends import RosClient + +with RosClient() as ros: + robot_cell = ros.load_robot_cell(load_geometry=True, precision=12) + + print(robot_cell.robot_model) diff --git a/docs/examples/02_description_models/files/02_robot_from_ros_with_cache.py b/docs/concepts/02_robot_model/files/02_robot_from_ros_with_cache.py similarity index 63% rename from docs/examples/02_description_models/files/02_robot_from_ros_with_cache.py rename to docs/concepts/02_robot_model/files/02_robot_from_ros_with_cache.py index fe9c7d6ed6..b20dd6e0e9 100644 --- a/docs/examples/02_description_models/files/02_robot_from_ros_with_cache.py +++ b/docs/concepts/02_robot_model/files/02_robot_from_ros_with_cache.py @@ -5,6 +5,6 @@ with RosClient() as ros: # Load complete model from ROS and set a local cache location local_directory = os.path.join(os.path.expanduser("~"), "robot_description", "robot_name") - robot = ros.load_robot(load_geometry=True, local_cache_directory=local_directory, precision=12) + robot_cell = ros.load_robot_cell(load_geometry=True, local_cache_directory=local_directory, precision=12) - print(robot.model) + robot_cell.print_info() diff --git a/docs/examples/02_description_models/files/03_robot_blender.py b/docs/concepts/02_robot_model/files/03_robot_blender.py similarity index 100% rename from docs/examples/02_description_models/files/03_robot_blender.py rename to docs/concepts/02_robot_model/files/03_robot_blender.py index c272b5c7bc..4b3c39b061 100644 --- a/docs/examples/02_description_models/files/03_robot_blender.py +++ b/docs/concepts/02_robot_model/files/03_robot_blender.py @@ -1,6 +1,6 @@ from compas_robots import RobotModel -from compas_robots.resources import GithubPackageMeshLoader from compas_robots.blender import RobotModelArtist +from compas_robots.resources import GithubPackageMeshLoader r = "ros-industrial/abb" p = "abb_irb6600_support" diff --git a/docs/examples/02_description_models/files/03_robot_grasshopper.ghx b/docs/concepts/02_robot_model/files/03_robot_grasshopper.ghx similarity index 100% rename from docs/examples/02_description_models/files/03_robot_grasshopper.ghx rename to docs/concepts/02_robot_model/files/03_robot_grasshopper.ghx diff --git a/docs/examples/02_description_models/files/03_robot_grasshopper_from_ros.ghx b/docs/concepts/02_robot_model/files/03_robot_grasshopper_from_ros.ghx similarity index 100% rename from docs/examples/02_description_models/files/03_robot_grasshopper_from_ros.ghx rename to docs/concepts/02_robot_model/files/03_robot_grasshopper_from_ros.ghx diff --git a/docs/examples/02_description_models/files/03_robot_rhino.py b/docs/concepts/02_robot_model/files/03_robot_rhino.py similarity index 100% rename from docs/examples/02_description_models/files/03_robot_rhino.py rename to docs/concepts/02_robot_model/files/03_robot_rhino.py diff --git a/docs/examples/02_description_models/files/03_robot_rhino_from_ros.py b/docs/concepts/02_robot_model/files/03_robot_rhino_from_ros.py similarity index 63% rename from docs/examples/02_description_models/files/03_robot_rhino_from_ros.py rename to docs/concepts/02_robot_model/files/03_robot_rhino_from_ros.py index 67d54881b8..72279f347b 100644 --- a/docs/examples/02_description_models/files/03_robot_rhino_from_ros.py +++ b/docs/concepts/02_robot_model/files/03_robot_rhino_from_ros.py @@ -1,11 +1,12 @@ -from compas_fab.backends import RosClient from compas.scene import Scene +from compas_fab.backends import RosClient + with RosClient() as ros: # Load complete model from ROS - robot = ros.load_robot(load_geometry=True, precision=12) + robot_cell = ros.load_robot_cell(load_geometry=True, precision=12) # Visualize robot scene = Scene() - scene_object = scene.add(robot.model) + scene_object = scene.add(robot_cell) scene.draw() diff --git a/docs/concepts/02_robot_model/files/04_forward_kinematics_urdf.py b/docs/concepts/02_robot_model/files/04_forward_kinematics_urdf.py new file mode 100644 index 0000000000..83028de4f5 --- /dev/null +++ b/docs/concepts/02_robot_model/files/04_forward_kinematics_urdf.py @@ -0,0 +1,25 @@ +from compas_robots import Configuration +from compas_robots import RobotModel + +import compas_fab + +# Load robot from URDF file, geometry is not required for forward kinematics +urdf_filename = compas_fab.get("robot_library/ur5_robot/urdf/robot_description.urdf") +robot_model = RobotModel.from_urdf_file(urdf_filename) # type: RobotModel + +# Create a configuration with joint values +configuration = robot_model.zero_configuration() +configuration.joint_values = [-2.238, -1.153, -2.174, 0.185, 0.667, 0.0] +frame = robot_model.forward_kinematics(configuration, "tool0") +# The frame result is correct +print("Configuration: {}".format(configuration)) +print("Correct Frame of tool0: {}".format(frame)) +# This is the expected output: +assert str(frame.point) == "Point(x=0.300, y=0.100, z=0.500)" + +# Do not use a Configuration object without joint names +configuration = Configuration.from_revolute_values([-2.238, -1.153, -2.174, 0.185, 0.667, 0.0]) +frame = robot_model.forward_kinematics(configuration, "tool0") +# It will produce incorrect results +print("Configuration: {}".format(configuration)) +print("Incorrect Frame of tool0: {}".format(frame)) diff --git a/docs/concepts/02_robot_model/files/04_robot_cell_library_pybullet.py b/docs/concepts/02_robot_model/files/04_robot_cell_library_pybullet.py new file mode 100644 index 0000000000..336ae47484 --- /dev/null +++ b/docs/concepts/02_robot_model/files/04_robot_cell_library_pybullet.py @@ -0,0 +1,9 @@ +from compas_fab.backends.pybullet import PyBulletClient +from compas_fab.backends.pybullet import PyBulletPlanner +from compas_fab.robots import RobotCellLibrary + +with PyBulletClient() as client: + planner = PyBulletPlanner(client) + robot_cell, robot_cell_state = RobotCellLibrary.abb_irb4600_40_255_gripper_one_beam(client, load_geometry=True) + planner.set_robot_cell(robot_cell, robot_cell_state) + input("Press Enter to exit...") diff --git a/docs/concepts/02_robot_model/files/05_robot_model_chains.py b/docs/concepts/02_robot_model/files/05_robot_model_chains.py new file mode 100644 index 0000000000..f4607a5172 --- /dev/null +++ b/docs/concepts/02_robot_model/files/05_robot_model_chains.py @@ -0,0 +1,89 @@ +# This example file demonstrates how to print the chain of links and joints in a robot model. + +from compas_robots.model import Joint + +from compas_fab.robots import RobotCellLibrary + +# RobotCellLibrary also contains .ur5(), .ur10e(), abb_irb120_3_58(), abb_irb4600_40_255(), .rfl(), .panda() +robot_cell, robot_cell_state = RobotCellLibrary.panda(load_geometry=False) + +model = robot_cell.robot_model +print("Robot Model Chain:") + +# ---------------------------------------------- +# Example 1: Print the chain of links and joints +# ---------------------------------------------- + +base_link_name = model.get_base_link_name() +print("Base Link: {}".format(base_link_name)) + + +# Iteratively print the chain of links and joints +def print_joint(joint, level=1): + # type: (Joint, int) -> None + link = joint.child_link + print("-" * level + "Joint: {} Link: {}".format(joint.name, link.name)) + for child_joint in link.joints: + print_joint(child_joint, level + 1) + + +first_joint = model.joints[0] +print_joint(first_joint) + +# --------------------------------------------------------------------------------- +# Example 2: Print the chain and highlight the links and joints in a planning group +# --------------------------------------------------------------------------------- + +print("") + + +def print_planning_group_chain(group): + print("Planning Group: {}".format(group)) + base_link = robot_cell.get_base_link_name(group) + print("Base Link: {}".format(base_link)) + tip_link = robot_cell.get_end_effector_link_name(group) + print("Tip Link: {}".format(tip_link)) + print("--------------------") + group_object = robot_cell.robot_semantics.groups[group] + joints_in_group = group_object["joints"] + links_in_group = group_object["links"] + + # Iteratively print the chain of links and joints + def print_joint(joint, level=1): + # type: (Joint, int) -> None + link = joint.child_link + line = "-" * level + + # Joint info + if joint.name in joints_in_group: + line += " [Joint: {}]".format(joint.name) + else: + line += " Joint: {}".format(joint.name) + + # Type of joint + line += " (" + if joint.is_configurable(): + line += "Configurable" + elif joint.mimic: + line += "Mimic" + else: + line += "Fixed" + line += " {})".format(Joint.SUPPORTED_TYPES[joint.type]) + + # Link info + if link.name in links_in_group: + line += " [Link: {}]".format(link.name) + else: + line += " Link: {}".format(link.name) + print(line) + for child_joint in link.joints: + print_joint(child_joint, level + 1) + + print("Base Link: {}".format(model.get_base_link_name())) + first_joint = model.joints[0] + print_joint(first_joint) + print("") + + +for group in robot_cell.robot_semantics.groups: + print_planning_group_chain(group) diff --git a/docs/concepts/02_robot_model/files/05_robot_viewer.py b/docs/concepts/02_robot_model/files/05_robot_viewer.py new file mode 100644 index 0000000000..c9e3ff1318 --- /dev/null +++ b/docs/concepts/02_robot_model/files/05_robot_viewer.py @@ -0,0 +1,53 @@ +from compas_robots.model import Joint +from compas_robots.viewer.scene.robotmodelobject import RobotModelObject +from compas_viewer import Viewer +from compas_viewer.components import Slider + +from compas_fab.robots import RobotCellLibrary + +# Initialize the viewer +viewer = Viewer() +viewer.renderer.rendermode = "lighted" +viewer.ui.sidedock.show = True + +# Load robot from RobotCellLibrary +# RobotCellLibrary also contains .ur5(), .ur10e(), abb_irb120_3_58(), abb_irb4600_40_255(), .rfl(), .panda() +robot_cell, robot_cell_state = RobotCellLibrary.ur5() +model = robot_cell.robot_model + +start_configuration = model.zero_configuration() +robot_object: RobotModelObject = viewer.scene.add(model, show_lines=False, configuration=start_configuration) # type: ignore + + +# Callback function when the slider is moved to update the robot's joint values +def make_rotate_function(joint_name): + def rotate(slider: Slider, value: float): + config = robot_object.configuration + config[joint_name] = value + robot_object.update_joints(config) + + return rotate + + +# Create one slider for each joint +for joint in robot_cell.get_configurable_joints(): + starting_val = start_configuration[joint.name] + print(joint.name, Joint.SUPPORTED_TYPES[joint.type], joint.limit.lower, joint.limit.upper, starting_val) + # Units are in radians or meters + step_size = (joint.limit.upper - joint.limit.lower) / 100 + rotate_function = make_rotate_function(joint.name) + viewer.ui.sidedock.add( + Slider( + title=joint.name + " (" + Joint.SUPPORTED_TYPES[joint.type] + ")", + starting_val=starting_val, + min_val=joint.limit.lower, + max_val=joint.limit.upper, + step=step_size, + action=rotate_function, + ) + ) + +# configuration = model.zero_configuration() +robot_object.update_joints(start_configuration) + +viewer.show() diff --git a/docs/examples/03_backends_ros/files/03_forward_and_inverse_kinematics.jpg b/docs/concepts/02_robot_model/images/04_forward_and_inverse_kinematics.jpg similarity index 100% rename from docs/examples/03_backends_ros/files/03_forward_and_inverse_kinematics.jpg rename to docs/concepts/02_robot_model/images/04_forward_and_inverse_kinematics.jpg diff --git a/docs/concepts/05_robot_cell/files/01_ros_robot_cell_with_tools.py b/docs/concepts/05_robot_cell/files/01_ros_robot_cell_with_tools.py new file mode 100644 index 0000000000..798c65057a --- /dev/null +++ b/docs/concepts/05_robot_cell/files/01_ros_robot_cell_with_tools.py @@ -0,0 +1,84 @@ +from compas.datastructures import Mesh +from compas.geometry import Frame + +import compas_fab +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import RigidBody +from compas_fab.robots import ToolLibrary + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + + # ========= + # Example 1 + # ========= + + # Create a robot cell with the robot from the client + # Add a floor as rigid body to the robot cell, this will be static. + floor_mesh = Mesh.from_stl(compas_fab.get("planning_scene/floor.stl")) + robot_cell.rigid_body_models["floor"] = RigidBody.from_mesh(floor_mesh) + # Add a demo cone tool to the robot cell, attachment is set later + cone_tool = ToolLibrary.cone(radius=0.1, length=0.3) + robot_cell.tool_models[cone_tool.name] = cone_tool + # Set the robot cell in the planner + result = planner.set_robot_cell(robot_cell) + # If you are running ROS with UI, you should see a floor in the PyBullet world but the cone is not attached yet + input("Press Enter to continue...") + + # In order to see the tool attached it is necessary to update the robot_cell_state + robot_cell_state = robot_cell.default_cell_state() + # Modify the tool state to attach the cone to the robot + robot_cell_state.tool_states["cone"].attached_to_group = robot_cell.main_group_name + robot_cell_state.tool_states["cone"].attachment_frame = Frame([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]) + # Specify the link of the robot that the tool is allowed to collide with + robot_cell_state.tool_states["cone"].touch_links = ["wrist_3_link"] + # Move the robot to a different configuration + robot_cell_state.robot_configuration._joint_values[1] = -0.5 + robot_cell_state.robot_configuration._joint_values[2] = 0.5 + # Specify the base of the robot is allowed to collide with the floor + robot_cell_state.rigid_body_states["floor"].touch_links = ["base_link_inertia"] + # Set the robot cell state in the planner + result = planner.set_robot_cell_state(robot_cell_state) + # If you are running ROS with UI, the cone should be attached to the robot + input("Press Enter to continue...") + + # ========= + # Example 2 + # ========= + # Demonstrate that it is possible to have multiple tools in the robot cell + + # Create a robot cell with the robot from the client + robot_cell = client.load_robot_cell() + # Add a two tools (gripper and cone) to the robot cell + gripper = ToolLibrary.static_gripper_small() + robot_cell.tool_models[gripper.name] = gripper + robot_cell.tool_models[cone_tool.name] = cone_tool + # Attach the gripper to the robot + robot_cell_state = robot_cell.default_cell_state() + # The following function `set_tool_attached_to_group` is an alternative way than that in example 1 + # It will also ensure that only one tool is attached to the specified group by removing others + robot_cell_state.set_tool_attached_to_group( + gripper.name, + robot_cell.main_group_name, + attachment_frame=Frame([0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]), + touch_links=["wrist_3_link"], + ) + # Specify the location of the detached cone tool + robot_cell_state.tool_states[cone_tool.name].frame = Frame([1.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0]) + # Move the robot to a different configuration + robot_cell_state.robot_configuration = robot_cell.zero_configuration() + robot_cell_state.robot_configuration._joint_values[1] = -0.5 + robot_cell_state.robot_configuration._joint_values[2] = 0.5 + result = planner.set_robot_cell(robot_cell, robot_cell_state) + # If you are running ROS with UI, you should see a floor in the PyBullet world + input("Press Enter to continue...") + + # ========= + # Example 3 + # ========= + + # It is possible to remove all tools and objects from the robot cell + robot_cell = client.load_robot_cell() + result = planner.set_robot_cell(robot_cell) diff --git a/docs/concepts/05_robot_cell/files/01_ros_set_robot_cell.py b/docs/concepts/05_robot_cell/files/01_ros_set_robot_cell.py new file mode 100644 index 0000000000..ba93f510e6 --- /dev/null +++ b/docs/concepts/05_robot_cell/files/01_ros_set_robot_cell.py @@ -0,0 +1,60 @@ +from compas.datastructures import Mesh + +import compas_fab +from compas_fab.backends import MoveItPlanner +from compas_fab.backends import RosClient +from compas_fab.robots import RigidBody + +with RosClient() as client: + robot_cell = client.load_robot_cell() + planner = MoveItPlanner(client) + + # ========= + # Example 1 + # ========= + + floor_mesh = Mesh.from_stl(compas_fab.get("planning_scene/floor.stl")) + robot_cell.rigid_body_models["floor"] = RigidBody.from_mesh(floor_mesh) + + result = planner.set_robot_cell(robot_cell) + print(result) + + # If you are running ROS with UI, you should see a floor in the PyBullet world + input("Press Enter to continue...") + + # ========= + # Example 2 + # ========= + + # Create another robot cell with a cone geometry + robot_cell = client.load_robot_cell() + cone = Mesh.from_stl(compas_fab.get("planning_scene/cone.stl")).scaled(5) + robot_cell.rigid_body_models["cone"] = RigidBody.from_mesh(cone) + + # This will replace the previous robot cell in the planner + # There will not be any `floor`` in the PyBullet world + result = planner.set_robot_cell(robot_cell) + print(result) + + # If you are running ROS with UI, you should see a cone in the PyBullet world + # and the floor should be gone + input("Press Enter to continue...") + + # ========= + # Example 3 + # ========= + + # Add the floor mesh to this robot cell + # The `cone` rigid body is also in the robot cell + floor_mesh = Mesh.from_stl(compas_fab.get("planning_scene/floor.stl")) + robot_cell.rigid_body_models["floor"] = RigidBody.from_mesh(floor_mesh) + + # Calling `set_robot_cell` again will update the robot cell in the planner + # Behind the scene, compas_fab will compare the new robot cell with the previous one + # Because the cone rigid body is identical to the previous one, the `cone` rigid body + # will not be sent again to the PyBullet backend. Only the `floor` rigid body will be sent. + result = planner.set_robot_cell(robot_cell) + print(result) + + # If you are running ROS with UI, you should see the floor and the cone in the PyBullet world + input("Press Enter to continue...") diff --git a/docs/concepts/06_planning/files/01_planning_group_names.py b/docs/concepts/06_planning/files/01_planning_group_names.py new file mode 100644 index 0000000000..f45e73da48 --- /dev/null +++ b/docs/concepts/06_planning/files/01_planning_group_names.py @@ -0,0 +1,40 @@ +from compas_fab.robots import RobotCellLibrary + +# Load from the robot cell library +robot_cell, _ = RobotCellLibrary.rfl(load_geometry=False) + +# Print all available planning groups +group_names = robot_cell.robot_semantics.group_names +for group_name in group_names: + base_link = robot_cell.get_base_link_name(group_name) + end_effector_link = robot_cell.get_end_effector_link_name(group_name) + print("Planning group: '{}'".format(group_name)) + print(" base_link: '{}' end_effector_link: '{}'".format(base_link, end_effector_link)) + +""" +Output: +>>> Planning group: 'robot11' +>>> base_link: 'robot11_base_link' end_effector_link: 'robot11_tool0' +>>> Planning group: 'robot12' +>>> base_link: 'robot12_base_link' end_effector_link: 'robot12_tool0' +>>> Planning group: 'robot11_eaXYZ' +>>> base_link: 'x_rail' end_effector_link: 'robot11_tool0' +>>> Planning group: 'robot12_eaYZ' +>>> base_link: 'bridge1' end_effector_link: 'robot12_tool0' +>>> Planning group: 'robot21' +>>> base_link: 'robot21_base_link' end_effector_link: 'robot21_tool0' +>>> Planning group: 'robot22' +>>> base_link: 'robot22_base_link' end_effector_link: 'robot22_tool0' +>>> Planning group: 'robot21_eaXYZ' +>>> base_link: 'x_rail' end_effector_link: 'robot21_tool0' +>>> Planning group: 'robot22_eaYZ' +>>> base_link: 'bridge2' end_effector_link: 'robot22_tool0' +>>> Planning group: 'robot11_eaYZ' +>>> base_link: 'bridge1' end_effector_link: 'robot11_tool0' +>>> Planning group: 'robot21_eaYZ' +>>> base_link: 'bridge2' end_effector_link: 'robot21_tool0' +>>> Planning group: 'robot12_eaXYZ' +>>> base_link: 'x_rail' end_effector_link: 'robot12_tool0' +>>> Planning group: 'robot22_eaXYZ' +>>> base_link: 'x_rail' end_effector_link: 'robot22_tool0' +""" diff --git a/docs/concepts/06_planning/files/01_planning_group_print.py b/docs/concepts/06_planning/files/01_planning_group_print.py new file mode 100644 index 0000000000..cd04de7c3c --- /dev/null +++ b/docs/concepts/06_planning/files/01_planning_group_print.py @@ -0,0 +1,17 @@ +from compas_fab.robots import RobotCellLibrary + +# Load from the robot cell library +robot_cell, _ = RobotCellLibrary.ur10e(load_geometry=False) + +# Print all available planning groups +for group_name, group in robot_cell.robot_semantics.groups.items(): + print("Planning group: '{}'".format(group_name)) + print(" Joints: {}".format(group["joints"])) + print(" Links: {}".format(group["links"])) + +""" +Output: +>>> Planning group: 'manipulator' +>>> Joints: ['base_link-base_link_inertia', 'shoulder_pan_joint', 'shoulder_lift_joint', 'elbow_joint', 'wrist_1_joint', 'wrist_2_joint', 'wrist_3_joint', 'wrist_3-flange', 'flange-tool0'] +>>> Links: ['base_link', 'base_link_inertia', 'shoulder_link', 'upper_arm_link', 'forearm_link', 'wrist_1_link', 'wrist_2_link', 'wrist_3_link', 'flange', 'tool0'] +""" diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 5d4cc62260..0000000000 --- a/docs/conf.py +++ /dev/null @@ -1,169 +0,0 @@ -# flake8: noqa -# -*- coding: utf-8 -*- - -from sphinx.writers import html, html5 -import sphinx_compas2_theme - -# -- General configuration ------------------------------------------------ - -project = "COMPAS FAB" -copyright = "Gramazio Kohler Research" -author = "Gramazio Kohler Research" -organization = "compas-dev" -package = "compas_fab" - -master_doc = "index" -source_suffix = {".rst": "restructuredtext", ".md": "markdown"} -templates_path = sphinx_compas2_theme.get_autosummary_templates_path() -exclude_patterns = sphinx_compas2_theme.default_exclude_patterns -add_module_names = True -language = "en" - -latest_version = sphinx_compas2_theme.get_latest_version() - -if latest_version == "Unreleased": - release = "Unreleased" - version = "latest" -else: - release = latest_version - version = ".".join(release.split(".")[0:2]) # type: ignore - -# -- Extension configuration ------------------------------------------------ - -extensions = sphinx_compas2_theme.default_extensions - -# numpydoc options - -numpydoc_show_class_members = False -numpydoc_class_members_toctree = False -numpydoc_attributes_as_param_list = True -numpydoc_show_inherited_class_members = False - -# bibtex options - -# autodoc options - -autodoc_type_aliases = {} -autodoc_typehints_description_target = "documented" -autodoc_mock_imports = sphinx_compas2_theme.default_mock_imports -autodoc_default_options = { - "undoc-members": True, - "show-inheritance": True, -} -autodoc_member_order = "groupwise" -autodoc_typehints = "description" -autodoc_class_signature = "separated" - -autoclass_content = "class" - - -def setup(app): - app.connect("autodoc-skip-member", sphinx_compas2_theme.skip) - - -# autosummary options - -autosummary_generate = True -autosummary_mock_imports = sphinx_compas2_theme.default_mock_imports - -# graph options - -# plot options - -plot_include_source = False -plot_html_show_source_link = False -plot_html_show_formats = False -plot_formats = ["png"] - -# intersphinx options - -intersphinx_mapping = { - "python": ("https://docs.python.org/", None), - "compas": ("https://compas.dev/compas/latest/", None), - "compas_robots": ("https://compas.dev/compas_robots/latest/", None), - "roslibpy": ("https://roslibpy.readthedocs.io/en/latest/", None), -} - -# linkcode - -linkcode_resolve = sphinx_compas2_theme.get_linkcode_resolve(organization, package) - -# extlinks - -extlinks = { - "rhino": ("https://developer.rhino3d.com/api/RhinoCommon/html/T_%s.htm", "%s"), - "blender": ("https://docs.blender.org/api/2.93/%s.html", "%s"), -} - -# from pytorch - -sphinx_compas2_theme.replace(html.HTMLTranslator) -sphinx_compas2_theme.replace(html5.HTML5Translator) - -# -- Options for HTML output ---------------------------------------------- - -html_theme = "sidebaronly" -html_title = project -html_sidebars = {"index": []} - -favicons = [ - { - "rel": "icon", - "href": "compas.ico", - } -] - -html_theme_options = { - "external_links": [ - {"name": "COMPAS Framework", "url": "https://compas.dev"}, - ], - "icon_links": [ - { - "name": "GitHub", - "url": f"https://github.com/{organization}/{package}", - "icon": "fa-brands fa-github", - "type": "fontawesome", - }, - { - "name": "Discourse", - "url": "http://forum.compas-framework.org/", - "icon": "fa-brands fa-discourse", - "type": "fontawesome", - }, - { - "name": "PyPI", - "url": f"https://pypi.org/project/{package}/", - "icon": "fa-brands fa-python", - "type": "fontawesome", - }, - ], - "switcher": { - "json_url": f"https://raw.githubusercontent.com/{organization}/{package}/gh-pages/versions.json", - "version_match": version, - }, - "check_switcher": False, - "logo": { - "image_light": "_static/compas_icon_white.png", - "image_dark": "_static/compas_icon_white.png", - "text": "COMPAS FAB", - }, - "navigation_depth": 2, -} - -html_context = { - "github_url": "https://github.com", - "github_user": organization, - "github_repo": package, - "github_version": "main", - "doc_path": "docs", -} - -html_static_path = sphinx_compas2_theme.get_html_static_path() + ["_static"] -html_css_files = [] -html_extra_path = [] -html_last_updated_fmt = "" -html_copy_source = False -html_show_sourcelink = True -html_permalinks = False -html_permalinks_icon = "" -html_compact_lists = True diff --git a/docs/examples/conftest.py b/docs/conftest.py similarity index 100% rename from docs/examples/conftest.py rename to docs/conftest.py diff --git a/docs/contributing.rst b/docs/contributing.rst deleted file mode 100644 index e582053ea0..0000000000 --- a/docs/contributing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../CONTRIBUTING.rst diff --git a/docs/developer/architecture.md b/docs/developer/architecture.md new file mode 100644 index 0000000000..2f9d0724e3 --- /dev/null +++ b/docs/developer/architecture.md @@ -0,0 +1,76 @@ +# Backend client architecture + +This document details the architecture used to implement backend clients and +backend features. It is aimed at contributors extending `compas_fab` with a +new backend. + +## Clients vs. planners + +To keep backends consistent and modular, `compas_fab` defines two interfaces: + +- [ClientInterface][compas_fab.backends.interfaces.ClientInterface] — + connection lifecycle and shared state (connect, disconnect, etc.). +- [PlannerInterface][compas_fab.backends.interfaces.PlannerInterface] — + planning, scene management, and kinematics. + +Any new backend should inherit from `ClientInterface` and pair it with a +`PlannerInterface` subclass. + +> Eventually, execution and control will move to a separate `ControlInterface`; +> for now those methods sit on the client. + +## Backend features + +`PlannerInterface` provides default behavior for each planning method. +To override a default, implement the appropriate **backend feature** +interface from +[compas_fab.backends.interfaces][compas_fab.backends.interfaces]. + +Backend feature classes are **callable**: their `__call__` magic method +delegates to the named method. For example: + +```python +from compas.geometry import Frame +from compas_fab.backends.interfaces import InverseKinematics + +class ExampleInverseKinematics(InverseKinematics): + def inverse_kinematics(self, robot, frame_WCF, + start_configuration=None, + group=None, + options=None): + ... + +calculate_example_ik = ExampleInverseKinematics() +frame = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]) +ik_result = calculate_example_ik(robot, frame) +# equivalent to: +ik_result = calculate_example_ik.inverse_kinematics(robot, frame) +``` + +The feature interfaces enforce a common signature across all +implementations. Adhere to the documented types for arguments and +return values. + +## Mixing features from multiple backends + +The feature interfaces are designed so a user can mix and match features +from different backends. For example, if `ClientA` is fast at inverse +kinematics but lacks motion planning, and `ClientB` is slow at IK but +plans well: + +```python +with ClientA() as client_a, ClientB() as client_b: + inverse_kinematics = ClientAInverseKinematics(client_a) + plan_motion = ClientBPlanMotion(client_b) +``` + +## Implemented backend features + +The following backend feature modules are provided: + +- [compas_fab.backends.interfaces][compas_fab.backends.interfaces] — + the abstract base classes. +- [compas_fab.backends.ros.backend_features][compas_fab.backends.ros.backend_features] — + ROS / MoveIt implementations. +- [compas_fab.backends.pybullet.backend_features][compas_fab.backends.pybullet.backend_features] — + PyBullet implementations. diff --git a/docs/developer/backends.rst b/docs/developer/backends.rst deleted file mode 100644 index 991bd40fdb..0000000000 --- a/docs/developer/backends.rst +++ /dev/null @@ -1,96 +0,0 @@ -.. _architecture: - -******************************************************************************* -Backend clients -******************************************************************************* - -This document details the architecture used to implement backend clients and -backend features. - -To maintain consistency from one backend client to another and to promote -modularity, we make use of several interfaces. Any new backend client -should inherit from :class:`~compas_fab.backends.interfaces.ClientInterface` and -make use of the :class:`~compas_fab.backends.interfaces.PlannerInterface`. -Methods for connecting, disconnecting, and generally managing the client state -are a part of the client, while any methods for planning, scene management or -kinematics are attached to the planner. Eventually, methods for -execution and control will be included in the ``ControlInterface``, -but for now, such methods and attributes will be left with the -client. - -The ``PlannerInterface`` serves as a template for any client-specific -planner, providing default behavior for each of the -methods listed within. When a developer wishes to override any -of these defaults, they should make use of the appropriate backend -feature interface from ``backends/interfaces.py``. The file -``interfaces.py`` consists of a collection of classes, any -implementation of which is callable through its ``__call__`` magic -method. For example: - -.. code-block:: python - - from compas.geometry import Frame - from compas_fab.backends.interfaces import InverseKinematics - - class ExampleInverseKinematics(InverseKinematics): - def inverse_kinematics(self, robot, - frame_WCF, - start_configuration=None, - group=None, - options=None): - # insert fancy code here - pass - -can be instantiated and called in the following manner: - -.. code-block:: python - - calculate_example_ik = ExampleInverseKinematics() - frame = Frame([0, 0, 0], [1, 0, 0], [0, 1, 0]) - ik_result = calculate_example_ik(robot, frame) - # or equivalently: - ik_result = calculate_example_ik.inverse_kinematics(robot, frame) - - -These backend feature interfaces exist in part to enforce a common -signature across all implementations of, say, -``inverse_kinematics`` for greater end-user ease. Please adhere to the -types listed for the arguments and return values listed in the documentation -for the backend features as much as possible. - -These interfaces as exist to allow mixing and matching of the backend -features of various clients to suit the performance and overhead -requirements of the end-user. To illustrate this last point, consider the -following example, where the backend of ``ClientA`` is very efficient at -computing inverse kinematics and has no feature to plan motion, while the -backend of ``ClientB`` is slow to compute inverse kinematics but can plan motion: - -.. code-block:: python - - with ClientA() as client_a, ClientB() as client_b: - inverse_kinematics = ClientAInverseKinematics(client_a) - plan_motion = ClientBPlanMotion(client_b) - -Here we can assign the inverse kinematics to be calculated by the backend -of ``ClientA``, while the motion planning is calculated by the backend of -``ClientB``. (We assume ``ClientA`` and ``ClientB`` inherit from -``ClientInterface`` and that ``ClientAInverseKinematics`` and -``ClientBPlanMotion`` inherit from ``InverseKinematics`` and -``PlanMotion``, resp.) - -Backend interfaces -================== - -.. automodule:: compas_fab.backends.interfaces - -Implemented backend features -============================ - -The following backend features are implemented for the ROS backend: - -.. automodule:: compas_fab.backends.ros.backend_features - -The following backend features are implemented for the PyBullet backend: - -.. automodule:: compas_fab.backends.pybullet.backend_features - diff --git a/docs/developer/grasshopper.md b/docs/developer/grasshopper.md new file mode 100644 index 0000000000..187c71812e --- /dev/null +++ b/docs/developer/grasshopper.md @@ -0,0 +1,14 @@ +# Grasshopper components + +The Grasshopper user objects are built with +[COMPAS Github Action componentizer](https://github.com/compas-dev/compas-actions.ghpython_components). + +1. Apply your changes to the component source code under + [`src/compas_fab/ghpython/components_cpython`](https://github.com/compas-dev/compas_fab/tree/main/src/compas_fab/ghpython/components_cpython). +2. Rebuild them: + + ```bash + invoke build-cpython-ghuser-components + ``` + +3. Install them on Rhino/Grasshopper simply by dropping the generated files into Grasshopper. diff --git a/docs/developer/grasshopper.rst b/docs/developer/grasshopper.rst deleted file mode 100644 index d87b3824de..0000000000 --- a/docs/developer/grasshopper.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. _grasshopper_components: - -******************************************************************************* -Grasshopper components -******************************************************************************* - -Grasshopper user objects need to be built using `COMPAS Github Action componentizer `_. - -1. Apply your changes to the component source code (``src/compas_fab/ghpython/components``). -2. Rebuild them: - - .. code-block:: bash - - invoke build-ghuser-components --gh-io-folder= - -3. Install them on Rhino/Grasshopper as usual: - - .. code-block:: bash - - python -m compas_rhino.install - -The install step does not copy them, but creates a symlink to the location in which they are built, -so after the first installation, it is usually not required to reinstall them, only rebuild them (unless a new component is added). - -.. note:: - - This step requires IronPython 2.7 to be available on the system, ie. `ipy` should be callable from the command line. - The path to the GH_IO.dll is platform-specific, on Mac it is under the ``GrasshopperPlugin.rhp`` of the Rhino app - and on Windows is in the ``Grasshopper`` folder within the Rhino folder in ``ProgramFiles``. diff --git a/docs/developer/icons/icon_system.html b/docs/developer/icons/icon_system.html new file mode 100644 index 0000000000..22683cdd6e --- /dev/null +++ b/docs/developer/icons/icon_system.html @@ -0,0 +1,371 @@ + + + + + +COMPAS FAB — Grasshopper Component Icons + + + + + + +
+ + +
+
+
COMPAS FAB · Robotic Fabrication
+

Grasshopper Icon System

+

A unified 24×24 icon set for the COMPAS FAB component toolbar — built from one geometric kit so the whole library reads as a family.

+
+
+
32 components
+
24×24 px · PNG/SVG
+
7 subcategories
+
rev A · 2026-06
+
+
+ + +
+
+
Grid
+
24 px artboard
3 px keyline · 18² live
+
+
+
Stroke
+
1.8 px monoline
round cap · round join
+
+
+
Ink / Accent
+
+ + + slate · teal-215 +
+
+
+
Accent rule
+
color marks the
semantic subject
+
+
+ + +
+ 00

Primitive Kit

+ shared vocabulary +
+
+ + +
+

In Grasshopper

+ cartesian-motion pipeline +
+
+ Components on canvas · icons at true 24px +
+
+ + +
+ +
+ COMPAS FAB — compas.dev/compas_fab + Icon system · proposal rev A +
+ +
+ + + + + diff --git a/docs/developer/icons/icons.js b/docs/developer/icons/icons.js new file mode 100644 index 0000000000..969907c76d --- /dev/null +++ b/docs/developer/icons/icons.js @@ -0,0 +1,236 @@ +/* COMPAS FAB — Grasshopper component icons + All icons authored on a 24x24 grid, ~3px padding, 1.8 monoline stroke. + class "ac" -> accent stroke | class "adot" -> accent fill + default -> ink stroke | class "dot" -> ink fill + The semantic SUBJECT of each icon carries the accent. */ + +const ICONS = { + /* ---------------- ROBOT CELL ---------------- */ + // iso-cube (the cell volume) drawn from library brackets + cellLibrary: + '' + + '' + + '', + + // cell loaded from a URDF/SRDF document + cellUrdf: + '' + + '' + + '' + + '', + + // cell streamed from a ROS bridge + cellRos: + '' + + '' + + '' + + '', + + // add a tool (cone) to the cell + addTool: + '' + + '' + + '', + + // add a rigid body (box) to the cell + addBody: + '' + + '', + + // rigid body built from a mesh (triangulated patch) + bodyFromMesh: + '' + + '' + + '', + + // tool pulled from the tool library + toolFromLibrary: + '' + + '' + + '', + + // rigid body pulled from the body library + bodyFromLibrary: + '' + + '', + + /* ---------------- CELL STATE ---------------- */ + // default state: robot at home (articulated arm at rest) + defaultState: + '' + + '' + + '' + + '' + + '' + + '', + + // override the robot configuration in a state + setConfig: + '' + + '' + + '' + + '' + + '', + + // attach a tool to the robot flange + attachTool: + '' + + '' + + '' + + '', + + // add a tool to the cell AND attach it (shortcut) + addAttachTool: + '' + + '' + + '' + + '' + + '', + + // attach a rigid body to a robot link + attachBodyLink: + '' + + '' + + '' + + '' + + '', + + // attach a rigid body (workpiece) to a tool + attachBodyTool: + '' + + '' + + '', + + // place an unattached body at a world frame + placeBody: + '' + + '' + + '' + + '', + + // set allowed-collision (touch) links + touchLinks: + '' + + '' + + '' + + '' + + '' + + '', + + /* ---------------- TARGETS ---------------- */ + // fully-constrained frame target (pose + goal ring) + frameTarget: + '' + + '' + + '', + + // point + axis target, rotation free + pointAxisTarget: + '' + + '' + + '', + + // configuration target: joints -> goal + configTarget: + '' + + '' + + '' + + '' + + '', + + // sequence of frame waypoints along a path + frameWaypoints: + '' + + '' + + '' + + '', + + // sequence of point-axis waypoints along a path + pointAxisWaypoints: + '' + + '' + + '' + + '' + + '' + + '' + + '', + + // build a configuration from joint sliders + robotConfig: + '' + + '' + + '', + + /* ---------------- PLANNING ---------------- */ + // forward kinematics: configuration -> frame + fk: + '' + + '' + + '' + + '' + + '', + + // inverse kinematics: frame -> configuration + ik: + '' + + '' + + '' + + '' + + '', + + // free-space (joint interpolated) motion -> goal + planMotion: + '' + + '' + + '', + + // linear cartesian motion through waypoints (hero) + planCartesian: + '' + + '' + + '' + + '' + + '', + + /* ---------------- BACKENDS ---------------- */ + // analytical (closed-form) solver chip + analyticalPlanner: + '' + + '' + + '', + + // MoveIt / ROS-backed planner chip + moveitPlanner: + '' + + '' + + '' + + '' + + '', + + // ROS bridge client (connection) + rosClient: + '' + + '' + + '' + + '', + + /* ---------------- ROS ---------------- */ + // publish to a topic (out) + publish: + '' + + '' + + '', + + // subscribe to a topic (in) + subscribe: + '' + + '' + + '', + + /* ---------------- DISPLAY ---------------- */ + // visualize the robot cell (viewport preview) + visualize: + '' + + '' + + '', +}; diff --git a/docs/developer/icons/index.md b/docs/developer/icons/index.md new file mode 100644 index 0000000000..b4fffc6a6c --- /dev/null +++ b/docs/developer/icons/index.md @@ -0,0 +1,25 @@ +# Grasshopper Icon System for COMPAS FAB + +- **`icon.png`**: true 24×24, transparent, anti-aliased. Each overwrites + the placeholder icon in its component folder. Colors: ink `#2E333A`, + accent `#008EA6` (teal). +- **`icons.js`**: the editable SVG source for every glyph (one 24-grid kit). + Re-render the PNGs from this if you tweak a path. +- **[`icon_system.html`](icon_system.html)**: the catalog / spec sheet + (grid, stroke, primitive kit, all icons grouped by subcategory, plus an + "in Grasshopper" preview). Loads `icons.js` from the same folder. + +## Design rules + +- 24×24 artboard · 3px keyline · 18² live area +- 1.8px monoline stroke · round cap · round join +- Shared kit: cube = cell · square = body · cone = tool · triad = frame · + ring = goal · polyline = motion · brackets = library · chip = backend +- Accent (teal) marks the *semantic subject* of each icon only. + +## Notes + +- These PNGs were exported at native 24px. To regenerate at a different size, + render `icons.js` glyphs onto an N×N canvas (viewBox stays `0 0 24 24`). +- `icon_system.html` is a static reference; if you add it to the mkdocs site, + keep `icons.js` beside it (it is loaded with a relative `