diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index a90fcd2c..06b81e92 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -9,45 +9,8 @@ on: jobs: - package: - name: Package the project - runs-on: ubuntu-22.04 - - steps: - - - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: "3.10" - - - name: Install Python tools - run: pip install build twine - - - name: Create distributions - run: python -m build -o dist/ - - - name: Inspect dist folder - run: ls -lah dist/ - - - name: Check wheel's abi and platform tags - run: test $(find dist/ -name *-none-any.whl | wc -l) -gt 0 - - - name: Run twine check - run: twine check dist/* - - - name: Upload artifacts - uses: actions/upload-artifact@v3 - with: - path: dist/* - name: dist - test: name: 'Python${{ matrix.python }}@${{ matrix.os }}' - needs: package runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -62,20 +25,24 @@ jobs: steps: - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python }} + - uses: actions/checkout@v2 - - name: Download Python packages - uses: actions/download-artifact@v3 + - uses: conda-incubator/setup-miniconda@v2 with: - path: dist - name: dist - - - name: Install wheel - shell: bash - run: pip install dist/*.whl - - - name: Import the package - run: python -c "import hippopt" + miniforge-variant: Mambaforge + miniforge-version: latest + + - name: Dependencies + shell: bash -l {0} + run: | + mamba install python=${{ matrix.python }} casadi pytest + + - name: Install + shell: bash -l {0} + run: | + pip install --no-deps -e .[all] + + - name: Test + shell: bash -l {0} + run: | + pytest diff --git a/README.md b/README.md index 5673f586..3a917313 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # hippopt -### HIghly Pythonized Planning and OPTimization framework - +### HIgh Performance* Planning and OPTimization framework hippopt is an open-source framework for generating whole-body trajectories for legged robots, with a focus on direct transcription of optimal control problems solved with multiple-shooting methods. The framework takes as input the robot model and generates optimized trajectories that include both kinematic and dynamic quantities. +*supposedly + ## Features - [ ] Direct transcription of optimal control problems with multiple-shooting methods @@ -13,8 +14,11 @@ hippopt is an open-source framework for generating whole-body trajectories for l - [ ] Extensive documentation and examples to help you get started ## Installation - -TODO +It is suggested to use [``conda``](https://docs.conda.io/en/latest/). +```bash +conda install -c conda-forge casadi pytest +pip install --no-deps -e .[all] +``` ## Citing this work @@ -41,4 +45,3 @@ This repository is maintained by: | | | | :----------------------------------------------------------: | :--------------------------------------------------: | | [](https://github.com/S-Dafarra) | [@S-Dafarra](https://github.com/S-Dafarra) | - diff --git a/pyproject.toml b/pyproject.toml index 884d3a71..0bba479c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,3 +15,10 @@ line-length = 88 [tool.isort] profile = "black" multi_line_output = 3 + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-ra -q" +testpaths = [ + "test", +] diff --git a/setup.cfg b/setup.cfg index 1e34fae3..fb353f48 100644 --- a/setup.cfg +++ b/setup.cfg @@ -50,13 +50,18 @@ package_dir = =src python_requires = >=3.10 install_requires = + casadi + numpy [options.extras_require] style = black isort +testing= + pytest all = %(style)s + %(testing)s [options.packages.find] where = src diff --git a/src/hippopt/__init__.py b/src/hippopt/__init__.py index e69de29b..927b4b4f 100644 --- a/src/hippopt/__init__.py +++ b/src/hippopt/__init__.py @@ -0,0 +1,9 @@ +from . import base +from .base.optimization_object import ( + OptimizationObject, + StorageType, + TOptimizationObject, + default_storage_field, +) +from .base.parameter import Parameter, TParameter +from .base.variable import TVariable, Variable diff --git a/src/hippopt/base/__init__.py b/src/hippopt/base/__init__.py new file mode 100644 index 00000000..f2a23186 --- /dev/null +++ b/src/hippopt/base/__init__.py @@ -0,0 +1 @@ +from . import optimization_object, parameter, variable diff --git a/src/hippopt/base/optimization_object.py b/src/hippopt/base/optimization_object.py new file mode 100644 index 00000000..4d75da95 --- /dev/null +++ b/src/hippopt/base/optimization_object.py @@ -0,0 +1,59 @@ +import abc +import copy +import dataclasses +from typing import Any, ClassVar, Type, TypeVar + +import casadi as cs +import numpy as np + +TOptimizationObject = TypeVar("TOptimizationObject", bound="OptimizationObject") +StorageType = cs.MX | np.ndarray + + +@dataclasses.dataclass +class OptimizationObject(abc.ABC): + StorageType: ClassVar[str] = "generic" + StorageTypeMetadata: ClassVar[dict[str, Any]] = dict(StorageType=StorageType) + + def get_default_initialization( + self: TOptimizationObject, field_name: str + ) -> np.ndarray: + """ + Get the default initialization of a given field + It is supposed to be called only for the fields having the StorageType metadata + """ + return np.zeros(dataclasses.asdict(self)[field_name].shape) + + def get_default_initialized_object( + self: TOptimizationObject, + ) -> TOptimizationObject: + """ + :return: A copy of the object with its initial values + """ + + output = copy.deepcopy(self) + output_dict = dataclasses.asdict(output) + + for field in dataclasses.fields(output): + if "StorageType" in field.metadata: + output.__setattr__( + field.name, output.get_default_initialization(field.name) + ) + continue + + if isinstance(output.__getattribute__(field.name), OptimizationObject): + output.__setattr__( + field.name, + output.__getattribute__( + field.name + ).get_default_initialized_object(), + ) + + return output + + +def default_storage_field(cls: Type[OptimizationObject]): + return dataclasses.field( + default=None, + metadata=cls.StorageTypeMetadata, + ) diff --git a/src/hippopt/base/parameter.py b/src/hippopt/base/parameter.py new file mode 100644 index 00000000..6d1db6a0 --- /dev/null +++ b/src/hippopt/base/parameter.py @@ -0,0 +1,14 @@ +import dataclasses +from typing import Any, ClassVar, TypeVar + +from hippopt.base.optimization_object import OptimizationObject + +TParameter = TypeVar("TParameter", bound="Parameter") + + +@dataclasses.dataclass +class Parameter(OptimizationObject): + """""" + + StorageType: ClassVar[str] = "parameter" + StorageTypeMetadata: ClassVar[dict[str, Any]] = dict(StorageType=StorageType) diff --git a/src/hippopt/base/problem.py b/src/hippopt/base/problem.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/base/solver.py b/src/hippopt/base/solver.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/base/variable.py b/src/hippopt/base/variable.py new file mode 100644 index 00000000..64912985 --- /dev/null +++ b/src/hippopt/base/variable.py @@ -0,0 +1,14 @@ +import dataclasses +from typing import Any, ClassVar, TypeVar + +from hippopt.base.optimization_object import OptimizationObject + +TVariable = TypeVar("TVariable", bound="Variable") + + +@dataclasses.dataclass +class Variable(OptimizationObject): + """""" + + StorageType: ClassVar[str] = "variable" + StorageTypeMetadata: ClassVar[dict[str, Any]] = dict(StorageType=StorageType) diff --git a/src/hippopt/integrators/__init__.py b/src/hippopt/integrators/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/multiple_shooting/__init__.py b/src/hippopt/multiple_shooting/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/multiple_shooting/builder.py b/src/hippopt/multiple_shooting/builder.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/multiple_shooting/discretized_variable.py b/src/hippopt/multiple_shooting/discretized_variable.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/robot_planning/dynamics/__init__.py b/src/hippopt/robot_planning/dynamics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/robot_planning/expressions/__init__.py b/src/hippopt/robot_planning/expressions/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/robot_planning/utilities/__init__.py b/src/hippopt/robot_planning/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/hippopt/robot_planning/variables/__init__.py b/src/hippopt/robot_planning/variables/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/test/test_base.py b/test/test_base.py new file mode 100644 index 00000000..8c7dc394 --- /dev/null +++ b/test/test_base.py @@ -0,0 +1,93 @@ +import dataclasses + +import numpy as np + +from hippopt import ( + OptimizationObject, + Parameter, + StorageType, + TOptimizationObject, + Variable, + default_storage_field, +) + + +@dataclasses.dataclass +class TestVariable(OptimizationObject): + storage: StorageType = default_storage_field(cls=Variable) + + def __post_init__(self): + self.storage = np.ones(shape=3) + + +@dataclasses.dataclass +class TestParameter(OptimizationObject): + storage: StorageType = default_storage_field(cls=Parameter) + + def __post_init__(self): + self.storage = np.ones(shape=3) + + +def test_zero_variable(): + test_var = TestVariable() + test_var_zero = test_var.get_default_initialized_object() + assert test_var_zero.storage.shape == (3,) + assert np.all(test_var_zero.storage == 0) + + +def test_zero_parameter(): + test_par = TestParameter() + test_par_zero = test_par.get_default_initialized_object() + assert test_par_zero.storage.shape == (3,) + assert np.all(test_par_zero.storage == 0) + + +@dataclasses.dataclass +class CustomInitializationVariable(OptimizationObject): + variable: StorageType = default_storage_field(cls=Variable) + parameter: StorageType = default_storage_field(cls=Parameter) + + def __post_init__(self): + self.variable = np.ones(shape=3) + self.parameter = np.ones(shape=3) + + def get_default_initialization( + self: TOptimizationObject, field_name: str + ) -> np.ndarray: + if field_name == "variable": + return 2 * np.ones(2) + + return OptimizationObject.get_default_initialization(self, field_name) + + +def test_custom_initialization(): + test_var = CustomInitializationVariable() + test_var_init = test_var.get_default_initialized_object() + assert test_var_init.parameter.shape == (3,) + assert np.all(test_var_init.parameter == 0) + assert test_var_init.variable.shape == (2,) + assert np.all(test_var_init.variable == 2) + + +@dataclasses.dataclass +class AggregateClass(OptimizationObject): + aggregated: CustomInitializationVariable + other_parameter: StorageType = default_storage_field(cls=Parameter) + other: str = "" + + def __post_init__(self): + self.aggregated = CustomInitializationVariable() + self.other_parameter = np.ones(3) + self.other = "untouched" + + +def test_aggregated(): + test_var = AggregateClass(aggregated=CustomInitializationVariable()) + test_var_init = test_var.get_default_initialized_object() + assert test_var_init.aggregated.parameter.shape == (3,) + assert np.all(test_var_init.aggregated.parameter == 0) + assert test_var_init.aggregated.variable.shape == (2,) + assert np.all(test_var_init.aggregated.variable == 2) + assert test_var_init.other_parameter.shape == (3,) + assert np.all(test_var_init.other_parameter == 0) + assert test_var_init.other == "untouched"