From 791d4e7f003e6e5e75df2b4223fce40e05f87eef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:21:17 +0900 Subject: [PATCH 001/181] Bump matplotlib from 3.9.1.post1 to 3.9.2 in /requirements (#1061) Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.9.1.post1 to 3.9.2. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.9.1.post1...v3.9.2) --- updated-dependencies: - dependency-name: matplotlib dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 54a3c3eaf5..dcba39a756 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ numpy == 2.0.1 scipy == 1.14.0 -matplotlib == 3.9.1.post1 +matplotlib == 3.9.2 cvxpy == 1.5.3 pytest == 8.3.2 # For unit test pytest-xdist == 3.6.1 # For unit test From 6eed25537f3c683047ff154bcd09d7dea2dd898f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 07:07:58 +0900 Subject: [PATCH 002/181] Bump ruff from 0.6.1 to 0.6.2 in /requirements (#1064) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.1 to 0.6.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.1...0.6.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index dcba39a756..f219e1d279 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.2 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.0 # For unit test -ruff == 0.6.1 # For unit test +ruff == 0.6.2 # For unit test From 2b6c9b0888734b4caa4bf1a8f3c810def3dfa0ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:14:07 +0900 Subject: [PATCH 003/181] Bump scipy from 1.14.0 to 1.14.1 in /requirements (#1066) Bumps [scipy](https://github.com/scipy/scipy) from 1.14.0 to 1.14.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.14.0...v1.14.1) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f219e1d279..500002ff0e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,5 @@ numpy == 2.0.1 -scipy == 1.14.0 +scipy == 1.14.1 matplotlib == 3.9.2 cvxpy == 1.5.3 pytest == 8.3.2 # For unit test From b139717b297f90daa063e107154fc1ce96c5066e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:23:07 +0900 Subject: [PATCH 004/181] Bump mypy from 1.11.0 to 1.11.2 in /requirements (#1065) Bumps [mypy](https://github.com/python/mypy) from 1.11.0 to 1.11.2. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.11...v1.11.2) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 500002ff0e..0b8a51fcac 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,5 +4,5 @@ matplotlib == 3.9.2 cvxpy == 1.5.3 pytest == 8.3.2 # For unit test pytest-xdist == 3.6.1 # For unit test -mypy == 1.11.0 # For unit test +mypy == 1.11.2 # For unit test ruff == 0.6.2 # For unit test From dab1a9b2dca0b27bc4eef30d98c516ec655ac2aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 21:34:35 +0900 Subject: [PATCH 005/181] Bump ruff from 0.6.2 to 0.6.3 in /requirements (#1069) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.2 to 0.6.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.2...0.6.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0b8a51fcac..8a27c1e301 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.2 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.2 # For unit test -ruff == 0.6.2 # For unit test +ruff == 0.6.3 # For unit test From 4ae9555a699b2823ea2e065e0ffe11b3622aa2b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:39:10 +0900 Subject: [PATCH 006/181] Bump numpy from 2.0.1 to 2.1.1 in /requirements (#1071) Bumps [numpy](https://github.com/numpy/numpy) from 2.0.1 to 2.1.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.0.1...v2.1.1) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8a27c1e301..87107816bb 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.0.1 +numpy == 2.1.1 scipy == 1.14.1 matplotlib == 3.9.2 cvxpy == 1.5.3 From ad600cd9023e67cd8064b678213d6586d076dd15 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Sep 2024 21:39:27 +0900 Subject: [PATCH 007/181] Bump ruff from 0.6.3 to 0.6.4 in /requirements (#1072) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.3 to 0.6.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.3...0.6.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 87107816bb..c614c4e96a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.2 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.2 # For unit test -ruff == 0.6.3 # For unit test +ruff == 0.6.4 # For unit test From bf69f444afcfffd8ebb44ec8f0988598a0b37ab6 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Mon, 16 Sep 2024 22:03:54 +0900 Subject: [PATCH 008/181] add turning radius calculation doc (#1068) * add turning radius calculation doc * add turning radius calculation doc * add turning radius calculation doc * add turning radius calculation doc * add turning radius calculation doc * add turning radius calculation doc * add turning radius calculation doc --- docs/modules/appendix/appendix_main.rst | 1 + .../steering_motion_model/steering_model.png | Bin 0 -> 72443 bytes .../turning_radius_calc1.png | Bin 0 -> 14926 bytes .../turning_radius_calc2.png | Bin 0 -> 21792 bytes .../appendix/steering_motion_model_main.rst | 97 ++++++++++++++++++ 5 files changed, 98 insertions(+) create mode 100644 docs/modules/appendix/steering_motion_model/steering_model.png create mode 100644 docs/modules/appendix/steering_motion_model/turning_radius_calc1.png create mode 100644 docs/modules/appendix/steering_motion_model/turning_radius_calc2.png create mode 100644 docs/modules/appendix/steering_motion_model_main.rst diff --git a/docs/modules/appendix/appendix_main.rst b/docs/modules/appendix/appendix_main.rst index 8a29d84676..cb1ac04066 100644 --- a/docs/modules/appendix/appendix_main.rst +++ b/docs/modules/appendix/appendix_main.rst @@ -7,6 +7,7 @@ Appendix :maxdepth: 2 :caption: Contents + steering_motion_model Kalmanfilter_basics Kalmanfilter_basics_2 diff --git a/docs/modules/appendix/steering_motion_model/steering_model.png b/docs/modules/appendix/steering_motion_model/steering_model.png new file mode 100644 index 0000000000000000000000000000000000000000..c66dded87afebc082d11743b5bab8b7531da1d20 GIT binary patch literal 72443 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

B=42M# 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 z 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 G{{H|kNPn*Y literal 0 HcmV?d00001 diff --git a/docs/modules/appendix/steering_motion_model_main.rst b/docs/modules/appendix/steering_motion_model_main.rst new file mode 100644 index 0000000000..6e444b7909 --- /dev/null +++ b/docs/modules/appendix/steering_motion_model_main.rst @@ -0,0 +1,97 @@ + +Steering Motion Model +----------------------- + +Turning radius calculation by steering motion model +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The turning Radius represents the radius of the circle when the robot turns, as shown in the diagram below. + +.. image:: steering_motion_model/steering_model.png + +When the steering angle is tilted by :math:`\delta`, +the turning radius :math:`R` can be calculated using the following equation, +based on the geometric relationship between the wheelbase (WB), +which is the distance between the rear wheel center and the front wheel center, +and the assumption that the turning radius circle passes through the center of +the rear wheels in the diagram above. + +:math:`R = \frac{WB}{tan\delta}` + +The curvature :math:`\kappa` is the reciprocal of the turning radius: + +:math:`\kappa = \frac{tan\delta}{WB}` + +In the diagram above, the angular difference :math:`\Delta \theta` in the vehicle’s heading between two points on the turning radius :math:`R` +is the same as the angle of the vector connecting the two points from the center of the turn. + +From the formula for the length of an arc and the radius, + +:math:`\Delta \theta = \frac{s}{R}` + +Here, :math:`s` is the distance between two points on the turning radius. + +So, yaw rate :math:`\omega` can be calculated as follows. + +:math:`\omega = \frac{v}{R}` + +and + +:math:`\omega = v\kappa` + +here, :math:`v` is the velocity of the vehicle. + + +Turning radius calculation by 2 consecutive positions of the robot trajectory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In this section, we will derive the formula for the turning radius from 2 consecutive positions of the robot trajectory. + +.. image:: steering_motion_model/turning_radius_calc1.png + +As shown in the upper diagram above, the robot moves from a point at time :math:`t` to a point at time :math:`t+1`. +Each point is represented by a 2D position :math:`(x_t, y_t)` and an orientation :math:`\theta_t`. + +The distance between the two points is :math:`d = \sqrt{(x_{t+1} - x_t)^2 + (y_{t+1} - y_t)^2}`. + +The angle between the two vectors from the turning center to the two points is :math:`\theta = \theta_{t+1} - \theta_t`. +Here, by drawing a perpendicular line from the center of the turning radius +to a straight line of length :math:`d` connecting two points, +the following equation can be derived from the resulting right triangle. + +:math:`sin\frac{\theta}{2} = \frac{d}{2R}` + +So, the turning radius :math:`R` can be calculated as follows. + +:math:`R = \frac{d}{2sin\frac{\theta}{2}}` + +The curvature :math:`\kappa` is the reciprocal of the turning radius. +So, the curvature can be calculated as follows. + +:math:`\kappa = \frac{2sin\frac{\theta}{2}}{d}` + +Target speed by maximum steering speed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the maximum steering speed is given as :math:`\dot{\delta}_{max}`, +the maximum curvature change rate :math:`\dot{\kappa}_{max}` can be calculated as follows: + +:math:`\dot{\kappa}_{max} = \frac{tan\dot{\delta}_{max}}{WB}` + +From the curvature calculation by 2 consecutive positions of the robot trajectory, + +the maximum curvature change rate :math:`\dot{\kappa}_{max}` can be calculated as follows: + +:math:`\dot{\kappa}_{max} = \frac{\kappa_{t+1}-\kappa_{t}}{\Delta t}` + +If we can assume that the vehicle will not exceed the maximum curvature change rate, + +the target minimum velocity :math:`v_{min}` can be calculated as follows: + +:math:`v_{min} = \frac{d_{t+1}+d_{t}}{\Delta t} = \frac{d_{t+1}+d_{t}}{(\kappa_{t+1}-\kappa_{t})}\frac{tan\dot{\delta}_{max}}{WB}` + + +References: +~~~~~~~~~~~ + +- `Vehicle Dynamics and Control `_ From e26d5cb99aed3d282508cc18457feb7a7b8ca18f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:13:36 +0900 Subject: [PATCH 009/181] Bump pytest from 8.3.2 to 8.3.3 in /requirements (#1077) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.2 to 8.3.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.2...8.3.3) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index c614c4e96a..6bbe33e378 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,7 +2,7 @@ numpy == 2.1.1 scipy == 1.14.1 matplotlib == 3.9.2 cvxpy == 1.5.3 -pytest == 8.3.2 # For unit test +pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.2 # For unit test ruff == 0.6.4 # For unit test From 3e178598d467b65154fb1b409ac9fab8339957b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Sep 2024 21:55:03 +0900 Subject: [PATCH 010/181] Bump ruff from 0.6.4 to 0.6.5 in /requirements (#1078) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.4 to 0.6.5. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.4...0.6.5) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 6bbe33e378..f149183e96 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.2 # For unit test -ruff == 0.6.4 # For unit test +ruff == 0.6.5 # For unit test From 660646a361162b9c286d5182558775eda5207ebe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 06:19:38 -0700 Subject: [PATCH 011/181] Bump ruff from 0.6.5 to 0.6.7 in /requirements (#1080) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.5 to 0.6.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.5...0.6.7) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f149183e96..453922eb8c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.2 # For unit test -ruff == 0.6.5 # For unit test +ruff == 0.6.7 # For unit test From fafae87236eaf95785f5566e35fe9d6ba2fa3701 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 1 Oct 2024 20:51:46 +0900 Subject: [PATCH 012/181] Bump ruff from 0.6.7 to 0.6.8 in /requirements (#1081) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.7 to 0.6.8. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.7...0.6.8) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 453922eb8c..83f9bc438f 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.2 # For unit test -ruff == 0.6.7 # For unit test +ruff == 0.6.8 # For unit test From 857a80febf8aef959d7df441f8948fe073e790d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:05:58 +0900 Subject: [PATCH 013/181] Bump numpy from 2.1.1 to 2.1.2 in /requirements (#1083) Bumps [numpy](https://github.com/numpy/numpy) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.1...v2.1.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 83f9bc438f..e006f944ff 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.1.1 +numpy == 2.1.2 scipy == 1.14.1 matplotlib == 3.9.2 cvxpy == 1.5.3 From a791c79da200435cb89d707320e106c10ca5b6db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Oct 2024 21:37:36 +0900 Subject: [PATCH 014/181] Bump ruff from 0.6.8 to 0.6.9 in /requirements (#1082) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.8 to 0.6.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.8...0.6.9) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e006f944ff..62f9bc183a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.11.2 # For unit test -ruff == 0.6.8 # For unit test +ruff == 0.6.9 # For unit test From 4d009a7916bffa53d2747cc6100180a3521e376c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Oct 2024 12:48:45 +0900 Subject: [PATCH 015/181] Bump mypy from 1.11.2 to 1.12.0 in /requirements (#1084) Bumps [mypy](https://github.com/python/mypy) from 1.11.2 to 1.12.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.11.2...v1.12.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 62f9bc183a..cbab816c8a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,5 +4,5 @@ matplotlib == 3.9.2 cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test -mypy == 1.11.2 # For unit test +mypy == 1.12.0 # For unit test ruff == 0.6.9 # For unit test From 74e9dcc0921f8128488b449901812b1b51e54bad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 22:01:11 +0900 Subject: [PATCH 016/181] Bump ruff from 0.6.9 to 0.7.0 in /requirements (#1088) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.6.9 to 0.7.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.6.9...0.7.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cbab816c8a..f275d9d82e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.12.0 # For unit test -ruff == 0.6.9 # For unit test +ruff == 0.7.0 # For unit test From 899f749f33637159be3515264705ca7fe2d8609e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 22:24:33 +0900 Subject: [PATCH 017/181] Bump mypy from 1.12.0 to 1.12.1 in /requirements (#1089) Bumps [mypy](https://github.com/python/mypy) from 1.12.0 to 1.12.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.12.0...v1.12.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f275d9d82e..0b44092a15 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,5 +4,5 @@ matplotlib == 3.9.2 cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test -mypy == 1.12.0 # For unit test +mypy == 1.12.1 # For unit test ruff == 0.7.0 # For unit test From 9ecc98d3e6af70865d902a88b5d96dffd4764b7c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 30 Oct 2024 23:27:05 +0900 Subject: [PATCH 018/181] Bump mypy from 1.12.1 to 1.13.0 in /requirements (#1093) Bumps [mypy](https://github.com/python/mypy) from 1.12.1 to 1.13.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.12.1...v1.13.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0b44092a15..92ffcf3a74 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,5 +4,5 @@ matplotlib == 3.9.2 cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test -mypy == 1.12.1 # For unit test +mypy == 1.13.0 # For unit test ruff == 0.7.0 # For unit test From 41fea75c0ee0cfb6a1c24307f0e0c1f1bb0af133 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 31 Oct 2024 21:32:30 +0900 Subject: [PATCH 019/181] Bump ruff from 0.7.0 to 0.7.1 in /requirements (#1092) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.0 to 0.7.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.0...0.7.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 92ffcf3a74..9fd97ba106 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test -ruff == 0.7.0 # For unit test +ruff == 0.7.1 # For unit test From 7c5959c5f2d0dbde7889870fc0056491577a5653 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 5 Nov 2024 21:01:38 +0900 Subject: [PATCH 020/181] Bump numpy from 2.1.2 to 2.1.3 in /requirements (#1095) Bumps [numpy](https://github.com/numpy/numpy) from 2.1.2 to 2.1.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.2...v2.1.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9fd97ba106..38a0aed0f8 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.1.2 +numpy == 2.1.3 scipy == 1.14.1 matplotlib == 3.9.2 cvxpy == 1.5.3 From 5d8326957bcb40d62076e2920f5d61290f658fe6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 13 Nov 2024 21:44:25 +0900 Subject: [PATCH 021/181] Bump ruff from 0.7.1 to 0.7.3 in /requirements (#1097) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.1 to 0.7.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.1...0.7.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 38a0aed0f8..21c9c64364 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test -ruff == 0.7.1 # For unit test +ruff == 0.7.3 # For unit test From 3f87bd4cfc9b8cc5a6e3c97dcfd113f020023801 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 19 Nov 2024 07:40:59 +0900 Subject: [PATCH 022/181] Bump ruff from 0.7.3 to 0.7.4 in /requirements (#1099) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.3 to 0.7.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.3...0.7.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 21c9c64364..d3d3627d5c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.3 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test -ruff == 0.7.3 # For unit test +ruff == 0.7.4 # For unit test From 0f91e58ca3d3e67bdbc592105140791efa6b2dbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 08:25:12 +0900 Subject: [PATCH 023/181] Bump pytest from 8.3.3 to 8.3.4 in /requirements (#1102) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.3 to 8.3.4. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.3...8.3.4) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d3d3627d5c..44875f28d6 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,7 +2,7 @@ numpy == 2.1.3 scipy == 1.14.1 matplotlib == 3.9.2 cvxpy == 1.5.3 -pytest == 8.3.3 # For unit test +pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test ruff == 0.7.4 # For unit test From 455542d7178565517ac751a025e103d5ff11ced0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 4 Dec 2024 15:36:51 +0900 Subject: [PATCH 024/181] Bump ruff from 0.7.4 to 0.8.1 in /requirements (#1103) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.7.4 to 0.8.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.7.4...0.8.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 44875f28d6..a224af5d40 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test -ruff == 0.7.4 # For unit test +ruff == 0.8.1 # For unit test From 115b32d2feb03bd96b52efeb2403dfb272c5e703 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 08:31:54 +0900 Subject: [PATCH 025/181] Bump matplotlib from 3.9.2 to 3.9.3 in /requirements (#1101) Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.9.2 to 3.9.3. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.9.2...v3.9.3) --- updated-dependencies: - dependency-name: matplotlib dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a224af5d40..91566508d6 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ numpy == 2.1.3 scipy == 1.14.1 -matplotlib == 3.9.2 +matplotlib == 3.9.3 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test From f86eecec7fa851fcc52aeb09a7c58b4c3c0110d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 06:46:52 +0900 Subject: [PATCH 026/181] Bump numpy from 2.1.3 to 2.2.0 in /requirements (#1106) Bumps [numpy](https://github.com/numpy/numpy) from 2.1.3 to 2.2.0. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.1.3...v2.2.0) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 91566508d6..a99ed51781 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.1.3 +numpy == 2.2.0 scipy == 1.14.1 matplotlib == 3.9.3 cvxpy == 1.5.3 From 4ebe7543ffac5f4eb40bceedec14f11703cc6a1f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:17:18 +0900 Subject: [PATCH 027/181] Bump ruff from 0.8.1 to 0.8.2 in /requirements (#1105) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.1 to 0.8.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.1...0.8.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index a99ed51781..dbc555e0c9 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test -ruff == 0.8.1 # For unit test +ruff == 0.8.2 # For unit test From 0d8cd9bb6cca5697a3cd3f1b279e4cd8a27d3cb7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:32:02 +0900 Subject: [PATCH 028/181] Bump matplotlib from 3.9.3 to 3.10.0 in /requirements (#1107) Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.9.3 to 3.10.0. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.9.3...v3.10.0) --- updated-dependencies: - dependency-name: matplotlib dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index dbc555e0c9..650e7a4048 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ numpy == 2.2.0 scipy == 1.14.1 -matplotlib == 3.9.3 +matplotlib == 3.10.0 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test From e5eea35ee4a03082cd5acf5efea208d677df0a0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Dec 2024 09:37:51 +0900 Subject: [PATCH 029/181] Bump ruff from 0.8.2 to 0.8.3 in /requirements (#1108) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.2 to 0.8.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.2...0.8.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 650e7a4048..cd4a01f88f 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test -ruff == 0.8.2 # For unit test +ruff == 0.8.3 # For unit test From 4bc70480448295205fceb28053d0ed9c5efca99d Mon Sep 17 00:00:00 2001 From: Weipu Shan Date: Fri, 20 Dec 2024 20:32:57 +0800 Subject: [PATCH 030/181] fix: arm_obstacle_navigation calc_heuristic_map node through 4 corners error (#1034) * fix: arm_obstacle_navigation calc_heuristic_map node through 4 corners error * style: remove trailing whitespace --- .../arm_obstacle_navigation/arm_obstacle_navigation.py | 8 ++++---- .../arm_obstacle_navigation_2.py | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py index 75b1f9d2c4..e377897e54 100644 --- a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py +++ b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py @@ -203,10 +203,10 @@ def calc_heuristic_map(M, goal_node): for i in range(heuristic_map.shape[0]): for j in range(heuristic_map.shape[1]): heuristic_map[i, j] = min(heuristic_map[i, j], - i + 1 + heuristic_map[M - 1, j], - M - i + heuristic_map[0, j], - j + 1 + heuristic_map[i, M - 1], - M - j + heuristic_map[i, 0] + M - i - 1 + heuristic_map[M - 1, j], + i + heuristic_map[0, j], + M - j - 1 + heuristic_map[i, M - 1], + j + heuristic_map[i, 0] ) return heuristic_map diff --git a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py index 591cd401eb..10f4615c34 100644 --- a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py +++ b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py @@ -105,7 +105,7 @@ def get_occupancy_grid(arm, obstacles): Args: arm: An instance of NLinkArm obstacles: A list of obstacles, with each obstacle defined as a list - of xy coordinates and a radius. + of xy coordinates and a radius. Returns: Occupancy grid in joint space @@ -234,10 +234,10 @@ def calc_heuristic_map(M, goal_node): for i in range(heuristic_map.shape[0]): for j in range(heuristic_map.shape[1]): heuristic_map[i, j] = min(heuristic_map[i, j], - i + 1 + heuristic_map[M - 1, j], - M - i + heuristic_map[0, j], - j + 1 + heuristic_map[i, M - 1], - M - j + heuristic_map[i, 0] + M - i - 1 + heuristic_map[M - 1, j], + i + heuristic_map[0, j], + M - j - 1 + heuristic_map[i, M - 1], + j + heuristic_map[i, 0] ) return heuristic_map From 1f4214012a612f2163b01930444d83712d99b5f9 Mon Sep 17 00:00:00 2001 From: Aryaz Eghbali <64126826+AryazE@users.noreply.github.com> Date: Fri, 20 Dec 2024 14:00:49 +0100 Subject: [PATCH 031/181] Performance improvement (#1075) --- PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py b/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py index 8968c54dcc..7e93caa8fb 100644 --- a/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py +++ b/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py @@ -64,7 +64,7 @@ def transform( is_visited = np.zeros_like(transform_matrix, dtype=bool) is_visited[src[0], src[1]] = True traversal_queue = [src] - calculated = [(src[0] - 1) * n_cols + src[1]] + calculated = set([(src[0] - 1) * n_cols + src[1]]) def is_valid_neighbor(g_i, g_j): return 0 <= g_i < n_rows and 0 <= g_j < n_cols \ @@ -86,7 +86,7 @@ def is_valid_neighbor(g_i, g_j): if not is_visited[ni][nj] \ and ((ni - 1) * n_cols + nj) not in calculated: traversal_queue.append((ni, nj)) - calculated.append((ni - 1) * n_cols + nj) + calculated.add((ni - 1) * n_cols + nj) return transform_matrix From af456c70b0ec336442b0b32c5aab1a220aefaf74 Mon Sep 17 00:00:00 2001 From: Surabhi Gupta <103124390+SurabhiGupta17@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:42:31 +0530 Subject: [PATCH 032/181] Implement Catmull-Rom Spline with test and documentation (#1085) --- .../blending_functions.py | 34 ++++++ .../catmull_rom_spline_path.py | 86 +++++++++++++++ .../catmull_rom_spline/blending_functions.png | Bin 0 -> 41129 bytes .../catmull_rom_path_planning.png | Bin 0 -> 33505 bytes .../catmull_rom_spline_main.rst | 103 ++++++++++++++++++ tests/test_catmull_rom_spline.py | 16 +++ 6 files changed, 239 insertions(+) create mode 100644 PathPlanning/Catmull_RomSplinePath/blending_functions.py create mode 100644 PathPlanning/Catmull_RomSplinePath/catmull_rom_spline_path.py create mode 100644 docs/modules/path_planning/catmull_rom_spline/blending_functions.png create mode 100644 docs/modules/path_planning/catmull_rom_spline/catmull_rom_path_planning.png create mode 100644 docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst create mode 100644 tests/test_catmull_rom_spline.py diff --git a/PathPlanning/Catmull_RomSplinePath/blending_functions.py b/PathPlanning/Catmull_RomSplinePath/blending_functions.py new file mode 100644 index 0000000000..f3ef5dd323 --- /dev/null +++ b/PathPlanning/Catmull_RomSplinePath/blending_functions.py @@ -0,0 +1,34 @@ +import numpy as np +import matplotlib.pyplot as plt + +def blending_function_1(t): + return -t + 2*t**2 - t**3 + +def blending_function_2(t): + return 2 - 5*t**2 + 3*t**3 + +def blending_function_3(t): + return t + 4*t**2 - 3*t**3 + +def blending_function_4(t): + return -t**2 + t**3 + +def plot_blending_functions(): + t = np.linspace(0, 1, 100) + + plt.plot(t, blending_function_1(t), label='b1') + plt.plot(t, blending_function_2(t), label='b2') + plt.plot(t, blending_function_3(t), label='b3') + plt.plot(t, blending_function_4(t), label='b4') + + plt.title("Catmull-Rom Blending Functions") + plt.xlabel("t") + plt.ylabel("Value") + plt.legend() + plt.grid(True) + plt.axhline(y=0, color='k', linestyle='--') + plt.axvline(x=0, color='k', linestyle='--') + plt.show() + +if __name__ == "__main__": + plot_blending_functions() \ No newline at end of file diff --git a/PathPlanning/Catmull_RomSplinePath/catmull_rom_spline_path.py b/PathPlanning/Catmull_RomSplinePath/catmull_rom_spline_path.py new file mode 100644 index 0000000000..79916330c9 --- /dev/null +++ b/PathPlanning/Catmull_RomSplinePath/catmull_rom_spline_path.py @@ -0,0 +1,86 @@ +""" +Path Planner with Catmull-Rom Spline +Author: Surabhi Gupta (@this_is_surabhi) +Source: http://graphics.cs.cmu.edu/nsp/course/15-462/Fall04/assts/catmullRom.pdf +""" + +import sys +import pathlib +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) + +import numpy as np +import matplotlib.pyplot as plt + +def catmull_rom_point(t, p0, p1, p2, p3): + """ + Parameters + ---------- + t : float + Parameter value (0 <= t <= 1) (0 <= t <= 1) + p0, p1, p2, p3 : np.ndarray + Control points for the spline segment + + Returns + ------- + np.ndarray + Calculated point on the spline + """ + return 0.5 * ((2 * p1) + + (-p0 + p2) * t + + (2 * p0 - 5 * p1 + 4 * p2 - p3) * t**2 + + (-p0 + 3 * p1 - 3 * p2 + p3) * t**3) + + +def catmull_rom_spline(control_points, num_points): + """ + Parameters + ---------- + control_points : list + List of control points + num_points : int + Number of points to generate on the spline + + Returns + ------- + tuple + x and y coordinates of the spline points + """ + t_vals = np.linspace(0, 1, num_points) + spline_points = [] + + control_points = np.array(control_points) + + for i in range(len(control_points) - 1): + if i == 0: + p1, p2, p3 = control_points[i:i+3] + p0 = p1 + elif i == len(control_points) - 2: + p0, p1, p2 = control_points[i-1:i+2] + p3 = p2 + else: + p0, p1, p2, p3 = control_points[i-1:i+3] + + for t in t_vals: + point = catmull_rom_point(t, p0, p1, p2, p3) + spline_points.append(point) + + return np.array(spline_points).T + + +def main(): + print(__file__ + " start!!") + + way_points = [[-1.0, -2.0], [1.0, -1.0], [3.0, -2.0], [4.0, -1.0], [3.0, 1.0], [1.0, 2.0], [0.0, 2.0]] + n_course_point = 100 + spline_x, spline_y = catmull_rom_spline(way_points, n_course_point) + + plt.plot(spline_x,spline_y, '-r', label="Catmull-Rom Spline Path") + plt.plot(np.array(way_points).T[0], np.array(way_points).T[1], '-og', label="Way points") + plt.title("Catmull-Rom Spline Path") + plt.grid(True) + plt.legend() + plt.axis("equal") + plt.show() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/docs/modules/path_planning/catmull_rom_spline/blending_functions.png b/docs/modules/path_planning/catmull_rom_spline/blending_functions.png new file mode 100644 index 0000000000000000000000000000000000000000..928946df46c7f14524247d675697d81e049c9a86 GIT binary patch literal 41129 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

tE>;rj18uQ$r_-%kmJ0~e2j$!iQ~wUR=gX9pvB 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 O%hb^P{1biW*nb0@J+h|& literal 0 HcmV?d00001 diff --git a/docs/modules/path_planning/catmull_rom_spline/catmull_rom_path_planning.png b/docs/modules/path_planning/catmull_rom_spline/catmull_rom_path_planning.png new file mode 100644 index 0000000000000000000000000000000000000000..1d0ff001e60bbe01787ab8f134768a6558542ad7 GIT binary patch literal 33505 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 literal 0 HcmV?d00001 diff --git a/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst b/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst new file mode 100644 index 0000000000..4d8d3bdefe --- /dev/null +++ b/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst @@ -0,0 +1,103 @@ +Catmull-Rom Spline Planning +----------------- + +.. image:: catmull_rom_path_planning.png + +This is a Catmull-Rom spline path planning routine. + +If you provide waypoints, the Catmull-Rom spline generates a smooth path that always passes through the control points, +exhibits local control, and maintains 𝐶1 continuity. + + +Catmull-Rom Spline Fundamentals +~~~~~~~~~~~~~~ + +Catmull-Rom splines are a type of cubic spline that passes through a given set of points, known as control points. + +They are defined by the following equation for calculating a point on the spline: + +:math:`P(t) = 0.5 \times \left( 2P_1 + (-P_0 + P_2)t + (2P_0 - 5P_1 + 4P_2 - P_3)t^2 + (-P_0 + 3P_1 - 3P_2 + P_3)t^3 \right)` + +Where: + +* :math:`P(t)` is the point on the spline at parameter :math:`t`. +* :math:`P_0, P_1, P_2, P_3` are the control points surrounding the parameter :math:`t`. + +Types of Catmull-Rom Splines +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are different types of Catmull-Rom splines based on the choice of the **tau** parameter, which influences how the curve +behaves in relation to the control points: + +1. **Uniform Catmull-Rom Spline**: + The standard implementation where the parameterization is uniform. Each segment of the spline is treated equally, + regardless of the distances between control points. + + +2. **Chordal Catmull-Rom Spline**: + This spline type takes into account the distance between control points. The parameterization is based on the actual distance + along the spline, ensuring smoother transitions. The equation can be modified to include the chord length :math:`L_i` between + points :math:`P_i` and :math:`P_{i+1}`: + + .. math:: + \tau_i = \sqrt{(x_{i+1} - x_i)^2 + (y_{i+1} - y_i)^2} + +3. **Centripetal Catmull-Rom Spline**: + This variation improves upon the chordal spline by using the square root of the distance to determine the parameterization, + which avoids oscillations and creates a more natural curve. The parameter :math:`t_i` is adjusted using the following relation: + + .. math:: + t_i = \sqrt{(x_{i+1} - x_i)^2 + (y_{i+1} - y_i)^2} + + +Blending Functions +~~~~~~~~~~~~~~~~~~ + +In Catmull-Rom spline interpolation, blending functions are used to calculate the influence of each control point on the spline at a +given parameter :math:`t`. The blending functions ensure that the spline is smooth and passes through the control points while +maintaining continuity. The four blending functions used in Catmull-Rom splines are defined as follows: + +1. **Blending Function 1**: + + .. math:: + b_1(t) = -t + 2t^2 - t^3 + +2. **Blending Function 2**: + + .. math:: + b_2(t) = 2 - 5t^2 + 3t^3 + +3. **Blending Function 3**: + + .. math:: + b_3(t) = t + 4t^2 - 3t^3 + +4. **Blending Function 4**: + + .. math:: + b_4(t) = -t^2 + t^3 + +The blending functions are combined in the spline equation to create a smooth curve that reflects the influence of each control point. + +The following figure illustrates the blending functions over the interval :math:`[0, 1]`: + +.. image:: blending_functions.png + +Catmull-Rom Spline API +~~~~~~~~~~~~~~~~~~~~~~~ + +This section provides an overview of the functions used for Catmull-Rom spline path planning. + +API +++++ + +.. autofunction:: PathPlanning.Catmull_RomSplinePath.catmull_rom_spline_path.catmull_rom_point + +.. autofunction:: PathPlanning.Catmull_RomSplinePath.catmull_rom_spline_path.catmull_rom_spline + + +References +~~~~~~~~~~ + +- `Catmull-Rom Spline - Wikipedia `__ +- `Catmull-Rom Splines `__ \ No newline at end of file diff --git a/tests/test_catmull_rom_spline.py b/tests/test_catmull_rom_spline.py new file mode 100644 index 0000000000..41a73588c3 --- /dev/null +++ b/tests/test_catmull_rom_spline.py @@ -0,0 +1,16 @@ +import conftest +from PathPlanning.Catmull_RomSplinePath.catmull_rom_spline_path import catmull_rom_spline + +def test_catmull_rom_spline(): + way_points = [[0, 0], [1, 2], [2, 0], [3, 3]] + num_points = 100 + + spline_x, spline_y = catmull_rom_spline(way_points, num_points) + + assert spline_x.size > 0, "Spline X coordinates should not be empty" + assert spline_y.size > 0, "Spline Y coordinates should not be empty" + + assert spline_x.shape == spline_y.shape, "Spline X and Y coordinates should have the same shape" + +if __name__ == '__main__': + conftest.run_this_test(__file__) From 7eeb9d215cb5652a800db507f066047bfd5d023f Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sat, 21 Dec 2024 15:04:02 +0900 Subject: [PATCH 033/181] Add catmull_rom_spline doc (#1109) --- docs/modules/path_planning/path_planning_main.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/modules/path_planning/path_planning_main.rst b/docs/modules/path_planning/path_planning_main.rst index a3237f16f1..30802fd7a6 100644 --- a/docs/modules/path_planning/path_planning_main.rst +++ b/docs/modules/path_planning/path_planning_main.rst @@ -18,6 +18,7 @@ Path Planning rrt/rrt cubic_spline/cubic_spline bspline_path/bspline_path + catmull_rom_spline/catmull_rom_spline clothoid_path/clothoid_path eta3_spline/eta3_spline bezier_path/bezier_path From b5988dbcbc3c1d8cdbb3f9f3f9dec75cf63defb9 Mon Sep 17 00:00:00 2001 From: parmaski <89462537+parmaski@users.noreply.github.com> Date: Sat, 21 Dec 2024 19:39:16 +0900 Subject: [PATCH 034/181] Fix: Hybrid A* direction is incorrect (#1086) * fix to preseve direction history * add path to Reeds Shepp and its users e.g. hybrid A* --- PathPlanning/HybridAStar/hybrid_a_star.py | 5 +++-- PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/PathPlanning/HybridAStar/hybrid_a_star.py b/PathPlanning/HybridAStar/hybrid_a_star.py index 03db0dc167..1962efe295 100644 --- a/PathPlanning/HybridAStar/hybrid_a_star.py +++ b/PathPlanning/HybridAStar/hybrid_a_star.py @@ -105,12 +105,13 @@ def calc_next_node(current, steer, direction, config, ox, oy, kd_tree): x, y, yaw = current.x_list[-1], current.y_list[-1], current.yaw_list[-1] arc_l = XY_GRID_RESOLUTION * 1.5 - x_list, y_list, yaw_list = [], [], [] + x_list, y_list, yaw_list, direction_list = [], [], [], [] for _ in np.arange(0, arc_l, MOTION_RESOLUTION): x, y, yaw = move(x, y, yaw, MOTION_RESOLUTION * direction, steer) x_list.append(x) y_list.append(y) yaw_list.append(yaw) + direction_list.append(direction == 1) if not check_car_collision(x_list, y_list, yaw_list, ox, oy, kd_tree): return None @@ -134,7 +135,7 @@ def calc_next_node(current, steer, direction, config, ox, oy, kd_tree): cost = current.cost + added_cost + arc_l node = Node(x_ind, y_ind, yaw_ind, d, x_list, - y_list, yaw_list, [d], + y_list, yaw_list, direction_list, parent_index=calc_index(current, config), cost=cost, steer=steer) diff --git a/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py b/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py index 8e134ff38b..618d1d99ba 100644 --- a/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py +++ b/PathPlanning/ReedsSheppPath/reeds_shepp_path_planning.py @@ -6,6 +6,10 @@ co-author Videh Patel(@videh25) : Added the missing RS paths """ +import sys +import pathlib +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) + import math import matplotlib.pyplot as plt From b137ba1817af11ed944a28f8c1c8ad1a95848726 Mon Sep 17 00:00:00 2001 From: parmaski <89462537+parmaski@users.noreply.github.com> Date: Sat, 21 Dec 2024 19:40:30 +0900 Subject: [PATCH 035/181] fix behavior when path is not found (#1104) --- PathPlanning/HybridAStar/car.py | 2 +- PathPlanning/HybridAStar/hybrid_a_star.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/PathPlanning/HybridAStar/car.py b/PathPlanning/HybridAStar/car.py index d7631133c7..959db74078 100644 --- a/PathPlanning/HybridAStar/car.py +++ b/PathPlanning/HybridAStar/car.py @@ -97,7 +97,7 @@ def pi_2_pi(angle): def move(x, y, yaw, distance, steer, L=WB): x += distance * cos(yaw) y += distance * sin(yaw) - yaw += pi_2_pi(distance * tan(steer) / L) # distance/2 + yaw = pi_2_pi(yaw + distance * tan(steer) / L) # distance/2 return x, y, yaw diff --git a/PathPlanning/HybridAStar/hybrid_a_star.py b/PathPlanning/HybridAStar/hybrid_a_star.py index 1962efe295..0fa04189c6 100644 --- a/PathPlanning/HybridAStar/hybrid_a_star.py +++ b/PathPlanning/HybridAStar/hybrid_a_star.py @@ -282,7 +282,7 @@ def hybrid_a_star_planning(start, goal, ox, oy, xy_resolution, yaw_resolution): while True: if not openList: print("Error: Cannot find path, No open set") - return [], [], [] + return Path([], [], [], [], 0) cost, c_id = heapq.heappop(pq) if c_id in openList: From ca54f1b6880b9731b06fc33db338bc28bd63c864 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 07:50:00 +0900 Subject: [PATCH 036/181] Bump ruff from 0.8.3 to 0.8.4 in /requirements (#1111) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.3 to 0.8.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.3...0.8.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cd4a01f88f..d28cd5f956 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.13.0 # For unit test -ruff == 0.8.3 # For unit test +ruff == 0.8.4 # For unit test From d1f1190106d624e555b0fdd0853b65ad844e3204 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 07:59:16 +0900 Subject: [PATCH 037/181] Bump numpy from 2.2.0 to 2.2.1 in /requirements (#1113) Bumps [numpy](https://github.com/numpy/numpy) from 2.2.0 to 2.2.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.0...v2.2.1) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d28cd5f956..0992ab6cf0 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.2.0 +numpy == 2.2.1 scipy == 1.14.1 matplotlib == 3.10.0 cvxpy == 1.5.3 From aacae0d79a9595356bd7f15ad8e565220a04de61 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:13:35 +0900 Subject: [PATCH 038/181] Bump mypy from 1.13.0 to 1.14.0 in /requirements (#1112) Bumps [mypy](https://github.com/python/mypy) from 1.13.0 to 1.14.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.13.0...v1.14.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 0992ab6cf0..47db4245fd 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,5 +4,5 @@ matplotlib == 3.10.0 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test -mypy == 1.13.0 # For unit test +mypy == 1.14.0 # For unit test ruff == 0.8.4 # For unit test From 7d33f5f7140513d14bad016f48d132a08bc5c8a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 31 Dec 2024 07:04:57 +0900 Subject: [PATCH 039/181] Bump mypy from 1.14.0 to 1.14.1 in /requirements (#1116) Bumps [mypy](https://github.com/python/mypy) from 1.14.0 to 1.14.1. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.0...v1.14.1) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 47db4245fd..bcfbbf97ac 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,5 +4,5 @@ matplotlib == 3.10.0 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test -mypy == 1.14.0 # For unit test +mypy == 1.14.1 # For unit test ruff == 0.8.4 # For unit test From c27803f781c00384c9de29fe1fd82ace8279d788 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 11:49:14 +0900 Subject: [PATCH 040/181] build(deps): bump ruff from 0.8.4 to 0.8.6 in /requirements (#1117) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.4 to 0.8.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.4...0.8.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index bcfbbf97ac..5291c45c58 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.14.1 # For unit test -ruff == 0.8.4 # For unit test +ruff == 0.8.6 # For unit test From 01d398f4ab276bcbcac3a7ce8d62b2e2712f0a5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:01:34 +0900 Subject: [PATCH 041/181] build(deps): bump scipy from 1.14.1 to 1.15.0 in /requirements (#1118) Bumps [scipy](https://github.com/scipy/scipy) from 1.14.1 to 1.15.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.14.1...v1.15.0) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5291c45c58..5aeded446f 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,5 @@ numpy == 2.2.1 -scipy == 1.14.1 +scipy == 1.15.0 matplotlib == 3.10.0 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test From 647ce9ccfa1f581bff558a16803bfd7e2c343c3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 07:24:59 +0900 Subject: [PATCH 042/181] build(deps): bump scipy from 1.15.0 to 1.15.1 in /requirements (#1119) Bumps [scipy](https://github.com/scipy/scipy) from 1.15.0 to 1.15.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.15.0...v1.15.1) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5aeded446f..f5a7ff315c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,5 @@ numpy == 2.2.1 -scipy == 1.15.0 +scipy == 1.15.1 matplotlib == 3.10.0 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test From 4cf7531839a12faa9b7daf6a70aab4f596aa1b0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 07:25:29 +0900 Subject: [PATCH 043/181] build(deps): bump ruff from 0.8.6 to 0.9.1 in /requirements (#1120) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.8.6 to 0.9.1. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.8.6...0.9.1) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f5a7ff315c..d212202cdb 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.14.1 # For unit test -ruff == 0.8.6 # For unit test +ruff == 0.9.1 # For unit test From 4c2e3e8f9b686baef5774dcfe0afd7dda6150af4 Mon Sep 17 00:00:00 2001 From: Mritunjai <101278178+Mritunjaii@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:59:15 +0530 Subject: [PATCH 044/181] hashmap instead of a linear search (#1110) --- .../AStar/a_star_searching_from_two_side.py | 70 +++++++++---------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/PathPlanning/AStar/a_star_searching_from_two_side.py b/PathPlanning/AStar/a_star_searching_from_two_side.py index 822f5ea500..f43cea71b4 100644 --- a/PathPlanning/AStar/a_star_searching_from_two_side.py +++ b/PathPlanning/AStar/a_star_searching_from_two_side.py @@ -78,43 +78,43 @@ def boundary_and_obstacles(start, goal, top_vertex, bottom_vertex, obs_number): def find_neighbor(node, ob, closed): - # generate neighbors in certain condition - ob_list = ob.tolist() - neighbor: list = [] + # Convert obstacles to a set for faster lookup + ob_set = set(map(tuple, ob.tolist())) + neighbor_set = set() + + # Generate neighbors within the 3x3 grid around the node for x in range(node.coordinate[0] - 1, node.coordinate[0] + 2): for y in range(node.coordinate[1] - 1, node.coordinate[1] + 2): - if [x, y] not in ob_list: - # find all possible neighbor nodes - neighbor.append([x, y]) - # remove node violate the motion rule - # 1. remove node.coordinate itself - neighbor.remove(node.coordinate) - # 2. remove neighbor nodes who cross through two diagonal - # positioned obstacles since there is no enough space for - # robot to go through two diagonal positioned obstacles - - # top bottom left right neighbors of node - top_nei = [node.coordinate[0], node.coordinate[1] + 1] - bottom_nei = [node.coordinate[0], node.coordinate[1] - 1] - left_nei = [node.coordinate[0] - 1, node.coordinate[1]] - right_nei = [node.coordinate[0] + 1, node.coordinate[1]] - # neighbors in four vertex - lt_nei = [node.coordinate[0] - 1, node.coordinate[1] + 1] - rt_nei = [node.coordinate[0] + 1, node.coordinate[1] + 1] - lb_nei = [node.coordinate[0] - 1, node.coordinate[1] - 1] - rb_nei = [node.coordinate[0] + 1, node.coordinate[1] - 1] - - # remove the unnecessary neighbors - if top_nei and left_nei in ob_list and lt_nei in neighbor: - neighbor.remove(lt_nei) - if top_nei and right_nei in ob_list and rt_nei in neighbor: - neighbor.remove(rt_nei) - if bottom_nei and left_nei in ob_list and lb_nei in neighbor: - neighbor.remove(lb_nei) - if bottom_nei and right_nei in ob_list and rb_nei in neighbor: - neighbor.remove(rb_nei) - neighbor = [x for x in neighbor if x not in closed] - return neighbor + coord = (x, y) + if coord not in ob_set and coord != tuple(node.coordinate): + neighbor_set.add(coord) + + # Define neighbors in cardinal and diagonal directions + top_nei = (node.coordinate[0], node.coordinate[1] + 1) + bottom_nei = (node.coordinate[0], node.coordinate[1] - 1) + left_nei = (node.coordinate[0] - 1, node.coordinate[1]) + right_nei = (node.coordinate[0] + 1, node.coordinate[1]) + lt_nei = (node.coordinate[0] - 1, node.coordinate[1] + 1) + rt_nei = (node.coordinate[0] + 1, node.coordinate[1] + 1) + lb_nei = (node.coordinate[0] - 1, node.coordinate[1] - 1) + rb_nei = (node.coordinate[0] + 1, node.coordinate[1] - 1) + + # Remove neighbors that violate diagonal motion rules + if top_nei in ob_set and left_nei in ob_set: + neighbor_set.discard(lt_nei) + if top_nei in ob_set and right_nei in ob_set: + neighbor_set.discard(rt_nei) + if bottom_nei in ob_set and left_nei in ob_set: + neighbor_set.discard(lb_nei) + if bottom_nei in ob_set and right_nei in ob_set: + neighbor_set.discard(rb_nei) + + # Filter out neighbors that are in the closed set + closed_set = set(map(tuple, closed)) + neighbor_set -= closed_set + + return list(neighbor_set) + def find_node_index(coordinate, node_list): From 1cb45a5d3bdcb07b254c20ce7763d527435c355e Mon Sep 17 00:00:00 2001 From: Aryaz Eghbali <64126826+AryazE@users.noreply.github.com> Date: Mon, 20 Jan 2025 09:16:20 +0100 Subject: [PATCH 045/181] Fixed multitype list (#1076) * Fixed multitype list * Cast to float * Reverted to all floats * Moved all remaining to float --- .../BreadthFirstSearch/breadth_first_search.py | 10 +++++----- PathPlanning/Dijkstra/dijkstra.py | 10 +++++----- .../ProbabilisticRoadMap/probabilistic_road_map.py | 10 +++++----- PathPlanning/VoronoiRoadMap/voronoi_road_map.py | 10 +++++----- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/PathPlanning/BreadthFirstSearch/breadth_first_search.py b/PathPlanning/BreadthFirstSearch/breadth_first_search.py index ff662170e7..ad994732a5 100644 --- a/PathPlanning/BreadthFirstSearch/breadth_first_search.py +++ b/PathPlanning/BreadthFirstSearch/breadth_first_search.py @@ -220,20 +220,20 @@ def main(): # set obstacle positions ox, oy = [], [] for i in range(-10, 60): - ox.append(i) + ox.append(float(i)) oy.append(-10.0) for i in range(-10, 60): ox.append(60.0) - oy.append(i) + oy.append(float(i)) for i in range(-10, 61): - ox.append(i) + ox.append(float(i)) oy.append(60.0) for i in range(-10, 61): ox.append(-10.0) - oy.append(i) + oy.append(float(i)) for i in range(-10, 40): ox.append(20.0) - oy.append(i) + oy.append(float(i)) for i in range(0, 40): ox.append(40.0) oy.append(60.0 - i) diff --git a/PathPlanning/Dijkstra/dijkstra.py b/PathPlanning/Dijkstra/dijkstra.py index 004e49f15a..8a585e4b18 100644 --- a/PathPlanning/Dijkstra/dijkstra.py +++ b/PathPlanning/Dijkstra/dijkstra.py @@ -221,20 +221,20 @@ def main(): # set obstacle positions ox, oy = [], [] for i in range(-10, 60): - ox.append(i) + ox.append(float(i)) oy.append(-10.0) for i in range(-10, 60): ox.append(60.0) - oy.append(i) + oy.append(float(i)) for i in range(-10, 61): - ox.append(i) + ox.append(float(i)) oy.append(60.0) for i in range(-10, 61): ox.append(-10.0) - oy.append(i) + oy.append(float(i)) for i in range(-10, 40): ox.append(20.0) - oy.append(i) + oy.append(float(i)) for i in range(0, 40): ox.append(40.0) oy.append(60.0 - i) diff --git a/PathPlanning/ProbabilisticRoadMap/probabilistic_road_map.py b/PathPlanning/ProbabilisticRoadMap/probabilistic_road_map.py index 294c389023..8bacfd5d19 100644 --- a/PathPlanning/ProbabilisticRoadMap/probabilistic_road_map.py +++ b/PathPlanning/ProbabilisticRoadMap/probabilistic_road_map.py @@ -273,20 +273,20 @@ def main(rng=None): oy = [] for i in range(60): - ox.append(i) + ox.append(float(i)) oy.append(0.0) for i in range(60): ox.append(60.0) - oy.append(i) + oy.append(float(i)) for i in range(61): - ox.append(i) + ox.append(float(i)) oy.append(60.0) for i in range(61): ox.append(0.0) - oy.append(i) + oy.append(float(i)) for i in range(40): ox.append(20.0) - oy.append(i) + oy.append(float(i)) for i in range(40): ox.append(40.0) oy.append(60.0 - i) diff --git a/PathPlanning/VoronoiRoadMap/voronoi_road_map.py b/PathPlanning/VoronoiRoadMap/voronoi_road_map.py index 0a1f6f5526..a27e1b6928 100644 --- a/PathPlanning/VoronoiRoadMap/voronoi_road_map.py +++ b/PathPlanning/VoronoiRoadMap/voronoi_road_map.py @@ -146,20 +146,20 @@ def main(): oy = [] for i in range(60): - ox.append(i) + ox.append(float(i)) oy.append(0.0) for i in range(60): ox.append(60.0) - oy.append(i) + oy.append(float(i)) for i in range(61): - ox.append(i) + ox.append(float(i)) oy.append(60.0) for i in range(61): ox.append(0.0) - oy.append(i) + oy.append(float(i)) for i in range(40): ox.append(20.0) - oy.append(i) + oy.append(float(i)) for i in range(40): ox.append(40.0) oy.append(60.0 - i) From 9672434483459a00b13ece407079fbb53dd8a6b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 08:02:30 +0900 Subject: [PATCH 046/181] build(deps): bump numpy from 2.2.1 to 2.2.2 in /requirements (#1122) Bumps [numpy](https://github.com/numpy/numpy) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.1...v2.2.2) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index d212202cdb..e1c117cb1e 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.2.1 +numpy == 2.2.2 scipy == 1.15.1 matplotlib == 3.10.0 cvxpy == 1.5.3 From 2bc59fe61247982eaed5b0b7eab842658a5c07d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jan 2025 08:15:53 +0900 Subject: [PATCH 047/181] build(deps): bump ruff from 0.9.1 to 0.9.2 in /requirements (#1123) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.1 to 0.9.2. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.1...0.9.2) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e1c117cb1e..11b91abb1f 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.14.1 # For unit test -ruff == 0.9.1 # For unit test +ruff == 0.9.2 # For unit test From 5a66105ff5dc9924fc6b8f8986afaf52f0fd870d Mon Sep 17 00:00:00 2001 From: Aglargil <34728006+Aglargil@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:57:12 +0800 Subject: [PATCH 048/181] Extend frenet_optimal_trajectory to support more scenarios (#1114) * feat: Add third derivative and curvature rate calculations to CubicSpline classes * feat: Extend frenet_optimal_trajectory to support more scenarios * fix: frenet optimal trajectory type check * fix: cubic spline planner code style check * fix: frenet optimal trajectory review * feat: frenet_optimal_trajectory update doc * fix: frenet optimal trajectory review * fix: frenet optimal trajectory * fix: frenet optimal trajectory --- .../CubicSpline/cubic_spline_planner.py | 67 +++ .../cartesian_frenet_converter.py | 144 +++++ .../frenet_optimal_trajectory.py | 550 +++++++++++++----- .../frenet_frame_path_main.rst | 30 +- tests/test_frenet_optimal_trajectory.py | 40 +- 5 files changed, 674 insertions(+), 157 deletions(-) create mode 100644 PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py diff --git a/PathPlanning/CubicSpline/cubic_spline_planner.py b/PathPlanning/CubicSpline/cubic_spline_planner.py index 7cc7bedc79..2391f67c39 100644 --- a/PathPlanning/CubicSpline/cubic_spline_planner.py +++ b/PathPlanning/CubicSpline/cubic_spline_planner.py @@ -76,6 +76,11 @@ def calc_position(self, x): if `x` is outside the data point's `x` range, return None. + Parameters + ---------- + x : float + x position to calculate y. + Returns ------- y : float @@ -99,6 +104,11 @@ def calc_first_derivative(self, x): if x is outside the input x, return None + Parameters + ---------- + x : float + x position to calculate first derivative. + Returns ------- dy : float @@ -121,6 +131,11 @@ def calc_second_derivative(self, x): if x is outside the input x, return None + Parameters + ---------- + x : float + x position to calculate second derivative. + Returns ------- ddy : float @@ -137,6 +152,31 @@ def calc_second_derivative(self, x): ddy = 2.0 * self.c[i] + 6.0 * self.d[i] * dx return ddy + def calc_third_derivative(self, x): + """ + Calc third derivative at given x. + + if x is outside the input x, return None + + Parameters + ---------- + x : float + x position to calculate third derivative. + + Returns + ------- + dddy : float + third derivative for given x. + """ + if x < self.x[0]: + return None + elif x > self.x[-1]: + return None + + i = self.__search_index(x) + dddy = 6.0 * self.d[i] + return dddy + def __search_index(self, x): """ search data segment index @@ -287,6 +327,33 @@ def calc_curvature(self, s): k = (ddy * dx - ddx * dy) / ((dx ** 2 + dy ** 2)**(3 / 2)) return k + def calc_curvature_rate(self, s): + """ + calc curvature rate + + Parameters + ---------- + s : float + distance from the start point. if `s` is outside the data point's + range, return None. + + Returns + ------- + k : float + curvature rate for given s. + """ + dx = self.sx.calc_first_derivative(s) + dy = self.sy.calc_first_derivative(s) + ddx = self.sx.calc_second_derivative(s) + ddy = self.sy.calc_second_derivative(s) + dddx = self.sx.calc_third_derivative(s) + dddy = self.sy.calc_third_derivative(s) + a = dx * ddy - dy * ddx + b = dx * dddy - dy * dddx + c = dx * ddx + dy * ddy + d = dx * dx + dy * dy + return (b * d - 3.0 * a * c) / (d * d * d) + def calc_yaw(self, s): """ calc yaw diff --git a/PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py b/PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py new file mode 100644 index 0000000000..4cc8650c89 --- /dev/null +++ b/PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py @@ -0,0 +1,144 @@ +""" + +A converter between Cartesian and Frenet coordinate systems + +author: Wang Zheng (@Aglargil) + +Ref: + +- [Optimal Trajectory Generation for Dynamic Street Scenarios in a Frenet Frame] +(https://www.researchgate.net/profile/Moritz_Werling/publication/224156269_Optimal_Trajectory_Generation_for_Dynamic_Street_Scenarios_in_a_Frenet_Frame/links/54f749df0cf210398e9277af.pdf) + +""" + +import math + + +class CartesianFrenetConverter: + """ + A class for converting states between Cartesian and Frenet coordinate systems + """ + + @ staticmethod + def cartesian_to_frenet(rs, rx, ry, rtheta, rkappa, rdkappa, x, y, v, a, theta, kappa): + """ + Convert state from Cartesian coordinate to Frenet coordinate + + Parameters + ---------- + rs: reference line s-coordinate + rx, ry: reference point coordinates + rtheta: reference point heading + rkappa: reference point curvature + rdkappa: reference point curvature rate + x, y: current position + v: velocity + a: acceleration + theta: heading angle + kappa: curvature + + Returns + ------- + s_condition: [s(t), s'(t), s''(t)] + d_condition: [d(s), d'(s), d''(s)] + """ + dx = x - rx + dy = y - ry + + cos_theta_r = math.cos(rtheta) + sin_theta_r = math.sin(rtheta) + + cross_rd_nd = cos_theta_r * dy - sin_theta_r * dx + d = math.copysign(math.hypot(dx, dy), cross_rd_nd) + + delta_theta = theta - rtheta + tan_delta_theta = math.tan(delta_theta) + cos_delta_theta = math.cos(delta_theta) + + one_minus_kappa_r_d = 1 - rkappa * d + d_dot = one_minus_kappa_r_d * tan_delta_theta + + kappa_r_d_prime = rdkappa * d + rkappa * d_dot + + d_ddot = (-kappa_r_d_prime * tan_delta_theta + + one_minus_kappa_r_d / (cos_delta_theta * cos_delta_theta) * + (kappa * one_minus_kappa_r_d / cos_delta_theta - rkappa)) + + s = rs + s_dot = v * cos_delta_theta / one_minus_kappa_r_d + + delta_theta_prime = one_minus_kappa_r_d / cos_delta_theta * kappa - rkappa + s_ddot = (a * cos_delta_theta - + s_dot * s_dot * + (d_dot * delta_theta_prime - kappa_r_d_prime)) / one_minus_kappa_r_d + + return [s, s_dot, s_ddot], [d, d_dot, d_ddot] + + @ staticmethod + def frenet_to_cartesian(rs, rx, ry, rtheta, rkappa, rdkappa, s_condition, d_condition): + """ + Convert state from Frenet coordinate to Cartesian coordinate + + Parameters + ---------- + rs: reference line s-coordinate + rx, ry: reference point coordinates + rtheta: reference point heading + rkappa: reference point curvature + rdkappa: reference point curvature rate + s_condition: [s(t), s'(t), s''(t)] + d_condition: [d(s), d'(s), d''(s)] + + Returns + ------- + x, y: position + theta: heading angle + kappa: curvature + v: velocity + a: acceleration + """ + if abs(rs - s_condition[0]) >= 1.0e-6: + raise ValueError( + "The reference point s and s_condition[0] don't match") + + cos_theta_r = math.cos(rtheta) + sin_theta_r = math.sin(rtheta) + + x = rx - sin_theta_r * d_condition[0] + y = ry + cos_theta_r * d_condition[0] + + one_minus_kappa_r_d = 1 - rkappa * d_condition[0] + + tan_delta_theta = d_condition[1] / one_minus_kappa_r_d + delta_theta = math.atan2(d_condition[1], one_minus_kappa_r_d) + cos_delta_theta = math.cos(delta_theta) + + theta = CartesianFrenetConverter.normalize_angle(delta_theta + rtheta) + + kappa_r_d_prime = rdkappa * d_condition[0] + rkappa * d_condition[1] + + kappa = (((d_condition[2] + kappa_r_d_prime * tan_delta_theta) * + cos_delta_theta * cos_delta_theta) / one_minus_kappa_r_d + rkappa) * \ + cos_delta_theta / one_minus_kappa_r_d + + d_dot = d_condition[1] * s_condition[1] + v = math.sqrt(one_minus_kappa_r_d * one_minus_kappa_r_d * + s_condition[1] * s_condition[1] + d_dot * d_dot) + + delta_theta_prime = one_minus_kappa_r_d / cos_delta_theta * kappa - rkappa + + a = (s_condition[2] * one_minus_kappa_r_d / cos_delta_theta + + s_condition[1] * s_condition[1] / cos_delta_theta * + (d_condition[1] * delta_theta_prime - kappa_r_d_prime)) + + return x, y, theta, kappa, v, a + + @ staticmethod + def normalize_angle(angle): + """ + Normalize angle to [-pi, pi] + """ + a = math.fmod(angle + math.pi, 2.0 * math.pi) + if a < 0.0: + a += 2.0 * math.pi + return a - math.pi diff --git a/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py b/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py index 331df36309..248894c1c6 100644 --- a/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py +++ b/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py @@ -17,43 +17,314 @@ import numpy as np import matplotlib.pyplot as plt import copy -import math import sys import pathlib + sys.path.append(str(pathlib.Path(__file__).parent.parent)) -from QuinticPolynomialsPlanner.quintic_polynomials_planner import \ - QuinticPolynomial +from QuinticPolynomialsPlanner.quintic_polynomials_planner import QuinticPolynomial from CubicSpline import cubic_spline_planner -SIM_LOOP = 500 +from enum import Enum, auto +from FrenetOptimalTrajectory.cartesian_frenet_converter import ( + CartesianFrenetConverter, +) + + +class LateralMovement(Enum): + HIGH_SPEED = auto() + LOW_SPEED = auto() + + +class LongitudinalMovement(Enum): + MERGING_AND_STOPPING = auto() + VELOCITY_KEEPING = auto() + + +# Default Parameters + +LATERAL_MOVEMENT = LateralMovement.HIGH_SPEED +LONGITUDINAL_MOVEMENT = LongitudinalMovement.VELOCITY_KEEPING -# Parameter MAX_SPEED = 50.0 / 3.6 # maximum speed [m/s] -MAX_ACCEL = 2.0 # maximum acceleration [m/ss] +MAX_ACCEL = 5.0 # maximum acceleration [m/ss] MAX_CURVATURE = 1.0 # maximum curvature [1/m] -MAX_ROAD_WIDTH = 7.0 # maximum road width [m] -D_ROAD_W = 1.0 # road width sampling length [m] DT = 0.2 # time tick [s] MAX_T = 5.0 # max prediction time [m] MIN_T = 4.0 # min prediction time [m] -TARGET_SPEED = 30.0 / 3.6 # target speed [m/s] -D_T_S = 5.0 / 3.6 # target speed sampling length [m/s] N_S_SAMPLE = 1 # sampling number of target speed -ROBOT_RADIUS = 2.0 # robot radius [m] # cost weights K_J = 0.1 K_T = 0.1 +K_S_DOT = 1.0 K_D = 1.0 +K_S = 1.0 K_LAT = 1.0 K_LON = 1.0 +SIM_LOOP = 500 show_animation = True -class QuarticPolynomial: +if LATERAL_MOVEMENT == LateralMovement.LOW_SPEED: + MAX_ROAD_WIDTH = 1.0 # maximum road width [m] + D_ROAD_W = 0.2 # road width sampling length [m] + TARGET_SPEED = 3.0 / 3.6 # maximum speed [m/s] + D_T_S = 0.5 / 3.6 # target speed sampling length [m/s] + # Waypoints + WX = [0.0, 2.0, 4.0, 6.0, 8.0, 10.0] + WY = [0.0, 0.0, 1.0, 0.0, -1.0, -2.0] + OBSTACLES = np.array([[3.0, 1.0], [5.0, -0.0], [6.0, 0.5], [8.0, -1.5]]) + ROBOT_RADIUS = 0.5 # robot radius [m] + + # Initial state parameters + INITIAL_SPEED = 1.0 / 3.6 # current speed [m/s] + INITIAL_ACCEL = 0.0 # current acceleration [m/ss] + INITIAL_LAT_POSITION = 0.5 # current lateral position [m] + INITIAL_LAT_SPEED = 0.0 # current lateral speed [m/s] + INITIAL_LAT_ACCELERATION = 0.0 # current lateral acceleration [m/s] + INITIAL_COURSE_POSITION = 0.0 # current course position + + ANIMATION_AREA = 5.0 # Animation area length [m] + + STOP_S = 4.0 # Merge and stop distance [m] + D_S = 0.3 # Stop point sampling length [m] + N_STOP_S_SAMPLE = 3 # Stop point sampling number +else: + MAX_ROAD_WIDTH = 7.0 # maximum road width [m] + D_ROAD_W = 1.0 # road width sampling length [m] + TARGET_SPEED = 30.0 / 3.6 # target speed [m/s] + D_T_S = 5.0 / 3.6 # target speed sampling length [m/s] + # Waypoints + WX = [0.0, 10.0, 20.5, 35.0, 70.5] + WY = [0.0, -6.0, 5.0, 6.5, 0.0] + # Obstacle list + OBSTACLES = np.array( + [[20.0, 10.0], [30.0, 6.0], [30.0, 8.0], [35.0, 8.0], [50.0, 3.0]] + ) + ROBOT_RADIUS = 2.0 # robot radius [m] + + # Initial state parameters + INITIAL_SPEED = 10.0 / 3.6 # current speed [m/s] + INITIAL_ACCEL = 0.0 # current acceleration [m/ss] + INITIAL_LAT_POSITION = 2.0 # current lateral position [m] + INITIAL_LAT_SPEED = 0.0 # current lateral speed [m/s] + INITIAL_LAT_ACCELERATION = 0.0 # current lateral acceleration [m/s] + INITIAL_COURSE_POSITION = 0.0 # current course position + + ANIMATION_AREA = 20.0 # Animation area length [m] + STOP_S = 25.0 # Merge and stop distance [m] + D_S = 2 # Stop point sampling length [m] + N_STOP_S_SAMPLE = 4 # Stop point sampling number + + +class LateralMovementStrategy: + def calc_lateral_trajectory(self, fp, di, c_d, c_d_d, c_d_dd, Ti): + """ + Calculate the lateral trajectory + """ + raise NotImplementedError("calc_lateral_trajectory not implemented") + + def calc_cartesian_parameters(self, fp, csp): + """ + Calculate the cartesian parameters (x, y, yaw, curvature, v, a) + """ + raise NotImplementedError("calc_cartesian_parameters not implemented") + + +class HighSpeedLateralMovementStrategy(LateralMovementStrategy): + def calc_lateral_trajectory(self, fp, di, c_d, c_d_d, c_d_dd, Ti): + tp = copy.deepcopy(fp) + s0_d = fp.s_d[0] + s0_dd = fp.s_dd[0] + # d'(t) = d'(s) * s'(t) + # d''(t) = d''(s) * s'(t)^2 + d'(s) * s''(t) + lat_qp = QuinticPolynomial( + c_d, c_d_d * s0_d, c_d_dd * s0_d**2 + c_d_d * s0_dd, di, 0.0, 0.0, Ti + ) + + tp.d = [] + tp.d_d = [] + tp.d_dd = [] + tp.d_ddd = [] + + # Calculate all derivatives in a single loop to reduce iterations + for i in range(len(fp.t)): + t = fp.t[i] + s_d = fp.s_d[i] + s_dd = fp.s_dd[i] + + s_d_inv = 1.0 / (s_d + 1e-6) + 1e-6 # Avoid division by zero + s_d_inv_sq = s_d_inv * s_d_inv # Square of inverse + + d = lat_qp.calc_point(t) + d_d = lat_qp.calc_first_derivative(t) + d_dd = lat_qp.calc_second_derivative(t) + d_ddd = lat_qp.calc_third_derivative(t) + + tp.d.append(d) + # d'(s) = d'(t) / s'(t) + tp.d_d.append(d_d * s_d_inv) + # d''(s) = (d''(t) - d'(s) * s''(t)) / s'(t)^2 + tp.d_dd.append((d_dd - tp.d_d[i] * s_dd) * s_d_inv_sq) + tp.d_ddd.append(d_ddd) + + return tp + + def calc_cartesian_parameters(self, fp, csp): + # calc global positions + for i in range(len(fp.s)): + ix, iy = csp.calc_position(fp.s[i]) + if ix is None: + break + i_yaw = csp.calc_yaw(fp.s[i]) + i_kappa = csp.calc_curvature(fp.s[i]) + i_dkappa = csp.calc_curvature_rate(fp.s[i]) + s_condition = [fp.s[i], fp.s_d[i], fp.s_dd[i]] + d_condition = [ + fp.d[i], + fp.d_d[i], + fp.d_dd[i], + ] + x, y, theta, kappa, v, a = CartesianFrenetConverter.frenet_to_cartesian( + fp.s[i], ix, iy, i_yaw, i_kappa, i_dkappa, s_condition, d_condition + ) + fp.x.append(x) + fp.y.append(y) + fp.yaw.append(theta) + fp.c.append(kappa) + fp.v.append(v) + fp.a.append(a) + return fp + + +class LowSpeedLateralMovementStrategy(LateralMovementStrategy): + def calc_lateral_trajectory(self, fp, di, c_d, c_d_d, c_d_dd, Ti): + s0 = fp.s[0] + s1 = fp.s[-1] + tp = copy.deepcopy(fp) + # d = d(s), d_d = d'(s), d_dd = d''(s) + # * shift s range from [s0, s1] to [0, s1 - s0] + lat_qp = QuinticPolynomial(c_d, c_d_d, c_d_dd, di, 0.0, 0.0, s1 - s0) + + tp.d = [lat_qp.calc_point(s - s0) for s in fp.s] + tp.d_d = [lat_qp.calc_first_derivative(s - s0) for s in fp.s] + tp.d_dd = [lat_qp.calc_second_derivative(s - s0) for s in fp.s] + tp.d_ddd = [lat_qp.calc_third_derivative(s - s0) for s in fp.s] + return tp + + def calc_cartesian_parameters(self, fp, csp): + # calc global positions + for i in range(len(fp.s)): + ix, iy = csp.calc_position(fp.s[i]) + if ix is None: + break + i_yaw = csp.calc_yaw(fp.s[i]) + i_kappa = csp.calc_curvature(fp.s[i]) + i_dkappa = csp.calc_curvature_rate(fp.s[i]) + s_condition = [fp.s[i], fp.s_d[i], fp.s_dd[i]] + d_condition = [fp.d[i], fp.d_d[i], fp.d_dd[i]] + x, y, theta, kappa, v, a = CartesianFrenetConverter.frenet_to_cartesian( + fp.s[i], ix, iy, i_yaw, i_kappa, i_dkappa, s_condition, d_condition + ) + fp.x.append(x) + fp.y.append(y) + fp.yaw.append(theta) + fp.c.append(kappa) + fp.v.append(v) + fp.a.append(a) + return fp + + +class LongitudinalMovementStrategy: + def calc_longitudinal_trajectory(self, c_speed, c_accel, Ti, s0): + """ + Calculate the longitudinal trajectory + """ + raise NotImplementedError("calc_longitudinal_trajectory not implemented") + + def get_d_arrange(self, s0): + """ + Get the d sample range + """ + raise NotImplementedError("get_d_arrange not implemented") + + def calc_destination_cost(self, fp): + """ + Calculate the destination cost + """ + raise NotImplementedError("calc_destination_cost not implemented") + + +class VelocityKeepingLongitudinalMovementStrategy(LongitudinalMovementStrategy): + def calc_longitudinal_trajectory(self, c_speed, c_accel, Ti, s0): + fplist = [] + for tv in np.arange( + TARGET_SPEED - D_T_S * N_S_SAMPLE, TARGET_SPEED + D_T_S * N_S_SAMPLE, D_T_S + ): + fp = FrenetPath() + lon_qp = QuarticPolynomial(s0, c_speed, c_accel, tv, 0.0, Ti) + fp.t = [t for t in np.arange(0.0, Ti, DT)] + fp.s = [lon_qp.calc_point(t) for t in fp.t] + fp.s_d = [lon_qp.calc_first_derivative(t) for t in fp.t] + fp.s_dd = [lon_qp.calc_second_derivative(t) for t in fp.t] + fp.s_ddd = [lon_qp.calc_third_derivative(t) for t in fp.t] + fplist.append(fp) + return fplist + + def get_d_arrange(self, s0): + return np.arange(-MAX_ROAD_WIDTH, MAX_ROAD_WIDTH, D_ROAD_W) + + def calc_destination_cost(self, fp): + ds = (TARGET_SPEED - fp.s_d[-1]) ** 2 + return K_S_DOT * ds + + +class MergingAndStoppingLongitudinalMovementStrategy(LongitudinalMovementStrategy): + def calc_longitudinal_trajectory(self, c_speed, c_accel, Ti, s0): + if s0 >= STOP_S: + return [] + fplist = [] + for s in np.arange( + STOP_S - D_S * N_STOP_S_SAMPLE, STOP_S + D_S * N_STOP_S_SAMPLE, D_S + ): + fp = FrenetPath() + lon_qp = QuinticPolynomial(s0, c_speed, c_accel, s, 0.0, 0.0, Ti) + fp.t = [t for t in np.arange(0.0, Ti, DT)] + fp.s = [lon_qp.calc_point(t) for t in fp.t] + fp.s_d = [lon_qp.calc_first_derivative(t) for t in fp.t] + fp.s_dd = [lon_qp.calc_second_derivative(t) for t in fp.t] + fp.s_ddd = [lon_qp.calc_third_derivative(t) for t in fp.t] + fplist.append(fp) + return fplist + + def get_d_arrange(self, s0): + # Only if s0 is less than STOP_S / 3, then we sample the road width + if s0 < STOP_S / 3: + return np.arange(-MAX_ROAD_WIDTH, MAX_ROAD_WIDTH, D_ROAD_W) + else: + return [0.0] + + def calc_destination_cost(self, fp): + ds = (STOP_S - fp.s[-1]) ** 2 + return K_S * ds + +LATERAL_MOVEMENT_STRATEGY: LateralMovementStrategy +LONGITUDINAL_MOVEMENT_STRATEGY: LongitudinalMovementStrategy + +if LATERAL_MOVEMENT == LateralMovement.HIGH_SPEED: + LATERAL_MOVEMENT_STRATEGY = HighSpeedLateralMovementStrategy() +else: + LATERAL_MOVEMENT_STRATEGY = LowSpeedLateralMovementStrategy() + +if LONGITUDINAL_MOVEMENT == LongitudinalMovement.VELOCITY_KEEPING: + LONGITUDINAL_MOVEMENT_STRATEGY = VelocityKeepingLongitudinalMovementStrategy() +else: + LONGITUDINAL_MOVEMENT_STRATEGY = MergingAndStoppingLongitudinalMovementStrategy() + +class QuarticPolynomial: def __init__(self, xs, vxs, axs, vxe, axe, time): # calc coefficient of quartic polynomial @@ -61,29 +332,25 @@ def __init__(self, xs, vxs, axs, vxe, axe, time): self.a1 = vxs self.a2 = axs / 2.0 - A = np.array([[3 * time ** 2, 4 * time ** 3], - [6 * time, 12 * time ** 2]]) - b = np.array([vxe - self.a1 - 2 * self.a2 * time, - axe - 2 * self.a2]) + A = np.array([[3 * time**2, 4 * time**3], [6 * time, 12 * time**2]]) + b = np.array([vxe - self.a1 - 2 * self.a2 * time, axe - 2 * self.a2]) x = np.linalg.solve(A, b) self.a3 = x[0] self.a4 = x[1] def calc_point(self, t): - xt = self.a0 + self.a1 * t + self.a2 * t ** 2 + \ - self.a3 * t ** 3 + self.a4 * t ** 4 + xt = self.a0 + self.a1 * t + self.a2 * t**2 + self.a3 * t**3 + self.a4 * t**4 return xt def calc_first_derivative(self, t): - xt = self.a1 + 2 * self.a2 * t + \ - 3 * self.a3 * t ** 2 + 4 * self.a4 * t ** 3 + xt = self.a1 + 2 * self.a2 * t + 3 * self.a3 * t**2 + 4 * self.a4 * t**3 return xt def calc_second_derivative(self, t): - xt = 2 * self.a2 + 6 * self.a3 * t + 12 * self.a4 * t ** 2 + xt = 2 * self.a2 + 6 * self.a3 * t + 12 * self.a4 * t**2 return xt @@ -94,111 +361,85 @@ def calc_third_derivative(self, t): class FrenetPath: - def __init__(self): self.t = [] self.d = [] - self.d_d = [] - self.d_dd = [] - self.d_ddd = [] + self.d_d = [] # d'(s) + self.d_dd = [] # d''(s) + self.d_ddd = [] # d'''(t) in low speed / d'''(s) in high speed self.s = [] - self.s_d = [] - self.s_dd = [] - self.s_ddd = [] - self.cd = 0.0 - self.cv = 0.0 + self.s_d = [] # s'(t) + self.s_dd = [] # s''(t) + self.s_ddd = [] # s'''(t) self.cf = 0.0 self.x = [] self.y = [] self.yaw = [] + self.v = [] + self.a = [] self.ds = [] self.c = [] - -def calc_frenet_paths(c_speed, c_accel, c_d, c_d_d, c_d_dd, s0): + def pop_front(self): + self.x.pop(0) + self.y.pop(0) + self.yaw.pop(0) + self.v.pop(0) + self.a.pop(0) + self.s.pop(0) + self.s_d.pop(0) + self.s_dd.pop(0) + self.s_ddd.pop(0) + self.d.pop(0) + self.d_d.pop(0) + self.d_dd.pop(0) + self.d_ddd.pop(0) + + +def calc_frenet_paths(c_s_d, c_s_dd, c_d, c_d_d, c_d_dd, s0): frenet_paths = [] - # generate path to each offset goal - for di in np.arange(-MAX_ROAD_WIDTH, MAX_ROAD_WIDTH, D_ROAD_W): - - # Lateral motion planning - for Ti in np.arange(MIN_T, MAX_T, DT): - fp = FrenetPath() - - # lat_qp = quintic_polynomial(c_d, c_d_d, c_d_dd, di, 0.0, 0.0, Ti) - lat_qp = QuinticPolynomial(c_d, c_d_d, c_d_dd, di, 0.0, 0.0, Ti) - - fp.t = [t for t in np.arange(0.0, Ti, DT)] - fp.d = [lat_qp.calc_point(t) for t in fp.t] - fp.d_d = [lat_qp.calc_first_derivative(t) for t in fp.t] - fp.d_dd = [lat_qp.calc_second_derivative(t) for t in fp.t] - fp.d_ddd = [lat_qp.calc_third_derivative(t) for t in fp.t] - - # Longitudinal motion planning (Velocity keeping) - for tv in np.arange(TARGET_SPEED - D_T_S * N_S_SAMPLE, - TARGET_SPEED + D_T_S * N_S_SAMPLE, D_T_S): - tfp = copy.deepcopy(fp) - lon_qp = QuarticPolynomial(s0, c_speed, c_accel, tv, 0.0, Ti) - - tfp.s = [lon_qp.calc_point(t) for t in fp.t] - tfp.s_d = [lon_qp.calc_first_derivative(t) for t in fp.t] - tfp.s_dd = [lon_qp.calc_second_derivative(t) for t in fp.t] - tfp.s_ddd = [lon_qp.calc_third_derivative(t) for t in fp.t] - - Jp = sum(np.power(tfp.d_ddd, 2)) # square of jerk - Js = sum(np.power(tfp.s_ddd, 2)) # square of jerk - - # square of diff from target speed - ds = (TARGET_SPEED - tfp.s_d[-1]) ** 2 - - tfp.cd = K_J * Jp + K_T * Ti + K_D * tfp.d[-1] ** 2 - tfp.cv = K_J * Js + K_T * Ti + K_D * ds - tfp.cf = K_LAT * tfp.cd + K_LON * tfp.cv - - frenet_paths.append(tfp) + for Ti in np.arange(MIN_T, MAX_T, DT): + lon_paths = LONGITUDINAL_MOVEMENT_STRATEGY.calc_longitudinal_trajectory( + c_s_d, c_s_dd, Ti, s0 + ) + + for fp in lon_paths: + for di in LONGITUDINAL_MOVEMENT_STRATEGY.get_d_arrange(s0): + tp = LATERAL_MOVEMENT_STRATEGY.calc_lateral_trajectory( + fp, di, c_d, c_d_d, c_d_dd, Ti + ) + + Jp = sum(np.power(tp.d_ddd, 2)) # square of jerk + Js = sum(np.power(tp.s_ddd, 2)) # square of jerk + + lat_cost = K_J * Jp + K_T * Ti + K_D * tp.d[-1] ** 2 + lon_cost = ( + K_J * Js + + K_T * Ti + + LONGITUDINAL_MOVEMENT_STRATEGY.calc_destination_cost(tp) + ) + tp.cf = K_LAT * lat_cost + K_LON * lon_cost + frenet_paths.append(tp) return frenet_paths def calc_global_paths(fplist, csp): - for fp in fplist: - - # calc global positions - for i in range(len(fp.s)): - ix, iy = csp.calc_position(fp.s[i]) - if ix is None: - break - i_yaw = csp.calc_yaw(fp.s[i]) - di = fp.d[i] - fx = ix + di * math.cos(i_yaw + math.pi / 2.0) - fy = iy + di * math.sin(i_yaw + math.pi / 2.0) - fp.x.append(fx) - fp.y.append(fy) - - # calc yaw and ds - for i in range(len(fp.x) - 1): - dx = fp.x[i + 1] - fp.x[i] - dy = fp.y[i + 1] - fp.y[i] - fp.yaw.append(math.atan2(dy, dx)) - fp.ds.append(math.hypot(dx, dy)) - - fp.yaw.append(fp.yaw[-1]) - fp.ds.append(fp.ds[-1]) - - # calc curvature - for i in range(len(fp.yaw) - 1): - fp.c.append((fp.yaw[i + 1] - fp.yaw[i]) / fp.ds[i]) - - return fplist + return [ + LATERAL_MOVEMENT_STRATEGY.calc_cartesian_parameters(fp, csp) for fp in fplist + ] def check_collision(fp, ob): for i in range(len(ob[:, 0])): - d = [((ix - ob[i, 0]) ** 2 + (iy - ob[i, 1]) ** 2) - for (ix, iy) in zip(fp.x, fp.y)] + d = [ + ((ix - ob[i, 0]) ** 2 + (iy - ob[i, 1]) ** 2) + for (ix, iy) in zip(fp.x, fp.y) + ] - collision = any([di <= ROBOT_RADIUS ** 2 for di in d]) + collision = any([di <= ROBOT_RADIUS**2 for di in d]) if collision: return False @@ -207,38 +448,41 @@ def check_collision(fp, ob): def check_paths(fplist, ob): - ok_ind = [] + path_dict = { + "max_speed_error": [], + "max_accel_error": [], + "max_curvature_error": [], + "collision_error": [], + "ok": [], + } for i, _ in enumerate(fplist): - if any([v > MAX_SPEED for v in fplist[i].s_d]): # Max speed check - continue - elif any([abs(a) > MAX_ACCEL for a in - fplist[i].s_dd]): # Max accel check - continue - elif any([abs(c) > MAX_CURVATURE for c in - fplist[i].c]): # Max curvature check - continue + if any([v > MAX_SPEED for v in fplist[i].v]): # Max speed check + path_dict["max_speed_error"].append(fplist[i]) + elif any([abs(a) > MAX_ACCEL for a in fplist[i].a]): # Max accel check + path_dict["max_accel_error"].append(fplist[i]) + elif any([abs(c) > MAX_CURVATURE for c in fplist[i].c]): # Max curvature check + path_dict["max_curvature_error"].append(fplist[i]) elif not check_collision(fplist[i], ob): - continue + path_dict["collision_error"].append(fplist[i]) + else: + path_dict["ok"].append(fplist[i]) + return path_dict - ok_ind.append(i) - return [fplist[i] for i in ok_ind] - - -def frenet_optimal_planning(csp, s0, c_speed, c_accel, c_d, c_d_d, c_d_dd, ob): - fplist = calc_frenet_paths(c_speed, c_accel, c_d, c_d_d, c_d_dd, s0) +def frenet_optimal_planning(csp, s0, c_s_d, c_s_dd, c_d, c_d_d, c_d_dd, ob): + fplist = calc_frenet_paths(c_s_d, c_s_dd, c_d, c_d_d, c_d_dd, s0) fplist = calc_global_paths(fplist, csp) - fplist = check_paths(fplist, ob) + fpdict = check_paths(fplist, ob) # find minimum cost path min_cost = float("inf") best_path = None - for fp in fplist: + for fp in fpdict["ok"]: if min_cost >= fp.cf: min_cost = fp.cf best_path = fp - return best_path + return [best_path, fpdict] def generate_target_course(x, y): @@ -259,40 +503,39 @@ def generate_target_course(x, y): def main(): print(__file__ + " start!!") - # way points - wx = [0.0, 10.0, 20.5, 35.0, 70.5] - wy = [0.0, -6.0, 5.0, 6.5, 0.0] - # obstacle lists - ob = np.array([[20.0, 10.0], - [30.0, 6.0], - [30.0, 8.0], - [35.0, 8.0], - [50.0, 3.0] - ]) - - tx, ty, tyaw, tc, csp = generate_target_course(wx, wy) - - # initial state - c_speed = 10.0 / 3.6 # current speed [m/s] - c_accel = 0.0 # current acceleration [m/ss] - c_d = 2.0 # current lateral position [m] - c_d_d = 0.0 # current lateral speed [m/s] - c_d_dd = 0.0 # current lateral acceleration [m/s] - s0 = 0.0 # current course position - - area = 20.0 # animation area length [m] + tx, ty, tyaw, tc, csp = generate_target_course(WX, WY) + + # Initialize state using global parameters + c_s_d = INITIAL_SPEED + c_s_dd = INITIAL_ACCEL + c_d = INITIAL_LAT_POSITION + c_d_d = INITIAL_LAT_SPEED + c_d_dd = INITIAL_LAT_ACCELERATION + s0 = INITIAL_COURSE_POSITION + + area = ANIMATION_AREA + + last_path = None for i in range(SIM_LOOP): - path = frenet_optimal_planning( - csp, s0, c_speed, c_accel, c_d, c_d_d, c_d_dd, ob) + [path, fpdict] = frenet_optimal_planning( + csp, s0, c_s_d, c_s_dd, c_d, c_d_d, c_d_dd, OBSTACLES + ) + + if path is None: + path = copy.deepcopy(last_path) + path.pop_front() + if len(path.x) <= 1: + print("Finish") + break + last_path = path s0 = path.s[1] c_d = path.d[1] c_d_d = path.d_d[1] c_d_dd = path.d_dd[1] - c_speed = path.s_d[1] - c_accel = path.s_dd[1] - + c_s_d = path.s_d[1] + c_s_dd = path.s_dd[1] if np.hypot(path.x[1] - tx[-1], path.y[1] - ty[-1]) <= 1.0: print("Goal") break @@ -301,15 +544,16 @@ def main(): plt.cla() # for stopping simulation with the esc key. plt.gcf().canvas.mpl_connect( - 'key_release_event', - lambda event: [exit(0) if event.key == 'escape' else None]) + "key_release_event", + lambda event: [exit(0) if event.key == "escape" else None], + ) plt.plot(tx, ty) - plt.plot(ob[:, 0], ob[:, 1], "xk") + plt.plot(OBSTACLES[:, 0], OBSTACLES[:, 1], "xk") plt.plot(path.x[1:], path.y[1:], "-or") plt.plot(path.x[1], path.y[1], "vc") plt.xlim(path.x[1] - area, path.x[1] + area) plt.ylim(path.y[1] - area, path.y[1] + area) - plt.title("v[km/h]:" + str(c_speed * 3.6)[0:4]) + plt.title("v[km/h]:" + str(path.v[1] * 3.6)[0:4]) plt.grid(True) plt.pause(0.0001) @@ -320,5 +564,5 @@ def main(): plt.show() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/docs/modules/path_planning/frenet_frame_path/frenet_frame_path_main.rst b/docs/modules/path_planning/frenet_frame_path/frenet_frame_path_main.rst index e9d9041e0e..38efaf2b53 100644 --- a/docs/modules/path_planning/frenet_frame_path/frenet_frame_path_main.rst +++ b/docs/modules/path_planning/frenet_frame_path/frenet_frame_path_main.rst @@ -1,14 +1,40 @@ Optimal Trajectory in a Frenet Frame ------------------------------------ -.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/FrenetOptimalTrajectory/animation.gif - This is optimal trajectory generation in a Frenet Frame. The cyan line is the target course and black crosses are obstacles. The red line is predicted path. +High Speed and Velocity Keeping Scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/FrenetOptimalTrajectory/high_speed_and_velocity_keeping_frenet_path.gif + +This scenario shows how the trajectory is maintained at high speeds while keeping a consistent velocity. + +High Speed and Merging and Stopping Scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/FrenetOptimalTrajectory/high_speed_and_merging_and_stopping_frenet_path.gif + +This scenario demonstrates the trajectory planning at high speeds with merging and stopping behaviors. + +Low Speed and Velocity Keeping Scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/FrenetOptimalTrajectory/low_speed_and_velocity_keeping_frenet_path.gif + +This scenario demonstrates how the trajectory is managed at low speeds while maintaining a steady velocity. + +Low Speed and Merging and Stopping Scenario +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/FrenetOptimalTrajectory/low_speed_and_merging_and_stopping_frenet_path.gif + +This scenario illustrates the trajectory planning at low speeds with merging and stopping behaviors. + Ref: - `Optimal Trajectory Generation for Dynamic Street Scenarios in a diff --git a/tests/test_frenet_optimal_trajectory.py b/tests/test_frenet_optimal_trajectory.py index 72bb7a8341..b8761ff4a6 100644 --- a/tests/test_frenet_optimal_trajectory.py +++ b/tests/test_frenet_optimal_trajectory.py @@ -1,12 +1,48 @@ import conftest from PathPlanning.FrenetOptimalTrajectory import frenet_optimal_trajectory as m +from PathPlanning.FrenetOptimalTrajectory.frenet_optimal_trajectory import ( + LateralMovement, + LongitudinalMovement, +) -def test1(): +def default_scenario_test(): m.show_animation = False m.SIM_LOOP = 5 m.main() -if __name__ == '__main__': +def high_speed_and_merging_and_stopping_scenario_test(): + m.show_animation = False + m.LATERAL_MOVEMENT = LateralMovement.HIGH_SPEED + m.LONGITUDINAL_MOVEMENT = LongitudinalMovement.MERGING_AND_STOPPING + m.SIM_LOOP = 5 + m.main() + + +def high_speed_and_velocity_keeping_scenario_test(): + m.show_animation = False + m.LATERAL_MOVEMENT = LateralMovement.HIGH_SPEED + m.LONGITUDINAL_MOVEMENT = LongitudinalMovement.VELOCITY_KEEPING + m.SIM_LOOP = 5 + m.main() + + +def low_speed_and_velocity_keeping_scenario_test(): + m.show_animation = False + m.LATERAL_MOVEMENT = LateralMovement.LOW_SPEED + m.LONGITUDINAL_MOVEMENT = LongitudinalMovement.VELOCITY_KEEPING + m.SIM_LOOP = 5 + m.main() + + +def low_speed_and_merging_and_stopping_scenario_test(): + m.show_animation = False + m.LATERAL_MOVEMENT = LateralMovement.LOW_SPEED + m.LONGITUDINAL_MOVEMENT = LongitudinalMovement.MERGING_AND_STOPPING + m.SIM_LOOP = 5 + m.main() + + +if __name__ == "__main__": conftest.run_this_test(__file__) From 95eedba44713883d805fb92ff74aabbe1fa4f735 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Fri, 24 Jan 2025 13:24:26 +0900 Subject: [PATCH 049/181] Fix lint error (#1124) --- .../arm_obstacle_navigation.py | 4 ++-- .../arm_obstacle_navigation_2.py | 4 ++-- .../dynamic_movement_primitives.py | 13 ++++++------- .../Eta3SplineTrajectory/eta3_spline_trajectory.py | 7 +++---- PathPlanning/RRT/sobol/sobol.py | 12 ++++++------ 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py index e377897e54..593db42b34 100644 --- a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py +++ b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py @@ -158,7 +158,7 @@ def astar_torus(grid, start_node, goal_node): while parent_map[route[0][0]][route[0][1]] != (): route.insert(0, parent_map[route[0][0]][route[0][1]]) - print("The route found covers %d grid cells." % len(route)) + print(f"The route found covers {len(route)} grid cells.") for i in range(1, len(route)): grid[route[i]] = 6 plt.cla() @@ -212,7 +212,7 @@ def calc_heuristic_map(M, goal_node): return heuristic_map -class NLinkArm(object): +class NLinkArm: """ Class for controlling and plotting a planar arm with an arbitrary number of links. """ diff --git a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py index 10f4615c34..5295b7ca3a 100644 --- a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py +++ b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py @@ -189,7 +189,7 @@ def astar_torus(grid, start_node, goal_node): while parent_map[route[0][0]][route[0][1]] != (): route.insert(0, parent_map[route[0][0]][route[0][1]]) - print("The route found covers %d grid cells." % len(route)) + print(f"The route found covers {len(route)} grid cells.") for i in range(1, len(route)): grid[route[i]] = 6 plt.cla() @@ -243,7 +243,7 @@ def calc_heuristic_map(M, goal_node): return heuristic_map -class NLinkArm(object): +class NLinkArm: """ Class for controlling and plotting a planar arm with an arbitrary number of links. """ diff --git a/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py b/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py index 468d3b9f97..aa34934c0e 100644 --- a/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py +++ b/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py @@ -18,7 +18,7 @@ import numpy as np -class DMP(object): +class DMP: def __init__(self, training_data, data_period, K=156.25, B=25): """ @@ -215,21 +215,21 @@ def show_DMP_purpose(self): T_vals = np.linspace(T_orig, 2*T_orig, 20) for new_q0_value in q0_vals: - plot_title = "Initial Position = [%s, %s]" % \ - (round(new_q0_value[0], 2), round(new_q0_value[1], 2)) + plot_title = (f"Initial Position = [{round(new_q0_value[0], 2)}," + f" {round(new_q0_value[1], 2)}]") _, path = self.recreate_trajectory(new_q0_value, g_orig, T_orig) self.view_trajectory(path, title=plot_title, demo=True) for new_g_value in g_vals: - plot_title = "Goal Position = [%s, %s]" % \ - (round(new_g_value[0], 2), round(new_g_value[1], 2)) + plot_title = (f"Goal Position = [{round(new_g_value[0], 2)}," + f" {round(new_g_value[1], 2)}]") _, path = self.recreate_trajectory(q0_orig, new_g_value, T_orig) self.view_trajectory(path, title=plot_title, demo=True) for new_T_value in T_vals: - plot_title = "Period = %s [sec]" % round(new_T_value, 2) + plot_title = f"Period = {round(new_T_value, 2)} [sec]" _, path = self.recreate_trajectory(q0_orig, g_orig, new_T_value) self.view_trajectory(path, title=plot_title, demo=True) @@ -257,5 +257,4 @@ def example_DMP(): if __name__ == '__main__': - example_DMP() diff --git a/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py b/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py index d034cb8a32..2dab7a7add 100644 --- a/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py +++ b/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py @@ -26,8 +26,7 @@ class MaxVelocityNotReached(Exception): def __init__(self, actual_vel, max_vel): - self.message = 'Actual velocity {} does not equal desired max velocity {}!'.format( - actual_vel, max_vel) + self.message = f'Actual velocity {actual_vel} does not equal desired max velocity {max_vel}!' class eta3_trajectory(Eta3Path): @@ -42,7 +41,7 @@ def __init__(self, segments, max_vel, v0=0.0, a0=0.0, max_accel=2.0, max_jerk=5. # ensure that all inputs obey the assumptions of the model assert max_vel > 0 and v0 >= 0 and a0 >= 0 and max_accel > 0 and max_jerk > 0 \ and a0 <= max_accel and v0 <= max_vel - super(eta3_trajectory, self).__init__(segments=segments) + super(__class__, self).__init__(segments=segments) self.total_length = sum([s.segment_length for s in self.segments]) self.max_vel = float(max_vel) self.v0 = float(v0) @@ -166,7 +165,7 @@ def velocity_profile(self): try: assert np.isclose(self.vels[index], 0) except AssertionError as e: - print('The final velocity {} is not zero'.format(self.vels[index])) + print(f'The final velocity {self.vels[index]} is not zero') raise e self.seg_lengths[index] = s_sf diff --git a/PathPlanning/RRT/sobol/sobol.py b/PathPlanning/RRT/sobol/sobol.py index 428ba58a98..cddc9d7c72 100644 --- a/PathPlanning/RRT/sobol/sobol.py +++ b/PathPlanning/RRT/sobol/sobol.py @@ -370,8 +370,8 @@ def i4_sobol(dim_num, seed): if (dim_num < 1 or dim_max < dim_num): print('I4_SOBOL - Fatal error!') print(' The spatial dimension DIM_NUM should satisfy:') - print(' 1 <= DIM_NUM <= %d' % dim_max) - print(' But this input value is DIM_NUM = %d' % dim_num) + print(f' 1 <= DIM_NUM <= {dim_max:d}') + print(f' But this input value is DIM_NUM = {dim_num:d}') return None dim_num_save = dim_num @@ -443,7 +443,7 @@ def i4_sobol(dim_num, seed): # l_var = i4_bit_lo0(seed) - elif (seed <= seed_save): + elif seed <= seed_save: seed_save = 0 lastq = np.zeros(dim_num) @@ -471,8 +471,8 @@ def i4_sobol(dim_num, seed): if maxcol < l_var: print('I4_SOBOL - Fatal error!') print(' Too many calls!') - print(' MAXCOL = %d\n' % maxcol) - print(' L = %d\n' % l_var) + print(f' MAXCOL = {maxcol:d}\n') + print(f' L = {l_var:d}\n') return None @@ -819,7 +819,7 @@ def r8mat_write(filename, m, n, a): with open(filename, 'w') as output: for i in range(0, m): for j in range(0, n): - s = ' %g' % (a[i, j]) + s = f' {a[i, j]:g}' output.write(s) output.write('\n') From 2a489b3b82d3d0a3152cf7641fa137deb116972d Mon Sep 17 00:00:00 2001 From: parmaski <89462537+parmaski@users.noreply.github.com> Date: Fri, 24 Jan 2025 13:28:12 +0900 Subject: [PATCH 050/181] Fix: dead link URL in doc (#1087) * fix dead url links * change link to MPC course * remove dead link --- .github/FUNDING.yml | 2 +- .../arm_obstacle_navigation.py | 2 +- .../arm_obstacle_navigation_2.py | 2 +- PathPlanning/BugPlanning/bug.py | 2 +- .../dynamic_movement_primitives.py | 2 +- .../eta3_spline_trajectory.py | 2 +- .../InformedRRTStar/informed_rrt_star.py | 2 +- .../quintic_polynomials_planner.py | 2 +- PathPlanning/RRT/sobol/sobol.py | 2 +- .../state_lattice_planner.py | 4 ++-- .../wavefront_coverage_path_planner.py | 2 +- PathTracking/cgmres_nmpc/cgmres_nmpc.py | 2 +- README.md | 21 +++++++++---------- SLAM/GraphBasedSLAM/data/README.rst | 2 +- appveyor.yml | 2 +- docs/conf.py | 2 +- docs/getting_started_main.rst | 2 +- docs/how_to_contribute_main.rst | 2 +- docs/make.bat | 2 +- .../particle_filter_localization_main.rst | 2 +- .../rectangle_fitting_main.rst | 4 ++-- .../bezier_path/bezier_path_main.rst | 2 +- .../bugplanner/bugplanner_main.rst | 2 +- .../coverage_path/coverage_path_main.rst | 2 +- .../dubins_path/dubins_path_main.rst | 2 +- ...l_predictive_trajectory_generator_main.rst | 2 +- .../quintic_polynomials_planner_main.rst | 2 +- .../reeds_shepp_path_main.rst | 2 +- docs/modules/path_planning/rrt/rrt_main.rst | 8 +++---- docs/modules/path_planning/rrt/rrt_star.rst | 2 +- .../state_lattice_planner_main.rst | 4 ++-- .../cgmres_nmpc/cgmres_nmpc_main.rst | 2 +- .../lqr_speed_and_steering_control_main.rst | 2 +- ...ictive_speed_and_steering_control_main.rst | 4 ++-- users_comments.md | 16 +++++--------- 35 files changed, 55 insertions(+), 62 deletions(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 24f0926299..0d943696cc 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ # These are supported funding model platforms github: AtsushiSakai patreon: myenigma -custom: https://www.paypal.me/myenigmapay/ +custom: https://www.paypal.com/paypalme/myenigmapay/ diff --git a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py index 593db42b34..9047c13851 100644 --- a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py +++ b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation.py @@ -34,7 +34,7 @@ def detect_collision(line_seg, circle): """ Determines whether a line segment (arm link) is in contact with a circle (obstacle). - Credit to: http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html + Credit to: https://web.archive.org/web/20200130224918/http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html Args: line_seg: List of coordinates of line segment endpoints e.g. [[1, 1], [2, 2]] circle: List of circle coordinates and radius e.g. [0, 0, 0.5] is a circle centered diff --git a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py index 5295b7ca3a..f5d435082a 100644 --- a/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py +++ b/ArmNavigation/arm_obstacle_navigation/arm_obstacle_navigation_2.py @@ -66,7 +66,7 @@ def detect_collision(line_seg, circle): """ Determines whether a line segment (arm link) is in contact with a circle (obstacle). - Credit to: http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html + Credit to: https://web.archive.org/web/20200130224918/http://doswa.com/2009/07/13/circle-segment-intersectioncollision.html Args: line_seg: List of coordinates of line segment endpoints e.g. [[1, 1], [2, 2]] circle: List of circle coordinates and radius e.g. [0, 0, 0.5] is a circle centered diff --git a/PathPlanning/BugPlanning/bug.py b/PathPlanning/BugPlanning/bug.py index 62fec7c109..34890cb55a 100644 --- a/PathPlanning/BugPlanning/bug.py +++ b/PathPlanning/BugPlanning/bug.py @@ -1,7 +1,7 @@ """ Bug Planning author: Sarim Mehdi(muhammadsarim.mehdi@studio.unibo.it) -Source: https://sites.google.com/site/ece452bugalgorithms/ +Source: https://web.archive.org/web/20201103052224/https://sites.google.com/site/ece452bugalgorithms/ """ import numpy as np diff --git a/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py b/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py index aa34934c0e..9ccd18b7c2 100644 --- a/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py +++ b/PathPlanning/DynamicMovementPrimitives/dynamic_movement_primitives.py @@ -9,7 +9,7 @@ More information on Dynamic Movement Primitives available at: https://arxiv.org/abs/2102.03861 -https://www.frontiersin.org/articles/10.3389/fncom.2013.00138/full +https://www.frontiersin.org/journals/computational-neuroscience/articles/10.3389/fncom.2013.00138/full """ diff --git a/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py b/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py index 2dab7a7add..a73797dacb 100644 --- a/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py +++ b/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py @@ -6,7 +6,7 @@ Atsushi Sakai (@Atsushi_twi) Refs: -- https://jwdinius.github.io/blog/2018/eta3traj +- https://jwdinius.github.io/blog/2018/eta3traj/ - [eta^3-Splines for the Smooth Path Generation of Wheeled Mobile Robots] (https://ieeexplore.ieee.org/document/4339545/) diff --git a/PathPlanning/InformedRRTStar/informed_rrt_star.py b/PathPlanning/InformedRRTStar/informed_rrt_star.py index c37326e490..0483949c99 100644 --- a/PathPlanning/InformedRRTStar/informed_rrt_star.py +++ b/PathPlanning/InformedRRTStar/informed_rrt_star.py @@ -6,7 +6,7 @@ Reference: Informed RRT*: Optimal Sampling-based Path planning Focused via Direct Sampling of an Admissible Ellipsoidal Heuristic -https://arxiv.org/pdf/1404.2334.pdf +https://arxiv.org/pdf/1404.2334 """ import sys diff --git a/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py b/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py index 8ba11a23d2..fdc181afab 100644 --- a/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py +++ b/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py @@ -6,7 +6,7 @@ Ref: -- [Local Path planning And Motion Control For Agv In Positioning](http://ieeexplore.ieee.org/document/637936/) +- [Local Path planning And Motion Control For Agv In Positioning](https://ieeexplore.ieee.org/document/637936/) """ diff --git a/PathPlanning/RRT/sobol/sobol.py b/PathPlanning/RRT/sobol/sobol.py index cddc9d7c72..520d686a1d 100644 --- a/PathPlanning/RRT/sobol/sobol.py +++ b/PathPlanning/RRT/sobol/sobol.py @@ -13,7 +13,7 @@ PYTHON versions by Corrado Chisari Original code is available at - http://people.sc.fsu.edu/~jburkardt/py_src/sobol/sobol.html + https://people.sc.fsu.edu/~jburkardt/py_src/sobol/sobol.html Note: the i4 prefix means that the function takes a numeric argument or returns a number which is interpreted inside the function as a 4 diff --git a/PathPlanning/StateLatticePlanner/state_lattice_planner.py b/PathPlanning/StateLatticePlanner/state_lattice_planner.py index f4b1461b7a..7f8e725e0a 100644 --- a/PathPlanning/StateLatticePlanner/state_lattice_planner.py +++ b/PathPlanning/StateLatticePlanner/state_lattice_planner.py @@ -12,8 +12,8 @@ - State Space Sampling of Feasible Motions for High-Performance Mobile Robot Navigation in Complex Environments -http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.187.8210&rep=rep1 -&type=pdf +https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf +&doi=e2256b5b24137f89e473f01df288cb3aa72e56a0 """ import sys diff --git a/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py b/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py index 7e93caa8fb..c5a139454b 100644 --- a/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py +++ b/PathPlanning/WavefrontCPP/wavefront_coverage_path_planner.py @@ -4,7 +4,7 @@ author: Todd Tang paper: Planning paths of complete coverage of an unstructured environment by a mobile robot - Zelinsky et.al. -link: http://pinkwink.kr/attachment/cfile3.uf@1354654A4E8945BD13FE77.pdf +link: https://pinkwink.kr/attachment/cfile3.uf@1354654A4E8945BD13FE77.pdf """ import os diff --git a/PathTracking/cgmres_nmpc/cgmres_nmpc.py b/PathTracking/cgmres_nmpc/cgmres_nmpc.py index bb5699b888..a582c9da81 100644 --- a/PathTracking/cgmres_nmpc/cgmres_nmpc.py +++ b/PathTracking/cgmres_nmpc/cgmres_nmpc.py @@ -6,7 +6,7 @@ Ref: Shunichi09/nonlinear_control: Implementing the nonlinear model predictive -control, sliding mode control https://github.com/Shunichi09/nonlinear_control +control, sliding mode control https://github.com/Shunichi09/PythonLinearNonlinearControl """ diff --git a/README.md b/README.md index ee40dfc7eb..ec65342430 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ ![GitHub_Action_MacOS_CI](https://github.com/AtsushiSakai/PythonRobotics/workflows/MacOS_CI/badge.svg) ![GitHub_Action_Windows_CI](https://github.com/AtsushiSakai/PythonRobotics/workflows/Windows_CI/badge.svg) [![Build status](https://ci.appveyor.com/api/projects/status/sb279kxuv1be391g?svg=true)](https://ci.appveyor.com/project/AtsushiSakai/pythonrobotics) -[![codecov](https://codecov.io/gh/AtsushiSakai/PythonRobotics/branch/master/graph/badge.svg)](https://codecov.io/gh/AtsushiSakai/PythonRobotics) Python codes for robotics algorithm. @@ -111,7 +110,7 @@ For development: - [pytest-xdist](https://pypi.org/project/pytest-xdist/) (for parallel unit tests) -- [mypy](http://mypy-lang.org/) (for type check) +- [mypy](https://mypy-lang.org/) (for type check) - [sphinx](https://www.sphinx-doc.org/) (for document generation) @@ -328,7 +327,7 @@ The animation shows a robot finding its path and rerouting to avoid obstacles as Refs: -- [D* Lite](http://idm-lab.org/bib/abstracts/papers/aaai02b.pd) +- [D* Lite](http://idm-lab.org/bib/abstracts/papers/aaai02b.pdf) - [Improved Fast Replanning for Robot Navigation in Unknown Terrain](http://www.cs.cmu.edu/~maxim/files/dlite_icra02.pdf) ### Potential Field algorithm @@ -357,9 +356,9 @@ This code uses the model predictive trajectory generator to solve boundary probl Ref: -- [Optimal rough terrain trajectory generation for wheeled mobile robots](http://journals.sagepub.com/doi/pdf/10.1177/0278364906075328) +- [Optimal rough terrain trajectory generation for wheeled mobile robots](https://journals.sagepub.com/doi/pdf/10.1177/0278364906075328) -- [State Space Sampling of Feasible Motions for High-Performance Mobile Robot Navigation in Complex Environments](http://www.frc.ri.cmu.edu/~alonzo/pubs/papers/JFR_08_SS_Sampling.pdf) +- [State Space Sampling of Feasible Motions for High-Performance Mobile Robot Navigation in Complex Environments](https://www.frc.ri.cmu.edu/~alonzo/pubs/papers/JFR_08_SS_Sampling.pdf) ### Biased polar sampling @@ -403,7 +402,7 @@ Ref: - [Incremental Sampling-based Algorithms for Optimal Motion Planning](https://arxiv.org/abs/1005.0416) -- [Sampling-based Algorithms for Optimal Motion Planning](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.419.5503&rep=rep1&type=pdf) +- [Sampling-based Algorithms for Optimal Motion Planning](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=bddbc99f97173430aa49a0ada53ab5bade5902fa) ### RRT\* with reeds-shepp path @@ -421,7 +420,7 @@ A double integrator motion model is used for LQR local planner. Ref: -- [LQR\-RRT\*: Optimal Sampling\-Based Motion Planning with Automatically Derived Extension Heuristics](http://lis.csail.mit.edu/pubs/perez-icra12.pdf) +- [LQR\-RRT\*: Optimal Sampling\-Based Motion Planning with Automatically Derived Extension Heuristics](https://lis.csail.mit.edu/pubs/perez-icra12.pdf) - [MahanFathi/LQR\-RRTstar: LQR\-RRT\* method is used for random motion planning of a simple pendulum in its phase plot](https://github.com/MahanFathi/LQR-RRTstar) @@ -436,7 +435,7 @@ It can calculate a 2D path, velocity, and acceleration profile based on quintic Ref: -- [Local Path Planning And Motion Control For Agv In Positioning](http://ieeexplore.ieee.org/document/637936/) +- [Local Path Planning And Motion Control For Agv In Positioning](https://ieeexplore.ieee.org/document/637936/) ## Reeds Shepp planning @@ -523,7 +522,7 @@ Path tracking simulation with LQR speed and steering control. Ref: -- [Towards fully autonomous driving: Systems and algorithms \- IEEE Conference Publication](http://ieeexplore.ieee.org/document/5940562/) +- [Towards fully autonomous driving: Systems and algorithms \- IEEE Conference Publication](https://ieeexplore.ieee.org/document/5940562/) ## Model predictive speed and steering control @@ -630,7 +629,7 @@ If you or your company would like to support this project, please consider: - [Become a backer or sponsor on Patreon](https://www.patreon.com/myenigma) -- [One-time donation via PayPal](https://www.paypal.me/myenigmapay/) +- [One-time donation via PayPal](https://www.paypal.com/paypalme/myenigmapay/) If you would like to support us in some other way, please contact with creating an issue. @@ -640,7 +639,7 @@ If you would like to support us in some other way, please contact with creating They are providing a free license of their IDEs for this OSS development. -### [1Password](https://github.com/1Password/1password-teams-open-source) +### [1Password](https://github.com/1Password/for-open-source) They are providing a free license of their 1Password team license for this OSS project. diff --git a/SLAM/GraphBasedSLAM/data/README.rst b/SLAM/GraphBasedSLAM/data/README.rst index c875cce73f..15bc5b6c03 100644 --- a/SLAM/GraphBasedSLAM/data/README.rst +++ b/SLAM/GraphBasedSLAM/data/README.rst @@ -3,4 +3,4 @@ Acknowledgments and References Thanks to Luca Larlone for allowing inclusion of the `Intel dataset `_ in this repo. -1. Carlone, L. and Censi, A., 2014. `From angular manifolds to the integer lattice: Guaranteed orientation estimation with application to pose graph optimization `_. IEEE Transactions on Robotics, 30(2), pp.475-492. +1. Carlone, L. and Censi, A., 2014. `From angular manifolds to the integer lattice: Guaranteed orientation estimation with application to pose graph optimization `_. IEEE Transactions on Robotics, 30(2), pp.475-492. diff --git a/appveyor.yml b/appveyor.yml index 4964bab3da..05ad8a2820 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ environment: global: # SDK v7.0 MSVC Express 2008's SetEnv.cmd script will fail if the # /E:ON and /V:ON options are not enabled in the batch script intepreter - # See: http://stackoverflow.com/a/13751649/163740 + # See: https://stackoverflow.com/a/13751649/163740 CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" matrix: diff --git a/docs/conf.py b/docs/conf.py index 46f69cf17b..4e06f3f988 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,7 +3,7 @@ # # This file does only contain a selection of the most common options. For a # full list see the documentation: -# http://www.sphinx-doc.org/en/master/config +# https://www.sphinx-doc.org/en/master/config # -- Path setup -------------------------------------------------------------- diff --git a/docs/getting_started_main.rst b/docs/getting_started_main.rst index 88f218545e..004d68227b 100644 --- a/docs/getting_started_main.rst +++ b/docs/getting_started_main.rst @@ -49,7 +49,7 @@ For development: .. _`pytest-xdist`: https://github.com/pytest-dev/pytest-xdist .. _`mypy`: https://mypy-lang.org/ .. _`sphinx`: https://www.sphinx-doc.org/en/master/index.html -.. _`ruff`: https://github.com/charliermarsh/ruff +.. _`ruff`: https://github.com/astral-sh/ruff How to use diff --git a/docs/how_to_contribute_main.rst b/docs/how_to_contribute_main.rst index 48895d6fda..915b2155e4 100644 --- a/docs/how_to_contribute_main.rst +++ b/docs/how_to_contribute_main.rst @@ -159,6 +159,6 @@ Sponsors .. _`JetBrains`: https://www.jetbrains.com/ .. _`Sponsor @AtsushiSakai on GitHub Sponsors`: https://github.com/sponsors/AtsushiSakai .. _`Become a backer or sponsor on Patreon`: https://www.patreon.com/myenigma -.. _`One-time donation via PayPal`: https://www.paypal.me/myenigmapay/ +.. _`One-time donation via PayPal`: https://www.paypal.com/paypalme/myenigmapay/ diff --git a/docs/make.bat b/docs/make.bat index 6aab964dcc..07dcebea41 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -22,7 +22,7 @@ if errorlevel 9009 ( echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ + echo.https://sphinx-doc.org/ exit /b 1 ) diff --git a/docs/modules/localization/particle_filter_localization/particle_filter_localization_main.rst b/docs/modules/localization/particle_filter_localization/particle_filter_localization_main.rst index c8652ce8a7..20a9eb58fc 100644 --- a/docs/modules/localization/particle_filter_localization/particle_filter_localization_main.rst +++ b/docs/modules/localization/particle_filter_localization/particle_filter_localization_main.rst @@ -34,4 +34,4 @@ References: ~~~~~~~~~~~ - `_PROBABILISTIC ROBOTICS: `_ -- `Improving the particle filter in high dimensions using conjugate artificial process noise `_ +- `Improving the particle filter in high dimensions using conjugate artificial process noise `_ diff --git a/docs/modules/mapping/rectangle_fitting/rectangle_fitting_main.rst b/docs/modules/mapping/rectangle_fitting/rectangle_fitting_main.rst index 50a50141b2..b6ced1dc1d 100644 --- a/docs/modules/mapping/rectangle_fitting/rectangle_fitting_main.rst +++ b/docs/modules/mapping/rectangle_fitting/rectangle_fitting_main.rst @@ -7,7 +7,7 @@ This is an object shape recognition using rectangle fitting. This example code is based on this paper algorithm: -- `Efficient L\-Shape Fitting for Vehicle Detection Using Laser Scanners \- The Robotics Institute Carnegie Mellon University `_ +- `Efficient L\-Shape Fitting for Vehicle Detection Using Laser Scanners \- The Robotics Institute Carnegie Mellon University `_ The algorithm consists of 2 steps as below. @@ -66,4 +66,4 @@ API References ~~~~~~~~~~ -- `Efficient L\-Shape Fitting for Vehicle Detection Using Laser Scanners \- The Robotics Institute Carnegie Mellon University `_ +- `Efficient L\-Shape Fitting for Vehicle Detection Using Laser Scanners \- The Robotics Institute Carnegie Mellon University `_ diff --git a/docs/modules/path_planning/bezier_path/bezier_path_main.rst b/docs/modules/path_planning/bezier_path/bezier_path_main.rst index fbba6a4496..d306a85352 100644 --- a/docs/modules/path_planning/bezier_path/bezier_path_main.rst +++ b/docs/modules/path_planning/bezier_path/bezier_path_main.rst @@ -17,4 +17,4 @@ Ref: - `Continuous Curvature Path Generation Based on Bezier Curves for Autonomous - Vehicles `__ + Vehicles ` diff --git a/docs/modules/path_planning/bugplanner/bugplanner_main.rst b/docs/modules/path_planning/bugplanner/bugplanner_main.rst index 72cc46709f..81c91c0313 100644 --- a/docs/modules/path_planning/bugplanner/bugplanner_main.rst +++ b/docs/modules/path_planning/bugplanner/bugplanner_main.rst @@ -5,4 +5,4 @@ This is a 2D planning with Bug algorithm. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/BugPlanner/animation.gif -- `ECE452 Bug Algorithms `_ +- `ECE452 Bug Algorithms `_ diff --git a/docs/modules/path_planning/coverage_path/coverage_path_main.rst b/docs/modules/path_planning/coverage_path/coverage_path_main.rst index 828342a294..0a688b5ed2 100644 --- a/docs/modules/path_planning/coverage_path/coverage_path_main.rst +++ b/docs/modules/path_planning/coverage_path/coverage_path_main.rst @@ -29,6 +29,6 @@ This is a 2D grid based wavefront coverage path planner simulation: .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/WavefrontCPP/animation2.gif .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/WavefrontCPP/animation3.gif -- `Planning paths of complete coverage of an unstructured environment by a mobile robot `_ +- `Planning paths of complete coverage of an unstructured environment by a mobile robot `_ diff --git a/docs/modules/path_planning/dubins_path/dubins_path_main.rst b/docs/modules/path_planning/dubins_path/dubins_path_main.rst index 516638d83d..12172fb51e 100644 --- a/docs/modules/path_planning/dubins_path/dubins_path_main.rst +++ b/docs/modules/path_planning/dubins_path/dubins_path_main.rst @@ -72,5 +72,5 @@ Reference ~~~~~~~~~~~~~~~~~~~~ - `On Curves of Minimal Length with a Constraint on Average Curvature, and with Prescribed Initial and Terminal Positions and Tangents `__ - `Dubins path - Wikipedia `__ -- `15.3.1 Dubins Curves `__ +- `15.3.1 Dubins Curves `__ - `A Comprehensive, Step-by-Step Tutorial to Computing Dubin’s Paths `__ diff --git a/docs/modules/path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst b/docs/modules/path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst index 0fc997898e..463363ddcf 100644 --- a/docs/modules/path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst +++ b/docs/modules/path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst @@ -19,4 +19,4 @@ Lookup table generation sample Ref: - `Optimal rough terrain trajectory generation for wheeled mobile - robots `__ + robots `__ diff --git a/docs/modules/path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst b/docs/modules/path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst index feec345bae..0412b3c9b3 100644 --- a/docs/modules/path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst +++ b/docs/modules/path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst @@ -101,6 +101,6 @@ References: ~~~~~~~~~~~ - `Local Path Planning And Motion Control For Agv In - Positioning `__ + Positioning `__ diff --git a/docs/modules/path_planning/reeds_shepp_path/reeds_shepp_path_main.rst b/docs/modules/path_planning/reeds_shepp_path/reeds_shepp_path_main.rst index 6d456417fe..ff377eb91b 100644 --- a/docs/modules/path_planning/reeds_shepp_path/reeds_shepp_path_main.rst +++ b/docs/modules/path_planning/reeds_shepp_path/reeds_shepp_path_main.rst @@ -383,7 +383,7 @@ Hence, we have: Ref: - `15.3.2 Reeds-Shepp - Curves `__ + Curves `__ - `optimal paths for a car that goes both forwards and backwards `__ diff --git a/docs/modules/path_planning/rrt/rrt_main.rst b/docs/modules/path_planning/rrt/rrt_main.rst index 5a3a30dcff..e5f351a7ba 100644 --- a/docs/modules/path_planning/rrt/rrt_main.rst +++ b/docs/modules/path_planning/rrt/rrt_main.rst @@ -57,7 +57,7 @@ Ref: - `Informed RRT\*: Optimal Sampling-based Path Planning Focused via Direct Sampling of an Admissible Ellipsoidal - Heuristic `__ + Heuristic `__ .. _batch-informed-rrt*: @@ -90,10 +90,10 @@ PID is used for speed control. Ref: - `Motion Planning in Complex Environments using Closed-loop - Prediction `__ + Prediction `__ - `Real-time Motion Planning with Applications to Autonomous Urban - Driving `__ + Driving `__ - `[1601.06326] Sampling-based Algorithms for Optimal Motion Planning Using Closed-loop Prediction `__ @@ -113,6 +113,6 @@ Ref: - `LQR-RRT\*: Optimal Sampling-Based Motion Planning with Automatically Derived Extension - Heuristics `__ + Heuristics `__ - `MahanFathi/LQR-RRTstar: LQR-RRT\* method is used for random motion planning of a simple pendulum in its phase plot `__ \ No newline at end of file diff --git a/docs/modules/path_planning/rrt/rrt_star.rst b/docs/modules/path_planning/rrt/rrt_star.rst index d36eaac70e..6deb6b9172 100644 --- a/docs/modules/path_planning/rrt/rrt_star.rst +++ b/docs/modules/path_planning/rrt/rrt_star.rst @@ -16,6 +16,6 @@ Simulation Ref ^^^ -- `Sampling-based Algorithms for Optimal Motion Planning `__ +- `Sampling-based Algorithms for Optimal Motion Planning `__ - `Incremental Sampling-based Algorithms for Optimal Motion Planning `__ diff --git a/docs/modules/path_planning/state_lattice_planner/state_lattice_planner_main.rst b/docs/modules/path_planning/state_lattice_planner/state_lattice_planner_main.rst index 3ef0c7eca2..d5e7ed17d1 100644 --- a/docs/modules/path_planning/state_lattice_planner/state_lattice_planner_main.rst +++ b/docs/modules/path_planning/state_lattice_planner/state_lattice_planner_main.rst @@ -25,9 +25,9 @@ Lane sampling Ref: - `Optimal rough terrain trajectory generation for wheeled mobile - robots `__ + robots `__ - `State Space Sampling of Feasible Motions for High-Performance Mobile Robot Navigation in Complex - Environments `__ + Environments `__ diff --git a/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst b/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst index a1980ba17e..23f1218a94 100644 --- a/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst +++ b/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst @@ -60,7 +60,7 @@ Ref - `Shunichi09/nonlinear_control: Implementing the nonlinear model predictive control, sliding mode - control `__ + control `__ - `非線形モデル予測制御におけるCGMRES法をpythonで実装する - Qiita `__ diff --git a/docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst b/docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst index a0b145456c..ded187e972 100644 --- a/docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst +++ b/docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst @@ -137,4 +137,4 @@ Simulation results References: ~~~~~~~~~~~ -- `Towards fully autonomous driving: Systems and algorithms `__ +- `Towards fully autonomous driving: Systems and algorithms `__ diff --git a/docs/modules/path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst b/docs/modules/path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst index 59a7166674..de55b545ba 100644 --- a/docs/modules/path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst +++ b/docs/modules/path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst @@ -133,5 +133,5 @@ Reference - `Vehicle Dynamics and Control \| Rajesh Rajamani \| Springer `__ -- `MPC Course Material - MPC Lab @ - UC-Berkeley `__ +- `MPC Book - MPC Lab @ + UC-Berkeley `__ diff --git a/users_comments.md b/users_comments.md index 66e8b4022e..a2f798eac4 100644 --- a/users_comments.md +++ b/users_comments.md @@ -17,13 +17,7 @@ Ref: # Educational users -This is a list of users who are using PythonRobotics for education. - -If you found other users, please make an issue to let me know. - -- [CSCI/ARTI 4530/6530: Introduction to Robotics (Fall 2018), University of Georgia ](http://cobweb.cs.uga.edu/~ramviyas/csci_x530.html) - -- [CIT Modules & Programmes \- COMP9073 \- Automation with Python](https://courses.cit.ie/index.cfm/page/module/moduleId/14416) +If you found users who are using PythonRobotics for education, please make an issue to let me know. # Stargazers location map @@ -386,14 +380,14 @@ Dear Atsushi Sakai,
Thank you so much for creating PythonRobotics and docume 1. B. Blaga, M. Deac, R. W. Y. Al-doori, M. Negru and R. Dǎnescu, "Miniature Autonomous Vehicle Development on Raspberry Pi," 2018 IEEE 14th International Conference on Intelligent Computer Communication and Processing (ICCP), Cluj-Napoca, Romania, 2018, pp. 229-236. doi: 10.1109/ICCP.2018.8516589 keywords: {Automobiles;Task analysis;Autonomous vehicles;Path planning;Global Positioning System;Cameras;miniature autonomous vehicle;path planning;navigation;parking assist;lane detection and tracking;traffic sign recognition}, -URL: http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8516589&isnumber=8516425 +URL: https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8516589&isnumber=8516425 2. Peggy (Yuchun) Wang and Caitlin Hogan, "Path Planning with Dynamic Obstacle Avoidance for a Jumping-Enabled Robot", AA228/CS238 class report, Department of Computer Science, Stanford University, URL: https://web.stanford.edu/class/aa228/reports/2018/final113.pdf -3. Welburn, E, Hakim Khalili, H, Gupta, A, Watson, S & Carrasco, J 2019, A Navigational System for Quadcopter Remote Inspection of Offshore Substations. in The Fifteenth International Conference on Autonomic and Autonomous Systems 2019. URL:https://www.research.manchester.ac.uk/portal/files/107169964/ICAS19_A_Navigational_System_for_Quadcopter_Remote_Inspection_of_Offshore_Substations.pdf +3. Welburn, E, Hakim Khalili, H, Gupta, A, Watson, S & Carrasco, J 2019, A Navigational System for Quadcopter Remote Inspection of Offshore Substations. in The Fifteenth International Conference on Autonomic and Autonomous Systems 2019. URL:https://research.manchester.ac.uk/portal/files/107169964/ICAS19_A_Navigational_System_for_Quadcopter_Remote_Inspection_of_Offshore_Substations.pdf 4. E. Horváth, C. Hajdu, C. Radu and Á. Ballagi, "Range Sensor-based Occupancy Grid Mapping with Signatures," 2019 20th International Carpathian Control Conference (ICCC), Krakow-Wieliczka, Poland, 2019, pp. 1-5. -URL: http://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8765684&isnumber=8765679 +URL: https://ieeexplore.ieee.org/stamp/stamp.jsp?tp=&arnumber=8765684&isnumber=8765679 5. Josie Hughes, Masaru Shimizu, and Arnoud Visser, "A Review of Robot Rescue Simulation Platforms for Robotics Education" URL: https://2019.robocup.org/downloads/program/HughesEtAl2019.pdf @@ -408,7 +402,7 @@ URL: https://arxiv.org/abs/1910.01557 URL: https://pdfs.semanticscholar.org/5c06/f3cb9542a51e1bf1a32523c1bc7fea6cecc5.pdf 9. Brijen Thananjeyan, et al. "ABC-LMPC: Safe Sample-Based Learning MPC for Stochastic Nonlinear Dynamical Systems with Adjustable Boundary Conditions" -URL: https://arxiv.org/pdf/2003.01410.pdf +URL: https://arxiv.org/pdf/2003.01410 # Others From 732db3d40d1dbdcfe60a3a5a02948d306c6a4c88 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sat, 25 Jan 2025 21:06:23 +0900 Subject: [PATCH 051/181] Fix Doc generation warning (#1125) --- docs/getting_started_main.rst | 2 +- .../catmull_rom_spline/catmull_rom_spline_main.rst | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/getting_started_main.rst b/docs/getting_started_main.rst index 004d68227b..256319ab8f 100644 --- a/docs/getting_started_main.rst +++ b/docs/getting_started_main.rst @@ -40,7 +40,7 @@ For development: - `sphinx`_ (for document generation) - `ruff`_ (for code style check) -.. _`Python 3.11.x`: https://www.python.org/ +.. _`Python 3.12.x`: https://www.python.org/ .. _`NumPy`: https://numpy.org/ .. _`SciPy`: https://scipy.org/ .. _`Matplotlib`: https://matplotlib.org/ diff --git a/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst b/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst index 4d8d3bdefe..72e558c486 100644 --- a/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst +++ b/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst @@ -1,5 +1,5 @@ Catmull-Rom Spline Planning ------------------ +---------------------------- .. image:: catmull_rom_path_planning.png @@ -10,7 +10,7 @@ exhibits local control, and maintains 𝐶1 continuity. Catmull-Rom Spline Fundamentals -~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Catmull-Rom splines are a type of cubic spline that passes through a given set of points, known as control points. @@ -24,7 +24,7 @@ Where: * :math:`P_0, P_1, P_2, P_3` are the control points surrounding the parameter :math:`t`. Types of Catmull-Rom Splines -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ There are different types of Catmull-Rom splines based on the choice of the **tau** parameter, which influences how the curve behaves in relation to the control points: @@ -51,7 +51,7 @@ behaves in relation to the control points: Blending Functions -~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~ In Catmull-Rom spline interpolation, blending functions are used to calculate the influence of each control point on the spline at a given parameter :math:`t`. The blending functions ensure that the spline is smooth and passes through the control points while @@ -97,7 +97,7 @@ API References -~~~~~~~~~~ +~~~~~~~~~~~~~~ - `Catmull-Rom Spline - Wikipedia `__ - `Catmull-Rom Splines `__ \ No newline at end of file From bf8f1774b753d3b0915960628386bb220de8f0af Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sat, 25 Jan 2025 23:30:14 +0900 Subject: [PATCH 052/181] Update doc organization (#1126) --- docs/conf.py | 2 +- docs/index_main.rst | 24 +++++----- .../{control => 10_control}/control_main.rst | 0 .../inverted-pendulum.png | Bin .../inverted_pendulum_control_main.rst | 0 .../move_to_a_pose_control_main.rst | 0 .../plot/curvature_plot.png | Bin .../{utils => 11_utils}/plot/plot_main.rst | 0 .../{utils => 11_utils}/utils_main.rst | 0 .../Kalmanfilter_basics_2_5_0.png | Bin .../Kalmanfilter_basics_2_main.rst | 0 .../Kalmanfilter_basics_14_1.png | Bin .../Kalmanfilter_basics_16_0.png | Bin .../Kalmanfilter_basics_19_1.png | Bin .../Kalmanfilter_basics_21_1.png | Bin .../Kalmanfilter_basics_22_0.png | Bin .../Kalmanfilter_basics_28_1.png | Bin .../Kalmanfilter_basics_main.rst | 0 .../appendix_main.rst | 0 .../steering_motion_model/steering_model.png | Bin .../turning_radius_calc1.png | Bin .../turning_radius_calc2.png | Bin .../steering_motion_model_main.rst | 0 .../definition_of_robotics_main.rst | 4 ++ .../software_for_robotics_main.rst | 7 +++ .../technology_for_robotics_main.rst | 4 ++ .../1_introduction/introduction_main.rst | 13 ++++++ ...samble_kalman_filter_localization_main.rst | 0 .../ekf_with_velocity_correction_1_0.png | Bin ...xtended_kalman_filter_localization_1_0.png | Bin ...tended_kalman_filter_localization_main.rst | 0 .../histogram_filter_localization/1.png | Bin .../histogram_filter_localization/2.png | Bin .../histogram_filter_localization/3.png | Bin .../histogram_filter_localization/4.png | Bin .../histogram_filter_localization_main.rst | 0 .../localization_main.rst | 0 .../particle_filter_localization_main.rst | 0 ...cented_kalman_filter_localization_main.rst | 0 .../circle_fitting/circle_fitting_main.rst | 0 .../gaussian_grid_map_main.rst | 0 .../k_means_object_clustering_main.rst | 0 .../grid_map_example.png | Bin .../lidar_to_grid_map_tutorial_12_0.png | Bin .../lidar_to_grid_map_tutorial_14_1.png | Bin .../lidar_to_grid_map_tutorial_5_0.png | Bin .../lidar_to_grid_map_tutorial_7_0.png | Bin .../lidar_to_grid_map_tutorial_8_0.png | Bin .../lidar_to_grid_map_tutorial_main.rst | 0 .../{mapping => 3_mapping}/mapping_main.rst | 0 .../ndt_map/grid_clustering.png | Bin .../ndt_map/ndt_map1.png | Bin .../ndt_map/ndt_map2.png | Bin .../ndt_map/ndt_map_main.rst | 0 .../ndt_map/raw_observations.png | Bin .../normal_vector_calc.png | Bin .../normal_vector_estimation_main.rst | 0 .../ransac_normal_vector_estimation.png | Bin .../farthest_point_sampling.png | Bin .../point_cloud_sampling_main.rst | 0 .../poisson_disk_sampling.png | Bin .../voxel_point_sampling.png | Bin .../ray_casting_grid_map_main.rst | 0 .../rectangle_fitting_main.rst | 0 .../FastSLAM1/FastSLAM1_12_0.png | Bin .../FastSLAM1/FastSLAM1_12_1.png | Bin .../FastSLAM1/FastSLAM1_1_0.png | Bin .../FastSLAM1/FastSLAM1_main.rst | 0 .../FastSLAM2/FastSLAM2_main.rst | 0 .../ekf_slam/ekf_slam_1_0.png | Bin .../ekf_slam/ekf_slam_main.rst | 0 .../graph_slam/graphSLAM_SE2_example.rst | 0 .../graphSLAM_SE2_example_13_0.png | Bin .../graphSLAM_SE2_example_15_0.png | Bin .../graphSLAM_SE2_example_16_0.png | Bin .../graphSLAM_SE2_example_4_0.png | Bin .../graphSLAM_SE2_example_8_0.png | Bin .../graphSLAM_SE2_example_9_0.png | Bin .../graph_slam/graphSLAM_doc.rst | 0 .../graphSLAM_doc_11_1.png | Bin .../graphSLAM_doc_11_2.png | Bin .../graphSLAM_doc_files/graphSLAM_doc_2_0.png | Bin .../graphSLAM_doc_files/graphSLAM_doc_2_2.png | Bin .../graphSLAM_doc_files/graphSLAM_doc_4_0.png | Bin .../graphSLAM_doc_files/graphSLAM_doc_9_1.png | Bin .../graph_slam/graphSLAM_formulation.rst | 0 .../graph_slam/graph_slam_main.rst | 0 .../iterative_closest_point_matching_main.rst | 0 docs/modules/{slam => 4_slam}/slam_main.rst | 0 .../bezier_path/Figure_1.png | Bin .../bezier_path/Figure_2.png | Bin .../bezier_path/bezier_path_main.rst | 0 .../bspline_path/approx_and_curvature.png | Bin .../bspline_path/approximation1.png | Bin .../bspline_path/basis_functions.png | Bin .../bspline_path/bspline_path_main.rst | 0 .../bspline_path/bspline_path_planning.png | Bin .../bspline_path/interp_and_curvature.png | Bin .../bspline_path/interpolation1.png | Bin .../bugplanner/bugplanner_main.rst | 0 .../catmull_rom_spline/blending_functions.png | Bin .../catmull_rom_path_planning.png | Bin .../catmull_rom_spline_main.rst | 0 .../closed_loop_rrt_star_car/Figure_1.png | Bin .../closed_loop_rrt_star_car/Figure_3.png | Bin .../closed_loop_rrt_star_car/Figure_4.png | Bin .../closed_loop_rrt_star_car/Figure_5.png | Bin .../clothoid_path/clothoid_path_main.rst | 0 .../coverage_path/coverage_path_main.rst | 0 .../cubic_spline/cubic_spline_1d.png | Bin .../cubic_spline_2d_curvature.png | Bin .../cubic_spline/cubic_spline_2d_path.png | Bin .../cubic_spline/cubic_spline_2d_yaw.png | Bin .../cubic_spline/cubic_spline_main.rst | 0 .../cubic_spline/spline.png | Bin .../cubic_spline/spline_continuity.png | Bin .../dubins_path/RLR.jpg | Bin .../dubins_path/RSR.jpg | Bin .../dubins_path/dubins_path.jpg | Bin .../dubins_path/dubins_path_main.rst | 0 .../dynamic_window_approach_main.rst | 0 .../eta3_spline/eta3_spline_main.rst | 0 .../frenet_frame_path_main.rst | 0 .../grid_base_search_main.rst | 0 .../hybridastar/hybridastar_main.rst | 0 .../lqr_path/lqr_path_main.rst | 0 .../lookup_table.png | Bin ...l_predictive_trajectory_generator_main.rst | 0 .../path_planning_main.rst | 0 .../prm_planner/prm_planner_main.rst | 0 .../quintic_polynomials_planner_main.rst | 0 .../reeds_shepp_path/LR_L.png | Bin .../reeds_shepp_path/LR_LR.png | Bin .../reeds_shepp_path/LSL.png | Bin .../reeds_shepp_path/LSL90xR.png | Bin .../reeds_shepp_path/LSR.png | Bin .../reeds_shepp_path/LSR90_L.png | Bin .../reeds_shepp_path/L_R90SL.png | Bin .../reeds_shepp_path/L_R90SL90_R.png | Bin .../reeds_shepp_path/L_R90SR.png | Bin .../reeds_shepp_path/L_RL.png | Bin .../reeds_shepp_path/L_RL_R.png | Bin .../reeds_shepp_path/L_R_L.png | Bin .../reeds_shepp_path_main.rst | 0 .../rrt/figure_1.png | Bin .../rrt/rrt_main.rst | 0 .../rrt/rrt_star.rst | 0 .../rrt/rrt_star/rrt_star_1_0.png | Bin .../rrt/rrt_star_reeds_shepp/figure_1.png | Bin .../state_lattice_planner/Figure_1.png | Bin .../state_lattice_planner/Figure_2.png | Bin .../state_lattice_planner/Figure_3.png | Bin .../state_lattice_planner/Figure_4.png | Bin .../state_lattice_planner/Figure_5.png | Bin .../state_lattice_planner/Figure_6.png | Bin .../state_lattice_planner_main.rst | 0 .../visibility_road_map_planner/step0.png | Bin .../visibility_road_map_planner/step1.png | Bin .../visibility_road_map_planner/step2.png | Bin .../visibility_road_map_planner/step3.png | Bin .../visibility_road_map_planner_main.rst | 0 .../vrm_planner/vrm_planner_main.rst | 0 .../cgmres_nmpc/cgmres_nmpc_1_0.png | Bin .../cgmres_nmpc/cgmres_nmpc_2_0.png | Bin .../cgmres_nmpc/cgmres_nmpc_3_0.png | Bin .../cgmres_nmpc/cgmres_nmpc_4_0.png | Bin .../cgmres_nmpc/cgmres_nmpc_main.rst | 0 .../lqr_speed_and_steering_control_main.rst | 0 .../lqr_steering_control_model.jpg | Bin .../lqr_speed_and_steering_control/speed.png | Bin .../lqr_speed_and_steering_control/x-y.png | Bin .../lqr_speed_and_steering_control/yaw.png | Bin .../lqr_steering_control_main.rst | 0 .../lqr_steering_control_model.jpg | Bin ...ictive_speed_and_steering_control_main.rst | 0 .../path_tracking_main.rst | 0 .../pure_pursuit_tracking_main.rst | 0 .../rear_wheel_feedback_control_main.rst | 0 .../stanley_control/stanley_control_main.rst | 0 .../Planar_Two_Link_IK_12_0.png | Bin .../Planar_Two_Link_IK_5_0.png | Bin .../Planar_Two_Link_IK_7_0.png | Bin .../Planar_Two_Link_IK_9_0.png | Bin .../arm_navigation_main.rst | 0 .../n_joint_arm_to_point_control_main.rst | 0 ...obstacle_avoidance_arm_navigation_main.rst | 0 .../planar_two_link_ik_main.rst | 0 .../aerial_navigation_main.rst | 0 .../drone_3d_trajectory_following_main.rst | 0 .../rocket_powered_landing_main.rst | 0 .../{bipedal => 9_bipedal}/bipedal_main.rst | 0 .../bipedal_planner/bipedal_planner_main.rst | 0 docs/modules/introduction_main.rst | 42 ------------------ .../Graph_SLAM_optimization.gif | Bin 114966 -> 0 bytes 194 files changed, 41 insertions(+), 55 deletions(-) rename docs/modules/{control => 10_control}/control_main.rst (100%) rename docs/modules/{control => 10_control}/inverted_pendulum_control/inverted-pendulum.png (100%) rename docs/modules/{control => 10_control}/inverted_pendulum_control/inverted_pendulum_control_main.rst (100%) rename docs/modules/{control => 10_control}/move_to_a_pose_control/move_to_a_pose_control_main.rst (100%) rename docs/modules/{utils => 11_utils}/plot/curvature_plot.png (100%) rename docs/modules/{utils => 11_utils}/plot/plot_main.rst (100%) rename docs/modules/{utils => 11_utils}/utils_main.rst (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_2_files/Kalmanfilter_basics_2_5_0.png (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_2_main.rst (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_files/Kalmanfilter_basics_14_1.png (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_files/Kalmanfilter_basics_16_0.png (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_files/Kalmanfilter_basics_19_1.png (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_files/Kalmanfilter_basics_21_1.png (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_files/Kalmanfilter_basics_22_0.png (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_files/Kalmanfilter_basics_28_1.png (100%) rename docs/modules/{appendix => 12_appendix}/Kalmanfilter_basics_main.rst (100%) rename docs/modules/{appendix => 12_appendix}/appendix_main.rst (100%) rename docs/modules/{appendix => 12_appendix}/steering_motion_model/steering_model.png (100%) rename docs/modules/{appendix => 12_appendix}/steering_motion_model/turning_radius_calc1.png (100%) rename docs/modules/{appendix => 12_appendix}/steering_motion_model/turning_radius_calc2.png (100%) rename docs/modules/{appendix => 12_appendix}/steering_motion_model_main.rst (100%) create mode 100644 docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst create mode 100644 docs/modules/1_introduction/2_software_for_robotics/software_for_robotics_main.rst create mode 100644 docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst create mode 100644 docs/modules/1_introduction/introduction_main.rst rename docs/modules/{localization => 2_localization}/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst (100%) rename docs/modules/{localization => 2_localization}/extended_kalman_filter_localization_files/ekf_with_velocity_correction_1_0.png (100%) rename docs/modules/{localization => 2_localization}/extended_kalman_filter_localization_files/extended_kalman_filter_localization_1_0.png (100%) rename docs/modules/{localization => 2_localization}/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst (100%) rename docs/modules/{localization => 2_localization}/histogram_filter_localization/1.png (100%) rename docs/modules/{localization => 2_localization}/histogram_filter_localization/2.png (100%) rename docs/modules/{localization => 2_localization}/histogram_filter_localization/3.png (100%) rename docs/modules/{localization => 2_localization}/histogram_filter_localization/4.png (100%) rename docs/modules/{localization => 2_localization}/histogram_filter_localization/histogram_filter_localization_main.rst (100%) rename docs/modules/{localization => 2_localization}/localization_main.rst (100%) rename docs/modules/{localization => 2_localization}/particle_filter_localization/particle_filter_localization_main.rst (100%) rename docs/modules/{localization => 2_localization}/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/circle_fitting/circle_fitting_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/gaussian_grid_map/gaussian_grid_map_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/k_means_object_clustering/k_means_object_clustering_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/lidar_to_grid_map_tutorial/grid_map_example.png (100%) rename docs/modules/{mapping => 3_mapping}/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_12_0.png (100%) rename docs/modules/{mapping => 3_mapping}/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_14_1.png (100%) rename docs/modules/{mapping => 3_mapping}/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_5_0.png (100%) rename docs/modules/{mapping => 3_mapping}/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_7_0.png (100%) rename docs/modules/{mapping => 3_mapping}/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_8_0.png (100%) rename docs/modules/{mapping => 3_mapping}/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/mapping_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/ndt_map/grid_clustering.png (100%) rename docs/modules/{mapping => 3_mapping}/ndt_map/ndt_map1.png (100%) rename docs/modules/{mapping => 3_mapping}/ndt_map/ndt_map2.png (100%) rename docs/modules/{mapping => 3_mapping}/ndt_map/ndt_map_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/ndt_map/raw_observations.png (100%) rename docs/modules/{mapping => 3_mapping}/normal_vector_estimation/normal_vector_calc.png (100%) rename docs/modules/{mapping => 3_mapping}/normal_vector_estimation/normal_vector_estimation_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/normal_vector_estimation/ransac_normal_vector_estimation.png (100%) rename docs/modules/{mapping => 3_mapping}/point_cloud_sampling/farthest_point_sampling.png (100%) rename docs/modules/{mapping => 3_mapping}/point_cloud_sampling/point_cloud_sampling_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/point_cloud_sampling/poisson_disk_sampling.png (100%) rename docs/modules/{mapping => 3_mapping}/point_cloud_sampling/voxel_point_sampling.png (100%) rename docs/modules/{mapping => 3_mapping}/ray_casting_grid_map/ray_casting_grid_map_main.rst (100%) rename docs/modules/{mapping => 3_mapping}/rectangle_fitting/rectangle_fitting_main.rst (100%) rename docs/modules/{slam => 4_slam}/FastSLAM1/FastSLAM1_12_0.png (100%) rename docs/modules/{slam => 4_slam}/FastSLAM1/FastSLAM1_12_1.png (100%) rename docs/modules/{slam => 4_slam}/FastSLAM1/FastSLAM1_1_0.png (100%) rename docs/modules/{slam => 4_slam}/FastSLAM1/FastSLAM1_main.rst (100%) rename docs/modules/{slam => 4_slam}/FastSLAM2/FastSLAM2_main.rst (100%) rename docs/modules/{slam => 4_slam}/ekf_slam/ekf_slam_1_0.png (100%) rename docs/modules/{slam => 4_slam}/ekf_slam/ekf_slam_main.rst (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_SE2_example.rst (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_13_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_15_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_16_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_4_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_8_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_9_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_doc.rst (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_1.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_2.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_2.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_doc_files/graphSLAM_doc_4_0.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_doc_files/graphSLAM_doc_9_1.png (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graphSLAM_formulation.rst (100%) rename docs/modules/{slam => 4_slam}/graph_slam/graph_slam_main.rst (100%) rename docs/modules/{slam => 4_slam}/iterative_closest_point_matching/iterative_closest_point_matching_main.rst (100%) rename docs/modules/{slam => 4_slam}/slam_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/bezier_path/Figure_1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bezier_path/Figure_2.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bezier_path/bezier_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/bspline_path/approx_and_curvature.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bspline_path/approximation1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bspline_path/basis_functions.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bspline_path/bspline_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/bspline_path/bspline_path_planning.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bspline_path/interp_and_curvature.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bspline_path/interpolation1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/bugplanner/bugplanner_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/catmull_rom_spline/blending_functions.png (100%) rename docs/modules/{path_planning => 5_path_planning}/catmull_rom_spline/catmull_rom_path_planning.png (100%) rename docs/modules/{path_planning => 5_path_planning}/catmull_rom_spline/catmull_rom_spline_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/closed_loop_rrt_star_car/Figure_1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/closed_loop_rrt_star_car/Figure_3.png (100%) rename docs/modules/{path_planning => 5_path_planning}/closed_loop_rrt_star_car/Figure_4.png (100%) rename docs/modules/{path_planning => 5_path_planning}/closed_loop_rrt_star_car/Figure_5.png (100%) rename docs/modules/{path_planning => 5_path_planning}/clothoid_path/clothoid_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/coverage_path/coverage_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/cubic_spline/cubic_spline_1d.png (100%) rename docs/modules/{path_planning => 5_path_planning}/cubic_spline/cubic_spline_2d_curvature.png (100%) rename docs/modules/{path_planning => 5_path_planning}/cubic_spline/cubic_spline_2d_path.png (100%) rename docs/modules/{path_planning => 5_path_planning}/cubic_spline/cubic_spline_2d_yaw.png (100%) rename docs/modules/{path_planning => 5_path_planning}/cubic_spline/cubic_spline_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/cubic_spline/spline.png (100%) rename docs/modules/{path_planning => 5_path_planning}/cubic_spline/spline_continuity.png (100%) rename docs/modules/{path_planning => 5_path_planning}/dubins_path/RLR.jpg (100%) rename docs/modules/{path_planning => 5_path_planning}/dubins_path/RSR.jpg (100%) rename docs/modules/{path_planning => 5_path_planning}/dubins_path/dubins_path.jpg (100%) rename docs/modules/{path_planning => 5_path_planning}/dubins_path/dubins_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/dynamic_window_approach/dynamic_window_approach_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/eta3_spline/eta3_spline_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/frenet_frame_path/frenet_frame_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/grid_base_search/grid_base_search_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/hybridastar/hybridastar_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/lqr_path/lqr_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/model_predictive_trajectory_generator/lookup_table.png (100%) rename docs/modules/{path_planning => 5_path_planning}/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/path_planning_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/prm_planner/prm_planner_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/quintic_polynomials_planner/quintic_polynomials_planner_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/LR_L.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/LR_LR.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/LSL.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/LSL90xR.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/LSR.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/LSR90_L.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/L_R90SL.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/L_R90SL90_R.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/L_R90SR.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/L_RL.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/L_RL_R.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/L_R_L.png (100%) rename docs/modules/{path_planning => 5_path_planning}/reeds_shepp_path/reeds_shepp_path_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/rrt/figure_1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/rrt/rrt_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/rrt/rrt_star.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/rrt/rrt_star/rrt_star_1_0.png (100%) rename docs/modules/{path_planning => 5_path_planning}/rrt/rrt_star_reeds_shepp/figure_1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/state_lattice_planner/Figure_1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/state_lattice_planner/Figure_2.png (100%) rename docs/modules/{path_planning => 5_path_planning}/state_lattice_planner/Figure_3.png (100%) rename docs/modules/{path_planning => 5_path_planning}/state_lattice_planner/Figure_4.png (100%) rename docs/modules/{path_planning => 5_path_planning}/state_lattice_planner/Figure_5.png (100%) rename docs/modules/{path_planning => 5_path_planning}/state_lattice_planner/Figure_6.png (100%) rename docs/modules/{path_planning => 5_path_planning}/state_lattice_planner/state_lattice_planner_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/visibility_road_map_planner/step0.png (100%) rename docs/modules/{path_planning => 5_path_planning}/visibility_road_map_planner/step1.png (100%) rename docs/modules/{path_planning => 5_path_planning}/visibility_road_map_planner/step2.png (100%) rename docs/modules/{path_planning => 5_path_planning}/visibility_road_map_planner/step3.png (100%) rename docs/modules/{path_planning => 5_path_planning}/visibility_road_map_planner/visibility_road_map_planner_main.rst (100%) rename docs/modules/{path_planning => 5_path_planning}/vrm_planner/vrm_planner_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/cgmres_nmpc/cgmres_nmpc_1_0.png (100%) rename docs/modules/{path_tracking => 6_path_tracking}/cgmres_nmpc/cgmres_nmpc_2_0.png (100%) rename docs/modules/{path_tracking => 6_path_tracking}/cgmres_nmpc/cgmres_nmpc_3_0.png (100%) rename docs/modules/{path_tracking => 6_path_tracking}/cgmres_nmpc/cgmres_nmpc_4_0.png (100%) rename docs/modules/{path_tracking => 6_path_tracking}/cgmres_nmpc/cgmres_nmpc_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/lqr_speed_and_steering_control/lqr_steering_control_model.jpg (100%) rename docs/modules/{path_tracking => 6_path_tracking}/lqr_speed_and_steering_control/speed.png (100%) rename docs/modules/{path_tracking => 6_path_tracking}/lqr_speed_and_steering_control/x-y.png (100%) rename docs/modules/{path_tracking => 6_path_tracking}/lqr_speed_and_steering_control/yaw.png (100%) rename docs/modules/{path_tracking => 6_path_tracking}/lqr_steering_control/lqr_steering_control_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/lqr_steering_control/lqr_steering_control_model.jpg (100%) rename docs/modules/{path_tracking => 6_path_tracking}/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/path_tracking_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/pure_pursuit_tracking/pure_pursuit_tracking_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst (100%) rename docs/modules/{path_tracking => 6_path_tracking}/stanley_control/stanley_control_main.rst (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/Planar_Two_Link_IK_files/Planar_Two_Link_IK_12_0.png (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/Planar_Two_Link_IK_files/Planar_Two_Link_IK_5_0.png (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/Planar_Two_Link_IK_files/Planar_Two_Link_IK_7_0.png (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/Planar_Two_Link_IK_files/Planar_Two_Link_IK_9_0.png (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/arm_navigation_main.rst (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/n_joint_arm_to_point_control_main.rst (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/obstacle_avoidance_arm_navigation_main.rst (100%) rename docs/modules/{arm_navigation => 7_arm_navigation}/planar_two_link_ik_main.rst (100%) rename docs/modules/{aerial_navigation => 8_aerial_navigation}/aerial_navigation_main.rst (100%) rename docs/modules/{aerial_navigation => 8_aerial_navigation}/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst (100%) rename docs/modules/{aerial_navigation => 8_aerial_navigation}/rocket_powered_landing/rocket_powered_landing_main.rst (100%) rename docs/modules/{bipedal => 9_bipedal}/bipedal_main.rst (100%) rename docs/modules/{bipedal => 9_bipedal}/bipedal_planner/bipedal_planner_main.rst (100%) delete mode 100644 docs/modules/introduction_main.rst delete mode 100644 docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/Graph_SLAM_optimization.gif diff --git a/docs/conf.py b/docs/conf.py index 4e06f3f988..919fa9ac76 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ # -- Project information ----------------------------------------------------- project = 'PythonRobotics' -copyright = '2018-2023, Atsushi Sakai' +copyright = '2018-now, Atsushi Sakai' author = 'Atsushi Sakai' # The short X.Y version diff --git a/docs/index_main.rst b/docs/index_main.rst index 67bd9889f2..08c318ea83 100644 --- a/docs/index_main.rst +++ b/docs/index_main.rst @@ -34,18 +34,18 @@ See this paper for more details: :caption: Contents getting_started - modules/introduction - modules/localization/localization - modules/mapping/mapping - modules/slam/slam - modules/path_planning/path_planning - modules/path_tracking/path_tracking - modules/arm_navigation/arm_navigation - modules/aerial_navigation/aerial_navigation - modules/bipedal/bipedal - modules/control/control - modules/utils/utils - modules/appendix/appendix + modules/1_introduction/introduction + modules/2_localization/localization + modules/3_mapping/mapping + modules/4_slam/slam + modules/5_path_planning/path_planning + modules/6_path_tracking/path_tracking + modules/7_arm_navigation/arm_navigation + modules/8_aerial_navigation/aerial_navigation + modules/9_bipedal/bipedal + modules/10_control/control + modules/11_utils/utils + modules/12_appendix/appendix how_to_contribute diff --git a/docs/modules/control/control_main.rst b/docs/modules/10_control/control_main.rst similarity index 100% rename from docs/modules/control/control_main.rst rename to docs/modules/10_control/control_main.rst diff --git a/docs/modules/control/inverted_pendulum_control/inverted-pendulum.png b/docs/modules/10_control/inverted_pendulum_control/inverted-pendulum.png similarity index 100% rename from docs/modules/control/inverted_pendulum_control/inverted-pendulum.png rename to docs/modules/10_control/inverted_pendulum_control/inverted-pendulum.png diff --git a/docs/modules/control/inverted_pendulum_control/inverted_pendulum_control_main.rst b/docs/modules/10_control/inverted_pendulum_control/inverted_pendulum_control_main.rst similarity index 100% rename from docs/modules/control/inverted_pendulum_control/inverted_pendulum_control_main.rst rename to docs/modules/10_control/inverted_pendulum_control/inverted_pendulum_control_main.rst diff --git a/docs/modules/control/move_to_a_pose_control/move_to_a_pose_control_main.rst b/docs/modules/10_control/move_to_a_pose_control/move_to_a_pose_control_main.rst similarity index 100% rename from docs/modules/control/move_to_a_pose_control/move_to_a_pose_control_main.rst rename to docs/modules/10_control/move_to_a_pose_control/move_to_a_pose_control_main.rst diff --git a/docs/modules/utils/plot/curvature_plot.png b/docs/modules/11_utils/plot/curvature_plot.png similarity index 100% rename from docs/modules/utils/plot/curvature_plot.png rename to docs/modules/11_utils/plot/curvature_plot.png diff --git a/docs/modules/utils/plot/plot_main.rst b/docs/modules/11_utils/plot/plot_main.rst similarity index 100% rename from docs/modules/utils/plot/plot_main.rst rename to docs/modules/11_utils/plot/plot_main.rst diff --git a/docs/modules/utils/utils_main.rst b/docs/modules/11_utils/utils_main.rst similarity index 100% rename from docs/modules/utils/utils_main.rst rename to docs/modules/11_utils/utils_main.rst diff --git a/docs/modules/appendix/Kalmanfilter_basics_2_files/Kalmanfilter_basics_2_5_0.png b/docs/modules/12_appendix/Kalmanfilter_basics_2_files/Kalmanfilter_basics_2_5_0.png similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_2_files/Kalmanfilter_basics_2_5_0.png rename to docs/modules/12_appendix/Kalmanfilter_basics_2_files/Kalmanfilter_basics_2_5_0.png diff --git a/docs/modules/appendix/Kalmanfilter_basics_2_main.rst b/docs/modules/12_appendix/Kalmanfilter_basics_2_main.rst similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_2_main.rst rename to docs/modules/12_appendix/Kalmanfilter_basics_2_main.rst diff --git a/docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_14_1.png b/docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_14_1.png similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_14_1.png rename to docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_14_1.png diff --git a/docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_16_0.png b/docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_16_0.png similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_16_0.png rename to docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_16_0.png diff --git a/docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_19_1.png b/docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_19_1.png similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_19_1.png rename to docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_19_1.png diff --git a/docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_21_1.png b/docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_21_1.png similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_21_1.png rename to docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_21_1.png diff --git a/docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_22_0.png b/docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_22_0.png similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_22_0.png rename to docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_22_0.png diff --git a/docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_28_1.png b/docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_28_1.png similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_28_1.png rename to docs/modules/12_appendix/Kalmanfilter_basics_files/Kalmanfilter_basics_28_1.png diff --git a/docs/modules/appendix/Kalmanfilter_basics_main.rst b/docs/modules/12_appendix/Kalmanfilter_basics_main.rst similarity index 100% rename from docs/modules/appendix/Kalmanfilter_basics_main.rst rename to docs/modules/12_appendix/Kalmanfilter_basics_main.rst diff --git a/docs/modules/appendix/appendix_main.rst b/docs/modules/12_appendix/appendix_main.rst similarity index 100% rename from docs/modules/appendix/appendix_main.rst rename to docs/modules/12_appendix/appendix_main.rst diff --git a/docs/modules/appendix/steering_motion_model/steering_model.png b/docs/modules/12_appendix/steering_motion_model/steering_model.png similarity index 100% rename from docs/modules/appendix/steering_motion_model/steering_model.png rename to docs/modules/12_appendix/steering_motion_model/steering_model.png diff --git a/docs/modules/appendix/steering_motion_model/turning_radius_calc1.png b/docs/modules/12_appendix/steering_motion_model/turning_radius_calc1.png similarity index 100% rename from docs/modules/appendix/steering_motion_model/turning_radius_calc1.png rename to docs/modules/12_appendix/steering_motion_model/turning_radius_calc1.png diff --git a/docs/modules/appendix/steering_motion_model/turning_radius_calc2.png b/docs/modules/12_appendix/steering_motion_model/turning_radius_calc2.png similarity index 100% rename from docs/modules/appendix/steering_motion_model/turning_radius_calc2.png rename to docs/modules/12_appendix/steering_motion_model/turning_radius_calc2.png diff --git a/docs/modules/appendix/steering_motion_model_main.rst b/docs/modules/12_appendix/steering_motion_model_main.rst similarity index 100% rename from docs/modules/appendix/steering_motion_model_main.rst rename to docs/modules/12_appendix/steering_motion_model_main.rst diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst new file mode 100644 index 0000000000..bdc7dc53e9 --- /dev/null +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -0,0 +1,4 @@ +Definition of Robotics +---------------------- + +TBD diff --git a/docs/modules/1_introduction/2_software_for_robotics/software_for_robotics_main.rst b/docs/modules/1_introduction/2_software_for_robotics/software_for_robotics_main.rst new file mode 100644 index 0000000000..835441f85d --- /dev/null +++ b/docs/modules/1_introduction/2_software_for_robotics/software_for_robotics_main.rst @@ -0,0 +1,7 @@ +Software for Robotics +---------------------- + +Python for Robotics +~~~~~~~~~~~~~~~~~~~~~ + +TBD diff --git a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst new file mode 100644 index 0000000000..4c60ab8851 --- /dev/null +++ b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst @@ -0,0 +1,4 @@ +Technology for Robotics +------------------------- + +TBD diff --git a/docs/modules/1_introduction/introduction_main.rst b/docs/modules/1_introduction/introduction_main.rst new file mode 100644 index 0000000000..4561efd349 --- /dev/null +++ b/docs/modules/1_introduction/introduction_main.rst @@ -0,0 +1,13 @@ +.. _introduction: + +Introduction +============ + +.. toctree:: + :maxdepth: 2 + :caption: Table of Contents + + 1_definition_of_robotics/definition_of_robotics + 2_software_for_robotics/software_for_robotics + 3_technology_for_robotics/technology_for_robotics + diff --git a/docs/modules/localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst b/docs/modules/2_localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst similarity index 100% rename from docs/modules/localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst rename to docs/modules/2_localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst diff --git a/docs/modules/localization/extended_kalman_filter_localization_files/ekf_with_velocity_correction_1_0.png b/docs/modules/2_localization/extended_kalman_filter_localization_files/ekf_with_velocity_correction_1_0.png similarity index 100% rename from docs/modules/localization/extended_kalman_filter_localization_files/ekf_with_velocity_correction_1_0.png rename to docs/modules/2_localization/extended_kalman_filter_localization_files/ekf_with_velocity_correction_1_0.png diff --git a/docs/modules/localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_1_0.png b/docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_1_0.png similarity index 100% rename from docs/modules/localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_1_0.png rename to docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_1_0.png diff --git a/docs/modules/localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst b/docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst similarity index 100% rename from docs/modules/localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst rename to docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst diff --git a/docs/modules/localization/histogram_filter_localization/1.png b/docs/modules/2_localization/histogram_filter_localization/1.png similarity index 100% rename from docs/modules/localization/histogram_filter_localization/1.png rename to docs/modules/2_localization/histogram_filter_localization/1.png diff --git a/docs/modules/localization/histogram_filter_localization/2.png b/docs/modules/2_localization/histogram_filter_localization/2.png similarity index 100% rename from docs/modules/localization/histogram_filter_localization/2.png rename to docs/modules/2_localization/histogram_filter_localization/2.png diff --git a/docs/modules/localization/histogram_filter_localization/3.png b/docs/modules/2_localization/histogram_filter_localization/3.png similarity index 100% rename from docs/modules/localization/histogram_filter_localization/3.png rename to docs/modules/2_localization/histogram_filter_localization/3.png diff --git a/docs/modules/localization/histogram_filter_localization/4.png b/docs/modules/2_localization/histogram_filter_localization/4.png similarity index 100% rename from docs/modules/localization/histogram_filter_localization/4.png rename to docs/modules/2_localization/histogram_filter_localization/4.png diff --git a/docs/modules/localization/histogram_filter_localization/histogram_filter_localization_main.rst b/docs/modules/2_localization/histogram_filter_localization/histogram_filter_localization_main.rst similarity index 100% rename from docs/modules/localization/histogram_filter_localization/histogram_filter_localization_main.rst rename to docs/modules/2_localization/histogram_filter_localization/histogram_filter_localization_main.rst diff --git a/docs/modules/localization/localization_main.rst b/docs/modules/2_localization/localization_main.rst similarity index 100% rename from docs/modules/localization/localization_main.rst rename to docs/modules/2_localization/localization_main.rst diff --git a/docs/modules/localization/particle_filter_localization/particle_filter_localization_main.rst b/docs/modules/2_localization/particle_filter_localization/particle_filter_localization_main.rst similarity index 100% rename from docs/modules/localization/particle_filter_localization/particle_filter_localization_main.rst rename to docs/modules/2_localization/particle_filter_localization/particle_filter_localization_main.rst diff --git a/docs/modules/localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst b/docs/modules/2_localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst similarity index 100% rename from docs/modules/localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst rename to docs/modules/2_localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst diff --git a/docs/modules/mapping/circle_fitting/circle_fitting_main.rst b/docs/modules/3_mapping/circle_fitting/circle_fitting_main.rst similarity index 100% rename from docs/modules/mapping/circle_fitting/circle_fitting_main.rst rename to docs/modules/3_mapping/circle_fitting/circle_fitting_main.rst diff --git a/docs/modules/mapping/gaussian_grid_map/gaussian_grid_map_main.rst b/docs/modules/3_mapping/gaussian_grid_map/gaussian_grid_map_main.rst similarity index 100% rename from docs/modules/mapping/gaussian_grid_map/gaussian_grid_map_main.rst rename to docs/modules/3_mapping/gaussian_grid_map/gaussian_grid_map_main.rst diff --git a/docs/modules/mapping/k_means_object_clustering/k_means_object_clustering_main.rst b/docs/modules/3_mapping/k_means_object_clustering/k_means_object_clustering_main.rst similarity index 100% rename from docs/modules/mapping/k_means_object_clustering/k_means_object_clustering_main.rst rename to docs/modules/3_mapping/k_means_object_clustering/k_means_object_clustering_main.rst diff --git a/docs/modules/mapping/lidar_to_grid_map_tutorial/grid_map_example.png b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/grid_map_example.png similarity index 100% rename from docs/modules/mapping/lidar_to_grid_map_tutorial/grid_map_example.png rename to docs/modules/3_mapping/lidar_to_grid_map_tutorial/grid_map_example.png diff --git a/docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_12_0.png b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_12_0.png similarity index 100% rename from docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_12_0.png rename to docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_12_0.png diff --git a/docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_14_1.png b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_14_1.png similarity index 100% rename from docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_14_1.png rename to docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_14_1.png diff --git a/docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_5_0.png b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_5_0.png similarity index 100% rename from docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_5_0.png rename to docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_5_0.png diff --git a/docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_7_0.png b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_7_0.png similarity index 100% rename from docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_7_0.png rename to docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_7_0.png diff --git a/docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_8_0.png b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_8_0.png similarity index 100% rename from docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_8_0.png rename to docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_8_0.png diff --git a/docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst similarity index 100% rename from docs/modules/mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst rename to docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst diff --git a/docs/modules/mapping/mapping_main.rst b/docs/modules/3_mapping/mapping_main.rst similarity index 100% rename from docs/modules/mapping/mapping_main.rst rename to docs/modules/3_mapping/mapping_main.rst diff --git a/docs/modules/mapping/ndt_map/grid_clustering.png b/docs/modules/3_mapping/ndt_map/grid_clustering.png similarity index 100% rename from docs/modules/mapping/ndt_map/grid_clustering.png rename to docs/modules/3_mapping/ndt_map/grid_clustering.png diff --git a/docs/modules/mapping/ndt_map/ndt_map1.png b/docs/modules/3_mapping/ndt_map/ndt_map1.png similarity index 100% rename from docs/modules/mapping/ndt_map/ndt_map1.png rename to docs/modules/3_mapping/ndt_map/ndt_map1.png diff --git a/docs/modules/mapping/ndt_map/ndt_map2.png b/docs/modules/3_mapping/ndt_map/ndt_map2.png similarity index 100% rename from docs/modules/mapping/ndt_map/ndt_map2.png rename to docs/modules/3_mapping/ndt_map/ndt_map2.png diff --git a/docs/modules/mapping/ndt_map/ndt_map_main.rst b/docs/modules/3_mapping/ndt_map/ndt_map_main.rst similarity index 100% rename from docs/modules/mapping/ndt_map/ndt_map_main.rst rename to docs/modules/3_mapping/ndt_map/ndt_map_main.rst diff --git a/docs/modules/mapping/ndt_map/raw_observations.png b/docs/modules/3_mapping/ndt_map/raw_observations.png similarity index 100% rename from docs/modules/mapping/ndt_map/raw_observations.png rename to docs/modules/3_mapping/ndt_map/raw_observations.png diff --git a/docs/modules/mapping/normal_vector_estimation/normal_vector_calc.png b/docs/modules/3_mapping/normal_vector_estimation/normal_vector_calc.png similarity index 100% rename from docs/modules/mapping/normal_vector_estimation/normal_vector_calc.png rename to docs/modules/3_mapping/normal_vector_estimation/normal_vector_calc.png diff --git a/docs/modules/mapping/normal_vector_estimation/normal_vector_estimation_main.rst b/docs/modules/3_mapping/normal_vector_estimation/normal_vector_estimation_main.rst similarity index 100% rename from docs/modules/mapping/normal_vector_estimation/normal_vector_estimation_main.rst rename to docs/modules/3_mapping/normal_vector_estimation/normal_vector_estimation_main.rst diff --git a/docs/modules/mapping/normal_vector_estimation/ransac_normal_vector_estimation.png b/docs/modules/3_mapping/normal_vector_estimation/ransac_normal_vector_estimation.png similarity index 100% rename from docs/modules/mapping/normal_vector_estimation/ransac_normal_vector_estimation.png rename to docs/modules/3_mapping/normal_vector_estimation/ransac_normal_vector_estimation.png diff --git a/docs/modules/mapping/point_cloud_sampling/farthest_point_sampling.png b/docs/modules/3_mapping/point_cloud_sampling/farthest_point_sampling.png similarity index 100% rename from docs/modules/mapping/point_cloud_sampling/farthest_point_sampling.png rename to docs/modules/3_mapping/point_cloud_sampling/farthest_point_sampling.png diff --git a/docs/modules/mapping/point_cloud_sampling/point_cloud_sampling_main.rst b/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst similarity index 100% rename from docs/modules/mapping/point_cloud_sampling/point_cloud_sampling_main.rst rename to docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst diff --git a/docs/modules/mapping/point_cloud_sampling/poisson_disk_sampling.png b/docs/modules/3_mapping/point_cloud_sampling/poisson_disk_sampling.png similarity index 100% rename from docs/modules/mapping/point_cloud_sampling/poisson_disk_sampling.png rename to docs/modules/3_mapping/point_cloud_sampling/poisson_disk_sampling.png diff --git a/docs/modules/mapping/point_cloud_sampling/voxel_point_sampling.png b/docs/modules/3_mapping/point_cloud_sampling/voxel_point_sampling.png similarity index 100% rename from docs/modules/mapping/point_cloud_sampling/voxel_point_sampling.png rename to docs/modules/3_mapping/point_cloud_sampling/voxel_point_sampling.png diff --git a/docs/modules/mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst b/docs/modules/3_mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst similarity index 100% rename from docs/modules/mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst rename to docs/modules/3_mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst diff --git a/docs/modules/mapping/rectangle_fitting/rectangle_fitting_main.rst b/docs/modules/3_mapping/rectangle_fitting/rectangle_fitting_main.rst similarity index 100% rename from docs/modules/mapping/rectangle_fitting/rectangle_fitting_main.rst rename to docs/modules/3_mapping/rectangle_fitting/rectangle_fitting_main.rst diff --git a/docs/modules/slam/FastSLAM1/FastSLAM1_12_0.png b/docs/modules/4_slam/FastSLAM1/FastSLAM1_12_0.png similarity index 100% rename from docs/modules/slam/FastSLAM1/FastSLAM1_12_0.png rename to docs/modules/4_slam/FastSLAM1/FastSLAM1_12_0.png diff --git a/docs/modules/slam/FastSLAM1/FastSLAM1_12_1.png b/docs/modules/4_slam/FastSLAM1/FastSLAM1_12_1.png similarity index 100% rename from docs/modules/slam/FastSLAM1/FastSLAM1_12_1.png rename to docs/modules/4_slam/FastSLAM1/FastSLAM1_12_1.png diff --git a/docs/modules/slam/FastSLAM1/FastSLAM1_1_0.png b/docs/modules/4_slam/FastSLAM1/FastSLAM1_1_0.png similarity index 100% rename from docs/modules/slam/FastSLAM1/FastSLAM1_1_0.png rename to docs/modules/4_slam/FastSLAM1/FastSLAM1_1_0.png diff --git a/docs/modules/slam/FastSLAM1/FastSLAM1_main.rst b/docs/modules/4_slam/FastSLAM1/FastSLAM1_main.rst similarity index 100% rename from docs/modules/slam/FastSLAM1/FastSLAM1_main.rst rename to docs/modules/4_slam/FastSLAM1/FastSLAM1_main.rst diff --git a/docs/modules/slam/FastSLAM2/FastSLAM2_main.rst b/docs/modules/4_slam/FastSLAM2/FastSLAM2_main.rst similarity index 100% rename from docs/modules/slam/FastSLAM2/FastSLAM2_main.rst rename to docs/modules/4_slam/FastSLAM2/FastSLAM2_main.rst diff --git a/docs/modules/slam/ekf_slam/ekf_slam_1_0.png b/docs/modules/4_slam/ekf_slam/ekf_slam_1_0.png similarity index 100% rename from docs/modules/slam/ekf_slam/ekf_slam_1_0.png rename to docs/modules/4_slam/ekf_slam/ekf_slam_1_0.png diff --git a/docs/modules/slam/ekf_slam/ekf_slam_main.rst b/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst similarity index 100% rename from docs/modules/slam/ekf_slam/ekf_slam_main.rst rename to docs/modules/4_slam/ekf_slam/ekf_slam_main.rst diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example.rst b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example.rst similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_SE2_example.rst rename to docs/modules/4_slam/graph_slam/graphSLAM_SE2_example.rst diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_13_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_13_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_13_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_13_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_15_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_15_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_15_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_15_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_16_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_16_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_16_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_16_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_4_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_4_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_4_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_4_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_8_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_8_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_8_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_8_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_9_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_9_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_9_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_SE2_example_files/graphSLAM_SE2_example_9_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_doc.rst b/docs/modules/4_slam/graph_slam/graphSLAM_doc.rst similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_doc.rst rename to docs/modules/4_slam/graph_slam/graphSLAM_doc.rst diff --git a/docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_1.png b/docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_1.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_1.png rename to docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_1.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_2.png b/docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_2.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_2.png rename to docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_11_2.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_2.png b/docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_2.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_2.png rename to docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_2_2.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_4_0.png b/docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_4_0.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_4_0.png rename to docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_4_0.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_9_1.png b/docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_9_1.png similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_9_1.png rename to docs/modules/4_slam/graph_slam/graphSLAM_doc_files/graphSLAM_doc_9_1.png diff --git a/docs/modules/slam/graph_slam/graphSLAM_formulation.rst b/docs/modules/4_slam/graph_slam/graphSLAM_formulation.rst similarity index 100% rename from docs/modules/slam/graph_slam/graphSLAM_formulation.rst rename to docs/modules/4_slam/graph_slam/graphSLAM_formulation.rst diff --git a/docs/modules/slam/graph_slam/graph_slam_main.rst b/docs/modules/4_slam/graph_slam/graph_slam_main.rst similarity index 100% rename from docs/modules/slam/graph_slam/graph_slam_main.rst rename to docs/modules/4_slam/graph_slam/graph_slam_main.rst diff --git a/docs/modules/slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst b/docs/modules/4_slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst similarity index 100% rename from docs/modules/slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst rename to docs/modules/4_slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst diff --git a/docs/modules/slam/slam_main.rst b/docs/modules/4_slam/slam_main.rst similarity index 100% rename from docs/modules/slam/slam_main.rst rename to docs/modules/4_slam/slam_main.rst diff --git a/docs/modules/path_planning/bezier_path/Figure_1.png b/docs/modules/5_path_planning/bezier_path/Figure_1.png similarity index 100% rename from docs/modules/path_planning/bezier_path/Figure_1.png rename to docs/modules/5_path_planning/bezier_path/Figure_1.png diff --git a/docs/modules/path_planning/bezier_path/Figure_2.png b/docs/modules/5_path_planning/bezier_path/Figure_2.png similarity index 100% rename from docs/modules/path_planning/bezier_path/Figure_2.png rename to docs/modules/5_path_planning/bezier_path/Figure_2.png diff --git a/docs/modules/path_planning/bezier_path/bezier_path_main.rst b/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst similarity index 100% rename from docs/modules/path_planning/bezier_path/bezier_path_main.rst rename to docs/modules/5_path_planning/bezier_path/bezier_path_main.rst diff --git a/docs/modules/path_planning/bspline_path/approx_and_curvature.png b/docs/modules/5_path_planning/bspline_path/approx_and_curvature.png similarity index 100% rename from docs/modules/path_planning/bspline_path/approx_and_curvature.png rename to docs/modules/5_path_planning/bspline_path/approx_and_curvature.png diff --git a/docs/modules/path_planning/bspline_path/approximation1.png b/docs/modules/5_path_planning/bspline_path/approximation1.png similarity index 100% rename from docs/modules/path_planning/bspline_path/approximation1.png rename to docs/modules/5_path_planning/bspline_path/approximation1.png diff --git a/docs/modules/path_planning/bspline_path/basis_functions.png b/docs/modules/5_path_planning/bspline_path/basis_functions.png similarity index 100% rename from docs/modules/path_planning/bspline_path/basis_functions.png rename to docs/modules/5_path_planning/bspline_path/basis_functions.png diff --git a/docs/modules/path_planning/bspline_path/bspline_path_main.rst b/docs/modules/5_path_planning/bspline_path/bspline_path_main.rst similarity index 100% rename from docs/modules/path_planning/bspline_path/bspline_path_main.rst rename to docs/modules/5_path_planning/bspline_path/bspline_path_main.rst diff --git a/docs/modules/path_planning/bspline_path/bspline_path_planning.png b/docs/modules/5_path_planning/bspline_path/bspline_path_planning.png similarity index 100% rename from docs/modules/path_planning/bspline_path/bspline_path_planning.png rename to docs/modules/5_path_planning/bspline_path/bspline_path_planning.png diff --git a/docs/modules/path_planning/bspline_path/interp_and_curvature.png b/docs/modules/5_path_planning/bspline_path/interp_and_curvature.png similarity index 100% rename from docs/modules/path_planning/bspline_path/interp_and_curvature.png rename to docs/modules/5_path_planning/bspline_path/interp_and_curvature.png diff --git a/docs/modules/path_planning/bspline_path/interpolation1.png b/docs/modules/5_path_planning/bspline_path/interpolation1.png similarity index 100% rename from docs/modules/path_planning/bspline_path/interpolation1.png rename to docs/modules/5_path_planning/bspline_path/interpolation1.png diff --git a/docs/modules/path_planning/bugplanner/bugplanner_main.rst b/docs/modules/5_path_planning/bugplanner/bugplanner_main.rst similarity index 100% rename from docs/modules/path_planning/bugplanner/bugplanner_main.rst rename to docs/modules/5_path_planning/bugplanner/bugplanner_main.rst diff --git a/docs/modules/path_planning/catmull_rom_spline/blending_functions.png b/docs/modules/5_path_planning/catmull_rom_spline/blending_functions.png similarity index 100% rename from docs/modules/path_planning/catmull_rom_spline/blending_functions.png rename to docs/modules/5_path_planning/catmull_rom_spline/blending_functions.png diff --git a/docs/modules/path_planning/catmull_rom_spline/catmull_rom_path_planning.png b/docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_path_planning.png similarity index 100% rename from docs/modules/path_planning/catmull_rom_spline/catmull_rom_path_planning.png rename to docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_path_planning.png diff --git a/docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst b/docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_spline_main.rst similarity index 100% rename from docs/modules/path_planning/catmull_rom_spline/catmull_rom_spline_main.rst rename to docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_spline_main.rst diff --git a/docs/modules/path_planning/closed_loop_rrt_star_car/Figure_1.png b/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_1.png similarity index 100% rename from docs/modules/path_planning/closed_loop_rrt_star_car/Figure_1.png rename to docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_1.png diff --git a/docs/modules/path_planning/closed_loop_rrt_star_car/Figure_3.png b/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_3.png similarity index 100% rename from docs/modules/path_planning/closed_loop_rrt_star_car/Figure_3.png rename to docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_3.png diff --git a/docs/modules/path_planning/closed_loop_rrt_star_car/Figure_4.png b/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_4.png similarity index 100% rename from docs/modules/path_planning/closed_loop_rrt_star_car/Figure_4.png rename to docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_4.png diff --git a/docs/modules/path_planning/closed_loop_rrt_star_car/Figure_5.png b/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_5.png similarity index 100% rename from docs/modules/path_planning/closed_loop_rrt_star_car/Figure_5.png rename to docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_5.png diff --git a/docs/modules/path_planning/clothoid_path/clothoid_path_main.rst b/docs/modules/5_path_planning/clothoid_path/clothoid_path_main.rst similarity index 100% rename from docs/modules/path_planning/clothoid_path/clothoid_path_main.rst rename to docs/modules/5_path_planning/clothoid_path/clothoid_path_main.rst diff --git a/docs/modules/path_planning/coverage_path/coverage_path_main.rst b/docs/modules/5_path_planning/coverage_path/coverage_path_main.rst similarity index 100% rename from docs/modules/path_planning/coverage_path/coverage_path_main.rst rename to docs/modules/5_path_planning/coverage_path/coverage_path_main.rst diff --git a/docs/modules/path_planning/cubic_spline/cubic_spline_1d.png b/docs/modules/5_path_planning/cubic_spline/cubic_spline_1d.png similarity index 100% rename from docs/modules/path_planning/cubic_spline/cubic_spline_1d.png rename to docs/modules/5_path_planning/cubic_spline/cubic_spline_1d.png diff --git a/docs/modules/path_planning/cubic_spline/cubic_spline_2d_curvature.png b/docs/modules/5_path_planning/cubic_spline/cubic_spline_2d_curvature.png similarity index 100% rename from docs/modules/path_planning/cubic_spline/cubic_spline_2d_curvature.png rename to docs/modules/5_path_planning/cubic_spline/cubic_spline_2d_curvature.png diff --git a/docs/modules/path_planning/cubic_spline/cubic_spline_2d_path.png b/docs/modules/5_path_planning/cubic_spline/cubic_spline_2d_path.png similarity index 100% rename from docs/modules/path_planning/cubic_spline/cubic_spline_2d_path.png rename to docs/modules/5_path_planning/cubic_spline/cubic_spline_2d_path.png diff --git a/docs/modules/path_planning/cubic_spline/cubic_spline_2d_yaw.png b/docs/modules/5_path_planning/cubic_spline/cubic_spline_2d_yaw.png similarity index 100% rename from docs/modules/path_planning/cubic_spline/cubic_spline_2d_yaw.png rename to docs/modules/5_path_planning/cubic_spline/cubic_spline_2d_yaw.png diff --git a/docs/modules/path_planning/cubic_spline/cubic_spline_main.rst b/docs/modules/5_path_planning/cubic_spline/cubic_spline_main.rst similarity index 100% rename from docs/modules/path_planning/cubic_spline/cubic_spline_main.rst rename to docs/modules/5_path_planning/cubic_spline/cubic_spline_main.rst diff --git a/docs/modules/path_planning/cubic_spline/spline.png b/docs/modules/5_path_planning/cubic_spline/spline.png similarity index 100% rename from docs/modules/path_planning/cubic_spline/spline.png rename to docs/modules/5_path_planning/cubic_spline/spline.png diff --git a/docs/modules/path_planning/cubic_spline/spline_continuity.png b/docs/modules/5_path_planning/cubic_spline/spline_continuity.png similarity index 100% rename from docs/modules/path_planning/cubic_spline/spline_continuity.png rename to docs/modules/5_path_planning/cubic_spline/spline_continuity.png diff --git a/docs/modules/path_planning/dubins_path/RLR.jpg b/docs/modules/5_path_planning/dubins_path/RLR.jpg similarity index 100% rename from docs/modules/path_planning/dubins_path/RLR.jpg rename to docs/modules/5_path_planning/dubins_path/RLR.jpg diff --git a/docs/modules/path_planning/dubins_path/RSR.jpg b/docs/modules/5_path_planning/dubins_path/RSR.jpg similarity index 100% rename from docs/modules/path_planning/dubins_path/RSR.jpg rename to docs/modules/5_path_planning/dubins_path/RSR.jpg diff --git a/docs/modules/path_planning/dubins_path/dubins_path.jpg b/docs/modules/5_path_planning/dubins_path/dubins_path.jpg similarity index 100% rename from docs/modules/path_planning/dubins_path/dubins_path.jpg rename to docs/modules/5_path_planning/dubins_path/dubins_path.jpg diff --git a/docs/modules/path_planning/dubins_path/dubins_path_main.rst b/docs/modules/5_path_planning/dubins_path/dubins_path_main.rst similarity index 100% rename from docs/modules/path_planning/dubins_path/dubins_path_main.rst rename to docs/modules/5_path_planning/dubins_path/dubins_path_main.rst diff --git a/docs/modules/path_planning/dynamic_window_approach/dynamic_window_approach_main.rst b/docs/modules/5_path_planning/dynamic_window_approach/dynamic_window_approach_main.rst similarity index 100% rename from docs/modules/path_planning/dynamic_window_approach/dynamic_window_approach_main.rst rename to docs/modules/5_path_planning/dynamic_window_approach/dynamic_window_approach_main.rst diff --git a/docs/modules/path_planning/eta3_spline/eta3_spline_main.rst b/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst similarity index 100% rename from docs/modules/path_planning/eta3_spline/eta3_spline_main.rst rename to docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst diff --git a/docs/modules/path_planning/frenet_frame_path/frenet_frame_path_main.rst b/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst similarity index 100% rename from docs/modules/path_planning/frenet_frame_path/frenet_frame_path_main.rst rename to docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst diff --git a/docs/modules/path_planning/grid_base_search/grid_base_search_main.rst b/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst similarity index 100% rename from docs/modules/path_planning/grid_base_search/grid_base_search_main.rst rename to docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst diff --git a/docs/modules/path_planning/hybridastar/hybridastar_main.rst b/docs/modules/5_path_planning/hybridastar/hybridastar_main.rst similarity index 100% rename from docs/modules/path_planning/hybridastar/hybridastar_main.rst rename to docs/modules/5_path_planning/hybridastar/hybridastar_main.rst diff --git a/docs/modules/path_planning/lqr_path/lqr_path_main.rst b/docs/modules/5_path_planning/lqr_path/lqr_path_main.rst similarity index 100% rename from docs/modules/path_planning/lqr_path/lqr_path_main.rst rename to docs/modules/5_path_planning/lqr_path/lqr_path_main.rst diff --git a/docs/modules/path_planning/model_predictive_trajectory_generator/lookup_table.png b/docs/modules/5_path_planning/model_predictive_trajectory_generator/lookup_table.png similarity index 100% rename from docs/modules/path_planning/model_predictive_trajectory_generator/lookup_table.png rename to docs/modules/5_path_planning/model_predictive_trajectory_generator/lookup_table.png diff --git a/docs/modules/path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst b/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst similarity index 100% rename from docs/modules/path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst rename to docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst diff --git a/docs/modules/path_planning/path_planning_main.rst b/docs/modules/5_path_planning/path_planning_main.rst similarity index 100% rename from docs/modules/path_planning/path_planning_main.rst rename to docs/modules/5_path_planning/path_planning_main.rst diff --git a/docs/modules/path_planning/prm_planner/prm_planner_main.rst b/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst similarity index 100% rename from docs/modules/path_planning/prm_planner/prm_planner_main.rst rename to docs/modules/5_path_planning/prm_planner/prm_planner_main.rst diff --git a/docs/modules/path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst b/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst similarity index 100% rename from docs/modules/path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst rename to docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst diff --git a/docs/modules/path_planning/reeds_shepp_path/LR_L.png b/docs/modules/5_path_planning/reeds_shepp_path/LR_L.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/LR_L.png rename to docs/modules/5_path_planning/reeds_shepp_path/LR_L.png diff --git a/docs/modules/path_planning/reeds_shepp_path/LR_LR.png b/docs/modules/5_path_planning/reeds_shepp_path/LR_LR.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/LR_LR.png rename to docs/modules/5_path_planning/reeds_shepp_path/LR_LR.png diff --git a/docs/modules/path_planning/reeds_shepp_path/LSL.png b/docs/modules/5_path_planning/reeds_shepp_path/LSL.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/LSL.png rename to docs/modules/5_path_planning/reeds_shepp_path/LSL.png diff --git a/docs/modules/path_planning/reeds_shepp_path/LSL90xR.png b/docs/modules/5_path_planning/reeds_shepp_path/LSL90xR.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/LSL90xR.png rename to docs/modules/5_path_planning/reeds_shepp_path/LSL90xR.png diff --git a/docs/modules/path_planning/reeds_shepp_path/LSR.png b/docs/modules/5_path_planning/reeds_shepp_path/LSR.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/LSR.png rename to docs/modules/5_path_planning/reeds_shepp_path/LSR.png diff --git a/docs/modules/path_planning/reeds_shepp_path/LSR90_L.png b/docs/modules/5_path_planning/reeds_shepp_path/LSR90_L.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/LSR90_L.png rename to docs/modules/5_path_planning/reeds_shepp_path/LSR90_L.png diff --git a/docs/modules/path_planning/reeds_shepp_path/L_R90SL.png b/docs/modules/5_path_planning/reeds_shepp_path/L_R90SL.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/L_R90SL.png rename to docs/modules/5_path_planning/reeds_shepp_path/L_R90SL.png diff --git a/docs/modules/path_planning/reeds_shepp_path/L_R90SL90_R.png b/docs/modules/5_path_planning/reeds_shepp_path/L_R90SL90_R.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/L_R90SL90_R.png rename to docs/modules/5_path_planning/reeds_shepp_path/L_R90SL90_R.png diff --git a/docs/modules/path_planning/reeds_shepp_path/L_R90SR.png b/docs/modules/5_path_planning/reeds_shepp_path/L_R90SR.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/L_R90SR.png rename to docs/modules/5_path_planning/reeds_shepp_path/L_R90SR.png diff --git a/docs/modules/path_planning/reeds_shepp_path/L_RL.png b/docs/modules/5_path_planning/reeds_shepp_path/L_RL.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/L_RL.png rename to docs/modules/5_path_planning/reeds_shepp_path/L_RL.png diff --git a/docs/modules/path_planning/reeds_shepp_path/L_RL_R.png b/docs/modules/5_path_planning/reeds_shepp_path/L_RL_R.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/L_RL_R.png rename to docs/modules/5_path_planning/reeds_shepp_path/L_RL_R.png diff --git a/docs/modules/path_planning/reeds_shepp_path/L_R_L.png b/docs/modules/5_path_planning/reeds_shepp_path/L_R_L.png similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/L_R_L.png rename to docs/modules/5_path_planning/reeds_shepp_path/L_R_L.png diff --git a/docs/modules/path_planning/reeds_shepp_path/reeds_shepp_path_main.rst b/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst similarity index 100% rename from docs/modules/path_planning/reeds_shepp_path/reeds_shepp_path_main.rst rename to docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst diff --git a/docs/modules/path_planning/rrt/figure_1.png b/docs/modules/5_path_planning/rrt/figure_1.png similarity index 100% rename from docs/modules/path_planning/rrt/figure_1.png rename to docs/modules/5_path_planning/rrt/figure_1.png diff --git a/docs/modules/path_planning/rrt/rrt_main.rst b/docs/modules/5_path_planning/rrt/rrt_main.rst similarity index 100% rename from docs/modules/path_planning/rrt/rrt_main.rst rename to docs/modules/5_path_planning/rrt/rrt_main.rst diff --git a/docs/modules/path_planning/rrt/rrt_star.rst b/docs/modules/5_path_planning/rrt/rrt_star.rst similarity index 100% rename from docs/modules/path_planning/rrt/rrt_star.rst rename to docs/modules/5_path_planning/rrt/rrt_star.rst diff --git a/docs/modules/path_planning/rrt/rrt_star/rrt_star_1_0.png b/docs/modules/5_path_planning/rrt/rrt_star/rrt_star_1_0.png similarity index 100% rename from docs/modules/path_planning/rrt/rrt_star/rrt_star_1_0.png rename to docs/modules/5_path_planning/rrt/rrt_star/rrt_star_1_0.png diff --git a/docs/modules/path_planning/rrt/rrt_star_reeds_shepp/figure_1.png b/docs/modules/5_path_planning/rrt/rrt_star_reeds_shepp/figure_1.png similarity index 100% rename from docs/modules/path_planning/rrt/rrt_star_reeds_shepp/figure_1.png rename to docs/modules/5_path_planning/rrt/rrt_star_reeds_shepp/figure_1.png diff --git a/docs/modules/path_planning/state_lattice_planner/Figure_1.png b/docs/modules/5_path_planning/state_lattice_planner/Figure_1.png similarity index 100% rename from docs/modules/path_planning/state_lattice_planner/Figure_1.png rename to docs/modules/5_path_planning/state_lattice_planner/Figure_1.png diff --git a/docs/modules/path_planning/state_lattice_planner/Figure_2.png b/docs/modules/5_path_planning/state_lattice_planner/Figure_2.png similarity index 100% rename from docs/modules/path_planning/state_lattice_planner/Figure_2.png rename to docs/modules/5_path_planning/state_lattice_planner/Figure_2.png diff --git a/docs/modules/path_planning/state_lattice_planner/Figure_3.png b/docs/modules/5_path_planning/state_lattice_planner/Figure_3.png similarity index 100% rename from docs/modules/path_planning/state_lattice_planner/Figure_3.png rename to docs/modules/5_path_planning/state_lattice_planner/Figure_3.png diff --git a/docs/modules/path_planning/state_lattice_planner/Figure_4.png b/docs/modules/5_path_planning/state_lattice_planner/Figure_4.png similarity index 100% rename from docs/modules/path_planning/state_lattice_planner/Figure_4.png rename to docs/modules/5_path_planning/state_lattice_planner/Figure_4.png diff --git a/docs/modules/path_planning/state_lattice_planner/Figure_5.png b/docs/modules/5_path_planning/state_lattice_planner/Figure_5.png similarity index 100% rename from docs/modules/path_planning/state_lattice_planner/Figure_5.png rename to docs/modules/5_path_planning/state_lattice_planner/Figure_5.png diff --git a/docs/modules/path_planning/state_lattice_planner/Figure_6.png b/docs/modules/5_path_planning/state_lattice_planner/Figure_6.png similarity index 100% rename from docs/modules/path_planning/state_lattice_planner/Figure_6.png rename to docs/modules/5_path_planning/state_lattice_planner/Figure_6.png diff --git a/docs/modules/path_planning/state_lattice_planner/state_lattice_planner_main.rst b/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst similarity index 100% rename from docs/modules/path_planning/state_lattice_planner/state_lattice_planner_main.rst rename to docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst diff --git a/docs/modules/path_planning/visibility_road_map_planner/step0.png b/docs/modules/5_path_planning/visibility_road_map_planner/step0.png similarity index 100% rename from docs/modules/path_planning/visibility_road_map_planner/step0.png rename to docs/modules/5_path_planning/visibility_road_map_planner/step0.png diff --git a/docs/modules/path_planning/visibility_road_map_planner/step1.png b/docs/modules/5_path_planning/visibility_road_map_planner/step1.png similarity index 100% rename from docs/modules/path_planning/visibility_road_map_planner/step1.png rename to docs/modules/5_path_planning/visibility_road_map_planner/step1.png diff --git a/docs/modules/path_planning/visibility_road_map_planner/step2.png b/docs/modules/5_path_planning/visibility_road_map_planner/step2.png similarity index 100% rename from docs/modules/path_planning/visibility_road_map_planner/step2.png rename to docs/modules/5_path_planning/visibility_road_map_planner/step2.png diff --git a/docs/modules/path_planning/visibility_road_map_planner/step3.png b/docs/modules/5_path_planning/visibility_road_map_planner/step3.png similarity index 100% rename from docs/modules/path_planning/visibility_road_map_planner/step3.png rename to docs/modules/5_path_planning/visibility_road_map_planner/step3.png diff --git a/docs/modules/path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst b/docs/modules/5_path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst similarity index 100% rename from docs/modules/path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst rename to docs/modules/5_path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst diff --git a/docs/modules/path_planning/vrm_planner/vrm_planner_main.rst b/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst similarity index 100% rename from docs/modules/path_planning/vrm_planner/vrm_planner_main.rst rename to docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst diff --git a/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_1_0.png b/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_1_0.png similarity index 100% rename from docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_1_0.png rename to docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_1_0.png diff --git a/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_2_0.png b/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_2_0.png similarity index 100% rename from docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_2_0.png rename to docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_2_0.png diff --git a/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_3_0.png b/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_3_0.png similarity index 100% rename from docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_3_0.png rename to docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_3_0.png diff --git a/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_4_0.png b/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_4_0.png similarity index 100% rename from docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_4_0.png rename to docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_4_0.png diff --git a/docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst b/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst similarity index 100% rename from docs/modules/path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst rename to docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst diff --git a/docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst similarity index 100% rename from docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst rename to docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst diff --git a/docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_steering_control_model.jpg b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_steering_control_model.jpg similarity index 100% rename from docs/modules/path_tracking/lqr_speed_and_steering_control/lqr_steering_control_model.jpg rename to docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_steering_control_model.jpg diff --git a/docs/modules/path_tracking/lqr_speed_and_steering_control/speed.png b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/speed.png similarity index 100% rename from docs/modules/path_tracking/lqr_speed_and_steering_control/speed.png rename to docs/modules/6_path_tracking/lqr_speed_and_steering_control/speed.png diff --git a/docs/modules/path_tracking/lqr_speed_and_steering_control/x-y.png b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/x-y.png similarity index 100% rename from docs/modules/path_tracking/lqr_speed_and_steering_control/x-y.png rename to docs/modules/6_path_tracking/lqr_speed_and_steering_control/x-y.png diff --git a/docs/modules/path_tracking/lqr_speed_and_steering_control/yaw.png b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/yaw.png similarity index 100% rename from docs/modules/path_tracking/lqr_speed_and_steering_control/yaw.png rename to docs/modules/6_path_tracking/lqr_speed_and_steering_control/yaw.png diff --git a/docs/modules/path_tracking/lqr_steering_control/lqr_steering_control_main.rst b/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst similarity index 100% rename from docs/modules/path_tracking/lqr_steering_control/lqr_steering_control_main.rst rename to docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst diff --git a/docs/modules/path_tracking/lqr_steering_control/lqr_steering_control_model.jpg b/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_model.jpg similarity index 100% rename from docs/modules/path_tracking/lqr_steering_control/lqr_steering_control_model.jpg rename to docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_model.jpg diff --git a/docs/modules/path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst b/docs/modules/6_path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst similarity index 100% rename from docs/modules/path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst rename to docs/modules/6_path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst diff --git a/docs/modules/path_tracking/path_tracking_main.rst b/docs/modules/6_path_tracking/path_tracking_main.rst similarity index 100% rename from docs/modules/path_tracking/path_tracking_main.rst rename to docs/modules/6_path_tracking/path_tracking_main.rst diff --git a/docs/modules/path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst b/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst similarity index 100% rename from docs/modules/path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst rename to docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst diff --git a/docs/modules/path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst b/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst similarity index 100% rename from docs/modules/path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst rename to docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst diff --git a/docs/modules/path_tracking/stanley_control/stanley_control_main.rst b/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst similarity index 100% rename from docs/modules/path_tracking/stanley_control/stanley_control_main.rst rename to docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst diff --git a/docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_12_0.png b/docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_12_0.png similarity index 100% rename from docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_12_0.png rename to docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_12_0.png diff --git a/docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_5_0.png b/docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_5_0.png similarity index 100% rename from docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_5_0.png rename to docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_5_0.png diff --git a/docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_7_0.png b/docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_7_0.png similarity index 100% rename from docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_7_0.png rename to docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_7_0.png diff --git a/docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_9_0.png b/docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_9_0.png similarity index 100% rename from docs/modules/arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_9_0.png rename to docs/modules/7_arm_navigation/Planar_Two_Link_IK_files/Planar_Two_Link_IK_9_0.png diff --git a/docs/modules/arm_navigation/arm_navigation_main.rst b/docs/modules/7_arm_navigation/arm_navigation_main.rst similarity index 100% rename from docs/modules/arm_navigation/arm_navigation_main.rst rename to docs/modules/7_arm_navigation/arm_navigation_main.rst diff --git a/docs/modules/arm_navigation/n_joint_arm_to_point_control_main.rst b/docs/modules/7_arm_navigation/n_joint_arm_to_point_control_main.rst similarity index 100% rename from docs/modules/arm_navigation/n_joint_arm_to_point_control_main.rst rename to docs/modules/7_arm_navigation/n_joint_arm_to_point_control_main.rst diff --git a/docs/modules/arm_navigation/obstacle_avoidance_arm_navigation_main.rst b/docs/modules/7_arm_navigation/obstacle_avoidance_arm_navigation_main.rst similarity index 100% rename from docs/modules/arm_navigation/obstacle_avoidance_arm_navigation_main.rst rename to docs/modules/7_arm_navigation/obstacle_avoidance_arm_navigation_main.rst diff --git a/docs/modules/arm_navigation/planar_two_link_ik_main.rst b/docs/modules/7_arm_navigation/planar_two_link_ik_main.rst similarity index 100% rename from docs/modules/arm_navigation/planar_two_link_ik_main.rst rename to docs/modules/7_arm_navigation/planar_two_link_ik_main.rst diff --git a/docs/modules/aerial_navigation/aerial_navigation_main.rst b/docs/modules/8_aerial_navigation/aerial_navigation_main.rst similarity index 100% rename from docs/modules/aerial_navigation/aerial_navigation_main.rst rename to docs/modules/8_aerial_navigation/aerial_navigation_main.rst diff --git a/docs/modules/aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst b/docs/modules/8_aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst similarity index 100% rename from docs/modules/aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst rename to docs/modules/8_aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst diff --git a/docs/modules/aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst b/docs/modules/8_aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst similarity index 100% rename from docs/modules/aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst rename to docs/modules/8_aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst diff --git a/docs/modules/bipedal/bipedal_main.rst b/docs/modules/9_bipedal/bipedal_main.rst similarity index 100% rename from docs/modules/bipedal/bipedal_main.rst rename to docs/modules/9_bipedal/bipedal_main.rst diff --git a/docs/modules/bipedal/bipedal_planner/bipedal_planner_main.rst b/docs/modules/9_bipedal/bipedal_planner/bipedal_planner_main.rst similarity index 100% rename from docs/modules/bipedal/bipedal_planner/bipedal_planner_main.rst rename to docs/modules/9_bipedal/bipedal_planner/bipedal_planner_main.rst diff --git a/docs/modules/introduction_main.rst b/docs/modules/introduction_main.rst deleted file mode 100644 index f9fb487297..0000000000 --- a/docs/modules/introduction_main.rst +++ /dev/null @@ -1,42 +0,0 @@ - -Introduction -============ - -TBD - -Definition Of Robotics ----------------------- - -TBD - -History Of Robotics ----------------------- - -TBD - -Application Of Robotics ------------------------- - -TBD - -Software for Robotics ----------------------- - -TBD - -Software for Robotics ----------------------- - -TBD - -Python for Robotics ----------------------- - -TBD - -Learning Robotics Algorithms ----------------------------- - -TBD - - diff --git a/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/Graph_SLAM_optimization.gif b/docs/modules/slam/graph_slam/graphSLAM_SE2_example_files/Graph_SLAM_optimization.gif deleted file mode 100644 index 2fabeaafc9cd4ad4b71bbf451637bf2560f1be5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 114966 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
9c|O;e7FnVD_n2A9rMrVj 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≻MieUHv{SR=GG|% zu~~87I5@lOO!;PoA$Tgg$7Xb@L9U{)b;6T+Istx^PxIC(a^ZwkGoXD-d*cmI%?uef zlf_#*eM%jAXl^1{8+!y5&ayu@&IWgx4SwG}^!;Jhj8|fln3}_v+f9_?Wt;;0dOLtS zF(}Bz1en+nNaMrL?Yj~hv#}hsUV$V^p!QMlUOhOx%u$yg)|+{79GV=el1 zZuaZF$eFOoa<~<6>_=}wtO!maHh%gW<4RixZsZ5>!@P9(Tb&g zOW@Q8_FfeUG@9;XyL!T?8l|gpkB+Xyh)>yZWLW_@X~kWl3u2%NVmeo0Q!|OOyj3z` zqgd@YT;=E}3R?@4qrRCK?4zp)WwbDR*tQ{va4EFUx|Dci6DmRofX0h&ZXTHa5EAVx zoeZFXj?l0oX#@&^?mGngMitNV3y$ zq4SX*G5wb47W5lb#D)^b_35x0v6C?!v{~3Tc;fM^4nUC}b4!qy9|M`4bV7{_X99+=Y?0SRQG-#r*D`E%T+E0F^=y!Z0>%pk zS~+~}Fn{QYEfD+@5<-U_KMvO7g9|s9I3f|rsDsV6@(0CEkJ88XY`_fZ&4oJNwih1t zGDpdUgK8=r9K@-d9@@XrlFhXz_A5vf=pD0V`wmK?rB*t=)9ent4vjf=jm|zJ6 zU<7ocxnNN*C122hD=@9PeWaR>Zz-pyV$u20QM8;me}3Gn_Y%^PlQn#~cyR>=1p=|l zI=?sVr9syd7;-su`FhD73grq$I+!kooD?oCd_f~bTiy&VkR~B(jnDx6kz>E`} z*T!PhA|4J=SSu}G^wWh>`JL5^&E687<+iJ(~-5>acfd zA(C_VKSQrQ0M3-Ynz)jz9txlIaXQFI4WjRV8i7(Qd>b_x)E|QErXx<$5obhTZ%N}0 zKDglP)Eys`Hxjx_czGuv^Hb<6SuWyMs`#rmaGxpAK)BR^54&*Au?&T*BExL);LH)e zuUL+T3cHpAdVCaqneteG9zu~{U%VOLdJLtf1qEz?&4fUH?t6UQ0{u_jJ4L4K;rgeu zO6c*ez-E4~pT+(NK+?K;eES`${)dal%v}yBOI&qfp!M+`~thG@U~h_>`dEJRjQnodQ5H=Nh9JP!QKTeLgQR&_#E@>8xwqUr#%fs|{y zI~%-Sp!Tc-)zj+JByyXQpKHhvGRHy@1HQO>|Btf1EWCA(U(Y)A#*4QI;}gKGQmG?S zF6%^=uTn)f1z8vkFDdj5R{r#9Kf2~8Xa|28>(XLD{tH6~#WCRLr7K5o%UgJ4j^s~F3`^vS5C^9re6q6#?n)xX4ZrAjI7brD&;D+_TP(1*zf^&`s z@%%8FoZ!|kdEo7ErENV@coHKum?F115%qEILD-kMPE}|q|I1+p-17TES|Tt&^eyD~ zYDm!%UK^MxMPW<=0ILZygbZrnHIdjBQfV11B=Fn;eahSALbY`<}bM*-PWH98I-lhs;6_yv{;%)r&uRubfmpWwR2x zceFWVXWwVGfzo|=ppy6Ayw72prpkAZ=*UKUF6D&8eXfTR^;L}Hr|7NRN!8%m6r=?+ zc*)Nw+NM}!i+T7qe)RN)I)$OpFoc4$o!!W{JW7>O`t8*9tsVm}<aE0moZc;@W3|(8{>f^eE8Cd|jAo5~IrxF?`o*E}uocQcZ$u3d1TTGfrITY-o z-*)4ayfW5L%jatBlP95uN3qz=G_38W^Ig9M7lYdLNwqmo@Fx^b1TEweuEHn^0rWHj z5F*c4BAiZ|Fe|lcW`Lk<-L-|12zsVZ4$ED**&TgxYP?Q9j@7_t8d4WHEGlKN2OZma zU{rI%A#PK>+~@oeCP7rEbLa=4O?lwyp30K%o9Vp9_CpX!|Ttj?{(-Nz52!np5ELUxGjjO$Q|B$(BO6# zfgcA~JLx_WSa7did1~Zf3>@XwF(VTkm39K|R~k;<4{=qjoem0dJHC{Q|9EvAqvp&w z6vUqCsV8nXP*jeL&L^hyzRy3m)@~!iZQ=eyI0^$bPXrC~C{(#D%ObmLf0pjFKKgaKlC()L3$5nUQ89 zA`U1vefWt52>7c${3Khx{z{AUq^CU#ZD^cPu4~`(Y8DfNfId z6IAy&H9BtHf~+-e?TJ^gx<`Il;%E0}QPmkyvgo5k5iyMr2n}}rXbok-!_*M7v}+V_ z;d!98$?UvpfKr89p{fJB7R>hKf6r#Er8@mn;A-qu6d=29m)b~(yA&+jg|2zMcQji~ z&BM)Pl74_Qfy5;3Y#zj}XK|o4;!&kVpVQ0DSJ^Cqct5lSh;Sy?KkzVB+7P-Ec6U1} ze|}J{gTXZ-4M4PJfr`<~B?{&0*Udx&NZnjlMMS6Et8cQY0BJ~vM)hH-3z5Y0=Z!nlKXVeQr zp+emOzoGN4vRJSUt<~RJ>o$K7>E~n}Wrx>7J3j|dT|)?_Rp z!qgxYCo~N*NUIRFF)5UpJ_8$2^Uqq)w<^zYWk~q>VQE-B-SH=(yN#J2$MmZM?VFF1 zR}EPzGeOBZJmt6cL9VL=5L!T%IS1~?)B_QfGzE;SRPu{BiiQHjCKk0Y5DJeWdN1$& z-h-JrArFC$>v|mC+pzm!JY1Xn)MG0UYE3GzY^RL?Lo1QtDpIwJcz|E-!$53>3H<5f zZt?%f~NKG*Et*beM^=>q2iuH&NbW-($TmSt=F$U=E z-B;d`vM z1@CDk2UX)JH?@%&wOvEj3_xs3AijU>0i_>q%haW3TLAmo}w1#aCjFK zIoROx!N_8aCTHHkGV;za3DpYS^><5K`v+)_A%qFNAh311z7iQBbtO#xTs;SL+**_* zeYmQ6mnG7FpKyQd48{whA`qi_oS;0dTUB+UF){~&nf6^~3 zMl(~-&OQ5TSqV8W%syWA==Qd`(5r8DMfb*! zl?UdUNFgM$BTQu%O@%`XIesa_pzUTXfkni8Q#uU`91$i_vYWvojx;%ZV&hQ z{<(gE(Kybc30J>p{N0lG?gxL!bLZKj-y!a4&8o}mPOC5GNdeN2#o-aj%0`kQ>D`A6YvI#DgJ_ucrl(zOu~5pwI&i32aRm29Ka`D-KOC2VFpYQA5-T2}^Hh2l(Vh|V`nQY>P&_8r!bK1>+ ztoNcdGgPl!;ra7z0!U6gU=NDakT0z&5N~6P`kd_{KKWgL>duqHV50MiGz(p$nUpIKn3c-YwYpl^w>SLUr z#03b<<(7LsNlKfd&m&8!3O<*8g#te`jYOv0ywP7z88w)Hbq9BKBP}f_DLaIWdNU67=YfoqE}nL{+)xSC zcLemrZ$4l>a+>F|tHPyn4LCb#+ZU&0Do`{_E;~Eh9Q(z6TV|twByy7u!l4HoB7r$C zgK9NhW`=UXf{Ur`7h`%ZMhm$yR4%XwI?-u%r&T>PiK!$4C^7&@JX5m{?)s#rFH_k< z#5z`{!=kWWKZcMcK;!imSsa+b!e7|+zSMnI#j{SZF^;8L3^rJ1;T5W1yXMZh!lhSo zA57O*)0pPXxy%yA?ow{WUa(%i60rwlNHb)KwFnu?_EuviaP}DmP25+wk9fq+zWkCD z=Q9rJ&fW! z?*l`FjVQI~{&7QHSfsJJ9MFej)dIuhgTX zAXa!%cT%%Rr8uDspu}f)&mg_=Tt67ZF3GvzAhVvYikM|%ngO{7Z?Ee0CAQ<;B(|Dm z@b#O;E@*dlBHSt`B)d_L_(gHIyHm z3Nqk<&qgcWPSby9Wjz~n+KQn$(hLnCu{$$(COB~KSd~Ai?KUwmMxRHqx;Bk%c?(f{ zDR67YLjz^4TomV1@fNn2u_+lOiyfK7Y46eqcxU5gz|iz&K;bp05-+xY4+K%nvKCed zL>_D$Wf*%mUzS2dfr6LWAq1#14!WwF=ROAU>w&nC$~KEH{P|guBe69g+w`^o>;y6v z4*bMdV1%prrbl`CyO_menBg?6#1QIOffWvJD&g#{hex28_siB11@(wZF~I-veOB%n zbvS>cF3%3dwi1B<8e>z*s7vH377yqY%HKT8x?hF%rGOk5)0@62IlTd>Ewgf)p-LTG zzh)4RPiblUOTga_7X$9b6es>>5y!fn=svABf!+XU-sG)zGoCtyCGFQkzv;s<{2USs zI^6{M8BhZ*$nfx#>pz1Pd>8!{nK}yUQ){wC$U45)S*CcjV6e$}PAZm5ZRVC~RRw#w z>6|Wvqh$CAhk|42CFO=)2b_&m*tTXK^sO+2q3Qf{ntU2$&G%Y09XQLs_v82N_(@D8 z%4#bPY$^Z+<}vKDct>?*?Pk^wJ>GyGPi>vTiZRDomXd??M}8FKQl*e=0ccu>oO+^w zp$Bs!vJLi4Wz|e=cR9WGHMiwXH)R&$-@~z#@e_FP-Ggovzq~{Uc9zE`I zD#$kw75YkQ_U)Sen+QW?u%E#qvRY>Ir9FPb)Y@4VRSNX1_x3b{8AxoIdR;6O-#CLU z`(QV`89sLyF>1-P7~_PUDHnFZI=H!~aWX6uWSFaImI=OZD$o=H&Wiy~Jz$41I~N#) zodh-^`JFfp^F2{?ZtBVC7zb>t)oBo2!GjtTh4oT~5e-aX$lyG%8sPleZ=8d1A(b*4 zJH+*&HVhmtFeLu9ep6d>*r1XsVX*`Ntbm~eC{?2abhTTQ9l9*2didE}(VHAxh)J6)?o|mu5dQTw4Ip#_bqg_F3Edbm8gsPnl1+ zhx6W!z+wc?3&>!??2b_e^JvhyMT5#Q5lM48xC`gmglnyR+xVL)v}|U5@5&1%#5xq* zOlvs1DmEfTvJ7OzCKY4?m=o}2@2Bl0k9Vb(HGq-UuJYPt?a6Q55D*YC zQ~^;#Q@{o&f;}oK)*B0ow~8nC^Souhdp~=hebzer%UNfweB={rW|En?e%Ie#HAoD% zWPxU6#1}&o|8ig%!;lLU#O@Q^ii24?aDBz*35MZOBPSP*Gwg2)tMWJDzA8sIw4(sX z<|x7tshVHLtGrLZlmW^DWx`z1oRp=O&88X&6a^ehG0fo}S6=xfNjblENNGt4O}(t= z`{NN`>D~^!0R1Q5Iv{*fq;ubG_|@M3FEDT( zrC$Wmu$y``z4Z294nA7B=CRblg?(U`%H{o_xkysDNqfsF9PeC*j48C@S_}tNXS0kC&4#uTP;g z5Pr=5z>z>j?bG=Vd;^Er{aLo*4sfgH6(_72t?=YO%MjWUZXxlWLpm~s%UwN=NWZ%_ z&bxGF2H%x+?jo_qp%=CcfZ<63Z7JN1$or5KX##gg0tL+lfZ7OXAy8HaRLm1Nb~7AH z)-pXQtD*Mp>U_R!IPD>;gB4;F|L< z3c8h4`(mO<-0B8aY^%5EXr;QK7==w)@<(s=-$FhjgSFjx!EA#3=5OZIV*JZY9bTsT z)C=hv9QkgjgA)1*a@vsbOodExtV+G*efg$)10*l_^+WwPT3EV*ke*S%{rHM~Nw13i zQ#n$W_RMitBu9_T52xtSkf1(E{x&C8ey_&iY#FBTBX@2e7QH?{$}`VKntO5iLXNKt ztD-z=_U3J)7l<8UryPwm1K#b)+g>i=+w-23jTax4@)zpBxk_Q1lA6ZDv@`oJTgk7x zM_CjNUfvaaeCdkQhlf$DRWRfHZNyiE$b zlfKI25_%>a^c8t*=?ga?!p!btnvD`1rv>TTP}3dT)Q_>07&RS)cN}&vhnB3-x@rIQ z(|;TW>>EIH)@ogRnq4=z+AA^i8Nxy}nyR z?^Y%idAa%H8uBo3|HU;g_;OHSLknvL2YH4$v)7=r6$Z;Y~&ma|xdc|3}oq-UwI*Z*3s zdiTD)?5{{s)KRYE41B#;BnJy~E`zT-%%Mnn4k7u7c&^>-$~W_ZXdmwH_~SeuU}WrU zw8$c}&*BbqK~RA+t3s`x$HWFz{YcC(UA!0VrLuwe5!egH-bN-(DsaZDrbVj;8t59*OIt4-4RPD3CtRR zb46@ZMwXGr)~v(RX1duEk4roX`2s%2n(@;+xnOJ{vn$g)Vkl>?&+VlHllJ4Fe>Qm1 zVnyLP!muZ$Km_l4aK3gX0X>&z)38JSO}3f@4b}T>Ojez)1*kG+4QZ#Pf5eeF=^PVA z>sZUlk{|O2W|Ne>NUT$St3pl=(^BWl(YL2A)yfy`07z0rCT^#liz{S+SJNuA z9Tnihdg4<8i-v=(qAPnz6Pcr-F5Y<5%S@x9#L;4e$!kjHWC)*6bxj#-rMc!B(Zm)# zJ=ozQ=fovN{#i}IebkNx?3uYnk@bfSAm&jrYOaY;aBLbsMp)Fgc z+g^;lO6C_v#{|xsHd;RJGsNlC5^c9>k^HT3B|C2mo^%Jn-Edrtdc7&f&V4Fpsy4bT zJ#cs|_{(5LO^xT1pM_^`mzww>r`q0WrQV<3lW?u&?Cv`*sNxx|T=QxB^yuI|?F>E7 zI!#ijSZun7EI)KFC)x4a8r3RDw3eM<%U#vOmAmbpR8ddZ?nSB93UKtWfnuB1zH9aV zE5H8w8NSXrBk=jR(AoWx$^&hhH9Xz@qx@?cfdw#k%^vNVA=938*um&y?-KbYNlZ@# z3n)HGgdh~yj{18Mh7?uOc;{(;ue}+A8ZlK88e++Zx!GOMOxRtze?twSR`ks%9##Mx z4(HJb*l@$B(_f5qeY(SO?wJW`pAajHonHA^UUP}d)u2%`JW^?)d;xyrNRURs?iXM_ zf!_Ce5Lz!FSp?sun!~0VDGXva?O5c=&nlh`WV&?NgMTF`9YR z-^2;WSLgePhXT$VSo^weLuizF>6;chu`+CC5yJMvDSv( zt&*KiEQ&xx~Xx|o#TiYrO0DK zeV^th6Xd9cyRLe(?&_`^2bv=gu>q+eCJ&q^IJ6I(2HOzB0Fk)5_UGD_`-`4^8q*%# zo8jTV#?(63xRVkcHTmtuvMzd7T9suf<6CHdK0ZCN)R6P6)gUB zDw#mg_aJQiX~9+xn%~*FH793t?{mYojmL&D-ZwJd5{9Z}p_>SiP0xdIHvPBcJK1|g zgU_DbbG3RmyC+a%`k>{Teb<_o!cc1p0sA|6me>X5o^Bqpa?Ml8#peeemdHoh4n{Vc zcMoxznc1sPu8TVRwtgB~6GQA~x+|8gFq#i@f9Cq-o8|H0IpeV$|IZI!ZD==)L70t1|Zh!#V==<3>w-5u4-7khdK9*Yw(qfSYIBivblZ8 zBD2u^Q35imIlN!&zR%b;=k;}F&<;5ZGPW1)xe-EvW&{Ga;DtF3WxPFmQIn z9Txu70W2$7+TIlI?xQ0Lba!ASc>2E%SZ8{>b>3DTN#j$`pzTA53=7Q{4}iR}^P#mR zK>G+}nK@?NhBs`gqDuWV=aC!>Db6Q&C`^qQn&%h~L+s&&FM0q+->4*~-Ed!n!UKb@ zcIR^34M3@W#D$mfO+6y>7}Na55uYyLFsTXt6Wx9Z&%?2y2N5ZOK(pCM(1yZxyb3M5QPaHnI6pF}GN@?&YvIweDOTxuksJ$G7~``k zjgRh%dZ@2{o@qLx{1!N2Q`(AfOtotA_4ZT(0C3(i;yP##v5a7Id`-hK{KLCGS(lv% z!7KnE10I5s+9PL6!TakNf2m0?A@`Gc1{P0I!QBIDm&m!Ro1ZS*(LJcUL>9PNJi`@t z4;e27QhYqMHUYVlP>2e~Jnwt6_m8pM4gfxHS@xNB+d{V^!w@PRfT=O>( zQYHNznj)FWLsN1h^vTrj@6Z(Z@1d#xfe6V%Q)Sq!ntu=>iB9STBW9af)~Jg2lv&Vw zd(Uh3De}tHxBnnQ^r#P1x||5z@ymMd&y*{n&(X{u|0quV?-L{}I3@r54Tbjq_e!Xy zrl!5U{lbL{zjJf{nVXU;p$nWV|5QTq+*D=d@7mhm|5QSKeZPkau3Rhnn+Q!+{hgbd zJ#$4)gxp~1m_O@#ix+M1I4pk4pxd{CQBI^&;-san48F|OA?d=NwK zgN~@JW$m1#{HG5pKmrIPMaHm%#i;bI#TNFn00qNk`w{{9tydv(KBf*+G}T>p?CwS1 z!uDbKVS;_U7(2~AzdXY+z0NlXxLayuo$Zu935<0xUk?~jcE5b$f$|FiDBBHUI@IJ) zD0G_T0ReOIJG+6XXIG9_Zrw$sz=pw-OX^4oah@`c#<8Z{#jFpZu*dr3sEM^MinX^6 zS~ci)|E{i_IGqnuo_m&;{;p?Q)v(tc&)QZsL;OX%J169A}9DR%)S z`l6I-RZ5(p-q$rWFb5?G15n!3YkasPrIG^Jy0U;}h`UnwZ`JR?EU;ByN-1!%XB|ZB zKmCZZ<7`_k1w)v7xOe&N8mxKe+%x211dLS?w#nceLa{=WZih0$o-)rlHbfx@_v7|vSHdOQHr4pZO#xHY6DtoLp_ zAjbW$ql7ghWcR2oa|^HbDigx0j`rFD@uJe0-1(bMIh|W!M@ZvueYP#6eYv@uDBn`= zvr_JZj7&%Gvk$p$U6=~r{O>+U8^w_R4?gIs<+q1Br?-B4l>O@L`K9||O?0~%mfON^ zPdI@lm4SM?i_g4GTPA>Sy0Z??OP1R?dLXFV#$S}~eys8;>yY!-XD5zr+vC+c5hNAF zA9iNFmi$Jn^(fE|8-LgO>I41*{?~=5_)2y60fZX8CTF_Ob2jg(nNL6oN`<TnKU&` z$5MYe!@zIv)~Ngn8Dg33;Ungzi&v(d%R%r^poxu3iDcv6k~tyNwV8d=P;akccr;{S z<62B2?^Oe20l6m5$WOD}NsOM%XA!GaL{+u)g5l}i#ozrYxl7JC*mrRAU@pPh@LU7s zOLn)4uUI5p%Z~fvK^a@PEPgWSvTqnR4K%2356$@dZSK^o$Sa2Zfxyt^cE321g z)gr|SI?MNTIv5WXYnwhtSo9P-ryVJ_^M(_gupScjQalsdj?mrV6a(+oacsSATrEZjVi_X|3xKAMM8Juik4< zVr!svtQt#)I0ZAH1ig8VXUQnS82e?Khy@&D(o;_#iyFwI`}9QkTCt_oIOr&)U=#H) zL9(ZWb_w5-QYc&{?=%E9fLcy$w9ov!YO*CVvb0weB$+I5>H$@ET6@MB51iPPU>0ly zI)92`p(^}bFw&-LC>qcl$O3gtC494Jm{PtGPs^qs*#BKUw+#}(r&h>^%lH*#_9%-iT8Gz61{dT6JKv#wE_Ud$t7-E&4KqQjkpXy|1 zrq3Fz{7ano;*x9o&POkxB2k2oND3M$YRllt zp{^xrQl7CAj*}GNg6Usq5yY9Z&-)w!UzbYdWAuCrNj1t;^jM`y#-IPxhAkf-GG1#f za1?WngZ8`~YuBHy@fi>+G!8Ai z2nXJ7VY?thYKVmhm@(p-?)uI?q`#NZPN|}8vDzhsFiOrNmigK5-=&_h!wOscgr0S#3tH;mYB%5 zVSrfm!@7CDG}Hx@6ZS4 zIk7JOV#H?NYqUiQ=hH5oP}yv%|LH)j(y;H zM6$C)${2lnHRe?4;^xP(Xxno5I++V0l*Q5g;5%E#sr?9MAZBxxT8gJXXXf-9yBSle zqj@`39;p`cwQBZ5)HLkp1X|j_ZC{v{^y{TW554bx*OOOll%DU|9p3gLU4K`FhsFI>aIou^NoEzNfSl@F{0s4fybzijQrZd*~Na$17OqM#OPz3f;%O%z2M85y*jFAs`TvY^~h@@}MfW*O$KuntZnLX{fM zcEFZkGRDi0v0p0m6~(GbkagG{)+W1h2w4x{u#NM(t%%MhtgOy@u;;Ybix6bqd~keg zOlF6(8>`^iEN93Kwc%K7uqU={T;VJoe_n!okp%OZ-xwt2nBcNZcVbS?XH2MUjwa^U z&_WlUA54Y>IikH;q(4*vC`5#^pN}+47g$P!DyGiv0Di5!sY9m(ci|Y+^$8}hJ>!b` z9*HamM2>y#ix3V7LkEeV)UqYK+dMT1OOuEwV5pxO16DhZ+QMS|(x8Q675X}mCjhj3 zkae##v6qM`mqCMzA>v%9)oc9ah`WKSxufF`!z-qXyL!3>{St(DF>yJDTpQmY`B73aPSG*6#3N z8nlU+AH_K6GXt6toh@jJr$iWmI^@3?87G0v?)t{PMtxLF{e!Bq`5rEa>Lk&uRFNvc zkkKZ|P9YI6|Maa2)o2VIwlIuOF+>1KI~;LLVIu4_5hD>RP6jJ7X$l##vLh&nEQL8r zHrFkQ6=0?1=U%{QtG4sEN<#BcU`%B;>bA-fX@b_ zYQ{loIu7Rz^ zdNdI8?aFFQV2b8lr>L!=R8?nJiGr8I=0H(Ch2*?pEmk5{Xi}ufdq+!LN%$rj^dY(Y zqMGwg+KD)Hy}dQs6f6G$pV7udUH*=4l^$+XRjC(yH#w+HBA_rDIw%aylPUCF5u96q zO_f3!SOuXL?WlK}HXqt7MZG0tVS1|Ziw&gKFic@3ua4O(hHCl9KjZMO{JUI+4Li?m7MHb3?QK2_y06$Cpl>8VaspF_UEbRb+4pBRB=Ct_Zba ztT8w=pLh?=lC!KcAJega_cQ#z8}+h!Bm^!hujPmDFdxd5#usk@agqxD0zFfewnp+;yK)D9?lNOXI<;b`AT0W zfqBz9IgA-C=tBB6_B1{j>Okg zqPTS3D6fZHMhDQkD+r2quoZBveJ{6TP+3ofgp8J7y7Bj)Mv zp|D;tdN&2xq%KsWpiZLD_}Wp6IK}OqB1+?B3$xq+i`*T8ZHpS{DiMk)xiu)iBn!zm zy-%m*BSlH!aZ*(6`CQwp0}IVpOwV}P0#Fhl!Q6rG+=pW>RNUiLrOqu~oP^)3&+K}P z;#XCqe68F1E$aH$W_$_{jGd3Lk=`QrD{7C!EPl20%91q6cP#x;jp;p;28zKJ*c2>O zE-louTpu3_+a!Uy=qj()^eb9#h&@C;=Lj>E_&V8^F~?7Y=&01CC;QH~Z=tGeqtUT0 z?WBjRl?|Qmt#Ec0g(8v_468;{Zv@v|36IXdV_JxbZKB6GPi8z#B2w0C8q?U8GlfG} zDuxgbzamT+iuN#!(7gzofe9ibogS_}M^W^XOphIeO1IwAehS1J7?mcgsMBG-08<>P zQi&DgKJFNLXty5`MPv=rAK{c4>rR%a#40HEiag?u<8_uCq$s@|^5^K2J5UaBZk=#X z$3l;7V$9&9#?`pnsxnsyGrfDOZK@G^_LThjH2lgI75iC$KL}Ku+IZd4*$+roR)3`M zKsdpLZ($*Gz9!sj!r0xF9(GpiA)=>-|5!yv_w9QqPbOVYa8PKyyk`t#KI12 z>3Yx(PxC-5YrzB&z%l^cmX7w6CPii;a%hTsl%gILqyIdO+`>}0J?df>YnfV#X_cVj zXfgU`e~4Zq&WB!7lN|Lwv0>%Y6^F#DAMRFJ`l{}1&qx4taVb5LW;0kvee7d*cehZq z?Nr^835Z<%@Iw8GiFSoBnnH>gnN36U)S%^>&Yq(C{*0mI?>IEW0VR3@&zQwLMVNO% z8!1O;FXDQ}Z_AG5kjUBRhT+T>RHC?*PE)9)pi4y4Dzuls18;6ghuS+8!dVJ=^LI@9 z2G-Mau>p@PR_{+frnzM0Pn9TS>Wi;|zu?arR>Msp&TB z=uw`0z~-}(4-n>*Gfv)__Fz6nZDuuCZgh&@XO?B*Z0)~Zm8g)=GXDDRxjl9&l~5AtdsIR&Fu zl)&zx1^c=~-O^#2Qn=|7nuS$3b3S;C+q zYwEmCvRMDhOQ7qViERj>DO@`;_{0LcG4flu2*rKdC%O9rsP>gnIM6a(Y8mYxi7De+ zTbE};Lf_hO;p*1I#{QLAw5sfi*$Nnc0~tvC19$uW&z{XUFCU0vGKbjR=Z z*zDM9YsOdav~FB)PWf7YPt6)9}xhyI+|L z$g?=giZ2Mr+{2fKQ7f4}Rd)xk!}AP@RP&?RWlRKm_bko~ZAeK4)m8Fdh|ZH3AYT|6 zUL)02Y@5GK8qRBMF(@ZaoGpLX?MBcsb_G52s`fP~=82HLokFucZ8vnSt0*0P8l0te zzM7#(N+!J|wqG^<@nf|`FQv%Z78O}G=c)F#f_39qY&rAhF` z#u8b-W|^0A3!Qvca)Z*}WE13aphkcU*Q}f4tY5ho5(W%%cKXx*C|#yT;!$o@(;|WX z`dPIkw?k1@F(F;dl~bKXLp4p*vtg2DHbEl8;V<~*4#fUI#;qyyz7dKJUN6Gl)F5BM zbdgzu9+Ue{>M>h3ddFa&+<@~;0tEKGm`(ep0 zZh3!t)XzV;RPy+v8Y$F4WyW_Ozh}$FBeibg=(H=2=;M1n_^C%>>r36;DZ)A|{;G82 zSkb1?#ED$?R4qzdyKC*AyhM=z?CM?G`-HnKjuV;}DR8}pst|zr8^NPrw-!GFe4g5$ zba265T(gl>>V8aIQ{cZhxxt~^EG435`OyN-vOLyp{J>0^aemfIPys@VL+jI*H#sWC z5}nnco7Scc5-{KTx`V+XIUE{N3IB_U3s@c`b*V=y>v1u8M+NahqjL`2;y|X_wXa_u z+^Gh2W)7Rc?TWxi{th9 z))ap7*(NE@RA0~P#9LJd8RZ3e;h8yvt%1^( zE(pKv)7zvIELTi2z>Vwj&ONktv#XBl3O@>)nlcWx8rp1Rvl_Nqc)vs}7GK+T!kl0c zKjHDn7NPcwQf;yjST#>&7g@Llv;?{l9Q2MF3MjSc2_r)^0E8;>a0sH*5I=Bj1`8v} z-UK3zKY5hccM?m`c~(ySyybH&o0Eb8&FtE1p&icvXSwb|voK*;M#HLN=73cl@0Y{W zS@1gndaTb5nsX&&S&Uj(;g{^sAA(?vB!mHi!wMnt8YC`5fj3N7L`jL@n*FK4@6$nJ zCm7^M;#z&7;CyBW(81)m$&pVXY+E88i~|hO&ya8p2C%9a?&T^8zelWr?8RFX*Mu2i zv^>;mrRF2+pKdJH76VtETMQAdkuhazolSh@p}kPiLom&+cdHrQNP<``Z`SFK^qU8i z3lF#Pde}N?ai@Ne8$71`OWL%vrkO{bgdSjwz!flRJ4h1s`%{xz!bi7+4pyWQmYF@T ztIwx@*9|5_L=LDGcqrzj!%hy#?+5>mEE!UW8q$qNwVW$EuTC{P+(85NLY+M&TvIsDxJNQrUx01+my3C zyRqpM<)9fVf&4PoYQrnZ>xxq09$sG1p@>_1^G{Lbf$pK!@0a-p@OQFK%c$J%6QQRF zIt6Ou1T6dJUdJzL6Gljnz%Tx0KfWwe-D;92!3o`D#@c`zXiPX{YSqV5 zFzRu&+$Cs?URQgMaZeO4e63)Dt%hnezG_{Tfe&BSnB;%jxyd`X<+TqFe%08x`GEd~ zw51&Z*Yj!m_q27YH-GyaUEAP$;`i;x>E|mi5+dM-i~w+d{@ojweaxFr60V}Dx1W6I zQYYLM&-<>_4)J8rFPFP166hTJfddXlGF?Jert6^v6v>rkhf;J)FH~*a%U7T*@Ell9 zvxH~dNY)O4TCJ?^da~n@igdQHR;Fx!lFnr9``lETgW5#CiS7vhn31E>udA`#+C|l{ zboE95^dgpnAFEq*LRXD7O+YJ4O5h?y+?d{@{Fy!}&7 zg3J64ag=rlb!E=AFt^1^Zc5Kzo)Dk`_Ll|C*nT1m_9DU+6_<7Kg4X zKmB>UoijDCccJqFgE*>^JR#Wn>?ue<+$X3uK9W?S_m-x+hzU%Ou zIx^_(8n7elr_`+eA+ZD_iqq@(Q;|Z~B;+};?{}9_DJ69mHqgEChrHEX-y%%QI051d z)3QrpSlA{lLEJmcpm966T95Z+E>w&vk+i^$uWlfxLq_lM0^aLImhGae8!qO~QTIPdV=juxK zh-*C8ep#=NU_CQn&E=mVuHwS;TjH(4NI*{hq0HMbvtp!mH^-h_HF6ss_nZ>#g6V({ zF3ocY#7n|w)izA+ei)%`@eC<#aQ4D+rH+!&jFQlvK~=hZ@R@~^8qlOXMNKGbuE5zA zxqhCdO1ikgNw^Y@UOxw?Q*d7<_{b{xrl|80hE-(Q0GxJi0lhkW=%_BC{y~(WpaPK5 z?A5*UOeLH*P^h=KS~O3}(jK%Nh>Vz|Bp$P#_zOB`e3Ii`W7CIb%=uY`?2QDie=I=rg7 z)o}N^LQyYOn8Mt9aExg&OmC>@}4itt3PAiQb6rOHj z!byu{fAqKbQanl{tr5Vm467|vF}=!x(ZZ_x zWEcgA8E*v1zzZ|-PIk?y8hH6LNTNdh_6!WU~fk@u~x z5*r*of~#g+M=Ay1kvAOf1?Sx3hVDc6HEV99!EMAG*WPuzUZ{S40FPY=(Ws^GeT#uT z3hSOcAzx${Fv4>v6*!RK+P2zr%@_n99A?99NN`&Xgw_n(MR2Q@0cwlj{v!dtvop1o z0xXLwDi7O<3g(FVfra4#85l)Y8T>fvqhyq1ekC$rh2)@Ht^>OJ-Y@lnMDv_Y`^y{n z_{`wTrebhhT#9CK66w`qye*>#j!{7NkpE5S8&n1d&Q`lU;O0K+cAElquCWzj0uTQyT5G2U$wZ9ZwXYZ zh=J?N8q^ymO=-O1&KqG3CT0}K5Xj3_Z?%0b#Vg!bT1~))5*E&POK1qdpi3q9qL+l2)PWlUxCNmF<(g!-V@} z$G0j5D2LKgso7aGK~w{1MxokBIDaLff|-woYNr{_$AFZEo_Nq$#!_Z*%*0Q4axT;j zYW$R8bp>?{hQphq9Pa(k%i6D20{(-3A`XZ@Ac*b;$9<3+N6gN;)TyjT`MieRWXH_J zAb&Mc@r{n`{@ACH0$b6NsagSu{_^yo27icKuIF(qdLCgB{sdJ93qCFwFJWm&I5tGY zTM{InRV+ewtga1@A#zD9NXKfzG#P2ljM2NNZLw4uR0hX2fmCe$R<_(A0zq1bOpB01 zn$>Riz*pWp1ozAknPHaxOu6g%vXT|Z!!qM{g;C_9@{!&KFAfJM@S&M`^spYpln^Av znkj4z;%#{r+f>HUk`UJe{Eb)~{2}Gt!qbj1)~mBx>th{!ybzJ`Pc|t$)_QT@h2FR> zZARFA5217+AIVk3@%Qg}-hKnp@YpD`P9CmAk%>^}OlbrXa(VNtE~dl@hFVu0Ask8A zd$f|UM42jaoEqB0Rt!XJ6hL?8UUniNOaEYJwRkn2T_DWH##XFZc9G7f0;&H>qwXb!uw`vfD5i;giSDK3$dJa z6rL+_Qwu+!;t(M30=AU^>SD2b2D=h!G-hxu7bn!FfEL>@(_+Yuk|z^@f0iRxvO>~J zWYZ&j-vRIX^;|zG;z?b&zo@%V#MLAw8y$eO3w((epw;7e3H zh~#EUgTvnU&*{cs=3$*!pFHBJZ2QYWdL=%65zoh2I;<0JSdN?sMJ!g+NPh-*^M#gd zrJ05^S%>Jzm}%Ar!<;bncOiCwrYUnn4DhPoIBnwrl#yJ?j3jSq0f$ad8QTO=61Sf( z@(|Y`lNQdqACK$;#(XGXoEC^EW^tXb$#s^SjM>eM9gN^C@Y&CEkGs319y%^Ra-%Cn z@_4H6pu~iCT)0XKTCmo3Yar}qc4`raObh*)GbQc@)2MS3`w3O#RP7ZPqkbF1VBs8FPy_Htn(eQdHT;ex?K;%sJ zWkd)Y%x@~;Nex*lBygQZDvH68pO(wJ0o865VfqWrLO>&&u*YNPK@JW#f)f{=LVw5kB6#3LMWCP~)+(5#+RI82Y3VxbRF*L)_~lcDtPuNghPwvDxg0Ru{^cqw$I+VHybZ5!jve1obw%RqA9e9YbV(y zWpGWHY%Ofmk|_Y)w7vTo$g>yBvz@=#OEr^D$=MNipbF3?a>-(WrkJy36Ij~v>iwJe z{as&gx8w)wX=(oW+4t1Q(}A_?;qLx7-(m>C{DIr)M}yXkfB`J9Hf`-&QSr^w?i>;K z!goU4C-gc#=lV0tR(E)?!@YA(Nnd3Cm~fu_8a%nQO+@P-Plr_;FOC{PRP}&X7voz2 zq&LKTZ8p;cScM_bHfc<;-q61#rk=z#meM(B~+p5w{+j~PYTJThC%EApiAHEu|DK>v?=c^%pX4GG2qdYPXaFJ%0}RHt{amjw=E>{*)&ImGno>P>kT{Rj7jz6kOlBvRu%tjua^^3|6z!s~<)q0;zV zLSRXpUS4;dT8I(C_$hs5Ku0w^Nom`VtL7%LBoOjNq`2#`3&IWEG;2u4WtEor{@tb* zGkG4WRpBq)8)k-rlA#xwe8Stpk0`=kdi-tku7|3_nl?V{D;DXl9V2*}VN?F@@?jfD zRk+0JUycrS_Du{y!mQ1RP-bvfi6=jK;q%$=KE*6CzH`x;wah5RU zBHJu`6f{gYyQfggcQ?XuI3}1pWljyIJk9m~SeQ6jTk3&06C6`&i_x_>4!9FGVY?^r z$r&(abLS-btWV1Rh>5d12Ubj2M%I3%?!}I}LaJ3386TDqDjuy#Tff=Yjuf_0S`oC8 z^>1*^ijk~5la!%}BB*R12dQk)Qz*1}nETC&MKX^LG_ov|9aBnaQF@|HE79j+^5}$o zLMSxzyS8Ckqn-u|cIXn%`4L)2993ajG^V-wPzF4^WuUOrRwU0pV{H?rL`Yu)?N9NV5v z#uEC#^}7Kgm?H7#ERJaLntblz`|TYrD&x~zKAQK8;6Fq?G9UXj#yNjnn#eNJEGyql zf^TSIUd2Bi^be49NR?f-uCbWVO$53xDn?QdKxjXFcw23$DRQ+*kBh`%1WWIJkr`8* z|8Coj@~&{q~DT; z2u{;q_WR(l<$7aPG!=zy33oF;d?a5Z^O)vsLDA@(p^-ka27NGlyYQ#I5_vFDmLYH2%ySU7OrieB$S< zAqF)TSQJ!LNgXxyZ5#{f^38AFXJp}6=Hq&$?PyokJ&PmRavwBVnvQ3|IREB@v|0b~ zK{oB>BWcFAr%Z1!*6*me+h=TjMtEbZPjKbKpD%4LHu}MsHhX&LRo`y*xZG-NPkrxK z<9J(hV^-6r!9ZI;xfTlc=jvLJ(l7 zDtY_CG}>(xf!lyq$c&-FAhHKn5B%yt!VxqU=MD%%P$-rkN+(O-aByY|Op~aIJa{-= zUD12Q)&9QxZ>3ABy(EfxuRW_wYpZIr_%+;POrZGiTBFM@Da~^4Nb`|DKAAoa5zhU0TJiZO>gZR<`DNl{e@ zixIUGVKfW56a)r;h8#kYl#&;k^3T2*Xvv;RE4RwO2U0l1-gFiisy4%MsF8RWAT81I z0e#D^)`F1mw1`NNBC@M^r{$Tj&Fk8SLra_O-ZwpztZjezCjM+M(>zhn^`T$pS7I-1 zBoi|!xYN?y;OYP7dO8~uBYOs{yr{n8RAg{d&9VCWtwZ2i6iWUN0j~I8k58^=4H!y0 z+|k9nc#&_^b+kq-1`7ZPnc(Q9j<&qD=^UNbP4F*kYW^oaXuWk_@ZX84?qO9F^8XwZ z()gc2p}WiEQ0N~mp?`-$A4&fYpisLuvsNdo@87M^9n+xqYrOs!R%rIWTA_bY6k7IQ zib4d!e-?%QTP`Fo3ei{m{rS5nWd3hOq5l;Z!sN1%|5gZiOD~ zD!;M0TyBMCQ~y_1=+u_~_o2|E+W#9U^dBvuN00tXOX&ZHLf&n)qs64)rs&bOx@*VC z31d~>?e#Y*9d^8VINE;t)=3H%t>e?tFmcMO*eqtOqw)4>n$iCR3XRA^LjM&CJt`kj1BJZd;je4luQ3c1&>1?}Kd{H(VZDZsJUvrnsBlD)qvju} zZUw7bjalQvC<5i=u(qy~B4v4kinfzy@+Y2xX-7;pMxCEx0}x@!^Q11rMaEXtHgy6* z)!ATO6qKDWO>=*T&=Gr_yF}!C9fUOQB!7J!&w}@MyRzc{%m|4}DMZk;Ht)hokygdm z)sDkD?gUZ(fWxFn7~D4fF$EO7jN02vkP55{k?GPu5Jp%E06x@^m%S%IAQf~JRZ{ee zm^GATPDEMJ1{`VJ46ZbU+yZBL(XxAK4yY3OA``~Z_s)`&l=Ycj*I=9QU!GHxZDlN& zmfkqU#Z{SZoWfLaG%Vitj2?aTt4opg;dS6zTI?E7{P^UAKOR-(@?m$_arjhC14Tsf zq2<9{GBreMmQsV`Zjtw6>%GkGy=t-#VLmZiLTKzfW)Az3B;Fg@B3Dc3eU7hBFybRw zF1BcS!6?O&N2ytRcLxWmc5b08M#S z6tzx0<_jD-H|aI$vF5$4PcsszsfRDD>&>%~nZOFGoL7d{MHVg% zw_4Wu_4wM#=fE}Z6iJJ)I%HGe+Q4621#Lh^XE)l zfK9)dc#6<=-tezek72t(-k<#KZ0kKls%?1DV&DWWuFR|eL2*>Lx8clH;>wW~nx?6hnwpiBHm$3xsH=UI z?+?8{=X<{2pT7S9KX7=R^E@w}x5wS_+*lI`^DAG2X6{(O&dXAxsQzG;u-|HoYf0Au z;HQ0AZ_h~6J=weEuWn&?dhrV!*U!uSmXnbe7hmXOw|=~Dw>{r(5As>wlw)EFF%;Q|k zTaKzV)(N$bW4Isci?uE<#C#F0&6bV%sILAuk|` zKd_c!D0Ff?U$2eMl@GxqQ%P+rr-IQb5$Y8N+76mg#f9~bbaDIvEE}i7vGSj&eYTxe zVD=ugipwW6MkB;?JOM{6`|cQkoeo9&KoK2VEHe~)sb>!VTYh5X%lk9TO90*VJyYN z4qL|4)R0?bslDhwXn3Z>(M`ugiAI9=iC>F;r4jH!EB8$!Ldnq>5G6WRlU`JG?Clv+O6j;6)yax`(NV6Q6s zM6C^`96b`ck~l$XrGwtOl7>V1K)k+yOI-)`B!_c%9SA86qXAKw9KGNnK=APU`5x;x z<4MzxAXBIAit9mfG`|hdM4&gwi1PlMFD2srlir=DDA2S6AA`pZe*(FS^fbscAja{V zMqL$4ixw&I+$<7XU6&)XED9+M>CLhv9ngq62=qv3LaWf?WQQs1A8Rr%W2zFrXm3Oy z^eRxH^cz@T%2n+!pyb|P5m3qE{P_$PI-TTX1zkFzy63y=)v3#&5t{+sf09H!Lc2N(DnqPS&Jn{J7t0;aZNQTmLbXV7-jU8!2p6wu6hE04} z;ihDQIpng1W!q#mmxS{Ux&;TF-DbZ;I@3{*7HNB#tqSh1u1nT$rjShC-@0G{sP;vl66u`W!f>mDS$y z4MJ!?UhIuCkRgy@rh?5KE45&`?QT+%0jUBV0vH^ebBB6{HHjbg#E2j9Flf-SS{F@G zEyDj!gLab`3OSuUO&5kSzDQarCCDuc5$v}h8UHO7yd#kpmTfPU-@sBZFAfaIjFOZ; zPa+f?kCI1)Aq^M4Kw4-|J^+1dFtiicCIJZjVzt-e<_)cwNTjwKA002|nIV*jvF9~^ zGtV^cPN827qx_5g0!FBR#pYWeUZlggO43&!1-P>|yxRZpL-W?*I zgjmZVPi%ZBKC^vu;?{vDpN603|NT4+Y8>+nd)JAq^8Ju>upshtFnMxW)+cvba~-kk z!g$h;&^TO^_f+!DL(6Jm!fEQDmilSqs`l09K>p@p{6 zvbMH^J!y#+TcIRDXtkNN7(3)@LXR-uy?hjl7^;JZCiAGr7|6&NpoTDEKgFh87pWv~r zw*g8DqE1Xly^M{(EI{vnfjw1&MG+-}bECZYi83NKcr9ba80@eRn*)6OHIKnM!>#y$uKru!b-_8{0n{0EZ#04N=w zn3(uKP$+4E25^N^iLmA7q6QH{fJUBJ2>wuope;bWgeiUTgi<{sAAtIAvW5^Fw9taEgj68-HeD$*yxEn2Rzi-eNZ19HF^O$gv$dLqZ%lu z1z(-_9E?;t>NN4WMqmLvOkqq8M%{8KmlZ({Q=B?P$A3;?luaPu6@+72Ou_#uA%Vn7 zs>C5WEOmn>`6I&@0N%w1&)tN9x6`YcFM)%yDPG^uBMXS-uZhf7i7tevN-sn~kaGme zrsK{mKZgRD?luz(yVSJ{4z)N@+EczhI)MG)=mNkGF+-> zpnfEn)(AS4+5jm}IIwWKs2tiXDt~LyL|8oV1(rAX*+yL$e^Pj=hcDr~05x!`*`-pv z>?h%_l3;0>Ucm<%1X6hc*%Pg9M%p!XX=nm28JwQ~s|_*2M>2@WLW)E*4f^%+#J;cjE{h7a!BS#x&N{;Bw5H#GlMozZK^XR+9gm)=o4TLG!eiW zmV*Ut94>p1xV;=Lv3aXTY$}?0AQd4{_E=%1=y(qr^-xOSD zMSOryt2whae=uQG2<<3``-#)m zA%7sxRV`pTBTL)>T5g6rYyQDOguEh8Q8tlLbak2vb`m2Ss3|o`h5>KDqvmyJJ#-Wo z5Ui$+CNY2{5+=kAt2`hPgz%?{GGgeclgBYbLa~p6KK+Mht{@+2;*J#KAR3Gxcs@_Q zvG2{{uP#8g2$3D!CiX#o@wqlc&JA4`#YCtcP}C-SW%tH;tn_}&HlS@K8sdwG;U1xXyV~qu0F!834a1?( z+4!M$u#y1ckcjD{qz2&m62)gXspx)EavbjzMLOgJDn!*0xJtI!C-yTSL0BhH-?-`d+q+6%qlf7!t1$c(mFk*ZQ;Un$d*K(zdj*P&2W+{w+ z`i{<+qz3Vjk5QN%zcAbdNH!pGS_8_$k4UP7E~|l$(-6t6rKuYbec@%e3Mw}j_OrHm z3p@#CA}utL;rxi}5751QSTf!-c_D8k_5I z1fj%YdEp4UDo}*@ts}F2*aWz1(=G$huTO(N4n-aVz#vYrUB{3P&#&w?4DZ~4l42@V zRVJC7IIWDNTQ}od011nVs35#x8A~HeJ6G0959&Z^8^^qbry#|w;{3z{I%-H%)*`~} zTgUve0w%*J)O-*)y0tqcPEK^_m`TJ@V#=g5CF!^eSBS6>*~Fk^VuofFKL(x`K<8wI zTHUBlz61w-f_33+JpwtVc55$FLYY)g3cjuhWV=YxGjU`qg+DUr5c)19-&VQ9c;3~< z3GXS(gO_s>qXmfWed(Q)RQ0ht(POO~C$KLa_5(?_72fRjKwNs@VwL5w_E(OxLxzm7 ze)--u=yg)ZVgYQu!tUKVMwtzwkwT2{ca0YR_@MhmmqziS3JdqTCIUOqQBNHtJgm`) zL})b;d$n$VbRaNt;g1jcD}G$k&ULqv{3Qz@T-%|B2$gxYZ1@<{D$4d0_A^bebH?V_AQjd+s*23xSASnAxsdSS9}^x%^Pw<$x2d^z;?UZekx4#N;4ChVEy3Hck3b zz3_Y=Iw7n z(!UDQ`-kQt9LkwckdMcz@}exDQc!3yN4lL`aD$D=MVm|xl;*%XO*%h1hIq$(sUi3eNEw^hBQf217MCqP8=!^h2g zsc`6J*tMX;FBBW?#~0z1n@o$w)8hZozyB@%sszbez-`$v_x^Gx=Rl-IY%9nds45_K zY#ci?7@5POwKRAf^=_HmSpoXKN(LW{>i&6W2 z+g$%w<2KZs}{AyRFC;Rg64P-K!YWaS<#gb%Cbtke)Eq+4=Pwi_L1 zFq2o&p|uw??!01FU%ol?`s5CW9Qs9rg zCIm0ZEHApDzjdXb)P_w20edH(FGZ|Mf1KWFxL^}%qe3cplmy{wz;5~@M$o82A=q>S zTCPw|TY!oeM8!7fx<1~1H}LE9HX~UC?WneuP?_-OkAdvqPfHC}_2VZHFD z*bMUnmt)&b(jl+v7kq$;oMWG39zMId;@7d|2aFNC1SDTir^2H2(ErJF$!8!gv70izUQ~UC=?l`I+Jz7b5yOsMjX8pPJW(^S!tKAV|@fEaD2achi zQaKu@+_;AH^5=K)rE0F#0#%{&71sA02oWOS`u-JoDg5fGlTMK=PFJ+-N}1|q5TVF5 z#Y>Q3}RlMQE|5`Xev;Oj-6HYH^fz9)g|k?dgC?u1wDL2w}46w z%-%iN?KIz)l3Jl!m6F{&^}ixkFNUAi_~%tQQi@a-cN>b7-c9i9LtU?I@-aahbtqtt<26Uq@tsz{TtO45a`8<(vg!*bth+It))Xv@{7UVdbibe zrNjEd{&Q-3^ro=a@zN)wzE-SBCmLCRrQz=MPOJWPq~(?BWP<%_cMXHRCqfZ~cA7^Q z6zJFRn#52Dk=Pv+@xw#w;vM(9IV^SU7@eLh{C}a4PnS$%EnwtajGdz%vW~>$e}lb~ zZqd|T{veA-87(0&_i7j*>(B65308@)7q98_sgbY#*76MBts$o0Q}QTFq$U}fux$sb zXyG-TH9SASo3QJ3 z?bKUzk~3kuMa$VfdROUbQWEBWpSUO8{M9HiyHQZ2b8QW2Vs2PnrVzR9q%Vd-J~;)1 z1zziPS$OE^bZ86lyL-xBAHTS+D{epcQQs4GR(DmVuHP#dRQtZu_ zw7qcbBX?chrl`7i)+S#RvupV*w}^~)V2uk2&HN{VZvLYloeQXM_qU-=1$9Xmc;`Cz zYc(|6#01Sp=n58^ij0@2YT{ArACR*BXKyUz$fV%AK#ud@oA{b-3HG=2mNsH!Y6&hQ zx0Up^R4Tk>I&yZXov58iXOZzi5-hxa1i;V;$r1r&8aCLP+vr}HiMZzzJj{|Us<77c zVW2+DE{%mSt`i(1@iI8DT#HBnnPzwN#(2(~l>67TuD*j5s?A9G0ziAE_6DU08Ucl` zuQUCp)kcP=^rDWEv}fX_4)C#3la*rx`lb|38!X#S2eSs4*+Iny!iz%P=a9`ypLKKQ9mx$f;*MuLaeowBa4?eMd}NkHc?t5MO5k2pYPN_=iZd3i9U zjaVZ){1_;!)dRYm4okvnh>WFKq$T zQJC-ATV=0M64me5>Xi>IMiX~^E!a&5q&9*k4J&_)#DRFay*?aIvO4OxC#@7W!jZki z(<`%fH|^n)&A-H&dXpQW#*FbE^{E?(?v(nSf0L7lDp1VTr6BOgYo}T9=JrBsHFoCi zUJ0FkO7<&bl`}HZwqOYNa+*4!59_Yi$IXvrOZja;A}a8pvQv)7c-jB}l_~9vW&lb> zT=j52Nb*IW{VI64Qn+QnS@T(^2Kt0VhM>mVLq*DSDO@+?D5LRhlPpxo(S`H;yTU)y zN}@og+AV_^n`{j|0>{9yciW|Ko`no6`yyY~ce>jRI__T3 zRJQ?C8fz-AMr(yPB~mAKR!5EcI}24`w$G;@tM?u4ioPaJic03~^dmaZ+~;bx8cZ)H zhIh|9FXBxP2Q_|8@6f)q4B1Ow+yVUv+F$ywEcwr@CRyktKCNB8`3@Zuiqo<{gYxWg zXRkKfq2MYq+s4`{_|ufYtn^kS>b$OYqfTZ(N z&yDgx*ldg2=XJy2<|GN7q$s3f>4&=Ub-|%mlXrjHaW1fbz4nev2Kf@-!}sid1++?n z9*OKrG-~y5JH6zS!E3Vfqp&H)o{PnblU+t`v4_h?mx3%f¨k!z_aNFW=6G#yxJ? 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%0AI;4Y4{-Ta%I zB;|+2w}p04{4z9FJI2rGT#4&K{bhhJ<>AB(2(Ugb1B*p|cL&+)?sY}S4U;alcAu^u n1bp8i9={4d81rZV00i)b-%LrT_UQcoLPCF_P*NiU0I>QW)2;|X From f1b171d88b00d8579e8cd4b0b1ee7cc42e0c8407 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sun, 26 Jan 2025 22:12:28 +0900 Subject: [PATCH 053/181] Update head doc (#1127) * update head doc * update head doc --- LICENSE | 2 +- docs/getting_started_main.rst | 19 +++++++++++++++++-- docs/index_main.rst | 32 ++++++++++++++++---------------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/LICENSE b/LICENSE index c0c52a4321..1c9b928b54 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2016 - 2022 Atsushi Sakai and other contributors: +Copyright (c) 2016 - now Atsushi Sakai and other contributors: https://github.com/AtsushiSakai/PythonRobotics/contributors Permission is hereby granted, free of charge, to any person obtaining a copy diff --git a/docs/getting_started_main.rst b/docs/getting_started_main.rst index 256319ab8f..db71960dcf 100644 --- a/docs/getting_started_main.rst +++ b/docs/getting_started_main.rst @@ -3,11 +3,15 @@ Getting Started =============== -What is this? -------------- +.. _`What is PythonRobotics?`: + +What is PythonRobotics? +------------------------ This is an Open Source Software (OSS) project: PythonRobotics, which is a Python code collection of robotics algorithms. +This is developped under `MIT license`_ and on `GitHub`_. + The focus of the project is on autonomous navigation, and the goal is for beginners in robotics to understand the basic ideas behind each algorithm. In this project, the algorithms which are practical and widely used in both academia and industry are selected. @@ -16,6 +20,17 @@ Each sample code is written in Python3 and only depends on some standard modules It includes intuitive animations to understand the behavior of the simulation. +.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics +.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE + +Features: + +1. Easy to read for understanding each algorithm's basic idea. + +2. Widely used and practical algorithms are selected. + +3. Minimum dependency. + See this paper for more details: diff --git a/docs/index_main.rst b/docs/index_main.rst index 08c318ea83..8c967ac59e 100644 --- a/docs/index_main.rst +++ b/docs/index_main.rst @@ -6,32 +6,32 @@ Welcome to PythonRobotics's documentation! ========================================== -Python codes for robotics algorithm. The project is on `GitHub`_. +"PythonRobotics" is a Python code collections and html based text book +(This document) for robotics algorithm, which is developed on `GitHub`_. -This is a Python code collection of robotics algorithms. +See this section (:ref:`What is PythonRobotics?`) for more details of this project. -Features: +This project is `the one of the most popular open-source software (OSS) in +the field of robotics on GitHub`_. +This is `user comments about this project`_, and +this graph shows GitHub star history of this project: -1. Easy to read for understanding each algorithm's basic idea. +.. image:: https://api.star-history.com/svg?repos=AtsushiSakai/PythonRobotics&type=Date + :alt: Star History + :width: 80% + :align: center -2. Widely used and practical algorithms are selected. -3. Minimum dependency. - -See this paper for more details: - -- `[1808.10703] PythonRobotics: a Python code collection of robotics - algorithms`_ (`BibTeX`_) - - -.. _`[1808.10703] PythonRobotics: a Python code collection of robotics algorithms`: https://arxiv.org/abs/1808.10703 -.. _BibTeX: https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib .. _GitHub: https://github.com/AtsushiSakai/PythonRobotics +.. _`user comments about this project`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/users_comments.md +.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE +.. _`the one of the most popular open-source software (OSS) in the field of robotics on GitHub`: https://github.com/topics/robotics +---------------------------------- .. toctree:: :maxdepth: 2 - :caption: Contents + :caption: Table of Contents getting_started modules/1_introduction/introduction From e93ada6cb84ad6be0cb7dd70af22e4defe84b0af Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Mon, 27 Jan 2025 22:11:15 +0900 Subject: [PATCH 054/181] update README (#1128) --- README.md | 12 ++++++------ docs/getting_started_main.rst | 2 +- docs/index_main.rst | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ec65342430..8621a40923 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![GitHub_Action_Windows_CI](https://github.com/AtsushiSakai/PythonRobotics/workflows/Windows_CI/badge.svg) [![Build status](https://ci.appveyor.com/api/projects/status/sb279kxuv1be391g?svg=true)](https://ci.appveyor.com/project/AtsushiSakai/pythonrobotics) -Python codes for robotics algorithm. +Python codes and textbook for robotics algorithm. # Table of Contents @@ -73,9 +73,9 @@ Python codes for robotics algorithm. * [1Password](#1password) * [Authors](#authors) -# What is this? +# What is PythonRobotics? -This is a Python code collection of robotics algorithms. +PythonRobotics is a Python code collection and a [textbook](https://atsushisakai.github.io/PythonRobotics/index.html) of robotics algorithms. Features: @@ -90,7 +90,7 @@ See this paper for more details: - [\[1808\.10703\] PythonRobotics: a Python code collection of robotics algorithms](https://arxiv.org/abs/1808.10703) ([BibTeX](https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib)) -# Requirements +# Requirements to run the code For running each sample code: @@ -116,13 +116,13 @@ For development: - [pycodestyle](https://pypi.org/project/pycodestyle/) (for code style check) -# Documentation +# Documentation (Textbook) This README only shows some examples of this project. If you are interested in other examples or mathematical backgrounds of each algorithm, -You can check the full documentation online: [Welcome to PythonRobotics’s documentation\! — PythonRobotics documentation](https://atsushisakai.github.io/PythonRobotics/index.html) +You can check the full documentation (textbook) online: [Welcome to PythonRobotics’s documentation\! — PythonRobotics documentation](https://atsushisakai.github.io/PythonRobotics/index.html) All animation gifs are stored here: [AtsushiSakai/PythonRoboticsGifs: Animation gifs of PythonRobotics](https://github.com/AtsushiSakai/PythonRoboticsGifs) diff --git a/docs/getting_started_main.rst b/docs/getting_started_main.rst index db71960dcf..86049ac2bf 100644 --- a/docs/getting_started_main.rst +++ b/docs/getting_started_main.rst @@ -10,7 +10,7 @@ What is PythonRobotics? This is an Open Source Software (OSS) project: PythonRobotics, which is a Python code collection of robotics algorithms. -This is developped under `MIT license`_ and on `GitHub`_. +This is developed under `MIT license`_ and on `GitHub`_. The focus of the project is on autonomous navigation, and the goal is for beginners in robotics to understand the basic ideas behind each algorithm. diff --git a/docs/index_main.rst b/docs/index_main.rst index 8c967ac59e..b9f334dd9b 100644 --- a/docs/index_main.rst +++ b/docs/index_main.rst @@ -6,7 +6,7 @@ Welcome to PythonRobotics's documentation! ========================================== -"PythonRobotics" is a Python code collections and html based text book +"PythonRobotics" is a Python code collections and textbook (This document) for robotics algorithm, which is developed on `GitHub`_. See this section (:ref:`What is PythonRobotics?`) for more details of this project. From 6744d4b104d9281f0a6c3404860b996f9d53d0a8 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Mon, 27 Jan 2025 22:41:52 +0900 Subject: [PATCH 055/181] copy paper contents (#1129) * copy paper contents * copy paper contents --- README.md | 8 +++- docs/getting_started_main.rst | 42 ++++++++++++++----- .../definition_of_robotics_main.rst | 5 ++- .../technology_for_robotics_main.rst | 10 ++++- .../2_localization/localization_main.rst | 1 + docs/modules/3_mapping/mapping_main.rst | 2 + docs/modules/4_slam/slam_main.rst | 2 + .../5_path_planning/path_planning_main.rst | 2 + .../6_path_tracking/path_tracking_main.rst | 2 + 9 files changed, 60 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8621a40923..cb1816c2b5 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ![GitHub_Action_Windows_CI](https://github.com/AtsushiSakai/PythonRobotics/workflows/Windows_CI/badge.svg) [![Build status](https://ci.appveyor.com/api/projects/status/sb279kxuv1be391g?svg=true)](https://ci.appveyor.com/project/AtsushiSakai/pythonrobotics) -Python codes and textbook for robotics algorithm. +Python codes and [textbook](https://atsushisakai.github.io/PythonRobotics/index.html) for robotics algorithm. # Table of Contents @@ -85,7 +85,11 @@ Features: 3. Minimum dependency. -See this paper for more details: +See this documentation + +- [Getting Started — PythonRobotics documentation](https://atsushisakai.github.io/PythonRobotics/getting_started.html#what-is-pythonrobotics) + +or this paper for more details: - [\[1808\.10703\] PythonRobotics: a Python code collection of robotics algorithms](https://arxiv.org/abs/1808.10703) ([BibTeX](https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib)) diff --git a/docs/getting_started_main.rst b/docs/getting_started_main.rst index 86049ac2bf..aaf304514d 100644 --- a/docs/getting_started_main.rst +++ b/docs/getting_started_main.rst @@ -9,27 +9,49 @@ What is PythonRobotics? ------------------------ This is an Open Source Software (OSS) project: PythonRobotics, which is a Python code collection of robotics algorithms. +These codes are developed under `MIT license`_ and on `GitHub`_. -This is developed under `MIT license`_ and on `GitHub`_. +This project has three main philosophies below: -The focus of the project is on autonomous navigation, and the goal is for beginners in robotics to understand the basic ideas behind each algorithm. +1. Easy to understand each algorithm's basic idea. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -In this project, the algorithms which are practical and widely used in both academia and industry are selected. +The goal is for beginners in robotics to understand the basic ideas behind each algorithm. +If the code is not easy to read, it would be difficult to achieve our goal of +allowing beginners to understand the algorithms. -Each sample code is written in Python3 and only depends on some standard modules for readability and ease of use. +Python[12] programming language is adopted in this project. +Python has great libraries for matrix operation, mathematical and scientific operation, +and visualization, which makes code more readable because such operations +don’t need to be re-implemented. +Having the core Python packages allows the user to focus on the algorithms, +rather than the implementations. It includes intuitive animations to understand the behavior of the simulation. -.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics -.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE - -Features: - -1. Easy to read for understanding each algorithm's basic idea. +about documenttion 2. Widely used and practical algorithms are selected. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second philosophy is that implemented algorithms have to be practical +and widely used in both academia and industry. +We believe learning these algorithms will be useful in many applications. +For example, Kalman filters and particle filter for localization, +grid mapping for mapping, +dynamic programming based approaches and sampling based approaches for path planning, +and optimal control based approach for path tracking. +These algorithms are implemented in this project. 3. Minimum dependency. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each sample code is written in Python3 and only depends on some standard +modules for readability and ease of use. + + +.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics +.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE See this paper for more details: diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index bdc7dc53e9..2f31834017 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -1,4 +1,7 @@ Definition of Robotics ---------------------- -TBD +In recent years, autonomous navigation technologies have received huge +attention in many fields. +Such fields include, autonomous driving[22], drone flight navigation, +and other transportation systems. diff --git a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst index 4c60ab8851..4dd1d1842f 100644 --- a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst +++ b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst @@ -1,4 +1,12 @@ Technology for Robotics ------------------------- -TBD +An autonomous navigation system is a system that can move to a goal over long +periods of time without any external control by an operator. +The system requires a wide range of technologies: +- It needs to know where it is (localization) +- Where it is safe (mapping) +- Where and how to move (path planning) +- How to control its motion (path following). + +The autonomous system would not work correctly if any of these technologies is missing. diff --git a/docs/modules/2_localization/localization_main.rst b/docs/modules/2_localization/localization_main.rst index db6651f6b8..22cbd094da 100644 --- a/docs/modules/2_localization/localization_main.rst +++ b/docs/modules/2_localization/localization_main.rst @@ -2,6 +2,7 @@ Localization ============ +Localization is the ability of a robot to know its position and orientation with sensors such as Global Navigation Satellite System:GNSS etc. In localization, Bayesian filters such as Kalman filters, histogram filter, and particle filter are widely used[31]. Fig.2 shows localization simulations using histogram filter and particle filter. .. toctree:: :maxdepth: 2 diff --git a/docs/modules/3_mapping/mapping_main.rst b/docs/modules/3_mapping/mapping_main.rst index a98acaaf50..28e18984d3 100644 --- a/docs/modules/3_mapping/mapping_main.rst +++ b/docs/modules/3_mapping/mapping_main.rst @@ -2,6 +2,8 @@ Mapping ======= +Mapping is the ability of a robot to understand its surroundings with external sensors such as LIDAR and camera. Robots have to recognize the position and shape of obstacles to avoid them. In mapping, grid mapping and machine learning algorithms are widely used[31][18]. Fig.3 shows mapping simulation results using grid mapping with 2D ray casting and 2D object clustering with k-means algorithm. + .. toctree:: :maxdepth: 2 :caption: Contents diff --git a/docs/modules/4_slam/slam_main.rst b/docs/modules/4_slam/slam_main.rst index 86befa6e35..dec04f253a 100644 --- a/docs/modules/4_slam/slam_main.rst +++ b/docs/modules/4_slam/slam_main.rst @@ -4,6 +4,8 @@ SLAM ==== Simultaneous Localization and Mapping(SLAM) examples +Simultaneous Localization and Mapping (SLAM) is an ability to estimate the pose of a robot and the map of the environment at the same time. The SLAM problem is hard to +solve, because a map is needed for localization and localization is needed for mapping. In this way, SLAM is often said to be similar to a ‘chicken-and-egg’ problem. Popular SLAM solution methods include the extended Kalman filter, particle filter, and Fast SLAM algorithm[31]. Fig.4 shows SLAM simulation results using extended Kalman filter and results using FastSLAM2.0[31]. .. toctree:: :maxdepth: 2 diff --git a/docs/modules/5_path_planning/path_planning_main.rst b/docs/modules/5_path_planning/path_planning_main.rst index 30802fd7a6..4960330b3e 100644 --- a/docs/modules/5_path_planning/path_planning_main.rst +++ b/docs/modules/5_path_planning/path_planning_main.rst @@ -3,6 +3,8 @@ Path Planning ============= +Path planning is the ability of a robot to search feasible and efficient path to the goal. The path has to satisfy some constraints based on the robot’s motion model and obstacle positions, and optimize some objective functions such as time to goal and distance to obstacle. In path planning, dynamic programming based approaches and sampling based approaches are widely used[22]. Fig.5 shows simulation results of potential field path planning and LQRRRT* path planning[27]. + .. toctree:: :maxdepth: 2 :caption: Contents diff --git a/docs/modules/6_path_tracking/path_tracking_main.rst b/docs/modules/6_path_tracking/path_tracking_main.rst index 504ba08795..d7a895b562 100644 --- a/docs/modules/6_path_tracking/path_tracking_main.rst +++ b/docs/modules/6_path_tracking/path_tracking_main.rst @@ -3,6 +3,8 @@ Path Tracking ============= +Path tracking is the ability of a robot to follow the reference path generated by a path planner while simultaneously stabilizing the robot. The path tracking controller may need to account for modeling error and other forms of uncertainty. In path tracking, feedback control techniques and optimization based control techniques are widely used[22]. Fig.6 shows simulations using rear wheel feedback steering control and PID speed control, and iterative linear model predictive path tracking control[27]. + .. toctree:: :maxdepth: 2 :caption: Contents From 53f1ba35d5c1b981eca55a91b72c53fb1000263a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Jan 2025 09:19:20 +0900 Subject: [PATCH 056/181] build(deps): bump ruff from 0.9.2 to 0.9.3 in /requirements (#1130) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.2 to 0.9.3. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.2...0.9.3) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 11b91abb1f..e8bed7b6d7 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.14.1 # For unit test -ruff == 0.9.2 # For unit test +ruff == 0.9.3 # For unit test From fc3562733814e43134da8a1d7d73e370d44b9f4e Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Tue, 28 Jan 2025 22:25:53 +0900 Subject: [PATCH 057/181] update getting started (#1131) --- docs/getting_started_main.rst | 122 ++------------------------ docs/how_to_read_textbook_main.rst | 4 + docs/how_to_use_main.rst | 30 +++++++ docs/what_is_python_robotics_main.rst | 100 +++++++++++++++++++++ 4 files changed, 140 insertions(+), 116 deletions(-) create mode 100644 docs/how_to_read_textbook_main.rst create mode 100644 docs/how_to_use_main.rst create mode 100644 docs/what_is_python_robotics_main.rst diff --git a/docs/getting_started_main.rst b/docs/getting_started_main.rst index aaf304514d..07abeec451 100644 --- a/docs/getting_started_main.rst +++ b/docs/getting_started_main.rst @@ -3,120 +3,10 @@ Getting Started =============== -.. _`What is PythonRobotics?`: - -What is PythonRobotics? ------------------------- - -This is an Open Source Software (OSS) project: PythonRobotics, which is a Python code collection of robotics algorithms. -These codes are developed under `MIT license`_ and on `GitHub`_. - -This project has three main philosophies below: - -1. Easy to understand each algorithm's basic idea. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The goal is for beginners in robotics to understand the basic ideas behind each algorithm. -If the code is not easy to read, it would be difficult to achieve our goal of -allowing beginners to understand the algorithms. - -Python[12] programming language is adopted in this project. -Python has great libraries for matrix operation, mathematical and scientific operation, -and visualization, which makes code more readable because such operations -don’t need to be re-implemented. -Having the core Python packages allows the user to focus on the algorithms, -rather than the implementations. - -It includes intuitive animations to understand the behavior of the simulation. - -about documenttion - -2. Widely used and practical algorithms are selected. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The second philosophy is that implemented algorithms have to be practical -and widely used in both academia and industry. -We believe learning these algorithms will be useful in many applications. -For example, Kalman filters and particle filter for localization, -grid mapping for mapping, -dynamic programming based approaches and sampling based approaches for path planning, -and optimal control based approach for path tracking. -These algorithms are implemented in this project. - -3. Minimum dependency. -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Each sample code is written in Python3 and only depends on some standard -modules for readability and ease of use. - - -.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics -.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE - - -See this paper for more details: - -- PythonRobotics: a Python code collection of robotics algorithms: https://arxiv.org/abs/1808.10703 - -.. _`Requirements`: - -Requirements -------------- - -- `Python 3.12.x`_ -- `NumPy`_ -- `SciPy`_ -- `Matplotlib`_ -- `cvxpy`_ - -For development: - -- `pytest`_ (for unit tests) -- `pytest-xdist`_ (for parallel unit tests) -- `mypy`_ (for type check) -- `sphinx`_ (for document generation) -- `ruff`_ (for code style check) - -.. _`Python 3.12.x`: https://www.python.org/ -.. _`NumPy`: https://numpy.org/ -.. _`SciPy`: https://scipy.org/ -.. _`Matplotlib`: https://matplotlib.org/ -.. _`cvxpy`: https://www.cvxpy.org/ -.. _`pytest`: https://docs.pytest.org/en/latest/ -.. _`pytest-xdist`: https://github.com/pytest-dev/pytest-xdist -.. _`mypy`: https://mypy-lang.org/ -.. _`sphinx`: https://www.sphinx-doc.org/en/master/index.html -.. _`ruff`: https://github.com/astral-sh/ruff - - -How to use ----------- - -1. Clone this repo and go into dir. - -.. code-block:: - - >$ git clone https://github.com/AtsushiSakai/PythonRobotics.git - - >$ cd PythonRobotics - - -2. Install the required libraries. - -using conda : - -.. code-block:: - - >$ conda env create -f requirements/environment.yml - -using pip : - -.. code-block:: - - >$ pip install -r requirements/requirements.txt - - -3. Execute python script in each directory. - -4. Add star to this repo if you like it 😃. +.. toctree:: + :maxdepth: 2 + :caption: Contents + what_is_python_robotics + how_to_use + how_to_read_textbook diff --git a/docs/how_to_read_textbook_main.rst b/docs/how_to_read_textbook_main.rst new file mode 100644 index 0000000000..3b92970e26 --- /dev/null +++ b/docs/how_to_read_textbook_main.rst @@ -0,0 +1,4 @@ +How To read this textbook +-------------------------- + +TBD \ No newline at end of file diff --git a/docs/how_to_use_main.rst b/docs/how_to_use_main.rst new file mode 100644 index 0000000000..d21b07c1a3 --- /dev/null +++ b/docs/how_to_use_main.rst @@ -0,0 +1,30 @@ +How to use +---------- + +1. Clone this repo and go into dir. + +.. code-block:: + + >$ git clone https://github.com/AtsushiSakai/PythonRobotics.git + + >$ cd PythonRobotics + + +2. Install the required libraries. + +using conda : + +.. code-block:: + + >$ conda env create -f requirements/environment.yml + +using pip : + +.. code-block:: + + >$ pip install -r requirements/requirements.txt + + +3. Execute python script in each directory. + +4. Add star to this repo if you like it 😃. diff --git a/docs/what_is_python_robotics_main.rst b/docs/what_is_python_robotics_main.rst new file mode 100644 index 0000000000..ce6e059dba --- /dev/null +++ b/docs/what_is_python_robotics_main.rst @@ -0,0 +1,100 @@ +.. _`What is PythonRobotics?`: + +What is PythonRobotics? +------------------------ + +This is an Open Source Software (OSS) project: PythonRobotics, which is a Python code collection of robotics algorithms. +These codes are developed under `MIT license`_ and on `GitHub`_. + +This project has three main philosophies below: + +1. Easy to understand each algorithm's basic idea. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The goal is for beginners in robotics to understand the basic ideas behind each algorithm. + +`Python`_ programming language is adopted in this project to achieve the goal. +Python is a high-level programming language that is easy to read and write. +If the code is not easy to read, it would be difficult to achieve our goal of +allowing beginners to understand the algorithms. +Python has great libraries for matrix operation, mathematical and scientific operation, +and visualization, which makes code more readable because such operations +don’t need to be re-implemented. +Having the core Python packages allows the user to focus on the algorithms, +rather than the implementations. + +PythonRobotics provides not only the code but also intuitive animations that +visually demonstrate the process and behavior of each algorithm over time. +This is an example of an animation gif file that shows the process of the +path planning in a highway scenario for autonomous vehicle. + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/FrenetOptimalTrajectory/high_speed_and_velocity_keeping_frenet_path.gif + +This animation is a gif file and is created using the `Matplotlib`_ library. +All animaion gif files are stored in the `PythonRoboticsGifs`_ repository and +all animation movies are uploaded to this `YouTube channel`_. + +PythonRobotics also provides a textbook that explains the basic ideas of each algorithm. + +.. _`Python`: https://www.python.org/ +.. _`PythonRoboticsGifs`: https://github.com/AtsushiSakai/PythonRoboticsGifs +.. _`YouTube channel`: https://youtube.com/playlist?list=PL12URV8HFpCozuz0SDxke6b2ae5UZvIwa&si=AH2fNPPYufPtK20S + + +2. Widely used and practical algorithms are selected. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second philosophy is that implemented algorithms have to be practical +and widely used in both academia and industry. +We believe learning these algorithms will be useful in many applications. +For example, Kalman filters and particle filter for localization, +grid mapping for mapping, +dynamic programming based approaches and sampling based approaches for path planning, +and optimal control based approach for path tracking. +These algorithms are implemented in this project. + +3. Minimum dependency. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Each sample code is written in Python3 and only depends on some standard +modules for readability and ease of use. + + +.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics +.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE + + +See this paper for more details: + +- PythonRobotics: a Python code collection of robotics algorithms: https://arxiv.org/abs/1808.10703 + +.. _`Requirements`: + +Requirements +============ + +- `Python 3.12.x`_ +- `NumPy`_ +- `SciPy`_ +- `Matplotlib`_ +- `cvxpy`_ + +For development: + +- `pytest`_ (for unit tests) +- `pytest-xdist`_ (for parallel unit tests) +- `mypy`_ (for type check) +- `sphinx`_ (for document generation) +- `ruff`_ (for code style check) + +.. _`Python 3.12.x`: https://www.python.org/ +.. _`NumPy`: https://numpy.org/ +.. _`SciPy`: https://scipy.org/ +.. _`Matplotlib`: https://matplotlib.org/ +.. _`cvxpy`: https://www.cvxpy.org/ +.. _`pytest`: https://docs.pytest.org/en/latest/ +.. _`pytest-xdist`: https://github.com/pytest-dev/pytest-xdist +.. _`mypy`: https://mypy-lang.org/ +.. _`sphinx`: https://www.sphinx-doc.org/en/master/index.html +.. _`ruff`: https://github.com/astral-sh/ruff + From 44bad786829e713c44259bc18cc7ac24b6c1e24c Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Wed, 29 Jan 2025 21:54:18 +0900 Subject: [PATCH 058/181] update getting started (#1132) --- docs/how_to_read_textbook_main.rst | 4 ---- docs/index_main.rst | 3 +-- .../0_getting_started_main.rst} | 7 +++--- .../1_what_is_python_robotics_main.rst} | 23 +++++++++++++------ .../0_getting_started/2_how_to_use_main.rst} | 0 .../3_how_to_contribute_main.rst} | 0 .../4_how_to_read_textbook_main.rst | 6 +++++ 7 files changed, 27 insertions(+), 16 deletions(-) delete mode 100644 docs/how_to_read_textbook_main.rst rename docs/{getting_started_main.rst => modules/0_getting_started/0_getting_started_main.rst} (53%) rename docs/{what_is_python_robotics_main.rst => modules/0_getting_started/1_what_is_python_robotics_main.rst} (79%) rename docs/{how_to_use_main.rst => modules/0_getting_started/2_how_to_use_main.rst} (100%) rename docs/{how_to_contribute_main.rst => modules/0_getting_started/3_how_to_contribute_main.rst} (100%) create mode 100644 docs/modules/0_getting_started/4_how_to_read_textbook_main.rst diff --git a/docs/how_to_read_textbook_main.rst b/docs/how_to_read_textbook_main.rst deleted file mode 100644 index 3b92970e26..0000000000 --- a/docs/how_to_read_textbook_main.rst +++ /dev/null @@ -1,4 +0,0 @@ -How To read this textbook --------------------------- - -TBD \ No newline at end of file diff --git a/docs/index_main.rst b/docs/index_main.rst index b9f334dd9b..c1e8d22d32 100644 --- a/docs/index_main.rst +++ b/docs/index_main.rst @@ -33,7 +33,7 @@ this graph shows GitHub star history of this project: :maxdepth: 2 :caption: Table of Contents - getting_started + modules/0_getting_started/0_getting_started modules/1_introduction/introduction modules/2_localization/localization modules/3_mapping/mapping @@ -46,7 +46,6 @@ this graph shows GitHub star history of this project: modules/10_control/control modules/11_utils/utils modules/12_appendix/appendix - how_to_contribute Indices and tables diff --git a/docs/getting_started_main.rst b/docs/modules/0_getting_started/0_getting_started_main.rst similarity index 53% rename from docs/getting_started_main.rst rename to docs/modules/0_getting_started/0_getting_started_main.rst index 07abeec451..cfec5b3c73 100644 --- a/docs/getting_started_main.rst +++ b/docs/modules/0_getting_started/0_getting_started_main.rst @@ -7,6 +7,7 @@ Getting Started :maxdepth: 2 :caption: Contents - what_is_python_robotics - how_to_use - how_to_read_textbook + 1_what_is_python_robotics + 2_how_to_use + 3_how_to_contribute + 4_how_to_read_textbook diff --git a/docs/what_is_python_robotics_main.rst b/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst similarity index 79% rename from docs/what_is_python_robotics_main.rst rename to docs/modules/0_getting_started/1_what_is_python_robotics_main.rst index ce6e059dba..32b1ad4e5d 100644 --- a/docs/what_is_python_robotics_main.rst +++ b/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst @@ -8,7 +8,7 @@ These codes are developed under `MIT license`_ and on `GitHub`_. This project has three main philosophies below: -1. Easy to understand each algorithm's basic idea. +Philosophy 1. Easy to understand each algorithm's basic idea. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The goal is for beginners in robotics to understand the basic ideas behind each algorithm. @@ -31,17 +31,26 @@ path planning in a highway scenario for autonomous vehicle. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/FrenetOptimalTrajectory/high_speed_and_velocity_keeping_frenet_path.gif This animation is a gif file and is created using the `Matplotlib`_ library. -All animaion gif files are stored in the `PythonRoboticsGifs`_ repository and -all animation movies are uploaded to this `YouTube channel`_. +All animation gif files are stored in the `PythonRoboticsGifs`_ repository and +all animation movies are also uploaded to this `YouTube channel`_. PythonRobotics also provides a textbook that explains the basic ideas of each algorithm. +The PythonRobotics textbook allows you to learn fundamental algorithms used in +robotics with minimal mathematical formulas and textual explanations, +based on PythonRobotics’ sample codes and animations. +The contents of this document, like the code, are managed in the PythonRobotics +`GitHub`_ repository and converted into HTML-based online documents using `Sphinx`_, +which is a Python-based documentation builder. +Please refer to this section ":ref:`How to read this textbook`" for information on +how to read this document for learning. + .. _`Python`: https://www.python.org/ .. _`PythonRoboticsGifs`: https://github.com/AtsushiSakai/PythonRoboticsGifs .. _`YouTube channel`: https://youtube.com/playlist?list=PL12URV8HFpCozuz0SDxke6b2ae5UZvIwa&si=AH2fNPPYufPtK20S -2. Widely used and practical algorithms are selected. +Philosophy 2. Widely used and practical algorithms are selected. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The second philosophy is that implemented algorithms have to be practical @@ -53,7 +62,7 @@ dynamic programming based approaches and sampling based approaches for path plan and optimal control based approach for path tracking. These algorithms are implemented in this project. -3. Minimum dependency. +Philosophy 3. Minimum dependency. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Each sample code is written in Python3 and only depends on some standard @@ -84,7 +93,7 @@ For development: - `pytest`_ (for unit tests) - `pytest-xdist`_ (for parallel unit tests) - `mypy`_ (for type check) -- `sphinx`_ (for document generation) +- `Sphinx`_ (for document generation) - `ruff`_ (for code style check) .. _`Python 3.12.x`: https://www.python.org/ @@ -95,6 +104,6 @@ For development: .. _`pytest`: https://docs.pytest.org/en/latest/ .. _`pytest-xdist`: https://github.com/pytest-dev/pytest-xdist .. _`mypy`: https://mypy-lang.org/ -.. _`sphinx`: https://www.sphinx-doc.org/en/master/index.html +.. _`Sphinx`: https://www.sphinx-doc.org/en/master/index.html .. _`ruff`: https://github.com/astral-sh/ruff diff --git a/docs/how_to_use_main.rst b/docs/modules/0_getting_started/2_how_to_use_main.rst similarity index 100% rename from docs/how_to_use_main.rst rename to docs/modules/0_getting_started/2_how_to_use_main.rst diff --git a/docs/how_to_contribute_main.rst b/docs/modules/0_getting_started/3_how_to_contribute_main.rst similarity index 100% rename from docs/how_to_contribute_main.rst rename to docs/modules/0_getting_started/3_how_to_contribute_main.rst diff --git a/docs/modules/0_getting_started/4_how_to_read_textbook_main.rst b/docs/modules/0_getting_started/4_how_to_read_textbook_main.rst new file mode 100644 index 0000000000..fb8db348c6 --- /dev/null +++ b/docs/modules/0_getting_started/4_how_to_read_textbook_main.rst @@ -0,0 +1,6 @@ +.. _`How to read this textbook`: + +How to read this textbook +-------------------------- + +TBD \ No newline at end of file From 2b9cc06000a4ae57dce885240d04e38e30cff8e3 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Thu, 30 Jan 2025 22:25:45 +0900 Subject: [PATCH 059/181] update getting started (#1133) --- .../0_getting_started_main.rst | 2 +- .../1_what_is_python_robotics_main.rst | 49 ++++++++++++------- ...rst => 2_how_to_run_sample_codes_main.rst} | 6 ++- 3 files changed, 37 insertions(+), 20 deletions(-) rename docs/modules/0_getting_started/{2_how_to_use_main.rst => 2_how_to_run_sample_codes_main.rst} (84%) diff --git a/docs/modules/0_getting_started/0_getting_started_main.rst b/docs/modules/0_getting_started/0_getting_started_main.rst index cfec5b3c73..cb2cba4784 100644 --- a/docs/modules/0_getting_started/0_getting_started_main.rst +++ b/docs/modules/0_getting_started/0_getting_started_main.rst @@ -8,6 +8,6 @@ Getting Started :caption: Contents 1_what_is_python_robotics - 2_how_to_use + 2_how_to_run_sample_codes 3_how_to_contribute 4_how_to_read_textbook diff --git a/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst b/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst index 32b1ad4e5d..8c932b7263 100644 --- a/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst +++ b/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst @@ -6,6 +6,9 @@ What is PythonRobotics? This is an Open Source Software (OSS) project: PythonRobotics, which is a Python code collection of robotics algorithms. These codes are developed under `MIT license`_ and on `GitHub`_. +.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics +.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE + This project has three main philosophies below: Philosophy 1. Easy to understand each algorithm's basic idea. @@ -60,35 +63,32 @@ For example, Kalman filters and particle filter for localization, grid mapping for mapping, dynamic programming based approaches and sampling based approaches for path planning, and optimal control based approach for path tracking. -These algorithms are implemented in this project. +These algorithms are implemented and explained in the textbook in this project. Philosophy 3. Minimum dependency. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Each sample code is written in Python3 and only depends on some standard -modules for readability and ease of use. - - -.. _GitHub: https://github.com/AtsushiSakai/PythonRobotics -.. _`MIT license`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/LICENSE - - -See this paper for more details: +Each sample code of PythonRobotics is written in Python3 and only depends on +some standard modules for readability and ease to setup and use. -- PythonRobotics: a Python code collection of robotics algorithms: https://arxiv.org/abs/1808.10703 .. _`Requirements`: Requirements ============ -- `Python 3.12.x`_ -- `NumPy`_ -- `SciPy`_ -- `Matplotlib`_ -- `cvxpy`_ +PythonRobotics depends only on the following five libraries, +including Python itself, to run each sample code. -For development: +- `Python 3.12.x`_ (for Python runtime) +- `NumPy`_ (for matrix operation) +- `SciPy`_ (for scientific operation) +- `cvxpy`_ (for convex optimization) +- `Matplotlib`_ (for visualization) + +If you only need to run the code, the five libraries mentioned above are sufficient. +However, for code development or creating documentation for the textbook, +the following additional libraries are required. - `pytest`_ (for unit tests) - `pytest-xdist`_ (for parallel unit tests) @@ -107,3 +107,18 @@ For development: .. _`Sphinx`: https://www.sphinx-doc.org/en/master/index.html .. _`ruff`: https://github.com/astral-sh/ruff +For instructions on installing the above libraries, please refer to +this section ":ref:`How to run sample codes`". + +Arxiv paper +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We have published a paper on this project on Arxiv in 2018. + +See this paper for more details about this Project: + +- `PythonRobotics: a Python code collection of robotics algorithms`_ (`BibTeX`_) + +.. _`PythonRobotics: a Python code collection of robotics algorithms`: https://arxiv.org/abs/1808.10703 +.. _`BibTeX`: https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib + diff --git a/docs/modules/0_getting_started/2_how_to_use_main.rst b/docs/modules/0_getting_started/2_how_to_run_sample_codes_main.rst similarity index 84% rename from docs/modules/0_getting_started/2_how_to_use_main.rst rename to docs/modules/0_getting_started/2_how_to_run_sample_codes_main.rst index d21b07c1a3..eff54d3426 100644 --- a/docs/modules/0_getting_started/2_how_to_use_main.rst +++ b/docs/modules/0_getting_started/2_how_to_run_sample_codes_main.rst @@ -1,5 +1,7 @@ -How to use ----------- +.. _`How to run sample codes`: + +How to run sample codes +------------------------- 1. Clone this repo and go into dir. From a5fc2d039db9cdf6182d9fc97073b0d6e7388830 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Fri, 31 Jan 2025 21:38:06 +0900 Subject: [PATCH 060/181] update getting started (#1134) Co-authored-by: Atsushi Sakai --- .../2_how_to_run_sample_codes_main.rst | 94 ++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/docs/modules/0_getting_started/2_how_to_run_sample_codes_main.rst b/docs/modules/0_getting_started/2_how_to_run_sample_codes_main.rst index eff54d3426..b92fc9bde0 100644 --- a/docs/modules/0_getting_started/2_how_to_run_sample_codes_main.rst +++ b/docs/modules/0_getting_started/2_how_to_run_sample_codes_main.rst @@ -3,7 +3,21 @@ How to run sample codes ------------------------- -1. Clone this repo and go into dir. +In this chapter, we will explain the setup process for running each sample code +in PythonRobotics and describe the contents of each directory. + +Steps to setup and run sample codes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +1. Install `Python 3.12.x`_ + +Note that older versions of Python3 might work, but we recommend using +the specified version, because the sample codes are only tested with it. + +2. If you prefer to use conda for package management, install +Anaconda or Miniconda to use `conda`_ command. + +3. Clone this repo and go into dir. .. code-block:: @@ -12,7 +26,10 @@ How to run sample codes >$ cd PythonRobotics -2. Install the required libraries. +4. Install the required libraries. + +We have prepared requirements management files for `conda`_ and `pip`_ under +the requirements directory. Using these files makes it easy to install the necessary libraries. using conda : @@ -27,6 +44,75 @@ using pip : >$ pip install -r requirements/requirements.txt -3. Execute python script in each directory. +5. Execute python script in each directory. + +For example, to run the sample code of `Extented Kalman Filter` in the +`localization` directory, execute the following command: + +.. code-block:: + + >$ cd localization/extended_kalman_filter + + >$ python extended_kalman_filter.py + +Then, you can see this animation of the EKF algorithm based localization: + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Localization/extended_kalman_filter/animation.gif + +Please refer to the `Directory structure`_ section for more details on the contents of each directory. + +6. Add star to this repo if you like it 😃. + +.. _`Python 3.12.x`: https://www.python.org/ +.. _`conda`: https://docs.conda.io/projects/conda/en/stable/user-guide/install/index.html +.. _`pip`: https://pip.pypa.io/en/stable/ +.. _`the requirements directory`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/requirements + +.. _`Directory structure`: + +Directory structure +~~~~~~~~~~~~~~~~~~~~ + +The top-level directory structure of PythonRobotics is as follows: + +Sample codes directories: + +- `AerialNavigation`_ : the sample codes of aerial navigation algorithms for drones and rocket landing. +- `ArmNavigation`_ : the sample codes of arm navigation algorithms for robotic arms. +- `Localization`_ : the sample codes of localization algorithms. +- `Bipedal`_ : the sample codes of bipedal walking algorithms for legged robots. +- `Control`_ : the sample codes of control algorithms for robotic systems. +- `Mapping`_ : the sample codes of mapping or obstacle shape recognition algorithms. +- `PathPlanning`_ : the sample codes of path planning algorithms. +- `PathTracking`_ : the sample codes of path tracking algorithms for car like robots. +- `SLAM`_ : the sample codes of SLAM algorithms. + +Other directories: + +- `docs`_ : This directory contains the documentation of PythonRobotics. +- `requirements`_ : This directory contains the requirements management files. +- `tests`_ : This directory contains the unit test files. +- `utils`_ : This directory contains utility functions used in some sample codes in common. +- `.github`_ : This directory contains the GitHub Actions configuration files. +- `.circleci`_ : This directory contains the CircleCI configuration files. + +The structure of this document is the same as that of the sample code +directories mentioned above. +For more details, please refer to the :ref:`How to read this textbook` section. + -4. Add star to this repo if you like it 😃. +.. _`AerialNavigation`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/AerialNavigation +.. _`ArmNavigation`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/ArmNavigation +.. _`Localization`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/Localization +.. _`Bipedal`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/Bipedal +.. _`Control`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/Control +.. _`Mapping`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/Mapping +.. _`PathPlanning`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/PathPlanning +.. _`PathTracking`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/PathTracking +.. _`SLAM`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/SLAM +.. _`docs`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/docs +.. _`requirements`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/requirements +.. _`tests`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/tests +.. _`utils`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/utils +.. _`.github`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/.github +.. _`.circleci`: https://github.com/AtsushiSakai/PythonRobotics/tree/master/.circleci From f225c1845775cf8e1d8c62d04164743c2e9e00fc Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sat, 1 Feb 2025 22:47:52 +0900 Subject: [PATCH 061/181] update getting started (#1135) Co-authored-by: Atsushi Sakai --- .../3_how_to_contribute_main.rst | 45 +++++++++++++++---- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/docs/modules/0_getting_started/3_how_to_contribute_main.rst b/docs/modules/0_getting_started/3_how_to_contribute_main.rst index 915b2155e4..296e4e8658 100644 --- a/docs/modules/0_getting_started/3_how_to_contribute_main.rst +++ b/docs/modules/0_getting_started/3_how_to_contribute_main.rst @@ -2,9 +2,18 @@ How To Contribute ================= This document describes how to contribute this project. +There are several ways to contribute to this project as below: + +#. `Adding a new algorithm example`_ +#. `Reporting and fixing a defect`_ +#. `Adding missed documentations for existing examples`_ +#. `Supporting this project`_ + + +.. _`Adding a new algorithm example`: Adding a new algorithm example -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is a step by step manual to add a new algorithm example. @@ -67,9 +76,16 @@ Please check other documents for details. You can build the doc locally based on `doc README`_. -Note that the `reStructuredText`_ based doc should only focus on the mathematics and the algorithm of the example. +For creating a gif animation, you can use this tool: `matplotrecorder`_. + +The created gif file should be stored in the `PythonRoboticsGifs`_ repository, +so please create a PR to add it and refer to it in the doc. + +Note that the `reStructuredText`_ based doc should only focus on the +mathematics and the algorithm of the example. -Documentations related codes should be in the python script as the header comments of the script or docstrings of each function. +Documentations related codes should be in the python script as the header +comments of the script or docstrings of each function. .. _`submit a pull request`: @@ -92,6 +108,9 @@ After that, I will start the review. Note that this is my hobby project; I appreciate your patience during the review process. +  + +.. _`Reporting and fixing a defect`: Reporting and fixing a defect ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -115,19 +134,24 @@ in the test code to show the issue was solved. This doc `submit a pull request`_ can be helpful to submit a pull request. +.. _`Adding missed documentations for existing examples`: + Adding missed documentations for existing examples ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Adding the missed documentations for existing examples is also great contribution. If you check the `Python Robotics Docs`_, you can notice that some of the examples -only have a simulation gif or short overview descriptions, +only have a simulation gif or short overview descriptions or just TBD., but no detailed algorithm or mathematical description. +These documents needs to be improved. This doc `how to write doc`_ can be helpful to write documents. +.. _`Supporting this project`: + Supporting this project -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^^^^^ Supporting this project financially is also a great contribution!!. @@ -141,10 +165,12 @@ If you or your company would like to support this project, please consider: If you would like to support us in some other way, please contact with creating an issue. -Sponsors ---------- +Current Major Sponsors +----------------------- + +#. `JetBrains`_ : They are providing a free license of their IDEs for this OSS development. +#. `1Password`_ : They are providing a free license of their 1Password team license for this OSS project. -1. `JetBrains`_ : They are providing a free license of their IDEs for this OSS development. .. _`Python Robotics Docs`: https://atsushisakai.github.io/PythonRobotics @@ -160,5 +186,8 @@ Sponsors .. _`Sponsor @AtsushiSakai on GitHub Sponsors`: https://github.com/sponsors/AtsushiSakai .. _`Become a backer or sponsor on Patreon`: https://www.patreon.com/myenigma .. _`One-time donation via PayPal`: https://www.paypal.com/paypalme/myenigmapay/ +.. _`1Password`: https://github.com/1Password/for-open-source +.. _`matplotrecorder`: https://github.com/AtsushiSakai/matplotrecorder +.. _`PythonRoboticsGifs`: https://github.com/AtsushiSakai/PythonRoboticsGifs From e6f5dfe537d97cf79d55ad2803911a135aeaa1fd Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sun, 2 Feb 2025 21:34:26 +0900 Subject: [PATCH 062/181] update getting started (#1136) Co-authored-by: Atsushi Sakai --- .../0_getting_started/3_how_to_contribute_main.rst | 2 +- .../0_getting_started/4_how_to_read_textbook_main.rst | 10 +++++++++- docs/modules/1_introduction/introduction_main.rst | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/modules/0_getting_started/3_how_to_contribute_main.rst b/docs/modules/0_getting_started/3_how_to_contribute_main.rst index 296e4e8658..6e5c1be8ee 100644 --- a/docs/modules/0_getting_started/3_how_to_contribute_main.rst +++ b/docs/modules/0_getting_started/3_how_to_contribute_main.rst @@ -1,4 +1,4 @@ -How To Contribute +How to contribute ================= This document describes how to contribute this project. diff --git a/docs/modules/0_getting_started/4_how_to_read_textbook_main.rst b/docs/modules/0_getting_started/4_how_to_read_textbook_main.rst index fb8db348c6..1625c838af 100644 --- a/docs/modules/0_getting_started/4_how_to_read_textbook_main.rst +++ b/docs/modules/0_getting_started/4_how_to_read_textbook_main.rst @@ -3,4 +3,12 @@ How to read this textbook -------------------------- -TBD \ No newline at end of file +This document is structured to help you learn the fundamental concepts +behind each sample code in PythonRobotics. + +If you already have some knowledge of robotics technologies, you can start +by reading any document that interests you. + +However, if you have no prior knowledge of robotics technologies, it is +recommended that you first read the :ref:`Introduction` section and then proceed +to the documents related to the technical fields that interest you. \ No newline at end of file diff --git a/docs/modules/1_introduction/introduction_main.rst b/docs/modules/1_introduction/introduction_main.rst index 4561efd349..ec1f237545 100644 --- a/docs/modules/1_introduction/introduction_main.rst +++ b/docs/modules/1_introduction/introduction_main.rst @@ -1,4 +1,4 @@ -.. _introduction: +.. _Introduction: Introduction ============ From 70269fe9601b6189914a739c4c0d42b598c3425b Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Mon, 3 Feb 2025 22:19:10 +0900 Subject: [PATCH 063/181] update getting started (#1138) --- .../python_for_robotics_main.rst} | 5 +---- docs/modules/1_introduction/introduction_main.rst | 7 ++++++- 2 files changed, 7 insertions(+), 5 deletions(-) rename docs/modules/1_introduction/{2_software_for_robotics/software_for_robotics_main.rst => 2_python_for_robotics/python_for_robotics_main.rst} (51%) diff --git a/docs/modules/1_introduction/2_software_for_robotics/software_for_robotics_main.rst b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst similarity index 51% rename from docs/modules/1_introduction/2_software_for_robotics/software_for_robotics_main.rst rename to docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst index 835441f85d..23f31da779 100644 --- a/docs/modules/1_introduction/2_software_for_robotics/software_for_robotics_main.rst +++ b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst @@ -1,7 +1,4 @@ -Software for Robotics ----------------------- - Python for Robotics -~~~~~~~~~~~~~~~~~~~~~ +---------------------- TBD diff --git a/docs/modules/1_introduction/introduction_main.rst b/docs/modules/1_introduction/introduction_main.rst index ec1f237545..a7ce55f9bf 100644 --- a/docs/modules/1_introduction/introduction_main.rst +++ b/docs/modules/1_introduction/introduction_main.rst @@ -3,11 +3,16 @@ Introduction ============ +PythonRobotics is composed of two words: "Python" and "Robotics". +Therefore, I will first explain these two topics, Robotics and Python. +After that, I will provide an overview of the robotics technologies +covered in PythonRobotics. + .. toctree:: :maxdepth: 2 :caption: Table of Contents 1_definition_of_robotics/definition_of_robotics - 2_software_for_robotics/software_for_robotics + 2_python_for_robotics/python_for_robotics 3_technology_for_robotics/technology_for_robotics From 5b06435be9e24a73c8ee9d0f0acb1e409a118141 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 07:37:17 +0900 Subject: [PATCH 064/181] build(deps): bump ruff from 0.9.3 to 0.9.4 in /requirements (#1139) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.3 to 0.9.4. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.3...0.9.4) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e8bed7b6d7..9d4e7deb4d 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.14.1 # For unit test -ruff == 0.9.3 # For unit test +ruff == 0.9.4 # For unit test From 7b7bd784093e4d46108b667c54261d054894e875 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Wed, 5 Feb 2025 13:41:28 +0900 Subject: [PATCH 065/181] update introduction (#1141) --- .../python_for_robotics_main.rst | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst index 23f31da779..1ad5316f53 100644 --- a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst +++ b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst @@ -1,4 +1,50 @@ Python for Robotics ---------------------- +Python for general-purpose programming +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +`Python `_ is an general-purpose programming language developed by +`Guido van Rossum `_ in the late 1980s. + +It features as follows: + +#. High-level +#. Interpreted +#. Dynamic type system (also type annotation is supported) +#. Emphasizes code readability +#. Rapid prototyping +#. Batteries included +#. Interoperability for C and Fortran + +Due to these features, Python is the most popular programming language +for educational purposes for programming beginners. + +Python for Scientific Computing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Python itself was not designed for scientific computing. +However, scientists quickly recognized its strengths. +For example, + +#. High-level and interpreted features enable scientists to focus on their problems without dealing with low-level programming tasks like memory management. +#. Code readability, rapid prototyping, and batteries included features enables scientists who are not professional programmers, to solve their problems easily. +#. The interoperability to wrap C and Fortran libraries enables scientists to access already existed powerful scientific computing libraries. + +To address the more needs of scientific computing, many libraries have been developed. + +- `NumPy `_ is the fundamental package for scientific computing with Python. +- `SciPy `_ is a library that builds on NumPy and provides a large number of functions that operate on NumPy arrays and are useful for different types of scientific and engineering applications. +- `Matplotlib `_ is a plotting library for the Python programming language and its numerical mathematics extension NumPy. +- `Pandas `_ is a fast, powerful, flexible, and easy-to-use open-source data analysis and data manipulation library built on top of NumPy. +- `SymPy `_ is a Python library for symbolic mathematics. + +And more domain-specific libraries have been developed: +- `Scikit-learn `_ is a free software machine learning library for the Python programming language. +- `Scikit-image `_ is a collection of algorithms for image processing. + +Python for Robotics +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + TBD + From 322fead45a62417c5d0d7dbb07ef17067122da7b Mon Sep 17 00:00:00 2001 From: Aglargil <34728006+Aglargil@users.noreply.github.com> Date: Wed, 5 Feb 2025 20:56:13 +0800 Subject: [PATCH 066/181] feat: add DistanceMap (#1142) * feat: add DistanceMap * feat: add DistanceMap test * feat: add DistanceMap doc * feat: DistanceMap doc update * feat: DistanceMap update --- Mapping/DistanceMap/distance_map.py | 151 ++++++++++++++++++ .../3_mapping/distance_map/distance_map.png | Bin 0 -> 32698 bytes .../distance_map/distance_map_main.rst | 27 ++++ docs/modules/3_mapping/mapping_main.rst | 1 + tests/test_distance_map.py | 118 ++++++++++++++ 5 files changed, 297 insertions(+) create mode 100644 Mapping/DistanceMap/distance_map.py create mode 100644 docs/modules/3_mapping/distance_map/distance_map.png create mode 100644 docs/modules/3_mapping/distance_map/distance_map_main.rst create mode 100644 tests/test_distance_map.py diff --git a/Mapping/DistanceMap/distance_map.py b/Mapping/DistanceMap/distance_map.py new file mode 100644 index 0000000000..54c98c6a75 --- /dev/null +++ b/Mapping/DistanceMap/distance_map.py @@ -0,0 +1,151 @@ +""" +Distance Map + +author: Wang Zheng (@Aglargil) + +Ref: + +- [Distance Map] +(https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf) +""" + +import numpy as np +import matplotlib.pyplot as plt + +INF = 1e20 +ENABLE_PLOT = True + + +def compute_sdf(obstacles): + """ + Compute the signed distance field (SDF) from a boolean field. + + Parameters + ---------- + obstacles : array_like + A 2D boolean array where '1' represents obstacles and '0' represents free space. + + Returns + ------- + array_like + A 2D array representing the signed distance field, where positive values indicate distance + to the nearest obstacle, and negative values indicate distance to the nearest free space. + """ + a = compute_udf(obstacles) + b = compute_udf(obstacles == 0) + return a - b + + +def compute_udf(obstacles): + """ + Compute the unsigned distance field (UDF) from a boolean field. + + Parameters + ---------- + obstacles : array_like + A 2D boolean array where '1' represents obstacles and '0' represents free space. + + Returns + ------- + array_like + A 2D array of distances from the nearest obstacle, with the same dimensions as `bool_field`. + """ + edt = obstacles.copy() + if not np.all(np.isin(edt, [0, 1])): + raise ValueError("Input array should only contain 0 and 1") + edt = np.where(edt == 0, INF, edt) + edt = np.where(edt == 1, 0, edt) + for row in range(len(edt)): + dt(edt[row]) + edt = edt.T + for row in range(len(edt)): + dt(edt[row]) + edt = edt.T + return np.sqrt(edt) + + +def dt(d): + """ + Compute 1D distance transform under the squared Euclidean distance + + Parameters + ---------- + d : array_like + Input array containing the distances. + + Returns: + -------- + d : array_like + The transformed array with computed distances. + """ + v = np.zeros(len(d) + 1) + z = np.zeros(len(d) + 1) + k = 0 + v[0] = 0 + z[0] = -INF + z[1] = INF + for q in range(1, len(d)): + s = ((d[q] + q * q) - (d[int(v[k])] + v[k] * v[k])) / (2 * q - 2 * v[k]) + while s <= z[k]: + k = k - 1 + s = ((d[q] + q * q) - (d[int(v[k])] + v[k] * v[k])) / (2 * q - 2 * v[k]) + k = k + 1 + v[k] = q + z[k] = s + z[k + 1] = INF + k = 0 + for q in range(len(d)): + while z[k + 1] < q: + k = k + 1 + dx = q - v[k] + d[q] = dx * dx + d[int(v[k])] + + +def main(): + obstacles = np.array( + [ + [1, 0, 0, 0, 0], + [0, 1, 1, 1, 0], + [0, 1, 1, 1, 0], + [0, 0, 1, 1, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 0], + ] + ) + + # Compute the signed distance field + sdf = compute_sdf(obstacles) + udf = compute_udf(obstacles) + + if ENABLE_PLOT: + _, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(15, 5)) + + obstacles_plot = ax1.imshow(obstacles, cmap="binary") + ax1.set_title("Obstacles") + ax1.set_xlabel("x") + ax1.set_ylabel("y") + plt.colorbar(obstacles_plot, ax=ax1) + + udf_plot = ax2.imshow(udf, cmap="viridis") + ax2.set_title("Unsigned Distance Field") + ax2.set_xlabel("x") + ax2.set_ylabel("y") + plt.colorbar(udf_plot, ax=ax2) + + sdf_plot = ax3.imshow(sdf, cmap="RdBu") + ax3.set_title("Signed Distance Field") + ax3.set_xlabel("x") + ax3.set_ylabel("y") + plt.colorbar(sdf_plot, ax=ax3) + + plt.tight_layout() + plt.show() + + +if __name__ == "__main__": + main() diff --git a/docs/modules/3_mapping/distance_map/distance_map.png b/docs/modules/3_mapping/distance_map/distance_map.png new file mode 100644 index 0000000000000000000000000000000000000000..2d89252a70358ad1b60025d7bb3d9a217a2cefcc GIT binary patch literal 32698 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 literal 0 HcmV?d00001 diff --git a/docs/modules/3_mapping/distance_map/distance_map_main.rst b/docs/modules/3_mapping/distance_map/distance_map_main.rst new file mode 100644 index 0000000000..45273cd3bb --- /dev/null +++ b/docs/modules/3_mapping/distance_map/distance_map_main.rst @@ -0,0 +1,27 @@ +Distance Map +------------ + +This is an implementation of the Distance Map algorithm for path planning. + +The Distance Map algorithm computes the unsigned distance field (UDF) and signed distance field (SDF) from a boolean field representing obstacles. + +The UDF gives the distance from each point to the nearest obstacle. The SDF gives positive distances for points outside obstacles and negative distances for points inside obstacles. + +Example +~~~~~~~ + +The algorithm is demonstrated on a simple 2D grid with obstacles: + +.. image:: distance_map.png + +API +~~~ + +.. autofunction:: PathPlanning.DistanceMap.distance_map.compute_sdf + +.. autofunction:: PathPlanning.DistanceMap.distance_map.compute_udf + +References +~~~~~~~~~~ + +- `Distance Transforms of Sampled Functions `_ paper by Pedro F. Felzenszwalb and Daniel P. Huttenlocher. \ No newline at end of file diff --git a/docs/modules/3_mapping/mapping_main.rst b/docs/modules/3_mapping/mapping_main.rst index 28e18984d3..825b08d3ec 100644 --- a/docs/modules/3_mapping/mapping_main.rst +++ b/docs/modules/3_mapping/mapping_main.rst @@ -17,3 +17,4 @@ Mapping is the ability of a robot to understand its surroundings with external s circle_fitting/circle_fitting rectangle_fitting/rectangle_fitting normal_vector_estimation/normal_vector_estimation + distance_map/distance_map diff --git a/tests/test_distance_map.py b/tests/test_distance_map.py new file mode 100644 index 0000000000..df6e394e2c --- /dev/null +++ b/tests/test_distance_map.py @@ -0,0 +1,118 @@ +import conftest # noqa +import numpy as np +from Mapping.DistanceMap import distance_map as m + + +def test_compute_sdf(): + """Test the computation of Signed Distance Field (SDF)""" + # Create a simple obstacle map for testing + obstacles = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]) + + sdf = m.compute_sdf(obstacles) + + # Verify basic properties of SDF + assert sdf.shape == obstacles.shape, "SDF should have the same shape as input map" + assert np.all(np.isfinite(sdf)), "SDF should not contain infinite values" + + # Verify SDF value is negative at obstacle position + assert sdf[1, 1] < 0, "SDF value should be negative at obstacle position" + + # Verify SDF value is positive in free space + assert sdf[0, 0] > 0, "SDF value should be positive in free space" + + +def test_compute_udf(): + """Test the computation of Unsigned Distance Field (UDF)""" + # Create obstacle map for testing + obstacles = np.array([[0, 0, 0], [0, 1, 0], [0, 0, 0]]) + + udf = m.compute_udf(obstacles) + + # Verify basic properties of UDF + assert udf.shape == obstacles.shape, "UDF should have the same shape as input map" + assert np.all(np.isfinite(udf)), "UDF should not contain infinite values" + assert np.all(udf >= 0), "All UDF values should be non-negative" + + # Verify UDF value is 0 at obstacle position + assert np.abs(udf[1, 1]) < 1e-10, "UDF value should be 0 at obstacle position" + + # Verify UDF value is 1 for adjacent cells + assert np.abs(udf[0, 1] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + assert np.abs(udf[1, 0] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + assert np.abs(udf[1, 2] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + assert np.abs(udf[2, 1] - 1.0) < 1e-10, ( + "UDF value should be 1 for cells adjacent to obstacle" + ) + + +def test_dt(): + """Test the computation of 1D distance transform""" + # Create test data + d = np.array([m.INF, 0, m.INF]) + m.dt(d) + + # Verify distance transform results + assert np.all(np.isfinite(d)), ( + "Distance transform result should not contain infinite values" + ) + assert d[1] == 0, "Distance at obstacle position should be 0" + assert d[0] == 1, "Distance at adjacent position should be 1" + assert d[2] == 1, "Distance at adjacent position should be 1" + + +def test_compute_sdf_empty(): + """Test SDF computation with empty map""" + # Test with empty map (no obstacles) + empty_map = np.zeros((5, 5)) + sdf = m.compute_sdf(empty_map) + + assert np.all(sdf > 0), "All SDF values should be positive for empty map" + assert sdf.shape == empty_map.shape, "Output shape should match input shape" + + +def test_compute_sdf_full(): + """Test SDF computation with fully occupied map""" + # Test with fully occupied map + full_map = np.ones((5, 5)) + sdf = m.compute_sdf(full_map) + + assert np.all(sdf < 0), "All SDF values should be negative for fully occupied map" + assert sdf.shape == full_map.shape, "Output shape should match input shape" + + +def test_compute_udf_invalid_input(): + """Test UDF computation with invalid input values""" + # Test with invalid values (not 0 or 1) + invalid_map = np.array([[0, 2, 0], [0, -1, 0], [0, 0.5, 0]]) + + try: + m.compute_udf(invalid_map) + assert False, "Should raise ValueError for invalid input values" + except ValueError: + pass + + +def test_compute_udf_empty(): + """Test UDF computation with empty map""" + # Test with empty map + empty_map = np.zeros((5, 5)) + udf = m.compute_udf(empty_map) + + assert np.all(udf > 0), "All UDF values should be positive for empty map" + assert np.all(np.isfinite(udf)), "UDF should not contain infinite values" + + +def test_main(): + """Test the execution of main function""" + m.ENABLE_PLOT = False + m.main() + + +if __name__ == "__main__": + conftest.run_this_test(__file__) From 2234abf63d07e5496e37c1b57a1b927c303272a8 Mon Sep 17 00:00:00 2001 From: Aglargil <34728006+Aglargil@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:05:20 +0800 Subject: [PATCH 067/181] fix: DistanceMap doc autofunction (#1143) --- docs/modules/3_mapping/distance_map/distance_map_main.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modules/3_mapping/distance_map/distance_map_main.rst b/docs/modules/3_mapping/distance_map/distance_map_main.rst index 45273cd3bb..0ef9e3022f 100644 --- a/docs/modules/3_mapping/distance_map/distance_map_main.rst +++ b/docs/modules/3_mapping/distance_map/distance_map_main.rst @@ -17,9 +17,9 @@ The algorithm is demonstrated on a simple 2D grid with obstacles: API ~~~ -.. autofunction:: PathPlanning.DistanceMap.distance_map.compute_sdf +.. autofunction:: Mapping.DistanceMap.distance_map.compute_sdf -.. autofunction:: PathPlanning.DistanceMap.distance_map.compute_udf +.. autofunction:: Mapping.DistanceMap.distance_map.compute_udf References ~~~~~~~~~~ From 0676dfd67e099198c34c485e176b077ad6fa7374 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Thu, 6 Feb 2025 15:16:17 +0900 Subject: [PATCH 068/181] update introduction (#1144) --- .../python_for_robotics_main.rst | 57 +++++++++++++++++-- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst index 1ad5316f53..2f89f0c7b5 100644 --- a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst +++ b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst @@ -1,11 +1,13 @@ Python for Robotics ---------------------- +This section explains the Python itself and features for Robotics. + Python for general-purpose programming ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Python `_ is an general-purpose programming language developed by -`Guido van Rossum `_ in the late 1980s. +`Guido van Rossum `_ from the late 1980s. It features as follows: @@ -17,7 +19,7 @@ It features as follows: #. Batteries included #. Interoperability for C and Fortran -Due to these features, Python is the most popular programming language +Due to these features, Python is one of the most popular programming language for educational purposes for programming beginners. Python for Scientific Computing @@ -29,9 +31,9 @@ For example, #. High-level and interpreted features enable scientists to focus on their problems without dealing with low-level programming tasks like memory management. #. Code readability, rapid prototyping, and batteries included features enables scientists who are not professional programmers, to solve their problems easily. -#. The interoperability to wrap C and Fortran libraries enables scientists to access already existed powerful scientific computing libraries. +#. The interoperability to wrap C and Fortran libraries enables scientists to access already existed powerful and optimized scientific computing libraries. -To address the more needs of scientific computing, many libraries have been developed. +To address the more needs of scientific computing, many fundamental scientific computation libraries have been developed based on the upper features. - `NumPy `_ is the fundamental package for scientific computing with Python. - `SciPy `_ is a library that builds on NumPy and provides a large number of functions that operate on NumPy arrays and are useful for different types of scientific and engineering applications. @@ -39,12 +41,55 @@ To address the more needs of scientific computing, many libraries have been deve - `Pandas `_ is a fast, powerful, flexible, and easy-to-use open-source data analysis and data manipulation library built on top of NumPy. - `SymPy `_ is a Python library for symbolic mathematics. -And more domain-specific libraries have been developed: +Also, more domain-specific libraries have been developed based on these fundamental libraries: + - `Scikit-learn `_ is a free software machine learning library for the Python programming language. - `Scikit-image `_ is a collection of algorithms for image processing. +- `Networkx `_ is a Python package for the creation, manipulation, and study of the structure, dynamics, and functions of complex networks. +- `SunPy `_ is a community-developed free and open-source software package for solar physics. +- `Astropy `_ is a community-developed free and open-source software package for astronomy. + +Currently, Python is one of the most popular programming languages for scientific computing. Python for Robotics ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -TBD +Scientific computation routine are very important for robotics. +For example, matrix operation, optimization, and visualization are fundamental for robotics. + +Python has become an increasingly popular language in robotics due to its versatility, readability, and extensive libraries. Here's a breakdown of why Python is a great choice for robotics development: + +Advantages of Python for Robotics: + +Simplicity and Readability: Python's syntax is clear and concise, making it easier to learn and write code. This is crucial in robotics where complex algorithms and control logic are involved. +Extensive Libraries: Python boasts a rich collection of libraries specifically designed for robotics: +ROS (Robot Operating System): ROS, a widely used framework for robotics development, has strong Python support (rospy). This allows developers to easily create nodes, manage communication between different parts of a robot system, and utilize various ROS tools. +OpenCV: This powerful library provides tools for computer vision tasks like image processing, object detection, and motion tracking, essential for robots that perceive and interact with their environment. +NumPy and SciPy: These libraries offer efficient numerical computation and scientific tools, enabling developers to implement complex mathematical models and control algorithms. +Scikit-learn: This library provides machine learning algorithms, which are increasingly important in robotics for tasks like perception, planning, and control. +Cross-Platform Compatibility: Python code can run on various operating systems (Windows, macOS, Linux), providing flexibility in choosing hardware platforms for robotics projects. +Large Community and Support: Python has a vast and active community, offering ample resources, tutorials, and support for developers. This is invaluable when tackling challenges in robotics development. +Use Cases of Python in Robotics: + +Robot Control: Python can be used to write control algorithms for robot manipulators, mobile robots, and other robotic systems. +Perception: Python, combined with libraries like OpenCV, enables robots to process sensor data (camera images, lidar data) to understand their surroundings. +Path Planning: Python algorithms can be used to plan collision-free paths for robots to navigate in complex environments. +Machine Learning: Python libraries like Scikit-learn empower robots to learn from data and improve their performance in tasks like object recognition and manipulation. +Simulation: Python can be used to create simulated environments for testing and developing robot algorithms before deploying them on real hardware. +Examples of Python in Robotics: + +Autonomous Navigation: Python is used in self-driving cars and other autonomous vehicles for tasks like perception, localization, and path planning. +Industrial Robotics: Python is employed in manufacturing for robot control, quality inspection, and automation. +Service Robotics: Python powers robots that perform tasks like cleaning, delivery, and customer service in various environments. +Research and Education: Python is a popular choice in robotics research and education due to its ease of use and versatility. +Getting Started with Python in Robotics: + +Learn Python Basics: Familiarize yourself with Python syntax, data structures, and programming concepts. +Explore Robotics Libraries: Dive into libraries like ROS, OpenCV, and others relevant to your robotics interests. +Practice with Projects: Start with small projects to gain hands-on experience, gradually increasing complexity. +Join the Community: Engage with the robotics community through online forums, workshops, and conferences to learn from others and share your knowledge. +In conclusion, Python's simplicity, extensive libraries, and strong community support make it an ideal language for robotics development. Whether you're a beginner or an experienced programmer, Python offers the tools and resources you need to build innovative and capable robots. + +Python is used for this `PythonRobotics` project because of the above features +to achieve the purpose of this project described in the :ref:`What is PythonRobotics?`. From 9936f344635e146b9a2ee402ab1672b4e7216d6e Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Fri, 7 Feb 2025 13:32:11 +0900 Subject: [PATCH 069/181] update introduction (#1145) --- .../definition_of_robotics_main.rst | 7 +++ .../python_for_robotics_main.rst | 63 +++++++++---------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index 2f31834017..fd151e3f20 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -5,3 +5,10 @@ In recent years, autonomous navigation technologies have received huge attention in many fields. Such fields include, autonomous driving[22], drone flight navigation, and other transportation systems. + +Examples of Python in Robotics: + +Autonomous Navigation: Python is used in self-driving cars and other autonomous vehicles for tasks like perception, localization, and path planning. +Industrial Robotics: Python is employed in manufacturing for robot control, quality inspection, and automation. +Service Robotics: Python powers robots that perform tasks like cleaning, delivery, and customer service in various environments. +Research and Education: Python is a popular choice in robotics research and education due to its ease of use and versatility. \ No newline at end of file diff --git a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst index 2f89f0c7b5..90edd5dc0c 100644 --- a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst +++ b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst @@ -40,55 +40,52 @@ To address the more needs of scientific computing, many fundamental scientific c - `Matplotlib `_ is a plotting library for the Python programming language and its numerical mathematics extension NumPy. - `Pandas `_ is a fast, powerful, flexible, and easy-to-use open-source data analysis and data manipulation library built on top of NumPy. - `SymPy `_ is a Python library for symbolic mathematics. +- `CVXPy `_ is a Python-embedded modeling language for convex optimization problems. Also, more domain-specific libraries have been developed based on these fundamental libraries: - `Scikit-learn `_ is a free software machine learning library for the Python programming language. - `Scikit-image `_ is a collection of algorithms for image processing. -- `Networkx `_ is a Python package for the creation, manipulation, and study of the structure, dynamics, and functions of complex networks. +- `Networkx `_ is a package for the creation, manipulation for complex networks. - `SunPy `_ is a community-developed free and open-source software package for solar physics. - `Astropy `_ is a community-developed free and open-source software package for astronomy. Currently, Python is one of the most popular programming languages for scientific computing. Python for Robotics -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +^^^^^^^^^^^^^^^^^^^^ -Scientific computation routine are very important for robotics. -For example, matrix operation, optimization, and visualization are fundamental for robotics. +Python has become an increasingly popular language in robotics. -Python has become an increasingly popular language in robotics due to its versatility, readability, and extensive libraries. Here's a breakdown of why Python is a great choice for robotics development: +These are advantages of Python for Robotics: -Advantages of Python for Robotics: +Simplicity and Readability +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Python's syntax is clear and concise, making it easier to learn and write code. +This is crucial in robotics where complex algorithms and control logic are involved. -Simplicity and Readability: Python's syntax is clear and concise, making it easier to learn and write code. This is crucial in robotics where complex algorithms and control logic are involved. -Extensive Libraries: Python boasts a rich collection of libraries specifically designed for robotics: + +Extensive libraries for scientific computation. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Scientific computation routine are fundamental for robotics. +For example: + +- Matrix operation is needed for rigid body transformation, state estimation, and model based control. +- Optimization is needed for optimization based SLAM, optimal path planning, and optimal control. +- Visualization is needed for robot teleoperation, debugging, and simulation. + +ROS supports Python +~~~~~~~~~~~~~~~~~~~~~~~~~~~ ROS (Robot Operating System): ROS, a widely used framework for robotics development, has strong Python support (rospy). This allows developers to easily create nodes, manage communication between different parts of a robot system, and utilize various ROS tools. -OpenCV: This powerful library provides tools for computer vision tasks like image processing, object detection, and motion tracking, essential for robots that perceive and interact with their environment. -NumPy and SciPy: These libraries offer efficient numerical computation and scientific tools, enabling developers to implement complex mathematical models and control algorithms. -Scikit-learn: This library provides machine learning algorithms, which are increasingly important in robotics for tasks like perception, planning, and control. -Cross-Platform Compatibility: Python code can run on various operating systems (Windows, macOS, Linux), providing flexibility in choosing hardware platforms for robotics projects. -Large Community and Support: Python has a vast and active community, offering ample resources, tutorials, and support for developers. This is invaluable when tackling challenges in robotics development. -Use Cases of Python in Robotics: - -Robot Control: Python can be used to write control algorithms for robot manipulators, mobile robots, and other robotic systems. -Perception: Python, combined with libraries like OpenCV, enables robots to process sensor data (camera images, lidar data) to understand their surroundings. -Path Planning: Python algorithms can be used to plan collision-free paths for robots to navigate in complex environments. -Machine Learning: Python libraries like Scikit-learn empower robots to learn from data and improve their performance in tasks like object recognition and manipulation. -Simulation: Python can be used to create simulated environments for testing and developing robot algorithms before deploying them on real hardware. -Examples of Python in Robotics: - -Autonomous Navigation: Python is used in self-driving cars and other autonomous vehicles for tasks like perception, localization, and path planning. -Industrial Robotics: Python is employed in manufacturing for robot control, quality inspection, and automation. -Service Robotics: Python powers robots that perform tasks like cleaning, delivery, and customer service in various environments. -Research and Education: Python is a popular choice in robotics research and education due to its ease of use and versatility. -Getting Started with Python in Robotics: - -Learn Python Basics: Familiarize yourself with Python syntax, data structures, and programming concepts. -Explore Robotics Libraries: Dive into libraries like ROS, OpenCV, and others relevant to your robotics interests. -Practice with Projects: Start with small projects to gain hands-on experience, gradually increasing complexity. -Join the Community: Engage with the robotics community through online forums, workshops, and conferences to learn from others and share your knowledge. -In conclusion, Python's simplicity, extensive libraries, and strong community support make it an ideal language for robotics development. Whether you're a beginner or an experienced programmer, Python offers the tools and resources you need to build innovative and capable robots. + +Cross-Platform Compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Python code can run on various operating systems (Windows, macOS, Linux), providing flexibility in choosing hardware platforms for robotics projects. + +Large Community and Support +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Python has a vast and active community, offering ample resources, tutorials, and support for developers. This is invaluable when tackling challenges in robotics development. + Python is used for this `PythonRobotics` project because of the above features to achieve the purpose of this project described in the :ref:`What is PythonRobotics?`. From 15e106839285033e1fa68cbed6f14e219f3af88f Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Fri, 7 Feb 2025 13:42:56 +0900 Subject: [PATCH 070/181] Update CONTRIBUTING.md --- CONTRIBUTING.md | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91f6dfa822..3bcc499e6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,23 +1,5 @@ -# Contributing to Python +# Contributing -:+1::tada: First of off, thanks very much for taking the time to contribute! :tada::+1: +:+1::tada: First of all, thank you very much for taking the time to contribute! :tada::+1: -The following is a set of guidelines for contributing to PythonRobotics. - -These are mostly guidelines, not rules. - -Use your best judgment, and feel free to propose changes to this document in a pull request. - -# Before contributing - -## Taking a look at the paper. - -Please check this paper to understand the philosophy of this project. - -- [\[1808\.10703\] PythonRobotics: a Python code collection of robotics algorithms](https://arxiv.org/abs/1808.10703) ([BibTeX](https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib)) - -## Check your Python version. - -We only accept a PR for Python 3.8.x or higher. - -We will not accept a PR for Python 2.x. +Please check this document for contribution: [How to contribute — PythonRobotics documentation](https://atsushisakai.github.io/PythonRobotics/modules/0_getting_started/3_how_to_contribute.html) From a8f3388bbe80e41655acedc44c2cbb86d011fb48 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sun, 9 Feb 2025 21:00:02 +0900 Subject: [PATCH 071/181] update introduction (#1146) --- .../3_how_to_contribute_main.rst | 37 ++++++++++++++----- .../python_for_robotics_main.rst | 29 ++++++++++++--- 2 files changed, 51 insertions(+), 15 deletions(-) diff --git a/docs/modules/0_getting_started/3_how_to_contribute_main.rst b/docs/modules/0_getting_started/3_how_to_contribute_main.rst index 6e5c1be8ee..874564cbb8 100644 --- a/docs/modules/0_getting_started/3_how_to_contribute_main.rst +++ b/docs/modules/0_getting_started/3_how_to_contribute_main.rst @@ -9,10 +9,30 @@ There are several ways to contribute to this project as below: #. `Adding missed documentations for existing examples`_ #. `Supporting this project`_ +Before contributing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Please check following items before contributing: + +Understanding this project +--------------------------- + +Please check this :ref:`What is PythonRobotics?` section and this paper +`PythonRobotics: a Python code collection of robotics algorithms`_ +to understand the philosophies of this project. + +.. _`PythonRobotics: a Python code collection of robotics algorithms`: https://arxiv.org/abs/1808.10703 + +Check your Python version. +--------------------------- + +We only accept a PR for Python 3.12.x or higher. + +We will not accept a PR for Python 2.x. .. _`Adding a new algorithm example`: -Adding a new algorithm example +1. Adding a new algorithm example ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is a step by step manual to add a new algorithm example. @@ -112,8 +132,8 @@ Note that this is my hobby project; I appreciate your patience during the review .. _`Reporting and fixing a defect`: -Reporting and fixing a defect -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +2. Reporting and fixing a defect +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Reporting and fixing a defect is also great contribution. @@ -136,8 +156,8 @@ This doc `submit a pull request`_ can be helpful to submit a pull request. .. _`Adding missed documentations for existing examples`: -Adding missed documentations for existing examples -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +3. Adding missed documentations for existing examples +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Adding the missed documentations for existing examples is also great contribution. @@ -150,8 +170,8 @@ This doc `how to write doc`_ can be helpful to write documents. .. _`Supporting this project`: -Supporting this project -^^^^^^^^^^^^^^^^^^^^^^^^ +4. Supporting this project +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Supporting this project financially is also a great contribution!!. @@ -165,8 +185,7 @@ If you or your company would like to support this project, please consider: If you would like to support us in some other way, please contact with creating an issue. -Current Major Sponsors ------------------------ +Current Major Sponsors: #. `JetBrains`_ : They are providing a free license of their IDEs for this OSS development. #. `1Password`_ : They are providing a free license of their 1Password team license for this OSS project. diff --git a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst index 90edd5dc0c..65b1705150 100644 --- a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst +++ b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst @@ -1,6 +1,8 @@ Python for Robotics ---------------------- +Python is used for this `PythonRobotics` project because of the above features +to achieve the purpose of this project described in the :ref:`What is PythonRobotics?`. This section explains the Python itself and features for Robotics. Python for general-purpose programming @@ -76,7 +78,27 @@ For example: ROS supports Python ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -ROS (Robot Operating System): ROS, a widely used framework for robotics development, has strong Python support (rospy). This allows developers to easily create nodes, manage communication between different parts of a robot system, and utilize various ROS tools. +`ROS`_ (Robot Operating System) is an open-source and widely used framework for robotics development. +It is designed to help developping complicated robotic applications. +ROS provides essential tools, libraries, and drivers to simplify robot programming and integration. + +Key Features of ROS: + +- Modular Architecture – Uses a node-based system where different components (nodes) communicate via messages. +- Hardware Abstraction – Supports various robots, sensors, and actuators, making development more flexible. +- Powerful Communication System – Uses topics, services, and actions for efficient data exchange between components. +- Rich Ecosystem – Offers many pre-built packages for navigation, perception, and manipulation. +- Multi-language Support – Primarily uses Python and C++, but also supports other languages. +- Simulation & Visualization – Tools like Gazebo (for simulation) and RViz (for visualization) aid in development and testing. +- Scalability & Community Support – Widely used in academia and industry, with a large open-source community. + +ROS has strong Python support (`rospy`_ for ROS1 and `rclpy`_ for ROS2). +This allows developers to easily create nodes, manage communication between +different parts of a robot system, and utilize various ROS tools. + +.. _`ROS`: https://www.ros.org/ +.. _`rospy`: http://wiki.ros.org/rospy +.. _`rclpy`: https://docs.ros.org/en/jazzy/Tutorials/Beginner-Client-Libraries/Writing-A-Simple-Py-Publisher-And-Subscriber.html Cross-Platform Compatibility ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -85,8 +107,3 @@ Python code can run on various operating systems (Windows, macOS, Linux), provid Large Community and Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Python has a vast and active community, offering ample resources, tutorials, and support for developers. This is invaluable when tackling challenges in robotics development. - - -Python is used for this `PythonRobotics` project because of the above features -to achieve the purpose of this project described in the :ref:`What is PythonRobotics?`. - From e304f07a997bf40977a004ac2a66f476aa2aa861 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Mon, 10 Feb 2025 11:21:19 +0900 Subject: [PATCH 072/181] update introduction (#1147) --- .../python_for_robotics_main.rst | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst index 65b1705150..b677d2c59b 100644 --- a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst +++ b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst @@ -1,9 +1,10 @@ Python for Robotics ---------------------- -Python is used for this `PythonRobotics` project because of the above features -to achieve the purpose of this project described in the :ref:`What is PythonRobotics?`. -This section explains the Python itself and features for Robotics. +A programing language, Python is used for this `PythonRobotics` project +to achieve the purposes of this project described in the :ref:`What is PythonRobotics?`. + +This section explains the Python itself and features for science computing Robotics. Python for general-purpose programming ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -107,3 +108,28 @@ Python code can run on various operating systems (Windows, macOS, Linux), provid Large Community and Support ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Python has a vast and active community, offering ample resources, tutorials, and support for developers. This is invaluable when tackling challenges in robotics development. + +Situations which Python is NOT suitable for Robotics +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +We explained the advantages of Python for robotics. +However, Python is not always the best choice for robotics development. + +These are situations where Python is NOT suitable for robotics: + +High-speed real-time control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Python is an interpreted language, which means it is slower than compiled languages like C++. +This can be a disadvantage when real-time control is required, +such as in high-speed motion control or safety-critical systems. + +So, for these applications, we recommend to understand the each algorithm you +needed using this project and implement it in other suitable languages like C++. + +Resource-constrained systems +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Python is a high-level language that requires more memory and processing power +compared to low-level languages. +So, it is difficult to run Python on resource-constrained systems like +microcontrollers or embedded devices. +In such cases, C or C++ is more suitable for these applications. From 610f35ff58e6535efa619f9ed73a113c6ca2c7f7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 10:42:10 +0900 Subject: [PATCH 073/181] build(deps): bump mypy from 1.14.1 to 1.15.0 in /requirements (#1148) Bumps [mypy](https://github.com/python/mypy) from 1.14.1 to 1.15.0. - [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md) - [Commits](https://github.com/python/mypy/compare/v1.14.1...v1.15.0) --- updated-dependencies: - dependency-name: mypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 9d4e7deb4d..178046ac0d 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -4,5 +4,5 @@ matplotlib == 3.10.0 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test -mypy == 1.14.1 # For unit test +mypy == 1.15.0 # For unit test ruff == 0.9.4 # For unit test From ba307673013376204ceb5a6def16da0e3a86a15d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 11 Feb 2025 11:35:28 +0900 Subject: [PATCH 074/181] build(deps): bump ruff from 0.9.4 to 0.9.6 in /requirements (#1149) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.4 to 0.9.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.4...0.9.6) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 178046ac0d..f5f674d7d2 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.9.4 # For unit test +ruff == 0.9.6 # For unit test From b298609b2832c8029baa98b2d3906abce0263ec4 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Tue, 11 Feb 2025 21:15:34 +0900 Subject: [PATCH 075/181] update introduction doc (#1151) --- .../definition_of_robotics_main.rst | 81 ++++++++++++++++--- .../python_for_robotics_main.rst | 2 +- .../technology_for_robotics_main.rst | 9 +++ 3 files changed, 82 insertions(+), 10 deletions(-) diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index fd151e3f20..f6fba646b4 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -1,14 +1,77 @@ Definition of Robotics ---------------------- -In recent years, autonomous navigation technologies have received huge -attention in many fields. -Such fields include, autonomous driving[22], drone flight navigation, -and other transportation systems. +What is Robotics? +^^^^^^^^^^^^^^^^^^ -Examples of Python in Robotics: +Robot is a machine that can perform tasks automatically or semi-autonomously. +Robotics is the study of robots. +The field of robotics has wide areas of technologies such as mechanical engineering, +electrical engineering, computer science, and artificial intelligence (AI), +to create machines that can perform tasks autonomously or semi-autonomously. + +The History of Robots +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This timeline highlights key milestones in the history of robotics: + +Ancient and Early Concepts (Before 1500s) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The idea of **automated machines** has existed for thousands of years. Ancient civilizations imagined mechanical beings: + +- **Ancient Greece (4th Century BC)** – Greek engineer **Hero of Alexandria** designed early **automata** (self-operating machines) powered by water or air. +- **Chinese and Arabic Automata (9th–13th Century)** – Inventors like **Al-Jazari** created intricate mechanical devices, including water clocks and humanoid robots. + +The Birth of Modern Robotics (1500s–1800s) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **Leonardo da Vinci’s Robot (1495)** – Designed a humanoid knight with mechanical movement. +- **Jacques de Vaucanson’s Automata (1738)** – Created robotic figures like a mechanical duck that could "eat" and "digest." +- **Industrial Revolution (18th–19th Century)** – Machines began replacing human labor in factories, setting the foundation for automation. + +The Rise of Industrial Robots (1900s–1950s) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **The Term “Robot” (1921)** – Czech writer **Karel Čapek** introduced the word *“robot”* in his play *R.U.R. (Rossum’s Universal Robots)*. +- **Early Cybernetics (1940s–1950s)** – Scientists like **Norbert Wiener** developed theories of self-regulating machines, influencing modern robotics. + +The Birth of Modern Robotics (1950s–1980s) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **First Industrial Robot (1961)** – *Unimate*, created by **George Devol and Joseph Engelberger**, was the first programmable robot used in a factory. +- **Rise of AI & Autonomous Robots (1970s–1980s)** – Researchers developed mobile robots like **Shakey** (Stanford, 1966) and AI-based control systems. + +Advanced Robotics and AI Integration (1990s–Present) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- **Autonomous Vehicles & Drones** – Self-driving cars and UAVs (unmanned aerial vehicles) became more advanced. +- **Medical Robotics** – Robots like **da Vinci Surgical System** revolutionized healthcare. +- **Personal Robots** – Devices like **Roomba** (vacuum robot) and **Sophia** (AI humanoid) became popular. +- **Collaborative Robots (Cobots)** – Robots started working alongside humans in industries. + +Key Components of Robotics +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Robotics consists of several essential components: + +#. Sensors – Gather information from the environment (e.g., cameras, LiDAR, gyro, accelerometer, wheel encoders). +#. Actuators – Enable movement and interaction with the world (e.g., motors, hydraulic systems). +#. Computers – Process sensor data and make decisions (e.g., micro-controllers, CPUs, GPUs). +#. Power Supply – Provides energy to run the robot (e.g., batteries, solar power). +#. Software & Algorithms – Allow the robot to function and make intelligent decisions (e.g., ROS, machine learning models, localization, mapping, path planning, control). + +This project, PythonRobotics, focuses on the software and algorithms part of robotics. + +Applications of Robots +^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Robots come in various forms depending on their purpose: + +#. 🤖 Industrial Robots – Used in manufacturing (e.g., robotic arms in manufacturing factories). +#. 🏠 Service Robots – Assist in daily life (e.g., vacuum robots, delivery robots). +#. 🚗 Autonomous Vehicles – Self-driving cars and drones. +#. 👨‍⚕️ Medical Robots – Assist in surgeries and healthcare. +#. 🚀 Space & Exploration Robots – Used for planetary exploration (e.g., NASA’s Mars rovers). +#. 🐶 Humanoid & Social Robots – Designed to interact with humans (e.g., ASIMO, Sophia). -Autonomous Navigation: Python is used in self-driving cars and other autonomous vehicles for tasks like perception, localization, and path planning. -Industrial Robotics: Python is employed in manufacturing for robot control, quality inspection, and automation. -Service Robotics: Python powers robots that perform tasks like cleaning, delivery, and customer service in various environments. -Research and Education: Python is a popular choice in robotics research and education due to its ease of use and versatility. \ No newline at end of file diff --git a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst index b677d2c59b..c47c122853 100644 --- a/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst +++ b/docs/modules/1_introduction/2_python_for_robotics/python_for_robotics_main.rst @@ -4,7 +4,7 @@ Python for Robotics A programing language, Python is used for this `PythonRobotics` project to achieve the purposes of this project described in the :ref:`What is PythonRobotics?`. -This section explains the Python itself and features for science computing Robotics. +This section explains the Python itself and features for science computing and robotics. Python for general-purpose programming ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst index 4dd1d1842f..93dc9e3466 100644 --- a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst +++ b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst @@ -1,6 +1,15 @@ Technology for Robotics ------------------------- + +Autonomous Navigation +^^^^^^^^^^^^^^^^^^^^^^^^ + +In recent years, autonomous navigation technologies have received huge +attention in many fields. +Such fields include, autonomous driving[22], drone flight navigation, +and other transportation systems. + An autonomous navigation system is a system that can move to a goal over long periods of time without any external control by an operator. The system requires a wide range of technologies: From be608f067cbd96f34955b81d3e5be9e22a46f588 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Wed, 12 Feb 2025 21:51:29 +0900 Subject: [PATCH 076/181] update introduction doc (#1152) --- .../definition_of_robotics_main.rst | 38 ++++++++++++++----- .../technology_for_robotics_main.rst | 3 ++ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index f6fba646b4..63525057fe 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -1,14 +1,24 @@ Definition of Robotics ---------------------- +This section explains the definition, history, key components, and applications of robotics. + What is Robotics? ^^^^^^^^^^^^^^^^^^ Robot is a machine that can perform tasks automatically or semi-autonomously. Robotics is the study of robots. -The field of robotics has wide areas of technologies such as mechanical engineering, -electrical engineering, computer science, and artificial intelligence (AI), -to create machines that can perform tasks autonomously or semi-autonomously. + +The word “robot” comes from the Czech word “robota,” which means “forced labor” or “drudgery.” +It was first used in the 1920 science fiction play `R.U.R.`_ (Rossum’s Universal Robots) +by the Czech writer `Karel Čapek`_. +In the play, robots were artificial workers created to serve humans, but they eventually rebelled. + +Over time, “robot” came to refer to machines or automated systems that can perform tasks, +often with some level of intelligence or autonomy. + +.. _`R.U.R.`: https://thereader.mitpress.mit.edu/origin-word-robot-rur/ +.. _`Karel Čapek`: https://en.wikipedia.org/wiki/Karel_%C4%8Capek The History of Robots ^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -18,22 +28,30 @@ This timeline highlights key milestones in the history of robotics: Ancient and Early Concepts (Before 1500s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The idea of **automated machines** has existed for thousands of years. Ancient civilizations imagined mechanical beings: +The idea of **automated machines** has existed for thousands of years. +Ancient civilizations imagined mechanical beings: + +- **Ancient Greece (4th Century BC)** – Greek engineer `Hero of Alexandria`_ designed early **automata** (self-operating machines) powered by water or air. +- **Chinese and Arabic Automata (9th–13th Century)** – Inventors like `Ismail Al-Jazari`_ created intricate mechanical devices, including water clocks and automated moving peacocks driven by hydropower. -- **Ancient Greece (4th Century BC)** – Greek engineer **Hero of Alexandria** designed early **automata** (self-operating machines) powered by water or air. -- **Chinese and Arabic Automata (9th–13th Century)** – Inventors like **Al-Jazari** created intricate mechanical devices, including water clocks and humanoid robots. +.. _`Hero of Alexandria`: https://en.wikipedia.org/wiki/Hero_of_Alexandria +.. _`Ismail Al-Jazari`: https://en.wikipedia.org/wiki/Ismail_al-Jazari The Birth of Modern Robotics (1500s–1800s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- **Leonardo da Vinci’s Robot (1495)** – Designed a humanoid knight with mechanical movement. -- **Jacques de Vaucanson’s Automata (1738)** – Created robotic figures like a mechanical duck that could "eat" and "digest." -- **Industrial Revolution (18th–19th Century)** – Machines began replacing human labor in factories, setting the foundation for automation. +- `Leonardo da Vinci’s Robot`_ (1495) – Designed a humanoid knight with mechanical movement. +- `Jacques de Vaucanson’s Digesting Duck`_ (1738) – Created robotic figures like a mechanical duck that could "eat" and "digest." +- `Industrial Revolution`_ (18th–19th Century) – Machines began replacing human labor in factories, setting the foundation for automation. + +.. _`Leonardo da Vinci’s Robot`: https://en.wikipedia.org/wiki/Leonardo%27s_robot +.. _`Jacques de Vaucanson’s Digesting Duck`: https://en.wikipedia.org/wiki/Jacques_de_Vaucanson +.. _`Industrial Revolution`: https://en.wikipedia.org/wiki/Industrial_Revolution The Rise of Industrial Robots (1900s–1950s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- **The Term “Robot” (1921)** – Czech writer **Karel Čapek** introduced the word *“robot”* in his play *R.U.R. (Rossum’s Universal Robots)*. +- **The Term “Robot” (1921)** – Czech writer `Karel Čapek`_ introduced the word *“robot”* in his play `R.U.R.`_ (Rossum’s Universal Robots). - **Early Cybernetics (1940s–1950s)** – Scientists like **Norbert Wiener** developed theories of self-regulating machines, influencing modern robotics. The Birth of Modern Robotics (1950s–1980s) diff --git a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst index 93dc9e3466..e460059e20 100644 --- a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst +++ b/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst @@ -1,6 +1,9 @@ Technology for Robotics ------------------------- +The field of robotics needs wide areas of technologies such as mechanical engineering, +electrical engineering, computer science, and artificial intelligence (AI). + Autonomous Navigation ^^^^^^^^^^^^^^^^^^^^^^^^ From 1ecc154fbaf9bc2342671fe93a305e8f0e3f510f Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Thu, 13 Feb 2025 17:19:03 +0900 Subject: [PATCH 077/181] update contribution link in README.md to fix invalid link (#1154) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cb1816c2b5..818d7b0d4e 100644 --- a/README.md +++ b/README.md @@ -617,7 +617,7 @@ This is a list of user's comment and references:[users\_comments](https://github Any contribution is welcome!! -Please check this document:[How To Contribute — PythonRobotics documentation](https://atsushisakai.github.io/PythonRobotics/how_to_contribute.html) +Please check this document:[How To Contribute — PythonRobotics documentation](https://atsushisakai.github.io/PythonRobotics/modules/0_getting_started/3_how_to_contribute.html) # Citing From 156483000524488d64687a853ac030e8d57c18a6 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Thu, 13 Feb 2025 17:56:17 +0900 Subject: [PATCH 078/181] update robotics definition document to enhance references and clarity (#1155) --- .../definition_of_robotics_main.rst | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index 63525057fe..265814e068 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -52,21 +52,36 @@ The Rise of Industrial Robots (1900s–1950s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - **The Term “Robot” (1921)** – Czech writer `Karel Čapek`_ introduced the word *“robot”* in his play `R.U.R.`_ (Rossum’s Universal Robots). -- **Early Cybernetics (1940s–1950s)** – Scientists like **Norbert Wiener** developed theories of self-regulating machines, influencing modern robotics. +- **Early Cybernetics (1940s–1950s)** – Scientists like `Norbert Wiener`_ developed theories of self-regulating machines, influencing modern robotics (Cybernetics). + +.. _`Norbert Wiener`: https://en.wikipedia.org/wiki/Norbert_Wiener The Birth of Modern Robotics (1950s–1980s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- **First Industrial Robot (1961)** – *Unimate*, created by **George Devol and Joseph Engelberger**, was the first programmable robot used in a factory. -- **Rise of AI & Autonomous Robots (1970s–1980s)** – Researchers developed mobile robots like **Shakey** (Stanford, 1966) and AI-based control systems. +- **First Industrial Robot (1961)** – `Unimate`_, created by `George Devol`_ and `Joseph Engelberger`_, was the first programmable robot used in a factory. +- **Rise of AI & Autonomous Robots (1970s–1980s)** – Researchers developed mobile robots like `Shakey`_ (Stanford, 1966) and AI-based control systems. + +.. _`Unimate`: https://en.wikipedia.org/wiki/Unimate +.. _`George Devol`: https://en.wikipedia.org/wiki/George_Devol +.. _`Joseph Engelberger`: https://en.wikipedia.org/wiki/Joseph_Engelberger +.. _`Shakey`: https://en.wikipedia.org/wiki/Shakey_the_robot Advanced Robotics and AI Integration (1990s–Present) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -- **Autonomous Vehicles & Drones** – Self-driving cars and UAVs (unmanned aerial vehicles) became more advanced. -- **Medical Robotics** – Robots like **da Vinci Surgical System** revolutionized healthcare. -- **Personal Robots** – Devices like **Roomba** (vacuum robot) and **Sophia** (AI humanoid) became popular. -- **Collaborative Robots (Cobots)** – Robots started working alongside humans in industries. +- **Autonomous Vehicles** – Self-driving cars for robo taxi like `Waymo`_ and autonomous haulage system in mining like `AHS`_ became more advanced and bisiness-ready. +- **Medical Robotics** – Robots like `da Vinci Surgical System`_ revolutionized healthcare. +- **Personal Robots** – Devices like `Roomba`_ (vacuum robot) and `Sophia`_ (AI humanoid) became popular. +- **Service Robots** - Assistive robots like serving robots in restaurants and hotels like `Bellabot`_. +- **Collaborative Robots (Drones)** – Collaborative robots like UAV (Unmanned Aerial Vehicle) in drone shows and delivery services. + +.. _`Waymo`: https://waymo.com/ +.. _`AHS`: https://www.futurebridge.com/industry/perspectives-industrial-manufacturing/autonomous-haulage-systems-the-future-of-mining-operations/ +.. _`da Vinci Surgical System`: https://en.wikipedia.org/wiki/Da_Vinci_Surgical_System +.. _`Roomba`: https://en.wikipedia.org/wiki/Roomba +.. _`Sophia`: https://en.wikipedia.org/wiki/Sophia_(robot) +.. _`Bellabot`: https://www.pudurobotics.com/en Key Components of Robotics ^^^^^^^^^^^^^^^^^^^^^^^^^^^ From 77ad3344b5e9df26ca370725d3921dd354c755b1 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Fri, 14 Feb 2025 21:20:35 +0900 Subject: [PATCH 079/181] update robotics definition document to improve clarity and add references (#1157) --- .../definition_of_robotics_main.rst | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index 265814e068..f54d4d41fa 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -70,12 +70,17 @@ The Birth of Modern Robotics (1950s–1980s) Advanced Robotics and AI Integration (1990s–Present) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- **Industrial Robots** – Advanced robots like `Baxter`_ and `Amazon Robotics`_ revolutionized manufacturing and logistics. - **Autonomous Vehicles** – Self-driving cars for robo taxi like `Waymo`_ and autonomous haulage system in mining like `AHS`_ became more advanced and bisiness-ready. +- **Exploration Robots** – Used for planetary exploration (e.g., NASA’s `Mars rovers`_). - **Medical Robotics** – Robots like `da Vinci Surgical System`_ revolutionized healthcare. - **Personal Robots** – Devices like `Roomba`_ (vacuum robot) and `Sophia`_ (AI humanoid) became popular. - **Service Robots** - Assistive robots like serving robots in restaurants and hotels like `Bellabot`_. - **Collaborative Robots (Drones)** – Collaborative robots like UAV (Unmanned Aerial Vehicle) in drone shows and delivery services. +.. _`Baxter`: https://en.wikipedia.org/wiki/Baxter_(robot) +.. _`Amazon Robotics`: https://en.wikipedia.org/wiki/Amazon_Robotics +.. _`Mars rovers`: https://en.wikipedia.org/wiki/Mars_rover .. _`Waymo`: https://waymo.com/ .. _`AHS`: https://www.futurebridge.com/industry/perspectives-industrial-manufacturing/autonomous-haulage-systems-the-future-of-mining-operations/ .. _`da Vinci Surgical System`: https://en.wikipedia.org/wiki/Da_Vinci_Surgical_System @@ -88,23 +93,10 @@ Key Components of Robotics Robotics consists of several essential components: -#. Sensors – Gather information from the environment (e.g., cameras, LiDAR, gyro, accelerometer, wheel encoders). -#. Actuators – Enable movement and interaction with the world (e.g., motors, hydraulic systems). -#. Computers – Process sensor data and make decisions (e.g., micro-controllers, CPUs, GPUs). -#. Power Supply – Provides energy to run the robot (e.g., batteries, solar power). -#. Software & Algorithms – Allow the robot to function and make intelligent decisions (e.g., ROS, machine learning models, localization, mapping, path planning, control). +#. Sensors – Gather information from the environment (e.g., Cameras, LiDAR, GNSS, Gyro, Accelerometer, Wheel encoders). +#. Actuators – Enable movement and interaction with the world (e.g., Motors, Hydraulic systems). +#. Computers – Process sensor data and make decisions (e.g., Micro-controllers, CPUs, GPUs). +#. Power Supply – Provides energy to run the robot (e.g., Batteries, Solar power). +#. Software & Algorithms – Allow the robot to function and make intelligent decisions (e.g., ROS, Machine learning models, Localization, Mapping, Path planning, Control). This project, PythonRobotics, focuses on the software and algorithms part of robotics. - -Applications of Robots -^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -Robots come in various forms depending on their purpose: - -#. 🤖 Industrial Robots – Used in manufacturing (e.g., robotic arms in manufacturing factories). -#. 🏠 Service Robots – Assist in daily life (e.g., vacuum robots, delivery robots). -#. 🚗 Autonomous Vehicles – Self-driving cars and drones. -#. 👨‍⚕️ Medical Robots – Assist in surgeries and healthcare. -#. 🚀 Space & Exploration Robots – Used for planetary exploration (e.g., NASA’s Mars rovers). -#. 🐶 Humanoid & Social Robots – Designed to interact with humans (e.g., ASIMO, Sophia). - From 35c08824d00bd9fb452d1ee951018b177e65fd00 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sat, 15 Feb 2025 16:08:23 +0900 Subject: [PATCH 080/181] add external sensors documentation to appendix (#1159) --- docs/modules/12_appendix/appendix_main.rst | 1 + .../12_appendix/external_sensors_main.rst | 56 +++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 docs/modules/12_appendix/external_sensors_main.rst diff --git a/docs/modules/12_appendix/appendix_main.rst b/docs/modules/12_appendix/appendix_main.rst index cb1ac04066..89a7fa9303 100644 --- a/docs/modules/12_appendix/appendix_main.rst +++ b/docs/modules/12_appendix/appendix_main.rst @@ -10,4 +10,5 @@ Appendix steering_motion_model Kalmanfilter_basics Kalmanfilter_basics_2 + external_sensors diff --git a/docs/modules/12_appendix/external_sensors_main.rst b/docs/modules/12_appendix/external_sensors_main.rst new file mode 100644 index 0000000000..a1dba3b214 --- /dev/null +++ b/docs/modules/12_appendix/external_sensors_main.rst @@ -0,0 +1,56 @@ +External Sensors for Robots +============================ + +Introduction +------------ + +In recent years, the application of robotic technology has advanced, +particularly in areas such as autonomous vehicles and disaster response robots. +A crucial element in these technologies is external recognition—the robot's ability to understand its surrounding environment, identify safe zones, and detect moving objects using onboard sensors. Achieving effective external recognition involves various techniques, but equally important is the selection of appropriate sensors. Robots, like the sensors they employ, come in many forms, but external recognition sensors can be broadly categorized into three types. Developing an advanced external recognition system requires a thorough understanding of each sensor's principles and characteristics to determine their optimal application. This article summarizes the principles and features of these sensors for personal study purposes. + +Laser Sensors +------------- + +Laser sensors measure distances by utilizing light, commonly referred to as Light Detection and Ranging (LIDAR). They operate by emitting light towards an object and calculating the distance based on the time it takes for the reflected light to return, using the speed of light as a constant. + +Radar Sensors +------------- + +TBD + + +Monocular Cameras +----------------- + +Monocular cameras utilize a single camera to recognize the external environment. Compared to other sensors, they can detect color and brightness information, making them primarily useful for object recognition. However, they face challenges in independently measuring distances to surrounding objects and may struggle in low-light or dark conditions. + +Requirements for Cameras and Image Processing in Robotics +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While camera sensors are widely used in applications like surveillance, deploying them in robotics necessitates meeting specific requirements: + +1. High dynamic range to adapt to various lighting conditions +2. Wide measurement range +3. Capability for three-dimensional measurement through techniques like motion stereo +4. Real-time processing with high frame rates +5. Cost-effectiveness + +Stereo Cameras +-------------- + +Stereo cameras employ multiple cameras to measure distances to surrounding objects. By knowing the positions and orientations of each camera and analyzing the disparity in the images (parallax), the distance to a specific point (the object represented by a particular pixel) can be calculated. + +Characteristics of Stereo Cameras +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Advantages of stereo cameras include the ability to obtain high-precision and high-density distance information at close range, depending on factors like camera resolution and the distance between cameras (baseline). This makes them suitable for indoor robots that require precise shape recognition of nearby objects. Additionally, stereo cameras are relatively cost-effective compared to other sensors, leading to their use in consumer products like Subaru's EyeSight system. However, stereo cameras are less effective for long-distance measurements due to a decrease in accuracy proportional to the square of the distance. They are also susceptible to environmental factors such as lighting conditions. + +Ultrasonic Sensors +------------------ + +Ultrasonic sensors are commonly used in indoor robots and some automotive autonomous driving systems. Their features include affordability compared to laser or radar sensors, the ability to detect very close objects, and the capability to sense materials like glass, which may be challenging for lasers or cameras. However, they have limitations such as shorter maximum measurement distances and lower resolution and accuracy. + +References +---------- + +TBD From e82a12319b5336beaff01ab9fd2ce7818bb08dfb Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sun, 16 Feb 2025 21:41:22 +0900 Subject: [PATCH 081/181] add internal sensors documentation to appendix and create new internal sensors overview (#1161) --- docs/modules/12_appendix/appendix_main.rst | 1 + .../12_appendix/external_sensors_main.rst | 4 +++ .../12_appendix/internal_sensors_main.rst | 32 +++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 docs/modules/12_appendix/internal_sensors_main.rst diff --git a/docs/modules/12_appendix/appendix_main.rst b/docs/modules/12_appendix/appendix_main.rst index 89a7fa9303..a55389e1e6 100644 --- a/docs/modules/12_appendix/appendix_main.rst +++ b/docs/modules/12_appendix/appendix_main.rst @@ -10,5 +10,6 @@ Appendix steering_motion_model Kalmanfilter_basics Kalmanfilter_basics_2 + internal_sensors external_sensors diff --git a/docs/modules/12_appendix/external_sensors_main.rst b/docs/modules/12_appendix/external_sensors_main.rst index a1dba3b214..d36b852d42 100644 --- a/docs/modules/12_appendix/external_sensors_main.rst +++ b/docs/modules/12_appendix/external_sensors_main.rst @@ -1,6 +1,10 @@ External Sensors for Robots ============================ +This project, `PythonRobotics`, focuses on algorithms, so hardware is not included. +However, having basic knowledge of hardware in robotics is also important for understanding algorithms. +Therefore, we will provide an overview. + Introduction ------------ diff --git a/docs/modules/12_appendix/internal_sensors_main.rst b/docs/modules/12_appendix/internal_sensors_main.rst new file mode 100644 index 0000000000..13b8de4203 --- /dev/null +++ b/docs/modules/12_appendix/internal_sensors_main.rst @@ -0,0 +1,32 @@ +Internal Sensors for Robots +============================ + +This project, `PythonRobotics`, focuses on algorithms, so hardware is not included. +However, having basic knowledge of hardware in robotics is also important for understanding algorithms. +Therefore, we will provide an overview. + +Introduction +------------ + +Global Navigation Satellite System (GNSS) +------------------------------------------- + +Gyroscope +---------- + +Accelerometer +-------------- + +Magnetometer +-------------- + +Inertial Measurement Unit (IMU) +-------------------------------- + +Pressure Sensor +----------------- + +Temperature Sensor +-------------------- + + From c92aaf36d8665a3b9ecd1a661f151cfd6ede4c66 Mon Sep 17 00:00:00 2001 From: Aglargil <34728006+Aglargil@users.noreply.github.com> Date: Mon, 17 Feb 2025 18:47:04 +0800 Subject: [PATCH 082/181] feat: add ElasticBands (#1156) * feat: add ElasticBands * feat: Elastic Bands update * feat: ElasticBands update * feat: ElasticBands add test * feat: ElasticBands reduce occupation * fix: ElasticBands test * feat: ElasticBands remove tangential component * feat: Elastic Bands update * feat: Elastic Bands doc * feat: Elastic Bands update * feat: ElasticBands update --- Mapping/DistanceMap/distance_map.py | 51 +++ PathPlanning/ElasticBands/elastic_bands.py | 300 ++++++++++++++++++ PathPlanning/ElasticBands/obstacles.npy | Bin 0 -> 384 bytes PathPlanning/ElasticBands/path.npy | Bin 0 -> 224 bytes .../elastic_bands/elastic_bands_main.rst | 73 +++++ .../5_path_planning/path_planning_main.rst | 1 + tests/test_elastic_bands.py | 23 ++ 7 files changed, 448 insertions(+) create mode 100644 PathPlanning/ElasticBands/elastic_bands.py create mode 100644 PathPlanning/ElasticBands/obstacles.npy create mode 100644 PathPlanning/ElasticBands/path.npy create mode 100644 docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst create mode 100644 tests/test_elastic_bands.py diff --git a/Mapping/DistanceMap/distance_map.py b/Mapping/DistanceMap/distance_map.py index 54c98c6a75..0dcc7380c5 100644 --- a/Mapping/DistanceMap/distance_map.py +++ b/Mapping/DistanceMap/distance_map.py @@ -11,11 +11,62 @@ import numpy as np import matplotlib.pyplot as plt +import scipy INF = 1e20 ENABLE_PLOT = True +def compute_sdf_scipy(obstacles): + """ + Compute the signed distance field (SDF) from a boolean field using scipy. + This function has the same functionality as compute_sdf. + However, by using scipy.ndimage.distance_transform_edt, it can compute much faster. + + Example: 500×500 map + • compute_sdf: 3 sec + • compute_sdf_scipy: 0.05 sec + + Parameters + ---------- + obstacles : array_like + A 2D boolean array where '1' represents obstacles and '0' represents free space. + + Returns + ------- + array_like + A 2D array representing the signed distance field, where positive values indicate distance + to the nearest obstacle, and negative values indicate distance to the nearest free space. + """ + # distance_transform_edt use '0' as obstacles, so we need to convert the obstacles to '0' + a = scipy.ndimage.distance_transform_edt(obstacles == 0) + b = scipy.ndimage.distance_transform_edt(obstacles == 1) + return a - b + + +def compute_udf_scipy(obstacles): + """ + Compute the unsigned distance field (UDF) from a boolean field using scipy. + This function has the same functionality as compute_udf. + However, by using scipy.ndimage.distance_transform_edt, it can compute much faster. + + Example: 500×500 map + • compute_udf: 1.5 sec + • compute_udf_scipy: 0.02 sec + + Parameters + ---------- + obstacles : array_like + A 2D boolean array where '1' represents obstacles and '0' represents free space. + + Returns + ------- + array_like + A 2D array of distances from the nearest obstacle, with the same dimensions as `bool_field`. + """ + return scipy.ndimage.distance_transform_edt(obstacles == 0) + + def compute_sdf(obstacles): """ Compute the signed distance field (SDF) from a boolean field. diff --git a/PathPlanning/ElasticBands/elastic_bands.py b/PathPlanning/ElasticBands/elastic_bands.py new file mode 100644 index 0000000000..785f822d14 --- /dev/null +++ b/PathPlanning/ElasticBands/elastic_bands.py @@ -0,0 +1,300 @@ +""" +Elastic Bands + +author: Wang Zheng (@Aglargil) + +Ref: + +- [Elastic Bands: Connecting Path Planning and Control] +(http://www8.cs.umu.se/research/ifor/dl/Control/elastic%20bands.pdf) +""" + +import numpy as np +import sys +import pathlib +import matplotlib.pyplot as plt +from matplotlib.patches import Circle + +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) + +from Mapping.DistanceMap.distance_map import compute_sdf_scipy + +# Elastic Bands Params +MAX_BUBBLE_RADIUS = 100 +MIN_BUBBLE_RADIUS = 10 +RHO0 = 20.0 # Maximum distance for applying repulsive force +KC = 0.05 # Contraction force gain +KR = -0.1 # Repulsive force gain +LAMBDA = 0.7 # Overlap constraint factor +STEP_SIZE = 3.0 # Step size for calculating gradient + +# Visualization Params +ENABLE_PLOT = True +# ENABLE_INTERACTIVE is True allows user to add obstacles by left clicking +# and add path points by right clicking and start planning by middle clicking +ENABLE_INTERACTIVE = False +# ENABLE_SAVE_DATA is True allows saving the path and obstacles which added +# by user in interactive mode to file +ENABLE_SAVE_DATA = False +MAX_ITER = 50 + + +class Bubble: + def __init__(self, position, radius): + self.pos = np.array(position) # Bubble center coordinates [x, y] + self.radius = radius # Safety distance radius ρ(b) + if self.radius > MAX_BUBBLE_RADIUS: + self.radius = MAX_BUBBLE_RADIUS + if self.radius < MIN_BUBBLE_RADIUS: + self.radius = MIN_BUBBLE_RADIUS + + +class ElasticBands: + def __init__( + self, + initial_path, + obstacles, + rho0=RHO0, + kc=KC, + kr=KR, + lambda_=LAMBDA, + step_size=STEP_SIZE, + ): + self.distance_map = compute_sdf_scipy(obstacles) + self.bubbles = [ + Bubble(p, self.compute_rho(p)) for p in initial_path + ] # Initialize bubble chain + self.kc = kc # Contraction force gain + self.kr = kr # Repulsive force gain + self.rho0 = rho0 # Maximum distance for applying repulsive force + self.lambda_ = lambda_ # Overlap constraint factor + self.step_size = step_size # Step size for calculating gradient + self._maintain_overlap() + + def compute_rho(self, position): + """Compute the distance field value at the position""" + return self.distance_map[int(position[0]), int(position[1])] + + def contraction_force(self, i): + """Calculate internal contraction force for the i-th bubble""" + if i == 0 or i == len(self.bubbles) - 1: + return np.zeros(2) + + prev = self.bubbles[i - 1].pos + next_ = self.bubbles[i + 1].pos + current = self.bubbles[i].pos + + # f_c = kc * ( (prev-current)/|prev-current| + (next-current)/|next-current| ) + dir_prev = (prev - current) / (np.linalg.norm(prev - current) + 1e-6) + dir_next = (next_ - current) / (np.linalg.norm(next_ - current) + 1e-6) + return self.kc * (dir_prev + dir_next) + + def repulsive_force(self, i): + """Calculate external repulsive force for the i-th bubble""" + h = self.step_size # Step size + b = self.bubbles[i].pos + rho = self.bubbles[i].radius + + if rho >= self.rho0: + return np.zeros(2) + + # Finite difference approximation of the gradient ∂ρ/∂b + dx = np.array([h, 0]) + dy = np.array([0, h]) + grad_x = (self.compute_rho(b - dx) - self.compute_rho(b + dx)) / (2 * h) + grad_y = (self.compute_rho(b - dy) - self.compute_rho(b + dy)) / (2 * h) + grad = np.array([grad_x, grad_y]) + + return self.kr * (self.rho0 - rho) * grad + + def update_bubbles(self): + """Update bubble positions""" + new_bubbles = [] + for i in range(len(self.bubbles)): + if i == 0 or i == len(self.bubbles) - 1: + new_bubbles.append(self.bubbles[i]) # Fixed start and end points + continue + + f_total = self.contraction_force(i) + self.repulsive_force(i) + v = self.bubbles[i - 1].pos - self.bubbles[i + 1].pos + + # Remove tangential component + f_star = f_total - f_total * v * v / (np.linalg.norm(v) ** 2 + 1e-6) + + alpha = self.bubbles[i].radius # Adaptive step size + new_pos = self.bubbles[i].pos + alpha * f_star + new_pos = np.clip(new_pos, 0, 499) + new_radius = self.compute_rho(new_pos) + + # Update bubble and maintain overlap constraint + new_bubble = Bubble(new_pos, new_radius) + new_bubbles.append(new_bubble) + + self.bubbles = new_bubbles + self._maintain_overlap() + + def _maintain_overlap(self): + """Maintain bubble chain continuity (simplified insertion/deletion mechanism)""" + # Insert bubbles + i = 0 + while i < len(self.bubbles) - 1: + bi, bj = self.bubbles[i], self.bubbles[i + 1] + dist = np.linalg.norm(bi.pos - bj.pos) + if dist > self.lambda_ * (bi.radius + bj.radius): + new_pos = (bi.pos + bj.pos) / 2 + rho = self.compute_rho( + new_pos + ) # Calculate new radius using environment model + self.bubbles.insert(i + 1, Bubble(new_pos, rho)) + i += 2 # Skip the processed region + else: + i += 1 + + # Delete redundant bubbles + i = 1 + while i < len(self.bubbles) - 1: + prev = self.bubbles[i - 1] + next_ = self.bubbles[i + 1] + dist = np.linalg.norm(prev.pos - next_.pos) + if dist <= self.lambda_ * (prev.radius + next_.radius): + del self.bubbles[i] # Delete if redundant + else: + i += 1 + + +class ElasticBandsVisualizer: + def __init__(self): + self.obstacles = np.zeros((500, 500)) + self.obstacles_points = [] + self.path_points = [] + self.elastic_band = None + self.running = True + + if ENABLE_PLOT: + self.fig, self.ax = plt.subplots(figsize=(8, 8)) + self.fig.canvas.mpl_connect("close_event", self.on_close) + self.ax.set_xlim(0, 500) + self.ax.set_ylim(0, 500) + + if ENABLE_INTERACTIVE: + self.path_points = [] # Add a list to store path points + # Connect mouse events + self.fig.canvas.mpl_connect("button_press_event", self.on_click) + else: + self.path_points = np.load(pathlib.Path(__file__).parent / "path.npy") + self.obstacles_points = np.load( + pathlib.Path(__file__).parent / "obstacles.npy" + ) + for x, y in self.obstacles_points: + self.add_obstacle(x, y) + self.plan_path() + + self.plot_background() + + def on_close(self, event): + """Handle window close event""" + self.running = False + plt.close("all") # Close all figure windows + + def plot_background(self): + """Plot the background grid""" + if not ENABLE_PLOT or not self.running: + return + + self.ax.cla() + self.ax.set_xlim(0, 500) + self.ax.set_ylim(0, 500) + self.ax.grid(True) + + if ENABLE_INTERACTIVE: + self.ax.set_title( + "Elastic Bands Path Planning\n" + "Left click: Add obstacles\n" + "Right click: Add path points\n" + "Middle click: Start planning", + pad=20, + ) + else: + self.ax.set_title("Elastic Bands Path Planning", pad=20) + + if self.path_points: + self.ax.plot( + [p[0] for p in self.path_points], + [p[1] for p in self.path_points], + "yo", + markersize=8, + ) + + self.ax.imshow(self.obstacles.T, origin="lower", cmap="binary", alpha=0.8) + self.ax.plot([], [], color="black", label="obstacles") + if self.elastic_band is not None: + path = [b.pos.tolist() for b in self.elastic_band.bubbles] + path = np.array(path) + self.ax.plot(path[:, 0], path[:, 1], "b-", linewidth=2, label="path") + + for bubble in self.elastic_band.bubbles: + circle = Circle( + bubble.pos, bubble.radius, fill=False, color="g", alpha=0.3 + ) + self.ax.add_patch(circle) + self.ax.plot(bubble.pos[0], bubble.pos[1], "bo", markersize=10) + self.ax.plot([], [], color="green", label="bubbles") + + self.ax.legend(loc="upper right") + plt.draw() + plt.pause(0.01) + + def add_obstacle(self, x, y): + """Add an obstacle at the given coordinates""" + size = 30 # Side length of the square + half_size = size // 2 + x_start = max(0, x - half_size) + x_end = min(self.obstacles.shape[0], x + half_size) + y_start = max(0, y - half_size) + y_end = min(self.obstacles.shape[1], y + half_size) + self.obstacles[x_start:x_end, y_start:y_end] = 1 + + def on_click(self, event): + """Handle mouse click events""" + if event.inaxes != self.ax: + return + + x, y = int(event.xdata), int(event.ydata) + + if event.button == 1: # Left click to add obstacles + self.add_obstacle(x, y) + self.obstacles_points.append([x, y]) + + elif event.button == 3: # Right click to add path points + self.path_points.append([x, y]) + + elif event.button == 2: # Middle click to end path input and start planning + if len(self.path_points) >= 2: + if ENABLE_SAVE_DATA: + np.save( + pathlib.Path(__file__).parent / "path.npy", self.path_points + ) + np.save( + pathlib.Path(__file__).parent / "obstacles.npy", + self.obstacles_points, + ) + self.plan_path() + + self.plot_background() + + def plan_path(self): + """Plan the path""" + + initial_path = self.path_points + # Create an elastic band object and optimize + self.elastic_band = ElasticBands(initial_path, self.obstacles) + for _ in range(MAX_ITER): + self.elastic_band.update_bubbles() + self.path_points = [b.pos for b in self.elastic_band.bubbles] + self.plot_background() + + +if __name__ == "__main__": + _ = ElasticBandsVisualizer() + if ENABLE_PLOT: + plt.show(block=True) diff --git a/PathPlanning/ElasticBands/obstacles.npy b/PathPlanning/ElasticBands/obstacles.npy new file mode 100644 index 0000000000000000000000000000000000000000..af4376afcf0e987bbb62c4a80c7afac3d221961d GIT binary patch literal 384 zcmbR27wQ`j$;eQ~P_3SlTAW;@Zl$1ZlWC!@qoAIaUsO_*m=~X4l#&V(cT3DEP6dh= zXCxM+0{I$-W;zN+nmP)#3giN=d=|8_M@a;}<~r z%~1IiD1RoD9|z^LL+NCwxEhq72<3}I`IS(%lCm`7X literal 0 HcmV?d00001 diff --git a/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst b/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst new file mode 100644 index 0000000000..139996f291 --- /dev/null +++ b/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst @@ -0,0 +1,73 @@ +Elastic Bands +------------- + +This is a path planning with Elastic Bands. + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/ElasticBands/animation.gif + + +Core Concept +~~~~~~~~~~~~ +- **Elastic Band**: A dynamically deformable collision-free path initialized by a global planner. +- **Objective**: + + * Shorten and smooth the path. + * Maximize obstacle clearance. + * Maintain global path connectivity. + +Bubble Representation +~~~~~~~~~~~~~~~~~~~ +- **Definition**: A local free-space region around configuration :math:`b`: + + .. math:: + B(b) = \{ q: \|q - b\| < \rho(b) \}, + + where :math:`\rho(b)` is the radius of the bubble. + + +Force-Based Deformation +~~~~~~~~~~~~~~~~~~~~~~~ +The elastic band deforms under artificial forces: + +Internal Contraction Force +++++++++++++++++++++++++++ +- **Purpose**: Reduces path slack and length. +- **Formula**: For node :math:`b_i`: + + .. math:: + f_c(b_i) = k_c \left( \frac{b_{i-1} - b_i}{\|b_{i-1} - b_i\|} + \frac{b_{i+1} - b_i}{\|b_{i+1} - b_i\|} \right) + + where :math:`k_c` is the contraction gain. + +External Repulsion Force ++++++++++++++++++++++++++ +- **Purpose**: Pushes the path away from obstacles. +- **Formula**: For node :math:`b_i`: + + .. math:: + f_r(b_i) = \begin{cases} + k_r (\rho_0 - \rho(b_i)) \nabla \rho(b_i) & \text{if } \rho(b_i) < \rho_0, \\ + 0 & \text{otherwise}. + \end{cases} + + where :math:`k_r` is the repulsion gain, :math:`\rho_0` is the maximum distance for applying repulsion force, and :math:`\nabla \rho(b_i)` is approximated via finite differences: + + .. math:: + \frac{\partial \rho}{\partial x} \approx \frac{\rho(b_i + h) - \rho(b_i - h)}{2h}. + +Dynamic Path Maintenance +~~~~~~~~~~~~~~~~~~~~~~~ +1. **Node Update**: + + .. math:: + b_i^{\text{new}} = b_i^{\text{old}} + \alpha (f_c + f_r), + + where :math:`\alpha` is a step-size parameter, which often proportional to :math:`\rho(b_i^{\text{old}})` + +2. **Overlap Enforcement**: +- Insert new nodes if adjacent nodes are too far apart +- Remove redundant nodes if adjacent nodes are too close + +Ref: + +- `Elastic Bands: Connecting Path Planning and Control `__ diff --git a/docs/modules/5_path_planning/path_planning_main.rst b/docs/modules/5_path_planning/path_planning_main.rst index 4960330b3e..65fbfdbc3d 100644 --- a/docs/modules/5_path_planning/path_planning_main.rst +++ b/docs/modules/5_path_planning/path_planning_main.rst @@ -31,3 +31,4 @@ Path planning is the ability of a robot to search feasible and efficient path to hybridastar/hybridastar frenet_frame_path/frenet_frame_path coverage_path/coverage_path + elastic_bands/elastic_bands \ No newline at end of file diff --git a/tests/test_elastic_bands.py b/tests/test_elastic_bands.py new file mode 100644 index 0000000000..ad4e13af1a --- /dev/null +++ b/tests/test_elastic_bands.py @@ -0,0 +1,23 @@ +import conftest +import numpy as np +from PathPlanning.ElasticBands.elastic_bands import ElasticBands + + +def test_1(): + path = np.load("PathPlanning/ElasticBands/path.npy") + obstacles_points = np.load("PathPlanning/ElasticBands/obstacles.npy") + obstacles = np.zeros((500, 500)) + for x, y in obstacles_points: + size = 30 # Side length of the square + half_size = size // 2 + x_start = max(0, x - half_size) + x_end = min(obstacles.shape[0], x + half_size) + y_start = max(0, y - half_size) + y_end = min(obstacles.shape[1], y + half_size) + obstacles[x_start:x_end, y_start:y_end] = 1 + elastic_bands = ElasticBands(path, obstacles) + elastic_bands.update_bubbles() + + +if __name__ == "__main__": + conftest.run_this_test(__file__) From cbe61f8ca62785a072652741d8b33832caee1942 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:45:49 +0900 Subject: [PATCH 083/181] build(deps): bump scipy from 1.15.1 to 1.15.2 in /requirements (#1163) Bumps [scipy](https://github.com/scipy/scipy) from 1.15.1 to 1.15.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.15.1...v1.15.2) --- updated-dependencies: - dependency-name: scipy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f5f674d7d2..ea4550edb8 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,5 +1,5 @@ numpy == 2.2.2 -scipy == 1.15.1 +scipy == 1.15.2 matplotlib == 3.10.0 cvxpy == 1.5.3 pytest == 8.3.4 # For unit test From 395fca59cc1f9c44e9ea7f7c08ce41958e399481 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Tue, 18 Feb 2025 20:06:34 +0900 Subject: [PATCH 084/181] fix: update robotics documentation for clarity and correct terminology (#1165) --- docs/modules/12_appendix/external_sensors_main.rst | 2 ++ docs/modules/12_appendix/internal_sensors_main.rst | 2 ++ .../1_definition_of_robotics/definition_of_robotics_main.rst | 5 +++++ .../technologies_for_robotics_main.rst} | 4 +++- docs/modules/1_introduction/introduction_main.rst | 2 +- 5 files changed, 13 insertions(+), 2 deletions(-) rename docs/modules/1_introduction/{3_technology_for_robotics/technology_for_robotics_main.rst => 3_technologies_for_robotics/technologies_for_robotics_main.rst} (88%) diff --git a/docs/modules/12_appendix/external_sensors_main.rst b/docs/modules/12_appendix/external_sensors_main.rst index d36b852d42..3597418150 100644 --- a/docs/modules/12_appendix/external_sensors_main.rst +++ b/docs/modules/12_appendix/external_sensors_main.rst @@ -1,3 +1,5 @@ +.. _`External Sensors for Robots`: + External Sensors for Robots ============================ diff --git a/docs/modules/12_appendix/internal_sensors_main.rst b/docs/modules/12_appendix/internal_sensors_main.rst index 13b8de4203..18f209098e 100644 --- a/docs/modules/12_appendix/internal_sensors_main.rst +++ b/docs/modules/12_appendix/internal_sensors_main.rst @@ -1,3 +1,5 @@ +.. _`Internal Sensors for Robots`: + Internal Sensors for Robots ============================ diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index f54d4d41fa..1e7a833b19 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -17,6 +17,10 @@ In the play, robots were artificial workers created to serve humans, but they ev Over time, “robot” came to refer to machines or automated systems that can perform tasks, often with some level of intelligence or autonomy. +Currently, 2 millions robots are working in the world, and the number is increasing every year. +In South Korea, where the adoption of robots is particularly rapid, +50 robots are operating per 1,000 people. + .. _`R.U.R.`: https://thereader.mitpress.mit.edu/origin-word-robot-rur/ .. _`Karel Čapek`: https://en.wikipedia.org/wiki/Karel_%C4%8Capek @@ -100,3 +104,4 @@ Robotics consists of several essential components: #. Software & Algorithms – Allow the robot to function and make intelligent decisions (e.g., ROS, Machine learning models, Localization, Mapping, Path planning, Control). This project, PythonRobotics, focuses on the software and algorithms part of robotics. +If you are interested in `Sensors` hardware, you can check :ref:`Internal Sensors for Robotics`_ or :ref:`External Sensors for Robotics`_. diff --git a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst b/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst similarity index 88% rename from docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst rename to docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst index e460059e20..64c6945c32 100644 --- a/docs/modules/1_introduction/3_technology_for_robotics/technology_for_robotics_main.rst +++ b/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst @@ -1,8 +1,10 @@ -Technology for Robotics +Technologies for Robotics ------------------------- The field of robotics needs wide areas of technologies such as mechanical engineering, electrical engineering, computer science, and artificial intelligence (AI). +This project, `PythonRobotics`, only focus on computer science and artificial intelligence. + Autonomous Navigation diff --git a/docs/modules/1_introduction/introduction_main.rst b/docs/modules/1_introduction/introduction_main.rst index a7ce55f9bf..1871dfc3b1 100644 --- a/docs/modules/1_introduction/introduction_main.rst +++ b/docs/modules/1_introduction/introduction_main.rst @@ -14,5 +14,5 @@ covered in PythonRobotics. 1_definition_of_robotics/definition_of_robotics 2_python_for_robotics/python_for_robotics - 3_technology_for_robotics/technology_for_robotics + 3_technologies_for_robotics/technologies_for_robotics From d53711998aeb1d0b3aab7a62862ef5d358f2aff9 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Tue, 18 Feb 2025 21:49:28 +0900 Subject: [PATCH 085/181] fix: update robotics documentation for clarity and correct terminology (#1166) --- README.md | 4 ++++ .../0_getting_started/1_what_is_python_robotics_main.rst | 6 ++++++ .../definition_of_robotics_main.rst | 2 +- .../5_path_planning/elastic_bands/elastic_bands_main.rst | 7 ++++--- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 818d7b0d4e..9e605435ce 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,10 @@ See this documentation - [Getting Started — PythonRobotics documentation](https://atsushisakai.github.io/PythonRobotics/getting_started.html#what-is-pythonrobotics) +or this Youtube video: + +- [PythonRobotics project audio overview](https://www.youtube.com/watch?v=uMeRnNoJAfU) + or this paper for more details: - [\[1808\.10703\] PythonRobotics: a Python code collection of robotics algorithms](https://arxiv.org/abs/1808.10703) ([BibTeX](https://github.com/AtsushiSakai/PythonRoboticsPaper/blob/master/python_robotics.bib)) diff --git a/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst b/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst index 8c932b7263..2a7bd574f0 100644 --- a/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst +++ b/docs/modules/0_getting_started/1_what_is_python_robotics_main.rst @@ -110,6 +110,12 @@ the following additional libraries are required. For instructions on installing the above libraries, please refer to this section ":ref:`How to run sample codes`". +Audio overview of this project +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +For an audio overview of this project, please refer to this `YouTube video`_. + +.. _`YouTube video`: https://www.youtube.com/watch?v=uMeRnNoJAfU + Arxiv paper ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index 1e7a833b19..5d78bf858b 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -103,5 +103,5 @@ Robotics consists of several essential components: #. Power Supply – Provides energy to run the robot (e.g., Batteries, Solar power). #. Software & Algorithms – Allow the robot to function and make intelligent decisions (e.g., ROS, Machine learning models, Localization, Mapping, Path planning, Control). -This project, PythonRobotics, focuses on the software and algorithms part of robotics. +This project, `PythonRobotics`, focuses on the software and algorithms part of robotics. If you are interested in `Sensors` hardware, you can check :ref:`Internal Sensors for Robotics`_ or :ref:`External Sensors for Robotics`_. diff --git a/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst b/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst index 139996f291..8a3e517105 100644 --- a/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst +++ b/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst @@ -16,7 +16,7 @@ Core Concept * Maintain global path connectivity. Bubble Representation -~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~ - **Definition**: A local free-space region around configuration :math:`b`: .. math:: @@ -56,7 +56,7 @@ External Repulsion Force \frac{\partial \rho}{\partial x} \approx \frac{\rho(b_i + h) - \rho(b_i - h)}{2h}. Dynamic Path Maintenance -~~~~~~~~~~~~~~~~~~~~~~~ +~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. **Node Update**: .. math:: @@ -68,6 +68,7 @@ Dynamic Path Maintenance - Insert new nodes if adjacent nodes are too far apart - Remove redundant nodes if adjacent nodes are too close -Ref: +References +~~~~~~~~~~~~~~~~~~~~~~~ - `Elastic Bands: Connecting Path Planning and Control `__ From 8064488a1d63a1579d8c9d786a376c8b583c02a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Feb 2025 21:49:40 +0900 Subject: [PATCH 086/181] build(deps): bump numpy from 2.2.2 to 2.2.3 in /requirements (#1164) Bumps [numpy](https://github.com/numpy/numpy) from 2.2.2 to 2.2.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.2...v2.2.3) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index ea4550edb8..8176364c29 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.2.2 +numpy == 2.2.3 scipy == 1.15.2 matplotlib == 3.10.0 cvxpy == 1.5.3 From 2b7080991e5289c9524a505244fcd2a4995da06e Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Wed, 19 Feb 2025 13:36:07 +0900 Subject: [PATCH 087/181] Add GitHub copilot pro sponser (#1167) * fix: correct terminology in documentation and update Sphinx options * fix: correct terminology in documentation and update Sphinx options * fix: correct terminology in documentation and update Sphinx options * fix: correct terminology in documentation and update Sphinx options * fix: correct terminology in documentation and update Sphinx options --- docs/Makefile | 3 ++- docs/modules/0_getting_started/3_how_to_contribute_main.rst | 2 ++ .../1_definition_of_robotics/definition_of_robotics_main.rst | 2 +- docs/modules/4_slam/graph_slam/graphSLAM_SE2_example.rst | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index 9296811e02..ae495eb36d 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -2,7 +2,8 @@ # # You can set these variables from the command line. -SPHINXOPTS = +# SPHINXOPTS with -W means turn warnings into errors to fail the build when warnings are present. +SPHINXOPTS = -W SPHINXBUILD = sphinx-build SPHINXPROJ = PythonRobotics SOURCEDIR = . diff --git a/docs/modules/0_getting_started/3_how_to_contribute_main.rst b/docs/modules/0_getting_started/3_how_to_contribute_main.rst index 874564cbb8..1e61760649 100644 --- a/docs/modules/0_getting_started/3_how_to_contribute_main.rst +++ b/docs/modules/0_getting_started/3_how_to_contribute_main.rst @@ -187,6 +187,7 @@ If you would like to support us in some other way, please contact with creating Current Major Sponsors: +#. `GitHub`_ : They are providing a GitHub Copilot Pro license for this OSS development. #. `JetBrains`_ : They are providing a free license of their IDEs for this OSS development. #. `1Password`_ : They are providing a free license of their 1Password team license for this OSS project. @@ -202,6 +203,7 @@ Current Major Sponsors: .. _`doc README`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/docs/README.md .. _`test_codestyle.py`: https://github.com/AtsushiSakai/PythonRobotics/blob/master/tests/test_codestyle.py .. _`JetBrains`: https://www.jetbrains.com/ +.. _`GitHub`: https://www.github.com/ .. _`Sponsor @AtsushiSakai on GitHub Sponsors`: https://github.com/sponsors/AtsushiSakai .. _`Become a backer or sponsor on Patreon`: https://www.patreon.com/myenigma .. _`One-time donation via PayPal`: https://www.paypal.com/paypalme/myenigmapay/ diff --git a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst index 5d78bf858b..ca595301a6 100644 --- a/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst +++ b/docs/modules/1_introduction/1_definition_of_robotics/definition_of_robotics_main.rst @@ -104,4 +104,4 @@ Robotics consists of several essential components: #. Software & Algorithms – Allow the robot to function and make intelligent decisions (e.g., ROS, Machine learning models, Localization, Mapping, Path planning, Control). This project, `PythonRobotics`, focuses on the software and algorithms part of robotics. -If you are interested in `Sensors` hardware, you can check :ref:`Internal Sensors for Robotics`_ or :ref:`External Sensors for Robotics`_. +If you are interested in `Sensors` hardware, you can check :ref:`Internal Sensors for Robots` or :ref:`External Sensors for Robots`. diff --git a/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example.rst b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example.rst index 491320512b..15963aff79 100644 --- a/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example.rst +++ b/docs/modules/4_slam/graph_slam/graphSLAM_SE2_example.rst @@ -165,7 +165,7 @@ different data sources into a single optimization problem. 6 215.8405 -0.000000 -.. figure:: graphSLAM_SE2_example_files/Graph_SLAM_optimization.gif +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/SLAM/GraphBasedSLAM/Graph_SLAM_optimization.gif .. code:: ipython3 From c7fb228d24856d6c7a0cb28c715176bfb8b17281 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Thu, 20 Feb 2025 12:11:04 +0900 Subject: [PATCH 088/181] fix: update section references to use consistent formatting (#1169) --- .../technologies_for_robotics_main.rst | 48 +++++++++++++++---- .../2_localization/localization_main.rst | 2 +- docs/modules/3_mapping/mapping_main.rst | 2 +- docs/modules/4_slam/slam_main.rst | 2 +- .../5_path_planning/path_planning_main.rst | 2 +- .../6_path_tracking/path_tracking_main.rst | 2 +- .../7_arm_navigation/arm_navigation_main.rst | 2 +- .../aerial_navigation_main.rst | 2 +- docs/modules/9_bipedal/bipedal_main.rst | 2 +- 9 files changed, 48 insertions(+), 16 deletions(-) diff --git a/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst b/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst index 64c6945c32..c77997a138 100644 --- a/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst +++ b/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst @@ -5,22 +5,54 @@ The field of robotics needs wide areas of technologies such as mechanical engine electrical engineering, computer science, and artificial intelligence (AI). This project, `PythonRobotics`, only focus on computer science and artificial intelligence. +The technologies for robotics are categorized as following 3 categories: +#. `Autonomous Navigation`_ +#. `Manipulation`_ +#. `Robot type specific technologies`_ + +.. _`Autonomous Navigation`: Autonomous Navigation ^^^^^^^^^^^^^^^^^^^^^^^^ - -In recent years, autonomous navigation technologies have received huge -attention in many fields. -Such fields include, autonomous driving[22], drone flight navigation, -and other transportation systems. - -An autonomous navigation system is a system that can move to a goal over long +Autonomous navigation is a capability that can move to a goal over long periods of time without any external control by an operator. -The system requires a wide range of technologies: + +To achieve autonomous navigation, the robot needs to have the following technologies: - It needs to know where it is (localization) - Where it is safe (mapping) +- Where is is safe and where the robot is in the map (Simultaneous Localization and Mapping (SLAM)) - Where and how to move (path planning) - How to control its motion (path following). The autonomous system would not work correctly if any of these technologies is missing. + +In recent years, autonomous navigation technologies have received huge +attention in many fields. +For example, self-driving cars, drones, and autonomous mobile robots in indoor and outdoor environments. + +In this project, we provide many algorithms, sample codes, +and documentations for autonomous navigation. + +#. :ref:`Localization` +#. :ref:`Mapping` +#. :ref:`SLAM` +#. :ref:`Path planning` +#. :ref:`Path tracking` + + + +.. _`Manipulation`: + +Manipulation +^^^^^^^^^^^^^^^^^^^^^^^^ + +#. :ref:`Arm Navigation` + +.. _`Robot type specific technologies`: + +Robot type specific technologies +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +#. :ref:`Aerial Navigation` +#. :ref:`Bipedal` diff --git a/docs/modules/2_localization/localization_main.rst b/docs/modules/2_localization/localization_main.rst index 22cbd094da..770a234b69 100644 --- a/docs/modules/2_localization/localization_main.rst +++ b/docs/modules/2_localization/localization_main.rst @@ -1,4 +1,4 @@ -.. _localization: +.. _`Localization`: Localization ============ diff --git a/docs/modules/3_mapping/mapping_main.rst b/docs/modules/3_mapping/mapping_main.rst index 825b08d3ec..34a3744893 100644 --- a/docs/modules/3_mapping/mapping_main.rst +++ b/docs/modules/3_mapping/mapping_main.rst @@ -1,4 +1,4 @@ -.. _mapping: +.. _`Mapping`: Mapping ======= diff --git a/docs/modules/4_slam/slam_main.rst b/docs/modules/4_slam/slam_main.rst index dec04f253a..98211986c2 100644 --- a/docs/modules/4_slam/slam_main.rst +++ b/docs/modules/4_slam/slam_main.rst @@ -1,4 +1,4 @@ -.. _slam: +.. _`SLAM`: SLAM ==== diff --git a/docs/modules/5_path_planning/path_planning_main.rst b/docs/modules/5_path_planning/path_planning_main.rst index 65fbfdbc3d..a0f9c30a3d 100644 --- a/docs/modules/5_path_planning/path_planning_main.rst +++ b/docs/modules/5_path_planning/path_planning_main.rst @@ -1,4 +1,4 @@ -.. _path_planning: +.. _`Path Planning`: Path Planning ============= diff --git a/docs/modules/6_path_tracking/path_tracking_main.rst b/docs/modules/6_path_tracking/path_tracking_main.rst index d7a895b562..130a2340c1 100644 --- a/docs/modules/6_path_tracking/path_tracking_main.rst +++ b/docs/modules/6_path_tracking/path_tracking_main.rst @@ -1,4 +1,4 @@ -.. _path_tracking: +.. _`Path Tracking`: Path Tracking ============= diff --git a/docs/modules/7_arm_navigation/arm_navigation_main.rst b/docs/modules/7_arm_navigation/arm_navigation_main.rst index bbbc872c58..7acd3ee7d3 100644 --- a/docs/modules/7_arm_navigation/arm_navigation_main.rst +++ b/docs/modules/7_arm_navigation/arm_navigation_main.rst @@ -1,4 +1,4 @@ -.. _arm_navigation: +.. _`Arm Navigation`: Arm Navigation ============== diff --git a/docs/modules/8_aerial_navigation/aerial_navigation_main.rst b/docs/modules/8_aerial_navigation/aerial_navigation_main.rst index b2ccb071af..7f76689770 100644 --- a/docs/modules/8_aerial_navigation/aerial_navigation_main.rst +++ b/docs/modules/8_aerial_navigation/aerial_navigation_main.rst @@ -1,4 +1,4 @@ -.. _aerial_navigation: +.. _`Aerial Navigation`: Aerial Navigation ================= diff --git a/docs/modules/9_bipedal/bipedal_main.rst b/docs/modules/9_bipedal/bipedal_main.rst index fc5b933055..dc387dc4e8 100644 --- a/docs/modules/9_bipedal/bipedal_main.rst +++ b/docs/modules/9_bipedal/bipedal_main.rst @@ -1,4 +1,4 @@ -.. _bipedal: +.. _`Bipedal`: Bipedal ================= From f343573a7bb9b99fd8fcfc72cef7336ccf859c28 Mon Sep 17 00:00:00 2001 From: Aglargil <34728006+Aglargil@users.noreply.github.com> Date: Thu, 20 Feb 2025 18:09:30 +0800 Subject: [PATCH 089/181] Update move_to_pose for cases where alpha > pi/2 or alpha < -pi/2 (#1168) * Update move_to_pose for cases where alpha > pi/2 or alpha < -pi/2 * Update move_to_pose * Add move_to_pose test * Update move_to_pose --- Control/move_to_pose/move_to_pose.py | 90 +++++++++++++++++++--------- tests/test_move_to_pose.py | 74 ++++++++++++++++++++++- 2 files changed, 134 insertions(+), 30 deletions(-) diff --git a/Control/move_to_pose/move_to_pose.py b/Control/move_to_pose/move_to_pose.py index 279ba0625b..34736a2e21 100644 --- a/Control/move_to_pose/move_to_pose.py +++ b/Control/move_to_pose/move_to_pose.py @@ -5,6 +5,7 @@ Author: Daniel Ingram (daniel-s-ingram) Atsushi Sakai (@Atsushi_twi) Seied Muhammad Yazdian (@Muhammad-Yazdian) + Wang Zheng (@Aglargil) P. I. Corke, "Robotics, Vision & Control", Springer 2017, ISBN 978-3-319-54413-7 @@ -12,8 +13,13 @@ import matplotlib.pyplot as plt import numpy as np -from random import random +import sys +import pathlib + +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) from utils.angle import angle_mod +from random import random + class PathFinderController: """ @@ -70,14 +76,20 @@ def calc_control_command(self, x_diff, y_diff, theta, theta_goal): # [-pi, pi] to prevent unstable behavior e.g. difference going # from 0 rad to 2*pi rad with slight turn + # Ref: The velocity v always has a constant sign which depends on the initial value of α. rho = np.hypot(x_diff, y_diff) - alpha = angle_mod(np.arctan2(y_diff, x_diff) - theta) - beta = angle_mod(theta_goal - theta - alpha) v = self.Kp_rho * rho - w = self.Kp_alpha * alpha - self.Kp_beta * beta + alpha = angle_mod(np.arctan2(y_diff, x_diff) - theta) + beta = angle_mod(theta_goal - theta - alpha) if alpha > np.pi / 2 or alpha < -np.pi / 2: + # recalculate alpha to make alpha in the range of [-pi/2, pi/2] + alpha = angle_mod(np.arctan2(-y_diff, -x_diff) - theta) + beta = angle_mod(theta_goal - theta - alpha) + w = self.Kp_alpha * alpha - self.Kp_beta * beta v = -v + else: + w = self.Kp_alpha * alpha - self.Kp_beta * beta return rho, v, w @@ -85,6 +97,7 @@ def calc_control_command(self, x_diff, y_diff, theta, theta_goal): # simulation parameters controller = PathFinderController(9, 15, 3) dt = 0.01 +MAX_SIM_TIME = 5 # seconds, robot will stop moving when time exceeds this value # Robot specifications MAX_LINEAR_SPEED = 15 @@ -101,18 +114,19 @@ def move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal): x_diff = x_goal - x y_diff = y_goal - y - x_traj, y_traj = [], [] + x_traj, y_traj, v_traj, w_traj = [x], [y], [0], [0] rho = np.hypot(x_diff, y_diff) - while rho > 0.001: + t = 0 + while rho > 0.001 and t < MAX_SIM_TIME: + t += dt x_traj.append(x) y_traj.append(y) x_diff = x_goal - x y_diff = y_goal - y - rho, v, w = controller.calc_control_command( - x_diff, y_diff, theta, theta_goal) + rho, v, w = controller.calc_control_command(x_diff, y_diff, theta, theta_goal) if abs(v) > MAX_LINEAR_SPEED: v = np.sign(v) * MAX_LINEAR_SPEED @@ -120,18 +134,35 @@ def move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal): if abs(w) > MAX_ANGULAR_SPEED: w = np.sign(w) * MAX_ANGULAR_SPEED + v_traj.append(v) + w_traj.append(w) + theta = theta + w * dt x = x + v * np.cos(theta) * dt y = y + v * np.sin(theta) * dt if show_animation: # pragma: no cover plt.cla() - plt.arrow(x_start, y_start, np.cos(theta_start), - np.sin(theta_start), color='r', width=0.1) - plt.arrow(x_goal, y_goal, np.cos(theta_goal), - np.sin(theta_goal), color='g', width=0.1) + plt.arrow( + x_start, + y_start, + np.cos(theta_start), + np.sin(theta_start), + color="r", + width=0.1, + ) + plt.arrow( + x_goal, + y_goal, + np.cos(theta_goal), + np.sin(theta_goal), + color="g", + width=0.1, + ) plot_vehicle(x, y, theta, x_traj, y_traj) + return x_traj, y_traj, v_traj, w_traj + def plot_vehicle(x, y, theta, x_traj, y_traj): # pragma: no cover # Corners of triangular vehicle when pointing to the right (0 radians) @@ -144,16 +175,16 @@ def plot_vehicle(x, y, theta, x_traj, y_traj): # pragma: no cover p2 = np.matmul(T, p2_i) p3 = np.matmul(T, p3_i) - plt.plot([p1[0], p2[0]], [p1[1], p2[1]], 'k-') - plt.plot([p2[0], p3[0]], [p2[1], p3[1]], 'k-') - plt.plot([p3[0], p1[0]], [p3[1], p1[1]], 'k-') + plt.plot([p1[0], p2[0]], [p1[1], p2[1]], "k-") + plt.plot([p2[0], p3[0]], [p2[1], p3[1]], "k-") + plt.plot([p3[0], p1[0]], [p3[1], p1[1]], "k-") - plt.plot(x_traj, y_traj, 'b--') + plt.plot(x_traj, y_traj, "b--") # for stopping simulation with the esc key. plt.gcf().canvas.mpl_connect( - 'key_release_event', - lambda event: [exit(0) if event.key == 'escape' else None]) + "key_release_event", lambda event: [exit(0) if event.key == "escape" else None] + ) plt.xlim(0, 20) plt.ylim(0, 20) @@ -162,15 +193,16 @@ def plot_vehicle(x, y, theta, x_traj, y_traj): # pragma: no cover def transformation_matrix(x, y, theta): - return np.array([ - [np.cos(theta), -np.sin(theta), x], - [np.sin(theta), np.cos(theta), y], - [0, 0, 1] - ]) + return np.array( + [ + [np.cos(theta), -np.sin(theta), x], + [np.sin(theta), np.cos(theta), y], + [0, 0, 1], + ] + ) def main(): - for i in range(5): x_start = 20.0 * random() y_start = 20.0 * random() @@ -178,10 +210,14 @@ def main(): x_goal = 20 * random() y_goal = 20 * random() theta_goal = 2 * np.pi * random() - np.pi - print(f"Initial x: {round(x_start, 2)} m\nInitial y: {round(y_start, 2)} m\nInitial theta: {round(theta_start, 2)} rad\n") - print(f"Goal x: {round(x_goal, 2)} m\nGoal y: {round(y_goal, 2)} m\nGoal theta: {round(theta_goal, 2)} rad\n") + print( + f"Initial x: {round(x_start, 2)} m\nInitial y: {round(y_start, 2)} m\nInitial theta: {round(theta_start, 2)} rad\n" + ) + print( + f"Goal x: {round(x_goal, 2)} m\nGoal y: {round(y_goal, 2)} m\nGoal theta: {round(theta_goal, 2)} rad\n" + ) move_to_pose(x_start, y_start, theta_start, x_goal, y_goal, theta_goal) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tests/test_move_to_pose.py b/tests/test_move_to_pose.py index 8bc11a8d24..94c3ec1102 100644 --- a/tests/test_move_to_pose.py +++ b/tests/test_move_to_pose.py @@ -1,13 +1,81 @@ +import itertools +import numpy as np import conftest # Add root path to sys.path from Control.move_to_pose import move_to_pose as m -def test_1(): +def test_random(): m.show_animation = False m.main() -def test_2(): +def test_stability(): + """ + This unit test tests the move_to_pose.py program for stability + """ + m.show_animation = False + x_start = 5 + y_start = 5 + theta_start = 0 + x_goal = 1 + y_goal = 4 + theta_goal = 0 + _, _, v_traj, w_traj = m.move_to_pose( + x_start, y_start, theta_start, x_goal, y_goal, theta_goal + ) + + def v_is_change(current, previous): + return abs(current - previous) > m.MAX_LINEAR_SPEED + + def w_is_change(current, previous): + return abs(current - previous) > m.MAX_ANGULAR_SPEED + + # Check if the speed is changing too much + window_size = 10 + count_threshold = 4 + v_change = [v_is_change(v_traj[i], v_traj[i - 1]) for i in range(1, len(v_traj))] + w_change = [w_is_change(w_traj[i], w_traj[i - 1]) for i in range(1, len(w_traj))] + for i in range(len(v_change) - window_size + 1): + v_window = v_change[i : i + window_size] + w_window = w_change[i : i + window_size] + + v_unstable = sum(v_window) > count_threshold + w_unstable = sum(w_window) > count_threshold + + assert not v_unstable, ( + f"v_unstable in window [{i}, {i + window_size}], unstable count: {sum(v_window)}" + ) + assert not w_unstable, ( + f"w_unstable in window [{i}, {i + window_size}], unstable count: {sum(w_window)}" + ) + + +def test_reach_goal(): + """ + This unit test tests the move_to_pose.py program for reaching the goal + """ + m.show_animation = False + x_start = 5 + y_start = 5 + theta_start_list = [0, np.pi / 2, np.pi, 3 * np.pi / 2] + x_goal_list = [0, 5, 10] + y_goal_list = [0, 5, 10] + theta_goal = 0 + for theta_start, x_goal, y_goal in itertools.product( + theta_start_list, x_goal_list, y_goal_list + ): + x_traj, y_traj, _, _ = m.move_to_pose( + x_start, y_start, theta_start, x_goal, y_goal, theta_goal + ) + x_diff = x_goal - x_traj[-1] + y_diff = y_goal - y_traj[-1] + rho = np.hypot(x_diff, y_diff) + assert rho < 0.001, ( + f"start:[{x_start}, {y_start}, {theta_start}], goal:[{x_goal}, {y_goal}, {theta_goal}], rho: {rho} is too large" + ) + + +def test_max_speed(): """ This unit test tests the move_to_pose.py program for a MAX_LINEAR_SPEED and MAX_ANGULAR_SPEED @@ -18,5 +86,5 @@ def test_2(): m.main() -if __name__ == '__main__': +if __name__ == "__main__": conftest.run_this_test(__file__) From 64779298ffa77c918c63f9206b694f0d8d439c71 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Fri, 21 Feb 2025 21:40:21 +0900 Subject: [PATCH 090/181] =?UTF-8?q?refactor:=20rename=20files=20and=20upda?= =?UTF-8?q?te=20references=20for=20inverted=20pendulum=20an=E2=80=A6=20(#1?= =?UTF-8?q?171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: rename files and update references for inverted pendulum and path tracking modules * refactor: rename inverted pendulum control files and update type check references * refactor: update import statements to use consistent casing for InvertedPendulum module --- Control/move_to_pose/__init__.py | 0 .../inverted_pendulum_lqr_control.py | 0 .../inverted_pendulum_mpc_control.py | 0 {Control => PathTracking/move_to_pose}/__init__.py | 0 .../move_to_pose/move_to_pose.py | 0 .../move_to_pose/move_to_pose_robot.py | 0 docs/index_main.rst | 2 +- docs/modules/10_control/control_main.rst | 12 ------------ .../inverted-pendulum.png | Bin .../inverted_pendulum_main.rst} | 6 ++++-- docs/modules/11_utils/utils_main.rst | 2 +- docs/modules/12_appendix/appendix_main.rst | 2 +- .../technologies_for_robotics_main.rst | 10 ++++++++++ .../move_to_a_pose_control_main.rst | 0 docs/modules/6_path_tracking/path_tracking_main.rst | 1 + tests/test_inverted_pendulum_lqr_control.py | 2 +- tests/test_inverted_pendulum_mpc_control.py | 2 +- tests/test_move_to_pose.py | 2 +- tests/test_move_to_pose_robot.py | 2 +- tests/test_mypy_type_check.py | 2 +- 20 files changed, 23 insertions(+), 22 deletions(-) delete mode 100644 Control/move_to_pose/__init__.py rename {Control/inverted_pendulum => InvertedPendulum}/inverted_pendulum_lqr_control.py (100%) rename {Control/inverted_pendulum => InvertedPendulum}/inverted_pendulum_mpc_control.py (100%) rename {Control => PathTracking/move_to_pose}/__init__.py (100%) rename {Control => PathTracking}/move_to_pose/move_to_pose.py (100%) rename {Control => PathTracking}/move_to_pose/move_to_pose_robot.py (100%) delete mode 100644 docs/modules/10_control/control_main.rst rename docs/modules/{10_control/inverted_pendulum_control => 10_inverted_pendulum}/inverted-pendulum.png (100%) rename docs/modules/{10_control/inverted_pendulum_control/inverted_pendulum_control_main.rst => 10_inverted_pendulum/inverted_pendulum_main.rst} (97%) rename docs/modules/{10_control => 6_path_tracking}/move_to_a_pose_control/move_to_a_pose_control_main.rst (100%) diff --git a/Control/move_to_pose/__init__.py b/Control/move_to_pose/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/Control/inverted_pendulum/inverted_pendulum_lqr_control.py b/InvertedPendulum/inverted_pendulum_lqr_control.py similarity index 100% rename from Control/inverted_pendulum/inverted_pendulum_lqr_control.py rename to InvertedPendulum/inverted_pendulum_lqr_control.py diff --git a/Control/inverted_pendulum/inverted_pendulum_mpc_control.py b/InvertedPendulum/inverted_pendulum_mpc_control.py similarity index 100% rename from Control/inverted_pendulum/inverted_pendulum_mpc_control.py rename to InvertedPendulum/inverted_pendulum_mpc_control.py diff --git a/Control/__init__.py b/PathTracking/move_to_pose/__init__.py similarity index 100% rename from Control/__init__.py rename to PathTracking/move_to_pose/__init__.py diff --git a/Control/move_to_pose/move_to_pose.py b/PathTracking/move_to_pose/move_to_pose.py similarity index 100% rename from Control/move_to_pose/move_to_pose.py rename to PathTracking/move_to_pose/move_to_pose.py diff --git a/Control/move_to_pose/move_to_pose_robot.py b/PathTracking/move_to_pose/move_to_pose_robot.py similarity index 100% rename from Control/move_to_pose/move_to_pose_robot.py rename to PathTracking/move_to_pose/move_to_pose_robot.py diff --git a/docs/index_main.rst b/docs/index_main.rst index c1e8d22d32..75805d1184 100644 --- a/docs/index_main.rst +++ b/docs/index_main.rst @@ -43,7 +43,7 @@ this graph shows GitHub star history of this project: modules/7_arm_navigation/arm_navigation modules/8_aerial_navigation/aerial_navigation modules/9_bipedal/bipedal - modules/10_control/control + modules/10_inverted_pendulum/inverted_pendulum modules/11_utils/utils modules/12_appendix/appendix diff --git a/docs/modules/10_control/control_main.rst b/docs/modules/10_control/control_main.rst deleted file mode 100644 index cee2aa9e8e..0000000000 --- a/docs/modules/10_control/control_main.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _control: - -Control -================= - -.. toctree:: - :maxdepth: 2 - :caption: Contents - - inverted_pendulum_control/inverted_pendulum_control - move_to_a_pose_control/move_to_a_pose_control - diff --git a/docs/modules/10_control/inverted_pendulum_control/inverted-pendulum.png b/docs/modules/10_inverted_pendulum/inverted-pendulum.png similarity index 100% rename from docs/modules/10_control/inverted_pendulum_control/inverted-pendulum.png rename to docs/modules/10_inverted_pendulum/inverted-pendulum.png diff --git a/docs/modules/10_control/inverted_pendulum_control/inverted_pendulum_control_main.rst b/docs/modules/10_inverted_pendulum/inverted_pendulum_main.rst similarity index 97% rename from docs/modules/10_control/inverted_pendulum_control/inverted_pendulum_control_main.rst rename to docs/modules/10_inverted_pendulum/inverted_pendulum_main.rst index e41729fd61..048cbea9ac 100644 --- a/docs/modules/10_control/inverted_pendulum_control/inverted_pendulum_control_main.rst +++ b/docs/modules/10_inverted_pendulum/inverted_pendulum_main.rst @@ -1,5 +1,7 @@ -Inverted Pendulum Control ------------------------------ +.. _`Inverted Pendulum`: + +Inverted Pendulum +------------------ An inverted pendulum on a cart consists of a mass :math:`m` at the top of a pole of length :math:`l` pivoted on a horizontally moving base as shown in the adjacent. diff --git a/docs/modules/11_utils/utils_main.rst b/docs/modules/11_utils/utils_main.rst index ff79a26205..95c982b077 100644 --- a/docs/modules/11_utils/utils_main.rst +++ b/docs/modules/11_utils/utils_main.rst @@ -1,4 +1,4 @@ -.. _utils: +.. _`utils`: Utilities ========== diff --git a/docs/modules/12_appendix/appendix_main.rst b/docs/modules/12_appendix/appendix_main.rst index a55389e1e6..d0b9eeea3a 100644 --- a/docs/modules/12_appendix/appendix_main.rst +++ b/docs/modules/12_appendix/appendix_main.rst @@ -1,4 +1,4 @@ -.. _appendix: +.. _`Appendix`: Appendix ============== diff --git a/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst b/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst index c77997a138..0ed51e961b 100644 --- a/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst +++ b/docs/modules/1_introduction/3_technologies_for_robotics/technologies_for_robotics_main.rst @@ -56,3 +56,13 @@ Robot type specific technologies #. :ref:`Aerial Navigation` #. :ref:`Bipedal` +#. :ref:`Inverted Pendulum` + + +Additional Information +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +#. :ref:`utils` +#. :ref:`Appendix` + + diff --git a/docs/modules/10_control/move_to_a_pose_control/move_to_a_pose_control_main.rst b/docs/modules/6_path_tracking/move_to_a_pose_control/move_to_a_pose_control_main.rst similarity index 100% rename from docs/modules/10_control/move_to_a_pose_control/move_to_a_pose_control_main.rst rename to docs/modules/6_path_tracking/move_to_a_pose_control/move_to_a_pose_control_main.rst diff --git a/docs/modules/6_path_tracking/path_tracking_main.rst b/docs/modules/6_path_tracking/path_tracking_main.rst index 130a2340c1..d98e324583 100644 --- a/docs/modules/6_path_tracking/path_tracking_main.rst +++ b/docs/modules/6_path_tracking/path_tracking_main.rst @@ -16,3 +16,4 @@ Path tracking is the ability of a robot to follow the reference path generated b lqr_speed_and_steering_control/lqr_speed_and_steering_control model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control cgmres_nmpc/cgmres_nmpc + move_to_a_pose_control/move_to_a_pose_control diff --git a/tests/test_inverted_pendulum_lqr_control.py b/tests/test_inverted_pendulum_lqr_control.py index cbbabb93b1..62afda71c3 100644 --- a/tests/test_inverted_pendulum_lqr_control.py +++ b/tests/test_inverted_pendulum_lqr_control.py @@ -1,5 +1,5 @@ import conftest -from Control.inverted_pendulum import inverted_pendulum_lqr_control as m +from InvertedPendulum import inverted_pendulum_lqr_control as m def test_1(): diff --git a/tests/test_inverted_pendulum_mpc_control.py b/tests/test_inverted_pendulum_mpc_control.py index 800aefd7d5..94859c2e0a 100644 --- a/tests/test_inverted_pendulum_mpc_control.py +++ b/tests/test_inverted_pendulum_mpc_control.py @@ -1,6 +1,6 @@ import conftest -from Control.inverted_pendulum import inverted_pendulum_mpc_control as m +from InvertedPendulum import inverted_pendulum_mpc_control as m def test1(): diff --git a/tests/test_move_to_pose.py b/tests/test_move_to_pose.py index 94c3ec1102..e06d801555 100644 --- a/tests/test_move_to_pose.py +++ b/tests/test_move_to_pose.py @@ -1,7 +1,7 @@ import itertools import numpy as np import conftest # Add root path to sys.path -from Control.move_to_pose import move_to_pose as m +from PathTracking.move_to_pose import move_to_pose as m def test_random(): diff --git a/tests/test_move_to_pose_robot.py b/tests/test_move_to_pose_robot.py index a93b44d198..7a82f98556 100644 --- a/tests/test_move_to_pose_robot.py +++ b/tests/test_move_to_pose_robot.py @@ -1,5 +1,5 @@ import conftest # Add root path to sys.path -from Control.move_to_pose import move_to_pose as m +from PathTracking.move_to_pose import move_to_pose as m def test_1(): diff --git a/tests/test_mypy_type_check.py b/tests/test_mypy_type_check.py index 07afb40afd..6b933c1011 100644 --- a/tests/test_mypy_type_check.py +++ b/tests/test_mypy_type_check.py @@ -7,12 +7,12 @@ "AerialNavigation", "ArmNavigation", "Bipedal", - "Control", "Localization", "Mapping", "PathPlanning", "PathTracking", "SLAM", + "InvertedPendulum" ] From 6e13e8292aad80661093603ed262c6f0bdcb4137 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 08:07:42 +0900 Subject: [PATCH 091/181] build(deps): bump ruff from 0.9.6 to 0.9.7 in /requirements (#1173) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.6 to 0.9.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.6...0.9.7) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 8176364c29..b439ea4266 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.4 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.9.6 # For unit test +ruff == 0.9.7 # For unit test From 0c8ff11645e6804db18bec6bd918e03ed03454f7 Mon Sep 17 00:00:00 2001 From: Jonathan Schwartz Date: Tue, 25 Feb 2025 06:53:36 -0500 Subject: [PATCH 092/181] Space-Time AStar (#1170) * wip - sketch out obstacles * move to correct path * better animation * clean up * use np to sample points * implemented time-based A* * cleaning up Grid + adding new obstacle arrangement * added unit test * formatting p1 * format STA* file * remove newlines by docstrings * linter * working on typehints * fix linter errors * lint some more * appease AppVeyor * dataclasses are :fire: * back to @total_ordering * trailing whitespace * add docs page on SpaceTimeA* * docs lint * remove trailing newlines in doc * address comments * Update docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst --------- Co-authored-by: Atsushi Sakai --- .../GridWithDynamicObstacles.py | 273 ++++++++++++++++++ .../TimeBasedPathPlanning/SpaceTimeAStar.py | 220 ++++++++++++++ .../TimeBasedPathPlanning/__init__.py | 0 .../5_path_planning/path_planning_main.rst | 1 + .../time_based_grid_search_main.rst | 22 ++ tests/test_space_time_astar.py | 33 +++ 6 files changed, 549 insertions(+) create mode 100644 PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py create mode 100644 PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py create mode 100644 PathPlanning/TimeBasedPathPlanning/__init__.py create mode 100644 docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst create mode 100644 tests/test_space_time_astar.py diff --git a/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py b/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py new file mode 100644 index 0000000000..7b0190d023 --- /dev/null +++ b/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py @@ -0,0 +1,273 @@ +""" +This file implements a grid with a 3d reservation matrix with dimensions for x, y, and time. There +is also infrastructure to generate dynamic obstacles that move around the grid. The obstacles' paths +are stored in the reservation matrix on creation. +""" +import numpy as np +import matplotlib.pyplot as plt +from enum import Enum +from dataclasses import dataclass + +@dataclass(order=True) +class Position: + x: int + y: int + + def as_ndarray(self) -> np.ndarray: + return np.array([self.x, self.y]) + + def __add__(self, other): + if isinstance(other, Position): + return Position(self.x + other.x, self.y + other.y) + raise NotImplementedError( + f"Addition not supported for Position and {type(other)}" + ) + + def __sub__(self, other): + if isinstance(other, Position): + return Position(self.x - other.x, self.y - other.y) + raise NotImplementedError( + f"Subtraction not supported for Position and {type(other)}" + ) + + +class ObstacleArrangement(Enum): + # Random obstacle positions and movements + RANDOM = 0 + # Obstacles start in a line in y at center of grid and move side-to-side in x + ARRANGEMENT1 = 1 + + +class Grid: + # Set in constructor + grid_size: np.ndarray + reservation_matrix: np.ndarray + obstacle_paths: list[list[Position]] = [] + # Obstacles will never occupy these points. Useful to avoid impossible scenarios + obstacle_avoid_points: list[Position] = [] + + # Number of time steps in the simulation + time_limit: int + + # Logging control + verbose = False + + def __init__( + self, + grid_size: np.ndarray, + num_obstacles: int = 40, + obstacle_avoid_points: list[Position] = [], + obstacle_arrangement: ObstacleArrangement = ObstacleArrangement.RANDOM, + time_limit: int = 100, + ): + self.obstacle_avoid_points = obstacle_avoid_points + self.time_limit = time_limit + self.grid_size = grid_size + self.reservation_matrix = np.zeros((grid_size[0], grid_size[1], self.time_limit)) + + if num_obstacles > self.grid_size[0] * self.grid_size[1]: + raise Exception("Number of obstacles is greater than grid size!") + + if obstacle_arrangement == ObstacleArrangement.RANDOM: + self.obstacle_paths = self.generate_dynamic_obstacles(num_obstacles) + elif obstacle_arrangement == ObstacleArrangement.ARRANGEMENT1: + self.obstacle_paths = self.obstacle_arrangement_1(num_obstacles) + + for i, path in enumerate(self.obstacle_paths): + obs_idx = i + 1 # avoid using 0 - that indicates free space in the grid + for t, position in enumerate(path): + # Reserve old & new position at this time step + if t > 0: + self.reservation_matrix[path[t - 1].x, path[t - 1].y, t] = obs_idx + self.reservation_matrix[position.x, position.y, t] = obs_idx + + """ + Generate dynamic obstacles that move around the grid. Initial positions and movements are random + """ + def generate_dynamic_obstacles(self, obs_count: int) -> list[list[Position]]: + obstacle_paths = [] + for _ in (0, obs_count): + # Sample until a free starting space is found + initial_position = self.sample_random_position() + while not self.valid_obstacle_position(initial_position, 0): + initial_position = self.sample_random_position() + + positions = [initial_position] + if self.verbose: + print("Obstacle initial position: ", initial_position) + + # Encourage obstacles to mostly stay in place - too much movement leads to chaotic planning scenarios + # that are not fun to watch + weights = [0.05, 0.05, 0.05, 0.05, 0.8] + diffs = [ + Position(0, 1), + Position(0, -1), + Position(1, 0), + Position(-1, 0), + Position(0, 0), + ] + + for t in range(1, self.time_limit - 1): + sampled_indices = np.random.choice( + len(diffs), size=5, replace=False, p=weights + ) + rand_diffs = [diffs[i] for i in sampled_indices] + + valid_position = None + for diff in rand_diffs: + new_position = positions[-1] + diff + + if not self.valid_obstacle_position(new_position, t): + continue + + valid_position = new_position + break + + # Impossible situation for obstacle - stay in place + # -> this can happen if the oaths of other obstacles this one + if valid_position is None: + valid_position = positions[-1] + + positions.append(valid_position) + + obstacle_paths.append(positions) + + return obstacle_paths + + """ + Generate a line of obstacles in y at the center of the grid that move side-to-side in x + Bottom half start moving right, top half start moving left. If `obs_count` is less than the length of + the grid, only the first `obs_count` obstacles will be generated. + """ + def obstacle_arrangement_1(self, obs_count: int) -> list[list[Position]]: + obstacle_paths = [] + half_grid_x = self.grid_size[0] // 2 + half_grid_y = self.grid_size[1] // 2 + + for y_idx in range(0, min(obs_count, self.grid_size[1])): + moving_right = y_idx < half_grid_y + position = Position(half_grid_x, y_idx) + path = [position] + + for t in range(1, self.time_limit - 1): + # sit in place every other time step + if t % 2 == 0: + path.append(position) + continue + + # first check if we should switch direction (at edge of grid) + if (moving_right and position.x == self.grid_size[0] - 1) or ( + not moving_right and position.x == 0 + ): + moving_right = not moving_right + # step in direction + position = Position( + position.x + (1 if moving_right else -1), position.y + ) + path.append(position) + + obstacle_paths.append(path) + + return obstacle_paths + + """ + Check if the given position is valid at time t + + input: + position (Position): (x, y) position + t (int): time step + + output: + bool: True if position/time combination is valid, False otherwise + """ + def valid_position(self, position: Position, t: int) -> bool: + # Check if new position is in grid + if not self.inside_grid_bounds(position): + return False + + # Check if new position is not occupied at time t + return self.reservation_matrix[position.x, position.y, t] == 0 + + """ + Returns True if the given position is valid at time t and is not in the set of obstacle_avoid_points + """ + def valid_obstacle_position(self, position: Position, t: int) -> bool: + return ( + self.valid_position(position, t) + and position not in self.obstacle_avoid_points + ) + + """ + Returns True if the given position is within the grid's boundaries + """ + def inside_grid_bounds(self, position: Position) -> bool: + return ( + position.x >= 0 + and position.x < self.grid_size[0] + and position.y >= 0 + and position.y < self.grid_size[1] + ) + + """ + Sample a random position that is within the grid's boundaries + + output: + Position: (x, y) position + """ + def sample_random_position(self) -> Position: + return Position( + np.random.randint(0, self.grid_size[0]), + np.random.randint(0, self.grid_size[1]), + ) + + """ + Returns a tuple of (x_positions, y_positions) of the obstacles at time t + """ + def get_obstacle_positions_at_time(self, t: int) -> tuple[list[int], list[int]]: + x_positions = [] + y_positions = [] + for obs_path in self.obstacle_paths: + x_positions.append(obs_path[t].x) + y_positions.append(obs_path[t].y) + return (x_positions, y_positions) + + +show_animation = True + + +def main(): + grid = Grid( + np.array([11, 11]), + num_obstacles=10, + obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1, + ) + + if not show_animation: + return + + fig = plt.figure(figsize=(8, 7)) + ax = fig.add_subplot( + autoscale_on=False, + xlim=(0, grid.grid_size[0] - 1), + ylim=(0, grid.grid_size[1] - 1), + ) + ax.set_aspect("equal") + ax.grid() + ax.set_xticks(np.arange(0, 11, 1)) + ax.set_yticks(np.arange(0, 11, 1)) + (obs_points,) = ax.plot([], [], "ro", ms=15) + + # for stopping simulation with the esc key. + plt.gcf().canvas.mpl_connect( + "key_release_event", lambda event: [exit(0) if event.key == "escape" else None] + ) + + for i in range(0, grid.time_limit - 1): + obs_positions = grid.get_obstacle_positions_at_time(i) + obs_points.set_data(obs_positions[0], obs_positions[1]) + plt.pause(0.2) + plt.show() + + +if __name__ == "__main__": + main() diff --git a/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py b/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py new file mode 100644 index 0000000000..3b3613d695 --- /dev/null +++ b/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py @@ -0,0 +1,220 @@ +""" +Space-time A* Algorithm + This script demonstrates the Space-time A* algorithm for path planning in a grid world with moving obstacles. + This algorithm is different from normal 2D A* in one key way - the cost (often notated as g(n)) is + the number of time steps it took to get to a given node, instead of the number of cells it has + traversed. This ensures the path is time-optimal, while respescting any dynamic obstacles in the environment. + + Reference: https://www.davidsilver.uk/wp-content/uploads/2020/03/coop-path-AIWisdom.pdf +""" + +import numpy as np +import matplotlib.pyplot as plt +from PathPlanning.TimeBasedPathPlanning.GridWithDynamicObstacles import ( + Grid, + ObstacleArrangement, + Position, +) +import heapq +from collections.abc import Generator +import random +from dataclasses import dataclass +from functools import total_ordering + + +# Seed randomness for reproducibility +RANDOM_SEED = 50 +random.seed(RANDOM_SEED) +np.random.seed(RANDOM_SEED) + +@dataclass() +# Note: Total_ordering is used instead of adding `order=True` to the @dataclass decorator because +# this class needs to override the __lt__ and __eq__ methods to ignore parent_index. Parent +# index is just used to track the path found by the algorithm, and has no effect on the quality +# of a node. +@total_ordering +class Node: + position: Position + time: int + heuristic: int + parent_index: int + + """ + This is what is used to drive node expansion. The node with the lowest value is expanded next. + This comparison prioritizes the node with the lowest cost-to-come (self.time) + cost-to-go (self.heuristic) + """ + def __lt__(self, other: object): + if not isinstance(other, Node): + return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}") + return (self.time + self.heuristic) < (other.time + other.heuristic) + + def __eq__(self, other: object): + if not isinstance(other, Node): + return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}") + return self.position == other.position and self.time == other.time + + +class NodePath: + path: list[Node] + positions_at_time: dict[int, Position] = {} + + def __init__(self, path: list[Node]): + self.path = path + for node in path: + self.positions_at_time[node.time] = node.position + + """ + Get the position of the path at a given time + """ + def get_position(self, time: int) -> Position | None: + return self.positions_at_time.get(time) + + """ + Time stamp of the last node in the path + """ + def goal_reached_time(self) -> int: + return self.path[-1].time + + def __repr__(self): + repr_string = "" + for i, node in enumerate(self.path): + repr_string += f"{i}: {node}\n" + return repr_string + + +class SpaceTimeAStar: + grid: Grid + start: Position + goal: Position + + def __init__(self, grid: Grid, start: Position, goal: Position): + self.grid = grid + self.start = start + self.goal = goal + + def plan(self, verbose: bool = False) -> NodePath: + open_set: list[Node] = [] + heapq.heappush( + open_set, Node(self.start, 0, self.calculate_heuristic(self.start), -1) + ) + + expanded_set: list[Node] = [] + while open_set: + expanded_node: Node = heapq.heappop(open_set) + if verbose: + print("Expanded node:", expanded_node) + + if expanded_node.time + 1 >= self.grid.time_limit: + if verbose: + print(f"\tSkipping node that is past time limit: {expanded_node}") + continue + + if expanded_node.position == self.goal: + print(f"Found path to goal after {len(expanded_set)} expansions") + path = [] + path_walker: Node = expanded_node + while True: + path.append(path_walker) + if path_walker.parent_index == -1: + break + path_walker = expanded_set[path_walker.parent_index] + + # reverse path so it goes start -> goal + path.reverse() + return NodePath(path) + + expanded_idx = len(expanded_set) + expanded_set.append(expanded_node) + + for child in self.generate_successors(expanded_node, expanded_idx, verbose): + heapq.heappush(open_set, child) + + raise Exception("No path found") + + """ + Generate possible successors of the provided `parent_node` + """ + def generate_successors( + self, parent_node: Node, parent_node_idx: int, verbose: bool + ) -> Generator[Node, None, None]: + diffs = [ + Position(0, 0), + Position(1, 0), + Position(-1, 0), + Position(0, 1), + Position(0, -1), + ] + for diff in diffs: + new_pos = parent_node.position + diff + if self.grid.valid_position(new_pos, parent_node.time + 1): + new_node = Node( + new_pos, + parent_node.time + 1, + self.calculate_heuristic(new_pos), + parent_node_idx, + ) + if verbose: + print("\tNew successor node: ", new_node) + yield new_node + + def calculate_heuristic(self, position) -> int: + diff = self.goal - position + return abs(diff.x) + abs(diff.y) + + +show_animation = True +verbose = False + +def main(): + start = Position(1, 11) + goal = Position(19, 19) + grid_side_length = 21 + grid = Grid( + np.array([grid_side_length, grid_side_length]), + num_obstacles=40, + obstacle_avoid_points=[start, goal], + obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1, + ) + + planner = SpaceTimeAStar(grid, start, goal) + path = planner.plan(verbose) + + if verbose: + print(f"Path: {path}") + + if not show_animation: + return + + fig = plt.figure(figsize=(10, 7)) + ax = fig.add_subplot( + autoscale_on=False, + xlim=(0, grid.grid_size[0] - 1), + ylim=(0, grid.grid_size[1] - 1), + ) + ax.set_aspect("equal") + ax.grid() + ax.set_xticks(np.arange(0, grid_side_length, 1)) + ax.set_yticks(np.arange(0, grid_side_length, 1)) + + (start_and_goal,) = ax.plot([], [], "mD", ms=15, label="Start and Goal") + start_and_goal.set_data([start.x, goal.x], [start.y, goal.y]) + (obs_points,) = ax.plot([], [], "ro", ms=15, label="Obstacles") + (path_points,) = ax.plot([], [], "bo", ms=10, label="Path Found") + ax.legend(bbox_to_anchor=(1.05, 1)) + + # for stopping simulation with the esc key. + plt.gcf().canvas.mpl_connect( + "key_release_event", lambda event: [exit(0) if event.key == "escape" else None] + ) + + for i in range(0, path.goal_reached_time()): + obs_positions = grid.get_obstacle_positions_at_time(i) + obs_points.set_data(obs_positions[0], obs_positions[1]) + path_position = path.get_position(i) + path_points.set_data([path_position.x], [path_position.y]) + plt.pause(0.2) + plt.show() + + +if __name__ == "__main__": + main() diff --git a/PathPlanning/TimeBasedPathPlanning/__init__.py b/PathPlanning/TimeBasedPathPlanning/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/modules/5_path_planning/path_planning_main.rst b/docs/modules/5_path_planning/path_planning_main.rst index a0f9c30a3d..0c84a19c22 100644 --- a/docs/modules/5_path_planning/path_planning_main.rst +++ b/docs/modules/5_path_planning/path_planning_main.rst @@ -12,6 +12,7 @@ Path planning is the ability of a robot to search feasible and efficient path to dynamic_window_approach/dynamic_window_approach bugplanner/bugplanner grid_base_search/grid_base_search + time_based_grid_search/time_based_grid_search model_predictive_trajectory_generator/model_predictive_trajectory_generator state_lattice_planner/state_lattice_planner prm_planner/prm_planner diff --git a/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst new file mode 100644 index 0000000000..0c26badec7 --- /dev/null +++ b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst @@ -0,0 +1,22 @@ +Time based grid search +---------------------- + +Space-time astar +~~~~~~~~~~~~~~~~~~~~~~ + +This is an extension of A* algorithm that supports planning around dynamic obstacles. + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar/path_animation.gif + +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar/path_animation2.gif + +The key difference of this algorithm compared to vanilla A* is that the cost and heuristic are now time-based instead of distance-based. +Using a time-based cost and heuristic ensures the path found is optimal in terms of time to reach the goal. + +The cost is the amount of time it takes to reach a given node, and the heuristic is the minimum amount of time it could take to reach the goal from that node, disregarding all obstacles. +For a simple scenario where the robot can move 1 cell per time step and stop and go as it pleases, the heuristic for time is equivalent to the heuristic for distance. + +References: +~~~~~~~~~~~ + +- `Cooperative Pathfinding `__ diff --git a/tests/test_space_time_astar.py b/tests/test_space_time_astar.py new file mode 100644 index 0000000000..5290738eb4 --- /dev/null +++ b/tests/test_space_time_astar.py @@ -0,0 +1,33 @@ +from PathPlanning.TimeBasedPathPlanning.GridWithDynamicObstacles import ( + Grid, + ObstacleArrangement, + Position, +) +from PathPlanning.TimeBasedPathPlanning import SpaceTimeAStar as m +import numpy as np +import conftest + + +def test_1(): + start = Position(1, 11) + goal = Position(19, 19) + grid_side_length = 21 + grid = Grid( + np.array([grid_side_length, grid_side_length]), + obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1, + ) + + m.show_animation = False + planner = m.SpaceTimeAStar(grid, start, goal) + + path = planner.plan(False) + + # path should have 28 entries + assert len(path.path) == 31 + + # path should end at the goal + assert path.path[-1].position == goal + + +if __name__ == "__main__": + conftest.run_this_test(__file__) From 67a3ca7138384b4a013974dbf22383252f83686e Mon Sep 17 00:00:00 2001 From: Aglargil <34728006+Aglargil@users.noreply.github.com> Date: Fri, 28 Feb 2025 19:30:24 +0800 Subject: [PATCH 093/181] add state machine (#1172) * add state machine state_machine test state_machine_update * add state machine test/doc * state machine update * state machine generate_plantuml() can show diagram by using https://www.plantuml.com/plantuml/ --- .../StateMachine/robot_behavior_case.py | 111 +++++++ MissionPlanning/StateMachine/state_machine.py | 294 ++++++++++++++++++ docs/index_main.rst | 1 + .../mission_planning_main.rst | 12 + .../state_machine/robot_behavior_case.png | Bin 0 -> 28565 bytes .../state_machine/state_machine_main.rst | 74 +++++ tests/test_state_machine.py | 51 +++ 7 files changed, 543 insertions(+) create mode 100644 MissionPlanning/StateMachine/robot_behavior_case.py create mode 100644 MissionPlanning/StateMachine/state_machine.py create mode 100644 docs/modules/13_mission_planning/mission_planning_main.rst create mode 100644 docs/modules/13_mission_planning/state_machine/robot_behavior_case.png create mode 100644 docs/modules/13_mission_planning/state_machine/state_machine_main.rst create mode 100644 tests/test_state_machine.py diff --git a/MissionPlanning/StateMachine/robot_behavior_case.py b/MissionPlanning/StateMachine/robot_behavior_case.py new file mode 100644 index 0000000000..03ee60ae9f --- /dev/null +++ b/MissionPlanning/StateMachine/robot_behavior_case.py @@ -0,0 +1,111 @@ +""" +A case study of robot behavior using state machine + +author: Wang Zheng (@Aglargil) +""" + +from state_machine import StateMachine + + +class Robot: + def __init__(self): + self.battery = 100 + self.task_progress = 0 + + # Initialize state machine + self.machine = StateMachine("robot_sm", self) + + # Add state transition rules + self.machine.add_transition( + src_state="patrolling", + event="detect_task", + dst_state="executing_task", + guard=None, + action=None, + ) + + self.machine.add_transition( + src_state="executing_task", + event="task_complete", + dst_state="patrolling", + guard=None, + action="https://github.com/reset_task", + ) + + self.machine.add_transition( + src_state="executing_task", + event="low_battery", + dst_state="returning_to_base", + guard="is_battery_low", + ) + + self.machine.add_transition( + src_state="returning_to_base", + event="reach_base", + dst_state="charging", + guard=None, + action=None, + ) + + self.machine.add_transition( + src_state="charging", + event="charge_complete", + dst_state="patrolling", + guard=None, + action="https://github.com/battery_full", + ) + + # Set initial state + self.machine.set_current_state("patrolling") + + def is_battery_low(self): + """Battery level check condition""" + return self.battery < 30 + + def reset_task(self): + """Reset task progress""" + self.task_progress = 0 + print("[Action] Task progress has been reset") + + # Modify state entry callback naming convention (add state_ prefix) + def on_enter_executing_task(self): + print("\n------ Start Executing Task ------") + print(f"Current battery: {self.battery}%") + while self.machine.get_current_state().name == "executing_task": + self.task_progress += 10 + self.battery -= 25 + print( + f"Task progress: {self.task_progress}%, Remaining battery: {self.battery}%" + ) + + if self.task_progress >= 100: + self.machine.process("task_complete") + break + elif self.is_battery_low(): + self.machine.process("low_battery") + break + + def on_enter_returning_to_base(self): + print("\nLow battery, returning to charging station...") + self.machine.process("reach_base") + + def on_enter_charging(self): + print("\n------ Charging ------") + self.battery = 100 + print("Charging complete!") + self.machine.process("charge_complete") + + +# Keep the test section structure the same, only modify the trigger method +if __name__ == "__main__": + robot = Robot() + print(robot.machine.generate_plantuml()) + + print(f"Initial state: {robot.machine.get_current_state().name}") + print("------------") + + # Trigger task detection event + robot.machine.process("detect_task") + + print("\n------------") + print(f"Final state: {robot.machine.get_current_state().name}") diff --git a/MissionPlanning/StateMachine/state_machine.py b/MissionPlanning/StateMachine/state_machine.py new file mode 100644 index 0000000000..de72f0f451 --- /dev/null +++ b/MissionPlanning/StateMachine/state_machine.py @@ -0,0 +1,294 @@ +""" +State Machine + +author: Wang Zheng (@Aglargil) + +Ref: + +- [State Machine] +(https://en.wikipedia.org/wiki/Finite-state_machine) +""" + +import string +from urllib.request import urlopen, Request +from base64 import b64encode +from zlib import compress +from io import BytesIO +from collections.abc import Callable +from matplotlib.image import imread +from matplotlib import pyplot as plt + + +def deflate_and_encode(plantuml_text): + """ + zlib compress the plantuml text and encode it for the plantuml server. + + Ref: https://plantuml.com/en/text-encoding + """ + plantuml_alphabet = ( + string.digits + string.ascii_uppercase + string.ascii_lowercase + "-_" + ) + base64_alphabet = ( + string.ascii_uppercase + string.ascii_lowercase + string.digits + "+/" + ) + b64_to_plantuml = bytes.maketrans( + base64_alphabet.encode("utf-8"), plantuml_alphabet.encode("utf-8") + ) + zlibbed_str = compress(plantuml_text.encode("utf-8")) + compressed_string = zlibbed_str[2:-4] + return b64encode(compressed_string).translate(b64_to_plantuml).decode("utf-8") + + +class State: + def __init__(self, name, on_enter=None, on_exit=None): + self.name = name + self.on_enter = on_enter + self.on_exit = on_exit + + def enter(self): + print(f"entering <{self.name}>") + if self.on_enter: + self.on_enter() + + def exit(self): + print(f"exiting <{self.name}>") + if self.on_exit: + self.on_exit() + + +class StateMachine: + def __init__(self, name: str, model=object): + """Initialize the state machine. + + Args: + name (str): Name of the state machine. + model (object, optional): Model object used to automatically look up callback functions + for states and transitions: + State callbacks: Automatically searches for 'on_enter_' and 'on_exit_' methods. + Transition callbacks: When action or guard parameters are strings, looks up corresponding methods in the model. + + Example: + >>> class MyModel: + ... def on_enter_idle(self): + ... print("Entering idle state") + ... def on_exit_idle(self): + ... print("Exiting idle state") + ... def can_start(self): + ... return True + ... def on_start(self): + ... print("Starting operation") + >>> model = MyModel() + >>> machine = StateMachine("my_machine", model) + """ + self._name = name + self._states = {} + self._events = {} + self._transition_table = {} + self._model = model + self._state: State = None + + def _register_event(self, event: str): + self._events[event] = event + + def _get_state(self, name): + return self._states[name] + + def _get_event(self, name): + return self._events[name] + + def _has_event(self, event: str): + return event in self._events + + def add_transition( + self, + src_state: str | State, + event: str, + dst_state: str | State, + guard: str | Callable = None, + action: str | Callable = None, + ) -> None: + """Add a transition to the state machine. + + Args: + src_state (str | State): The source state where the transition begins. + Can be either a state name or a State object. + event (str): The event that triggers this transition. + dst_state (str | State): The destination state where the transition ends. + Can be either a state name or a State object. + guard (str | Callable, optional): Guard condition for the transition. + If callable: Function that returns bool. + If str: Name of a method in the model class. + If returns True: Transition proceeds. + If returns False: Transition is skipped. + action (str | Callable, optional): Action to execute during transition. + If callable: Function to execute. + If str: Name of a method in the model class. + Executed after guard passes and before entering new state. + + Example: + >>> machine.add_transition( + ... src_state="idle", + ... event="start", + ... dst_state="running", + ... guard="can_start", + ... action="https://github.com/on_start" + ... ) + """ + # Convert string parameters to objects if necessary + self.register_state(src_state) + self._register_event(event) + self.register_state(dst_state) + + def get_state_obj(state): + return state if isinstance(state, State) else self._get_state(state) + + def get_callable(func): + return func if callable(func) else getattr(self._model, func, None) + + src_state_obj = get_state_obj(src_state) + dst_state_obj = get_state_obj(dst_state) + + guard_func = get_callable(guard) if guard else None + action_func = get_callable(action) if action else None + self._transition_table[(src_state_obj.name, event)] = ( + dst_state_obj, + guard_func, + action_func, + ) + + def state_transition(self, src_state: State, event: str): + if (src_state.name, event) not in self._transition_table: + raise ValueError( + f"|{self._name}| invalid transition: <{src_state.name}> : [{event}]" + ) + + dst_state, guard, action = self._transition_table[(src_state.name, event)] + + def call_guard(guard): + if callable(guard): + return guard() + else: + return True + + def call_action(action): + if callable(action): + action() + + if call_guard(guard): + call_action(action) + if src_state.name != dst_state.name: + print( + f"|{self._name}| transitioning from <{src_state.name}> to <{dst_state.name}>" + ) + src_state.exit() + self._state = dst_state + dst_state.enter() + else: + print( + f"|{self._name}| skipping transition from <{src_state.name}> to <{dst_state.name}> because guard failed" + ) + + def register_state(self, state: str | State, on_enter=None, on_exit=None): + """Register a state in the state machine. + + Args: + state (str | State): The state to register. Can be either a string (state name) + or a State object. + on_enter (Callable, optional): Callback function to be executed when entering the state. + If state is a string and on_enter is None, it will look for + a method named 'on_enter_' in the model. + on_exit (Callable, optional): Callback function to be executed when exiting the state. + If state is a string and on_exit is None, it will look for + a method named 'on_exit_' in the model. + Example: + >>> machine.register_state("idle", on_enter=on_enter_idle, on_exit=on_exit_idle) + >>> machine.register_state(State("running", on_enter=on_enter_running, on_exit=on_exit_running)) + """ + if isinstance(state, str): + if on_enter is None: + on_enter = getattr(self._model, "on_enter_" + state, None) + if on_exit is None: + on_exit = getattr(self._model, "on_exit_" + state, None) + self._states[state] = State(state, on_enter, on_exit) + return + + self._states[state.name] = state + + def set_current_state(self, state: State | str): + if isinstance(state, str): + self._state = self._get_state(state) + else: + self._state = state + + def get_current_state(self): + return self._state + + def process(self, event: str) -> None: + """Process an event in the state machine. + + Args: + event: Event name. + + Example: + >>> machine.process("start") + """ + if self._state is None: + raise ValueError("State machine is not initialized") + + if self._has_event(event): + self.state_transition(self._state, event) + else: + raise ValueError(f"Invalid event: {event}") + + def generate_plantuml(self) -> str: + """Generate PlantUML state diagram representation of the state machine. + + Returns: + str: PlantUML state diagram code. + """ + if self._state is None: + raise ValueError("State machine is not initialized") + + plant_uml = ["@startuml"] + plant_uml.append("[*] --> " + self._state.name) + + # Generate transitions + for (src_state, event), ( + dst_state, + guard, + action, + ) in self._transition_table.items(): + transition = f"{src_state} --> {dst_state.name} : {event}" + + # Add guard and action if present + conditions = [] + if guard: + guard_name = guard.__name__ if callable(guard) else guard + conditions.append(f"[{guard_name}]") + if action: + action_name = action.__name__ if callable(action) else action + conditions.append(f"/ {action_name}") + + if conditions: + transition += "\\n" + " ".join(conditions) + + plant_uml.append(transition) + + plant_uml.append("@enduml") + plant_uml_text = "\n".join(plant_uml) + + try: + url = f"http://www.plantuml.com/plantuml/img/%7Bdeflate_and_encode(plant_uml_text)%7D" + headers = {"User-Agent": "Mozilla/5.0"} + request = Request(url, headers=headers) + + with urlopen(request) as response: + content = response.read() + + plt.imshow(imread(BytesIO(content), format="png")) + plt.axis("off") + plt.show() + except Exception as e: + print(f"Error showing PlantUML: {e}") + + return plant_uml_text diff --git a/docs/index_main.rst b/docs/index_main.rst index 75805d1184..65634f32e8 100644 --- a/docs/index_main.rst +++ b/docs/index_main.rst @@ -44,6 +44,7 @@ this graph shows GitHub star history of this project: modules/8_aerial_navigation/aerial_navigation modules/9_bipedal/bipedal modules/10_inverted_pendulum/inverted_pendulum + modules/13_mission_planning/mission_planning modules/11_utils/utils modules/12_appendix/appendix diff --git a/docs/modules/13_mission_planning/mission_planning_main.rst b/docs/modules/13_mission_planning/mission_planning_main.rst new file mode 100644 index 0000000000..385e62f68e --- /dev/null +++ b/docs/modules/13_mission_planning/mission_planning_main.rst @@ -0,0 +1,12 @@ +.. _`Mission Planning`: + +Mission Planning +================ + +Mission planning includes tools such as finite state machines and behavior trees used to describe robot behavior and high level task planning. + +.. toctree:: + :maxdepth: 2 + :caption: Contents + + state_machine/state_machine diff --git a/docs/modules/13_mission_planning/state_machine/robot_behavior_case.png b/docs/modules/13_mission_planning/state_machine/robot_behavior_case.png new file mode 100644 index 0000000000000000000000000000000000000000..fbc1369cbcd71f278a89d38adbcda6da4b737e36 GIT binary patch literal 28565 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 VMQeA>!#)E3qv`LZ-qN)T{cpp^WxxOc literal 0 HcmV?d00001 diff --git a/docs/modules/13_mission_planning/state_machine/state_machine_main.rst b/docs/modules/13_mission_planning/state_machine/state_machine_main.rst new file mode 100644 index 0000000000..abaece1b11 --- /dev/null +++ b/docs/modules/13_mission_planning/state_machine/state_machine_main.rst @@ -0,0 +1,74 @@ +State Machine +------------- + +A state machine is a model used to describe the transitions of an object between different states. It clearly shows how an object changes state based on events and may trigger corresponding actions. + +Core Concepts +~~~~~~~~~~~~~ + +- **State**: A distinct mode or condition of the system (e.g. "Idle", "Running"). Managed by State class with optional on_enter/on_exit callbacks +- **Event**: A trigger signal that may cause state transitions (e.g. "start", "stop") +- **Transition**: A state change path from source to destination state triggered by an event +- **Action**: An operation executed during transition (before entering new state) +- **Guard**: A precondition that must be satisfied to allow transition + +API +~~~ + +.. autoclass:: MissionPlanning.StateMachine.state_machine.StateMachine + :members: add_transition, process, register_state + :special-members: __init__ + +PlantUML Support +~~~~~~~~~~~~~~~~ + +The ``generate_plantuml()`` method creates diagrams showing: + +- Current state (marked with [*] arrow) +- All possible transitions +- Guard conditions in [brackets] +- Actions prefixed with / + +Example +~~~~~~~ + +state machine diagram: ++++++++++++++++++++++++ +.. image:: robot_behavior_case.png + +state transition table: ++++++++++++++++++++++++ +.. list-table:: State Transitions + :header-rows: 1 + :widths: 20 15 20 20 20 + + * - Source State + - Event + - Target State + - Guard + - Action + * - patrolling + - detect_task + - executing_task + - + - + * - executing_task + - task_complete + - patrolling + - + - reset_task + * - executing_task + - low_battery + - returning_to_base + - is_battery_low + - + * - returning_to_base + - reach_base + - charging + - + - + * - charging + - charge_complete + - patrolling + - + - \ No newline at end of file diff --git a/tests/test_state_machine.py b/tests/test_state_machine.py new file mode 100644 index 0000000000..e36a8120fd --- /dev/null +++ b/tests/test_state_machine.py @@ -0,0 +1,51 @@ +import conftest + +from MissionPlanning.StateMachine.state_machine import StateMachine + + +def test_transition(): + sm = StateMachine("state_machine") + sm.add_transition(src_state="idle", event="start", dst_state="running") + sm.set_current_state("idle") + sm.process("start") + assert sm.get_current_state().name == "running" + + +def test_guard(): + class Model: + def can_start(self): + return False + + sm = StateMachine("state_machine", Model()) + sm.add_transition( + src_state="idle", event="start", dst_state="running", guard="can_start" + ) + sm.set_current_state("idle") + sm.process("start") + assert sm.get_current_state().name == "idle" + + +def test_action(): + class Model: + def on_start(self): + self.start_called = True + + model = Model() + sm = StateMachine("state_machine", model) + sm.add_transition( + src_state="idle", event="start", dst_state="running", action="https://github.com/on_start" + ) + sm.set_current_state("idle") + sm.process("start") + assert model.start_called + + +def test_plantuml(): + sm = StateMachine("state_machine") + sm.add_transition(src_state="idle", event="start", dst_state="running") + sm.set_current_state("idle") + assert sm.generate_plantuml() + + +if __name__ == "__main__": + conftest.run_this_test(__file__) From 346037a6e2362ed17870928ccf26992b60b0e54c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 07:26:47 +0900 Subject: [PATCH 094/181] build(deps): bump pytest from 8.3.4 to 8.3.5 in /requirements (#1178) Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 8.3.5. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5) --- updated-dependencies: - dependency-name: pytest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b439ea4266..b46a0e41f1 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -2,7 +2,7 @@ numpy == 2.2.3 scipy == 1.15.2 matplotlib == 3.10.0 cvxpy == 1.5.3 -pytest == 8.3.4 # For unit test +pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test ruff == 0.9.7 # For unit test From cd09abd5e00ca29d15f42c539121cf62751ecd52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:21:33 +0900 Subject: [PATCH 095/181] build(deps): bump ruff from 0.9.7 to 0.9.9 in /requirements (#1179) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.7 to 0.9.9. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.7...0.9.9) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index b46a0e41f1..3e2a1e7c7c 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.9.7 # For unit test +ruff == 0.9.9 # For unit test From 5f3be9bccd6366493ea8d159ceeae26d55ed5250 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Mar 2025 12:52:57 +0900 Subject: [PATCH 096/181] build(deps): bump matplotlib from 3.10.0 to 3.10.1 in /requirements (#1181) Bumps [matplotlib](https://github.com/matplotlib/matplotlib) from 3.10.0 to 3.10.1. - [Release notes](https://github.com/matplotlib/matplotlib/releases) - [Commits](https://github.com/matplotlib/matplotlib/compare/v3.10.0...v3.10.1) --- updated-dependencies: - dependency-name: matplotlib dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3e2a1e7c7c..552bf8482b 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,6 +1,6 @@ numpy == 2.2.3 scipy == 1.15.2 -matplotlib == 3.10.0 +matplotlib == 3.10.1 cvxpy == 1.5.3 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test From 30a61add126e2c21c035d50a7a448ed8eb06afb0 Mon Sep 17 00:00:00 2001 From: Surya Singh <133056660+spnsingh@users.noreply.github.com> Date: Fri, 7 Mar 2025 09:01:37 -0500 Subject: [PATCH 097/181] bug: fix typo on line 6 of SpaceTimeAStar.py (#1182) * bug: fix typo on line 6 of SpaceTimeAStar.py * bug: removed extra line return on line 11 of SpaceTimeAStar.py --- PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py b/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py index 3b3613d695..a7aed41869 100644 --- a/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py +++ b/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py @@ -3,7 +3,7 @@ This script demonstrates the Space-time A* algorithm for path planning in a grid world with moving obstacles. This algorithm is different from normal 2D A* in one key way - the cost (often notated as g(n)) is the number of time steps it took to get to a given node, instead of the number of cells it has - traversed. This ensures the path is time-optimal, while respescting any dynamic obstacles in the environment. + traversed. This ensures the path is time-optimal, while respecting any dynamic obstacles in the environment. Reference: https://www.davidsilver.uk/wp-content/uploads/2020/03/coop-path-AIWisdom.pdf """ From fc160179c06cb4b57f821d83a9bdcf192379bd72 Mon Sep 17 00:00:00 2001 From: Aglargil <34728006+Aglargil@users.noreply.github.com> Date: Sat, 8 Mar 2025 18:26:23 +0800 Subject: [PATCH 098/181] feat: add behavior tree (#1177) * feat: add behavior tree * feat: add behavior tree test * feat: add behavior tree doc * feat: add behavior tree update --- MissionPlanning/BehaviorTree/behavior_tree.py | 690 ++++++++++++++++++ .../BehaviorTree/robot_behavior_case.py | 247 +++++++ .../BehaviorTree/robot_behavior_tree.xml | 57 ++ .../behavior_tree/behavior_tree_main.rst | 104 +++ .../behavior_tree/robot_behavior_case.svg | 22 + .../mission_planning_main.rst | 1 + tests/test_behavior_tree.py | 207 ++++++ 7 files changed, 1328 insertions(+) create mode 100644 MissionPlanning/BehaviorTree/behavior_tree.py create mode 100644 MissionPlanning/BehaviorTree/robot_behavior_case.py create mode 100644 MissionPlanning/BehaviorTree/robot_behavior_tree.xml create mode 100644 docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst create mode 100644 docs/modules/13_mission_planning/behavior_tree/robot_behavior_case.svg create mode 100644 tests/test_behavior_tree.py diff --git a/MissionPlanning/BehaviorTree/behavior_tree.py b/MissionPlanning/BehaviorTree/behavior_tree.py new file mode 100644 index 0000000000..59f4c713f1 --- /dev/null +++ b/MissionPlanning/BehaviorTree/behavior_tree.py @@ -0,0 +1,690 @@ +""" +Behavior Tree + +author: Wang Zheng (@Aglargil) + +Ref: + +- [Behavior Tree](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)) +""" + +import time +import xml.etree.ElementTree as ET +from enum import Enum + + +class Status(Enum): + SUCCESS = "success" + FAILURE = "failure" + RUNNING = "running" + + +class NodeType(Enum): + CONTROL_NODE = "ControlNode" + ACTION_NODE = "ActionNode" + DECORATOR_NODE = "DecoratorNode" + + +class Node: + """ + Base class for all nodes in a behavior tree. + """ + + def __init__(self, name): + self.name = name + self.status = None + + def tick(self) -> Status: + """ + Tick the node. + + Returns: + Status: The status of the node. + """ + raise ValueError("Node is not implemented") + + def tick_and_set_status(self) -> Status: + """ + Tick the node and set the status. + + Returns: + Status: The status of the node. + """ + self.status = self.tick() + return self.status + + def reset(self): + """ + Reset the node. + """ + self.status = None + + def reset_children(self): + """ + Reset the children of the node. + """ + pass + + +class ControlNode(Node): + """ + Base class for all control nodes in a behavior tree. + + Control nodes manage the execution flow of their child nodes according to specific rules. + They typically have multiple children and determine which children to execute and in what order. + """ + + def __init__(self, name): + super().__init__(name) + self.children = [] + self.type = NodeType.CONTROL_NODE + + def not_set_children_raise_error(self): + if len(self.children) == 0: + raise ValueError("Children are not set") + + def reset_children(self): + for child in self.children: + child.reset() + + +class SequenceNode(ControlNode): + """ + Executes child nodes in sequence until one fails or all succeed. + + Returns: + - Returns FAILURE if any child returns FAILURE + - Returns SUCCESS when all children have succeeded + - Returns RUNNING when a child is still running or when moving to the next child + + Example: + .. code-block:: xml + + + + + + """ + + def __init__(self, name): + super().__init__(name) + self.current_child_index = 0 + + def tick(self) -> Status: + self.not_set_children_raise_error() + + if self.current_child_index >= len(self.children): + self.reset_children() + return Status.SUCCESS + status = self.children[self.current_child_index].tick_and_set_status() + if status == Status.FAILURE: + self.reset_children() + return Status.FAILURE + elif status == Status.SUCCESS: + self.current_child_index += 1 + return Status.RUNNING + elif status == Status.RUNNING: + return Status.RUNNING + else: + raise ValueError("Unknown status") + + +class SelectorNode(ControlNode): + """ + Executes child nodes in sequence until one succeeds or all fail. + + Returns: + - Returns SUCCESS if any child returns SUCCESS + - Returns FAILURE when all children have failed + - Returns RUNNING when a child is still running or when moving to the next child + + Examples: + .. code-block:: xml + + + + + + """ + + def __init__(self, name): + super().__init__(name) + self.current_child_index = 0 + + def tick(self) -> Status: + self.not_set_children_raise_error() + + if self.current_child_index >= len(self.children): + self.reset_children() + return Status.FAILURE + status = self.children[self.current_child_index].tick_and_set_status() + if status == Status.SUCCESS: + self.reset_children() + return Status.SUCCESS + elif status == Status.FAILURE: + self.current_child_index += 1 + return Status.RUNNING + elif status == Status.RUNNING: + return Status.RUNNING + else: + raise ValueError("Unknown status") + + +class WhileDoElseNode(ControlNode): + """ + Conditional execution node with three parts: condition, do, and optional else. + + Returns: + First executes the condition node (child[0]) + If condition succeeds, executes do node (child[1]) and returns RUNNING + If condition fails, executes else node (child[2]) if present and returns result of else node + If condition fails and there is no else node, returns SUCCESS + + Example: + .. code-block:: xml + + + + + + + """ + + def __init__(self, name): + super().__init__(name) + + def tick(self) -> Status: + if len(self.children) != 3 and len(self.children) != 2: + raise ValueError("WhileDoElseNode must have exactly 3 or 2 children") + + condition_node = self.children[0] + do_node = self.children[1] + else_node = self.children[2] if len(self.children) == 3 else None + + condition_status = condition_node.tick_and_set_status() + if condition_status == Status.SUCCESS: + do_node.tick_and_set_status() + return Status.RUNNING + elif condition_status == Status.FAILURE: + if else_node is not None: + else_status = else_node.tick_and_set_status() + if else_status == Status.SUCCESS: + self.reset_children() + return Status.SUCCESS + elif else_status == Status.FAILURE: + self.reset_children() + return Status.FAILURE + elif else_status == Status.RUNNING: + return Status.RUNNING + else: + raise ValueError("Unknown status") + else: + self.reset_children() + return Status.SUCCESS + else: + raise ValueError("Unknown status") + + +class ActionNode(Node): + """ + Base class for all action nodes in a behavior tree. + + Action nodes are responsible for performing specific tasks or actions. + They do not have children and are typically used to execute logic or operations. + """ + + def __init__(self, name): + super().__init__(name) + self.type = NodeType.ACTION_NODE + + +class SleepNode(ActionNode): + """ + Sleep node that sleeps for a specified duration. + + Returns: + Returns SUCCESS after the specified duration has passed + Returns RUNNING if the duration has not yet passed + + Example: + .. code-block:: xml + + + """ + + def __init__(self, name, duration): + super().__init__(name) + self.duration = duration + self.start_time = None + + def tick(self) -> Status: + if self.start_time is None: + self.start_time = time.time() + if time.time() - self.start_time > self.duration: + return Status.SUCCESS + return Status.RUNNING + + +class EchoNode(ActionNode): + """ + Echo node that prints a message to the console. + + Returns: + Returns SUCCESS after the message has been printed + + Example: + .. code-block:: xml + + + """ + + def __init__(self, name, message): + super().__init__(name) + self.message = message + + def tick(self) -> Status: + print(self.name, self.message) + return Status.SUCCESS + + +class DecoratorNode(Node): + """ + Base class for all decorator nodes in a behavior tree. + + Decorator nodes modify the behavior of their child node. + They must have a single child and can alter the status of the child node. + """ + + def __init__(self, name): + super().__init__(name) + self.type = NodeType.DECORATOR_NODE + self.child = None + + def not_set_child_raise_error(self): + if self.child is None: + raise ValueError("Child is not set") + + def reset_children(self): + self.child.reset() + + +class InverterNode(DecoratorNode): + """ + Inverter node that inverts the status of its child node. + + Returns: + - Returns SUCCESS if the child returns FAILURE + - Returns FAILURE if the child returns SUCCESS + - Returns RUNNING if the child returns RUNNING + + Examples: + .. code-block:: xml + + + + + """ + + def __init__(self, name): + super().__init__(name) + + def tick(self) -> Status: + self.not_set_child_raise_error() + status = self.child.tick_and_set_status() + return Status.SUCCESS if status == Status.FAILURE else Status.FAILURE + + +class TimeoutNode(DecoratorNode): + """ + Timeout node that fails if the child node takes too long to execute + + Returns: + - FAILURE: If the timeout duration has been exceeded + - Child's status: Otherwise, passes through the status of the child node + + Example: + .. code-block:: xml + + + + + """ + + def __init__(self, name, timeout): + super().__init__(name) + self.timeout = timeout + self.start_time = None + + def tick(self) -> Status: + self.not_set_child_raise_error() + if self.start_time is None: + self.start_time = time.time() + if time.time() - self.start_time > self.timeout: + return Status.FAILURE + print(f"{self.name} is running") + return self.child.tick_and_set_status() + + +class DelayNode(DecoratorNode): + """ + Delay node that delays the execution of its child node for a specified duration. + + Returns: + - Returns RUNNING if the duration has not yet passed + - Returns child's status after the duration has passed + + Example: + .. code-block:: xml + + + + + """ + + def __init__(self, name, delay): + super().__init__(name) + self.delay = delay + self.start_time = None + + def tick(self) -> Status: + self.not_set_child_raise_error() + if self.start_time is None: + self.start_time = time.time() + if time.time() - self.start_time > self.delay: + return self.child.tick_and_set_status() + return Status.RUNNING + + +class ForceSuccessNode(DecoratorNode): + """ + ForceSuccess node that always returns SUCCESS. + + Returns: + - Returns RUNNING if the child returns RUNNING + - Returns SUCCESS if the child returns SUCCESS or FAILURE + """ + + def __init__(self, name): + super().__init__(name) + + def tick(self) -> Status: + self.not_set_child_raise_error() + status = self.child.tick_and_set_status() + if status == Status.FAILURE: + return Status.SUCCESS + return status + + +class ForceFailureNode(DecoratorNode): + """ + ForceFailure node that always returns FAILURE. + + Returns: + - Returns RUNNING if the child returns RUNNING + - Returns FAILURE if the child returns SUCCESS or FAILURE + """ + + def __init__(self, name): + super().__init__(name) + + def tick(self) -> Status: + self.not_set_child_raise_error() + status = self.child.tick_and_set_status() + if status == Status.SUCCESS: + return Status.FAILURE + return status + + +class BehaviorTree: + """ + Behavior tree class that manages the execution of a behavior tree. + """ + + def __init__(self, root): + self.root = root + + def tick(self): + """ + Tick once on the behavior tree. + """ + self.root.tick_and_set_status() + + def reset(self): + """ + Reset the behavior tree. + """ + self.root.reset() + + def tick_while_running(self, interval=None, enable_print=True): + """ + Tick the behavior tree while it is running. + + Args: + interval (float, optional): The interval between ticks. Defaults to None. + enable_print (bool, optional): Whether to print the behavior tree. Defaults to True. + """ + while self.root.tick_and_set_status() == Status.RUNNING: + if enable_print: + self.print_tree() + if interval is not None: + time.sleep(interval) + if enable_print: + self.print_tree() + + def to_text(self, root, indent=0): + """ + Recursively convert the behavior tree to a text representation. + """ + current_text = "" + if root.status == Status.RUNNING: + # yellow + current_text = "\033[93m" + root.name + "\033[0m" + elif root.status == Status.SUCCESS: + # green + current_text = "\033[92m" + root.name + "\033[0m" + elif root.status == Status.FAILURE: + # red + current_text = "\033[91m" + root.name + "\033[0m" + else: + current_text = root.name + if root.type == NodeType.CONTROL_NODE: + current_text = " " * indent + "[" + current_text + "]\n" + for child in root.children: + current_text += self.to_text(child, indent + 2) + elif root.type == NodeType.DECORATOR_NODE: + current_text = " " * indent + "(" + current_text + ")\n" + current_text += self.to_text(root.child, indent + 2) + elif root.type == NodeType.ACTION_NODE: + current_text = " " * indent + "<" + current_text + ">\n" + return current_text + + def print_tree(self): + """ + Print the behavior tree. + + Node print format: + Action: + Decorator: (Decorator) + Control: [Control] + + Node status colors: + Yellow: RUNNING + Green: SUCCESS + Red: FAILURE + """ + text = self.to_text(self.root) + text = text.strip() + print("\033[94m" + "Behavior Tree" + "\033[0m") + print(text) + print("\033[94m" + "Behavior Tree" + "\033[0m") + + +class BehaviorTreeFactory: + """ + Factory class for creating behavior trees from XML strings. + """ + + def __init__(self): + self.node_builders = {} + # Control nodes + self.register_node_builder( + "Sequence", + lambda node: SequenceNode(node.attrib.get("name", SequenceNode.__name__)), + ) + self.register_node_builder( + "Selector", + lambda node: SelectorNode(node.attrib.get("name", SelectorNode.__name__)), + ) + self.register_node_builder( + "WhileDoElse", + lambda node: WhileDoElseNode( + node.attrib.get("name", WhileDoElseNode.__name__) + ), + ) + # Decorator nodes + self.register_node_builder( + "Inverter", + lambda node: InverterNode(node.attrib.get("name", InverterNode.__name__)), + ) + self.register_node_builder( + "Timeout", + lambda node: TimeoutNode( + node.attrib.get("name", SelectorNode.__name__), + float(node.attrib["sec"]), + ), + ) + self.register_node_builder( + "Delay", + lambda node: DelayNode( + node.attrib.get("name", DelayNode.__name__), + float(node.attrib["sec"]), + ), + ) + self.register_node_builder( + "ForceSuccess", + lambda node: ForceSuccessNode( + node.attrib.get("name", ForceSuccessNode.__name__) + ), + ) + self.register_node_builder( + "ForceFailure", + lambda node: ForceFailureNode( + node.attrib.get("name", ForceFailureNode.__name__) + ), + ) + # Action nodes + self.register_node_builder( + "Sleep", + lambda node: SleepNode( + node.attrib.get("name", SleepNode.__name__), + float(node.attrib["sec"]), + ), + ) + self.register_node_builder( + "Echo", + lambda node: EchoNode( + node.attrib.get("name", EchoNode.__name__), + node.attrib["message"], + ), + ) + + def register_node_builder(self, node_name, builder): + """ + Register a builder for a node + + Args: + node_name (str): The name of the node. + builder (function): The builder function. + + Example: + .. code-block:: python + + factory = BehaviorTreeFactory() + factory.register_node_builder( + "MyNode", + lambda node: MyNode( + node.attrib.get("name", MyNode.__name__), + node.attrib["my_param"], + ), + ) + """ + self.node_builders[node_name] = builder + + def build_node(self, node): + """ + Build a node from an XML element. + + Args: + node (Element): The XML element to build the node from. + + Returns: + BehaviorTree Node: the built node + """ + if node.tag in self.node_builders: + root = self.node_builders[node.tag](node) + if root.type == NodeType.CONTROL_NODE: + if len(node) <= 0: + raise ValueError(f"{root.name} Control node must have children") + for child in node: + root.children.append(self.build_node(child)) + elif root.type == NodeType.DECORATOR_NODE: + if len(node) != 1: + raise ValueError( + f"{root.name} Decorator node must have exactly one child" + ) + root.child = self.build_node(node[0]) + elif root.type == NodeType.ACTION_NODE: + if len(node) != 0: + raise ValueError(f"{root.name} Action node must have no children") + return root + else: + raise ValueError(f"Unknown node type: {node.tag}") + + def build_tree(self, xml_string): + """ + Build a behavior tree from an XML string. + + Args: + xml_string (str): The XML string containing the behavior tree. + + Returns: + BehaviorTree: The behavior tree. + """ + xml_tree = ET.fromstring(xml_string) + root = self.build_node(xml_tree) + return BehaviorTree(root) + + def build_tree_from_file(self, file_path): + """ + Build a behavior tree from a file. + + Args: + file_path (str): The path to the file containing the behavior tree. + + Returns: + BehaviorTree: The behavior tree. + """ + with open(file_path) as file: + xml_string = file.read() + return self.build_tree(xml_string) + + +xml_string = """ + + + + + + + + """ + + +def main(): + factory = BehaviorTreeFactory() + tree = factory.build_tree(xml_string) + tree.tick_while_running() + + +if __name__ == "__main__": + main() diff --git a/MissionPlanning/BehaviorTree/robot_behavior_case.py b/MissionPlanning/BehaviorTree/robot_behavior_case.py new file mode 100644 index 0000000000..6c39aa76b2 --- /dev/null +++ b/MissionPlanning/BehaviorTree/robot_behavior_case.py @@ -0,0 +1,247 @@ +""" +Robot Behavior Tree Case + +This file demonstrates how to use a behavior tree to control robot behavior. +""" + +from behavior_tree import ( + BehaviorTreeFactory, + Status, + ActionNode, +) +import time +import random +import os + + +class CheckBatteryNode(ActionNode): + """ + Node to check robot battery level + + If battery level is below threshold, returns FAILURE, otherwise returns SUCCESS + """ + + def __init__(self, name, threshold=20): + super().__init__(name) + self.threshold = threshold + self.battery_level = 100 # Initial battery level is 100% + + def tick(self): + # Simulate battery level decreasing + self.battery_level -= random.randint(1, 5) + print(f"Current battery level: {self.battery_level}%") + + if self.battery_level <= self.threshold: + return Status.FAILURE + return Status.SUCCESS + + +class ChargeBatteryNode(ActionNode): + """ + Node to charge the robot's battery + """ + + def __init__(self, name, charge_rate=10): + super().__init__(name) + self.charge_rate = charge_rate + self.charging_time = 0 + + def tick(self): + # Simulate charging process + if self.charging_time == 0: + print("Starting to charge...") + + self.charging_time += 1 + charge_amount = self.charge_rate * self.charging_time + + if charge_amount >= 100: + print("Charging complete! Battery level: 100%") + self.charging_time = 0 + return Status.SUCCESS + else: + print(f"Charging in progress... Battery level: {min(charge_amount, 100)}%") + return Status.RUNNING + + +class MoveToPositionNode(ActionNode): + """ + Node to move to a specified position + """ + + def __init__(self, name, position, move_duration=2): + super().__init__(name) + self.position = position + self.move_duration = move_duration + self.start_time = None + + def tick(self): + if self.start_time is None: + self.start_time = time.time() + print(f"Starting movement to position {self.position}") + + elapsed_time = time.time() - self.start_time + + if elapsed_time >= self.move_duration: + print(f"Arrived at position {self.position}") + self.start_time = None + return Status.SUCCESS + else: + print( + f"Moving to position {self.position}... {int(elapsed_time / self.move_duration * 100)}% complete" + ) + return Status.RUNNING + + +class DetectObstacleNode(ActionNode): + """ + Node to detect obstacles + """ + + def __init__(self, name, obstacle_probability=0.3): + super().__init__(name) + self.obstacle_probability = obstacle_probability + + def tick(self): + # Use random probability to simulate obstacle detection + if random.random() < self.obstacle_probability: + print("Obstacle detected!") + return Status.SUCCESS + else: + print("No obstacle detected") + return Status.FAILURE + + +class AvoidObstacleNode(ActionNode): + """ + Node to avoid obstacles + """ + + def __init__(self, name, avoid_duration=1.5): + super().__init__(name) + self.avoid_duration = avoid_duration + self.start_time = None + + def tick(self): + if self.start_time is None: + self.start_time = time.time() + print("Starting obstacle avoidance...") + + elapsed_time = time.time() - self.start_time + + if elapsed_time >= self.avoid_duration: + print("Obstacle avoidance complete") + self.start_time = None + return Status.SUCCESS + else: + print("Avoiding obstacle...") + return Status.RUNNING + + +class PerformTaskNode(ActionNode): + """ + Node to perform a specific task + """ + + def __init__(self, name, task_name, task_duration=3): + super().__init__(name) + self.task_name = task_name + self.task_duration = task_duration + self.start_time = None + + def tick(self): + if self.start_time is None: + self.start_time = time.time() + print(f"Starting task: {self.task_name}") + + elapsed_time = time.time() - self.start_time + + if elapsed_time >= self.task_duration: + print(f"Task complete: {self.task_name}") + self.start_time = None + return Status.SUCCESS + else: + print( + f"Performing task: {self.task_name}... {int(elapsed_time / self.task_duration * 100)}% complete" + ) + return Status.RUNNING + + +def create_robot_behavior_tree(): + """ + Create robot behavior tree + """ + + factory = BehaviorTreeFactory() + + # Register custom nodes + factory.register_node_builder( + "CheckBattery", + lambda node: CheckBatteryNode( + node.attrib.get("name", "CheckBattery"), + int(node.attrib.get("threshold", "20")), + ), + ) + + factory.register_node_builder( + "ChargeBattery", + lambda node: ChargeBatteryNode( + node.attrib.get("name", "ChargeBattery"), + int(node.attrib.get("charge_rate", "10")), + ), + ) + + factory.register_node_builder( + "MoveToPosition", + lambda node: MoveToPositionNode( + node.attrib.get("name", "MoveToPosition"), + node.attrib.get("position", "Unknown Position"), + float(node.attrib.get("move_duration", "2")), + ), + ) + + factory.register_node_builder( + "DetectObstacle", + lambda node: DetectObstacleNode( + node.attrib.get("name", "DetectObstacle"), + float(node.attrib.get("obstacle_probability", "0.3")), + ), + ) + + factory.register_node_builder( + "AvoidObstacle", + lambda node: AvoidObstacleNode( + node.attrib.get("name", "AvoidObstacle"), + float(node.attrib.get("avoid_duration", "1.5")), + ), + ) + + factory.register_node_builder( + "PerformTask", + lambda node: PerformTaskNode( + node.attrib.get("name", "PerformTask"), + node.attrib.get("task_name", "Unknown Task"), + float(node.attrib.get("task_duration", "3")), + ), + ) + # Read XML from file + xml_path = os.path.join(os.path.dirname(__file__), "robot_behavior_tree.xml") + return factory.build_tree_from_file(xml_path) + + +def main(): + """ + Main function: Create and run the robot behavior tree + """ + print("Creating robot behavior tree...") + tree = create_robot_behavior_tree() + + print("\nStarting robot behavior tree execution...\n") + # Run for a period of time or until completion + + tree.tick_while_running(interval=0.01) + + print("\nBehavior tree execution complete!") + + +if __name__ == "__main__": + main() diff --git a/MissionPlanning/BehaviorTree/robot_behavior_tree.xml b/MissionPlanning/BehaviorTree/robot_behavior_tree.xml new file mode 100644 index 0000000000..0bca76a3ff --- /dev/null +++ b/MissionPlanning/BehaviorTree/robot_behavior_tree.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst b/docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst new file mode 100644 index 0000000000..ae3e16da81 --- /dev/null +++ b/docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst @@ -0,0 +1,104 @@ +Behavior Tree +------------- + +Behavior Tree is a modular, hierarchical decision model that is widely used in robot control, and game development. +It present some similarities to hierarchical state machines with the key difference that the main building block of a behavior is a task rather than a state. +Behavior Tree have been shown to generalize several other control architectures (https://ieeexplore.ieee.org/document/7790863) + +Core Concepts +~~~~~~~~~~~~~ + +Control Node +++++++++++++ + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.ControlNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.SequenceNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.SelectorNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.WhileDoElseNode + +Action Node +++++++++++++ + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.ActionNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.EchoNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.SleepNode + +Decorator Node +++++++++++++++ + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.DecoratorNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.InverterNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.TimeoutNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.DelayNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.ForceSuccessNode + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.ForceFailureNode + +Behavior Tree Factory ++++++++++++++++++++++ + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.BehaviorTreeFactory + :members: + +Behavior Tree ++++++++++++++ + +.. autoclass:: MissionPlanning.BehaviorTree.behavior_tree.BehaviorTree + :members: + +Example +~~~~~~~ + +Visualize the behavior tree by `xml-tree-visual `_. + +.. image:: ./robot_behavior_case.svg + +Print the behavior tree + +.. code-block:: text + + Behavior Tree + [Robot Main Controller] + [Battery Management] + (Low Battery Detection) + + + + [Patrol Task] + + [Move to Position A] + + [Obstacle Handling A] + [Obstacle Present] + + + + + [Move to Position B] + (Short Wait) + + + (Limited Time Obstacle Handling) + [Obstacle Present] + + + + [Conditional Move to C] + + [Perform Position C Task] + + (Ensure Completion) + + + + + Behavior Tree diff --git a/docs/modules/13_mission_planning/behavior_tree/robot_behavior_case.svg b/docs/modules/13_mission_planning/behavior_tree/robot_behavior_case.svg new file mode 100644 index 0000000000..a3d43aed52 --- /dev/null +++ b/docs/modules/13_mission_planning/behavior_tree/robot_behavior_case.svg @@ -0,0 +1,22 @@ +Selectorname: Robot Main ControllerSequencename: Battery ManagementSequencename: Patrol TaskInvertername: Low Battery DetectionEchoname: Low Battery Warningmessage: Battery level low!Charging neededChargeBatteryname: Charge Batterycharge_rate: 20Echoname: Start Taskmessage: Starting patrol taskSequencename: Move to Position ASequencename: Move to Position BWhileDoElsename: Conditional Move to CEchoname: Complete Patrolmessage: Patrol taskcompleted, returning tocharging stationMoveToPositionname: Return to ChargingStationposition: Charging Stationmove_duration: 4CheckBatteryname: Check Batterythreshold: 30MoveToPositionname: Move to Aposition: Amove_duration: 2Selectorname: Obstacle Handling APerformTaskname: Position A Tasktask_name: Check Device Statustask_duration: 2Delayname: Short Waitsec: 1MoveToPositionname: Move to Bposition: Bmove_duration: 3Timeoutname: Limited Time ObstacleHandlingsec: 2PerformTaskname: Position B Tasktask_name: Data Collectiontask_duration: 2.5CheckBatteryname: Check Sufficient Batterythreshold: 50Sequencename: Perform Position C TaskEchoname: Skip Position Cmessage: Insufficient power,skipping position C taskSequencename: Obstacle PresentEchoname: No Obstaclemessage: Path clearEchoname: Prepare Movementmessage: Preparing to move tonext positionSequencename: Obstacle PresentMoveToPositionname: Move to Cposition: Cmove_duration: 2.5ForceSuccessname: Ensure CompletionDetectObstaclename: Detect Obstacleobstacle_probability: 0.3AvoidObstaclename: Avoid Obstacleavoid_duration: 1.5DetectObstaclename: Detect Obstacleobstacle_probability: 0.4AvoidObstaclename: Avoid Obstacleavoid_duration: 1.8PerformTaskname: Position C Tasktask_name: EnvironmentMonitoringtask_duration: 2 \ No newline at end of file diff --git a/docs/modules/13_mission_planning/mission_planning_main.rst b/docs/modules/13_mission_planning/mission_planning_main.rst index 385e62f68e..c35eacd8d5 100644 --- a/docs/modules/13_mission_planning/mission_planning_main.rst +++ b/docs/modules/13_mission_planning/mission_planning_main.rst @@ -10,3 +10,4 @@ Mission planning includes tools such as finite state machines and behavior trees :caption: Contents state_machine/state_machine + behavior_tree/behavior_tree diff --git a/tests/test_behavior_tree.py b/tests/test_behavior_tree.py new file mode 100644 index 0000000000..0898690448 --- /dev/null +++ b/tests/test_behavior_tree.py @@ -0,0 +1,207 @@ +import pytest +import conftest + +from MissionPlanning.BehaviorTree.behavior_tree import ( + BehaviorTreeFactory, + Status, + ActionNode, +) + + +def test_sequence_node1(): + xml_string = """ + + + + + + + + """ + bt_factory = BehaviorTreeFactory() + bt = bt_factory.build_tree(xml_string) + bt.tick() + assert bt.root.status == Status.RUNNING + assert bt.root.children[0].status == Status.SUCCESS + assert bt.root.children[1].status is None + assert bt.root.children[2].status is None + bt.tick() + bt.tick() + assert bt.root.status == Status.FAILURE + assert bt.root.children[0].status is None + assert bt.root.children[1].status is None + assert bt.root.children[2].status is None + + +def test_sequence_node2(): + xml_string = """ + + + + + + + + """ + bt_factory = BehaviorTreeFactory() + bt = bt_factory.build_tree(xml_string) + bt.tick_while_running() + assert bt.root.status == Status.SUCCESS + assert bt.root.children[0].status is None + assert bt.root.children[1].status is None + assert bt.root.children[2].status is None + + +def test_selector_node1(): + xml_string = """ + + + + + + + + """ + bt_factory = BehaviorTreeFactory() + bt = bt_factory.build_tree(xml_string) + bt.tick() + assert bt.root.status == Status.RUNNING + assert bt.root.children[0].status == Status.FAILURE + assert bt.root.children[1].status is None + assert bt.root.children[2].status is None + bt.tick() + assert bt.root.status == Status.SUCCESS + assert bt.root.children[0].status is None + assert bt.root.children[1].status is None + assert bt.root.children[2].status is None + + +def test_selector_node2(): + xml_string = """ + + + + + + + + + """ + bt_factory = BehaviorTreeFactory() + bt = bt_factory.build_tree(xml_string) + bt.tick_while_running() + assert bt.root.status == Status.FAILURE + assert bt.root.children[0].status is None + assert bt.root.children[1].status is None + + +def test_while_do_else_node(): + xml_string = """ + + + + + + """ + + class CountNode(ActionNode): + def __init__(self, name, count_threshold): + super().__init__(name) + self.count = 0 + self.count_threshold = count_threshold + + def tick(self): + self.count += 1 + if self.count >= self.count_threshold: + return Status.FAILURE + else: + return Status.SUCCESS + + bt_factory = BehaviorTreeFactory() + bt_factory.register_node_builder( + "Count", + lambda node: CountNode( + node.attrib.get("name", CountNode.__name__), + int(node.attrib["count_threshold"]), + ), + ) + bt = bt_factory.build_tree(xml_string) + bt.tick() + assert bt.root.status == Status.RUNNING + assert bt.root.children[0].status == Status.SUCCESS + assert bt.root.children[1].status is Status.SUCCESS + assert bt.root.children[2].status is None + bt.tick() + assert bt.root.status == Status.RUNNING + assert bt.root.children[0].status == Status.SUCCESS + assert bt.root.children[1].status is Status.SUCCESS + assert bt.root.children[2].status is None + bt.tick() + assert bt.root.status == Status.SUCCESS + assert bt.root.children[0].status is None + assert bt.root.children[1].status is None + assert bt.root.children[2].status is None + + +def test_node_children(): + # ControlNode Must have children + xml_string = """ + + + """ + bt_factory = BehaviorTreeFactory() + with pytest.raises(ValueError): + bt_factory.build_tree(xml_string) + + # DecoratorNode Must have child + xml_string = """ + + + """ + with pytest.raises(ValueError): + bt_factory.build_tree(xml_string) + + # DecoratorNode Must have only one child + xml_string = """ + + + + + """ + with pytest.raises(ValueError): + bt_factory.build_tree(xml_string) + + # ActionNode Must have no children + xml_string = """ + + + + """ + with pytest.raises(ValueError): + bt_factory.build_tree(xml_string) + + # WhileDoElse Must have exactly 2 or 3 children + xml_string = """ + + + + """ + with pytest.raises(ValueError): + bt = bt_factory.build_tree(xml_string) + bt.tick() + + xml_string = """ + + + + + + + """ + with pytest.raises(ValueError): + bt = bt_factory.build_tree(xml_string) + bt.tick() + + +if __name__ == "__main__": + conftest.run_this_test(__file__) From e7f893ef1aa503df04c83ec6355d18dd05d36394 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sun, 9 Mar 2025 20:21:24 +0900 Subject: [PATCH 099/181] feat: add linkcode_resolve function for enhanced documentation linking (#1185) * feat: add linkcode_resolve function for enhanced documentation linking * feat: enhance documentation linking with linkcode and relative path resolution --- docs/conf.py | 47 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 919fa9ac76..eeabab11b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,6 +13,8 @@ # import os import sys +from pathlib import Path + sys.path.insert(0, os.path.abspath('../')) @@ -41,7 +43,7 @@ 'matplotlib.sphinxext.plot_directive', 'sphinx.ext.autodoc', 'sphinx.ext.mathjax', - 'sphinx.ext.viewcode', + 'sphinx.ext.linkcode', 'sphinx.ext.napoleon', 'sphinx.ext.imgconverter', 'IPython.sphinxext.ipython_console_highlighting', @@ -184,4 +186,45 @@ ] -# -- Extension configuration ------------------------------------------------- +# -- linkcode setting ------------------------------------------------- + +import inspect +import os +import sys +import functools + +GITHUB_REPO = "https://github.com/AtsushiSakai/PythonRobotics" +GITHUB_BRANCH = "master" + + +def linkcode_resolve(domain, info): + if domain != "py": + return None + + modname = info["module"] + fullname = info["fullname"] + + try: + module = __import__(modname, fromlist=[fullname]) + obj = functools.reduce(getattr, fullname.split("."), module) + except (ImportError, AttributeError): + return None + + try: + srcfile = inspect.getsourcefile(obj) + srcfile = get_relative_path_from_parent(srcfile, "PythonRobotics") + lineno = inspect.getsourcelines(obj)[1] + except Exception: + return None + + return f"{GITHUB_REPO}/blob/{GITHUB_BRANCH}/{srcfile}#L{lineno}" + + +def get_relative_path_from_parent(file_path: str, parent_dir: str): + path = Path(file_path).resolve() + + try: + parent_path = next(p for p in path.parents if p.name == parent_dir) + return str(path.relative_to(parent_path)) + except StopIteration: + raise ValueError(f"Parent directory '{parent_dir}' not found in {file_path}") From 73ebcd85dc967dbb160e29ee4c529b7a64f11e2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 21:26:09 +0900 Subject: [PATCH 100/181] build(deps): bump ruff from 0.9.9 to 0.9.10 in /requirements (#1186) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.9 to 0.9.10. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.9...0.9.10) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 552bf8482b..cf6dea3a46 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.9.9 # For unit test +ruff == 0.9.10 # For unit test From 1308e76424723eb68a4a864117e60c0bbd054df1 Mon Sep 17 00:00:00 2001 From: Jonathan Schwartz Date: Thu, 13 Mar 2025 10:24:53 -0400 Subject: [PATCH 101/181] Add expanded node set to SpaceTime AStar (#1183) * speed up spacetime astar * forgot to include hash impl on Position * add condition to test on node expansions * remove heuristic from Node __hash__ impl * update rst with note about optimization --- .../GridWithDynamicObstacles.py | 3 ++ .../TimeBasedPathPlanning/SpaceTimeAStar.py | 51 +++++++++++++------ .../time_based_grid_search_main.rst | 17 +++++++ tests/test_space_time_astar.py | 1 + 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py b/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py index 7b0190d023..7416ff2f08 100644 --- a/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py +++ b/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py @@ -30,6 +30,9 @@ def __sub__(self, other): f"Subtraction not supported for Position and {type(other)}" ) + def __hash__(self): + return hash((self.x, self.y)) + class ObstacleArrangement(Enum): # Random obstacle positions and movements diff --git a/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py b/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py index a7aed41869..c4e2802d37 100644 --- a/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py +++ b/PathPlanning/TimeBasedPathPlanning/SpaceTimeAStar.py @@ -20,7 +20,7 @@ import random from dataclasses import dataclass from functools import total_ordering - +import time # Seed randomness for reproducibility RANDOM_SEED = 50 @@ -48,11 +48,17 @@ def __lt__(self, other: object): return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}") return (self.time + self.heuristic) < (other.time + other.heuristic) + """ + Note: cost and heuristic are not included in eq or hash, since they will always be the same + for a given (position, time) pair. Including either cost or heuristic would be redundant. + """ def __eq__(self, other: object): if not isinstance(other, Node): return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}") return self.position == other.position and self.time == other.time + def __hash__(self): + return hash((self.position, self.time)) class NodePath: path: list[Node] @@ -86,6 +92,8 @@ class SpaceTimeAStar: grid: Grid start: Position goal: Position + # Used to evaluate solutions + expanded_node_count: int = -1 def __init__(self, grid: Grid, start: Position, goal: Position): self.grid = grid @@ -98,7 +106,8 @@ def plan(self, verbose: bool = False) -> NodePath: open_set, Node(self.start, 0, self.calculate_heuristic(self.start), -1) ) - expanded_set: list[Node] = [] + expanded_list: list[Node] = [] + expanded_set: set[Node] = set() while open_set: expanded_node: Node = heapq.heappop(open_set) if verbose: @@ -110,23 +119,25 @@ def plan(self, verbose: bool = False) -> NodePath: continue if expanded_node.position == self.goal: - print(f"Found path to goal after {len(expanded_set)} expansions") + print(f"Found path to goal after {len(expanded_list)} expansions") path = [] path_walker: Node = expanded_node while True: path.append(path_walker) if path_walker.parent_index == -1: break - path_walker = expanded_set[path_walker.parent_index] + path_walker = expanded_list[path_walker.parent_index] # reverse path so it goes start -> goal path.reverse() + self.expanded_node_count = len(expanded_set) return NodePath(path) - expanded_idx = len(expanded_set) - expanded_set.append(expanded_node) + expanded_idx = len(expanded_list) + expanded_list.append(expanded_node) + expanded_set.add(expanded_node) - for child in self.generate_successors(expanded_node, expanded_idx, verbose): + for child in self.generate_successors(expanded_node, expanded_idx, verbose, expanded_set): heapq.heappush(open_set, child) raise Exception("No path found") @@ -135,7 +146,7 @@ def plan(self, verbose: bool = False) -> NodePath: Generate possible successors of the provided `parent_node` """ def generate_successors( - self, parent_node: Node, parent_node_idx: int, verbose: bool + self, parent_node: Node, parent_node_idx: int, verbose: bool, expanded_set: set[Node] ) -> Generator[Node, None, None]: diffs = [ Position(0, 0), @@ -146,13 +157,17 @@ def generate_successors( ] for diff in diffs: new_pos = parent_node.position + diff + new_node = Node( + new_pos, + parent_node.time + 1, + self.calculate_heuristic(new_pos), + parent_node_idx, + ) + + if new_node in expanded_set: + continue + if self.grid.valid_position(new_pos, parent_node.time + 1): - new_node = Node( - new_pos, - parent_node.time + 1, - self.calculate_heuristic(new_pos), - parent_node_idx, - ) if verbose: print("\tNew successor node: ", new_node) yield new_node @@ -166,9 +181,12 @@ def calculate_heuristic(self, position) -> int: verbose = False def main(): - start = Position(1, 11) + start = Position(1, 5) goal = Position(19, 19) grid_side_length = 21 + + start_time = time.time() + grid = Grid( np.array([grid_side_length, grid_side_length]), num_obstacles=40, @@ -179,6 +197,9 @@ def main(): planner = SpaceTimeAStar(grid, start, goal) path = planner.plan(verbose) + runtime = time.time() - start_time + print(f"Planning took: {runtime:.5f} seconds") + if verbose: print(f"Path: {path}") diff --git a/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst index 0c26badec7..48dc1289c2 100644 --- a/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst +++ b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst @@ -16,6 +16,23 @@ Using a time-based cost and heuristic ensures the path found is optimal in terms The cost is the amount of time it takes to reach a given node, and the heuristic is the minimum amount of time it could take to reach the goal from that node, disregarding all obstacles. For a simple scenario where the robot can move 1 cell per time step and stop and go as it pleases, the heuristic for time is equivalent to the heuristic for distance. +One optimization that was added in `this PR `__ was to add an expanded set to the algorithm. The algorithm will not expand nodes that are already in that set. This greatly reduces the number of node expansions needed to find a path, since no duplicates are expanded. It also helps to reduce the amount of memory the algorithm uses. + +Before:: + + Found path to goal after 204490 expansions + Planning took: 1.72464 seconds + Memory usage (RSS): 68.19 MB + + +After:: + + Found path to goal after 2348 expansions + Planning took: 0.01550 seconds + Memory usage (RSS): 64.85 MB + +When starting at (1, 11) in the structured obstacle arrangement (second of the two gifs above). + References: ~~~~~~~~~~~ diff --git a/tests/test_space_time_astar.py b/tests/test_space_time_astar.py index 5290738eb4..390c7732dc 100644 --- a/tests/test_space_time_astar.py +++ b/tests/test_space_time_astar.py @@ -28,6 +28,7 @@ def test_1(): # path should end at the goal assert path.path[-1].position == goal + assert planner.expanded_node_count < 1000 if __name__ == "__main__": conftest.run_this_test(__file__) From 41187d67b11f06265431b87dcf0b9da679345249 Mon Sep 17 00:00:00 2001 From: Zichao Yang Date: Sun, 16 Mar 2025 21:40:11 +0800 Subject: [PATCH 102/181] fix "ModuleNotFoundError: No module named 'utils'" (#1188) --- .../lqr_speed_steer_control/lqr_speed_steer_control.py | 4 ++-- PathTracking/lqr_steer_control/lqr_steer_control.py | 4 ++-- PathTracking/rear_wheel_feedback/rear_wheel_feedback.py | 7 +++++-- PathTracking/stanley_controller/stanley_controller.py | 4 ++-- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py b/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py index d85fa98a84..5831d02d30 100644 --- a/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py +++ b/PathTracking/lqr_speed_steer_control/lqr_speed_steer_control.py @@ -11,9 +11,9 @@ import numpy as np import scipy.linalg as la import pathlib -from utils.angle import angle_mod -sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) +from utils.angle import angle_mod from PathPlanning.CubicSpline import cubic_spline_planner # === Parameters ===== diff --git a/PathTracking/lqr_steer_control/lqr_steer_control.py b/PathTracking/lqr_steer_control/lqr_steer_control.py index 461d6e3722..3c066917ff 100644 --- a/PathTracking/lqr_steer_control/lqr_steer_control.py +++ b/PathTracking/lqr_steer_control/lqr_steer_control.py @@ -11,9 +11,9 @@ import numpy as np import sys import pathlib -from utils.angle import angle_mod -sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) +from utils.angle import angle_mod from PathPlanning.CubicSpline import cubic_spline_planner Kp = 1.0 # speed proportional gain diff --git a/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py b/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py index 1702bd47dd..fd04fb6d17 100644 --- a/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py +++ b/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py @@ -8,11 +8,14 @@ import matplotlib.pyplot as plt import math import numpy as np -from utils.angle import angle_mod - +import sys +import pathlib from scipy import interpolate from scipy import optimize +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) +from utils.angle import angle_mod + Kp = 1.0 # speed proportional gain # steering control parameter KTH = 1.0 diff --git a/PathTracking/stanley_controller/stanley_controller.py b/PathTracking/stanley_controller/stanley_controller.py index 08aa049395..bc98175f17 100644 --- a/PathTracking/stanley_controller/stanley_controller.py +++ b/PathTracking/stanley_controller/stanley_controller.py @@ -13,9 +13,9 @@ import matplotlib.pyplot as plt import sys import pathlib -from utils.angle import angle_mod -sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) +sys.path.append(str(pathlib.Path(__file__).parent.parent.parent)) +from utils.angle import angle_mod from PathPlanning.CubicSpline import cubic_spline_planner k = 0.5 # control gain From aa61a6ea570e013d346c40874f8a33890ec66d28 Mon Sep 17 00:00:00 2001 From: Jonathan Schwartz Date: Mon, 17 Mar 2025 09:01:07 -0400 Subject: [PATCH 103/181] Safe Interval Path Planner (#1184) * it works and is WAY faster than a* * some bug fixes from testing different scenarios * add some docs & address todos * add sipp test * spiff up comments revert changes in speed-up * explain what the removal is doing * linting * fix docs build * docs formatting * revert change to file (maybe linter did it?) * point at gifs in gifs repo * use raw githubusercontent gif links * change formatting on planner results * format output differently * proper formatting final * missing underline * revert unintended change * grammar + add descriptions for gifs * missing :: * add title to gifs section * dont use sections for sub-sections * constent a* spelling * Update PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update tests/test_safe_interval_path_planner.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst Co-authored-by: Atsushi Sakai * Update PathPlanning/TimeBasedPathPlanning/SafeInterval.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * addressing comments * revert np.full change --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Atsushi Sakai --- .../GridWithDynamicObstacles.py | 57 +++- .../TimeBasedPathPlanning/SafeInterval.py | 303 ++++++++++++++++++ .../time_based_grid_search_main.rst | 43 ++- tests/test_safe_interval_path_planner.py | 33 ++ 4 files changed, 434 insertions(+), 2 deletions(-) create mode 100644 PathPlanning/TimeBasedPathPlanning/SafeInterval.py create mode 100644 tests/test_safe_interval_path_planner.py diff --git a/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py b/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py index 7416ff2f08..2a47023f8c 100644 --- a/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py +++ b/PathPlanning/TimeBasedPathPlanning/GridWithDynamicObstacles.py @@ -33,6 +33,10 @@ def __sub__(self, other): def __hash__(self): return hash((self.x, self.y)) +@dataclass +class Interval: + start_time: int + end_time: int class ObstacleArrangement(Enum): # Random obstacle positions and movements @@ -40,6 +44,14 @@ class ObstacleArrangement(Enum): # Obstacles start in a line in y at center of grid and move side-to-side in x ARRANGEMENT1 = 1 +""" +Generates a 2d numpy array with lists for elements. +""" +def empty_2d_array_of_lists(x: int, y: int) -> np.ndarray: + arr = np.empty((x, y), dtype=object) + # assign each element individually - np.full creates references to the same list + arr[:] = [[[] for _ in range(y)] for _ in range(x)] + return arr class Grid: # Set in constructor @@ -89,7 +101,7 @@ def __init__( """ def generate_dynamic_obstacles(self, obs_count: int) -> list[list[Position]]: obstacle_paths = [] - for _ in (0, obs_count): + for _ in range(0, obs_count): # Sample until a free starting space is found initial_position = self.sample_random_position() while not self.valid_obstacle_position(initial_position, 0): @@ -234,6 +246,49 @@ def get_obstacle_positions_at_time(self, t: int) -> tuple[list[int], list[int]]: y_positions.append(obs_path[t].y) return (x_positions, y_positions) + """ + Returns safe intervals for each cell. + """ + def get_safe_intervals(self) -> np.ndarray: + intervals = empty_2d_array_of_lists(self.grid_size[0], self.grid_size[1]) + for x in range(intervals.shape[0]): + for y in range(intervals.shape[1]): + intervals[x, y] = self.get_safe_intervals_at_cell(Position(x, y)) + + return intervals + + """ + Generate the safe intervals for a given cell. The intervals will be in order of start time. + ex: Interval (2, 3) will be before Interval (4, 5) + """ + def get_safe_intervals_at_cell(self, cell: Position) -> list[Interval]: + vals = self.reservation_matrix[cell.x, cell.y, :] + # Find where the array is zero + zero_mask = (vals == 0) + + # Identify transitions between zero and nonzero elements + diff = np.diff(zero_mask.astype(int)) + + # Start indices: where zeros begin (1 after a nonzero) + start_indices = np.where(diff == 1)[0] + 1 + + # End indices: where zeros stop (just before a nonzero) + end_indices = np.where(diff == -1)[0] + + # Handle edge cases if the array starts or ends with zeros + if zero_mask[0]: # If the first element is zero, add index 0 to start_indices + start_indices = np.insert(start_indices, 0, 0) + if zero_mask[-1]: # If the last element is zero, add the last index to end_indices + end_indices = np.append(end_indices, len(vals) - 1) + + # Create pairs of (first zero, last zero) + intervals = [Interval(int(start), int(end)) for start, end in zip(start_indices, end_indices)] + + # Remove intervals where a cell is only free for one time step. Those intervals not provide enough time to + # move into and out of the cell each take 1 time step, and the cell is considered occupied during + # both the time step when it is entering the cell, and the time step when it is leaving the cell. + intervals = [interval for interval in intervals if interval.start_time != interval.end_time] + return intervals show_animation = True diff --git a/PathPlanning/TimeBasedPathPlanning/SafeInterval.py b/PathPlanning/TimeBasedPathPlanning/SafeInterval.py new file mode 100644 index 0000000000..7fea10c67f --- /dev/null +++ b/PathPlanning/TimeBasedPathPlanning/SafeInterval.py @@ -0,0 +1,303 @@ +""" +Safe interval path planner + This script implements a safe-interval path planner for a 2d grid with dynamic obstacles. It is faster than + SpaceTime A* because it reduces the number of redundant node expansions by pre-computing regions of adjacent + time steps that are safe ("safe intervals") at each position. This allows the algorithm to skip expanding nodes + that are in intervals that have already been visited earlier. + + Reference: https://www.cs.cmu.edu/~maxim/files/sipp_icra11.pdf +""" + +import numpy as np +import matplotlib.pyplot as plt +from PathPlanning.TimeBasedPathPlanning.GridWithDynamicObstacles import ( + Grid, + Interval, + ObstacleArrangement, + Position, + empty_2d_array_of_lists, +) +import heapq +import random +from dataclasses import dataclass +from functools import total_ordering +import time + +@dataclass() +# Note: Total_ordering is used instead of adding `order=True` to the @dataclass decorator because +# this class needs to override the __lt__ and __eq__ methods to ignore parent_index. The Parent +# index and interval member variables are just used to track the path found by the algorithm, +# and has no effect on the quality of a node. +@total_ordering +class Node: + position: Position + time: int + heuristic: int + parent_index: int + interval: Interval + + """ + This is what is used to drive node expansion. The node with the lowest value is expanded next. + This comparison prioritizes the node with the lowest cost-to-come (self.time) + cost-to-go (self.heuristic) + """ + def __lt__(self, other: object): + if not isinstance(other, Node): + return NotImplementedError(f"Cannot compare Node with object of type: {type(other)}") + return (self.time + self.heuristic) < (other.time + other.heuristic) + + """ + Equality only cares about position and time. Heuristic and interval will always be the same for a given + (position, time) pairing, so they are not considered in equality. + """ + def __eq__(self, other: object): + if not isinstance(other, Node): + return NotImplemented + return self.position == other.position and self.time == other.time + +@dataclass +class EntryTimeAndInterval: + entry_time: int + interval: Interval + +class NodePath: + path: list[Node] + positions_at_time: dict[int, Position] = {} + + def __init__(self, path: list[Node]): + self.path = path + for (i, node) in enumerate(path): + if i > 0: + # account for waiting in interval at previous node + prev_node = path[i-1] + for t in range(prev_node.time, node.time): + self.positions_at_time[t] = prev_node.position + + self.positions_at_time[node.time] = node.position + + """ + Get the position of the path at a given time + """ + def get_position(self, time: int) -> Position | None: + return self.positions_at_time.get(time) + + """ + Time stamp of the last node in the path + """ + def goal_reached_time(self) -> int: + return self.path[-1].time + + def __repr__(self): + repr_string = "" + for i, node in enumerate(self.path): + repr_string += f"{i}: {node}\n" + return repr_string + + +class SafeIntervalPathPlanner: + grid: Grid + start: Position + goal: Position + + def __init__(self, grid: Grid, start: Position, goal: Position): + self.grid = grid + self.start = start + self.goal = goal + + # Seed randomness for reproducibility + RANDOM_SEED = 50 + random.seed(RANDOM_SEED) + np.random.seed(RANDOM_SEED) + + """ + Generate a plan given the loaded problem statement. Raises an exception if it fails to find a path. + Arguments: + verbose (bool): set to True to print debug information + """ + def plan(self, verbose: bool = False) -> NodePath: + + safe_intervals = self.grid.get_safe_intervals() + + open_set: list[Node] = [] + first_node_interval = safe_intervals[self.start.x, self.start.y][0] + heapq.heappush( + open_set, Node(self.start, 0, self.calculate_heuristic(self.start), -1, first_node_interval) + ) + + expanded_list: list[Node] = [] + visited_intervals = empty_2d_array_of_lists(self.grid.grid_size[0], self.grid.grid_size[1]) + while open_set: + expanded_node: Node = heapq.heappop(open_set) + if verbose: + print("Expanded node:", expanded_node) + + if expanded_node.time + 1 >= self.grid.time_limit: + if verbose: + print(f"\tSkipping node that is past time limit: {expanded_node}") + continue + + if expanded_node.position == self.goal: + print(f"Found path to goal after {len(expanded_list)} expansions") + path = [] + path_walker: Node = expanded_node + while True: + path.append(path_walker) + if path_walker.parent_index == -1: + break + path_walker = expanded_list[path_walker.parent_index] + + # reverse path so it goes start -> goal + path.reverse() + return NodePath(path) + + expanded_idx = len(expanded_list) + expanded_list.append(expanded_node) + entry_time_and_node = EntryTimeAndInterval(expanded_node.time, expanded_node.interval) + add_entry_to_visited_intervals_array(entry_time_and_node, visited_intervals, expanded_node) + + for child in self.generate_successors(expanded_node, expanded_idx, safe_intervals, visited_intervals): + heapq.heappush(open_set, child) + + raise Exception("No path found") + + """ + Generate list of possible successors of the provided `parent_node` that are worth expanding + """ + def generate_successors( + self, parent_node: Node, parent_node_idx: int, intervals: np.ndarray, visited_intervals: np.ndarray + ) -> list[Node]: + new_nodes = [] + diffs = [ + Position(0, 0), + Position(1, 0), + Position(-1, 0), + Position(0, 1), + Position(0, -1), + ] + for diff in diffs: + new_pos = parent_node.position + diff + if not self.grid.inside_grid_bounds(new_pos): + continue + + current_interval = parent_node.interval + + new_cell_intervals: list[Interval] = intervals[new_pos.x, new_pos.y] + for interval in new_cell_intervals: + # if interval starts after current ends, break + # assumption: intervals are sorted by start time, so all future intervals will hit this condition as well + if interval.start_time > current_interval.end_time: + break + + # if interval ends before current starts, skip + if interval.end_time < current_interval.start_time: + continue + + # if we have already expanded a node in this interval with a <= starting time, skip + better_node_expanded = False + for visited in visited_intervals[new_pos.x, new_pos.y]: + if interval == visited.interval and visited.entry_time <= parent_node.time + 1: + better_node_expanded = True + break + if better_node_expanded: + continue + + # We know there is a node worth expanding. Generate successor at the earliest possible time the + # new interval can be entered + for possible_t in range(max(parent_node.time + 1, interval.start_time), min(current_interval.end_time, interval.end_time)): + if self.grid.valid_position(new_pos, possible_t): + new_nodes.append(Node( + new_pos, + # entry is max of interval start and parent node time + 1 (get there as soon as possible) + max(interval.start_time, parent_node.time + 1), + self.calculate_heuristic(new_pos), + parent_node_idx, + interval, + )) + # break because all t's after this will make nodes with a higher cost, the same heuristic, and are in the same interval + break + + return new_nodes + + """ + Calculate the heuristic for a given position - Manhattan distance to the goal + """ + def calculate_heuristic(self, position) -> int: + diff = self.goal - position + return abs(diff.x) + abs(diff.y) + + +""" +Adds a new entry to the visited intervals array. If the entry is already present, the entry time is updated if the new +entry time is better. Otherwise, the entry is added to `visited_intervals` at the position of `expanded_node`. +""" +def add_entry_to_visited_intervals_array(entry_time_and_interval: EntryTimeAndInterval, visited_intervals: np.ndarray, expanded_node: Node): + # if entry is present, update entry time if better + for existing_entry_and_interval in visited_intervals[expanded_node.position.x, expanded_node.position.y]: + if existing_entry_and_interval.interval == entry_time_and_interval.interval: + existing_entry_and_interval.entry_time = min(existing_entry_and_interval.entry_time, entry_time_and_interval.entry_time) + + # Otherwise, append + visited_intervals[expanded_node.position.x, expanded_node.position.y].append(entry_time_and_interval) + + +show_animation = True +verbose = False + +def main(): + start = Position(1, 18) + goal = Position(19, 19) + grid_side_length = 21 + + start_time = time.time() + + grid = Grid( + np.array([grid_side_length, grid_side_length]), + num_obstacles=250, + obstacle_avoid_points=[start, goal], + obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1, + # obstacle_arrangement=ObstacleArrangement.RANDOM, + ) + + planner = SafeIntervalPathPlanner(grid, start, goal) + path = planner.plan(verbose) + runtime = time.time() - start_time + print(f"Planning took: {runtime:.5f} seconds") + + if verbose: + print(f"Path: {path}") + + if not show_animation: + return + + fig = plt.figure(figsize=(10, 7)) + ax = fig.add_subplot( + autoscale_on=False, + xlim=(0, grid.grid_size[0] - 1), + ylim=(0, grid.grid_size[1] - 1), + ) + ax.set_aspect("equal") + ax.grid() + ax.set_xticks(np.arange(0, grid_side_length, 1)) + ax.set_yticks(np.arange(0, grid_side_length, 1)) + + (start_and_goal,) = ax.plot([], [], "mD", ms=15, label="Start and Goal") + start_and_goal.set_data([start.x, goal.x], [start.y, goal.y]) + (obs_points,) = ax.plot([], [], "ro", ms=15, label="Obstacles") + (path_points,) = ax.plot([], [], "bo", ms=10, label="Path Found") + ax.legend(bbox_to_anchor=(1.05, 1)) + + # for stopping simulation with the esc key. + plt.gcf().canvas.mpl_connect( + "key_release_event", lambda event: [exit(0) if event.key == "escape" else None] + ) + + for i in range(0, path.goal_reached_time() + 1): + obs_positions = grid.get_obstacle_positions_at_time(i) + obs_points.set_data(obs_positions[0], obs_positions[1]) + path_position = path.get_position(i) + path_points.set_data([path_position.x], [path_position.y]) + plt.pause(0.2) + plt.show() + + +if __name__ == "__main__": + main() diff --git a/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst index 48dc1289c2..04001d4504 100644 --- a/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst +++ b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst @@ -33,7 +33,48 @@ After:: When starting at (1, 11) in the structured obstacle arrangement (second of the two gifs above). -References: + +Safe Interval Path Planning +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The safe interval path planning algorithm is described in this paper: + +`SIPP: Safe Interval Path Planning for Dynamic Environments `__ + +It is faster than space-time A* because it pre-computes the intervals of time that are unoccupied in each cell. This allows it to reduce the number of successor node it generates by avoiding nodes within the same interval. + +**Comparison with SpaceTime A*:** + +Arrangement 1 starting at (1, 18) + +SafeInterval planner:: + + Found path to goal after 322 expansions + Planning took: 0.00730 seconds + +SpaceTime A*:: + + Found path to goal after 2717154 expansions + Planning took: 20.51330 seconds + +**Benchmarking the Safe Interval Path Planner:** + +250 random obstacles:: + + Found path to goal after 764 expansions + Planning took: 0.60596 seconds + +.. image:: https://raw.githubusercontent.com/AtsushiSakai/PythonRoboticsGifs/refs/heads/master/PathPlanning/TimeBasedPathPlanning/SafeIntervalPathPlanner/path_animation.gif + +Arrangement 1 starting at (1, 18):: + + Found path to goal after 322 expansions + Planning took: 0.00730 seconds + +.. image:: https://raw.githubusercontent.com/AtsushiSakai/PythonRoboticsGifs/refs/heads/master/PathPlanning/TimeBasedPathPlanning/SafeIntervalPathPlanner/path_animation2.gif + +References ~~~~~~~~~~~ - `Cooperative Pathfinding `__ +- `SIPP: Safe Interval Path Planning for Dynamic Environments `__ diff --git a/tests/test_safe_interval_path_planner.py b/tests/test_safe_interval_path_planner.py new file mode 100644 index 0000000000..f1fbf90d2a --- /dev/null +++ b/tests/test_safe_interval_path_planner.py @@ -0,0 +1,33 @@ +from PathPlanning.TimeBasedPathPlanning.GridWithDynamicObstacles import ( + Grid, + ObstacleArrangement, + Position, +) +from PathPlanning.TimeBasedPathPlanning import SafeInterval as m +import numpy as np +import conftest + + +def test_1(): + start = Position(1, 11) + goal = Position(19, 19) + grid_side_length = 21 + grid = Grid( + np.array([grid_side_length, grid_side_length]), + obstacle_arrangement=ObstacleArrangement.ARRANGEMENT1, + ) + + m.show_animation = False + planner = m.SafeIntervalPathPlanner(grid, start, goal) + + path = planner.plan(False) + + # path should have 31 entries + assert len(path.path) == 31 + + # path should end at the goal + assert path.path[-1].position == goal + + +if __name__ == "__main__": + conftest.run_this_test(__file__) From 19fc48d60fea057f6c14365c502db4d18490c3a8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:22:31 +0900 Subject: [PATCH 104/181] build(deps): bump numpy from 2.2.3 to 2.2.4 in /requirements (#1189) Bumps [numpy](https://github.com/numpy/numpy) from 2.2.3 to 2.2.4. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/main/doc/RELEASE_WALKTHROUGH.rst) - [Commits](https://github.com/numpy/numpy/compare/v2.2.3...v2.2.4) --- updated-dependencies: - dependency-name: numpy dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index cf6dea3a46..3ea4197736 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,4 +1,4 @@ -numpy == 2.2.3 +numpy == 2.2.4 scipy == 1.15.2 matplotlib == 3.10.1 cvxpy == 1.5.3 From 764ba21f86fc0d5c2ae5b613449ca81ea6406036 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 20:29:12 +0900 Subject: [PATCH 105/181] build(deps): bump ruff from 0.9.10 to 0.11.0 in /requirements (#1191) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.9.10 to 0.11.0. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.9.10...0.11.0) --- updated-dependencies: - dependency-name: ruff dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 3ea4197736..5438678f87 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.9.10 # For unit test +ruff == 0.11.0 # For unit test From 1c1596be21f015b3b2c3db6489b93cd94d6eca01 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Mon, 28 Apr 2025 08:51:09 +0900 Subject: [PATCH 106/181] docs: enhance contribution guide with instructions for linking code in GitHub (#1202) --- docs/_static/img/source_link_1.png | Bin 0 -> 87987 bytes docs/_static/img/source_link_2.png | Bin 0 -> 237071 bytes .../3_how_to_contribute_main.rst | 13 +++++++++++++ 3 files changed, 13 insertions(+) create mode 100644 docs/_static/img/source_link_1.png create mode 100644 docs/_static/img/source_link_2.png diff --git a/docs/_static/img/source_link_1.png b/docs/_static/img/source_link_1.png new file mode 100644 index 0000000000000000000000000000000000000000..53febda7cbc62ba625f3f179b399552c7da351e5 GIT binary patch literal 87987 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%0D@AHN5z) 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

S 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 literal 0 HcmV?d00001 diff --git a/docs/modules/0_getting_started/3_how_to_contribute_main.rst b/docs/modules/0_getting_started/3_how_to_contribute_main.rst index 1e61760649..30e3001f36 100644 --- a/docs/modules/0_getting_started/3_how_to_contribute_main.rst +++ b/docs/modules/0_getting_started/3_how_to_contribute_main.rst @@ -107,6 +107,18 @@ mathematics and the algorithm of the example. Documentations related codes should be in the python script as the header comments of the script or docstrings of each function. +Also, each document should have a link to the code in Github. +You can easily add the link by using the `.. autoclass::`, `.. autofunction::`, and `.. automodule` by Sphinx's `autodoc`_ module. + +Using this `autodoc`_ module, the generated documentations have the link to the code in Github like: + +.. image:: /_static/img/source_link_1.png + +When you click the link, you will jump to the source code in Github like: + +.. image:: /_static/img/source_link_2.png + + .. _`submit a pull request`: @@ -210,5 +222,6 @@ Current Major Sponsors: .. _`1Password`: https://github.com/1Password/for-open-source .. _`matplotrecorder`: https://github.com/AtsushiSakai/matplotrecorder .. _`PythonRoboticsGifs`: https://github.com/AtsushiSakai/PythonRoboticsGifs +.. _`autodoc`: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html From f466f25f5328e019bd1e0ccc62b176ff2fc72187 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Apr 2025 08:53:35 +0900 Subject: [PATCH 107/181] build(deps): bump ruff from 0.11.0 to 0.11.6 in /requirements (#1201) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.0 to 0.11.6. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.0...0.11.6) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.6 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 5438678f87..f3e6c3dc51 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -5,4 +5,4 @@ cvxpy == 1.5.3 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.11.0 # For unit test +ruff == 0.11.6 # For unit test From af0442d358b1deca5710c0c6812424b12b39f7e7 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Thu, 1 May 2025 13:08:29 +0900 Subject: [PATCH 108/181] build(deps): update cvxpy version from 1.5.3 to 1.6.5 in requirements (#1203) * build(deps): update cvxpy version from 1.5.3 to 1.6.5 in requirements * Add ECOS solver and improve solver handling for stability Added ECOS to requirements and enhanced compatibility with cvxpy solvers by specifying 'order' for matrix reshaping. Updated solver configurations in rocket landing and pendulum control for consistency and reliability. Improved test behavior by enforcing stricter warning handling in pytest. --- .../rocket_powered_landing/rocket_powered_landing.py | 9 +++++---- InvertedPendulum/inverted_pendulum_mpc_control.py | 2 +- requirements/requirements.txt | 3 ++- tests/conftest.py | 2 +- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py b/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py index 239f3629c1..1918dc1cee 100644 --- a/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py +++ b/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py @@ -31,6 +31,7 @@ W_DELTA_SIGMA = 1e-1 # difference in flight time W_NU = 1e5 # virtual control +print(cvxpy.installed_solvers()) solver = 'ECOS' verbose_solver = False @@ -462,11 +463,11 @@ def __init__(self, m, K): # x_t+1 = A_*x_t+B_*U_t+C_*U_T+1*S_*sigma+zbar+nu constraints += [ self.var['X'][:, k + 1] == - cvxpy.reshape(self.par['A_bar'][:, k], (m.n_x, m.n_x)) @ + cvxpy.reshape(self.par['A_bar'][:, k], (m.n_x, m.n_x), order='F') @ self.var['X'][:, k] + - cvxpy.reshape(self.par['B_bar'][:, k], (m.n_x, m.n_u)) @ + cvxpy.reshape(self.par['B_bar'][:, k], (m.n_x, m.n_u), order='F') @ self.var['U'][:, k] + - cvxpy.reshape(self.par['C_bar'][:, k], (m.n_x, m.n_u)) @ + cvxpy.reshape(self.par['C_bar'][:, k], (m.n_x, m.n_u), order='F') @ self.var['U'][:, k + 1] + self.par['S_bar'][:, k] * self.var['sigma'] + self.par['z_bar'][:, k] + @@ -536,7 +537,7 @@ def solve(self, **kwargs): with warnings.catch_warnings(): # For User warning from solver warnings.simplefilter('ignore') self.prob.solve(verbose=verbose_solver, - solver=solver) + solver=solver) except cvxpy.SolverError: error = True diff --git a/InvertedPendulum/inverted_pendulum_mpc_control.py b/InvertedPendulum/inverted_pendulum_mpc_control.py index 9a5fa2ab41..c45dde8acc 100644 --- a/InvertedPendulum/inverted_pendulum_mpc_control.py +++ b/InvertedPendulum/inverted_pendulum_mpc_control.py @@ -91,7 +91,7 @@ def mpc_control(x0): prob = cvxpy.Problem(cvxpy.Minimize(cost), constr) start = time.time() - prob.solve(verbose=False) + prob.solve(verbose=False, solver=cvxpy.CLARABEL) elapsed_time = time.time() - start print(f"calc time:{elapsed_time:.6f} [sec]") diff --git a/requirements/requirements.txt b/requirements/requirements.txt index f3e6c3dc51..e9c68be7e6 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -1,7 +1,8 @@ numpy == 2.2.4 scipy == 1.15.2 matplotlib == 3.10.1 -cvxpy == 1.5.3 +cvxpy == 1.6.5 +ecos == 2.0.14 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test diff --git a/tests/conftest.py b/tests/conftest.py index 3485fe8150..b707b22d00 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,4 +10,4 @@ def run_this_test(file): - pytest.main([os.path.abspath(file)]) + pytest.main(args=["-W", "error", "-Werror", "--pythonwarnings=error", os.path.abspath(file)]) From 1f729cb8cbd5075871bb52c8fb31df2732ad6641 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 1 May 2025 13:59:15 +0900 Subject: [PATCH 109/181] build(deps): bump ruff from 0.11.6 to 0.11.7 in /requirements (#1205) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.6 to 0.11.7. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.6...0.11.7) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.7 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index e9c68be7e6..88cbb21f81 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,4 +6,4 @@ ecos == 2.0.14 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.11.6 # For unit test +ruff == 0.11.7 # For unit test From a2c42c3d6837658edb734e9f16ab64c1bd2662e7 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Thu, 1 May 2025 21:53:12 +0900 Subject: [PATCH 110/181] Update Python version to 3.13 across the project (#1208) * Update Python version to 3.13 across the project Upgraded the required Python version to 3.13 in configurations, CI workflows, documentation, and environment files. This ensures compatibility with the latest Python release and maintains consistency across all project components. * Update Python version to 3.13 across the project Upgraded the required Python version to 3.13 in configurations, CI workflows, documentation, and environment files. This ensures compatibility with the latest Python release and maintains consistency across all project components. --- .circleci/config.yml | 2 +- .github/workflows/Linux_CI.yml | 2 +- .github/workflows/MacOS_CI.yml | 2 +- .github/workflows/Windows_CI.yml | 2 +- README.md | 2 +- appveyor.yml | 2 +- docs/modules/0_getting_started/3_how_to_contribute_main.rst | 2 +- requirements/environment.yml | 2 +- ruff.toml | 4 ++-- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 9f803ece4a..f6eff674de 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -6,7 +6,7 @@ orbs: jobs: build_doc: docker: - - image: cimg/python:3.12 + - image: cimg/python:3.13 steps: - checkout - run: diff --git a/.github/workflows/Linux_CI.yml b/.github/workflows/Linux_CI.yml index 7b3dc14751..907d36452d 100644 --- a/.github/workflows/Linux_CI.yml +++ b/.github/workflows/Linux_CI.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ '3.12' ] + python-version: [ '3.13' ] name: Python ${{ matrix.python-version }} CI diff --git a/.github/workflows/MacOS_CI.yml b/.github/workflows/MacOS_CI.yml index 5ea15ac72e..03db65f43d 100644 --- a/.github/workflows/MacOS_CI.yml +++ b/.github/workflows/MacOS_CI.yml @@ -16,7 +16,7 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: [ '3.12' ] + python-version: [ '3.13' ] name: Python ${{ matrix.python-version }} CI steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/Windows_CI.yml b/.github/workflows/Windows_CI.yml index b9c8dea649..5cb19699b2 100644 --- a/.github/workflows/Windows_CI.yml +++ b/.github/workflows/Windows_CI.yml @@ -16,7 +16,7 @@ jobs: runs-on: windows-latest strategy: matrix: - python-version: [ '3.12' ] + python-version: [ '3.13' ] name: Python ${{ matrix.python-version }} CI steps: - uses: actions/checkout@v4 diff --git a/README.md b/README.md index 9e605435ce..6d04c90bc8 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ or this paper for more details: For running each sample code: -- [Python 3.12.x](https://www.python.org/) +- [Python 3.13.x](https://www.python.org/) - [NumPy](https://numpy.org/) diff --git a/appveyor.yml b/appveyor.yml index 05ad8a2820..72d89acf11 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,7 +8,7 @@ environment: CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\appveyor\\run_with_env.cmd" matrix: - - PYTHON_DIR: C:\Python310-x64 + - PYTHON_DIR: C:\Python313-x64 branches: only: diff --git a/docs/modules/0_getting_started/3_how_to_contribute_main.rst b/docs/modules/0_getting_started/3_how_to_contribute_main.rst index 30e3001f36..0325aaacae 100644 --- a/docs/modules/0_getting_started/3_how_to_contribute_main.rst +++ b/docs/modules/0_getting_started/3_how_to_contribute_main.rst @@ -26,7 +26,7 @@ to understand the philosophies of this project. Check your Python version. --------------------------- -We only accept a PR for Python 3.12.x or higher. +We only accept a PR for Python 3.13.x or higher. We will not accept a PR for Python 2.x. diff --git a/requirements/environment.yml b/requirements/environment.yml index afbb3fb8ce..023a3d75bf 100644 --- a/requirements/environment.yml +++ b/requirements/environment.yml @@ -2,7 +2,7 @@ name: python_robotics channels: - conda-forge dependencies: - - python=3.12 + - python=3.13 - pip - scipy - numpy diff --git a/ruff.toml b/ruff.toml index 5823ca3db7..d76b715a06 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,8 +5,8 @@ ignore = ["E501", "E741", "E402"] exclude = [ ] -# Assume Python 3.11 -target-version = "py312" +# Assume Python 3.13 +target-version = "py313" [per-file-ignores] From 22cbee49218a1f8acb07ad488416db3c952c19d6 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Fri, 2 May 2025 10:01:19 +0900 Subject: [PATCH 111/181] Standardize "Ref:" to "Reference" across files (#1210) Updated all instances of "Ref:" to "Reference" for consistency in both code and documentation. This change improves clarity and aligns with standard terminology practices. --- .../rocket_powered_landing.py | 2 +- .../two_joint_arm_to_point_control.py | 2 +- .../ensemble_kalman_filter.py | 2 +- Mapping/DistanceMap/distance_map.py | 2 +- .../rectangle_fitting/rectangle_fitting.py | 2 +- MissionPlanning/BehaviorTree/behavior_tree.py | 2 +- MissionPlanning/StateMachine/state_machine.py | 4 +- PathPlanning/ElasticBands/elastic_bands.py | 2 +- .../Eta3SplinePath/eta3_spline_path.py | 2 +- .../cartesian_frenet_converter.py | 2 +- .../frenet_optimal_trajectory.py | 2 +- .../potential_field_planning.py | 2 +- .../quintic_polynomials_planner.py | 2 +- .../state_lattice_planner.py | 2 +- PathTracking/cgmres_nmpc/cgmres_nmpc.py | 2 +- PathTracking/move_to_pose/move_to_pose.py | 2 +- .../stanley_controller/stanley_controller.py | 2 +- README.md | 42 ++--- .../Kalmanfilter_basics_2_main.rst | 2 +- .../12_appendix/Kalmanfilter_basics_main.rst | 2 +- .../steering_motion_model_main.rst | 2 +- ...samble_kalman_filter_localization_main.rst | 5 + ...tended_kalman_filter_localization_main.rst | 168 +++++++++--------- .../histogram_filter_localization_main.rst | 7 +- .../particle_filter_localization_main.rst | 8 +- ...cented_kalman_filter_localization_main.rst | 8 +- .../modules/4_slam/ekf_slam/ekf_slam_main.rst | 2 +- .../4_slam/graph_slam/graph_slam_main.rst | 2 +- .../bezier_path/bezier_path_main.rst | 2 +- .../eta3_spline/eta3_spline_main.rst | 2 +- .../frenet_frame_path_main.rst | 2 +- .../grid_base_search_main.rst | 6 +- ...l_predictive_trajectory_generator_main.rst | 2 +- .../prm_planner/prm_planner_main.rst | 2 +- .../quintic_polynomials_planner_main.rst | 2 +- .../reeds_shepp_path_main.rst | 2 +- docs/modules/5_path_planning/rrt/rrt_main.rst | 8 +- .../state_lattice_planner_main.rst | 2 +- .../vrm_planner/vrm_planner_main.rst | 2 +- .../lqr_speed_and_steering_control_main.rst | 2 +- .../lqr_steering_control_main.rst | 2 +- .../pure_pursuit_tracking_main.rst | 2 +- .../rear_wheel_feedback_control_main.rst | 2 +- .../stanley_control/stanley_control_main.rst | 2 +- users_comments.md | 2 +- 45 files changed, 173 insertions(+), 155 deletions(-) diff --git a/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py b/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py index 1918dc1cee..e8ba8fa220 100644 --- a/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py +++ b/AerialNavigation/rocket_powered_landing/rocket_powered_landing.py @@ -5,7 +5,7 @@ author: Sven Niederberger Atsushi Sakai -Ref: +Reference: - Python implementation of 'Successive Convexification for 6-DoF Mars Rocket Powered Landing with Free-Final-Time' paper by Michael Szmuk and Behcet Acıkmese. diff --git a/ArmNavigation/two_joint_arm_to_point_control/two_joint_arm_to_point_control.py b/ArmNavigation/two_joint_arm_to_point_control/two_joint_arm_to_point_control.py index c2227f18e3..09969c30be 100644 --- a/ArmNavigation/two_joint_arm_to_point_control/two_joint_arm_to_point_control.py +++ b/ArmNavigation/two_joint_arm_to_point_control/two_joint_arm_to_point_control.py @@ -5,7 +5,7 @@ Author: Daniel Ingram (daniel-s-ingram) Atsushi Sakai (@Atsushi_twi) -Ref: P. I. Corke, "Robotics, Vision & Control", Springer 2017, +Reference: P. I. Corke, "Robotics, Vision & Control", Springer 2017, ISBN 978-3-319-54413-7 p102 - [Robotics, Vision and Control] (https://link.springer.com/book/10.1007/978-3-642-20144-8) diff --git a/Localization/ensemble_kalman_filter/ensemble_kalman_filter.py b/Localization/ensemble_kalman_filter/ensemble_kalman_filter.py index 2bab3b49c1..e8a988a270 100644 --- a/Localization/ensemble_kalman_filter/ensemble_kalman_filter.py +++ b/Localization/ensemble_kalman_filter/ensemble_kalman_filter.py @@ -4,7 +4,7 @@ author: Ryohei Sasaki(rsasaki0109) -Ref: +Reference: Ensemble Kalman filtering (https://rmets.onlinelibrary.wiley.com/doi/10.1256/qj.05.135) diff --git a/Mapping/DistanceMap/distance_map.py b/Mapping/DistanceMap/distance_map.py index 0dcc7380c5..0f96c9e8c6 100644 --- a/Mapping/DistanceMap/distance_map.py +++ b/Mapping/DistanceMap/distance_map.py @@ -3,7 +3,7 @@ author: Wang Zheng (@Aglargil) -Ref: +Reference: - [Distance Map] (https://cs.brown.edu/people/pfelzens/papers/dt-final.pdf) diff --git a/Mapping/rectangle_fitting/rectangle_fitting.py b/Mapping/rectangle_fitting/rectangle_fitting.py index 177f078871..7902619666 100644 --- a/Mapping/rectangle_fitting/rectangle_fitting.py +++ b/Mapping/rectangle_fitting/rectangle_fitting.py @@ -4,7 +4,7 @@ author: Atsushi Sakai (@Atsushi_twi) -Ref: +Reference: - Efficient L-Shape Fitting for Vehicle Detection Using Laser Scanners - The Robotics Institute Carnegie Mellon University https://www.ri.cmu.edu/publications/ diff --git a/MissionPlanning/BehaviorTree/behavior_tree.py b/MissionPlanning/BehaviorTree/behavior_tree.py index 59f4c713f1..9ad886aafb 100644 --- a/MissionPlanning/BehaviorTree/behavior_tree.py +++ b/MissionPlanning/BehaviorTree/behavior_tree.py @@ -3,7 +3,7 @@ author: Wang Zheng (@Aglargil) -Ref: +Reference: - [Behavior Tree](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)) """ diff --git a/MissionPlanning/StateMachine/state_machine.py b/MissionPlanning/StateMachine/state_machine.py index de72f0f451..454759236e 100644 --- a/MissionPlanning/StateMachine/state_machine.py +++ b/MissionPlanning/StateMachine/state_machine.py @@ -3,7 +3,7 @@ author: Wang Zheng (@Aglargil) -Ref: +Reference: - [State Machine] (https://en.wikipedia.org/wiki/Finite-state_machine) @@ -23,7 +23,7 @@ def deflate_and_encode(plantuml_text): """ zlib compress the plantuml text and encode it for the plantuml server. - Ref: https://plantuml.com/en/text-encoding + Reference https://plantuml.com/en/text-encoding """ plantuml_alphabet = ( string.digits + string.ascii_uppercase + string.ascii_lowercase + "-_" diff --git a/PathPlanning/ElasticBands/elastic_bands.py b/PathPlanning/ElasticBands/elastic_bands.py index 785f822d14..77d4e6e399 100644 --- a/PathPlanning/ElasticBands/elastic_bands.py +++ b/PathPlanning/ElasticBands/elastic_bands.py @@ -3,7 +3,7 @@ author: Wang Zheng (@Aglargil) -Ref: +Reference: - [Elastic Bands: Connecting Path Planning and Control] (http://www8.cs.umu.se/research/ifor/dl/Control/elastic%20bands.pdf) diff --git a/PathPlanning/Eta3SplinePath/eta3_spline_path.py b/PathPlanning/Eta3SplinePath/eta3_spline_path.py index dc07d3c84b..3f685e512f 100644 --- a/PathPlanning/Eta3SplinePath/eta3_spline_path.py +++ b/PathPlanning/Eta3SplinePath/eta3_spline_path.py @@ -5,7 +5,7 @@ author: Joe Dinius, Ph.D (https://jwdinius.github.io) Atsushi Sakai (@Atsushi_twi) -Ref: +Reference: - [eta^3-Splines for the Smooth Path Generation of Wheeled Mobile Robots] (https://ieeexplore.ieee.org/document/4339545/) diff --git a/PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py b/PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py index 4cc8650c89..482712ceaf 100644 --- a/PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py +++ b/PathPlanning/FrenetOptimalTrajectory/cartesian_frenet_converter.py @@ -4,7 +4,7 @@ author: Wang Zheng (@Aglargil) -Ref: +Reference: - [Optimal Trajectory Generation for Dynamic Street Scenarios in a Frenet Frame] (https://www.researchgate.net/profile/Moritz_Werling/publication/224156269_Optimal_Trajectory_Generation_for_Dynamic_Street_Scenarios_in_a_Frenet_Frame/links/54f749df0cf210398e9277af.pdf) diff --git a/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py b/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py index 248894c1c6..4b82fb70fd 100644 --- a/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py +++ b/PathPlanning/FrenetOptimalTrajectory/frenet_optimal_trajectory.py @@ -4,7 +4,7 @@ author: Atsushi Sakai (@Atsushi_twi) -Ref: +Reference: - [Optimal Trajectory Generation for Dynamic Street Scenarios in a Frenet Frame] (https://www.researchgate.net/profile/Moritz_Werling/publication/224156269_Optimal_Trajectory_Generation_for_Dynamic_Street_Scenarios_in_a_Frenet_Frame/links/54f749df0cf210398e9277af.pdf) diff --git a/PathPlanning/PotentialFieldPlanning/potential_field_planning.py b/PathPlanning/PotentialFieldPlanning/potential_field_planning.py index 8f136b5ee3..603a9d16cf 100644 --- a/PathPlanning/PotentialFieldPlanning/potential_field_planning.py +++ b/PathPlanning/PotentialFieldPlanning/potential_field_planning.py @@ -4,7 +4,7 @@ author: Atsushi Sakai (@Atsushi_twi) -Ref: +Reference: https://www.cs.cmu.edu/~motionplanning/lecture/Chap4-Potential-Field_howie.pdf """ diff --git a/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py b/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py index fdc181afab..86f9f662da 100644 --- a/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py +++ b/PathPlanning/QuinticPolynomialsPlanner/quintic_polynomials_planner.py @@ -4,7 +4,7 @@ author: Atsushi Sakai (@Atsushi_twi) -Ref: +Reference: - [Local Path planning And Motion Control For Agv In Positioning](https://ieeexplore.ieee.org/document/637936/) diff --git a/PathPlanning/StateLatticePlanner/state_lattice_planner.py b/PathPlanning/StateLatticePlanner/state_lattice_planner.py index 7f8e725e0a..05f8df78f8 100644 --- a/PathPlanning/StateLatticePlanner/state_lattice_planner.py +++ b/PathPlanning/StateLatticePlanner/state_lattice_planner.py @@ -8,7 +8,7 @@ https://github.com/AtsushiSakai/PythonRobotics/blob/master/PathPlanning /ModelPredictiveTrajectoryGenerator/lookup_table_generator.py -Ref: +Reference: - State Space Sampling of Feasible Motions for High-Performance Mobile Robot Navigation in Complex Environments diff --git a/PathTracking/cgmres_nmpc/cgmres_nmpc.py b/PathTracking/cgmres_nmpc/cgmres_nmpc.py index a582c9da81..ee40e73504 100644 --- a/PathTracking/cgmres_nmpc/cgmres_nmpc.py +++ b/PathTracking/cgmres_nmpc/cgmres_nmpc.py @@ -4,7 +4,7 @@ author Atsushi Sakai (@Atsushi_twi) -Ref: +Reference: Shunichi09/nonlinear_control: Implementing the nonlinear model predictive control, sliding mode control https://github.com/Shunichi09/PythonLinearNonlinearControl diff --git a/PathTracking/move_to_pose/move_to_pose.py b/PathTracking/move_to_pose/move_to_pose.py index 34736a2e21..faf1264953 100644 --- a/PathTracking/move_to_pose/move_to_pose.py +++ b/PathTracking/move_to_pose/move_to_pose.py @@ -76,7 +76,7 @@ def calc_control_command(self, x_diff, y_diff, theta, theta_goal): # [-pi, pi] to prevent unstable behavior e.g. difference going # from 0 rad to 2*pi rad with slight turn - # Ref: The velocity v always has a constant sign which depends on the initial value of α. + # The velocity v always has a constant sign which depends on the initial value of α. rho = np.hypot(x_diff, y_diff) v = self.Kp_rho * rho diff --git a/PathTracking/stanley_controller/stanley_controller.py b/PathTracking/stanley_controller/stanley_controller.py index bc98175f17..01c2ec0229 100644 --- a/PathTracking/stanley_controller/stanley_controller.py +++ b/PathTracking/stanley_controller/stanley_controller.py @@ -4,7 +4,7 @@ author: Atsushi Sakai (@Atsushi_twi) -Ref: +Reference: - [Stanley: The robot that won the DARPA grand challenge](http://isl.ecst.csuchico.edu/DOCS/darpa2005/DARPA%202005%20Stanley.pdf) - [Autonomous Automobile Path Tracking](https://www.ri.cmu.edu/pub_files/2009/2/Automatic_Steering_Methods_for_Autonomous_Automobile_Path_Tracking.pdf) diff --git a/README.md b/README.md index 6d04c90bc8..05fd0262df 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ All animation gifs are stored here: [AtsushiSakai/PythonRoboticsGifs: Animation EKF pic -Ref: +Reference - [documentation](https://atsushisakai.github.io/PythonRobotics/modules/localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization.html) @@ -186,7 +186,7 @@ It is assumed that the robot can measure a distance from landmarks (RFID). These measurements are used for PF localization. -Ref: +Reference - [PROBABILISTIC ROBOTICS](http://www.probabilistic-robotics.org/) @@ -207,7 +207,7 @@ The filter integrates speed input and range observations from RFID for localizat Initial position is not needed. -Ref: +Reference - [PROBABILISTIC ROBOTICS](http://www.probabilistic-robotics.org/) @@ -256,7 +256,7 @@ It can calculate a rotation matrix, and a translation vector between points and ![3](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/SLAM/iterative_closest_point/animation.gif) -Ref: +Reference - [Introduction to Mobile Robotics: Iterative Closest Point Algorithm](https://cs.gmu.edu/~kosecka/cs685/cs685-icp.pdf) @@ -275,7 +275,7 @@ Black points are landmarks, blue crosses are estimated landmark positions by Fas ![3](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/SLAM/FastSLAM1/animation.gif) -Ref: +Reference - [PROBABILISTIC ROBOTICS](http://www.probabilistic-robotics.org/) @@ -321,7 +321,7 @@ This is a 2D grid based the shortest path planning with D star algorithm. The animation shows a robot finding its path avoiding an obstacle using the D* search algorithm. -Ref: +Reference - [D* Algorithm Wikipedia](https://en.wikipedia.org/wiki/D*) @@ -346,7 +346,7 @@ This is a 2D grid based path planning with Potential Field algorithm. In the animation, the blue heat map shows potential value on each grid. -Ref: +Reference - [Robotic Motion Planning:Potential Functions](https://www.cs.cmu.edu/~motionplanning/lecture/Chap4-Potential-Field_howie.pdf) @@ -362,7 +362,7 @@ This script is a path planning code with state lattice planning. This code uses the model predictive trajectory generator to solve boundary problem. -Ref: +Reference - [Optimal rough terrain trajectory generation for wheeled mobile robots](https://journals.sagepub.com/doi/pdf/10.1177/0278364906075328) @@ -390,7 +390,7 @@ Cyan crosses means searched points with Dijkstra method, The red line is the final path of PRM. -Ref: +Reference - [Probabilistic roadmap \- Wikipedia](https://en.wikipedia.org/wiki/Probabilistic_roadmap) @@ -406,7 +406,7 @@ This is a path planning code with RRT\* Black circles are obstacles, green line is a searched tree, red crosses are start and goal positions. -Ref: +Reference - [Incremental Sampling-based Algorithms for Optimal Motion Planning](https://arxiv.org/abs/1005.0416) @@ -426,7 +426,7 @@ A double integrator motion model is used for LQR local planner. ![LQR_RRT](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/LQRRRTStar/animation.gif) -Ref: +Reference - [LQR\-RRT\*: Optimal Sampling\-Based Motion Planning with Automatically Derived Extension Heuristics](https://lis.csail.mit.edu/pubs/perez-icra12.pdf) @@ -441,7 +441,7 @@ Motion planning with quintic polynomials. It can calculate a 2D path, velocity, and acceleration profile based on quintic polynomials. -Ref: +Reference - [Local Path Planning And Motion Control For Agv In Positioning](https://ieeexplore.ieee.org/document/637936/) @@ -451,7 +451,7 @@ A sample code with Reeds Shepp path planning. ![RSPlanning](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/ReedsSheppPath/animation.gif?raw=true) -Ref: +Reference - [15.3.2 Reeds\-Shepp Curves](http://planning.cs.uiuc.edu/node822.html) @@ -477,7 +477,7 @@ The cyan line is the target course and black crosses are obstacles. The red line is the predicted path. -Ref: +Reference - [Optimal Trajectory Generation for Dynamic Street Scenarios in a Frenet Frame](https://www.researchgate.net/profile/Moritz_Werling/publication/224156269_Optimal_Trajectory_Generation_for_Dynamic_Street_Scenarios_in_a_Frenet_Frame/links/54f749df0cf210398e9277af.pdf) @@ -492,7 +492,7 @@ This is a simulation of moving to a pose control ![2](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/move_to_pose/animation.gif) -Ref: +Reference - [P. I. Corke, "Robotics, Vision and Control" \| SpringerLink p102](https://link.springer.com/book/10.1007/978-3-642-20144-8) @@ -503,7 +503,7 @@ Path tracking simulation with Stanley steering control and PID speed control. ![2](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/stanley_controller/animation.gif) -Ref: +Reference - [Stanley: The robot that won the DARPA grand challenge](http://robots.stanford.edu/papers/thrun.stanley05.pdf) @@ -517,7 +517,7 @@ Path tracking simulation with rear wheel feedback steering control and PID speed ![PythonRobotics/figure_1.png at master · AtsushiSakai/PythonRobotics](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/rear_wheel_feedback/animation.gif) -Ref: +Reference - [A Survey of Motion Planning and Control Techniques for Self-driving Urban Vehicles](https://arxiv.org/abs/1604.07446) @@ -528,7 +528,7 @@ Path tracking simulation with LQR speed and steering control. ![3](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/lqr_speed_steer_control/animation.gif) -Ref: +Reference - [Towards fully autonomous driving: Systems and algorithms \- IEEE Conference Publication](https://ieeexplore.ieee.org/document/5940562/) @@ -539,7 +539,7 @@ Path tracking simulation with iterative linear model predictive speed and steeri MPC pic -Ref: +Reference - [documentation](https://atsushisakai.github.io/PythonRobotics/modules/path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control.html) @@ -551,7 +551,7 @@ A motion planning and path tracking simulation with NMPC of C-GMRES ![3](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/cgmres_nmpc/animation.gif) -Ref: +Reference - [documentation](https://atsushisakai.github.io/PythonRobotics/modules/path_tracking/cgmres_nmpc/cgmres_nmpc.html) @@ -591,7 +591,7 @@ This is a 3d trajectory generation simulation for a rocket powered landing. ![3](https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/AerialNavigation/rocket_powered_landing/animation.gif) -Ref: +Reference - [documentation](https://atsushisakai.github.io/PythonRobotics/modules/aerial_navigation/rocket_powered_landing/rocket_powered_landing.html) diff --git a/docs/modules/12_appendix/Kalmanfilter_basics_2_main.rst b/docs/modules/12_appendix/Kalmanfilter_basics_2_main.rst index 9ae6fc5bcb..b7ff84e6f6 100644 --- a/docs/modules/12_appendix/Kalmanfilter_basics_2_main.rst +++ b/docs/modules/12_appendix/Kalmanfilter_basics_2_main.rst @@ -331,7 +331,7 @@ are vectors and matrices, but the concepts are exactly the same: - Use the process model to predict the next state (the prior) - Form an estimate part way between the measurement and the prior -References: +Reference ~~~~~~~~~~~ 1. Roger Labbe’s diff --git a/docs/modules/12_appendix/Kalmanfilter_basics_main.rst b/docs/modules/12_appendix/Kalmanfilter_basics_main.rst index 6548377e07..a1d99d47ef 100644 --- a/docs/modules/12_appendix/Kalmanfilter_basics_main.rst +++ b/docs/modules/12_appendix/Kalmanfilter_basics_main.rst @@ -552,7 +552,7 @@ a given (X,Y) value. .. image:: Kalmanfilter_basics_files/Kalmanfilter_basics_28_1.png -References: +Reference ~~~~~~~~~~~ 1. Roger Labbe’s diff --git a/docs/modules/12_appendix/steering_motion_model_main.rst b/docs/modules/12_appendix/steering_motion_model_main.rst index 6e444b7909..c697123fa2 100644 --- a/docs/modules/12_appendix/steering_motion_model_main.rst +++ b/docs/modules/12_appendix/steering_motion_model_main.rst @@ -91,7 +91,7 @@ the target minimum velocity :math:`v_{min}` can be calculated as follows: :math:`v_{min} = \frac{d_{t+1}+d_{t}}{\Delta t} = \frac{d_{t+1}+d_{t}}{(\kappa_{t+1}-\kappa_{t})}\frac{tan\dot{\delta}_{max}}{WB}` -References: +Reference ~~~~~~~~~~~ - `Vehicle Dynamics and Control `_ diff --git a/docs/modules/2_localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst b/docs/modules/2_localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst index 2543d1186a..214e645d10 100644 --- a/docs/modules/2_localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst +++ b/docs/modules/2_localization/ensamble_kalman_filter_localization_files/ensamble_kalman_filter_localization_main.rst @@ -5,3 +5,8 @@ Ensamble Kalman Filter Localization This is a sensor fusion localization with Ensamble Kalman Filter(EnKF). +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: Localization.ensemble_kalman_filter.ensemble_kalman_filter.enkf_localization + diff --git a/docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst b/docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst index da214b6de5..adb41e5e40 100644 --- a/docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst +++ b/docs/modules/2_localization/extended_kalman_filter_localization_files/extended_kalman_filter_localization_main.rst @@ -23,28 +23,40 @@ is estimated trajectory with EKF. The red ellipse is estimated covariance ellipse with EKF. -Code: `PythonRobotics/extended_kalman_filter.py at master · -AtsushiSakai/PythonRobotics `__ +Code Link +~~~~~~~~~~~~~ -Kalman Filter with Speed Scale Factor Correction -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +.. autofunction:: Localization.extended_kalman_filter.extended_kalman_filter.ekf_estimation +Extended Kalman Filter algorithm +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. image:: ekf_with_velocity_correction_1_0.png - :width: 600px +Localization process using Extended Kalman Filter:EKF is -This is a Extended kalman filter (EKF) localization with velocity correction. +=== Predict === -This is for correcting the vehicle speed measured with scale factor errors due to factors such as wheel wear. +:math:`x_{Pred} = Fx_t+Bu_t` -Code: `PythonRobotics/extended_kalman_ekf_with_velocity_correctionfilter.py -AtsushiSakai/PythonRobotics `__ +:math:`P_{Pred} = J_f P_t J_f^T + Q` -Filter design -~~~~~~~~~~~~~ +=== Update === -Position Estimation Kalman Filter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +:math:`z_{Pred} = Hx_{Pred}` + +:math:`y = z - z_{Pred}` + +:math:`S = J_g P_{Pred}.J_g^T + R` + +:math:`K = P_{Pred}.J_g^T S^{-1}` + +:math:`x_{t+1} = x_{Pred} + Ky` + +:math:`P_{t+1} = ( I - K J_g) P_{Pred}` + + + +Filter design +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In this simulation, the robot has a state vector includes 4 states at time :math:`t`. @@ -82,27 +94,9 @@ In the code, “observation” function generates the input and observation vector with noise `code `__ -Kalman Filter with Speed Scale Factor Correction -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -In this simulation, the robot has a state vector includes 5 states at -time :math:`t`. - -.. math:: \textbf{x}_t=[x_t, y_t, \phi_t, v_t, s_t] - -x, y are a 2D x-y position, :math:`\phi` is orientation, v is -velocity, and s is a scale factor of velocity. - -In the code, “xEst” means the state vector. -`code `__ - -The rest is the same as the Position Estimation Kalman Filter. Motion Model -~~~~~~~~~~~~ - -Position Estimation Kalman Filter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~ The robot model is @@ -137,9 +131,61 @@ Its Jacobian matrix is :math:`\begin{equation*}  = \begin{bmatrix} 1& 0 & -v \sin(\phi) \Delta t & \cos(\phi) \Delta t\\ 0 & 1 & v \cos(\phi) \Delta t & \sin(\phi) \Delta t\\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \end{equation*}` +Observation Model +~~~~~~~~~~~~~~~~~ + +The robot can get x-y position information from GPS. + +So GPS Observation model is + +.. math:: \textbf{z}_{t} = g(\textbf{x}_t) = H \textbf{x}_t + +where + +:math:`\begin{equation*} H = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ \end{bmatrix} \end{equation*}` + +The observation function states that + +:math:`\begin{equation*} \begin{bmatrix} x' \\ y' \end{bmatrix} = g(\textbf{x}) = \begin{bmatrix} x \\ y \end{bmatrix} \end{equation*}` + +Its Jacobian matrix is + +:math:`\begin{equation*} J_g = \begin{bmatrix} \frac{\partial x'}{\partial x} & \frac{\partial x'}{\partial y} & \frac{\partial x'}{\partial \phi} & \frac{\partial x'}{\partial v}\\ \frac{\partial y'}{\partial x}& \frac{\partial y'}{\partial y} & \frac{\partial y'}{\partial \phi} & \frac{\partial y'}{ \partial v}\\ \end{bmatrix} \end{equation*}` + +:math:`\begin{equation*}  = \begin{bmatrix} 1& 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ \end{bmatrix} \end{equation*}` + Kalman Filter with Speed Scale Factor Correction ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +This is a Extended kalman filter (EKF) localization with velocity correction. + +This is for correcting the vehicle speed measured with scale factor errors due to factors such as wheel wear. + + +In this simulation, the robot has a state vector includes 5 states at +time :math:`t`. + +.. math:: \textbf{x}_t=[x_t, y_t, \phi_t, v_t, s_t] + +x, y are a 2D x-y position, :math:`\phi` is orientation, v is +velocity, and s is a scale factor of velocity. + +In the code, “xEst” means the state vector. +`code `__ + +The rest is the same as the Position Estimation Kalman Filter. + +.. image:: ekf_with_velocity_correction_1_0.png + :width: 600px + +Code Link +~~~~~~~~~~~~~ + +.. autofunction:: Localization.extended_kalman_filter.ekf_with_velocity_correction.ekf_estimation + + +Motion Model +~~~~~~~~~~~~ The robot model is @@ -178,33 +224,7 @@ Its Jacobian matrix is Observation Model ~~~~~~~~~~~~~~~~~ -Position Estimation Kalman Filter -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The robot can get x-y position infomation from GPS. - -So GPS Observation model is - -.. math:: \textbf{z}_{t} = g(\textbf{x}_t) = H \textbf{x}_t - -where - -:math:`\begin{equation*} H = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ \end{bmatrix} \end{equation*}` - -The observation function states that - -:math:`\begin{equation*} \begin{bmatrix} x' \\ y' \end{bmatrix} = g(\textbf{x}) = \begin{bmatrix} x \\ y \end{bmatrix} \end{equation*}` - -Its Jacobian matrix is - -:math:`\begin{equation*} J_g = \begin{bmatrix} \frac{\partial x'}{\partial x} & \frac{\partial x'}{\partial y} & \frac{\partial x'}{\partial \phi} & \frac{\partial x'}{\partial v}\\ \frac{\partial y'}{\partial x}& \frac{\partial y'}{\partial y} & \frac{\partial y'}{\partial \phi} & \frac{\partial y'}{ \partial v}\\ \end{bmatrix} \end{equation*}` - -:math:`\begin{equation*}  = \begin{bmatrix} 1& 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ \end{bmatrix} \end{equation*}` - -Kalman Filter with Speed Scale Factor Correction -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -The robot can get x-y position infomation from GPS. +The robot can get x-y position information from GPS. So GPS Observation model is @@ -225,32 +245,8 @@ Its Jacobian matrix is :math:`\begin{equation*}  = \begin{bmatrix} 1& 0 & 0 & 0 & 0\\ 0 & 1 & 0 & 0 & 0\\ \end{bmatrix} \end{equation*}` -Extended Kalman Filter -~~~~~~~~~~~~~~~~~~~~~~ - -Localization process using Extended Kalman Filter:EKF is - -=== Predict === - -:math:`x_{Pred} = Fx_t+Bu_t` - -:math:`P_{Pred} = J_f P_t J_f^T + Q` - -=== Update === - -:math:`z_{Pred} = Hx_{Pred}` - -:math:`y = z - z_{Pred}` - -:math:`S = J_g P_{Pred}.J_g^T + R` - -:math:`K = P_{Pred}.J_g^T S^{-1}` - -:math:`x_{t+1} = x_{Pred} + Ky` - -:math:`P_{t+1} = ( I - K J_g) P_{Pred}` -Ref: -~~~~ +Reference +^^^^^^^^^^^ - `PROBABILISTIC-ROBOTICS.ORG `__ diff --git a/docs/modules/2_localization/histogram_filter_localization/histogram_filter_localization_main.rst b/docs/modules/2_localization/histogram_filter_localization/histogram_filter_localization_main.rst index fafd578333..3a175b1316 100644 --- a/docs/modules/2_localization/histogram_filter_localization/histogram_filter_localization_main.rst +++ b/docs/modules/2_localization/histogram_filter_localization/histogram_filter_localization_main.rst @@ -16,6 +16,11 @@ The filter uses speed input and range observations from RFID for localization. Initial position information is not needed. +Code Link +~~~~~~~~~~~~~ + +.. autofunction:: Localization.histogram_filter.histogram_filter.histogram_filter_localization + Filtering algorithm ~~~~~~~~~~~~~~~~~~~~ @@ -107,7 +112,7 @@ There are two ways to calculate the final positions: -References: +Reference ~~~~~~~~~~~ - `_PROBABILISTIC ROBOTICS: `_ diff --git a/docs/modules/2_localization/particle_filter_localization/particle_filter_localization_main.rst b/docs/modules/2_localization/particle_filter_localization/particle_filter_localization_main.rst index 20a9eb58fc..d648d8e080 100644 --- a/docs/modules/2_localization/particle_filter_localization/particle_filter_localization_main.rst +++ b/docs/modules/2_localization/particle_filter_localization/particle_filter_localization_main.rst @@ -15,6 +15,12 @@ It is assumed that the robot can measure a distance from landmarks This measurements are used for PF localization. +Code Link +~~~~~~~~~~~~~ + +.. autofunction:: Localization.particle_filter.particle_filter.pf_localization + + How to calculate covariance matrix from particles ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -30,7 +36,7 @@ The covariance matrix :math:`\Xi` from particle information is calculated by the - :math:`\mu_j` is the :math:`j` th mean state of particles. -References: +Reference ~~~~~~~~~~~ - `_PROBABILISTIC ROBOTICS: `_ diff --git a/docs/modules/2_localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst b/docs/modules/2_localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst index bb6b5b664b..a7a5aab61b 100644 --- a/docs/modules/2_localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst +++ b/docs/modules/2_localization/unscented_kalman_filter_localization/unscented_kalman_filter_localization_main.rst @@ -7,7 +7,13 @@ This is a sensor fusion localization with Unscented Kalman Filter(UKF). The lines and points are same meaning of the EKF simulation. -References: +Code Link +~~~~~~~~~~~~~ + +.. autofunction:: Localization.unscented_kalman_filter.unscented_kalman_filter.ukf_estimation + + +Reference ~~~~~~~~~~~ - `Discriminatively Trained Unscented Kalman Filter for Mobile Robot Localization `_ diff --git a/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst b/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst index b27971225e..a1486ffe1e 100644 --- a/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst +++ b/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst @@ -578,7 +578,7 @@ reckoning and control functions are passed along here as well. .. image:: ekf_slam_1_0.png -References: +Reference ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - `PROBABILISTIC ROBOTICS `_ diff --git a/docs/modules/4_slam/graph_slam/graph_slam_main.rst b/docs/modules/4_slam/graph_slam/graph_slam_main.rst index 987eed385c..a2ef513527 100644 --- a/docs/modules/4_slam/graph_slam/graph_slam_main.rst +++ b/docs/modules/4_slam/graph_slam/graph_slam_main.rst @@ -17,7 +17,7 @@ The black stars are landmarks for graph edge generation. .. include:: graphSLAM_formulation.rst .. include:: graphSLAM_SE2_example.rst -References: +Reference ~~~~~~~~~~~ - `A Tutorial on Graph-Based SLAM `_ diff --git a/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst b/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst index d306a85352..9d9b3de709 100644 --- a/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst +++ b/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst @@ -13,7 +13,7 @@ You can get different Beizer course: .. image:: Figure_2.png -Ref: +Reference - `Continuous Curvature Path Generation Based on Bezier Curves for Autonomous diff --git a/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst b/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst index ffc3cc6451..82e0a71044 100644 --- a/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst +++ b/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst @@ -7,7 +7,7 @@ Eta^3 Spline path planning This is a path planning with Eta^3 spline. -Ref: +Reference - `\\eta^3-Splines for the Smooth Path Generation of Wheeled Mobile Robots `__ diff --git a/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst b/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst index 38efaf2b53..371d536e3f 100644 --- a/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst +++ b/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst @@ -35,7 +35,7 @@ Low Speed and Merging and Stopping Scenario This scenario illustrates the trajectory planning at low speeds with merging and stopping behaviors. -Ref: +Reference - `Optimal Trajectory Generation for Dynamic Street Scenarios in a Frenet diff --git a/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst b/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst index 1644ed00cc..bf82c9f391 100644 --- a/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst +++ b/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst @@ -63,7 +63,7 @@ This is a 2D grid based shortest path planning with D star algorithm. The animation shows a robot finding its path avoiding an obstacle using the D* search algorithm. -Ref: +Reference - `D* search Wikipedia `__ @@ -74,7 +74,7 @@ This is a 2D grid based path planning and replanning with D star lite algorithm. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/DStarLite/animation.gif -Ref: +Reference - `Improved Fast Replanning for Robot Navigation in Unknown Terrain `_ @@ -88,7 +88,7 @@ This is a 2D grid based path planning with Potential Field algorithm. In the animation, the blue heat map shows potential value on each grid. -Ref: +Reference - `Robotic Motion Planning:Potential Functions `__ diff --git a/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst b/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst index 463363ddcf..1c5df1c9cc 100644 --- a/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst +++ b/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst @@ -16,7 +16,7 @@ Lookup table generation sample .. image:: lookup_table.png -Ref: +Reference - `Optimal rough terrain trajectory generation for wheeled mobile robots `__ diff --git a/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst b/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst index 0628719176..f816c5c1b9 100644 --- a/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst +++ b/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst @@ -13,7 +13,7 @@ Cyan crosses means searched points with Dijkstra method, The red line is the final path of PRM. -Ref: +Reference - `Probabilistic roadmap - Wikipedia `__ diff --git a/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst b/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst index 0412b3c9b3..66c3001c05 100644 --- a/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst +++ b/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst @@ -97,7 +97,7 @@ Each velocity and acceleration boundary condition can be calculated with each or :math:`v_{xe}=v_ecos(\theta_e), v_{ye}=v_esin(\theta_e)` -References: +Reference ~~~~~~~~~~~ - `Local Path Planning And Motion Control For Agv In diff --git a/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst b/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst index ff377eb91b..a4a5d28e01 100644 --- a/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst +++ b/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst @@ -380,7 +380,7 @@ Hence, we have: - :math:`v = (t - φ)` -Ref: +Reference - `15.3.2 Reeds-Shepp Curves `__ diff --git a/docs/modules/5_path_planning/rrt/rrt_main.rst b/docs/modules/5_path_planning/rrt/rrt_main.rst index e5f351a7ba..da3a4957a5 100644 --- a/docs/modules/5_path_planning/rrt/rrt_main.rst +++ b/docs/modules/5_path_planning/rrt/rrt_main.rst @@ -53,7 +53,7 @@ This is a path planning code with Informed RRT*. The cyan ellipse is the heuristic sampling domain of Informed RRT*. -Ref: +Reference - `Informed RRT\*: Optimal Sampling-based Path Planning Focused via Direct Sampling of an Admissible Ellipsoidal @@ -68,7 +68,7 @@ Batch Informed RRT\* This is a path planning code with Batch Informed RRT*. -Ref: +Reference - `Batch Informed Trees (BIT*): Sampling-based Optimal Planning via the Heuristically Guided Search of Implicit Random Geometric @@ -87,7 +87,7 @@ In this code, pure-pursuit algorithm is used for steering control, PID is used for speed control. -Ref: +Reference - `Motion Planning in Complex Environments using Closed-loop Prediction `__ @@ -109,7 +109,7 @@ A double integrator motion model is used for LQR local planner. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/LQRRRTStar/animation.gif -Ref: +Reference - `LQR-RRT\*: Optimal Sampling-Based Motion Planning with Automatically Derived Extension diff --git a/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst b/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst index d5e7ed17d1..bf89ac11ae 100644 --- a/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst +++ b/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst @@ -22,7 +22,7 @@ Lane sampling .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/StateLatticePlanner/LaneSampling.gif -Ref: +Reference - `Optimal rough terrain trajectory generation for wheeled mobile robots `__ diff --git a/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst b/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst index 92e729ab29..65e0e53465 100644 --- a/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst +++ b/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst @@ -11,7 +11,7 @@ Cyan crosses mean searched points with Dijkstra method, The red line is the final path of Vornoi Road-Map. -Ref: +Reference - `Robotic Motion Planning `__ diff --git a/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst index ded187e972..568ef9a0df 100644 --- a/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst +++ b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst @@ -134,7 +134,7 @@ Simulation results -References: +Reference ~~~~~~~~~~~ - `Towards fully autonomous driving: Systems and algorithms `__ diff --git a/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst b/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst index baca7a33fc..831423f364 100644 --- a/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst +++ b/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst @@ -113,7 +113,7 @@ The optimal control input `u` can be calculated as: where `K` is the feedback gain matrix obtained by solving the Riccati equation. -References: +Reference ~~~~~~~~~~~ - `ApolloAuto/apollo: An open autonomous driving platform `_ diff --git a/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst b/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst index 5c7bcef85f..d7354cf8fb 100644 --- a/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst +++ b/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst @@ -9,7 +9,7 @@ speed control. The red line is a target course, the green cross means the target point for pure pursuit control, the blue line is the tracking. -References: +Reference ~~~~~~~~~~~ - `A Survey of Motion Planning and Control Techniques for Self-driving diff --git a/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst b/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst index 70875fdc6c..d18cd6fbf7 100644 --- a/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst +++ b/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst @@ -6,7 +6,7 @@ PID speed control. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/rear_wheel_feedback/animation.gif -References: +Reference ~~~~~~~~~~~ - `A Survey of Motion Planning and Control Techniques for Self-driving Urban Vehicles `__ diff --git a/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst b/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst index fe325b0102..69089ac33b 100644 --- a/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst +++ b/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst @@ -6,7 +6,7 @@ control. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/stanley_controller/animation.gif -References: +Reference ~~~~~~~~~~~ - `Stanley: The robot that won the DARPA grand diff --git a/users_comments.md b/users_comments.md index a2f798eac4..1674f21377 100644 --- a/users_comments.md +++ b/users_comments.md @@ -10,7 +10,7 @@ This is an electric wheelchair control demo by [Katsushun89](https://github.com/ ![1](https://github.com/AtsushiSakai/PythonRoboticsGifs/blob/master/Users/WHILL_Model_CR_with_move_to_a_pose/WHLL_Model_CR_with_move_to_a_pose.gif) -Ref: +Reference: - [toioと同じように動くWHILL Model CR (in Japanese)](https://qiita.com/KatsuShun89/items/68fde7544ecfe36096d2) From 5392fcff4d555421b4417827dc67c611ad14cc06 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sat, 3 May 2025 09:25:12 +0900 Subject: [PATCH 112/181] Refactor module links and improve code documentation. (#1211) * Refactor module links and improve code documentation. Updated documentation to rename "API" sections to "Code Link" for clarity and consistency. Enhanced docstrings for `circle_fitting` and `kmeans_clustering` functions, improving parameter descriptions and adding return value details. Fixed typos in function and file names in the ray casting grid map module. * Fix import typo in ray casting grid map test module. Corrected the import statement in the test file by updating the module's name to `ray_casting_grid_map` for consistency with the source file. This ensures proper functionality of the test suite. --- Mapping/circle_fitting/circle_fitting.py | 33 +++++++++++++++---- .../kmeans_clustering/kmeans_clustering.py | 27 ++++++++++++++- .../ray_casting_grid_map.py} | 4 +-- .../circle_fitting/circle_fitting_main.rst | 4 +++ .../distance_map/distance_map_main.rst | 4 +-- .../gaussian_grid_map_main.rst | 6 ++++ .../k_means_object_clustering_main.rst | 6 ++++ .../lidar_to_grid_map_tutorial_main.rst | 6 ++++ .../normal_vector_estimation_main.rst | 8 ++--- .../point_cloud_sampling_main.rst | 8 ++--- .../ray_casting_grid_map_main.rst | 7 +++- .../rectangle_fitting_main.rst | 4 +-- ...id_map.py => test_ray_casting_grid_map.py} | 2 +- 13 files changed, 96 insertions(+), 23 deletions(-) rename Mapping/{raycasting_grid_map/raycasting_grid_map.py => ray_casting_grid_map/ray_casting_grid_map.py} (96%) rename tests/{test_raycasting_grid_map.py => test_ray_casting_grid_map.py} (71%) diff --git a/Mapping/circle_fitting/circle_fitting.py b/Mapping/circle_fitting/circle_fitting.py index 2eba550127..b5714b507c 100644 --- a/Mapping/circle_fitting/circle_fitting.py +++ b/Mapping/circle_fitting/circle_fitting.py @@ -16,12 +16,33 @@ def circle_fitting(x, y): """ - Circle Fitting with least squared - input: point x-y positions - output cxe x center position - cye y center position - re radius of circle - error: prediction error + Fits a circle to a given set of points using a least-squares approach. + + This function calculates the center (x, y) and radius of a circle that best fits + the given set of points in a two-dimensional plane. It minimizes the squared + errors between the circle and the provided points and returns the calculated + center coordinates, radius, and the fitting error. + + Raises + ------ + ValueError + If the input lists x and y do not contain the same number of elements. + + Parameters + ---------- + x : list[float] + The x-coordinates of the points. + y : list[float] + The y-coordinates of the points. + + Returns + ------- + tuple[float, float, float, float] + A tuple containing: + - The x-coordinate of the center of the fitted circle (float). + - The y-coordinate of the center of the fitted circle (float). + - The radius of the fitted circle (float). + - The fitting error as a deviation metric (float). """ sumx = sum(x) diff --git a/Mapping/kmeans_clustering/kmeans_clustering.py b/Mapping/kmeans_clustering/kmeans_clustering.py index e18960e990..cee01e5ad5 100644 --- a/Mapping/kmeans_clustering/kmeans_clustering.py +++ b/Mapping/kmeans_clustering/kmeans_clustering.py @@ -17,12 +17,37 @@ def kmeans_clustering(rx, ry, nc): + """ + Performs k-means clustering on the given dataset, iteratively adjusting cluster centroids + until convergence within a defined threshold or reaching the maximum number of + iterations. + + The implementation initializes clusters, calculates initial centroids, and refines the + clusters through iterative updates to optimize the cost function based on minimum + distance between datapoints and centroids. + + Arguments: + rx: List[float] + The x-coordinates of the dataset points to be clustered. + ry: List[float] + The y-coordinates of the dataset points to be clustered. + nc: int + The number of clusters to group the data into. + + Returns: + Clusters + An instance containing the final cluster assignments and centroids after + convergence. + + Raises: + None + + """ clusters = Clusters(rx, ry, nc) clusters.calc_centroid() pre_cost = float("inf") for loop in range(MAX_LOOP): - print("loop:", loop) cost = clusters.update_clusters() clusters.calc_centroid() diff --git a/Mapping/raycasting_grid_map/raycasting_grid_map.py b/Mapping/ray_casting_grid_map/ray_casting_grid_map.py similarity index 96% rename from Mapping/raycasting_grid_map/raycasting_grid_map.py rename to Mapping/ray_casting_grid_map/ray_casting_grid_map.py index 8ce37b925b..c7e73f0630 100644 --- a/Mapping/raycasting_grid_map/raycasting_grid_map.py +++ b/Mapping/ray_casting_grid_map/ray_casting_grid_map.py @@ -48,7 +48,7 @@ def atan_zero_to_twopi(y, x): return angle -def precasting(minx, miny, xw, yw, xyreso, yawreso): +def pre_casting(minx, miny, xw, yw, xyreso, yawreso): precast = [[] for i in range(int(round((math.pi * 2.0) / yawreso)) + 1)] @@ -81,7 +81,7 @@ def generate_ray_casting_grid_map(ox, oy, xyreso, yawreso): pmap = [[0.0 for i in range(yw)] for i in range(xw)] - precast = precasting(minx, miny, xw, yw, xyreso, yawreso) + precast = pre_casting(minx, miny, xw, yw, xyreso, yawreso) for (x, y) in zip(ox, oy): diff --git a/docs/modules/3_mapping/circle_fitting/circle_fitting_main.rst b/docs/modules/3_mapping/circle_fitting/circle_fitting_main.rst index 1892d1f8f7..e243529a9c 100644 --- a/docs/modules/3_mapping/circle_fitting/circle_fitting_main.rst +++ b/docs/modules/3_mapping/circle_fitting/circle_fitting_main.rst @@ -11,3 +11,7 @@ The red crosses are observations from a ranging sensor. The red circle is the estimated object shape using circle fitting. +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: Mapping.circle_fitting.circle_fitting.circle_fitting diff --git a/docs/modules/3_mapping/distance_map/distance_map_main.rst b/docs/modules/3_mapping/distance_map/distance_map_main.rst index 0ef9e3022f..ec60e752c9 100644 --- a/docs/modules/3_mapping/distance_map/distance_map_main.rst +++ b/docs/modules/3_mapping/distance_map/distance_map_main.rst @@ -14,8 +14,8 @@ The algorithm is demonstrated on a simple 2D grid with obstacles: .. image:: distance_map.png -API -~~~ +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. autofunction:: Mapping.DistanceMap.distance_map.compute_sdf diff --git a/docs/modules/3_mapping/gaussian_grid_map/gaussian_grid_map_main.rst b/docs/modules/3_mapping/gaussian_grid_map/gaussian_grid_map_main.rst index b0f112a871..50033d2a20 100644 --- a/docs/modules/3_mapping/gaussian_grid_map/gaussian_grid_map_main.rst +++ b/docs/modules/3_mapping/gaussian_grid_map/gaussian_grid_map_main.rst @@ -6,3 +6,9 @@ Gaussian grid map This is a 2D Gaussian grid mapping example. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Mapping/gaussian_grid_map/animation.gif + +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: Mapping.gaussian_grid_map.gaussian_grid_map.generate_gaussian_grid_map + diff --git a/docs/modules/3_mapping/k_means_object_clustering/k_means_object_clustering_main.rst b/docs/modules/3_mapping/k_means_object_clustering/k_means_object_clustering_main.rst index e098ca5409..0ece604009 100644 --- a/docs/modules/3_mapping/k_means_object_clustering/k_means_object_clustering_main.rst +++ b/docs/modules/3_mapping/k_means_object_clustering/k_means_object_clustering_main.rst @@ -4,3 +4,9 @@ k-means object clustering This is a 2D object clustering with k-means algorithm. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Mapping/kmeans_clustering/animation.gif + +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: Mapping.kmeans_clustering.kmeans_clustering.kmeans_clustering + diff --git a/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst index 1f62179efd..29f5878e48 100644 --- a/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst +++ b/docs/modules/3_mapping/lidar_to_grid_map_tutorial/lidar_to_grid_map_tutorial_main.rst @@ -196,3 +196,9 @@ Let’s use this flood fill on real data: .. image:: lidar_to_grid_map_tutorial_14_1.png +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: Mapping.lidar_to_grid_map.lidar_to_grid_map.main + + diff --git a/docs/modules/3_mapping/normal_vector_estimation/normal_vector_estimation_main.rst b/docs/modules/3_mapping/normal_vector_estimation/normal_vector_estimation_main.rst index a4d1bf0df2..68a19e1534 100644 --- a/docs/modules/3_mapping/normal_vector_estimation/normal_vector_estimation_main.rst +++ b/docs/modules/3_mapping/normal_vector_estimation/normal_vector_estimation_main.rst @@ -25,8 +25,8 @@ This is an example of normal vector calculation: .. figure:: normal_vector_calc.png -API -===== +Code Link +========== .. autofunction:: Mapping.normal_vector_estimation.normal_vector_estimation.calc_normal_vector @@ -67,8 +67,8 @@ This is an example of RANSAC based normal vector estimation: .. figure:: ransac_normal_vector_estimation.png -API -===== +Code Link +========== .. autofunction:: Mapping.normal_vector_estimation.normal_vector_estimation.ransac_normal_vector_estimation diff --git a/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst b/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst index cbb5652f56..8cb08d4b78 100644 --- a/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst +++ b/docs/modules/3_mapping/point_cloud_sampling/point_cloud_sampling_main.rst @@ -27,8 +27,8 @@ This method determines which each point is in a grid, and replaces the point clouds that are in the same Voxel with their average to reduce the number of points. -API -===== +Code Link +========== .. autofunction:: Mapping.point_cloud_sampling.point_cloud_sampling.voxel_point_sampling @@ -61,8 +61,8 @@ Although this method does not have good performance comparing the Farthest distance sample where each point is distributed farther from each other, this is suitable for real-time processing because of its fast computation time. -API -===== +Code Link +========== .. autofunction:: Mapping.point_cloud_sampling.point_cloud_sampling.poisson_disk_sampling diff --git a/docs/modules/3_mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst b/docs/modules/3_mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst index cc5a1a1c5b..bee2f64193 100644 --- a/docs/modules/3_mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst +++ b/docs/modules/3_mapping/ray_casting_grid_map/ray_casting_grid_map_main.rst @@ -3,4 +3,9 @@ Ray casting grid map This is a 2D ray casting grid mapping example. -.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Mapping/raycasting_grid_map/animation.gif \ No newline at end of file +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Mapping/raycasting_grid_map/animation.gif + +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: Mapping.ray_casting_grid_map.ray_casting_grid_map.generate_ray_casting_grid_map diff --git a/docs/modules/3_mapping/rectangle_fitting/rectangle_fitting_main.rst b/docs/modules/3_mapping/rectangle_fitting/rectangle_fitting_main.rst index b6ced1dc1d..06d85efe61 100644 --- a/docs/modules/3_mapping/rectangle_fitting/rectangle_fitting_main.rst +++ b/docs/modules/3_mapping/rectangle_fitting/rectangle_fitting_main.rst @@ -57,8 +57,8 @@ This evaluation function uses the squreed distances between the edges of the rec Calculating the squared error is the same as calculating the variance. The smaller this variance, the more it signifies that the points fit within the rectangle. -API -~~~~~~ +Code Link +~~~~~~~~~~~ .. autoclass:: Mapping.rectangle_fitting.rectangle_fitting.LShapeFitting :members: diff --git a/tests/test_raycasting_grid_map.py b/tests/test_ray_casting_grid_map.py similarity index 71% rename from tests/test_raycasting_grid_map.py rename to tests/test_ray_casting_grid_map.py index f08ae9277e..2d192c9310 100644 --- a/tests/test_raycasting_grid_map.py +++ b/tests/test_ray_casting_grid_map.py @@ -1,5 +1,5 @@ import conftest # Add root path to sys.path -from Mapping.raycasting_grid_map import raycasting_grid_map as m +from Mapping.ray_casting_grid_map import ray_casting_grid_map as m def test1(): From e1cdb24ecfd68ff4eaf51626e5a3f4854da588a4 Mon Sep 17 00:00:00 2001 From: Yuri Harada <55676955+NightzDev@users.noreply.github.com> Date: Fri, 2 May 2025 21:51:20 -0300 Subject: [PATCH 113/181] docs: rewrite internal sensors section with detailed descriptions and references (#1197) * docs: rewrite and enhance internal sensors documentation * Update docs/modules/12_appendix/internal_sensors_main.rst --------- Co-authored-by: Atsushi Sakai --- .../12_appendix/internal_sensors_main.rst | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/docs/modules/12_appendix/internal_sensors_main.rst b/docs/modules/12_appendix/internal_sensors_main.rst index 18f209098e..fa2594a2bf 100644 --- a/docs/modules/12_appendix/internal_sensors_main.rst +++ b/docs/modules/12_appendix/internal_sensors_main.rst @@ -3,32 +3,59 @@ Internal Sensors for Robots ============================ -This project, `PythonRobotics`, focuses on algorithms, so hardware is not included. -However, having basic knowledge of hardware in robotics is also important for understanding algorithms. -Therefore, we will provide an overview. +This project, `PythonRobotics`, focuses on algorithms, so hardware is not included. However, having basic knowledge of hardware in robotics is also important for understanding algorithms. Therefore, we will provide an overview. Introduction ------------- +------------- + +In robotic systems, internal sensors play a crucial role in monitoring the robot’s internal state, such as orientation, acceleration, angular velocity, altitude, and temperature. These sensors provide essential feedback that supports control, localization, and safety mechanisms. While external sensors perceive the environment, internal sensors give the robot self-awareness of its own motion and condition. Understanding the principles and characteristics of these sensors is vital to fully utilize their information within algorithms and decision-making systems. This section outlines the main internal sensors used in robotics. Global Navigation Satellite System (GNSS) -------------------------------------------- +----------------------------------------- + +GNSS is a satellite-based navigation system that provides global positioning and time information by analyzing signals from multiple satellites. It is commonly used in outdoor environments for absolute positioning. Although GNSS offers global coverage without infrastructure dependency, its performance is limited indoors or in obstructed areas, and it suffers from low update rates and susceptibility to signal noise. It is widely used in outdoor navigation for drones, vehicles, and delivery robots. Gyroscope ---------- +A gyroscope measures angular velocity around the robot’s axes, enabling orientation and attitude estimation through detection of the Coriolis effect. Gyroscopes are compact, cost-effective, and provide high update rates, but they are prone to drift and require calibration or sensor fusion for long-term accuracy. These sensors are essential in drones, balancing robots, and IMU-based systems for motion tracking. + Accelerometer --------------- +--------------- + +An accelerometer measures linear acceleration along one or more axes, typically by detecting mass displacement due to motion. It is small, affordable, and useful for detecting movement, tilt, or vibration. However, accelerometers are limited by noisy output and cannot independently determine position without integration and fusion. They are commonly applied in wearable robotics, step counters, and vibration sensing. Magnetometer -------------- +A magnetometer measures the direction and intensity of magnetic fields, enabling heading estimation based on Earth’s magnetic field. It is lightweight and useful for orientation, especially in GPS-denied environments, though it is sensitive to interference from electronics and requires calibration. Magnetometers are often used in conjunction with IMUs for navigation and directional awareness. + Inertial Measurement Unit (IMU) -------------------------------- +An IMU integrates a gyroscope, accelerometer, and sometimes a magnetometer to estimate a robot's motion and orientation through sensor fusion techniques such as Kalman filters. IMUs are compact and provide high-frequency data, which is essential for localization and navigation in GPS-denied areas. Nonetheless, they accumulate drift over time and require complex filtering to maintain accuracy. IMUs are found in drones, mobile robots, and motion tracking systems. + Pressure Sensor ------------------ +---------------- + +Pressure sensors detect atmospheric or fluid pressure by measuring the force exerted on a diaphragm. They are effective for estimating altitude and monitoring environmental conditions, especially in drones and underwater robots. Although cost-effective and efficient, their accuracy may degrade due to temperature variation and limitations in low-altitude resolution. Temperature Sensor -------------------- +Temperature sensors monitor environmental or internal component temperatures, using changes in resistance or voltage depending on sensor type (e.g., RTD or thermocouple). They are simple and reliable for safety and thermal regulation, though they may respond slowly and be affected by nearby electronics. Common applications include battery and motor monitoring, safety systems, and ambient sensing. + +References +---------- +- *Introduction to Autonomous Mobile Robots*, Roland Siegwart, Illah Nourbakhsh, Davide Scaramuzza +- *Probabilistic Robotics*, Sebastian Thrun, Wolfram Burgard, Dieter Fox +- Wikipedia articles: + + - `Inertial Measurement Unit (IMU) `_ + - `Accelerometer `_ + - `Gyroscope `_ + - `Magnetometer `_ + - `Pressure sensor `_ + - `Temperature sensor `_ +- `Adafruit Sensor Guides `_ \ No newline at end of file From d2fe5ae8f0e414f394c00570771c07807d167e68 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sun, 4 May 2025 09:50:49 +0900 Subject: [PATCH 114/181] Add code links to SLAM module documentation (#1212) This commit updates SLAM module documentation files to include direct code links for better navigation. It also adjusts imports in `test_iterative_closest_point.py` to reflect updated module structure and adds a safety check for the directory in `runtests.sh`. --- .../icp_matching.py} | 0 docs/modules/4_slam/FastSLAM1/FastSLAM1_main.rst | 5 +++++ docs/modules/4_slam/FastSLAM2/FastSLAM2_main.rst | 6 ++++++ docs/modules/4_slam/ekf_slam/ekf_slam_main.rst | 6 ++++++ docs/modules/4_slam/graph_slam/graph_slam_main.rst | 6 ++++++ .../iterative_closest_point_matching_main.rst | 8 +++++++- runtests.sh | 1 + tests/test_iterative_closest_point.py | 2 +- 8 files changed, 32 insertions(+), 2 deletions(-) rename SLAM/{iterative_closest_point/iterative_closest_point.py => ICPMatching/icp_matching.py} (100%) diff --git a/SLAM/iterative_closest_point/iterative_closest_point.py b/SLAM/ICPMatching/icp_matching.py similarity index 100% rename from SLAM/iterative_closest_point/iterative_closest_point.py rename to SLAM/ICPMatching/icp_matching.py diff --git a/docs/modules/4_slam/FastSLAM1/FastSLAM1_main.rst b/docs/modules/4_slam/FastSLAM1/FastSLAM1_main.rst index a2aa521216..b6bafa0982 100644 --- a/docs/modules/4_slam/FastSLAM1/FastSLAM1_main.rst +++ b/docs/modules/4_slam/FastSLAM1/FastSLAM1_main.rst @@ -22,6 +22,11 @@ The red points are particles of FastSLAM. Black points are landmarks, blue crosses are estimated landmark positions by FastSLAM. +Code Link +~~~~~~~~~~~~ + +.. autofunction:: SLAM.FastSLAM1.fast_slam1.fast_slam1 + Introduction ~~~~~~~~~~~~ diff --git a/docs/modules/4_slam/FastSLAM2/FastSLAM2_main.rst b/docs/modules/4_slam/FastSLAM2/FastSLAM2_main.rst index 9e79b496a3..59ed3b9f75 100644 --- a/docs/modules/4_slam/FastSLAM2/FastSLAM2_main.rst +++ b/docs/modules/4_slam/FastSLAM2/FastSLAM2_main.rst @@ -7,6 +7,12 @@ The animation has the same meanings as one of FastSLAM 1.0. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/SLAM/FastSLAM2/animation.gif +Code Link +~~~~~~~~~~~ + +.. autofunction:: SLAM.FastSLAM2.fast_slam2.fast_slam2 + + References ~~~~~~~~~~ diff --git a/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst b/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst index a1486ffe1e..3967a7ae4d 100644 --- a/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst +++ b/docs/modules/4_slam/ekf_slam/ekf_slam_main.rst @@ -21,6 +21,12 @@ This is a simulation of EKF SLAM. - Blue line: ground truth - Red line: EKF SLAM position estimation +Code Link +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. autofunction:: SLAM.EKFSLAM.ekf_slam.ekf_slam + + Introduction ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/modules/4_slam/graph_slam/graph_slam_main.rst b/docs/modules/4_slam/graph_slam/graph_slam_main.rst index a2ef513527..2ef17e4179 100644 --- a/docs/modules/4_slam/graph_slam/graph_slam_main.rst +++ b/docs/modules/4_slam/graph_slam/graph_slam_main.rst @@ -13,6 +13,12 @@ The black stars are landmarks for graph edge generation. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/SLAM/GraphBasedSLAM/animation.gif +Code Link +~~~~~~~~~~~~ + +.. autofunction:: SLAM.GraphBasedSLAM.graph_based_slam.graph_based_slam + + .. include:: graphSLAM_doc.rst .. include:: graphSLAM_formulation.rst .. include:: graphSLAM_SE2_example.rst diff --git a/docs/modules/4_slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst b/docs/modules/4_slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst index a30b1fc99b..772fe62889 100644 --- a/docs/modules/4_slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst +++ b/docs/modules/4_slam/iterative_closest_point_matching/iterative_closest_point_matching_main.rst @@ -10,7 +10,13 @@ points to points. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/SLAM/iterative_closest_point/animation.gif +Code Link +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: SLAM.ICPMatching.icp_matching.icp_matching + + References -~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - `Introduction to Mobile Robotics: Iterative Closest Point Algorithm `_ diff --git a/runtests.sh b/runtests.sh index 12d1b80453..c4460c8aa7 100755 --- a/runtests.sh +++ b/runtests.sh @@ -1,4 +1,5 @@ #!/usr/bin/env bash +cd "$(dirname "$0")" || exit 1 echo "Run test suites! " # === pytest based test runner === diff --git a/tests/test_iterative_closest_point.py b/tests/test_iterative_closest_point.py index 3e726b5649..cdfa89cc55 100644 --- a/tests/test_iterative_closest_point.py +++ b/tests/test_iterative_closest_point.py @@ -1,5 +1,5 @@ import conftest -from SLAM.iterative_closest_point import iterative_closest_point as m +from SLAM.ICPMatching import icp_matching as m def test_1(): From a38da41bafaf82bb56d4491d94a0095c1019ac4a Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Sun, 4 May 2025 20:32:11 +0900 Subject: [PATCH 115/181] Add code links to documentation and fix naming inconsistencies (#1213) Added references to related Python functions in documentation for better navigation and usability. Corrected inconsistencies in module and test names to align with their respective directories and improve clarity. --- .../rear_wheel_feedback_control.py} | 0 .../stanley_control.py} | 0 .../10_inverted_pendulum/inverted_pendulum_main.rst | 12 ++++++++++++ .../behavior_tree/behavior_tree_main.rst | 2 +- .../state_machine/state_machine_main.rst | 4 ++-- .../cgmres_nmpc/cgmres_nmpc_main.rst | 5 +++++ .../lqr_speed_and_steering_control_main.rst | 7 ++++++- .../lqr_steering_control_main.rst | 6 +++++- ...l_predictive_speed_and_steering_control_main.rst | 13 ++++++------- .../move_to_a_pose_main.rst} | 8 +++++++- docs/modules/6_path_tracking/path_tracking_main.rst | 2 +- .../pure_pursuit_tracking_main.rst | 6 ++++++ .../rear_wheel_feedback_control_main.rst | 6 ++++++ .../stanley_control/stanley_control_main.rst | 6 ++++++ .../n_joint_arm_to_point_control_main.rst | 7 +++++++ .../obstacle_avoidance_arm_navigation_main.rst | 7 ++++++- .../7_arm_navigation/planar_two_link_ik_main.rst | 6 ++++++ .../drone_3d_trajectory_following_main.rst | 5 +++++ .../rocket_powered_landing_main.rst | 8 ++++++++ .../bipedal_planner/bipedal_planner_main.rst | 5 +++++ tests/test_rear_wheel_feedback.py | 2 +- tests/test_stanley_controller.py | 2 +- 22 files changed, 102 insertions(+), 17 deletions(-) rename PathTracking/{rear_wheel_feedback/rear_wheel_feedback.py => rear_wheel_feedback_control/rear_wheel_feedback_control.py} (100%) rename PathTracking/{stanley_controller/stanley_controller.py => stanley_control/stanley_control.py} (100%) rename docs/modules/6_path_tracking/{move_to_a_pose_control/move_to_a_pose_control_main.rst => move_to_a_pose/move_to_a_pose_main.rst} (97%) diff --git a/PathTracking/rear_wheel_feedback/rear_wheel_feedback.py b/PathTracking/rear_wheel_feedback_control/rear_wheel_feedback_control.py similarity index 100% rename from PathTracking/rear_wheel_feedback/rear_wheel_feedback.py rename to PathTracking/rear_wheel_feedback_control/rear_wheel_feedback_control.py diff --git a/PathTracking/stanley_controller/stanley_controller.py b/PathTracking/stanley_control/stanley_control.py similarity index 100% rename from PathTracking/stanley_controller/stanley_controller.py rename to PathTracking/stanley_control/stanley_control.py diff --git a/docs/modules/10_inverted_pendulum/inverted_pendulum_main.rst b/docs/modules/10_inverted_pendulum/inverted_pendulum_main.rst index 048cbea9ac..58dc0f2e57 100644 --- a/docs/modules/10_inverted_pendulum/inverted_pendulum_main.rst +++ b/docs/modules/10_inverted_pendulum/inverted_pendulum_main.rst @@ -89,6 +89,12 @@ and :math:`P` is the unique positive definite solution to the discrete time .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Control/InvertedPendulumCart/animation_lqr.gif +Code Link +^^^^^^^^^^^ + +.. autofunction:: InvertedPendulum.inverted_pendulum_lqr_control.main + + MPC control ~~~~~~~~~~~~~~~~~~~~~~~~~~~ The MPC controller minimize this cost function defined as: @@ -101,3 +107,9 @@ subject to: - Initial state .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Control/InvertedPendulumCart/animation.gif + +Code Link +^^^^^^^^^^^ + +.. autofunction:: InvertedPendulum.inverted_pendulum_mpc_control.main + diff --git a/docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst b/docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst index ae3e16da81..22849f7c54 100644 --- a/docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst +++ b/docs/modules/13_mission_planning/behavior_tree/behavior_tree_main.rst @@ -5,7 +5,7 @@ Behavior Tree is a modular, hierarchical decision model that is widely used in r It present some similarities to hierarchical state machines with the key difference that the main building block of a behavior is a task rather than a state. Behavior Tree have been shown to generalize several other control architectures (https://ieeexplore.ieee.org/document/7790863) -Core Concepts +Code Link ~~~~~~~~~~~~~ Control Node diff --git a/docs/modules/13_mission_planning/state_machine/state_machine_main.rst b/docs/modules/13_mission_planning/state_machine/state_machine_main.rst index abaece1b11..3f516d46a9 100644 --- a/docs/modules/13_mission_planning/state_machine/state_machine_main.rst +++ b/docs/modules/13_mission_planning/state_machine/state_machine_main.rst @@ -12,8 +12,8 @@ Core Concepts - **Action**: An operation executed during transition (before entering new state) - **Guard**: A precondition that must be satisfied to allow transition -API -~~~ +Code Link +~~~~~~~~~~~ .. autoclass:: MissionPlanning.StateMachine.state_machine.StateMachine :members: add_transition, process, register_state diff --git a/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst b/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst index 23f1218a94..914a4555ff 100644 --- a/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst +++ b/docs/modules/6_path_tracking/cgmres_nmpc/cgmres_nmpc_main.rst @@ -17,6 +17,11 @@ Nonlinear Model Predictive Control with C-GMRES .. figure:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/cgmres_nmpc/animation.gif :alt: gif +Code Link +~~~~~~~~~~~~ + +.. autofunction:: PathTracking.cgmres_nmpc.cgmres_nmpc.NMPCControllerCGMRES + Mathematical Formulation ~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst index 568ef9a0df..b0ba9e0d45 100644 --- a/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst +++ b/docs/modules/6_path_tracking/lqr_speed_and_steering_control/lqr_speed_and_steering_control_main.rst @@ -7,7 +7,12 @@ Path tracking simulation with LQR speed and steering control. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/lqr_speed_steer_control/animation.gif -`[Code Link] `_ + +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathTracking.lqr_speed_steer_control.lqr_speed_steer_control.lqr_speed_steering_control + Overview ~~~~~~~~ diff --git a/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst b/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst index 831423f364..fc8f2a33aa 100644 --- a/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst +++ b/docs/modules/6_path_tracking/lqr_steering_control/lqr_steering_control_main.rst @@ -8,7 +8,11 @@ control. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/lqr_steer_control/animation.gif -`[Code Link] `_ +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathTracking.lqr_steer_control.lqr_steer_control.lqr_steering_control + Overview ~~~~~~~~ diff --git a/docs/modules/6_path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst b/docs/modules/6_path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst index de55b545ba..76a6819a46 100644 --- a/docs/modules/6_path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst +++ b/docs/modules/6_path_tracking/model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control_main.rst @@ -5,13 +5,6 @@ Model predictive speed and steering control .. figure:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/model_predictive_speed_and_steer_control/animation.gif?raw=true :alt: Model predictive speed and steering control - Model predictive speed and steering control - -code: - -`PythonRobotics/model_predictive_speed_and_steer_control.py at master · -AtsushiSakai/PythonRobotics `__ - This is a path tracking simulation using model predictive control (MPC). The MPC controller controls vehicle speed and steering base on @@ -22,6 +15,12 @@ This code uses cvxpy as an optimization modeling tool. - `Welcome to CVXPY 1.0 — CVXPY 1.0.6 documentation `__ +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathTracking.model_predictive_speed_and_steer_control.model_predictive_speed_and_steer_control.iterative_linear_mpc_control + + MPC modeling ~~~~~~~~~~~~ diff --git a/docs/modules/6_path_tracking/move_to_a_pose_control/move_to_a_pose_control_main.rst b/docs/modules/6_path_tracking/move_to_a_pose/move_to_a_pose_main.rst similarity index 97% rename from docs/modules/6_path_tracking/move_to_a_pose_control/move_to_a_pose_control_main.rst rename to docs/modules/6_path_tracking/move_to_a_pose/move_to_a_pose_main.rst index 77ec682a30..19bb0e4c14 100644 --- a/docs/modules/6_path_tracking/move_to_a_pose_control/move_to_a_pose_control_main.rst +++ b/docs/modules/6_path_tracking/move_to_a_pose/move_to_a_pose_main.rst @@ -3,7 +3,13 @@ Move to a Pose Control In this section, we present the logic of PathFinderController that drives a car from a start pose (x, y, theta) to a goal pose. A simulation of moving to a pose control is presented below. -.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/move_to_pose/animation.gif +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Control/move_to_pose/animation.gif + +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathTracking.move_to_pose.move_to_pose.move_to_pose + Position Control of non-Holonomic Systems ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/modules/6_path_tracking/path_tracking_main.rst b/docs/modules/6_path_tracking/path_tracking_main.rst index d98e324583..742cc807e6 100644 --- a/docs/modules/6_path_tracking/path_tracking_main.rst +++ b/docs/modules/6_path_tracking/path_tracking_main.rst @@ -16,4 +16,4 @@ Path tracking is the ability of a robot to follow the reference path generated b lqr_speed_and_steering_control/lqr_speed_and_steering_control model_predictive_speed_and_steering_control/model_predictive_speed_and_steering_control cgmres_nmpc/cgmres_nmpc - move_to_a_pose_control/move_to_a_pose_control + move_to_a_pose/move_to_a_pose diff --git a/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst b/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst index d7354cf8fb..ff6749bbbe 100644 --- a/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst +++ b/docs/modules/6_path_tracking/pure_pursuit_tracking/pure_pursuit_tracking_main.rst @@ -9,6 +9,12 @@ speed control. The red line is a target course, the green cross means the target point for pure pursuit control, the blue line is the tracking. +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathTracking.pure_pursuit.pure_pursuit.pure_pursuit_steer_control + + Reference ~~~~~~~~~~~ diff --git a/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst b/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst index d18cd6fbf7..56d0db63ad 100644 --- a/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst +++ b/docs/modules/6_path_tracking/rear_wheel_feedback_control/rear_wheel_feedback_control_main.rst @@ -6,6 +6,12 @@ PID speed control. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/rear_wheel_feedback/animation.gif +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathTracking.rear_wheel_feedback_control.rear_wheel_feedback_control.rear_wheel_feedback_control + + Reference ~~~~~~~~~~~ - `A Survey of Motion Planning and Control Techniques for Self-driving diff --git a/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst b/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst index 69089ac33b..3c491804f6 100644 --- a/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst +++ b/docs/modules/6_path_tracking/stanley_control/stanley_control_main.rst @@ -6,6 +6,12 @@ control. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathTracking/stanley_controller/animation.gif +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathTracking.stanley_control.stanley_control.stanley_control + + Reference ~~~~~~~~~~~ diff --git a/docs/modules/7_arm_navigation/n_joint_arm_to_point_control_main.rst b/docs/modules/7_arm_navigation/n_joint_arm_to_point_control_main.rst index cc6279f681..56900acde1 100644 --- a/docs/modules/7_arm_navigation/n_joint_arm_to_point_control_main.rst +++ b/docs/modules/7_arm_navigation/n_joint_arm_to_point_control_main.rst @@ -11,3 +11,10 @@ plotting area. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/ArmNavigation/n_joint_arm_to_point_control/animation.gif In this simulation N = 10, however, you can change it. + + +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: ArmNavigation.n_joint_arm_to_point_control.n_joint_arm_to_point_control.main + diff --git a/docs/modules/7_arm_navigation/obstacle_avoidance_arm_navigation_main.rst b/docs/modules/7_arm_navigation/obstacle_avoidance_arm_navigation_main.rst index 899a64a5cd..4433ab531b 100644 --- a/docs/modules/7_arm_navigation/obstacle_avoidance_arm_navigation_main.rst +++ b/docs/modules/7_arm_navigation/obstacle_avoidance_arm_navigation_main.rst @@ -3,4 +3,9 @@ Arm navigation with obstacle avoidance Arm navigation with obstacle avoidance simulation. -.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/ArmNavigation/arm_obstacle_navigation/animation.gif \ No newline at end of file +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/ArmNavigation/arm_obstacle_navigation/animation.gif + +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: ArmNavigation.arm_obstacle_navigation.arm_obstacle_navigation.main diff --git a/docs/modules/7_arm_navigation/planar_two_link_ik_main.rst b/docs/modules/7_arm_navigation/planar_two_link_ik_main.rst index 2da2b0dc30..5b2712eb48 100644 --- a/docs/modules/7_arm_navigation/planar_two_link_ik_main.rst +++ b/docs/modules/7_arm_navigation/planar_two_link_ik_main.rst @@ -11,6 +11,12 @@ This is a interactive simulation. You can set the goal position of the end effector with left-click on the plotting area. +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: ArmNavigation.two_joint_arm_to_point_control.two_joint_arm_to_point_control.main + + Inverse Kinematics for a Planar Two-Link Robotic Arm ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/modules/8_aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst b/docs/modules/8_aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst index c3bdc33cdc..1805bb3f6d 100644 --- a/docs/modules/8_aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst +++ b/docs/modules/8_aerial_navigation/drone_3d_trajectory_following/drone_3d_trajectory_following_main.rst @@ -4,3 +4,8 @@ Drone 3d trajectory following This is a 3d trajectory following simulation for a quadrotor. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/AerialNavigation/drone_3d_trajectory_following/animation.gif + +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: AerialNavigation.drone_3d_trajectory_following.drone_3d_trajectory_following.quad_sim diff --git a/docs/modules/8_aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst b/docs/modules/8_aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst index 6c90d2b44e..4bf5117500 100644 --- a/docs/modules/8_aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst +++ b/docs/modules/8_aerial_navigation/rocket_powered_landing/rocket_powered_landing_main.rst @@ -1,6 +1,14 @@ Rocket powered landing ----------------------------- +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/AerialNavigation/rocket_powered_landing/animation.gif + +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: AerialNavigation.rocket_powered_landing.rocket_powered_landing.main + + Simulation ~~~~~~~~~~ diff --git a/docs/modules/9_bipedal/bipedal_planner/bipedal_planner_main.rst b/docs/modules/9_bipedal/bipedal_planner/bipedal_planner_main.rst index 2ee5971e7a..6253715cc1 100644 --- a/docs/modules/9_bipedal/bipedal_planner/bipedal_planner_main.rst +++ b/docs/modules/9_bipedal/bipedal_planner/bipedal_planner_main.rst @@ -4,3 +4,8 @@ Bipedal Planner Bipedal Walking with modifying designated footsteps .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/Bipedal/bipedal_planner/animation.gif + +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: Bipedal.bipedal_planner.bipedal_planner.BipedalPlanner diff --git a/tests/test_rear_wheel_feedback.py b/tests/test_rear_wheel_feedback.py index 895eb188b3..eccde52358 100644 --- a/tests/test_rear_wheel_feedback.py +++ b/tests/test_rear_wheel_feedback.py @@ -1,5 +1,5 @@ import conftest # Add root path to sys.path -from PathTracking.rear_wheel_feedback import rear_wheel_feedback as m +from PathTracking.rear_wheel_feedback_control import rear_wheel_feedback_control as m def test1(): diff --git a/tests/test_stanley_controller.py b/tests/test_stanley_controller.py index a1d8073764..bd590cb51a 100644 --- a/tests/test_stanley_controller.py +++ b/tests/test_stanley_controller.py @@ -1,5 +1,5 @@ import conftest # Add root path to sys.path -from PathTracking.stanley_controller import stanley_controller as m +from PathTracking.stanley_control import stanley_control as m def test1(): From 73e1c0bebccf4f4fc67ae59ada53b783a8533096 Mon Sep 17 00:00:00 2001 From: Atsushi Sakai Date: Mon, 5 May 2025 17:29:45 +0900 Subject: [PATCH 116/181] Add "Code Link" sections and rename classes for consistency (#1214) This commit adds "Code Link" sections to documentation across various path planning modules, linking to relevant class and function APIs. Additionally, several class renaming changes were made, such as `Dijkstra` to `DijkstraPlanner` and `eta3_trajectory` to `Eta3SplineTrajectory`, to enhance naming consistency. Minor fixes include file restructuring and image renaming for the RRT module. --- ..._rrtstar.py => batch_informed_rrt_star.py} | 0 PathPlanning/Dijkstra/dijkstra.py | 4 +- .../eta3_spline_trajectory.py | 14 ++--- .../bezier_path/bezier_path_main.rst | 9 ++- .../bspline_path/bspline_path_main.rst | 8 +-- .../bugplanner/bugplanner_main.rst | 8 +++ .../catmull_rom_spline_main.rst | 4 +- .../clothoid_path/clothoid_path_main.rst | 5 ++ .../coverage_path/coverage_path_main.rst | 21 +++++++ .../cubic_spline/cubic_spline_main.rst | 8 +-- .../dubins_path/dubins_path_main.rst | 2 +- .../dynamic_window_approach_main.rst | 12 +++- .../elastic_bands/elastic_bands_main.rst | 7 ++- .../eta3_spline/eta3_spline_main.rst | 7 +++ .../frenet_frame_path_main.rst | 6 ++ .../grid_base_search_main.rst | 50 ++++++++++++++++ .../hybridastar/hybridastar_main.rst | 5 ++ .../lqr_path/lqr_path_main.rst | 5 ++ ...l_predictive_trajectory_generator_main.rst | 7 +++ .../prm_planner/prm_planner_main.rst | 7 +++ .../quintic_polynomials_planner_main.rst | 5 ++ .../reeds_shepp_path_main.rst | 7 +++ .../closed_loop_rrt_star_car/Figure_1.png | Bin .../closed_loop_rrt_star_car/Figure_3.png | Bin .../closed_loop_rrt_star_car/Figure_4.png | Bin .../closed_loop_rrt_star_car/Figure_5.png | Bin docs/modules/5_path_planning/rrt/rrt_main.rst | 54 ++++++++++++++++++ docs/modules/5_path_planning/rrt/rrt_star.rst | 6 ++ .../state_lattice_planner_main.rst | 17 ++++++ .../time_based_grid_search_main.rst | 9 +++ .../visibility_road_map_planner_main.rst | 7 ++- .../vrm_planner/vrm_planner_main.rst | 6 ++ tests/test_batch_informed_rrt_star.py | 2 +- 33 files changed, 277 insertions(+), 25 deletions(-) rename PathPlanning/BatchInformedRRTStar/{batch_informed_rrtstar.py => batch_informed_rrt_star.py} (100%) rename docs/modules/5_path_planning/{ => rrt}/closed_loop_rrt_star_car/Figure_1.png (100%) rename docs/modules/5_path_planning/{ => rrt}/closed_loop_rrt_star_car/Figure_3.png (100%) rename docs/modules/5_path_planning/{ => rrt}/closed_loop_rrt_star_car/Figure_4.png (100%) rename docs/modules/5_path_planning/{ => rrt}/closed_loop_rrt_star_car/Figure_5.png (100%) diff --git a/PathPlanning/BatchInformedRRTStar/batch_informed_rrtstar.py b/PathPlanning/BatchInformedRRTStar/batch_informed_rrt_star.py similarity index 100% rename from PathPlanning/BatchInformedRRTStar/batch_informed_rrtstar.py rename to PathPlanning/BatchInformedRRTStar/batch_informed_rrt_star.py diff --git a/PathPlanning/Dijkstra/dijkstra.py b/PathPlanning/Dijkstra/dijkstra.py index 8a585e4b18..f5a4703910 100644 --- a/PathPlanning/Dijkstra/dijkstra.py +++ b/PathPlanning/Dijkstra/dijkstra.py @@ -12,7 +12,7 @@ show_animation = True -class Dijkstra: +class DijkstraPlanner: def __init__(self, ox, oy, resolution, robot_radius): """ @@ -246,7 +246,7 @@ def main(): plt.grid(True) plt.axis("equal") - dijkstra = Dijkstra(ox, oy, grid_size, robot_radius) + dijkstra = DijkstraPlanner(ox, oy, grid_size, robot_radius) rx, ry = dijkstra.planning(sx, sy, gx, gy) if show_animation: # pragma: no cover diff --git a/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py b/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py index a73797dacb..e72d33261e 100644 --- a/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py +++ b/PathPlanning/Eta3SplineTrajectory/eta3_spline_trajectory.py @@ -29,7 +29,7 @@ def __init__(self, actual_vel, max_vel): self.message = f'Actual velocity {actual_vel} does not equal desired max velocity {max_vel}!' -class eta3_trajectory(Eta3Path): +class Eta3SplineTrajectory(Eta3Path): """ eta3_trajectory @@ -300,8 +300,8 @@ def test1(max_vel=0.5): trajectory_segments.append(Eta3PathSegment( start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) - traj = eta3_trajectory(trajectory_segments, - max_vel=max_vel, max_accel=0.5) + traj = Eta3SplineTrajectory(trajectory_segments, + max_vel=max_vel, max_accel=0.5) # interpolate at several points along the path times = np.linspace(0, traj.total_time, 101) @@ -334,8 +334,8 @@ def test2(max_vel=0.5): trajectory_segments.append(Eta3PathSegment( start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) - traj = eta3_trajectory(trajectory_segments, - max_vel=max_vel, max_accel=0.5) + traj = Eta3SplineTrajectory(trajectory_segments, + max_vel=max_vel, max_accel=0.5) # interpolate at several points along the path times = np.linspace(0, traj.total_time, 101) @@ -400,8 +400,8 @@ def test3(max_vel=2.0): start_pose=start_pose, end_pose=end_pose, eta=eta, kappa=kappa)) # construct the whole path - traj = eta3_trajectory(trajectory_segments, - max_vel=max_vel, max_accel=0.5, max_jerk=1) + traj = Eta3SplineTrajectory(trajectory_segments, + max_vel=max_vel, max_accel=0.5, max_jerk=1) # interpolate at several points along the path times = np.linspace(0, traj.total_time, 1001) diff --git a/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst b/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst index 9d9b3de709..19fb89a1b1 100644 --- a/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst +++ b/docs/modules/5_path_planning/bezier_path/bezier_path_main.rst @@ -13,8 +13,15 @@ You can get different Beizer course: .. image:: Figure_2.png +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathPlanning.BezierPath.bezier_path.calc_4points_bezier_path + + Reference +~~~~~~~~~~~~~~~ - `Continuous Curvature Path Generation Based on Bezier Curves for Autonomous - Vehicles ` + Vehicles `__ diff --git a/docs/modules/5_path_planning/bspline_path/bspline_path_main.rst b/docs/modules/5_path_planning/bspline_path/bspline_path_main.rst index e734352e34..00e5ef2fdb 100644 --- a/docs/modules/5_path_planning/bspline_path/bspline_path_main.rst +++ b/docs/modules/5_path_planning/bspline_path/bspline_path_main.rst @@ -105,8 +105,8 @@ The default spline degree is 3, so curvature changes smoothly. .. image:: interp_and_curvature.png -API -++++ +Code link +++++++++++ .. autofunction:: PathPlanning.BSplinePath.bspline_path.interpolate_b_spline_path @@ -133,8 +133,8 @@ The default spline degree is 3, so curvature changes smoothly. .. image:: approx_and_curvature.png -API -++++ +Code Link +++++++++++ .. autofunction:: PathPlanning.BSplinePath.bspline_path.approximate_b_spline_path diff --git a/docs/modules/5_path_planning/bugplanner/bugplanner_main.rst b/docs/modules/5_path_planning/bugplanner/bugplanner_main.rst index 81c91c0313..e1cd2fe353 100644 --- a/docs/modules/5_path_planning/bugplanner/bugplanner_main.rst +++ b/docs/modules/5_path_planning/bugplanner/bugplanner_main.rst @@ -5,4 +5,12 @@ This is a 2D planning with Bug algorithm. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/BugPlanner/animation.gif +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathPlanning.BugPlanning.bug.main + +Reference +~~~~~~~~~~~~ + - `ECE452 Bug Algorithms `_ diff --git a/docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_spline_main.rst b/docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_spline_main.rst index 72e558c486..fa2a2ff72b 100644 --- a/docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_spline_main.rst +++ b/docs/modules/5_path_planning/catmull_rom_spline/catmull_rom_spline_main.rst @@ -88,8 +88,8 @@ Catmull-Rom Spline API This section provides an overview of the functions used for Catmull-Rom spline path planning. -API -++++ +Code Link +++++++++++ .. autofunction:: PathPlanning.Catmull_RomSplinePath.catmull_rom_spline_path.catmull_rom_point diff --git a/docs/modules/5_path_planning/clothoid_path/clothoid_path_main.rst b/docs/modules/5_path_planning/clothoid_path/clothoid_path_main.rst index 1e8cee694a..16d0ec03c1 100644 --- a/docs/modules/5_path_planning/clothoid_path/clothoid_path_main.rst +++ b/docs/modules/5_path_planning/clothoid_path/clothoid_path_main.rst @@ -73,6 +73,11 @@ The final clothoid path can be calculated with the path parameters and Fresnel i &y(s)=y_{0}+\int_{0}^{s} \sin \left(\frac{1}{2} \kappa^{\prime} \tau^{2}+\kappa \tau+\vartheta_{0}\right) \mathrm{d} \tau \end{aligned} +Code Link +~~~~~~~~~~~~~ + +.. autofunction:: PathPlanning.ClothoidPath.clothoid_path_planner.generate_clothoid_path + References ~~~~~~~~~~ diff --git a/docs/modules/5_path_planning/coverage_path/coverage_path_main.rst b/docs/modules/5_path_planning/coverage_path/coverage_path_main.rst index 0a688b5ed2..eaa876c80b 100644 --- a/docs/modules/5_path_planning/coverage_path/coverage_path_main.rst +++ b/docs/modules/5_path_planning/coverage_path/coverage_path_main.rst @@ -8,6 +8,11 @@ This is a 2D grid based sweep coverage path planner simulation: .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/GridBasedSweepCPP/animation.gif +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.GridBasedSweepCPP.grid_based_sweep_coverage_path_planner.planning + Spiral Spanning Tree ~~~~~~~~~~~~~~~~~~~~ @@ -17,6 +22,14 @@ This is a 2D grid based spiral spanning tree coverage path planner simulation: .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/SpiralSpanningTreeCPP/animation2.gif .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/SpiralSpanningTreeCPP/animation3.gif +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.SpiralSpanningTreeCPP.spiral_spanning_tree_coverage_path_planner.main + +Reference ++++++++++++++ + - `Spiral-STC: An On-Line Coverage Algorithm of Grid Environments by a Mobile Robot `_ @@ -29,6 +42,14 @@ This is a 2D grid based wavefront coverage path planner simulation: .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/WavefrontCPP/animation2.gif .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/WavefrontCPP/animation3.gif +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.WavefrontCPP.wavefront_coverage_path_planner.wavefront + +Reference ++++++++++++++ + - `Planning paths of complete coverage of an unstructured environment by a mobile robot `_ diff --git a/docs/modules/5_path_planning/cubic_spline/cubic_spline_main.rst b/docs/modules/5_path_planning/cubic_spline/cubic_spline_main.rst index 33471f7c85..a110217a2e 100644 --- a/docs/modules/5_path_planning/cubic_spline/cubic_spline_main.rst +++ b/docs/modules/5_path_planning/cubic_spline/cubic_spline_main.rst @@ -171,8 +171,8 @@ the second derivative by: These equations can be calculated by differentiating the cubic polynomial. -API -=== +Code Link +========== This is the 1D cubic spline class API: @@ -199,8 +199,8 @@ Curvature of each point can be also calculated analytically by: :math:`\kappa=\frac{y^{\prime \prime} x^{\prime}-x^{\prime \prime} y^{\prime}}{\left(x^{\prime2}+y^{\prime2}\right)^{\frac{2}{3}}}` -API -=== +Code Link +========== .. autoclass:: PathPlanning.CubicSpline.cubic_spline_planner.CubicSpline2D :members: diff --git a/docs/modules/5_path_planning/dubins_path/dubins_path_main.rst b/docs/modules/5_path_planning/dubins_path/dubins_path_main.rst index 12172fb51e..5a3d14464b 100644 --- a/docs/modules/5_path_planning/dubins_path/dubins_path_main.rst +++ b/docs/modules/5_path_planning/dubins_path/dubins_path_main.rst @@ -62,7 +62,7 @@ You can generate a path from these information and the maximum curvature informa A path type which has minimum course length among 6 types is selected, and then a path is constructed based on the selected type and its distances. -API +Code Link ~~~~~~~~~~~~~~~~~~~~ .. autofunction:: PathPlanning.DubinsPath.dubins_path_planner.plan_dubins_path diff --git a/docs/modules/5_path_planning/dynamic_window_approach/dynamic_window_approach_main.rst b/docs/modules/5_path_planning/dynamic_window_approach/dynamic_window_approach_main.rst index d645838597..ac5322df94 100644 --- a/docs/modules/5_path_planning/dynamic_window_approach/dynamic_window_approach_main.rst +++ b/docs/modules/5_path_planning/dynamic_window_approach/dynamic_window_approach_main.rst @@ -5,7 +5,17 @@ Dynamic Window Approach This is a 2D navigation sample code with Dynamic Window Approach. +.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/DynamicWindowApproach/animation.gif + +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.DynamicWindowApproach.dynamic_window_approach.dwa_control + + +Reference +~~~~~~~~~~~~ + - `The Dynamic Window Approach to Collision Avoidance `__ -.. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/DynamicWindowApproach/animation.gif diff --git a/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst b/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst index 8a3e517105..d0109d4ec3 100644 --- a/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst +++ b/docs/modules/5_path_planning/elastic_bands/elastic_bands_main.rst @@ -5,6 +5,11 @@ This is a path planning with Elastic Bands. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/ElasticBands/animation.gif +Code Link ++++++++++++++ + +.. autoclass:: PathPlanning.ElasticBands.elastic_bands.ElasticBands + Core Concept ~~~~~~~~~~~~ @@ -69,6 +74,6 @@ Dynamic Path Maintenance - Remove redundant nodes if adjacent nodes are too close References -~~~~~~~~~~~~~~~~~~~~~~~ ++++++++++++++ - `Elastic Bands: Connecting Path Planning and Control `__ diff --git a/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst b/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst index 82e0a71044..9ee343e8a7 100644 --- a/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst +++ b/docs/modules/5_path_planning/eta3_spline/eta3_spline_main.rst @@ -7,7 +7,14 @@ Eta^3 Spline path planning This is a path planning with Eta^3 spline. +Code Link +~~~~~~~~~~~~~~~ + +.. autoclass:: PathPlanning.Eta3SplineTrajectory.eta3_spline_trajectory.Eta3SplineTrajectory + + Reference +~~~~~~~~~~~~~~~ - `\\eta^3-Splines for the Smooth Path Generation of Wheeled Mobile Robots `__ diff --git a/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst b/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst index 371d536e3f..0f84d381ea 100644 --- a/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst +++ b/docs/modules/5_path_planning/frenet_frame_path/frenet_frame_path_main.rst @@ -7,6 +7,12 @@ The cyan line is the target course and black crosses are obstacles. The red line is predicted path. +Code Link +~~~~~~~~~~~~~~ + +.. autofunction:: PathPlanning.FrenetOptimalTrajectory.frenet_optimal_trajectory.main + + High Speed and Velocity Keeping Scenario ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst b/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst index bf82c9f391..c4aa6882aa 100644 --- a/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst +++ b/docs/modules/5_path_planning/grid_base_search/grid_base_search_main.rst @@ -10,6 +10,12 @@ This is a 2D grid based path planning with Breadth first search algorithm. In the animation, cyan points are searched nodes. +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.BreadthFirstSearch.breadth_first_search.BreadthFirstSearchPlanner + + Depth First Search ~~~~~~~~~~~~~~~~~~~~ @@ -19,6 +25,12 @@ This is a 2D grid based path planning with Depth first search algorithm. In the animation, cyan points are searched nodes. +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.DepthFirstSearch.depth_first_search.DepthFirstSearchPlanner + + .. _dijkstra: Dijkstra algorithm @@ -30,6 +42,12 @@ This is a 2D grid based shortest path planning with Dijkstra's algorithm. In the animation, cyan points are searched nodes. +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.Dijkstra.dijkstra.DijkstraPlanner + + .. _a*-algorithm: A\* algorithm @@ -43,6 +61,12 @@ In the animation, cyan points are searched nodes. Its heuristic is 2D Euclid distance. +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.AStar.a_star.AStarPlanner + + Bidirectional A\* algorithm ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -52,6 +76,12 @@ This is a 2D grid based shortest path planning with bidirectional A star algorit In the animation, cyan points are searched nodes. +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.BidirectionalAStar.bidirectional_a_star.BidirectionalAStarPlanner + + .. _D*-algorithm: D\* algorithm @@ -63,7 +93,14 @@ This is a 2D grid based shortest path planning with D star algorithm. The animation shows a robot finding its path avoiding an obstacle using the D* search algorithm. +Code Link ++++++++++++++ + +.. autoclass:: PathPlanning.DStar.dstar.Dstar + + Reference +++++++++++++ - `D* search Wikipedia `__ @@ -74,7 +111,13 @@ This is a 2D grid based path planning and replanning with D star lite algorithm. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/DStarLite/animation.gif +Code Link ++++++++++++++ + +.. autoclass:: PathPlanning.DStarLite.d_star_lite.DStarLite + Reference +++++++++++++ - `Improved Fast Replanning for Robot Navigation in Unknown Terrain `_ @@ -88,7 +131,14 @@ This is a 2D grid based path planning with Potential Field algorithm. In the animation, the blue heat map shows potential value on each grid. +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.PotentialFieldPlanning.potential_field_planning.potential_field_planning + + Reference +++++++++++++ - `Robotic Motion Planning:Potential Functions `__ diff --git a/docs/modules/5_path_planning/hybridastar/hybridastar_main.rst b/docs/modules/5_path_planning/hybridastar/hybridastar_main.rst index a9876fa3d4..36f340e0c2 100644 --- a/docs/modules/5_path_planning/hybridastar/hybridastar_main.rst +++ b/docs/modules/5_path_planning/hybridastar/hybridastar_main.rst @@ -4,3 +4,8 @@ Hybrid a star This is a simple vehicle model based hybrid A\* path planner. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/HybridAStar/animation.gif + +Code Link ++++++++++++++ + +.. autofunction:: PathPlanning.HybridAStar.hybrid_a_star.hybrid_a_star_planning diff --git a/docs/modules/5_path_planning/lqr_path/lqr_path_main.rst b/docs/modules/5_path_planning/lqr_path/lqr_path_main.rst index 788442ff63..1eb1e4d840 100644 --- a/docs/modules/5_path_planning/lqr_path/lqr_path_main.rst +++ b/docs/modules/5_path_planning/lqr_path/lqr_path_main.rst @@ -4,3 +4,8 @@ LQR based path planning A sample code using LQR based path planning for double integrator model. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/LQRPlanner/animation.gif?raw=true + +Code Link ++++++++++++++ + +.. autoclass:: PathPlanning.LQRPlanner.lqr_planner.LQRPlanner diff --git a/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst b/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst index 1c5df1c9cc..76472a6792 100644 --- a/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst +++ b/docs/modules/5_path_planning/model_predictive_trajectory_generator/model_predictive_trajectory_generator_main.rst @@ -6,6 +6,12 @@ generator. This algorithm is used for state lattice planner. +Code Link +~~~~~~~~~~~~~ + +.. autofunction:: PathPlanning.ModelPredictiveTrajectoryGenerator.trajectory_generator.optimize_trajectory + + Path optimization sample ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -17,6 +23,7 @@ Lookup table generation sample .. image:: lookup_table.png Reference +~~~~~~~~~~~~ - `Optimal rough terrain trajectory generation for wheeled mobile robots `__ diff --git a/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst b/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst index f816c5c1b9..d58d1e2633 100644 --- a/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst +++ b/docs/modules/5_path_planning/prm_planner/prm_planner_main.rst @@ -13,7 +13,14 @@ Cyan crosses means searched points with Dijkstra method, The red line is the final path of PRM. +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathPlanning.ProbabilisticRoadMap.probabilistic_road_map.prm_planning + + Reference +~~~~~~~~~~~ - `Probabilistic roadmap - Wikipedia `__ diff --git a/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst b/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst index 66c3001c05..c7bc3fb55c 100644 --- a/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst +++ b/docs/modules/5_path_planning/quintic_polynomials_planner/quintic_polynomials_planner_main.rst @@ -9,6 +9,11 @@ Motion planning with quintic polynomials. It can calculate 2D path, velocity, and acceleration profile based on quintic polynomials. +Code Link +~~~~~~~~~~~~~~~ + +.. autofunction:: PathPlanning.QuinticPolynomialsPlanner.quintic_polynomials_planner.quintic_polynomials_planner + Quintic polynomials for one dimensional robot motion diff --git a/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst b/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst index a4a5d28e01..4dd54d7c97 100644 --- a/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst +++ b/docs/modules/5_path_planning/reeds_shepp_path/reeds_shepp_path_main.rst @@ -5,6 +5,12 @@ A sample code with Reeds Shepp path planning. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/ReedsSheppPath/animation.gif?raw=true +Code Link +============== + +.. autofunction:: PathPlanning.ReedsSheppPath.reeds_shepp_path_planning.reeds_shepp_path_planning + + Mathematical Description of Individual Path Types ================================================= Here is an overview of mathematical derivations of formulae for individual path types. @@ -381,6 +387,7 @@ Hence, we have: Reference +============= - `15.3.2 Reeds-Shepp Curves `__ diff --git a/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_1.png b/docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_1.png similarity index 100% rename from docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_1.png rename to docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_1.png diff --git a/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_3.png b/docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_3.png similarity index 100% rename from docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_3.png rename to docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_3.png diff --git a/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_4.png b/docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_4.png similarity index 100% rename from docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_4.png rename to docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_4.png diff --git a/docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_5.png b/docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_5.png similarity index 100% rename from docs/modules/5_path_planning/closed_loop_rrt_star_car/Figure_5.png rename to docs/modules/5_path_planning/rrt/closed_loop_rrt_star_car/Figure_5.png diff --git a/docs/modules/5_path_planning/rrt/rrt_main.rst b/docs/modules/5_path_planning/rrt/rrt_main.rst index da3a4957a5..1bd5497f4c 100644 --- a/docs/modules/5_path_planning/rrt/rrt_main.rst +++ b/docs/modules/5_path_planning/rrt/rrt_main.rst @@ -14,6 +14,12 @@ This is a simple path planning code with Rapidly-Exploring Random Trees Black circles are obstacles, green line is a searched tree, red crosses are start and goal positions. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.RRT.rrt.RRT + + .. include:: rrt_star.rst @@ -24,6 +30,12 @@ RRT with dubins path Path planning for a car robot with RRT and dubins path planner. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.RRTDubins.rrt_dubins.RRTDubins + + .. _rrt*-with-dubins-path: RRT\* with dubins path @@ -33,6 +45,12 @@ RRT\* with dubins path Path planning for a car robot with RRT\* and dubins path planner. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.RRTStarDubins.rrt_star_dubins.RRTStarDubins + + .. _rrt*-with-reeds-sheep-path: RRT\* with reeds-sheep path @@ -42,6 +60,12 @@ RRT\* with reeds-sheep path Path planning for a car robot with RRT\* and reeds sheep path planner. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.RRTStarReedsShepp.rrt_star_reeds_shepp.RRTStarReedsShepp + + .. _informed-rrt*: Informed RRT\* @@ -53,7 +77,14 @@ This is a path planning code with Informed RRT*. The cyan ellipse is the heuristic sampling domain of Informed RRT*. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.InformedRRTStar.informed_rrt_star.InformedRRTStar + + Reference +^^^^^^^^^^ - `Informed RRT\*: Optimal Sampling-based Path Planning Focused via Direct Sampling of an Admissible Ellipsoidal @@ -68,12 +99,20 @@ Batch Informed RRT\* This is a path planning code with Batch Informed RRT*. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.BatchInformedRRTStar.batch_informed_rrt_star.BITStar + + Reference +^^^^^^^^^^^ - `Batch Informed Trees (BIT*): Sampling-based Optimal Planning via the Heuristically Guided Search of Implicit Random Geometric Graphs `__ + .. _closed-loop-rrt*: Closed Loop RRT\* @@ -87,7 +126,14 @@ In this code, pure-pursuit algorithm is used for steering control, PID is used for speed control. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.ClosedLoopRRTStar.closed_loop_rrt_star_car.ClosedLoopRRTStar + + Reference +^^^^^^^^^^^^ - `Motion Planning in Complex Environments using Closed-loop Prediction `__ @@ -98,6 +144,7 @@ Reference - `[1601.06326] Sampling-based Algorithms for Optimal Motion Planning Using Closed-loop Prediction `__ + .. _lqr-rrt*: LQR-RRT\* @@ -109,7 +156,14 @@ A double integrator motion model is used for LQR local planner. .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/LQRRRTStar/animation.gif +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.LQRRRTStar.lqr_rrt_star.LQRRRTStar + + Reference +~~~~~~~~~~~~~ - `LQR-RRT\*: Optimal Sampling-Based Motion Planning with Automatically Derived Extension diff --git a/docs/modules/5_path_planning/rrt/rrt_star.rst b/docs/modules/5_path_planning/rrt/rrt_star.rst index 6deb6b9172..960b9976d5 100644 --- a/docs/modules/5_path_planning/rrt/rrt_star.rst +++ b/docs/modules/5_path_planning/rrt/rrt_star.rst @@ -7,6 +7,12 @@ This is a path planning code with RRT\* Black circles are obstacles, green line is a searched tree, red crosses are start and goal positions. +Code Link +^^^^^^^^^^ + +.. autoclass:: PathPlanning.RRTStar.rrt_star.RRTStar + + Simulation ^^^^^^^^^^ diff --git a/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst b/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst index bf89ac11ae..9f8cc0c50f 100644 --- a/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst +++ b/docs/modules/5_path_planning/state_lattice_planner/state_lattice_planner_main.rst @@ -12,17 +12,34 @@ Uniform polar sampling .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/StateLatticePlanner/UniformPolarSampling.gif +Code Link +^^^^^^^^^^^^^ + +.. autofunction:: PathPlanning.StateLatticePlanner.state_lattice_planner.calc_uniform_polar_states + + Biased polar sampling ~~~~~~~~~~~~~~~~~~~~~ .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/StateLatticePlanner/BiasedPolarSampling.gif +Code Link +^^^^^^^^^^^^^ +.. autofunction:: PathPlanning.StateLatticePlanner.state_lattice_planner.calc_biased_polar_states + + Lane sampling ~~~~~~~~~~~~~ .. image:: https://github.com/AtsushiSakai/PythonRoboticsGifs/raw/master/PathPlanning/StateLatticePlanner/LaneSampling.gif +Code Link +^^^^^^^^^^^^^ +.. autofunction:: PathPlanning.StateLatticePlanner.state_lattice_planner.calc_lane_states + + Reference +~~~~~~~~~~~~~~~ - `Optimal rough terrain trajectory generation for wheeled mobile robots `__ diff --git a/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst index 04001d4504..9517e95b31 100644 --- a/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst +++ b/docs/modules/5_path_planning/time_based_grid_search/time_based_grid_search_main.rst @@ -33,6 +33,10 @@ After:: When starting at (1, 11) in the structured obstacle arrangement (second of the two gifs above). +Code Link +^^^^^^^^^^^^^ +.. autoclass:: PathPlanning.TimeBasedPathPlanning.SpaceTimeAStar.SpaceTimeAStar + Safe Interval Path Planning ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -73,6 +77,11 @@ Arrangement 1 starting at (1, 18):: .. image:: https://raw.githubusercontent.com/AtsushiSakai/PythonRoboticsGifs/refs/heads/master/PathPlanning/TimeBasedPathPlanning/SafeIntervalPathPlanner/path_animation2.gif +Code Link +^^^^^^^^^^^^^ +.. autoclass:: PathPlanning.TimeBasedPathPlanning.SafeInterval.SafeIntervalPathPlanner + + References ~~~~~~~~~~~ diff --git a/docs/modules/5_path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst b/docs/modules/5_path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst index 3c9b7c008c..aac96d6e19 100644 --- a/docs/modules/5_path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst +++ b/docs/modules/5_path_planning/visibility_road_map_planner/visibility_road_map_planner_main.rst @@ -13,6 +13,11 @@ red crosses are visibility nodes, and blue lines area collision free visibility The red line is the final path searched by dijkstra algorithm frm the visibility graphs. +Code Link +~~~~~~~~~~~~ +.. autoclass:: PathPlanning.VisibilityRoadMap.visibility_road_map.VisibilityRoadMap + + Algorithms ~~~~~~~~~~ @@ -64,7 +69,7 @@ The red line is searched path in the figure: You can find the details of Dijkstra algorithm in :ref:`dijkstra`. References -^^^^^^^^^^ +~~~~~~~~~~~~ - `Visibility graph - Wikipedia `_ diff --git a/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst b/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst index 65e0e53465..a9a41aab74 100644 --- a/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst +++ b/docs/modules/5_path_planning/vrm_planner/vrm_planner_main.rst @@ -11,7 +11,13 @@ Cyan crosses mean searched points with Dijkstra method, The red line is the final path of Vornoi Road-Map. +Code Link +~~~~~~~~~~~~~~~ +.. autoclass:: PathPlanning.VoronoiRoadMap.voronoi_road_map.VoronoiRoadMapPlanner + + Reference +~~~~~~~~~~~~ - `Robotic Motion Planning `__ diff --git a/tests/test_batch_informed_rrt_star.py b/tests/test_batch_informed_rrt_star.py index 3ad78bdb23..cf0a9827a8 100644 --- a/tests/test_batch_informed_rrt_star.py +++ b/tests/test_batch_informed_rrt_star.py @@ -1,7 +1,7 @@ import random import conftest -from PathPlanning.BatchInformedRRTStar import batch_informed_rrtstar as m +from PathPlanning.BatchInformedRRTStar import batch_informed_rrt_star as m def test_1(): From 0c82dde2be24e5f3955018caa061750d7404978d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 07:22:51 +0900 Subject: [PATCH 117/181] build(deps): bump ruff from 0.11.7 to 0.11.8 in /requirements (#1215) Bumps [ruff](https://github.com/astral-sh/ruff) from 0.11.7 to 0.11.8. - [Release notes](https://github.com/astral-sh/ruff/releases) - [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md) - [Commits](https://github.com/astral-sh/ruff/compare/0.11.7...0.11.8) --- updated-dependencies: - dependency-name: ruff dependency-version: 0.11.8 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 88cbb21f81..e78b9b8d51 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -6,4 +6,4 @@ ecos == 2.0.14 pytest == 8.3.5 # For unit test pytest-xdist == 3.6.1 # For unit test mypy == 1.15.0 # For unit test -ruff == 0.11.7 # For unit test +ruff == 0.11.8 # For unit test From 292c9dc613d1b0ace3d25ca8a0295e19549516f2 Mon Sep 17 00:00:00 2001 From: atm <72185056+sutatoruta@users.noreply.github.com> Date: Sat, 10 May 2025 09:34:19 +0900 Subject: [PATCH 118/181] fix broken links (#1216) --- .github/pull_request_template.md | 2 +- README.md | 12 ++++++------ users_comments.md | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b6ac52efa2..c0d9f7eab2 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,7 +1,7 @@