From 128726046850c79569a2c49a6271f00700caaa1e Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 20:04:31 +0800 Subject: [PATCH 01/42] change badge in readme.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 22146240..e5b3cf62 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # TOPP-RA -[![Build Status](https://travis-ci.org/hungpham2511/toppra.svg?branch=master)](https://travis-ci.org/hungpham2511/toppra) [![Coverage Status](https://coveralls.io/repos/github/hungpham2511/toppra/badge.svg?branch=master)](https://coveralls.io/github/hungpham2511/toppra?branch=master) +[![CircleCI](https://circleci.com/gh/hungpham2511/toppra.svg?style=svg)](https://circleci.com/gh/hungpham2511/toppra) +[![Coverage Status](https://coveralls.io/repos/github/hungpham2511/toppra/badge.svg?branch=master)](https://coveralls.io/github/hungpham2511/toppra?branch=master) [![Documentation Status](https://readthedocs.org/projects/toppra/badge/?version=latest)](https://toppra.readthedocs.io/en/latest/?badge=latest) -[![Codacy Badge](https://api.codacy.com/project/badge/Grade/e77c2abbdaa04578b115d2911a146fcb)](https://app.codacy.com/app/hungpham2511/toppra?utm_source=github.com&utm_medium=referral&utm_content=hungpham2511/toppra&utm_campaign=Badge_Grade_Dashboard) **Documentation and tutorials** are available at (https://toppra.readthedocs.io/en/latest/index.html). From 3775b3d7fa51e966ce11fd52fb1cde82143945c7 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 20:16:18 +0800 Subject: [PATCH 02/42] allow codestyle failure (for now) --- .circleci/config.yml | 2 +- requirements3.txt | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index b7e03432..0be6da91 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -43,7 +43,7 @@ jobs: name: check codestyle command: | . venv3/bin/activate - make lint + make lint || echo "allow failure" - store_artifacts: path: test-reports diff --git a/requirements3.txt b/requirements3.txt index 08596ac2..4cfce879 100644 --- a/requirements3.txt +++ b/requirements3.txt @@ -5,3 +5,8 @@ scipy>0.18 coloredlogs enum34 matplotlib +# dev +pylint +pycodestyle +pydocstyle +invoke From 2b9a69608318b21dcf4c273b996e1c3c9b4fc3e9 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 20:23:45 +0800 Subject: [PATCH 03/42] add openrave setup during tests --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 0f914bd4..97f4c1c5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,9 +11,11 @@ @pytest.fixture(scope="session") def rave_env(): + orpy.RaveInitialize(load_all_plugins=True) env = orpy.Environment() yield env env.Destroy() + orpy.RaveDestroy() def pytest_addoption(parser): From f1def9758c68a6c25c3cbaff801df4212ee351dd Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 20:43:27 +0800 Subject: [PATCH 04/42] fix failing tests --- toppra/constraint/linear_second_order.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/toppra/constraint/linear_second_order.py b/toppra/constraint/linear_second_order.py index 0be8bcc3..1e8b2c36 100644 --- a/toppra/constraint/linear_second_order.py +++ b/toppra/constraint/linear_second_order.py @@ -105,11 +105,7 @@ def __init__(self, inv_dyn, constraint_F, constraint_g, dof, custom_term=None, d self.constraint_F = constraint_F self.constraint_g = constraint_g self.dof = dof - if friction is None: - self.friction = lambda s: np.zeros(self.dof) - else: - logger.warn("Friction is not handled due to a bug.") - self.friction = friction + self.custom_term = custom_term self._format_string = " Kind: Generalized Second-order constraint\n" self._format_string = " Dimension:\n" self._format_string += " F in R^({:d}, {:d})\n".format( @@ -164,6 +160,9 @@ def compute_constraint_params(self, path, gridpoints, scaling=1): self.inv_dyn(_p, _ps, pss_) for _p, _ps, pss_ in zip(p_vec, ps_vec, pss_vec) ]) - c_vec + if self.custom_term is not None: + for i, _ in enumerate(gridpoints): + c_vec[i] = c_vec[i] + self.custom_term(path, gridpoints[i] / scaling) if self.discretization_type == DiscretizationType.Collocation: return a_vec, b_vec, c_vec, F_vec, g_vec, None, None From 0b6bed9a310c5db52af39512feff059c2c3bafd7 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 20:59:16 +0800 Subject: [PATCH 05/42] minor changes --- docs/source/conf.py | 7 +++++-- examples/kinematics.py | 2 +- requirements3.txt | 2 ++ toppra/constraint/__init__.py | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 77b5b54d..4dac13e0 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,7 +19,10 @@ import os import sys import toppra -import pathlib2 +try: + import pathlib2 as pathlib +except ImportError: + import pathlib # sys.path.insert(0, os.path.abspath('../../toppra/')) @@ -68,7 +71,7 @@ copyright = '2019, Hung Pham' author = 'Hung Pham' -version = pathlib2.Path('./../../VERSION').read_text() +version = pathlib.Path('./../../VERSION').read_text() # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/examples/kinematics.py b/examples/kinematics.py index d6c4dfd6..705f94ed 100644 --- a/examples/kinematics.py +++ b/examples/kinematics.py @@ -32,7 +32,7 @@ def main(): # Setup a parametrization instance. The keyword arguments are # optional. - instance = algo.TOPPRA([pc_vel, pc_acc], path) + instance = algo.TOPPRA([pc_vel, pc_acc], path, solver_wrapper='seidel') # Retime the trajectory, only this step is necessary. t0 = time.time() diff --git a/requirements3.txt b/requirements3.txt index 4cfce879..3a9ccb9c 100644 --- a/requirements3.txt +++ b/requirements3.txt @@ -5,8 +5,10 @@ scipy>0.18 coloredlogs enum34 matplotlib + # dev pylint pycodestyle pydocstyle invoke +cvxopt diff --git a/toppra/constraint/__init__.py b/toppra/constraint/__init__.py index 1843db70..106bf6f0 100644 --- a/toppra/constraint/__init__.py +++ b/toppra/constraint/__init__.py @@ -1,8 +1,8 @@ """This module defines different dynamic constraints.""" -from .constraint import ConstraintType, DiscretizationType +from .constraint import ConstraintType, DiscretizationType, Constraint from .joint_torque import JointTorqueConstraint from .linear_joint_acceleration import JointAccelerationConstraint from .linear_joint_velocity import JointVelocityConstraint, JointVelocityConstraintVarying from .linear_second_order import SecondOrderConstraint, canlinear_colloc_to_interpolate from .conic_constraint import RobustLinearConstraint -# from .linear_constraint import LinearConstraint +from .linear_constraint import LinearConstraint From 9d44a6fc2287569d9cbebd2266dae0142849b01e Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 21:23:37 +0800 Subject: [PATCH 06/42] modify setup.py --- Makefile | 6 ++++++ setup.py | 23 +++++++++++------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 2a255c7e..4aa895c4 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,12 @@ doc: coverage: python -m pytest -q --cov-report term --cov-report xml --cov=toppra tests +solvers: + git clone https://github.com/hungpham2511/qpOASES tmp-qpoases + cd tmp-qpoases/ && mkdir bin && make && cd interfaces/python/ && python setup.py install + rm -rf tmp-qpOASES + + # todos before publishing: # - increment version in setup.py diff --git a/setup.py b/setup.py index b94a1ce6..db4dd352 100644 --- a/setup.py +++ b/setup.py @@ -7,18 +7,22 @@ NAME = "toppra" with open("VERSION", "r") as file_: VERSION = file_.read() -DESCR = "An implementation of TOPP-RA (TOPP via Reachability Analysis) for time-parametrizing" \ - "trajectories for robots subject to kinematic (velocity and acceleration) and dynamic" \ - "(torque) constraints. Some other kinds of constraints are also supported." +DESCR = "toppra: time-optimal parametrization of trajectories for robots subject to constraints." +LONG_DESCRIPTION = "An implementation of TOPP-RA (TOPP via Reachability Analysis) for time-parametrizing" \ + "trajectories for robots subject to kinematic (velocity and acceleration) and dynamic" \ + "(torque) constraints. Some other kinds of constraints are also supported." + URL = "https://github.com/hungpham2511/toppra" # requirements if sys.version[0] == '2': with open("requirements.txt", "r") as f: - REQUIRES = [line.strip() for line in f if line.strip()] + REQUIRES = ["scipy==0.18.0", "numpy", "enum34"] + DEV_REQUIRES = [line.strip() for line in f if line.strip()] else: with open("requirements3.txt", "r") as f: - REQUIRES = [line.strip() for line in f if line.strip()] + REQUIRES = ["scipy>0.18", "numpy", "enum34"] + DEV_REQUIRES = [line.strip() for line in f if line.strip()] AUTHOR = "Hung Pham" EMAIL = "hungpham2511@gmail.com" @@ -48,19 +52,14 @@ setup(install_requires=REQUIRES, setup_requires=["numpy", "cython"], extras_require={ - 'dev': [ - 'pytest', - 'pytest-pep8', - 'pytest-cov', - 'tabulate', - 'cvxpy' - ] + 'dev': DEV_REQUIRES }, packages=PACKAGES, zip_safe=False, name=NAME, version=VERSION, description=DESCR, + long_description=LONG_DESCRIPTION, author=AUTHOR, author_email=EMAIL, url=URL, From 8c6da52fbc8a7703eb830d6892dec59e512085e4 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 21:49:50 +0800 Subject: [PATCH 07/42] some minor refactors --- Makefile | 5 ++--- tests/constraint/test_joint_acceleration.py | 18 +++++++++--------- toppra/__init__.py | 2 +- toppra/algorithm/algorithm.py | 8 ++++---- toppra/constraint/linear_joint_acceleration.py | 5 ++++- toppra/interpolator.py | 10 +++++----- 6 files changed, 25 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index 4aa895c4..124b5e46 100644 --- a/Makefile +++ b/Makefile @@ -11,9 +11,8 @@ coverage: python -m pytest -q --cov-report term --cov-report xml --cov=toppra tests solvers: - git clone https://github.com/hungpham2511/qpOASES tmp-qpoases - cd tmp-qpoases/ && mkdir bin && make && cd interfaces/python/ && python setup.py install - rm -rf tmp-qpOASES + git clone https://github.com/hungpham2511/qpOASES /tmp/tmp-qpoases + cd /tmp/tmp-qpoases/ && mkdir bin && make && cd interfaces/python/ && python setup.py install diff --git a/tests/constraint/test_joint_acceleration.py b/tests/constraint/test_joint_acceleration.py index c0621f52..c20cecec 100644 --- a/tests/constraint/test_joint_acceleration.py +++ b/tests/constraint/test_joint_acceleration.py @@ -15,7 +15,8 @@ def create_acceleration_pc_fixtures(request): data: A tuple. Contains path, ss, alim. pc: A `PathConstraint`. """ - if request.param == 1: # Scalar + dof = request.param + if dof == 1: # Scalar pi = ta.PolynomialPath([1, 2, 3]) # 1 + 2s + 3s^2 ss = np.linspace(0, 1, 3) alim = (np.r_[-1., 1]).reshape(1, 2) # Scalar case @@ -23,7 +24,7 @@ def create_acceleration_pc_fixtures(request): data = (pi, ss, alim) return data, pc_vel - if request.param == 2: + if dof == 2: coeff = [[1., 2, 3], [-2., -3., 4., 5.]] pi = ta.PolynomialPath(coeff) ss = np.linspace(0, 0.75, 4) @@ -32,7 +33,7 @@ def create_acceleration_pc_fixtures(request): data = (pi, ss, alim) return data, pc_vel - if request.param == 6: + if dof == 6: np.random.seed(10) N = 20 way_pts = np.random.randn(10, 6) @@ -55,11 +56,10 @@ def test_constraint_type(acceleration_pc_data): def test_constraint_params(acceleration_pc_data): """ Test constraint satisfaction with cvxpy. """ - data, constraint = acceleration_pc_data - path, ss, alim = data + (path, ss, alim), accel_const = acceleration_pc_data # An user of the class - a, b, c, F, g, ubound, xbound = constraint.compute_constraint_params(path, ss, 1.0) + a, b, c, F, g, ubound, xbound = accel_const.compute_constraint_params(path, ss) assert xbound is None N = ss.shape[0] - 1 @@ -82,12 +82,12 @@ def test_constraint_params(acceleration_pc_data): def test_wrong_dimension(acceleration_pc_data): - data, pc = acceleration_pc_data + _, path_constraint = acceleration_pc_data path_wrongdim = ta.SplineInterpolator(np.linspace(0, 1, 5), np.random.randn(5, 10)) with pytest.raises(ValueError) as e_info: - pc.compute_constraint_params(path_wrongdim, np.r_[0, 0.5, 1], 1.0) + path_constraint.compute_constraint_params(path_wrongdim, np.r_[0, 0.5, 1], 1.0) assert e_info.value.args[0] == "Wrong dimension: constraint dof ({:d}) not equal to path dof ({:d})".format( - pc.get_dof(), 10 + path_constraint.get_dof(), 10 ) diff --git a/toppra/__init__.py b/toppra/__init__.py index c7ab63ed..3b95eda4 100644 --- a/toppra/__init__.py +++ b/toppra/__init__.py @@ -8,7 +8,7 @@ """ from .interpolator import RaveTrajectoryWrapper, SplineInterpolator,\ - UnivariateSplineInterpolator, PolynomialPath, Interpolator + UnivariateSplineInterpolator, PolynomialPath from .utils import smooth_singularities, setup_logging from .planning_utils import retime_active_joints_kinematics,\ create_rave_torque_path_constraint diff --git a/toppra/algorithm/algorithm.py b/toppra/algorithm/algorithm.py index e41d1451..62acac55 100644 --- a/toppra/algorithm/algorithm.py +++ b/toppra/algorithm/algorithm.py @@ -3,7 +3,7 @@ import numpy as np from ..constants import TINY -from ..interpolator import SplineInterpolator, Interpolator +from toppra.interpolator import SplineInterpolator, AbstractGeometricPath import logging logger = logging.getLogger(__name__) @@ -26,7 +26,7 @@ class ParameterizationAlgorithm(object): Parameters ---------- constraint_list: list of `Constraint` - path: `Interpolator` + path: `AbstractGeometricPath` The geometric path, or the trajectory to parameterize. gridpoints: array, optional If not given, automatically generate a grid with 100 steps. @@ -89,10 +89,10 @@ def compute_trajectory(self, sd_start=0, sd_end=0, return_profile=False, bc_type Returns ------- - :class:`.Interpolator` + :class:`.AbstractGeometricPath` Time-parameterized joint position trajectory. If unable to parameterize, return None. - :class:`.Interpolator` + :class:`.AbstractGeometricPath` Time-parameterized auxiliary variable trajectory. If unable to parameterize or if there is no auxiliary variable, return None. diff --git a/toppra/constraint/linear_joint_acceleration.py b/toppra/constraint/linear_joint_acceleration.py index b3a2361b..6149c52c 100644 --- a/toppra/constraint/linear_joint_acceleration.py +++ b/toppra/constraint/linear_joint_acceleration.py @@ -52,11 +52,14 @@ def __init__(self, alim, discretization_scheme=DiscretizationType.Collocation): self._format_string += " J{:d}: {:}".format(i + 1, self.alim[i]) + "\n" self.identical = True - def compute_constraint_params(self, path, gridpoints, scaling): + def compute_constraint_params(self, path, gridpoints, scaling=None): + # type: (Path, np.ndarray, Optional[float]) -> Any if path.dof != self.dof: raise ValueError("Wrong dimension: constraint dof ({:d}) not equal to path dof ({:d})".format( self.dof, path.dof )) + if scaling is None: + scaling = 1 ps_vec = path.evald(gridpoints / scaling) / scaling pss_vec = path.evaldd(gridpoints / scaling) / scaling ** 2 dof = path.dof diff --git a/toppra/interpolator.py b/toppra/interpolator.py index 0577cb47..3e9f894f 100644 --- a/toppra/interpolator.py +++ b/toppra/interpolator.py @@ -47,7 +47,7 @@ def _find_left_index(gridpoints, s): return len(gridpoints) - 2 -class Interpolator(object): +class AbstractGeometricPath(object): """Base class for Interpolators. Derive Interpolator should inherit this abstract class. @@ -110,7 +110,7 @@ def to_ros_trajectory_msg(self): raise NotImplementedError -class RaveTrajectoryWrapper(Interpolator): +class RaveTrajectoryWrapper(AbstractGeometricPath): """An interpolator that wraps OpenRAVE's :class:`GenericTrajectory`. Only trajectories using quadratic interpolation or cubic @@ -255,7 +255,7 @@ def evaldd(self, ss_sam): return self.ppoly_dd(ss_sam) -class SplineInterpolator(Interpolator): +class SplineInterpolator(AbstractGeometricPath): """Interpolate the given waypoints by cubic spline. This interpolator is implemented as a simple wrapper over scipy's @@ -416,7 +416,7 @@ def compute_rave_trajectory(self, robot): return traj -class UnivariateSplineInterpolator(Interpolator): +class UnivariateSplineInterpolator(AbstractGeometricPath): """ Smooth given wayspoints by a cubic spline. This is a simple wrapper over `scipy.UnivariateSplineInterpolator` @@ -483,7 +483,7 @@ def evaldd(self, ss_sam): return np.array(data).T -class PolynomialPath(Interpolator): +class PolynomialPath(AbstractGeometricPath): """ A class representing polynominal paths. If coeff is a 1d array, the polynomial's equation is given by From 9c150268af582176b60d918787211f2ef27389e2 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 22:46:29 +0800 Subject: [PATCH 08/42] add invoke and tox config --- MANIFEST.in | 2 ++ Makefile | 5 ----- requirements.txt | 5 +++++ requirements3.txt | 9 +++++---- setup.py | 4 ++-- tasks.py | 18 ++++++++++++++++++ 6 files changed, 32 insertions(+), 11 deletions(-) create mode 100644 tasks.py diff --git a/MANIFEST.in b/MANIFEST.in index f8cce389..1fc1fc15 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,4 @@ recursive-include toppra *.pyx include requirements.txt +include requirements3.txt +include VERSION diff --git a/Makefile b/Makefile index 124b5e46..2a255c7e 100644 --- a/Makefile +++ b/Makefile @@ -10,11 +10,6 @@ doc: coverage: python -m pytest -q --cov-report term --cov-report xml --cov=toppra tests -solvers: - git clone https://github.com/hungpham2511/qpOASES /tmp/tmp-qpoases - cd /tmp/tmp-qpoases/ && mkdir bin && make && cd interfaces/python/ && python setup.py install - - # todos before publishing: # - increment version in setup.py diff --git a/requirements.txt b/requirements.txt index 593da84b..ad8c7ed4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,8 @@ scipy==0.18.0 coloredlogs enum34 matplotlib<3.0.0 + +cvxpy==0.4.11 +pandas +tabulate +cvxopt diff --git a/requirements3.txt b/requirements3.txt index 3a9ccb9c..0a795f9d 100644 --- a/requirements3.txt +++ b/requirements3.txt @@ -7,8 +7,9 @@ enum34 matplotlib # dev -pylint -pycodestyle -pydocstyle -invoke cvxopt +cvxpy +pandas +tabulate +cvxopt + diff --git a/setup.py b/setup.py index db4dd352..60107564 100644 --- a/setup.py +++ b/setup.py @@ -17,11 +17,11 @@ # requirements if sys.version[0] == '2': with open("requirements.txt", "r") as f: - REQUIRES = ["scipy==0.18.0", "numpy", "enum34"] + REQUIRES = ["scipy==0.18.0", "numpy", "enum34", "coloredlogs"] DEV_REQUIRES = [line.strip() for line in f if line.strip()] else: with open("requirements3.txt", "r") as f: - REQUIRES = ["scipy>0.18", "numpy", "enum34"] + REQUIRES = ["scipy>0.18", "numpy", "enum34", "coloredlogs"] DEV_REQUIRES = [line.strip() for line in f if line.strip()] AUTHOR = "Hung Pham" diff --git a/tasks.py b/tasks.py new file mode 100644 index 00000000..3bec93b4 --- /dev/null +++ b/tasks.py @@ -0,0 +1,18 @@ +from invoke import task +import os +try: + import pathlib2 as pathlib +except ImportError: + import pathlib + + +@task +def install_solvers(c): + install_dir = '/tmp/tox-qpoases' + path = pathlib.Path(install_dir) + if path.exists(): + print("Path already exist") + else: + c.run("git clone https://github.com/hungpham2511/qpOASES {}".format(install_dir)) + c.run("cd /tmp/tox-qpoases/ && mkdir bin && make") + c.run("cd /tmp/tox-qpoases/interfaces/python/ && python setup.py install") From c22323703178611e58517134779f11efd19e6ecf Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 23:36:26 +0800 Subject: [PATCH 09/42] add docker tasks --- dockerfiles/{dependency => }/Dockerfile | 0 dockerfiles/README.md | 0 dockerfiles/{dependency => }/build.sh | 0 tasks.py | 21 +++++++++++++++++++++ tox.ini | 17 +++++++++++++++++ 5 files changed, 38 insertions(+) rename dockerfiles/{dependency => }/Dockerfile (100%) delete mode 100644 dockerfiles/README.md rename dockerfiles/{dependency => }/build.sh (100%) create mode 100644 tox.ini diff --git a/dockerfiles/dependency/Dockerfile b/dockerfiles/Dockerfile similarity index 100% rename from dockerfiles/dependency/Dockerfile rename to dockerfiles/Dockerfile diff --git a/dockerfiles/README.md b/dockerfiles/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/dockerfiles/dependency/build.sh b/dockerfiles/build.sh similarity index 100% rename from dockerfiles/dependency/build.sh rename to dockerfiles/build.sh diff --git a/tasks.py b/tasks.py index 3bec93b4..cb5d806d 100644 --- a/tasks.py +++ b/tasks.py @@ -16,3 +16,24 @@ def install_solvers(c): c.run("git clone https://github.com/hungpham2511/qpOASES {}".format(install_dir)) c.run("cd /tmp/tox-qpoases/ && mkdir bin && make") c.run("cd /tmp/tox-qpoases/interfaces/python/ && python setup.py install") + + +@task +def docker_build(c): + c.run("docker build -f dockerfiles/Dockerfile . -t toppra-dev") + + +@task +def docker_start(c): + c.run("docker run --rm --name toppra-dep -d \ + -v /home/hung/git/toppra:$HOME/toppra \ + hungpham2511/toppra-dep:0.0.2 sleep infinity") + + +@task +def docker_exec(c): + c.run("docker exec -it toppra-dep bash", pty=True) + +@task +def docker_stop(c): + c.run("docker stop toppra-dep") diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..74721006 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +# content of: tox.ini , put in same dir as setup.py +[tox] +envlist = py27,py37 + +[testenv] +# install pytest in the virtualenv where commands will be executed +sitepackages=true +deps = + pytest + invoke + cython + numpy + +commands = + invoke install-solvers + pip install -e .[dev] + pytest -x \ No newline at end of file From 5ac443d6a55d832b801a83a09364dfe7da706849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hung=20Pham=20=28Ph=E1=BA=A1m=20Ti=E1=BA=BFn=20H=C3=B9ng?= =?UTF-8?q?=29?= Date: Sun, 8 Dec 2019 00:32:04 +0800 Subject: [PATCH 10/42] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e5b3cf62..cbdaddb5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ constraints can be found. ## Quick-start -To install toppra, simple clone the repo and install with pip +To install toppra, simply clone the repo and install with pip ``` shell git clone https://github.com/hungpham2511/toppra From 5f6b1886aa3427f851270afcbffb1c418888ec8f Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 00:05:07 +0800 Subject: [PATCH 11/42] cleanup circleci --- .circleci/config.yml | 17 ++++------------- tasks.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0be6da91..1e196e50 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -118,25 +118,16 @@ jobs: name: install dependencies command: | pip install virtualenv - python -m virtualenv venv - . venv/bin/activate - # main deps - pip install -r requirements.txt - git clone https://github.com/hungpham2511/qpOASES && cd qpOASES/ && mkdir bin && make && cd interfaces/python/ && python setup.py install - # test deps - pip install cvxpy==0.4.11 cvxopt pytest pytest-cov pandas tabulate pylint pydocstyle pycodestyle pathlib2 + python -m virtualenv venv && . venv/bin/activate + python -m pip install invoke pathlib2 numpy cython + python -m invoke install-solvers + pip install -e .[dev] - save_cache: paths: - ./venv key: v1-dependencies2-{{ checksum "requirements.txt" }} - - run: - name: build and install - command: | - . venv/bin/activate - python setup.py develop - - run: name: test command: | diff --git a/tasks.py b/tasks.py index cb5d806d..eee2cb83 100644 --- a/tasks.py +++ b/tasks.py @@ -7,7 +7,7 @@ @task -def install_solvers(c): +def install_solvers(c, user=False): install_dir = '/tmp/tox-qpoases' path = pathlib.Path(install_dir) if path.exists(): @@ -15,7 +15,33 @@ def install_solvers(c): else: c.run("git clone https://github.com/hungpham2511/qpOASES {}".format(install_dir)) c.run("cd /tmp/tox-qpoases/ && mkdir bin && make") - c.run("cd /tmp/tox-qpoases/interfaces/python/ && python setup.py install") + if user: + flag = "--user" + else: + flag = "" + c.run("cd /tmp/tox-qpoases/interfaces/python/ && python setup.py install {}".format(flag)) + + +@task +def test(c, python3=False): + """Convenient command to create different environments for testing.""" + if not python3: + venv_path = "/tmp/venv" + flag = "" + test_flag = "export PYTHONPATH=$PYTHONPATH:`openrave-config --python-dir` &&" + else: + venv_path = "/tmp/venv3" + flag = "--python python3" + test_flag = "" + + c.run("python -m virtualenv {flag} {venv_path} && \ + {venv_path}/bin/pip install invoke pathlib numpy cython pytest".format( + venv_path=venv_path, flag=flag + )) + c.run(". {venv_path}/bin/activate && \ + invoke install-solvers && \ + pip install -e .[dev]".format(venv_path=venv_path)) + c.run("{test_flag} {venv_path}/bin/pytest -x".format(test_flag=test_flag, venv_path=venv_path)) @task From 1a5f80bca557318d08a4fde8ddab15097eeba3d0 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 09:15:27 +0800 Subject: [PATCH 12/42] change openravepy load plugins flag --- .circleci/config.yml | 6 ++++- tasks.py | 12 +++++----- tests/conftest.py | 22 ++++++++++++++----- tests/interpolators/test_rave_trajectory.py | 7 +++--- .../interpolators/test_spline_interpolator.py | 2 +- tests/retime/test_retime_with_openrave.py | 6 ++--- toppra/utils.py | 1 + 7 files changed, 36 insertions(+), 20 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e196e50..760d50f5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -134,7 +134,11 @@ jobs: . venv/bin/activate export PYTHONPATH=$PYTHONPATH:`openrave-config --python-dir` mkdir test-reports - pytest -q tests --durations=3 --junitxml=test-reports/junit.xml + pytest --durations=3 --junitxml=test-reports/junit.xml + + - run: + command: | + cp /tmp/toppra.log test-reports - store_test_results: path: test-reports diff --git a/tasks.py b/tasks.py index eee2cb83..cff8b521 100644 --- a/tasks.py +++ b/tasks.py @@ -21,9 +21,8 @@ def install_solvers(c, user=False): flag = "" c.run("cd /tmp/tox-qpoases/interfaces/python/ && python setup.py install {}".format(flag)) - @task -def test(c, python3=False): +def make_venvs(c, python3=False, run_tests=False): """Convenient command to create different environments for testing.""" if not python3: venv_path = "/tmp/venv" @@ -41,7 +40,8 @@ def test(c, python3=False): c.run(". {venv_path}/bin/activate && \ invoke install-solvers && \ pip install -e .[dev]".format(venv_path=venv_path)) - c.run("{test_flag} {venv_path}/bin/pytest -x".format(test_flag=test_flag, venv_path=venv_path)) + if run_tests: + c.run("{test_flag} {venv_path}/bin/pytest -x".format(test_flag=test_flag, venv_path=venv_path)) @task @@ -52,8 +52,10 @@ def docker_build(c): @task def docker_start(c): c.run("docker run --rm --name toppra-dep -d \ - -v /home/hung/git/toppra:$HOME/toppra \ - hungpham2511/toppra-dep:0.0.2 sleep infinity") + -v /home/hung/git/toppra:$HOME/toppra \ + -e DISPLAY=unix$DISPLAY \ + --net=host \ + hungpham2511/toppra-dep:0.0.2 sleep infinity") @task diff --git a/tests/conftest.py b/tests/conftest.py index 97f4c1c5..d1d3c419 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,4 @@ +import logging import toppra import pytest try: @@ -8,14 +9,23 @@ except SyntaxError as err: IMPORT_OPENRAVE = False +logger = logging.getLogger('toppra.bug') -@pytest.fixture(scope="session") +@pytest.fixture(autouse=True, scope="session") def rave_env(): - orpy.RaveInitialize(load_all_plugins=True) - env = orpy.Environment() - yield env - env.Destroy() - orpy.RaveDestroy() + if IMPORT_OPENRAVE: + logger.warn("Starting openrave") + orpy.RaveInitialize(load_all_plugins=False) + orpy.RaveInitialize(load_all_plugins=True) + logger.warn("Starting a new environment") + env = orpy.Environment() + yield env + logger.warn("Destroying a new environment") + env.Destroy() + logger.warn("Destroying Rave runtime") + orpy.RaveDestroy() + else: + yield None def pytest_addoption(parser): diff --git a/tests/interpolators/test_rave_trajectory.py b/tests/interpolators/test_rave_trajectory.py index 2391efd8..05f41a7b 100644 --- a/tests/interpolators/test_rave_trajectory.py +++ b/tests/interpolators/test_rave_trajectory.py @@ -8,13 +8,12 @@ @pytest.fixture(scope='module') -def env(): +def env(rave_env): """Simple openrave environment.""" - rave_env = orpy.Environment() + rave_env.Reset() rave_env.Load('data/lab1.env.xml') rave_env.GetRobots()[0].SetActiveDOFs(range(7)) yield rave_env - rave_env.Destroy() # data for testing @@ -36,7 +35,7 @@ def env(): pytest.param(string_quad_2wp, id="quadratic 2wp"), pytest.param(string_quad_4wp, id="quadratic 4wp"), pytest.param(string_quad_24wp, id="quadratic 24wp"), - pytest.param(string_quad_dup, id="quadratic duplicate", marks=[pytest.mark.dup]), + pytest.param(string_quad_dup, id="quadratic duplicate", marks=[]), ]) def test_consistency(request, env, traj_string): "Check the consistency between the OpenRAVE trajectory and the interpolator." diff --git a/tests/interpolators/test_spline_interpolator.py b/tests/interpolators/test_spline_interpolator.py index 157a48a5..86aeb833 100644 --- a/tests/interpolators/test_spline_interpolator.py +++ b/tests/interpolators/test_spline_interpolator.py @@ -77,7 +77,7 @@ def robot_fixture(rave_env): ikmodel.autogenerate() print('IKFast {0} has been successfully generated'.format(iktype.name)) yield robot - rave_env.Destroy() + @pytest.mark.skipif(not IMPORT_OPENRAVEPY, reason=IMPORT_OPENRAVEPY_MSG) @pytest.mark.parametrize("ss_waypoints, waypoints", [ diff --git a/tests/retime/test_retime_with_openrave.py b/tests/retime/test_retime_with_openrave.py index b0220e12..d3695e3e 100644 --- a/tests/retime/test_retime_with_openrave.py +++ b/tests/retime/test_retime_with_openrave.py @@ -8,8 +8,9 @@ @pytest.fixture(scope='module') -def robot_fixture(): - env = orpy.Environment() +def robot_fixture(rave_env): + env = rave_env + env.Reset() env.Load("data/lab1.env.xml") robot = env.GetRobots()[0] manipulator = robot.GetManipulators()[0] @@ -24,7 +25,6 @@ def robot_fixture(): # env.SetViewer('qtosg') toppra.setup_logging("INFO") yield robot - env.Destroy() @pytest.mark.skipif(not IMPORT_OPENRAVEPY, reason="Not found openrave installation") diff --git a/toppra/utils.py b/toppra/utils.py index b6b9eeeb..592c40a4 100644 --- a/toppra/utils.py +++ b/toppra/utils.py @@ -35,6 +35,7 @@ def setup_logging(level="WARN"): fmt="%(levelname)5s [%(filename)s : %(lineno)d] [%(funcName)s] %(message)s", datefmt="%H:%M:%S", milliseconds=True) + logging.basicConfig(filename='/tmp/toppra.log', level=level, filemode='a') def compute_jacobian_wrench(robot, link, point): From ea2e98fae745d8df9035b96356e3ae4779ba2bc4 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 11:26:46 +0800 Subject: [PATCH 13/42] config 0.0.3 docker image --- .circleci/config.yml | 4 ++-- dockerfiles/Dockerfile | 6 ++++-- dockerfiles/build.sh | 2 +- tests/conftest.py | 1 - 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 760d50f5..e89a8479 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -100,7 +100,7 @@ jobs: test-python-2: docker: - - image: hungpham2511/toppra-dep:0.0.2 + - image: hungpham2511/toppra-dep:0.0.3 working_directory: ~/repo @@ -138,7 +138,7 @@ jobs: - run: command: | - cp /tmp/toppra.log test-reports + cp /tmp/toppra.log ~/repo/test-reports/ - store_test_results: path: test-reports diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index 54bd960a..44ba009e 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:18.04 +FROM ubuntu:xenial-20191108 MAINTAINER Hung Pham ENV DEBIAN_FRONTEND=noninteractive @@ -43,7 +43,9 @@ RUN cd /usr/src \ && rm -rf /usr/src/openrave-${RAVE_COMMIT}/build # Other deps -RUN sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends python-dev python3-dev python3-venv python-tk +RUN apt-get install -y --no-install-recommends python-dev python3-dev python3-venv python-tk && \ + pip install virtualenv invoke + # RUN sudo apt-get install -y --no-install-recommends python-dev python3-dev python3-venv # User diff --git a/dockerfiles/build.sh b/dockerfiles/build.sh index df8d62fb..73d4ed3b 100755 --- a/dockerfiles/build.sh +++ b/dockerfiles/build.sh @@ -1,7 +1,7 @@ #!/bin/bash USER=hungpham2511 IMAGE=toppra-dep -VERSION=0.0.2 +VERSION=0.0.3 echo "Building docker image: $USER/$IMAGE:$VERSION" docker build -t ${IMAGE} . diff --git a/tests/conftest.py b/tests/conftest.py index d1d3c419..925ca509 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,7 +15,6 @@ def rave_env(): if IMPORT_OPENRAVE: logger.warn("Starting openrave") - orpy.RaveInitialize(load_all_plugins=False) orpy.RaveInitialize(load_all_plugins=True) logger.warn("Starting a new environment") env = orpy.Environment() From e419623bd61a85ec69c1b3c79440a38bb31a39b2 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 11:37:29 +0800 Subject: [PATCH 14/42] modify Ci config --- .circleci/config.yml | 55 +++++++++++--------------------------------- requirements3.txt | 3 ++- 2 files changed, 15 insertions(+), 43 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e89a8479..45f9bf82 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,43 +2,28 @@ version: 2 jobs: check-codestyle: docker: - - image: hungpham2511/toppra-dep:0.0.2 + - image: hungpham2511/toppra-dep:0.0.3 working_directory: ~/repo steps: - checkout - - # Download and cache dependencies - restore_cache: keys: - v1-dependencies3-{{ checksum "requirements3.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies3- - - run: name: install dependencies command: | - python3 -m venv venv3 - . venv3/bin/activate - # main deps - pip install -r requirements3.txt - git clone https://github.com/hungpham2511/qpOASES && cd qpOASES/ && mkdir bin && make && cd interfaces/python/ && python setup.py install - # test deps - pip install cvxpy cvxopt pytest pytest-cov pandas tabulate pylint pydocstyle pycodestyle + python3 -m virtualenv --python python3 venv3 && . venv3/bin/activate + pip install invoke pathlib2 numpy cython + invoke install-solvers + pip install -e .[dev] - save_cache: paths: - ./venv3 key: v1-dependencies3-{{ checksum "requirements3.txt" }} - # build install - - run: - name: build and install - command: | - . venv3/bin/activate - python setup.py develop - - run: name: check codestyle command: | @@ -51,7 +36,7 @@ jobs: test-python-3: docker: - - image: hungpham2511/toppra-dep:0.0.2 + - image: hungpham2511/toppra-dep:0.0.3 working_directory: ~/repo @@ -62,32 +47,20 @@ jobs: - restore_cache: keys: - v1-dependencies3-{{ checksum "requirements3.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies3- - run: name: install dependencies command: | - python3 -m venv venv3 - . venv3/bin/activate - # main deps - pip install -r requirements3.txt - git clone https://github.com/hungpham2511/qpOASES && cd qpOASES/ && mkdir bin && make && cd interfaces/python/ && python setup.py install - # test deps - pip install cvxpy cvxopt pytest pytest-cov pandas tabulate pylint pydocstyle pycodestyle + python3 -m virtualenv --python python3 venv3 && . venv3/bin/activate + pip install invoke pathlib2 numpy cython + invoke install-solvers + pip install -e .[dev] - save_cache: paths: - ./venv3 key: v1-dependencies3-{{ checksum "requirements3.txt" }} - # build install - - run: - name: build and install - command: | - . venv3/bin/activate - python setup.py develop - - run: name: test command: | @@ -106,18 +79,14 @@ jobs: steps: - checkout - - # Download and cache dependencies - restore_cache: keys: - v1-dependencies2-{{ checksum "requirements.txt" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies2- - run: name: install dependencies command: | - pip install virtualenv + python -m pip install virtualenv --user python -m virtualenv venv && . venv/bin/activate python -m pip install invoke pathlib2 numpy cython python -m invoke install-solvers @@ -137,11 +106,13 @@ jobs: pytest --durations=3 --junitxml=test-reports/junit.xml - run: + name: Collect log data command: | cp /tmp/toppra.log ~/repo/test-reports/ - store_test_results: path: test-reports + destination: test-reports workflows: version: 2 diff --git a/requirements3.txt b/requirements3.txt index 0a795f9d..9265c8d0 100644 --- a/requirements3.txt +++ b/requirements3.txt @@ -3,8 +3,8 @@ cython>=0.22.0 PyYAML scipy>0.18 coloredlogs -enum34 matplotlib +enum34 # dev cvxopt @@ -12,4 +12,5 @@ cvxpy pandas tabulate cvxopt +pytest From 47622eca494354ba21cb81b1ff51b77da7a49f8e Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 19:35:04 +0800 Subject: [PATCH 15/42] install python3.7,3.6 in dockerfile --- dockerfiles/Dockerfile | 11 +++++++++-- tasks.py | 2 +- tox.ini | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/dockerfiles/Dockerfile b/dockerfiles/Dockerfile index 44ba009e..898d739b 100644 --- a/dockerfiles/Dockerfile +++ b/dockerfiles/Dockerfile @@ -42,9 +42,16 @@ RUN cd /usr/src \ && make -j `nproc` && make install \ && rm -rf /usr/src/openrave-${RAVE_COMMIT}/build +RUN apt-get install -y curl && curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py" \ + && python get-pip.py && python3 get-pip.py + # Other deps -RUN apt-get install -y --no-install-recommends python-dev python3-dev python3-venv python-tk && \ - pip install virtualenv invoke + +RUN apt-get install -y software-properties-common && add-apt-repository -y ppa:deadsnakes/ppa && \ + apt-get update && \ + apt-get install -y --no-install-recommends python-dev python3-dev python3-venv python-tk \ + python3.6 python3.6-dev python3.7 python3.7-dev && \ + python3 -m pip install virtualenv invoke tox # RUN sudo apt-get install -y --no-install-recommends python-dev python3-dev python3-venv diff --git a/tasks.py b/tasks.py index cff8b521..47877dec 100644 --- a/tasks.py +++ b/tasks.py @@ -55,7 +55,7 @@ def docker_start(c): -v /home/hung/git/toppra:$HOME/toppra \ -e DISPLAY=unix$DISPLAY \ --net=host \ - hungpham2511/toppra-dep:0.0.2 sleep infinity") + hungpham2511/toppra-dep:0.0.3 sleep infinity") @task diff --git a/tox.ini b/tox.ini index 74721006..f32ca75c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ # content of: tox.ini , put in same dir as setup.py [tox] -envlist = py27,py37 +envlist = py27,py37,py35,py36 [testenv] # install pytest in the virtualenv where commands will be executed From 902d4ede1497cb7a4757cc47566fa513a683ddc4 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 21:49:43 +0800 Subject: [PATCH 16/42] add release step --- .circleci/config.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 45f9bf82..87c9f80e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,6 +114,19 @@ jobs: path: test-reports destination: test-reports + release: + docker: + - image: buildpack-deps:trusty + working_directory: ~/repo + steps: + - checkout + - run: + name: Create tag + command: | + git tag tag-abc + git push origin tag-abc + + workflows: version: 2 test-and-lint: @@ -121,3 +134,12 @@ workflows: - test-python-2 - test-python-3 - check-codestyle + release: + jobs: + - release: + filters: + branches: + only: + - fix-ci + + From 95e482d4f7b277c38bcad381181e6e7849c07c65 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 21:57:59 +0800 Subject: [PATCH 17/42] add temp release tag --- .circleci/config.yml | 12 ++++++------ VERSION | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 87c9f80e..5aba6b03 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,18 +114,18 @@ jobs: path: test-reports destination: test-reports - release: + pre-release: docker: - image: buildpack-deps:trusty working_directory: ~/repo steps: - checkout - run: - name: Create tag + name: Pull all known tags and check that the tag in VERSION has not already exist. command: | - git tag tag-abc - git push origin tag-abc - + VERSION=`cat VERSION` + git fetch --tags + git tag $VERSION workflows: version: 2 @@ -136,7 +136,7 @@ workflows: - check-codestyle release: jobs: - - release: + - pre-release: filters: branches: only: diff --git a/VERSION b/VERSION index b6a83a76..aa40e09f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2a \ No newline at end of file +0.2.2a-test \ No newline at end of file From d06bfe2bcfe14c319abef56de64056a8669099a5 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 22:03:52 +0800 Subject: [PATCH 18/42] fix VERSION --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index aa40e09f..434a3874 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2a-test \ No newline at end of file +0.2.2b \ No newline at end of file From e4f95af267911df6917139d985add0878a1b498b Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 22:20:58 +0800 Subject: [PATCH 19/42] add release job --- .circleci/config.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5aba6b03..a0e1987c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,6 +127,24 @@ jobs: git fetch --tags git tag $VERSION + release: + docker: + - image: buildpack-deps:trusty + working_directory: ~/repo + steps: + - checkout + - run: + name: Pull all known tags and check that the tag in VERSION has not already exist. + command: | + VERSION=`cat VERSION` + git fetch --tags + git tag $VERSION + git push origin $VERSION + - run: + name: Publish to PyPI + command: | + echo "TODO" + workflows: version: 2 test-and-lint: @@ -141,5 +159,12 @@ workflows: branches: only: - fix-ci + - release-* + - release: + filters: + branches: + only: + - master + From 2aaa8c359e2dce18c41c38d3b8bdda14e4f99629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hung=20Pham=20=28Ph=E1=BA=A1m=20Ti=E1=BA=BFn=20H=C3=B9ng?= =?UTF-8?q?=29?= Date: Mon, 9 Dec 2019 09:56:31 +0800 Subject: [PATCH 20/42] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbdaddb5..cd18457a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # TOPP-RA -[![CircleCI](https://circleci.com/gh/hungpham2511/toppra.svg?style=svg)](https://circleci.com/gh/hungpham2511/toppra) +[![CircleCI](https://circleci.com/gh/hungpham2511/toppra/tree/develop.svg?style=svg)](https://circleci.com/gh/hungpham2511/toppra/tree/develop) [![Coverage Status](https://coveralls.io/repos/github/hungpham2511/toppra/badge.svg?branch=master)](https://coveralls.io/github/hungpham2511/toppra?branch=master) [![Documentation Status](https://readthedocs.org/projects/toppra/badge/?version=latest)](https://toppra.readthedocs.io/en/latest/?badge=latest) From 6b62e390143027fb5d67dc046eb526fe9d8b3140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hung=20Pham=20=28Ph=E1=BA=A1m=20Ti=E1=BA=BFn=20H=C3=B9ng?= =?UTF-8?q?=29?= Date: Mon, 9 Dec 2019 09:59:20 +0800 Subject: [PATCH 21/42] Update README.md --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index cd18457a..df3695fa 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,23 @@ -# TOPP-RA +# `toppra` [![CircleCI](https://circleci.com/gh/hungpham2511/toppra/tree/develop.svg?style=svg)](https://circleci.com/gh/hungpham2511/toppra/tree/develop) [![Coverage Status](https://coveralls.io/repos/github/hungpham2511/toppra/badge.svg?branch=master)](https://coveralls.io/github/hungpham2511/toppra?branch=master) [![Documentation Status](https://readthedocs.org/projects/toppra/badge/?version=latest)](https://toppra.readthedocs.io/en/latest/?badge=latest) -**Documentation and tutorials** are available at (https://toppra.readthedocs.io/en/latest/index.html). - -TOPP-RA is a library for computing the time-optimal path parametrization for robots subject to kinematic and dynamic constraints. +**toppra** is a library for computing the time-optimal path parametrization for robots subject to kinematic and dynamic constraints. In general, given the inputs: 1. a geometric path `p(s)`, `s` in `[0, s_end]` ; 2. a list of constraints on joint velocity, joint accelerations, tool Cartesian velocity, et cetera. -TOPP-RA returns the time-optimal path parameterization: `s_dot (s)`, from which the fastest trajectory `q(t)` that satisfies the given +**toppra** returns the time-optimal path parameterization: `s_dot (s)`, from which the fastest trajectory `q(t)` that satisfies the given constraints can be found. +**Documentation and tutorials** are available at (https://toppra.readthedocs.io/en/latest/index.html). + + ## Quick-start -To install toppra, simply clone the repo and install with pip +To install **toppra**, simply clone the repo and install with pip ``` shell git clone https://github.com/hungpham2511/toppra @@ -28,7 +29,7 @@ To install depencidencies for development, replace the second command with: cd toppra && pip install -e .[dev] ``` -## Citing TOPP-RA +## Citing **toppra** If you use this library for your research, we encourage you to 1. reference the accompanying paper [«A new approach to Time-Optimal Path Parameterization based on Reachability Analysis»](https://www.researchgate.net/publication/318671280_A_New_Approach_to_Time-Optimal_Path_Parameterization_Based_on_Reachability_Analysis), *IEEE Transactions on Robotics*, vol. 34(3), pp. 645–659, 2018. @@ -45,4 +46,4 @@ with the issue report. ## Contributions -Pull Requests are welcomed! Go ahead and create a Pull Request and we will review your proposal! For new features, or bug fixes, preferably the request should contain unit tests. Note that `toppra` uses [pytest](https://docs.pytest.org/en/latest/contents.html) for all tests. Check out the test folder for more details. +Pull Requests are welcomed! Go ahead and create a Pull Request and we will review your proposal! For new features, or bug fixes, preferably the request should contain unit tests. Note that **toppra** uses [pytest](https://docs.pytest.org/en/latest/contents.html) for all tests. Check out the test folder for more details. From 795ce25eae588059d9c2c758ee217be0381cd252 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 21:49:43 +0800 Subject: [PATCH 22/42] add release step --- .circleci/config.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 45f9bf82..87c9f80e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,6 +114,19 @@ jobs: path: test-reports destination: test-reports + release: + docker: + - image: buildpack-deps:trusty + working_directory: ~/repo + steps: + - checkout + - run: + name: Create tag + command: | + git tag tag-abc + git push origin tag-abc + + workflows: version: 2 test-and-lint: @@ -121,3 +134,12 @@ workflows: - test-python-2 - test-python-3 - check-codestyle + release: + jobs: + - release: + filters: + branches: + only: + - fix-ci + + From c41482654d0f8b2fda14387ff77b59510273e2fe Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 21:57:59 +0800 Subject: [PATCH 23/42] add temp release tag --- .circleci/config.yml | 12 ++++++------ VERSION | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 87c9f80e..5aba6b03 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -114,18 +114,18 @@ jobs: path: test-reports destination: test-reports - release: + pre-release: docker: - image: buildpack-deps:trusty working_directory: ~/repo steps: - checkout - run: - name: Create tag + name: Pull all known tags and check that the tag in VERSION has not already exist. command: | - git tag tag-abc - git push origin tag-abc - + VERSION=`cat VERSION` + git fetch --tags + git tag $VERSION workflows: version: 2 @@ -136,7 +136,7 @@ workflows: - check-codestyle release: jobs: - - release: + - pre-release: filters: branches: only: diff --git a/VERSION b/VERSION index b6a83a76..aa40e09f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2a \ No newline at end of file +0.2.2a-test \ No newline at end of file From baa255fdbb9c7444a0c1be0250b49ff6a4d70537 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 22:03:52 +0800 Subject: [PATCH 24/42] fix VERSION --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index aa40e09f..434a3874 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2a-test \ No newline at end of file +0.2.2b \ No newline at end of file From 3b3485ee3b4000f67509c63d39b0499d71c92f01 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 8 Dec 2019 22:20:58 +0800 Subject: [PATCH 25/42] add release job --- .circleci/config.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 5aba6b03..a0e1987c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,6 +127,24 @@ jobs: git fetch --tags git tag $VERSION + release: + docker: + - image: buildpack-deps:trusty + working_directory: ~/repo + steps: + - checkout + - run: + name: Pull all known tags and check that the tag in VERSION has not already exist. + command: | + VERSION=`cat VERSION` + git fetch --tags + git tag $VERSION + git push origin $VERSION + - run: + name: Publish to PyPI + command: | + echo "TODO" + workflows: version: 2 test-and-lint: @@ -141,5 +159,12 @@ workflows: branches: only: - fix-ci + - release-* + - release: + filters: + branches: + only: + - master + From 54ecc2f8d657fca360b5ec7c7a1bcf6232ce062a Mon Sep 17 00:00:00 2001 From: Hung Date: Tue, 10 Dec 2019 22:39:17 +0800 Subject: [PATCH 26/42] refactor openrave example --- README.md | 2 +- examples/cartesian_accel.py | 1 - examples/retime_openrave.py | 75 +++++++++++++++++++------------------ 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index cbdaddb5..cd18457a 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # TOPP-RA -[![CircleCI](https://circleci.com/gh/hungpham2511/toppra.svg?style=svg)](https://circleci.com/gh/hungpham2511/toppra) +[![CircleCI](https://circleci.com/gh/hungpham2511/toppra/tree/develop.svg?style=svg)](https://circleci.com/gh/hungpham2511/toppra/tree/develop) [![Coverage Status](https://coveralls.io/repos/github/hungpham2511/toppra/badge.svg?branch=master)](https://coveralls.io/github/hungpham2511/toppra?branch=master) [![Documentation Status](https://readthedocs.org/projects/toppra/badge/?version=latest)](https://toppra.readthedocs.io/en/latest/?badge=latest) diff --git a/examples/cartesian_accel.py b/examples/cartesian_accel.py index fc87cb0b..ebf5cf2f 100644 --- a/examples/cartesian_accel.py +++ b/examples/cartesian_accel.py @@ -8,7 +8,6 @@ ta.setup_logging("INFO") - def main(): # openrave setup env = orpy.Environment() diff --git a/examples/retime_openrave.py b/examples/retime_openrave.py index 67e01c76..1f68e508 100644 --- a/examples/retime_openrave.py +++ b/examples/retime_openrave.py @@ -21,24 +21,15 @@ def plot_trajectories(traj1, traj2, robot, N=50): else: style = "-" alpha = 1.0 - for i in range(traj_ra.get_dof()): - axs[0].plot(ts, qs[:, i], style, c="C"+str(i), alpha=alpha) - axs[1].plot(ts, qds[:, i], style, c="C"+str(i), alpha=alpha) - axs[2].plot(ts, qdds[:, i], style, c="C"+str(i), alpha=alpha) - - vel_max = robot.GetActiveDOFMaxVel() - accel_max = robot.GetActiveDOFMaxAccel() - style = "--" - for i in range(traj_ra.get_dof()): - axs[1].plot([0, ts[-1]], [vel_max[i], vel_max[i]], style, c="C" + str(i)) - axs[1].plot([0, ts[-1]], [-vel_max[i], -vel_max[i]], style, c="C" + str(i)) - axs[2].plot([0, ts[-1]], [accel_max[i], accel_max[i]], style, c="C" + str(i)) - axs[2].plot([0, ts[-1]], [-accel_max[i], -accel_max[i]], style, c="C" + str(i)) + for i in range(traj_ra.dof): + axs[0].plot(range(N), qs[:, i], style, c="C"+str(i), alpha=alpha) + axs[1].plot(range(N), qds[:, i], style, c="C"+str(i), alpha=alpha) + axs[2].plot(range(N), qdds[:, i], style, c="C"+str(i), alpha=alpha) + plt.show() -def main(env=None, test=False): - "Main function." +def parse_cli_arguments(): parser = argparse.ArgumentParser(description="A simple example in which trajectories, which are planned using" "OpenRAVE is retimed using toppra. The trajectories are found using" "birrt, the default planner. Goals are generated randomly and " @@ -47,41 +38,54 @@ def main(env=None, test=False): parser.add_argument('-v', '--verbose', help='Show DEBUG log and plot trajectories', action="store_true") parser.add_argument('-N', '--Ngrid', help='Number of discretization step', default=100, type=int) args = vars(parser.parse_args()) + return args - if args['verbose']: - toppra.setup_logging('DEBUG') - else: - toppra.setup_logging('INFO') - if env is None: - env = orpy.Environment() +def load_rave_robot(args): + env = orpy.Environment() env.SetDebugLevel(0) env.Load(args['env']) env.SetViewer('qtosg') robot = env.GetRobots()[0] robot.SetDOFAccelerationLimits(np.ones(11) * 3) - manipulator = robot.GetManipulators()[0] - robot.SetActiveManipulator(manipulator) - robot.SetActiveDOFs(manipulator.GetArmIndices()) - controller = robot.GetController() - basemanip = orpy.interfaces.BaseManipulation(robot) - dof = robot.GetActiveDOF() + robot.SetActiveManipulator(robot.GetManipulators()[0]) + robot.SetActiveDOFs(robot.GetManipulators()[0].GetArmIndices()) + return robot + + +def generate_random_configuration(robot): + lower, upper = robot.GetActiveDOFLimits() + with robot: + while True: + qrand = np.random.rand(robot.GetActiveDOF()) * (upper - lower) + lower + robot.SetActiveDOFValues(qrand) + if robot.GetEnv().CheckCollision(robot) or robot.CheckSelfCollision(): + continue + return qrand +def check_collision(robot, path): + with robot.GetEnv(): + for q in np.linspace(0, path.duration, 100): + robot.SetActiveDOFValues(path(q)) + if robot.GetEnv().CheckCollision(robot) or robot.CheckSelfCollision(): + print("Path in collision") + +def main(env=None, test=False): + "Main function." + args = parse_cli_arguments() + toppra.setup_logging('INFO') + robot = load_rave_robot(args) + basemanip = orpy.interfaces.BaseManipulation(robot) pc_torque = toppra.create_rave_torque_path_constraint( robot, discretization_scheme=toppra.constraint.DiscretizationType.Interpolation) it = 0 while True or (test and it > 5): - lower, upper = robot.GetActiveDOFLimits() - qrand = np.random.rand(dof) * (upper - lower) + lower - with robot: - robot.SetActiveDOFValues(qrand) - incollision = env.CheckCollision(robot) or robot.CheckSelfCollision() - if incollision: - continue + qrand = generate_random_configuration(robot) traj_original = basemanip.MoveActiveJoints(qrand, execute=False, outputtrajobj=True) if traj_original is None: continue + traj_retimed, trajra = toppra.retime_active_joints_kinematics( traj_original, robot, output_interpolator=True, N=args['Ngrid'], additional_constraints=[pc_torque], solver_wrapper='seidel') @@ -89,11 +93,10 @@ def main(env=None, test=False): print("Original duration: {:.3f}. Retimed duration: {:3f}.".format( traj_original.GetDuration(), traj_retimed.GetDuration())) + robot.GetController().SetPath(traj_retimed) if args['verbose']: plot_trajectories(traj_original, traj_retimed, robot) - time.sleep(1) - controller.SetPath(traj_retimed) robot.WaitForController(0) it += 1 From 6bd48716a49c21dae447104454f114b60777433d Mon Sep 17 00:00:00 2001 From: Hung Date: Tue, 10 Dec 2019 23:06:06 +0800 Subject: [PATCH 27/42] remove usage to deprecated eval function --- requirements.txt | 1 + tasks.py | 20 +++- .../interpolators/test_spline_interpolator.py | 32 +++--- toppra/__init__.py | 3 +- toppra/constants.py | 10 ++ toppra/interpolator.py | 108 ++++++------------ 6 files changed, 84 insertions(+), 90 deletions(-) diff --git a/requirements.txt b/requirements.txt index ad8c7ed4..f53ad08e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ cvxpy==0.4.11 pandas tabulate cvxopt +pytest diff --git a/tasks.py b/tasks.py index 47877dec..dbcca5a3 100644 --- a/tasks.py +++ b/tasks.py @@ -1,5 +1,5 @@ +"""Collection of different operational tasts.""" from invoke import task -import os try: import pathlib2 as pathlib except ImportError: @@ -8,6 +8,7 @@ @task def install_solvers(c, user=False): + """Install backend solvers, e.g, qpoases.""" install_dir = '/tmp/tox-qpoases' path = pathlib.Path(install_dir) if path.exists(): @@ -21,6 +22,7 @@ def install_solvers(c, user=False): flag = "" c.run("cd /tmp/tox-qpoases/interfaces/python/ && python setup.py install {}".format(flag)) + @task def make_venvs(c, python3=False, run_tests=False): """Convenient command to create different environments for testing.""" @@ -44,13 +46,26 @@ def make_venvs(c, python3=False, run_tests=False): c.run("{test_flag} {venv_path}/bin/pytest -x".format(test_flag=test_flag, venv_path=venv_path)) +@task +def lint(c): + """Run linting on selected source files.""" + c.run("python -m pylint --rcfile=.pylintrc \ + tasks.py \ + toppra/__init__.py \ + toppra/utils.py \ + toppra/interpolator.py \ + ") + + @task def docker_build(c): + """Build docker image to run toppra development.""" c.run("docker build -f dockerfiles/Dockerfile . -t toppra-dev") @task def docker_start(c): + """Start the development docker container.""" c.run("docker run --rm --name toppra-dep -d \ -v /home/hung/git/toppra:$HOME/toppra \ -e DISPLAY=unix$DISPLAY \ @@ -60,8 +75,11 @@ def docker_start(c): @task def docker_exec(c): + """Execute and link to a bash shell inside the stared docker container.""" c.run("docker exec -it toppra-dep bash", pty=True) + @task def docker_stop(c): + """Start the development docker container.""" c.run("docker stop toppra-dep") diff --git a/tests/interpolators/test_spline_interpolator.py b/tests/interpolators/test_spline_interpolator.py index 86aeb833..27cf2a19 100644 --- a/tests/interpolators/test_spline_interpolator.py +++ b/tests/interpolators/test_spline_interpolator.py @@ -15,10 +15,10 @@ def test_scalar(sswp, wp, ss, path_interval): pi = SplineInterpolator(sswp, wp) # 1 + 2s + 3s^2 assert pi.dof == 1 - assert pi.eval(ss).shape == (len(ss), ) - assert pi.evald(ss).shape == (len(ss), ) - assert pi.evaldd(ss).shape == (len(ss), ) - assert pi.eval(0).shape == () + assert pi(ss).shape == (len(ss), ) + assert pi(ss, 1).shape == (len(ss), ) + assert pi(ss, 2).shape == (len(ss), ) + assert pi(0).shape == () npt.assert_allclose(pi.path_interval, path_interval) @@ -29,9 +29,9 @@ def test_5_dof(): ss = np.linspace(0, 1, 10) assert pi.dof == 5 - assert pi.eval(ss).shape == (10, 5) - assert pi.evald(ss).shape == (10, 5) - assert pi.evaldd(ss).shape == (10, 5) + assert pi(ss).shape == (10, 5) + assert pi(ss, 1).shape == (10, 5) + assert pi(ss, 2).shape == (10, 5) npt.assert_allclose(pi.path_interval, np.r_[0, 1]) @@ -40,11 +40,11 @@ def test_1waypoints(): pi = SplineInterpolator([0], [[1, 2, 3]]) assert pi.dof == 3 npt.assert_allclose(pi.path_interval, np.r_[0, 0]) - npt.assert_allclose(pi.eval(0), np.r_[1, 2, 3]) - npt.assert_allclose(pi.evald(0), np.r_[0, 0, 0]) + npt.assert_allclose(pi(0), np.r_[1, 2, 3]) + npt.assert_allclose(pi(0, 1), np.r_[0, 0, 0]) - npt.assert_allclose(pi.eval([0, 0]), [[1, 2, 3], [1, 2, 3]]) - npt.assert_allclose(pi.evald([0, 0]), [[0, 0, 0], [0, 0, 0]]) + npt.assert_allclose(pi([0, 0]), [[1, 2, 3], [1, 2, 3]]) + npt.assert_allclose(pi([0, 0], 1), [[0, 0, 0], [0, 0, 0]]) @pytest.mark.parametrize("xs,ys, yd", [ @@ -55,8 +55,8 @@ def test_2waypoints(xs, ys, yd): "There is only two waypoints. Linear interpolation is done between them." pi = SplineInterpolator(xs, ys, bc_type='natural') npt.assert_allclose(pi.path_interval, xs) - npt.assert_allclose(pi.evald((xs[0] + xs[1]) / 2), yd) - npt.assert_allclose(pi.evaldd(0), np.zeros_like(ys[0])) + npt.assert_allclose(pi((xs[0] + xs[1]) / 2, 1), yd) + npt.assert_allclose(pi(0, 2), np.zeros_like(ys[0])) @pytest.fixture(scope='module') @@ -99,9 +99,9 @@ def test_compute_rave_trajectory(robot_fixture, ss_waypoints, waypoints): xs = np.linspace(0, path.duration, 10) # Interpolate with spline - qs_spline = path.eval(xs) - qds_spline = path.evald(xs) - qdds_spline = path.evaldd(xs) + qs_spline = path(xs) + qds_spline = path(xs, 1) + qdds_spline = path(xs, 2) # Interpolate with OpenRAVE qs_rave = [] diff --git a/toppra/__init__.py b/toppra/__init__.py index 3b95eda4..2f16e42b 100644 --- a/toppra/__init__.py +++ b/toppra/__init__.py @@ -6,7 +6,7 @@ This package produces routines for creation and handling path constraints using the algorithm `TOPP-RA`. """ - +import logging from .interpolator import RaveTrajectoryWrapper, SplineInterpolator,\ UnivariateSplineInterpolator, PolynomialPath from .utils import smooth_singularities, setup_logging @@ -18,5 +18,4 @@ from . import solverwrapper # set nullhandler by default -import logging logging.getLogger('toppra').addHandler(logging.NullHandler()) diff --git a/toppra/constants.py b/toppra/constants.py index bacb30f2..49d69a2d 100644 --- a/toppra/constants.py +++ b/toppra/constants.py @@ -1,3 +1,13 @@ +"""Some constants used by TOPPRA solvers.""" +import logging +try: + import openravepy as orpy + FOUND_OPENRAVE = True +except (ImportError, SyntaxError) as err: + FOUND_OPENRAVE = False + logging.getLogger('toppra').warn("Unable to import openrave.") + + # Constants SUPERTINY = 1e-10 TINY = 1e-8 diff --git a/toppra/interpolator.py b/toppra/interpolator.py index 3e9f894f..14f838e7 100644 --- a/toppra/interpolator.py +++ b/toppra/interpolator.py @@ -1,63 +1,25 @@ """This module implements interpolators for representing geometric paths and trajectories. """ import logging -import warnings import numpy as np from scipy.interpolate import UnivariateSpline, CubicSpline, PPoly -from .utils import deprecated +from toppra.utils import deprecated +from toppra.constants import FOUND_OPENRAVE logger = logging.getLogger(__name__) -try: +if FOUND_OPENRAVE: import openravepy as orpy -except ImportError as err: - logger.warning("Unable to import openravepy. Exception: %s", err.args[0]) -except SyntaxError as err: - logger.warning("Unable to import openravepy. Exception: %s", err.args[0]) - - -def normalize(gridpoints): - # type: (np.ndarray) -> np.ndarray - """Normalize the path discretization. - - Args: - gridpoints: Path position array. - - Returns: - out: Normalized path position array. - """ - return np.array(gridpoints) / gridpoints[-1] - - -def _find_left_index(gridpoints, s): - # type: (np.ndarray, float) -> int - """Find the least lowest entry that is larger or equal. - - Args: - gridpoints: Array of path positions. - s: A path position. - - Returns: - out: The desired index. - - """ - for i in range(1, len(gridpoints)): - if gridpoints[i - 1] <= s < gridpoints[i]: - return i - 1 - return len(gridpoints) - 2 class AbstractGeometricPath(object): - """Base class for Interpolators. - - Derive Interpolator should inherit this abstract class. + """Base geometric path. + Derive geometric paths classes should derive the below abstract methods. """ - def __init__(self): - pass - def __call__(self, path_positions, order=0): + # type: (np.ndarray, int) -> np.ndarray """Evaluate the path at given positions. Parameters @@ -79,11 +41,6 @@ def __call__(self, path_positions, order=0): """ raise NotImplementedError - @property - def duration(self): - """Return the duration of the path.""" - raise NotImplementedError - @property def dof(self): """Return the degrees-of-freedom of the path.""" @@ -101,14 +58,6 @@ def path_interval(self): """ raise NotImplementedError - def to_rave_trajectory(self, robot): - """Return the corresponding Openrave Trajectory.""" - raise NotImplementedError - - def to_ros_trajectory_msg(self): - """Return the corresponding ROS trajectory.""" - raise NotImplementedError - class RaveTrajectoryWrapper(AbstractGeometricPath): """An interpolator that wraps OpenRAVE's :class:`GenericTrajectory`. @@ -137,11 +86,15 @@ def __init__(self, traj, robot): self.spec = traj.GetConfigurationSpecification() self._dof = robot.GetActiveDOF() - self._interpolation = self.spec.GetGroupFromName('joint').interpolation - if self._interpolation not in ['quadratic', 'cubic']: - raise ValueError( - "This class only handles trajectories with quadratic or cubic interpolation" - ) + def _extract_interpolation_method(): + _interpolation = self.spec.GetGroupFromName('joint').interpolation + if _interpolation not in ['quadratic', 'cubic']: + raise ValueError( + "This class only handles trajectories with quadratic or cubic interpolation" + ) + return _interpolation + self._interpolation = _extract_interpolation_method() + self._duration = traj.GetDuration() all_waypoints = traj.GetWaypoints(0, traj.GetNumWaypoints()).reshape( traj.GetNumWaypoints(), -1) @@ -242,6 +195,16 @@ def duration(self): def dof(self): return self._dof + def __call__(self, ss_sam, order=0): + if order == 0: + return self.eval(ss_sam) + elif order == 1: + return self.evald(ss_sam) + elif order == 2: + return self.evaldd(ss_sam) + else: + raise ValueError("Order must be 0, 1 or 2.") + def eval(self, ss_sam): """Evalute path postition.""" return self.ppoly(ss_sam) @@ -324,11 +287,11 @@ def _1dof_cspld(s): def __call__(self, path_positions, order=0): if order == 0: - return self.eval(path_positions) + return self.cspl(path_positions) elif order == 1: - return self.evald(path_positions) + return self.cspld(path_positions) elif order == 2: - return self.evaldd(path_positions) + return self.cspldd(path_positions) else: raise ValueError("Invalid order %s" % order) @@ -365,14 +328,17 @@ def get_dof(self): # type: () -> int """Return the path's dof.""" return self.dof + @deprecated def eval(self, ss_sam): """Return the path position.""" return self.cspl(ss_sam) + @deprecated def evald(self, ss_sam): """Return the path velocity.""" return self.cspld(ss_sam) + @deprecated def evaldd(self, ss_sam): """Return the path acceleration.""" return self.cspldd(ss_sam) @@ -401,15 +367,15 @@ def compute_rave_trajectory(self, robot): for i in range(len(self.ss_waypoints) - 1): deltas.append(self.ss_waypoints[i + 1] - self.ss_waypoints[i]) if len(self.ss_waypoints) == 1: - q = self.eval(0) - qd = self.evald(0) - qdd = self.evaldd(0) + q = self(0) + qd = self(0, 1) + qdd = self(0, 2) traj.Insert(traj.GetNumWaypoints(), list(q) + list(qd) + list(qdd) + [0]) else: - qs = self.eval(self.ss_waypoints) - qds = self.evald(self.ss_waypoints) - qdds = self.evaldd(self.ss_waypoints) + qs = self(self.ss_waypoints) + qds = self(self.ss_waypoints, 1) + qdds = self(self.ss_waypoints, 2) for (q, qd, qdd, dt) in zip(qs, qds, qdds, deltas): traj.Insert(traj.GetNumWaypoints(), q.tolist() + qd.tolist() + qdd.tolist() + [dt]) From 936aa566409905f3577cf5ba350802696dbe416c Mon Sep 17 00:00:00 2001 From: Hung Date: Tue, 10 Dec 2019 23:50:41 +0800 Subject: [PATCH 28/42] refactor RaveTrajectoryWrapper to pass linting --- .../retime/robustness/test_robustness_main.py | 6 +- toppra/constraint/linear_joint_velocity.py | 2 +- toppra/interpolator.py | 204 +++++++++--------- 3 files changed, 109 insertions(+), 103 deletions(-) diff --git a/tests/retime/robustness/test_robustness_main.py b/tests/retime/robustness/test_robustness_main.py index 319e5078..2a84f3d0 100644 --- a/tests/retime/robustness/test_robustness_main.py +++ b/tests/retime/robustness/test_robustness_main.py @@ -85,9 +85,9 @@ def test_robustness_main(request): axs[0, 0].plot(data["K"][:, 0], c="C0") axs[0, 0].plot(data["K"][:, 1], c="C0") axs[0, 0].plot(data["sd"] ** 2, c="C1") - axs[0, 1].plot(_t, jnt_traj.eval(_t)) - axs[1, 0].plot(_t, jnt_traj.evald(_t)) - axs[1, 1].plot(_t, jnt_traj.evaldd(_t)) + axs[0, 1].plot(_t, jnt_traj(_t)) + axs[1, 0].plot(_t, jnt_traj(_t, 1)) + axs[1, 1].plot(_t, jnt_traj(_t, 2)) axs[0, 0].set_title("param") axs[0, 1].set_title("jnt. pos.") diff --git a/toppra/constraint/linear_joint_velocity.py b/toppra/constraint/linear_joint_velocity.py index 5be8d7da..a707ca76 100644 --- a/toppra/constraint/linear_joint_velocity.py +++ b/toppra/constraint/linear_joint_velocity.py @@ -36,7 +36,7 @@ def compute_constraint_params(self, path, gridpoints, scaling=1): raise ValueError( "Wrong dimension: constraint dof ({:d}) not equal to path dof ({:d})" .format(self.get_dof(), path.dof)) - qs = path.evald(gridpoints / scaling) / scaling + qs = path(gridpoints / scaling, 1) / scaling _, _, xbound_ = _create_velocity_constraint(qs, self.vlim) xbound = np.array(xbound_) xbound[:, 0] = xbound_[:, 1] diff --git a/toppra/interpolator.py b/toppra/interpolator.py index 14f838e7..4412bba1 100644 --- a/toppra/interpolator.py +++ b/toppra/interpolator.py @@ -69,6 +69,14 @@ class RaveTrajectoryWrapper(AbstractGeometricPath): object. """ + @staticmethod + def _extract_interpolation_method(spec): + _interpolation = spec.GetGroupFromName('joint').interpolation + if _interpolation not in ['quadratic', 'cubic']: + raise ValueError( + "This class only handles trajectories with quadratic or cubic interpolation" + ) + return _interpolation def __init__(self, traj, robot): # type: (orpy.RaveTrajectory, orpy.Robot) -> None @@ -80,102 +88,87 @@ def __init__(self, traj, robot): An OpenRAVE joint trajectory. robot: An OpenRAVE robot. + """ super(RaveTrajectoryWrapper, self).__init__() self.traj = traj #: init - self.spec = traj.GetConfigurationSpecification() + spec = traj.GetConfigurationSpecification() self._dof = robot.GetActiveDOF() - - def _extract_interpolation_method(): - _interpolation = self.spec.GetGroupFromName('joint').interpolation - if _interpolation not in ['quadratic', 'cubic']: - raise ValueError( - "This class only handles trajectories with quadratic or cubic interpolation" - ) - return _interpolation - self._interpolation = _extract_interpolation_method() - + self._interpolation = self._extract_interpolation_method(spec) self._duration = traj.GetDuration() + all_waypoints = traj.GetWaypoints(0, traj.GetNumWaypoints()).reshape( traj.GetNumWaypoints(), -1) valid_wp_indices = [0] self.ss_waypoints = [0.0] for i in range(1, traj.GetNumWaypoints()): - dt = self.spec.ExtractDeltaTime(all_waypoints[i]) + dt = spec.ExtractDeltaTime(all_waypoints[i]) if dt > 1e-5: # If delta is too small, skip it. valid_wp_indices.append(i) self.ss_waypoints.append(self.ss_waypoints[-1] + dt) - - self.n_waypoints = len(valid_wp_indices) self.ss_waypoints = np.array(self.ss_waypoints) - self.s_start = self.ss_waypoints[0] - self.s_end = self.ss_waypoints[-1] - - self.waypoints = np.array([ - self.spec.ExtractJointValues(all_waypoints[i], robot, - robot.GetActiveDOFIndices()) - for i in valid_wp_indices - ]) - self.waypoints_d = np.array([ - self.spec.ExtractJointValues(all_waypoints[i], robot, - robot.GetActiveDOFIndices(), 1) - for i in valid_wp_indices - ]) - - # Degenerate case: there is only one waypoint. - if self.n_waypoints == 1: - pp_coeffs = np.zeros((1, 1, self.dof)) - for idof in range(self.dof): - pp_coeffs[0, 0, idof] = self.waypoints[0, idof] - # A constant function - self.ppoly = PPoly(pp_coeffs, [0, 1]) - - elif self._interpolation == "quadratic": - self.waypoints_dd = [] - for i in range(self.n_waypoints - 1): - qdd = ((self.waypoints_d[i + 1] - self.waypoints_d[i]) / - (self.ss_waypoints[i + 1] - self.ss_waypoints[i])) - self.waypoints_dd.append(qdd) - self.waypoints_dd = np.array(self.waypoints_dd) - - # Fill the coefficient matrix for scipy.PPoly class - pp_coeffs = np.zeros((3, self.n_waypoints - 1, self.dof)) - for idof in range(self.dof): - for iseg in range(self.n_waypoints - 1): - pp_coeffs[:, iseg, idof] = [ - self.waypoints_dd[iseg, idof] / 2, - self.waypoints_d[iseg, idof], - self.waypoints[iseg, idof] - ] - self.ppoly = PPoly(pp_coeffs, self.ss_waypoints) - - elif self._interpolation == "cubic": - self.waypoints_dd = np.array([ - self.spec.ExtractJointValues(all_waypoints[i], robot, - robot.GetActiveDOFIndices(), 2) + n_waypoints = len(valid_wp_indices) + + def _extract_waypoints(order): + return np.array([ + spec.ExtractJointValues(all_waypoints[i], robot, robot.GetActiveDOFIndices(), order) for i in valid_wp_indices ]) - self.waypoints_ddd = [] - for i in range(self.n_waypoints - 1): - qddd = ((self.waypoints_dd[i + 1] - self.waypoints_dd[i]) / - (self.ss_waypoints[i + 1] - self.ss_waypoints[i])) - self.waypoints_ddd.append(qddd) - self.waypoints_ddd = np.array(self.waypoints_ddd) - - # Fill the coefficient matrix for scipy.PPoly class - pp_coeffs = np.zeros((4, self.n_waypoints - 1, self.dof)) - for idof in range(self.dof): - for iseg in range(self.n_waypoints - 1): - pp_coeffs[:, iseg, idof] = [ - self.waypoints_ddd[iseg, idof] / 6, - self.waypoints_dd[iseg, idof] / 2, - self.waypoints_d[iseg, idof], - self.waypoints[iseg, idof] - ] - self.ppoly = PPoly(pp_coeffs, self.ss_waypoints) - - self.ppoly_d = self.ppoly.derivative() - self.ppoly_dd = self.ppoly.derivative(2) + + def _make_ppoly(): + if n_waypoints == 1: + waypoints = _extract_waypoints(0) + pp_coeffs = np.zeros((1, 1, self.dof)) + for idof in range(self.dof): + pp_coeffs[0, 0, idof] = waypoints[0, idof] + return PPoly(pp_coeffs, [0, 1]) + + elif self._interpolation == "quadratic": + waypoints = _extract_waypoints(0) + waypoints_d = _extract_waypoints(1) + waypoints_dd = [] + for i in range(n_waypoints - 1): + qdd = ((waypoints_d[i + 1] - waypoints_d[i]) / + (self.ss_waypoints[i + 1] - self.ss_waypoints[i])) + waypoints_dd.append(qdd) + waypoints_dd = np.array(waypoints_dd) + + # Fill the coefficient matrix for scipy.PPoly class + pp_coeffs = np.zeros((3, n_waypoints - 1, self.dof)) + for idof in range(self.dof): + for iseg in range(n_waypoints - 1): + pp_coeffs[:, iseg, idof] = [ + waypoints_dd[iseg, idof] / 2, + waypoints_d[iseg, idof], + waypoints[iseg, idof] + ] + return PPoly(pp_coeffs, self.ss_waypoints) + + elif self._interpolation == "cubic": + waypoints = _extract_waypoints(0) + waypoints_d = _extract_waypoints(1) + waypoints_dd = _extract_waypoints(2) + waypoints_ddd = [] + for i in range(n_waypoints - 1): + qddd = ((waypoints_dd[i + 1] - waypoints_dd[i]) / + (self.ss_waypoints[i + 1] - self.ss_waypoints[i])) + waypoints_ddd.append(qddd) + waypoints_ddd = np.array(waypoints_ddd) + + # Fill the coefficient matrix for scipy.PPoly class + pp_coeffs = np.zeros((4, n_waypoints - 1, self.dof)) + for idof in range(self.dof): + for iseg in range(n_waypoints - 1): + pp_coeffs[:, iseg, idof] = [ + waypoints_ddd[iseg, idof] / 6, + waypoints_dd[iseg, idof] / 2, + waypoints_d[iseg, idof], + waypoints[iseg, idof] + ] + return PPoly(pp_coeffs, self.ss_waypoints) + raise ValueError("An error has occured. Unable to form PPoly.") + + self.ppoly = _make_ppoly() @deprecated def get_duration(self): @@ -189,8 +182,14 @@ def get_dof(self): # type: () -> int @property def duration(self): + """Return the duration of the path.""" return self._duration + @property + def path_interval(self): + """Return the start and end points.""" + return np.array([0, self._duration]) + @property def dof(self): return self._dof @@ -211,11 +210,11 @@ def eval(self, ss_sam): def evald(self, ss_sam): """Evalute path velocity.""" - return self.ppoly_d(ss_sam) + return self.ppoly.derivative()(ss_sam) def evaldd(self, ss_sam): """Evalute path acceleration.""" - return self.ppoly_dd(ss_sam) + return self.ppoly.derivative(2)(ss_sam) class SplineInterpolator(AbstractGeometricPath): @@ -254,11 +253,7 @@ def __init__(self, ss_waypoints, waypoints, bc_type='not-a-knot'): assert ss_waypoints[0] == 0, "First index must equals zero." self.ss_waypoints = np.array(ss_waypoints) self.waypoints = np.array(waypoints) - self.bc_type = bc_type - assert self.ss_waypoints.shape[0] == self.waypoints.shape[0] - self.s_start = self.ss_waypoints[0] - self.s_end = self.ss_waypoints[-1] if len(ss_waypoints) == 1: @@ -306,10 +301,12 @@ def get_duration(self): @property def duration(self): + """Return the duration of the path.""" return self.ss_waypoints[-1] - self.ss_waypoints[0] @property def path_interval(self): + """Return the start and end points.""" return np.array([self.ss_waypoints[0], self.ss_waypoints[-1]]) @deprecated @@ -402,14 +399,10 @@ def __init__(self, ss_waypoints, waypoints): self.ss_waypoints = np.array(ss_waypoints) self.waypoints = np.array(waypoints) if np.isscalar(waypoints[0]): - self.dof = 1 + self._dof = 1 else: - self.dof = waypoints[0].shape[0] - self.duration = ss_waypoints[-1] + self._dof = waypoints[0].shape[0] assert self.ss_waypoints.shape[0] == self.waypoints.shape[0] - self.s_start = self.ss_waypoints[0] - self.s_end = self.ss_waypoints[-1] - self.uspl = [] for i in range(self.dof): self.uspl.append( @@ -417,15 +410,27 @@ def __init__(self, ss_waypoints, waypoints): self.uspld = [spl.derivative() for spl in self.uspl] self.uspldd = [spl.derivative() for spl in self.uspld] - @deprecated - def get_duration(self): - """Return the path duration.""" - return self.duration + @property + def dof(self): + return self._dof - @deprecated - def get_path_interval(self): + @property + def path_interval(self): """Return the path interval.""" - return self.path_interval + return [self.ss_waypoints[0], self.ss_waypoints[-1]] + + def __call__(self, ss_sam, order=0): + data = [] + if order == 0: + for spl in self.uspl: + data.append(spl(ss_sam)) + elif order == 1: + for spl in self.uspld: + data.append(spl(ss_sam)) + elif order == 2: + for spl in self.uspldd: + data.append(spl(ss_sam)) + return np.array(data).T def eval(self, ss_sam): """Return the path position.""" @@ -510,6 +515,7 @@ def dof(self): @property def duration(self): + """Return the duration of the path.""" return self.s_end - self.s_start @property From 2aa7c745d33bbe15f432434e69375758e70dd66c Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 4 Aug 2019 15:25:06 +0800 Subject: [PATCH 29/42] implement the core function for estimating gridpoints --- toppra/interpolator.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/toppra/interpolator.py b/toppra/interpolator.py index 4412bba1..266c0e66 100644 --- a/toppra/interpolator.py +++ b/toppra/interpolator.py @@ -12,6 +12,20 @@ import openravepy as orpy +def propose_gridpoints(path, delta=1e-4, max_iteration=10): + """""" + gridpoints_ept = [0, path.duration] + for it in range(max_iteration): + for idx in range(len(gridpoints_ept) - 1): + gp_mid = 0.5 * (gridpoints_ept[idx] + gridpoints_ept[idx + 1]) + dist = gridpoints_ept[idx + 1] - gridpoints_ept[idx] + max_err = np.max(np.abs(0.5 * path.evaldd(gp_mid) * dist ** 2)) + if max_err > delta: + gridpoints_ept.append(gp_mid) + gridpoints_ept = sorted(gridpoints_ept) + return gridpoints_ept + + class AbstractGeometricPath(object): """Base geometric path. From 48d6fcc6c4472908bd9685ece306d21b8fc50ad4 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 7 Dec 2019 19:56:42 +0800 Subject: [PATCH 30/42] add test for finding gridpoints --- tests/interpolators/test_find_gridpoints.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/interpolators/test_find_gridpoints.py diff --git a/tests/interpolators/test_find_gridpoints.py b/tests/interpolators/test_find_gridpoints.py new file mode 100644 index 00000000..15e3b69f --- /dev/null +++ b/tests/interpolators/test_find_gridpoints.py @@ -0,0 +1,21 @@ +import toppra +import toppra.interpolator +import numpy as np +import matplotlib.pyplot as plt + + +def test_basic_usage(): + waypoints = [[0, 0.3, 0.5], [1, 2, 3], [0., 0.1, 0.2], [0, 0.5, 0]] + ss = np.linspace(0, 1, len(waypoints)) + path = toppra.interpolator.SplineInterpolator(ss, waypoints) + + gridpoints_ept = toppra.interpolator.propose_gridpoints(path) + + # visualize ############################################################### + ss_full = np.linspace(0, 1, 100) + for i in range(len(waypoints[0])): + plt.plot(ss_full, path(ss_full)[:, i], '--', c='C%d' % i) + plt.plot(gridpoints_ept, path(gridpoints_ept)[:, i], '-o', c='C%d' % i) + plt.show() + + From 7745c26ac6a6f8ce8dea474c0f67e56284d8aad2 Mon Sep 17 00:00:00 2001 From: Hung Date: Sun, 15 Dec 2019 22:59:39 +0800 Subject: [PATCH 31/42] some refactors to make using velocity/accel constraints easier --- examples/kinematics.py | 38 +++++++---------- tests/constraint/test_joint_velocity.py | 41 ++++++++++++++----- toppra/algorithm/algorithm.py | 4 +- .../reachability_algorithm.py | 26 ++++++------ toppra/constants.py | 2 +- .../constraint/linear_joint_acceleration.py | 11 ++++- toppra/constraint/linear_joint_velocity.py | 19 ++++++--- toppra/interpolator.py | 2 +- toppra/solverwrapper/__init__.py | 1 + toppra/solverwrapper/solverwrapper.py | 33 +++++++++++++++ 10 files changed, 119 insertions(+), 58 deletions(-) diff --git a/examples/kinematics.py b/examples/kinematics.py index 705f94ed..411b4226 100644 --- a/examples/kinematics.py +++ b/examples/kinematics.py @@ -8,47 +8,37 @@ ta.setup_logging("INFO") -def main(): +def generate_new_problem(): # Parameters N_samples = 5 SEED = 9 dof = 7 - - # Random waypoints used to obtain a random geometric path. Here, - # we use spline interpolation. np.random.seed(SEED) way_pts = np.random.randn(N_samples, dof) - path = ta.SplineInterpolator(np.linspace(0, 1, 5), way_pts) + return way_pts - # Create velocity bounds, then velocity constraint object - vlim_ = np.random.rand(dof) * 20 - vlim = np.vstack((-vlim_, vlim_)).T - # Create acceleration bounds, then acceleration constraint object - alim_ = np.random.rand(dof) * 2 - alim = np.vstack((-alim_, alim_)).T - pc_vel = constraint.JointVelocityConstraint(vlim) - pc_acc = constraint.JointAccelerationConstraint( - alim, discretization_scheme=constraint.DiscretizationType.Interpolation) - # Setup a parametrization instance. The keyword arguments are - # optional. - instance = algo.TOPPRA([pc_vel, pc_acc], path, solver_wrapper='seidel') +def main(): + way_pts = generate_new_problem() + path = ta.SplineInterpolator(np.linspace(0, 1, 5), way_pts) + pc_vel = constraint.JointVelocityConstraint(10 + np.random.rand(path.dof) * 20) + pc_acc = constraint.JointAccelerationConstraint(10 + np.random.rand(path.dof) * 2) + + # Setup a parametrization instance. The keyword arguments are optional. + instance = algo.TOPPRA([pc_vel, pc_acc], path) # Retime the trajectory, only this step is necessary. t0 = time.time() - jnt_traj, aux_traj, data = instance.compute_trajectory(0, 0, return_data=True) + jnt_traj, _, data = instance.compute_trajectory(0, 0, return_data=True) # return_data flag outputs internal data obtained while computing # the paramterization. This include the time stamps corresponding # to the original waypoints. See below (line 53) to see how to # extract the time stamps. print("Parameterization time: {:} secs".format(time.time() - t0)) - ts_sample = np.linspace(0, jnt_traj.get_duration(), 100) - qs_sample = jnt_traj.eval(ts_sample) # sampled joint positions - qds_sample = jnt_traj.evald(ts_sample) # sampled joint velocities - qdds_sample = jnt_traj.evaldd(ts_sample) # sampled joint accelerations - - for i in range(dof): + ts_sample = np.linspace(0, jnt_traj.duration, 100) + qs_sample = jnt_traj(ts_sample) + for i in range(path.dof): # plot the i-th joint trajectory plt.plot(ts_sample, qs_sample[:, i], c="C{:d}".format(i)) # plot the i-th joint waypoints diff --git a/tests/constraint/test_joint_velocity.py b/tests/constraint/test_joint_velocity.py index 1888f363..cc68f777 100644 --- a/tests/constraint/test_joint_velocity.py +++ b/tests/constraint/test_joint_velocity.py @@ -70,7 +70,7 @@ def test_constraint_satisfaction(self, velocity_pc_data): try: prob.solve(solver=cvx.ECOS, abstol=1e-9) xmax = sd.value ** 2 - + prob = cvx.Problem(cvx.Minimize(sd), constraints) prob.solve(solver=cvx.ECOS, abstol=1e-9) xmin = sd.value ** 2 @@ -93,15 +93,6 @@ def test_wrong_dimension(self, velocity_pc_data): ) -def test_negative_velocity(): - "illegal velocity limits input" - vlim = np.array([[-1., -2], [-2., 2]]) - with pytest.raises(AssertionError) as e_info: - constraint = ta.constraint.JointVelocityConstraint(vlim) - print(e_info) - assert e_info.value.args[0][:19] == "Bad velocity limits" - - def test_jnt_vel_varying_basic(): # constraint ss_wpts = np.r_[0, 1, 2] @@ -131,7 +122,7 @@ def test_jnt_vel_varying_basic(): prob = cvx.Problem(cvx.Maximize(sd), constraints) prob.solve(solver=cvx.ECOS, abstol=1e-9) xmax = sd.value ** 2 - + prob = cvx.Problem(cvx.Minimize(sd), constraints) prob.solve(solver=cvx.ECOS, abstol=1e-9) xmin = sd.value ** 2 @@ -144,3 +135,31 @@ def test_jnt_vel_varying_basic(): # assert non-negativity assert xlimit[i, 0] >= 0 + +def test_only_max_vel_given(): + c = ta.constraint.JointVelocityConstraint([1, 1.2, 2]) + np.testing.assert_allclose(c.vlim, + [ + [-1, 1], + [-1.2, 1.2], + [-2, 2], + ]) + +def test_negative_bound_given(): + with pytest.raises(ValueError) as err: + c = ta.constraint.JointVelocityConstraint([1, -1.2, 2]) + assert 'velocity' in err.value + + +def test_negative_velocity(): + "illegal velocity limits input" + vlim = np.array([[-1., -2], [-2., 2]]) + with pytest.raises(ValueError) as e_info: + constraint = ta.constraint.JointVelocityConstraint(vlim) + assert e_info.value.args[0][:19] == "Bad velocity limits" + + +@pytest.mark.parametrize('velocities', [[1.2, None], [0, 1.0]]) +def test_bad_velocity_given(velocities): + with pytest.raises(ValueError) as e_info: + constraint = ta.constraint.JointVelocityConstraint([1.2, None]) diff --git a/toppra/algorithm/algorithm.py b/toppra/algorithm/algorithm.py index 62acac55..e147ed6f 100644 --- a/toppra/algorithm/algorithm.py +++ b/toppra/algorithm/algorithm.py @@ -11,9 +11,9 @@ try: import openravepy as orpy except ImportError as err: - logger.warning("Unable to import openravepy. Exception: %s" % err.args[0]) + logger.debug("Unable to import openravepy. Exception: %s" % err.args[0]) except SyntaxError as err: - logger.warning("Unable to import openravepy. Exception: %s" % err.args[0]) + logger.debug("Unable to import openravepy. Exception: %s" % err.args[0]) class ParameterizationAlgorithm(object): diff --git a/toppra/algorithm/reachabilitybased/reachability_algorithm.py b/toppra/algorithm/reachabilitybased/reachability_algorithm.py index b892be2e..49bf140d 100644 --- a/toppra/algorithm/reachabilitybased/reachability_algorithm.py +++ b/toppra/algorithm/reachabilitybased/reachability_algorithm.py @@ -1,6 +1,8 @@ from ..algorithm import ParameterizationAlgorithm from ...constants import LARGE, SMALL, TINY, INFTY, CVXPY_MAXX, MAX_TRIES from ...constraint import ConstraintType +import toppra.solverwrapper +import toppra.exceptions as exceptions import numpy as np import logging @@ -57,15 +59,11 @@ def __init__(self, constraint_list, path, gridpoints=None, solver_wrapper=None, # Handle gridpoints if gridpoints is None: - gridpoints = np.linspace(0, path.duration, 100) - logger.info("Automatically choose a gridpoint with 100 segments/stages, spaning the input path domain uniformly.") - if path.path_interval[0] != gridpoints[0]: - logger.fatal("Manually supplied gridpoints does not start from 0.") - raise ValueError("Bad input gridpoints.") - if path.path_interval[1] != gridpoints[-1]: - logger.fatal("Manually supplied gridpoints have endpoint " - "different from input path duration.") - raise ValueError("Bad input gridpoints.") + gridpoints = np.linspace(path.path_interval[0], path.path_interval[1], 100) + logger.info("No gridpoint specified. Automatically choose a gridpoint with 100 " + "segments/stages, spaning the input path domain uniformly.") + if path.path_interval[0] != gridpoints[0] or path.path_interval[1] != gridpoints[-1]: + raise ValueError("Invalid manually supplied gridpoints.") self.gridpoints = np.array(gridpoints) self._N = len(gridpoints) - 1 # Number of stages. Number of point is _N + 1 for i in range(self._N): @@ -97,13 +95,17 @@ def __init__(self, constraint_list, path, gridpoints=None, solver_wrapper=None, has_conic = True # Select solver wrapper automatically + available_solvers = toppra.solverwrapper.available_solvers() if solver_wrapper is None: - logger.debug("Solver wrapper not supplied. Choose solver wrapper automatically!") + logger.info("Solver wrapper not supplied. Choose solver wrapper automatically!") if has_conic: + if not available_solvers['ecos']: + raise exceptions.ToppraError("Solverwrapper not available.") solver_wrapper = "ecos" else: - solver_wrapper = "qpOASES" - logger.debug("Select solver {:}".format(solver_wrapper)) + valid_solver = [solver for solver, avail in available_solvers if avail] + solver_wrapper = valid_solver[0] + logger.info("Select solver {:}".format(solver_wrapper)) # Check solver-wrapper suitability if has_conic: diff --git a/toppra/constants.py b/toppra/constants.py index 49d69a2d..67d08617 100644 --- a/toppra/constants.py +++ b/toppra/constants.py @@ -5,7 +5,7 @@ FOUND_OPENRAVE = True except (ImportError, SyntaxError) as err: FOUND_OPENRAVE = False - logging.getLogger('toppra').warn("Unable to import openrave.") + logging.getLogger('toppra').debug("Unable to import openrave.") # Constants diff --git a/toppra/constraint/linear_joint_acceleration.py b/toppra/constraint/linear_joint_acceleration.py index 6149c52c..f471159b 100644 --- a/toppra/constraint/linear_joint_acceleration.py +++ b/toppra/constraint/linear_joint_acceleration.py @@ -28,7 +28,7 @@ class JointAccelerationConstraint(LinearConstraint): - :code:`h` := :math:`[\ddot{\mathbf{q}}_{max}^T, -\ddot{\mathbf{q}}_{min}^T]^T` """ - def __init__(self, alim, discretization_scheme=DiscretizationType.Collocation): + def __init__(self, alim, discretization_scheme=DiscretizationType.Interpolation): """Initialize the joint acceleration class. Parameters @@ -43,9 +43,16 @@ def __init__(self, alim, discretization_scheme=DiscretizationType.Collocation): higher computational cost. """ super(JointAccelerationConstraint, self).__init__() - self.alim = np.array(alim, dtype=float) + alim = np.array(alim, dtype=float) + if np.isnan(alim).any(): + raise ValueError("Bad velocity given: %s" % alim) + if len(alim.shape) == 1: + self.alim = np.vstack((-np.array(alim), np.array(alim))).T + else: + self.alim = np.array(alim, dtype=float) self.dof = self.alim.shape[0] self.set_discretization_type(discretization_scheme) + assert self.alim.shape[1] == 2, "Wrong input shape." self._format_string = " Acceleration limit: \n" for i in range(self.alim.shape[0]): diff --git a/toppra/constraint/linear_joint_velocity.py b/toppra/constraint/linear_joint_velocity.py index a707ca76..e23f045b 100644 --- a/toppra/constraint/linear_joint_velocity.py +++ b/toppra/constraint/linear_joint_velocity.py @@ -18,14 +18,23 @@ class JointVelocityConstraint(LinearConstraint): def __init__(self, vlim): super(JointVelocityConstraint, self).__init__() - self.vlim = np.array(vlim, dtype=float) + vlim = np.array(vlim, dtype=float) + if np.isnan(vlim).any(): + raise ValueError("Bad velocity given: %s" % vlim) + if len(vlim.shape) == 1: + self.vlim = np.vstack((-np.array(vlim), np.array(vlim))).T + else: + self.vlim = np.array(vlim, dtype=float) self.dof = self.vlim.shape[0] + self._assert_valid_limits() + + def _assert_valid_limits(self): + """Check that the velocity limits is valid.""" assert self.vlim.shape[1] == 2, "Wrong input shape." for i in range(self.dof): - assert self.vlim[i, 0] < self.vlim[ - i, - 1], "Bad velocity limits: {:} (lower limit) > {:} (higher limit)".format( - self.vlim[i, 0], self.vlim[i, 1]) + if self.vlim[i, 0] >= self.vlim[i, 1]: + raise ValueError("Bad velocity limits: {:} (lower limit) > {:} (higher limit)".format( + self.vlim[i, 0], self.vlim[i, 1])) self._format_string = " Velocity limit: \n" for i in range(self.vlim.shape[0]): self._format_string += " J{:d}: {:}".format( diff --git a/toppra/interpolator.py b/toppra/interpolator.py index 266c0e66..1f4cf72a 100644 --- a/toppra/interpolator.py +++ b/toppra/interpolator.py @@ -14,7 +14,7 @@ def propose_gridpoints(path, delta=1e-4, max_iteration=10): """""" - gridpoints_ept = [0, path.duration] + gridpoints_ept = [0, path.duration] for it in range(max_iteration): for idx in range(len(gridpoints_ept) - 1): gp_mid = 0.5 * (gridpoints_ept[idx] + gridpoints_ept[idx + 1]) diff --git a/toppra/solverwrapper/__init__.py b/toppra/solverwrapper/__init__.py index 21ae6d6f..22bae49e 100644 --- a/toppra/solverwrapper/__init__.py +++ b/toppra/solverwrapper/__init__.py @@ -1,3 +1,4 @@ from .hot_qpoases_solverwrapper import hotqpOASESSolverWrapper from .cy_seidel_solverwrapper import seidelWrapper from .ecos_solverwrapper import ecosWrapper +from .solverwrapper import available_solvers diff --git a/toppra/solverwrapper/solverwrapper.py b/toppra/solverwrapper/solverwrapper.py index 0ba87fc9..598ccb6a 100644 --- a/toppra/solverwrapper/solverwrapper.py +++ b/toppra/solverwrapper/solverwrapper.py @@ -1,5 +1,38 @@ +import logging import numpy as np +logger = logging.getLogger(__name__) + + +def available_solvers(output_msg=True): + """Check for available solvers.""" + try: + import ecos + IMPORT_ECOS = True + except ImportError as err: + IMPORT_ECOS = False + try: + import qpoases + IMPORT_QPOASES = True + except ImportError as err: + IMPORT_QPOASES = False + try: + import cvxpy + IMPORT_CVXPY = True + except ImportError as err: + IMPORT_CVXPY = False + solver_availability = ( + ('seidel',True), + ('hotqpoases',IMPORT_QPOASES), + ('qpoases',IMPORT_QPOASES), + ('ecos',IMPORT_ECOS), + ('cvxpy',IMPORT_CVXPY) + ) + + if output_msg: + print(solver_availability) + return solver_availability + class SolverWrapper(object): """The base class for all solver wrappers. From eb6ca7367c6d5e9dbd2cb290d89f7eb6594cda27 Mon Sep 17 00:00:00 2001 From: Hung Date: Mon, 16 Dec 2019 21:48:31 +0800 Subject: [PATCH 32/42] add a new more tests for the auto-gridpoint feature --- tests/interpolators/test_find_gridpoints.py | 33 ++++++++++++---- toppra/interpolator.py | 43 ++++++++++++++++++--- 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/tests/interpolators/test_find_gridpoints.py b/tests/interpolators/test_find_gridpoints.py index 15e3b69f..d5c62dd5 100644 --- a/tests/interpolators/test_find_gridpoints.py +++ b/tests/interpolators/test_find_gridpoints.py @@ -2,20 +2,39 @@ import toppra.interpolator import numpy as np import matplotlib.pyplot as plt +import pytest -def test_basic_usage(): + +@pytest.fixture(params=[[0, 1], [1.5, 2.7]]) +def path(request): + start, end = request.param waypoints = [[0, 0.3, 0.5], [1, 2, 3], [0., 0.1, 0.2], [0, 0.5, 0]] - ss = np.linspace(0, 1, len(waypoints)) + ss = np.linspace(start, end, len(waypoints)) path = toppra.interpolator.SplineInterpolator(ss, waypoints) + yield path, waypoints + + +def test_basic_usage(path): + path, waypoints = path + gridpoints_ept = toppra.interpolator.propose_gridpoints(path, 1e-2) + assert gridpoints_ept[0] == path.path_interval[0] + assert gridpoints_ept[-1] == path.path_interval[1] - gridpoints_ept = toppra.interpolator.propose_gridpoints(path) + # The longest segment should be smaller than 0.1. This is to + # ensure a reasonable response. + assert np.max(np.diff(gridpoints_ept)) < 0.05 # visualize ############################################################### - ss_full = np.linspace(0, 1, 100) + ss_full = np.linspace(path.path_interval[0], path.path_interval[1], 100) for i in range(len(waypoints[0])): plt.plot(ss_full, path(ss_full)[:, i], '--', c='C%d' % i) plt.plot(gridpoints_ept, path(gridpoints_ept)[:, i], '-o', c='C%d' % i) - plt.show() - - + # plt.show() + + +def test_hard_path_difficult_to_approximate_within_iterations(path): + """The given setting makes the approximation fails.""" + path, _ = path + with pytest.raises(ValueError): + toppra.interpolator.propose_gridpoints(path, max_iteration=2) diff --git a/toppra/interpolator.py b/toppra/interpolator.py index 1f4cf72a..368777b2 100644 --- a/toppra/interpolator.py +++ b/toppra/interpolator.py @@ -12,17 +12,49 @@ import openravepy as orpy -def propose_gridpoints(path, delta=1e-4, max_iteration=10): - """""" - gridpoints_ept = [0, path.duration] +def propose_gridpoints(path, max_err_threshold=1e-4, max_iteration=100, max_seg_length=0.05): + """Generate a set of grid pooint for the given path. + + This function operates in multiple passes through the geometric + path from the start to the end point. In each pass, for each + segment, the maximum interpolation error is estimated using the + following equation: + + 0.5 * max(abs(p'' * d_segment ^ 2)) + + Here p'' is the second derivative of the path and d_segment is the + length of the segment. Intuitively, at positions with higher + curvature, there must be more points in order to improve + approximation quality. + + Arguments + --------- + path: Input geometric path. + max_err_threshold: Maximum worstcase error thrshold allowable. + max_iteration: Maximum number of iterations. + max_seg_length: All segments length should be smaller than this value. + + """ + gridpoints_ept = [path.path_interval[0], path.path_interval[1]] for it in range(max_iteration): + add_new_points = False for idx in range(len(gridpoints_ept) - 1): gp_mid = 0.5 * (gridpoints_ept[idx] + gridpoints_ept[idx + 1]) + if (gridpoints_ept[idx + 1] - gridpoints_ept[idx]) > max_seg_length: + gridpoints_ept.append(gp_mid) + add_new_points = True + continue + dist = gridpoints_ept[idx + 1] - gridpoints_ept[idx] - max_err = np.max(np.abs(0.5 * path.evaldd(gp_mid) * dist ** 2)) - if max_err > delta: + max_err = np.max(np.abs(0.5 * path(gp_mid, 2) * dist ** 2)) + if max_err > max_err_threshold: + add_new_points = True gridpoints_ept.append(gp_mid) gridpoints_ept = sorted(gridpoints_ept) + if not add_new_points: + break + if it == max_iteration - 1: + raise ValueError("Unable to find a good gridpoint for this path.") return gridpoints_ept @@ -264,7 +296,6 @@ class SplineInterpolator(AbstractGeometricPath): def __init__(self, ss_waypoints, waypoints, bc_type='not-a-knot'): super(SplineInterpolator, self).__init__() - assert ss_waypoints[0] == 0, "First index must equals zero." self.ss_waypoints = np.array(ss_waypoints) self.waypoints = np.array(waypoints) assert self.ss_waypoints.shape[0] == self.waypoints.shape[0] From a40d9f1f8313b2a413d23ca5bdf05b7444941fb7 Mon Sep 17 00:00:00 2001 From: Hung Date: Mon, 16 Dec 2019 22:09:34 +0800 Subject: [PATCH 33/42] use auto gridpoint in main algo --- .../algorithm/reachabilitybased/reachability_algorithm.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/toppra/algorithm/reachabilitybased/reachability_algorithm.py b/toppra/algorithm/reachabilitybased/reachability_algorithm.py index 49bf140d..1e01dba1 100644 --- a/toppra/algorithm/reachabilitybased/reachability_algorithm.py +++ b/toppra/algorithm/reachabilitybased/reachability_algorithm.py @@ -3,6 +3,7 @@ from ...constraint import ConstraintType import toppra.solverwrapper import toppra.exceptions as exceptions +import toppra.interpolator as interpolator import numpy as np import logging @@ -59,9 +60,9 @@ def __init__(self, constraint_list, path, gridpoints=None, solver_wrapper=None, # Handle gridpoints if gridpoints is None: - gridpoints = np.linspace(path.path_interval[0], path.path_interval[1], 100) - logger.info("No gridpoint specified. Automatically choose a gridpoint with 100 " - "segments/stages, spaning the input path domain uniformly.") + gridpoints = interpolator.propose_gridpoints(path, max_err_threshold=1e-3) + logger.info("No gridpoint specified. Automatically choose a gridpoint. See `propose_gridpoints`.") + if path.path_interval[0] != gridpoints[0] or path.path_interval[1] != gridpoints[-1]: raise ValueError("Invalid manually supplied gridpoints.") self.gridpoints = np.array(gridpoints) From cc258f97eeeaf1017155208a34aa47ee86309d8b Mon Sep 17 00:00:00 2001 From: Hung Date: Mon, 16 Dec 2019 22:19:13 +0800 Subject: [PATCH 34/42] fix codestyle --- .pylintrc | 138 +++++++++++++++++++++-------------------- toppra/interpolator.py | 35 +++++------ toppra/utils.py | 5 +- 3 files changed, 88 insertions(+), 90 deletions(-) diff --git a/.pylintrc b/.pylintrc index 1110ca94..5d8790db 100644 --- a/.pylintrc +++ b/.pylintrc @@ -54,92 +54,94 @@ confidence= # --enable=similarities". If you want to run only the classes checker, but have # no Warning level messages displayed, use"--disable=all --enable=classes # --disable=W" -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, +disable= + fixme,anomalous-backslash-in-string + len-as-condition, + no-member, + parameter-unpacking, + superfluous-parens, + too-few-public-methods, + unsubscriptable-object, + useless-object-inheritance, + apply-builtin, backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - invalid-unicode-literal, - raw-checker-failed, bad-inline-option, - locally-disabled, - locally-enabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - apply-builtin, + bad-python3-import, basestring-builtin, buffer-builtin, cmp-builtin, + cmp-method, coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, coerce-method, + comprehension-escape, delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, + deprecated-itertools-function, + deprecated-operator-function, + deprecated-pragma, + deprecated-str-translate-call, + deprecated-string-function, + deprecated-sys-function, + deprecated-types-field, + deprecated-urllib-function, + dict-items-not-iterating, dict-iter-method, + dict-keys-not-iterating, + dict-values-not-iterating, dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, + div-method, + eq-without-hash, + exception-escape, + exception-message-attribute, + execfile-builtin, + file-builtin, + file-ignored, + filter-builtin-not-iterating, + getslice-method, hex-method, - nonzero-method, - cmp-method, + idiv-method, + import-star-module-level, + indexing-exception, input-builtin, - round-builtin, intern-builtin, - unichr-builtin, + invalid-str-codec, + invalid-unicode-literal, + locally-disabled, + locally-enabled, + long-builtin, + long-suffix, map-builtin-not-iterating, - zip-builtin-not-iterating, + metaclass-assignment, + next-method-called, + next-method-defined, + no-absolute-import, + non-ascii-bytes-literal, + nonzero-method, + oct-method, + old-division, + old-ne-operator, + old-octal-literal, + old-raise-syntax, + print-statement, + raising-string, range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, + raw-checker-failed, + raw_input-builtin, rdiv-method, - exception-message-attribute, - invalid-str-codec, + reduce-builtin, + reload-builtin, + round-builtin, + setslice-method, + standarderror-builtin, + suppressed-message, sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, + unichr-builtin, + unicode-builtin, + unpacking-in-except, + useless-suppression, + using-cmp-argument, + xrange-builtin, xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - len-as-condition, - superfluous-parens, - no-member, - too-few-public-methods, - fixme,anomalous-backslash-in-string - + zip-builtin-not-iterating, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option diff --git a/toppra/interpolator.py b/toppra/interpolator.py index 368777b2..a0eb05c4 100644 --- a/toppra/interpolator.py +++ b/toppra/interpolator.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) if FOUND_OPENRAVE: - import openravepy as orpy + import openravepy as orpy # pylint: disable=import-error def propose_gridpoints(path, max_err_threshold=1e-4, max_iteration=100, max_seg_length=0.05): @@ -36,7 +36,7 @@ def propose_gridpoints(path, max_err_threshold=1e-4, max_iteration=100, max_seg_ """ gridpoints_ept = [path.path_interval[0], path.path_interval[1]] - for it in range(max_iteration): + for iteration in range(max_iteration): add_new_points = False for idx in range(len(gridpoints_ept) - 1): gp_mid = 0.5 * (gridpoints_ept[idx] + gridpoints_ept[idx + 1]) @@ -53,7 +53,7 @@ def propose_gridpoints(path, max_err_threshold=1e-4, max_iteration=100, max_seg_ gridpoints_ept = sorted(gridpoints_ept) if not add_new_points: break - if it == max_iteration - 1: + if iteration == max_iteration - 1: raise ValueError("Unable to find a good gridpoint for this path.") return gridpoints_ept @@ -169,7 +169,7 @@ def _make_ppoly(): pp_coeffs[0, 0, idof] = waypoints[0, idof] return PPoly(pp_coeffs, [0, 1]) - elif self._interpolation == "quadratic": + if self._interpolation == "quadratic": waypoints = _extract_waypoints(0) waypoints_d = _extract_waypoints(1) waypoints_dd = [] @@ -190,7 +190,7 @@ def _make_ppoly(): ] return PPoly(pp_coeffs, self.ss_waypoints) - elif self._interpolation == "cubic": + if self._interpolation == "cubic": waypoints = _extract_waypoints(0) waypoints_d = _extract_waypoints(1) waypoints_dd = _extract_waypoints(2) @@ -243,12 +243,11 @@ def dof(self): def __call__(self, ss_sam, order=0): if order == 0: return self.eval(ss_sam) - elif order == 1: + if order == 1: return self.evald(ss_sam) - elif order == 2: + if order == 2: return self.evaldd(ss_sam) - else: - raise ValueError("Order must be 0, 1 or 2.") + raise ValueError("Order must be 0, 1 or 2.") def eval(self, ss_sam): """Evalute path postition.""" @@ -296,8 +295,8 @@ class SplineInterpolator(AbstractGeometricPath): def __init__(self, ss_waypoints, waypoints, bc_type='not-a-knot'): super(SplineInterpolator, self).__init__() - self.ss_waypoints = np.array(ss_waypoints) - self.waypoints = np.array(waypoints) + self.ss_waypoints = np.array(ss_waypoints) # type: np.ndarray + self.waypoints = np.array(waypoints) # type: np.ndarray assert self.ss_waypoints.shape[0] == self.waypoints.shape[0] if len(ss_waypoints) == 1: @@ -328,12 +327,11 @@ def _1dof_cspld(s): def __call__(self, path_positions, order=0): if order == 0: return self.cspl(path_positions) - elif order == 1: + if order == 1: return self.cspld(path_positions) - elif order == 2: + if order == 2: return self.cspldd(path_positions) - else: - raise ValueError("Invalid order %s" % order) + raise ValueError("Invalid order %s" % order) def get_waypoints(self): """Return the appropriate scaled waypoints.""" @@ -547,12 +545,11 @@ def __init__(self, coeff, s_start=0.0, s_end=1.0): def __call__(self, path_positions, order=0): if order == 0: return self.eval(path_positions) - elif order == 1: + if order == 1: return self.evald(path_positions) - elif order == 2: + if order == 2: return self.evaldd(path_positions) - else: - raise ValueError("Invalid order %s" % order) + raise ValueError("Invalid order %s" % order) @property def dof(self): diff --git a/toppra/utils.py b/toppra/utils.py index 592c40a4..820b102f 100644 --- a/toppra/utils.py +++ b/toppra/utils.py @@ -118,8 +118,7 @@ def smooth_singularities(parametrization_instance, us, xs, vs=None): Notes ----- (`us_smth`, `xs_smth`) is a *valid* path-parameterization. They - satisfy the linear continuity condition :math:`x_{i+1} = x_i + 2 - \Delta_i u_i`. + satisfy the linear continuity condition :math:`x_{i+1} = x_i + 2 delta_i u_i`. This function is safe: it will always return a solution. @@ -147,7 +146,7 @@ def smooth_singularities(parametrization_instance, us, xs, vs=None): singular_indices = [] uds = np.diff(us, n=1) for i in range(parametrization_instance.N - 3): - if uds[i] < 0 and uds[i + 1] > 0 and uds[i + 2] < 0: + if uds[i] < 0 < uds[i + 1] and uds[i + 2] < 0: logger.debug("Found potential singularity at {:d}".format(i)) singular_indices.append(i) logger.debug("Found singularities at %s", singular_indices) From 516e367ec1f02b93388d52585404feecc1adecb5 Mon Sep 17 00:00:00 2001 From: Hung Date: Tue, 31 Dec 2019 23:24:24 +0800 Subject: [PATCH 35/42] add missing file and some minor bug fix --- tasks.py | 1 + tests/constraint/test_joint_acceleration.py | 38 ++++++++++++------- .../constraint/linear_joint_acceleration.py | 4 +- toppra/exceptions.py | 2 + 4 files changed, 30 insertions(+), 15 deletions(-) create mode 100644 toppra/exceptions.py diff --git a/tasks.py b/tasks.py index dbcca5a3..2126d8b7 100644 --- a/tasks.py +++ b/tasks.py @@ -54,6 +54,7 @@ def lint(c): toppra/__init__.py \ toppra/utils.py \ toppra/interpolator.py \ + toppra/exceptions.py \ ") diff --git a/tests/constraint/test_joint_acceleration.py b/tests/constraint/test_joint_acceleration.py index c20cecec..c5e3ea01 100644 --- a/tests/constraint/test_joint_acceleration.py +++ b/tests/constraint/test_joint_acceleration.py @@ -6,7 +6,7 @@ from toppra.constants import JACC_MAXU -@pytest.fixture(params=[1, 2, 6], name='acceleration_pc_data') +@pytest.fixture(params=[1, 2, 6, '6d'], name='accel_constraint_setup') def create_acceleration_pc_fixtures(request): """ Parameterized Acceleration path constraint. @@ -20,18 +20,18 @@ def create_acceleration_pc_fixtures(request): pi = ta.PolynomialPath([1, 2, 3]) # 1 + 2s + 3s^2 ss = np.linspace(0, 1, 3) alim = (np.r_[-1., 1]).reshape(1, 2) # Scalar case - pc_vel = constraint.JointAccelerationConstraint(alim) + accel_const = constraint.JointAccelerationConstraint(alim, constraint.DiscretizationType.Collocation) data = (pi, ss, alim) - return data, pc_vel + return data, accel_const if dof == 2: coeff = [[1., 2, 3], [-2., -3., 4., 5.]] pi = ta.PolynomialPath(coeff) ss = np.linspace(0, 0.75, 4) alim = np.array([[-1., 2], [-2., 2]]) - pc_vel = constraint.JointAccelerationConstraint(alim) + accel_const = constraint.JointAccelerationConstraint(alim, constraint.DiscretizationType.Collocation) data = (pi, ss, alim) - return data, pc_vel + return data, accel_const if dof == 6: np.random.seed(10) @@ -41,22 +41,34 @@ def create_acceleration_pc_fixtures(request): ss = np.linspace(0, 1, N + 1) vlim_ = np.random.rand(6) alim = np.vstack((-vlim_, vlim_)).T - pc_vel = constraint.JointAccelerationConstraint(alim) + accel_const = constraint.JointAccelerationConstraint(alim, constraint.DiscretizationType.Collocation) data = (pi, ss, alim) - return data, pc_vel + return data, accel_const + + if dof == '6d': + np.random.seed(10) + N = 20 + way_pts = np.random.randn(10, 6) + pi = ta.SplineInterpolator(np.linspace(0, 1, 10), way_pts) + ss = np.linspace(0, 1, N + 1) + alim_s = np.random.rand(6) + alim = np.vstack((-alim_s, alim_s)).T + accel_const = constraint.JointAccelerationConstraint(alim_s, constraint.DiscretizationType.Collocation) + data = (pi, ss, alim) + return data, accel_const -def test_constraint_type(acceleration_pc_data): +def test_constraint_type(accel_constraint_setup): """ Syntactic correct. """ - data, pc = acceleration_pc_data + data, pc = accel_constraint_setup assert pc.get_constraint_type() == constraint.ConstraintType.CanonicalLinear -def test_constraint_params(acceleration_pc_data): +def test_constraint_params(accel_constraint_setup): """ Test constraint satisfaction with cvxpy. """ - (path, ss, alim), accel_const = acceleration_pc_data + (path, ss, alim), accel_const = accel_constraint_setup # An user of the class a, b, c, F, g, ubound, xbound = accel_const.compute_constraint_params(path, ss) @@ -81,8 +93,8 @@ def test_constraint_params(acceleration_pc_data): assert xbound is None -def test_wrong_dimension(acceleration_pc_data): - _, path_constraint = acceleration_pc_data +def test_wrong_dimension(accel_constraint_setup): + _, path_constraint = accel_constraint_setup path_wrongdim = ta.SplineInterpolator(np.linspace(0, 1, 5), np.random.randn(5, 10)) with pytest.raises(ValueError) as e_info: path_constraint.compute_constraint_params(path_wrongdim, np.r_[0, 0.5, 1], 1.0) diff --git a/toppra/constraint/linear_joint_acceleration.py b/toppra/constraint/linear_joint_acceleration.py index f471159b..f8a50995 100644 --- a/toppra/constraint/linear_joint_acceleration.py +++ b/toppra/constraint/linear_joint_acceleration.py @@ -67,8 +67,8 @@ def compute_constraint_params(self, path, gridpoints, scaling=None): )) if scaling is None: scaling = 1 - ps_vec = path.evald(gridpoints / scaling) / scaling - pss_vec = path.evaldd(gridpoints / scaling) / scaling ** 2 + ps_vec = (path(gridpoints / scaling, order=1) / scaling).reshape((-1, path.dof)) + pss_vec = (path(gridpoints / scaling, order=2) / scaling ** 2).reshape((-1, path.dof)) dof = path.dof F_single = np.zeros((dof * 2, dof)) g_single = np.zeros(dof * 2) diff --git a/toppra/exceptions.py b/toppra/exceptions.py new file mode 100644 index 00000000..c44c39f6 --- /dev/null +++ b/toppra/exceptions.py @@ -0,0 +1,2 @@ +class ToppraError(Exception): + pass From 0689a16db52e30aa4517dcf56d2165dc0d146bc5 Mon Sep 17 00:00:00 2001 From: Hung Date: Wed, 1 Jan 2020 07:12:36 +0800 Subject: [PATCH 36/42] disable gridoint visualization during testing --- tests/interpolators/test_find_gridpoints.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/interpolators/test_find_gridpoints.py b/tests/interpolators/test_find_gridpoints.py index d5c62dd5..5a7a9f71 100644 --- a/tests/interpolators/test_find_gridpoints.py +++ b/tests/interpolators/test_find_gridpoints.py @@ -25,11 +25,11 @@ def test_basic_usage(path): # ensure a reasonable response. assert np.max(np.diff(gridpoints_ept)) < 0.05 - # visualize ############################################################### - ss_full = np.linspace(path.path_interval[0], path.path_interval[1], 100) - for i in range(len(waypoints[0])): - plt.plot(ss_full, path(ss_full)[:, i], '--', c='C%d' % i) - plt.plot(gridpoints_ept, path(gridpoints_ept)[:, i], '-o', c='C%d' % i) + # # visualize ############################################################### + # ss_full = np.linspace(path.path_interval[0], path.path_interval[1], 100) + # for i in range(len(waypoints[0])): + # plt.plot(ss_full, path(ss_full)[:, i], '--', c='C%d' % i) + # plt.plot(gridpoints_ept, path(gridpoints_ept)[:, i], '-o', c='C%d' % i) # plt.show() From 776098c1eebed9c999a60083af2e484ea044a91e Mon Sep 17 00:00:00 2001 From: Hung Date: Wed, 1 Jan 2020 07:24:43 +0800 Subject: [PATCH 37/42] some codestyle improvements --- .circleci/config.yml | 2 +- tasks.py | 6 +++++- .../desired_duration_algorithm.py | 7 +++---- .../reachabilitybased/reachability_algorithm.py | 17 +++++++++-------- toppra/exceptions.py | 5 ++++- tox.ini | 7 ++++++- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a0e1987c..fba7509c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -28,7 +28,7 @@ jobs: name: check codestyle command: | . venv3/bin/activate - make lint || echo "allow failure" + invoke lint || echo "allow failure" - store_artifacts: path: test-reports diff --git a/tasks.py b/tasks.py index 2126d8b7..0b9dc937 100644 --- a/tasks.py +++ b/tasks.py @@ -47,7 +47,7 @@ def make_venvs(c, python3=False, run_tests=False): @task -def lint(c): +def lint(c, pycodestyle=False, pydocstyle=False): """Run linting on selected source files.""" c.run("python -m pylint --rcfile=.pylintrc \ tasks.py \ @@ -56,6 +56,10 @@ def lint(c): toppra/interpolator.py \ toppra/exceptions.py \ ") + if pycodestyle: + c.run("pycodestyle toppra --max-line-length=120 --ignore=E731,W503,W605") + if pydocstyle: + c.run("pydocstyle toppra") @task diff --git a/toppra/algorithm/reachabilitybased/desired_duration_algorithm.py b/toppra/algorithm/reachabilitybased/desired_duration_algorithm.py index 9bb73b9d..4b8c48b6 100644 --- a/toppra/algorithm/reachabilitybased/desired_duration_algorithm.py +++ b/toppra/algorithm/reachabilitybased/desired_duration_algorithm.py @@ -110,7 +110,7 @@ def compute_parameterization(self, sd_start, sd_end, return_data=False, atol=1e- # numerical errors might violate this condition. xs[i + 1] = min(K[i + 1, 1], max(K[i + 1, 0], xs[i] + 2 * deltas[i] * us[i] - SMALL)) v_vec[i] = optim_res[2:] - logger.debug("[Forward pass] u_{:d} = {:f}, x_{:d} = {:f}".format(i, us[i], i+1, xs[i+1])) + logger.debug("[Forward pass] u_{:d} = {:f}, x_{:d} = {:f}".format(i, us[i], i + 1, xs[i + 1])) self.solver_wrapper.close_solver() # compute the slowest parametrization xs_slow = np.zeros(N + 1) @@ -134,7 +134,7 @@ def compute_parameterization(self, sd_start, sd_end, return_data=False, atol=1e- # numerical errors might violate this condition. xs_slow[i + 1] = min(K[i + 1, 1], max(K[i + 1, 0], xs_slow[i] + 2 * deltas[i] * us_slow[i] - SMALL)) v_vec_slow[i] = optim_res[2:] - logger.debug("[Forward pass] u_{:d} = {:f}, x_{:d} = {:f}".format(i, us_slow[i], i+1, xs_slow[i+1])) + logger.debug("[Forward pass] u_{:d} = {:f}, x_{:d} = {:f}".format(i, us_slow[i], i + 1, xs_slow[i + 1])) self.solver_wrapper.close_solver() # desired parametrization xs_desired = alpha * xs + (1 - alpha) / xs_slow @@ -172,7 +172,7 @@ def compute_parameterization(self, sd_start, sd_end, return_data=False, atol=1e- xs_alpha = alpha * xs + (1 - alpha) * xs_slow us_alpha = alpha * us + (1 - alpha) * us_slow v_vec_alpha = alpha * v_vec + (1 - alpha) * v_vec_slow - + sd_vec = np.sqrt(xs_alpha) sdd_vec = np.copy(us_alpha) if return_data: @@ -222,4 +222,3 @@ def _forward_step(self, i, x, K_next, slow=False): optim_var = self.solver_wrapper.solve_stagewise_optim( i, None, g_upper, x, x, K_next_min, K_next_max) return optim_var - diff --git a/toppra/algorithm/reachabilitybased/reachability_algorithm.py b/toppra/algorithm/reachabilitybased/reachability_algorithm.py index 1e01dba1..6363af37 100644 --- a/toppra/algorithm/reachabilitybased/reachability_algorithm.py +++ b/toppra/algorithm/reachabilitybased/reachability_algorithm.py @@ -18,7 +18,7 @@ class ReachabilityAlgorithm(ParameterizationAlgorithm): constraint_list: List[:class:`~toppra.constraint.Constraint`] List of constraints on the robot dynamics. path: Interpolator - + gridpoints: np.ndarray, optional Shape (N+1,). Gridpoints for discretization of the path position. solver_wrapper: str, optional @@ -110,9 +110,11 @@ def __init__(self, constraint_list, path, gridpoints=None, solver_wrapper=None, # Check solver-wrapper suitability if has_conic: - assert solver_wrapper.lower() in ['cvxpy', 'ecos'], "Problem has conic constraints, solver {:} is not suitable".format(solver_wrapper) + assert solver_wrapper.lower() in ['cvxpy', 'ecos'], \ + "Problem has conic constraints, solver {:} is not suitable".format(solver_wrapper) else: - assert solver_wrapper.lower() in ['cvxpy', 'qpoases', 'ecos', 'hotqpoases', 'seidel'], "Solver {:} not found".format(solver_wrapper) + assert solver_wrapper.lower() in ['cvxpy', 'qpoases', 'ecos', 'hotqpoases', 'seidel'], \ + "Solver {:} not found".format(solver_wrapper) if solver_wrapper.lower() == "cvxpy": from toppra.solverwrapper.cvxpy_solverwrapper import cvxpyWrapper @@ -346,7 +348,6 @@ def compute_parameterization(self, sd_start, sd_end, return_data=False): else: return sdd_vec, sd_vec, v_vec - def _one_step_forward(self, i, L_current, feasible_set_next): res = np.zeros(2) if np.isnan(L_current).any() or i < 0 or i > self._N: @@ -382,14 +383,14 @@ def compute_reachable_sets(self, sdmin, sdmax): logger.debug("Start computing the reachable sets") self.solver_wrapper.setup_solver() for i in range(0, self._N): - L[i + 1] = self._one_step_forward(i, L[i], feasible_sets[i+1]) + L[i + 1] = self._one_step_forward(i, L[i], feasible_sets[i + 1]) if L[i + 1, 0] < 0: L[i + 1, 0] = 0 if np.isnan(L[i + 1]).any(): - logger.warn("L[{:d}]={:}. Path not parametrizable.".format(i+1, L[i+1])) + logger.warn("L[{:d}]={:}. Path not parametrizable.".format(i + 1, L[i + 1])) return L if logger.isEnabledFor(logging.DEBUG): - logger.debug("[Compute reachable sets] L_{:d}={:}".format(i+1, L[i+1])) - + logger.debug("[Compute reachable sets] L_{:d}={:}".format(i + 1, L[i + 1])) + self.solver_wrapper.close_solver() return L diff --git a/toppra/exceptions.py b/toppra/exceptions.py index c44c39f6..7f35aba7 100644 --- a/toppra/exceptions.py +++ b/toppra/exceptions.py @@ -1,2 +1,5 @@ +"""Exceptions used in the toppra library.""" + + class ToppraError(Exception): - pass + """A generic error class used in the toppra library.""" diff --git a/tox.ini b/tox.ini index f32ca75c..5e7dbe87 100644 --- a/tox.ini +++ b/tox.ini @@ -14,4 +14,9 @@ deps = commands = invoke install-solvers pip install -e .[dev] - pytest -x \ No newline at end of file + pytest -x + +[pycodestyle] +ignore = E731,W503,W605 +max-line-length = 120 +statistics = True \ No newline at end of file From 62a537ed5dd33f42ad57e683e0227997c7600e42 Mon Sep 17 00:00:00 2001 From: Hung Date: Wed, 1 Jan 2020 07:32:40 +0800 Subject: [PATCH 38/42] some further codestyle improvement --- tasks.py | 1 + toppra/solverwrapper/solverwrapper.py | 12 +++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tasks.py b/tasks.py index 0b9dc937..f16f5941 100644 --- a/tasks.py +++ b/tasks.py @@ -56,6 +56,7 @@ def lint(c, pycodestyle=False, pydocstyle=False): toppra/interpolator.py \ toppra/exceptions.py \ ") + # toppra/solverwrapper/solverwrapper.py if pycodestyle: c.run("pycodestyle toppra --max-line-length=120 --ignore=E731,W503,W605") if pydocstyle: diff --git a/toppra/solverwrapper/solverwrapper.py b/toppra/solverwrapper/solverwrapper.py index 598ccb6a..64a879d2 100644 --- a/toppra/solverwrapper/solverwrapper.py +++ b/toppra/solverwrapper/solverwrapper.py @@ -1,9 +1,11 @@ +"""This module provides different solverwrapper implementations.""" import logging import numpy as np logger = logging.getLogger(__name__) +# pylint: disable=unused-import def available_solvers(output_msg=True): """Check for available solvers.""" try: @@ -22,11 +24,11 @@ def available_solvers(output_msg=True): except ImportError as err: IMPORT_CVXPY = False solver_availability = ( - ('seidel',True), - ('hotqpoases',IMPORT_QPOASES), - ('qpoases',IMPORT_QPOASES), - ('ecos',IMPORT_ECOS), - ('cvxpy',IMPORT_CVXPY) + ('seidel', True), + ('hotqpoases', IMPORT_QPOASES), + ('qpoases', IMPORT_QPOASES), + ('ecos', IMPORT_ECOS), + ('cvxpy', IMPORT_CVXPY) ) if output_msg: From 6291aad8758cd9a3e21cd2f598b436ecfce07930 Mon Sep 17 00:00:00 2001 From: Hung Date: Wed, 1 Jan 2020 12:18:04 +0800 Subject: [PATCH 39/42] add intro in document --- README.md | 12 +++---- docs/source/index.rst | 84 +++++++++++++++++++++++++++---------------- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index df3695fa..277e1218 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# `toppra` +# `TOPP-RA` [![CircleCI](https://circleci.com/gh/hungpham2511/toppra/tree/develop.svg?style=svg)](https://circleci.com/gh/hungpham2511/toppra/tree/develop) [![Coverage Status](https://coveralls.io/repos/github/hungpham2511/toppra/badge.svg?branch=master)](https://coveralls.io/github/hungpham2511/toppra?branch=master) [![Documentation Status](https://readthedocs.org/projects/toppra/badge/?version=latest)](https://toppra.readthedocs.io/en/latest/?badge=latest) -**toppra** is a library for computing the time-optimal path parametrization for robots subject to kinematic and dynamic constraints. +**TOPP-RA** is a library for computing the time-optimal path parametrization for robots subject to kinematic and dynamic constraints. In general, given the inputs: 1. a geometric path `p(s)`, `s` in `[0, s_end]` ; 2. a list of constraints on joint velocity, joint accelerations, tool Cartesian velocity, et cetera. -**toppra** returns the time-optimal path parameterization: `s_dot (s)`, from which the fastest trajectory `q(t)` that satisfies the given +**TOPP-RA** returns the time-optimal path parameterization: `s_dot (s)`, from which the fastest trajectory `q(t)` that satisfies the given constraints can be found. **Documentation and tutorials** are available at (https://toppra.readthedocs.io/en/latest/index.html). @@ -17,7 +17,7 @@ constraints can be found. ## Quick-start -To install **toppra**, simply clone the repo and install with pip +To install **TOPP-RA**, simply clone the repo and install with pip ``` shell git clone https://github.com/hungpham2511/toppra @@ -29,7 +29,7 @@ To install depencidencies for development, replace the second command with: cd toppra && pip install -e .[dev] ``` -## Citing **toppra** +## Citing **TOPP-RA** If you use this library for your research, we encourage you to 1. reference the accompanying paper [«A new approach to Time-Optimal Path Parameterization based on Reachability Analysis»](https://www.researchgate.net/publication/318671280_A_New_Approach_to_Time-Optimal_Path_Parameterization_Based_on_Reachability_Analysis), *IEEE Transactions on Robotics*, vol. 34(3), pp. 645–659, 2018. @@ -46,4 +46,4 @@ with the issue report. ## Contributions -Pull Requests are welcomed! Go ahead and create a Pull Request and we will review your proposal! For new features, or bug fixes, preferably the request should contain unit tests. Note that **toppra** uses [pytest](https://docs.pytest.org/en/latest/contents.html) for all tests. Check out the test folder for more details. +Pull Requests are welcomed! Go ahead and create a Pull Request and we will review your proposal! For new features, or bug fixes, preferably the request should contain unit tests. Note that **TOPP-RA** uses [pytest](https://docs.pytest.org/en/latest/contents.html) for all tests. Check out the test folder for more details. diff --git a/docs/source/index.rst b/docs/source/index.rst index 6988d97b..6c48a5e2 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,31 +3,46 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -TOPP-RA: Fast path-parameterization for robots +**TOPP-RA**: Path-parameterization for robots =================================================== -TOPP-RA is a library for computing `path parameterizations` of -arbitrary geometric paths/trajectories for robots! -TOPP-RA can account for several types of constraints on robot -dynamics: +.. image:: https://circleci.com/gh/hungpham2511/toppra/tree/develop.svg?style=svg + :target: https://circleci.com/gh/hungpham2511/toppra/tree/develop -1. joint torque, velocity and acceleration bounds; -2. *robust* joint torque, velocity and acceleration bounds; -3. Cartesian acceleration bounds; -4. contact stability for legged robots. -5. Your constraint! See the tutorials to understand how to implement - your own constraints and handle them with TOPP-RA. +.. image:: https://readthedocs.org/projects/toppra/badge/?version=latest + :target: https://toppra.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status -You can use TOPP-RA to compute the *time-optimal* path -parameterization -- the fastest possible movement the robot can -realize -- or a path parameterization with *a specified duration*. See -the tutorials for more details. +**TOPP-RA** is a library for computing the time-optimal path +parametrization for robots subject to kinematic and dynamic +constraints. In general, given the inputs: -A last remark, TOPP-RA is efficient. For standard use cases, it should -return a solution in 5ms-10ms. +1. a geometric path `p(s)`, `s` in `[0, s_end]` ; +2. a list of constraints on joint velocity, joint accelerations, tool + Cartesian velocity, et cetera. -**Update (Feb, 2019)** I have used TOPP-RA to plan *critically fast* +**TOPP-RA** returns the time-optimal path parameterization: `s_dot +(s)`, from which the fastest trajectory `q(t)` that satisfies the +given constraints can be found. All of this is done in a few +milliseconds. + +Features +--------- + +1. Return the time-optimal parametrization or a parametrization with + specified duration subject to constraints. +2. Able to handle multiple constraint types: + 1. joint torque, velocity and acceleration bounds; + 2. *robust* joint torque, velocity and acceleration bounds; + 3. Cartesian acceleration bounds; + 4. contact stability for legged robots. +3. Automatic grid-points selection. + +Applications +------------ + +**(Feb, 2019)** TOPP-RA was used to plan *critically fast* motions for robots doing bin picking with suction cup. Here *critically fast* motions are those that are fastest possible given the limited suction power and object weight. See the video below for @@ -38,23 +53,19 @@ more detail! -If you find this interesting, feel free to check out the paper: `«Critically fast pick-and-place with suction cups» `_. This paper will be presented at ICRA 2019. Feel free to come over to our poster for a chat! +If you find this interesting, feel free to check out the paper: +`«Critically fast pick-and-place with suction cups» +`_. This +paper has been presented at ICRA 2019. + +User Guide +---------- You can find on this page :ref:`installation`, :ref:`tutorials`, some :ref:`notes` and :ref:`module_ref`. -.. toctree:: - :hidden: - :maxdepth: 3 - - installation - tutorials - notes - FAQs - modules - Citing TOPP-RA! -~~~~~~~~~~~~~~~~ +---------------- If you find TOPP-RA useful and use it in your research, we encourage you to @@ -63,5 +74,16 @@ you to Bug reports and supports -~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------- Please report any issues, questions via `Github issues tracker `_. + +.. toctree:: + :hidden: + :maxdepth: 3 + + installation + tutorials + notes + FAQs + modules + From ea0a513101fa6b6a8142456daf4179e251d17366 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 25 Jan 2020 13:36:39 +0800 Subject: [PATCH 40/42] fix regex --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fba7509c..7238fe6b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -159,7 +159,7 @@ workflows: branches: only: - fix-ci - - release-* + - /release-.*/ - release: filters: branches: From 9ab277ad8a89d2d2aa07dd4e4c4a26d9258551b6 Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 25 Jan 2020 13:38:16 +0800 Subject: [PATCH 41/42] bump version --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 434a3874..373f8c6f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2b \ No newline at end of file +0.2.3 \ No newline at end of file From edaa85e0bb9d60b6e6002e94f67824c834dd904c Mon Sep 17 00:00:00 2001 From: Hung Date: Sat, 25 Jan 2020 13:39:52 +0800 Subject: [PATCH 42/42] minor change in CI desp. --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7238fe6b..6e2ebcbd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -134,7 +134,7 @@ jobs: steps: - checkout - run: - name: Pull all known tags and check that the tag in VERSION has not already exist. + name: Tag code with the latest version in VERSION and push tag. command: | VERSION=`cat VERSION` git fetch --tags