├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── PPOPT_CI.yml ├── .gitignore ├── .readthedocs.yaml ├── CODE_OF_CONDUCT.md ├── Figures ├── README.md ├── bench.png ├── loglog_scaling.png └── scaleing_eff.png ├── LICENSE ├── README.md ├── doc ├── Makefile ├── _build │ └── doctrees │ │ ├── algorithm_overview.doctree │ │ ├── environment.pickle │ │ ├── index.doctree │ │ ├── modules.doctree │ │ ├── ppopt.doctree │ │ ├── ppopt.geometry.doctree │ │ ├── ppopt.mp_solvers.doctree │ │ ├── ppopt.solver_interface.doctree │ │ ├── ppopt.upop.doctree │ │ ├── ppopt.utils.doctree │ │ └── tutorial.doctree ├── algorithm_overview.rst ├── conf.py ├── control_allocation_example.rst ├── controller_gain.rst ├── flex.svg ├── flexibility.rst ├── index.rst ├── make.bat ├── modules.rst ├── mpc.rst ├── mplp_tut.rst ├── mpmpc_sens_plot.svg ├── mpmpc_sol_plot.svg ├── multiobjective.rst ├── port.svg ├── port_comp.svg ├── portfolio.rst ├── ppopt.geometry.rst ├── ppopt.mp_solvers.rst ├── ppopt.rst ├── ppopt.solver_interface.rst ├── ppopt.upop.rst ├── ppopt.utils.rst ├── requirements.txt ├── risk_return_port.svg ├── roll-pitch_error_contour.svg ├── roll-pitch_error_whisker.svg ├── rotor_geometry.svg ├── transport.svg ├── transport_mplp.svg └── tutorial.rst ├── environment.yml ├── mp_plot.png ├── requirements.txt ├── ruff.toml ├── setup.py ├── src ├── PPOPT.egg-info │ ├── PKG-INFO │ ├── SOURCES.txt │ ├── dependency_links.txt │ ├── requires.txt │ └── top_level.txt ├── __init__.py ├── ppopt │ ├── __init__.py │ ├── critical_region.py │ ├── geometry │ │ ├── __init__.py │ │ ├── polytope.py │ │ └── polytope_operations.py │ ├── mp_solvers │ │ ├── __init__.py │ │ ├── mitree.py │ │ ├── mpmiqp_enumeration.py │ │ ├── mpqp_ahmadi.py │ │ ├── mpqp_combi_graph.py │ │ ├── mpqp_combinatorial.py │ │ ├── mpqp_dist_combinatorial.py │ │ ├── mpqp_geometric.py │ │ ├── mpqp_graph.py │ │ ├── mpqp_parallel_combinatorial_exp.py │ │ ├── mpqp_parallel_geometric.py │ │ ├── mpqp_parallel_geometric_exp.py │ │ ├── mpqp_parrallel_combinatorial.py │ │ ├── mpqp_parrallel_graph.py │ │ ├── solve_mplp.py │ │ ├── solve_mpmiqp.py │ │ ├── solve_mpqp.py │ │ └── solver_utils.py │ ├── mplp_program.py │ ├── mpmilp_program.py │ ├── mpmiqp_program.py │ ├── mpmodel.py │ ├── mpqp_program.py │ ├── plot.py │ ├── problem_generator.py │ ├── solution.py │ ├── solver.py │ ├── solver_interface │ │ ├── __init__.py │ │ ├── cvxopt_interface.py │ │ ├── daqp_solver_interface.py │ │ ├── gurobi_solver_interface.py │ │ ├── quad_prog_interface.py │ │ ├── solver_interface.py │ │ └── solver_interface_utils.py │ ├── upop │ │ ├── __init__.py │ │ ├── language_generation.py │ │ ├── lib_upop │ │ │ ├── non_python_code │ │ │ │ ├── cpp │ │ │ │ │ └── upop_cpp.h │ │ │ │ └── ts │ │ │ │ │ └── upop_web.ts │ │ │ ├── upop_cpp_template.py │ │ │ └── upop_js_template.py │ │ ├── linear_code_gen.py │ │ ├── point_location.py │ │ ├── ucontroller.py │ │ └── upop_utils.py │ └── utils │ │ ├── __init__.py │ │ ├── chebyshev_ball.py │ │ ├── constraint_utilities.py │ │ ├── general_utils.py │ │ ├── geometric.py │ │ ├── mpqp_utils.py │ │ └── region_overlap_utils.py └── requires.txt └── tests ├── __init__.py ├── geometry_tests ├── __init__.py └── test_polytope.py ├── mpmiqp_solver_tests ├── __init__.py └── test_mpmiqp.py ├── mpmodel_test ├── __init__.py └── test_mpmodel.py ├── mpqp_solver_tests ├── __init__.py ├── test_mpqp_combinatorial.py └── test_solution.py ├── other_tests ├── __init__.py ├── test_chebyshev_ball.py ├── test_constraint_utilities.py ├── test_critical_region.py ├── test_general_utils.py ├── test_mp_program.py ├── test_mpqp_utils.py ├── test_plot.py ├── test_problem_generator.py └── test_solve_mpqp.py ├── simple_fixtures └── __init__.py ├── solver_interface_tests ├── __init__.py ├── test_glpk_solver.py ├── test_gurobi_solver.py ├── test_solver.py ├── test_solver_consistency.py └── test_solver_interface.py ├── test_fixtures.py └── upop_tests ├── __init__.py ├── test_language_generation.py ├── test_linear_code_generation.py ├── test_point_location.py └── test_upop.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Python Version [e.g. 3.10] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/PPOPT_CI.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a single version of Python 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: PPOPT CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | paths-ignore: 10 | - 'doc/**' 11 | - 'README.md' 12 | - 'LICENSE' 13 | - '*.yml' 14 | pull_request: 15 | branches: [ main ] 16 | paths-ignore: 17 | - 'doc/**' 18 | - 'README.md' 19 | - 'LICENSE' 20 | - '*.yml' 21 | 22 | jobs: 23 | build: 24 | strategy: 25 | matrix: 26 | os: [ubuntu-latest, windows-latest] 27 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 28 | 29 | runs-on: ${{ matrix.os }} 30 | 31 | steps: 32 | - uses: actions/checkout@v2 33 | with: 34 | persist-credentials: false 35 | fetch-depth: 0 36 | 37 | - uses: conda-incubator/setup-miniconda@v2 38 | with: 39 | activate-environment: PPOPT_ENV 40 | python-version: ${{ matrix.python-version }} 41 | 42 | - name: Test conda installation 43 | shell: bash -l {0} 44 | run: conda info 45 | 46 | - name: Install Packages 47 | shell: bash -l {0} 48 | run: pip install flake8 numpy matplotlib scipy numba gurobipy pytest pathos plotly daqp 49 | 50 | - name: Install glpk 51 | shell: bash -l {0} 52 | run: conda install -c conda-forge cvxopt 53 | 54 | - name: Install Quadprog 55 | shell: bash -l {0} 56 | run: pip install git+https://github.com/HKaras/quadprog/ 57 | 58 | - name: Run tests 59 | shell: bash -l {0} 60 | run: pytest --disable-pytest-warnings 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .pytest_cache/ 3 | 4 | # compiled Python code 5 | __pycache__ -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | sphinx: 4 | configuration: doc/conf.py 5 | 6 | conda: 7 | environment: environment.yml 8 | 9 | build: 10 | os: "ubuntu-20.04" 11 | tools: 12 | python: "mambaforge-4.10" 13 | apt_packages: 14 | - gcc 15 | - libgmp3-dev 16 | 17 | python: 18 | install: 19 | - method: pip 20 | path: . 21 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /Figures/README.md: -------------------------------------------------------------------------------- 1 | # Figures ReadMe 2 | 3 | This folder simply holds the figures used in the main readme.md 4 | -------------------------------------------------------------------------------- /Figures/bench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/Figures/bench.png -------------------------------------------------------------------------------- /Figures/loglog_scaling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/Figures/loglog_scaling.png -------------------------------------------------------------------------------- /Figures/scaleing_eff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/Figures/scaleing_eff.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Dustin Kenefake, Efstratios N. Pistikopoulos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PPOPT 2 | 3 | [![Python package](https://github.com/TAMUparametric/PPOPT/actions/workflows/PPOPT_CI.yml/badge.svg)](https://github.com/TAMUparametric/PPOPT/actions/workflows/PPOPT_CI.yml) 4 | [![Documentation Status](https://readthedocs.org/projects/ppopt/badge/?version=latest)](https://ppopt.readthedocs.io/en/latest/?badge=latest) 5 | [![PyPI](https://img.shields.io/pypi/v/ppopt.svg)](https://pypi.org/project/ppopt) 6 | [![Downloads](https://static.pepy.tech/personalized-badge/ppopt?period=total&units=none&left_color=grey&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/ppopt) 7 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a7df65fcf0104c2ab7fd0105f10854c6)](https://app.codacy.com/gh/TAMUparametric/PPOPT/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 8 | 9 | **P**ython **P**arametric **OP**timization **T**oolbox (**PPOPT**) is a software platform for solving and manipulating 10 | multiparametric programs in Python. 11 | 12 | ## Installation 13 | 14 | Currently, PPOPT requires Python 3.8 or higher and can be installed with the following commands. 15 | 16 | ```bash 17 | pip install ppopt 18 | ``` 19 | 20 | To install PPOPT and install all optional solvers the following installation is recommended. 21 | 22 | ```bash 23 | pip install ppopt[optional] 24 | ``` 25 | 26 | In Python 3.11 and beyond there is currently an error with the quadprog package. An alternate version that fixed this error can be installed here. 27 | 28 | ```bash 29 | pip install git+https://github.com/HKaras/quadprog/ 30 | ``` 31 | 32 | ## Completed Features 33 | 34 | - Solver interface for mpLPs and mpQPs with the following algorithms 35 | 1. Serial and Parallel Combinatorial Algorithms 36 | 2. Serial and Parallel Geometrical Algorithms 37 | 3. Serial and Parallel Graph based Algorithms 38 | - Solver interface for mpMILPs and mpMIQPs with the following algorithms 39 | 1. Enumeration based algorithm 40 | - Multiparametric solution export to C++, JavaScript, and Python 41 | - Plotting utilities 42 | - Presolver and Conditioning for Multiparametric Programs 43 | 44 | ## Key Applications 45 | 46 | - Explicit Model Predictive Control 47 | - Multilevel Optimization 48 | - Integrated Design, Control, and Scheduling 49 | - Robust Optimization 50 | 51 | For more information about Multiparametric programming and it's 52 | applications, [this paper](https://www.frontiersin.org/articles/10.3389/fceng.2020.620168/full) is a good jumping point. 53 | 54 | ## Quick Overview 55 | 56 | To give a fast primer of what we are doing, we are solving multiparametric programming problems (fast) by writing 57 | parallel algorithms efficiently. Here is a quick scaling analysis on a large multiparametric program with the 58 | combinatorial algorithm. 59 | 60 | ![image](https://github.com/TAMUparametric/PPOPT/blob/main/Figures/loglog_scaling.png?raw=true) 61 | ![image](https://github.com/TAMUparametric/PPOPT/blob/main/Figures/scaleing_eff.png?raw=true) 62 | 63 | Here is a benchmark against the state of the art multiparametric programming solvers. All tests run on the Terra 64 | Supercomputer at Texas A&M University. Matlab 2021b was used for solvers written in matlab and Python 3.8 was used for 65 | PPOPT. 66 | 67 | ![image](https://github.com/TAMUparametric/PPOPT/blob/main/Figures/bench.png?raw=true) 68 | 69 | ## Citation 70 | 71 | Since a lot of time and effort has gone into PPOPT's development, please cite the following publication if you are using 72 | PPOPT for your own research. 73 | 74 | ```text 75 | @incollection{kenefake2022ppopt, 76 | title={PPOPT-Multiparametric Solver for Explicit MPC}, 77 | author={Kenefake, Dustin and Pistikopoulos, Efstratios N}, 78 | booktitle={Computer Aided Chemical Engineering}, 79 | volume={51}, 80 | pages={1273--1278}, 81 | year={2022}, 82 | publisher={Elsevier} 83 | } 84 | ``` 85 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/_build/doctrees/algorithm_overview.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/algorithm_overview.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/environment.pickle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/environment.pickle -------------------------------------------------------------------------------- /doc/_build/doctrees/index.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/index.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/modules.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/modules.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/ppopt.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/ppopt.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/ppopt.geometry.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/ppopt.geometry.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/ppopt.mp_solvers.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/ppopt.mp_solvers.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/ppopt.solver_interface.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/ppopt.solver_interface.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/ppopt.upop.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/ppopt.upop.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/ppopt.utils.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/ppopt.utils.doctree -------------------------------------------------------------------------------- /doc/_build/doctrees/tutorial.doctree: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/_build/doctrees/tutorial.doctree -------------------------------------------------------------------------------- /doc/algorithm_overview.rst: -------------------------------------------------------------------------------- 1 | Multiparametric Algorithms 2 | ========================== 3 | This section will give a basic overview of the algorithms used in this package for solving the multiparametric programming problems. All current algorithms depend on exploring the theta space of the problem, choosing differing ways to do so. 4 | 5 | 6 | Geometric Algorithm 7 | ------------------- 8 | The Geometric algorithm is based on exploring the feasible space. The name of the algorithm comes from the fact that it is geometrically exploring the space by flipping critical region facets. 9 | 10 | This is best used in situations where the number of theta dimensions is small and scales well with number of variables and constraints. 11 | 12 | Combinatorial Algorithm 13 | ----------------------- 14 | 15 | The combinatorial algorithm is based on exploring feasible active set combinations. It is called the combinatorial algorithm due the fact that it explores the combinatorial tree of active set combinations. This is the most robust multiparametric algorithm as it handles both primal and dual degeneracy and should always fully solve the multiparametric programming problem. 16 | 17 | This is best used in situations where the number of constraints and variables is small and scales well with number of theta dimensions. 18 | 19 | Graph Algorithm 20 | --------------- 21 | The graph algorithm is based on exploring the connected graph of active set combinations. This can be viewed as a combination certain aspects of the Geometric algorithm and the Combinatorial algorithm. 22 | 23 | This is a general purpose algorithm that scales ok with number of variables, constraints, and parameters. However this algorithm has been shown to be unstable in that it fails to fully solve some multiparametric programs with poor conditioning. 24 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | import sphinx_rtd_theme 16 | 17 | for x in os.walk("../src"): 18 | sys.path.insert(0, x[0]) 19 | 20 | sys.path.insert(0, os.path.abspath(".")) 21 | sys.path.insert(0, os.path.abspath("../src/ppopt/*")) 22 | sys.path.insert(0, os.path.abspath("../")) 23 | sys.setrecursionlimit(10000) 24 | 25 | # -- Project information ----------------------------------------------------- 26 | 27 | project = "PPOPT" 28 | copyright = "2021, Dustin Kenefake, Efstratios Pistikopoulos" 29 | author = "Dustin Kenefake, Efstratios Pistikopoulos" 30 | 31 | # The full version, including alpha/beta/rc tags 32 | release = "1.6.0" 33 | 34 | # -- General configuration --------------------------------------------------- 35 | 36 | # Add any Sphinx extension module names here, as strings. They can be 37 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 38 | # ones. 39 | extensions = ["sphinx.ext.autodoc", "sphinx_rtd_theme", "m2r2", "nbsphinx"] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ["_templates"] 43 | 44 | # List of patterns, relative to source directory, that match files and 45 | # directories to ignore when looking for source files. 46 | # This pattern also affects html_static_path and html_extra_path. 47 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 48 | 49 | # -- Options for HTML output ------------------------------------------------- 50 | 51 | # The theme to use for HTML and HTML Help pages. See the documentation for 52 | # a list of builtin themes. 53 | # 54 | html_theme = "sphinx_rtd_theme" 55 | 56 | # Add any paths that contain custom static files (such as style sheets) here, 57 | # relative to this directory. They are copied after the builtin static files, 58 | # so a file named "default.css" will overwrite the builtin "default.css". 59 | html_static_path = ["_static"] 60 | source_suffix = {".rst": 'restructuredtext', ".md": 'markdown'} 61 | nbsphinx_execute = "never" 62 | -------------------------------------------------------------------------------- /doc/controller_gain.rst: -------------------------------------------------------------------------------- 1 | Maximal Controller Gain 2 | ======================= 3 | 4 | In some optimal control applications it can be beneficial to not just solve the optimal control problem, but also have a bound on the controller gain to verify robustness of a controller. In this example we will show how to compute the maximum gain of an MPC using PPOPT. Here we will use the an example 10.4.1 from the book 'Multi-Parametric Optimization and Control' by Pistikopoulos, Diangelakis, and Oberdieck. 5 | 6 | The maximum gain of a controller can be seen as the :math:`\kappa_p`, where :math:`\theta_1` and :math:`\theta_0` can be seen as realizations different process states, and :math:`u_0(\theta_1)` and :math:`u_0(\theta_0)` are the corresponding initial control actions that are applied at the first time stage of each MPC. 7 | 8 | .. math:: 9 | || u_0(\theta_1) - u_0(\theta_0)||_p \leq \kappa_p ||\theta_1 - \theta_0||_p 10 | 11 | The mathematical description of the controller that we are looking at today can be seen below. This is based on a state space model, with constraints on state, inputs, operational constraints, and a terminal set constraint. This considers 10 time steps into the future. 12 | 13 | .. math:: 14 | 15 | \begin{align} 16 | \min_{u, x} \quad & x_{10}^T\begin{bmatrix}2.6235 & 1.6296\\ 1.6296 & 2.6457\end{bmatrix}x_{10} + \sum_{k=0}^9\left(x_k^Tx_k + 0.01u_k^2\right)\\ 17 | \text{s.t. }x_{k+1} &= \begin{bmatrix}1 & 1 \\ 0 & 1\end{bmatrix}x_k + \begin{bmatrix}0 \\ 1\end{bmatrix}u_k, \forall k \in [0,9]\\ 18 | &-25\leq \begin{bmatrix}1 & 2 \end{bmatrix}x_k \leq 25, \forall k \in [0,10]\\ 19 | &-1 \leq u_k \leq 1, \forall k \in [0,9]\\ 20 | &\begin{bmatrix} 21 | -10 \\ -10 22 | \end{bmatrix} \leq u_k \leq \begin{bmatrix} 23 | 10 \\ 10 24 | \end{bmatrix}, \forall k \in [0,10]\\ 25 | &\begin{bmatrix} 26 | 0.6136 & 1.6099\\ 27 | -.3742 & -0.3682\\ 28 | -0.6136 & -1.6099\\ 29 | .3742 & 0.3682\\ 30 | \end{bmatrix}x_{10} \leq \begin{bmatrix} 31 | 1 \\ 1 \\ 1 \\ 1 32 | \end{bmatrix} 33 | \end{align} 34 | 35 | This multiparametric program can be modeled in PPOPT, with the ``MPModeler`` interface. 36 | 37 | .. code:: python 38 | 39 | import numpy 40 | from ppopt.mpmodel import MPModeler 41 | 42 | num_x = 2 43 | N = 10 44 | 45 | m = MPModeler() 46 | 47 | 48 | # stabalizing terminal weight 49 | P = numpy.array([[2.6235, 1.6296],[1.6296, 2.6457]]) 50 | 51 | # add state and input variables to the model 52 | u = [m.add_var(f'u_[{t}]') for t in range(N-1)] 53 | x = [[m.add_var(f'x_[{t},{i}]') for i in range(num_x)] for t in range(N)] 54 | 55 | # add initital state params to model 56 | x_0 = [m.add_param(f'x_0[{i}]') for i in range(num_x)] 57 | 58 | # give bounds to the input actions 59 | m.add_constrs(u[k] <= 1 for k in range(N-1)) 60 | m.add_constrs(u[k] >= -1 for k in range(N-1)) 61 | 62 | # give bounds to the states 63 | m.add_constrs(x[k][nx] <= 10 for k in range(N) for nx in range(num_x)) 64 | m.add_constrs(x[k][nx] >= -10 for k in range(N) for nx in range(num_x)) 65 | 66 | # operational bounds 67 | m.add_constrs(x[k][0] + 2*x[k][1] <= 25 for k in range(N)) 68 | m.add_constrs(x[k][0] + 2*x[k][1] >= -25 for k in range(N)) 69 | 70 | # enforce initial state 71 | m.add_constrs(x_0[nx] == x[0][nx] for nx in range(num_x)) 72 | 73 | # enforce the state space model 74 | m.add_constrs(x[k+1][0] == x[k][0] + x[k][1] for k in range(N-1)) 75 | m.add_constrs(x[k+1][1] == x[k][1] + u[k] for k in range(N-1)) 76 | 77 | # terminal constraint set 78 | m.add_constr(0.6136*x[-1][0] + 1.6099*x[-1][1] <= 1) 79 | m.add_constr(-0.3742*x[-1][0] - 0.3682*x[-1][1] <= 1) 80 | m.add_constr(-0.6136*x[-1][0] - 1.6099*x[-1][1] <= 1) 81 | m.add_constr(0.3742*x[-1][0] + 0.3682*x[-1][1] <= 1) 82 | 83 | 84 | # declare the objective 85 | terminal_objective = sum(P[i,j]*x[-1][i]*x[-1][j] for i in range(num_x) for j in range(num_x)) 86 | m.set_objective(sum(x_t[0]**2 + x_t[1]**2 for x_t in x) + 0.01*sum(u_t**2 for u_t in u) + terminal_objective) 87 | 88 | # generate the mpp from the symbolic definition 89 | prog = m.formulate_problem() 90 | 91 | Now that the problem is formulated, we can solve it, we are going to be using one of the parallel graph algorithms to solve this problem. 92 | 93 | .. code:: python 94 | 95 | from ppopt.mp_solvers.solve_mpqp import solve_mpqp, mpqp_algorithm 96 | 97 | sol = solve_mpqp(prog, mpqp_algorithm.graph_parallel_exp) 98 | 99 | 100 | With the explicit solution now in hand, we can evaluate the the gain of the controller. It was shown in 'On the maximal controller gain in linear MPC' by Darun et al, that if we have a explicit solution that is a continuous piecewise affine function which is true for an mpMPC based on mpQP, then we can compute :math:`\kappa_p` in a rather simple way. 101 | 102 | .. math:: 103 | 104 | \begin{align} 105 | u_0(\theta) &= \begin{cases} 106 | A_0\theta + b_0 \text{ if } \theta \in \Theta_0\\ 107 | \dots\\\ 108 | A_J\theta + b_J\text{ if } \theta \in \Theta_J 109 | \end{cases}\\ 110 | \kappa_p &= \max_{j\in [0,\dots,J]}||A_j||_p 111 | \end{align} 112 | 113 | Implementing this in code, we take the piece of the explicit solution relating to :math:`u_0(\theta)`, which here is just taking the row from the explicit solution relating to the initial input action. We can then compute :math:`\kappa_1`, which is equal to 1.61, directly from the explicit solution. Other :math:`\kappa_p` values can be computed by changing the norm that we are taking in the max function. 114 | 115 | .. code:: python 116 | 117 | # get the index of the variable from the modeler 118 | idx = [idx for idx, v in enumerate(m.variables) if "u_[0]" == v.name] 119 | kappa_1 = max(numpy.linalg.norm(cr.A[idx],1), for cr in sol.critical_regions) 120 | 121 | -------------------------------------------------------------------------------- /doc/flexibility.rst: -------------------------------------------------------------------------------- 1 | Flexibility Analysis 2 | ==================== 3 | 4 | In this example we will show how multiparametric programming can be used to solve a flexibility problem using PPOPT. This example is from 'Active Constraint Strategy For Flexible Analysis in Chemical Processes' by Grossmann and Floudas. This is in effect a particular instance of using multiparametric programming to solve a bilevel optimization problem. 5 | 6 | The general statement of Flexibility Analysis can be stated as follows. Where we are trying to minimize the maximum values of our :math:`f_j` constraints. Where if :math:`\chi \leq 0`, then the process is flexible in that it can adapt without violating any operational constraints for any realization of uncertainty that we are considering. If this statement is not true, then there is a realization of uncertainty that the system cannot mitigate with the control actions :math:`z`, and lead to operational violations. 7 | 8 | .. math:: 9 | 10 | \begin{align} 11 | \Psi &= \min_{z}\max_{j\in\mathcal{J}}\{\dots, f_j, \dots \}\\ 12 | \chi &= \max_\theta \Psi(\theta) 13 | \end{align} 14 | 15 | The lower level min-max problem can be reformulated into the following mathematical problem, where we have introduced an auxiliary variable :math:`u`. Where if for every realization of uncertainty :math:`\theta`, there are a control variables :math:`z` that allows us to satisfy :math:`u \leq 0` then we can say a process is flexible to this uncertainty. 16 | 17 | .. math:: 18 | 19 | \begin{align} 20 | \Psi(\theta) = \quad \min_{z,u} &\quad u\\ 21 | \text{s.t.} \quad f_j(z,\theta) \leq &u, \quad \forall j \in \mathcal{J}\\ 22 | \theta &\in \Theta 23 | \end{align} 24 | 25 | For the problem we are looking at here, our problem takes the following form. We have three constraints, one control variable :math:`z`, and one uncertainty :math:`\theta`. 26 | 27 | .. math:: 28 | \begin{align} 29 | \Psi = \min_{z,u} u\\ 30 | \text{s.t.} \quad z - u&\leq \theta\\ 31 | -z -u &\leq- \frac{4}{3} + \frac{1}{3}\theta\\ 32 | z -u &\leq 4 -\theta\\ 33 | -\theta &\leq 0\\ 34 | \theta &\leq 4 35 | \end{align} 36 | 37 | Using the ``MPModeler`` interface we can write the multiparametric program. 38 | 39 | .. code:: python 40 | 41 | from ppopt.mpmodel import MPModeler 42 | 43 | # make a mpp modeler 44 | m = MPModeler() 45 | 46 | # add variables and parameter 47 | u = m.add_var(name='u') 48 | z = m.add_var(name='z') 49 | t = m.add_param(name='t') 50 | 51 | # add operability constraints 52 | m.add_constr(z - u <= t) 53 | m.add_constr(-z - u <= -4.0/3.0 + t/3) 54 | m.add_constr(z - u <= 4 - t) 55 | 56 | # add constraints on the uncertainty 57 | m.add_constr(t <= 4) 58 | m.add_constr(t >= 0) 59 | 60 | # set objective 61 | m.set_objective(u) 62 | 63 | # formulate the model 64 | prob = m.formulate_problem() 65 | 66 | We can now solve the formulated multiparametric program, here we will solve it with the geometric algorithm. This will allow us to generate the explicit representation of the flexibility function with respect to :math:`\theta`. 67 | 68 | .. code:: python 69 | 70 | from ppopt.mp_solvers.solve_mpqp import solve_mpqp, mpqp_algorithm 71 | 72 | sol = solve_mpqp(prob, mpqp_algorithm.geometric) 73 | 74 | Now we can plot the explicit solution flexibility function. We can visually see here that this process is NOT flexible to the entire range of uncertainty, as for some :math:`\theta` realizations it is above zero. 75 | 76 | .. code:: python 77 | 78 | from src.ppopt.plot import parametric_plot_1D 79 | 80 | parametric_plot_1D(sol, legend=[r'$\Psi^*(\theta)$'], plot_subset=[0]) 81 | 82 | .. image:: flex.svg 83 | 84 | However, this is not generally a good way to validate that the process is flexible for the entire range of uncertainty. Here what we can do is find the maximum of the objective function over the explicit solution with the following code, which will give us the exact value of :math:`\Psi`. If the maximum value of :math:`\Psi > 0`, then we know that the process is NOT flexible for the entire range of uncertainty. As a note, this code is specialized for the one parameter case but multidimensional generalization of this are direct. 85 | 86 | .. code:: python 87 | 88 | from ppopt.utils.mpqp_utils import get_bounds_1d 89 | import numpy 90 | 91 | def get_max_obj_1d(sol, cr) -> float: 92 | # find the lower and upper bounds of the region 93 | min_theta, max_theta = get_bounds_1d(cr.E, cr.f) 94 | 95 | # find the objective at the bounds 96 | J_min = sol.evaluate_objective(numpy.array([[max_theta]])) 97 | J_max = sol.evaluate_objective(numpy.array([[min_theta]])) 98 | 99 | # return the largest objective 100 | return max(J_max, J_min) 101 | 102 | # find the largest objective (e.g. u) over the uncertainty space 103 | chi = max(map(lambda x: get_max_obj_1d(sol, x), sol.critical_regions)) 104 | 105 | If we run this code, we get that it evaluates to :math:`\chi = \frac{2}{3}`, meaning that the process is not flexible for the entire range of uncertainty. 106 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. PPOPT documentation master file, created by Dustin Kenefake 2 | 3 | .. mdinclude:: ../README.md 4 | 5 | 6 | Tutorial 7 | -------- 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Overview 12 | 13 | algorithm_overview 14 | tutorial 15 | mplp_tut 16 | control_allocation_example 17 | portfolio 18 | mpc 19 | controller_gain 20 | flexibility 21 | 22 | 23 | API 24 | --- 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | :caption: API 29 | 30 | ppopt 31 | 32 | 33 | Indices and tables 34 | ================== 35 | 36 | * :ref:`genindex` 37 | * :ref:`modindex` 38 | * :ref:`search` 39 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /doc/modules.rst: -------------------------------------------------------------------------------- 1 | ppopt 2 | ===== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | ppopt 8 | -------------------------------------------------------------------------------- /doc/multiobjective.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/doc/multiobjective.rst -------------------------------------------------------------------------------- /doc/ppopt.geometry.rst: -------------------------------------------------------------------------------- 1 | ppopt.geometry package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | ppopt.geometry.polytope module 8 | ------------------------------ 9 | 10 | .. automodule:: ppopt.geometry.polytope 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ppopt.geometry.polytope\_operations module 16 | ------------------------------------------ 17 | 18 | .. automodule:: ppopt.geometry.polytope_operations 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: ppopt.geometry 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /doc/ppopt.mp_solvers.rst: -------------------------------------------------------------------------------- 1 | ppopt.mp\_solvers package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | ppopt.mp\_solvers.mpqp\_ahmadi module 8 | ------------------------------------- 9 | 10 | .. automodule:: ppopt.mp_solvers.mpqp_ahmadi 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ppopt.mp\_solvers.mpqp\_combinatorial module 16 | -------------------------------------------- 17 | 18 | .. automodule:: ppopt.mp_solvers.mpqp_combinatorial 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | ppopt.mp\_solvers.mpqp\_geometric module 24 | ---------------------------------------- 25 | 26 | .. automodule:: ppopt.mp_solvers.mpqp_geometric 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | ppopt.mp\_solvers.mpqp\_graph module 32 | ------------------------------------ 33 | 34 | .. automodule:: ppopt.mp_solvers.mpqp_graph 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | ppopt.mp\_solvers.mpqp\_parallel\_geometric module 40 | -------------------------------------------------- 41 | 42 | .. automodule:: ppopt.mp_solvers.mpqp_parallel_geometric 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | ppopt.mp\_solvers.mpqp\_parallel\_geometric\_exp module 48 | ------------------------------------------------------- 49 | 50 | .. automodule:: ppopt.mp_solvers.mpqp_parallel_geometric_exp 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | 55 | ppopt.mp\_solvers.mpqp\_parrallel\_combinatorial module 56 | ------------------------------------------------------- 57 | 58 | .. automodule:: ppopt.mp_solvers.mpqp_parrallel_combinatorial 59 | :members: 60 | :undoc-members: 61 | :show-inheritance: 62 | 63 | ppopt.mp\_solvers.mpqp\_parrallel\_combinatorial\_exp module 64 | ------------------------------------------------------------ 65 | 66 | .. automodule:: ppopt.mp_solvers.mpqp_parallel_combinatorial_exp 67 | :members: 68 | :undoc-members: 69 | :show-inheritance: 70 | 71 | ppopt.mp\_solvers.mpqp\_parrallel\_graph module 72 | ----------------------------------------------- 73 | 74 | .. automodule:: ppopt.mp_solvers.mpqp_parrallel_graph 75 | :members: 76 | :undoc-members: 77 | :show-inheritance: 78 | 79 | ppopt.mp\_solvers.solve\_mplp module 80 | ------------------------------------ 81 | 82 | .. automodule:: ppopt.mp_solvers.solve_mplp 83 | :members: 84 | :undoc-members: 85 | :show-inheritance: 86 | 87 | ppopt.mp\_solvers.solve\_mpqp module 88 | ------------------------------------ 89 | 90 | .. automodule:: ppopt.mp_solvers.solve_mpqp 91 | :members: 92 | :undoc-members: 93 | :show-inheritance: 94 | 95 | ppopt.mp\_solvers.solver\_utils module 96 | -------------------------------------- 97 | 98 | .. automodule:: ppopt.mp_solvers.solver_utils 99 | :members: 100 | :undoc-members: 101 | :show-inheritance: 102 | 103 | Module contents 104 | --------------- 105 | 106 | .. automodule:: ppopt.mp_solvers 107 | :members: 108 | :undoc-members: 109 | :show-inheritance: 110 | -------------------------------------------------------------------------------- /doc/ppopt.rst: -------------------------------------------------------------------------------- 1 | ppopt package 2 | ============= 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | ppopt.geometry 11 | ppopt.mp_solvers 12 | ppopt.solver_interface 13 | ppopt.upop 14 | ppopt.utils 15 | 16 | Submodules 17 | ---------- 18 | 19 | ppopt.critical\_region module 20 | ----------------------------- 21 | 22 | .. automodule:: ppopt.critical_region 23 | :members: 24 | :undoc-members: 25 | :show-inheritance: 26 | 27 | ppopt.mplp\_program module 28 | -------------------------- 29 | 30 | .. automodule:: ppopt.mplp_program 31 | :members: 32 | :undoc-members: 33 | :show-inheritance: 34 | 35 | ppopt.mpqp\_program module 36 | -------------------------- 37 | 38 | .. automodule:: ppopt.mpqp_program 39 | :members: 40 | :undoc-members: 41 | :show-inheritance: 42 | 43 | ppopt.plot module 44 | ----------------- 45 | 46 | .. automodule:: ppopt.plot 47 | :members: 48 | :undoc-members: 49 | :show-inheritance: 50 | 51 | ppopt.problem\_generator module 52 | ------------------------------- 53 | 54 | .. automodule:: ppopt.problem_generator 55 | :members: 56 | :undoc-members: 57 | :show-inheritance: 58 | 59 | ppopt.solution module 60 | --------------------- 61 | 62 | .. automodule:: ppopt.solution 63 | :members: 64 | :undoc-members: 65 | :show-inheritance: 66 | 67 | ppopt.solver module 68 | ------------------- 69 | 70 | .. automodule:: ppopt.solver 71 | :members: 72 | :undoc-members: 73 | :show-inheritance: 74 | 75 | Module contents 76 | --------------- 77 | 78 | .. automodule:: ppopt 79 | :members: 80 | :undoc-members: 81 | :show-inheritance: 82 | -------------------------------------------------------------------------------- /doc/ppopt.solver_interface.rst: -------------------------------------------------------------------------------- 1 | ppopt.solver\_interface package 2 | =============================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | ppopt.solver\_interface.cvxopt\_interface module 8 | ------------------------------------------------ 9 | 10 | .. automodule:: ppopt.solver_interface.cvxopt_interface 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ppopt.solver\_interface.gurobi\_solver\_interface module 16 | -------------------------------------------------------- 17 | 18 | .. automodule:: ppopt.solver_interface.gurobi_solver_interface 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | ppopt.solver\_interface.quad\_prog\_interface module 24 | ---------------------------------------------------- 25 | 26 | .. automodule:: ppopt.solver_interface.quad_prog_interface 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | ppopt.solver\_interface.solver\_interface module 32 | ------------------------------------------------ 33 | 34 | .. automodule:: ppopt.solver_interface.solver_interface 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | ppopt.solver\_interface.solver\_interface\_utils module 40 | ------------------------------------------------------- 41 | 42 | .. automodule:: ppopt.solver_interface.solver_interface_utils 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: ppopt.solver_interface 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /doc/ppopt.upop.rst: -------------------------------------------------------------------------------- 1 | ppopt.upop package 2 | ================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | ppopt.upop.language\_generation module 8 | -------------------------------------- 9 | 10 | .. automodule:: ppopt.upop.language_generation 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ppopt.upop.linear\_code\_gen module 16 | ----------------------------------- 17 | 18 | .. automodule:: ppopt.upop.linear_code_gen 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | ppopt.upop.point\_location module 24 | --------------------------------- 25 | 26 | .. automodule:: ppopt.upop.point_location 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | ppopt.upop.ucontroller module 32 | ----------------------------- 33 | 34 | .. automodule:: ppopt.upop.ucontroller 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | ppopt.upop.upop\_utils module 40 | ----------------------------- 41 | 42 | .. automodule:: ppopt.upop.upop_utils 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: ppopt.upop 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /doc/ppopt.utils.rst: -------------------------------------------------------------------------------- 1 | ppopt.utils package 2 | =================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | ppopt.utils.chebyshev\_ball module 8 | ---------------------------------- 9 | 10 | .. automodule:: ppopt.utils.chebyshev_ball 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | ppopt.utils.constraint\_utilities module 16 | ---------------------------------------- 17 | 18 | .. automodule:: ppopt.utils.constraint_utilities 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | ppopt.utils.general\_utils module 24 | --------------------------------- 25 | 26 | .. automodule:: ppopt.utils.general_utils 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | ppopt.utils.geometric module 32 | ---------------------------- 33 | 34 | .. automodule:: ppopt.utils.geometric 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | ppopt.utils.mpqp\_utils module 40 | ------------------------------ 41 | 42 | .. automodule:: ppopt.utils.mpqp_utils 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | 47 | Module contents 48 | --------------- 49 | 50 | .. automodule:: ppopt.utils 51 | :members: 52 | :undoc-members: 53 | :show-inheritance: 54 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | m2r2 2 | sphinx_mdinclude 3 | sphinx-rtd-theme 4 | nbsphinx 5 | ipykernel 6 | ipython 7 | lxml[html_clean] 8 | pandoc -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: PPOPT 2 | channels: 3 | - conda-forge 4 | - defaults 5 | - http://conda.anaconda.org/gurobi 6 | dependencies: 7 | - python=3.8 8 | - pip: 9 | - m2r2 10 | - numpy 11 | - matplotlib 12 | - scipy 13 | - numba 14 | - gurobipy 15 | - pytest 16 | - setuptools 17 | - psutil 18 | - pathos 19 | - plotly 20 | - cvxopt 21 | - quadprog 22 | - sphinx_rtd_theme 23 | - sphinx_mdinclude 24 | - nbsphinx 25 | - ipykernel 26 | - ipython 27 | - daqp 28 | - lxml[html_clean] 29 | - pandoc 30 | prefix: /home/docs/.conda/envs/PPOPT 31 | -------------------------------------------------------------------------------- /mp_plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/mp_plot.png -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | scipy 4 | numba 5 | gurobipy 6 | pathos 7 | plotly 8 | daqp -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | select = ["F", "E", "W", "I", "NPY", "S", "B", "BLE", "A", "COM", "C4", "T10", "RET", "ARG", "PL", "TRY", "RUF"] 2 | ignore = ["E501", "RET505", "E731", "B905", "PLR0913", "ARG001", "TRY003"] 3 | exclude = ['test', '.github', 'build', 'dist', 'doc', 'Figure'] 4 | 5 | [per-file-ignores] 6 | 7 | [mccabe] 8 | max-complexity = 10 9 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | __version__ = "1.6.12" 4 | 5 | short_desc = ( 6 | "Extensible Multiparametric Solver in Python" 7 | ) 8 | 9 | with open('README.md') as f: 10 | long_description = f.read() 11 | 12 | setup( 13 | name='ppopt', 14 | version=__version__, 15 | author='Dustin R. Kenefake', 16 | author_email='Dustin.Kenefake@tamu.edu', 17 | description=short_desc, 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | license='MIT', 21 | url='https://github.com/TAMUparametric/PPOPT', 22 | extras_require={ 23 | 'test': ['pytest', 'cvxopt', 'quadprog'], 24 | 'optional': ['cvxopt', 'quadprog'], 25 | }, 26 | install_requires=["numpy", 27 | "matplotlib", 28 | "scipy", 29 | "numba", 30 | "gurobipy", 31 | "pathos", 32 | "plotly", 33 | "daqp"], 34 | packages=find_packages(where='src'), 35 | package_dir={'': 'src'}, 36 | ) 37 | -------------------------------------------------------------------------------- /src/PPOPT.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 2.1 2 | Name: ppopt 3 | Version: 1.5.1 4 | Summary: Extensible Multiparametric Solver in Python 5 | Home-page: https://github.com/TAMUparametric/PPOPT 6 | Author: Dustin R. Kenefake 7 | Author-email: Dustin.Kenefake@tamu.edu 8 | License: MIT 9 | Description-Content-Type: text/markdown 10 | License-File: LICENSE 11 | Requires-Dist: numpy 12 | Requires-Dist: matplotlib 13 | Requires-Dist: scipy 14 | Requires-Dist: numba 15 | Requires-Dist: gurobipy 16 | Requires-Dist: pytest 17 | Requires-Dist: setuptools 18 | Requires-Dist: psutil 19 | Requires-Dist: pathos 20 | Requires-Dist: plotly 21 | Requires-Dist: daqp 22 | Provides-Extra: test 23 | Requires-Dist: pytest; extra == "test" 24 | Provides-Extra: optional 25 | Requires-Dist: cvxopt; extra == "optional" 26 | Requires-Dist: quadprog; extra == "optional" 27 | 28 | # PPOPT 29 | 30 | [![Python package](https://github.com/TAMUparametric/PPOPT/actions/workflows/python-package.yml/badge.svg)](https://github.com/TAMUparametric/PPOPT/actions/workflows/python-package.yml) 31 | [![Documentation Status](https://readthedocs.org/projects/ppopt/badge/?version=latest)](https://ppopt.readthedocs.io/en/latest/?badge=latest) 32 | [![PyPI](https://img.shields.io/pypi/v/ppopt.svg)](https://pypi.org/project/ppopt) 33 | [![Downloads](https://static.pepy.tech/personalized-badge/ppopt?period=total&units=none&left_color=grey&right_color=brightgreen&left_text=Downloads)](https://pepy.tech/project/ppopt) 34 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a7df65fcf0104c2ab7fd0105f10854c6)](https://app.codacy.com/gh/TAMUparametric/PPOPT/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) 35 | 36 | **P**ython **P**arametric **OP**timization **T**oolbox (**PPOPT**) is a software platform for solving and manipulating 37 | multiparametric programs in Python. 38 | 39 | ## Installation 40 | 41 | Currently, PPOPT requires Python 3.7 or higher and can be installed with the following commands. 42 | 43 | ```bash 44 | pip install ppopt 45 | ``` 46 | 47 | To install PPOPT and install all optional solvers the following installation is recommended. 48 | 49 | ```bash 50 | pip install ppopt[optional] 51 | ``` 52 | 53 | In Python 3.11 and beyond there is currently an error with the quadprog package. An alternate version that fixed this error can be installed here. 54 | 55 | ```bash 56 | pip install git+https://github.com/HKaras/quadprog/ 57 | ``` 58 | 59 | ## Completed Features 60 | 61 | - Solver interface for mpLPs and mpQPs with the following algorithms 62 | 1. Serial and Parallel Combinatorial Algorithms 63 | 2. Serial and Parallel Geometrical Algorithms 64 | 3. Serial and Parallel Graph based Algorithms 65 | - Solver interface for mpMILPs and mpMIQPs with the following algorithms 66 | 1. Enumeration based algorithm 67 | - Multiparametric solution export to C++, JavaScript, and Python 68 | - Plotting utilities 69 | - Presolver and Conditioning for Multiparametric Programs 70 | 71 | ## Key Applications 72 | 73 | - Explicit Model Predictive Control 74 | - Multilevel Optimization 75 | - Integrated Design, Control, and Scheduling 76 | - Robust Optimization 77 | 78 | For more information about Multiparametric programming and it's 79 | applications, [this paper](https://www.frontiersin.org/articles/10.3389/fceng.2020.620168/full) is a good jumping point. 80 | 81 | ## Quick Overview 82 | 83 | To give a fast primer of what we are doing, we are solving multiparametric programming problems (fast) by writing 84 | parallel algorithms efficiently. Here is a quick scaling analysis on a large multiparametric program with the 85 | combinatorial algorithm. 86 | 87 | ![image](https://github.com/TAMUparametric/PPOPT/blob/main/Figures/loglog_scaling.png?raw=true) 88 | ![image](https://github.com/TAMUparametric/PPOPT/blob/main/Figures/scaleing_eff.png?raw=true) 89 | 90 | Here is a benchmark against the state of the art multiparametric programming solvers. All tests run on the Terra 91 | Supercomputer at Texas A&M University. Matlab 2021b was used for solvers written in matlab and Python 3.8 was used for 92 | PPOPT. 93 | 94 | ![image](https://github.com/TAMUparametric/PPOPT/blob/main/Figures/bench.png?raw=true) 95 | 96 | ## Citation 97 | 98 | Since a lot of time and effort has gone into PPOPT's development, please cite the following publication if you are using 99 | PPOPT for your own research. 100 | 101 | ```text 102 | @incollection{kenefake2022ppopt, 103 | title={PPOPT-Multiparametric Solver for Explicit MPC}, 104 | author={Kenefake, Dustin and Pistikopoulos, Efstratios N}, 105 | booktitle={Computer Aided Chemical Engineering}, 106 | volume={51}, 107 | pages={1273--1278}, 108 | year={2022}, 109 | publisher={Elsevier} 110 | } 111 | ``` 112 | -------------------------------------------------------------------------------- /src/PPOPT.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | LICENSE 2 | README.md 3 | setup.py 4 | src/ppopt/__init__.py 5 | src/ppopt/critical_region.py 6 | src/ppopt/mplp_program.py 7 | src/ppopt/mpmilp_program.py 8 | src/ppopt/mpmiqp_program.py 9 | src/ppopt/mpqp_program.py 10 | src/ppopt/plot.py 11 | src/ppopt/problem_generator.py 12 | src/ppopt/solution.py 13 | src/ppopt/solver.py 14 | src/ppopt.egg-info/PKG-INFO 15 | src/ppopt.egg-info/SOURCES.txt 16 | src/ppopt.egg-info/dependency_links.txt 17 | src/ppopt.egg-info/requires.txt 18 | src/ppopt.egg-info/top_level.txt 19 | src/ppopt/geometry/__init__.py 20 | src/ppopt/geometry/polytope.py 21 | src/ppopt/geometry/polytope_operations.py 22 | src/ppopt/mp_solvers/__init__.py 23 | src/ppopt/mp_solvers/mitree.py 24 | src/ppopt/mp_solvers/mpmiqp_enumeration.py 25 | src/ppopt/mp_solvers/mpqp_ahmadi.py 26 | src/ppopt/mp_solvers/mpqp_combi_graph.py 27 | src/ppopt/mp_solvers/mpqp_combinatorial.py 28 | src/ppopt/mp_solvers/mpqp_dist_combinatorial.py 29 | src/ppopt/mp_solvers/mpqp_geometric.py 30 | src/ppopt/mp_solvers/mpqp_graph.py 31 | src/ppopt/mp_solvers/mpqp_parallel_combinatorial_exp.py 32 | src/ppopt/mp_solvers/mpqp_parallel_geometric.py 33 | src/ppopt/mp_solvers/mpqp_parallel_geometric_exp.py 34 | src/ppopt/mp_solvers/mpqp_parrallel_combinatorial.py 35 | src/ppopt/mp_solvers/mpqp_parrallel_graph.py 36 | src/ppopt/mp_solvers/solve_mplp.py 37 | src/ppopt/mp_solvers/solve_mpmiqp.py 38 | src/ppopt/mp_solvers/solve_mpqp.py 39 | src/ppopt/mp_solvers/solver_utils.py 40 | src/ppopt/solver_interface/__init__.py 41 | src/ppopt/solver_interface/cvxopt_interface.py 42 | src/ppopt/solver_interface/daqp_solver_interface.py 43 | src/ppopt/solver_interface/gurobi_solver_interface.py 44 | src/ppopt/solver_interface/quad_prog_interface.py 45 | src/ppopt/solver_interface/solver_interface.py 46 | src/ppopt/solver_interface/solver_interface_utils.py 47 | src/ppopt/upop/__init__.py 48 | src/ppopt/upop/language_generation.py 49 | src/ppopt/upop/linear_code_gen.py 50 | src/ppopt/upop/point_location.py 51 | src/ppopt/upop/ucontroller.py 52 | src/ppopt/upop/upop_utils.py 53 | src/ppopt/utils/__init__.py 54 | src/ppopt/utils/chebyshev_ball.py 55 | src/ppopt/utils/constraint_utilities.py 56 | src/ppopt/utils/general_utils.py 57 | src/ppopt/utils/geometric.py 58 | src/ppopt/utils/mpqp_utils.py 59 | tests/test_fixtures.py -------------------------------------------------------------------------------- /src/PPOPT.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/PPOPT.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | scipy 4 | numba 5 | gurobipy 6 | pytest 7 | setuptools 8 | psutil 9 | pathos 10 | plotly 11 | daqp 12 | 13 | [optional] 14 | cvxopt 15 | quadprog 16 | 17 | [test] 18 | pytest 19 | -------------------------------------------------------------------------------- /src/PPOPT.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | ppopt 2 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | """Main __init__.py for the ppopt package""" 2 | import os 3 | 4 | os.environ["OMP_NUM_THREADS"] = "1" # export OMP_NUM_THREADS=1 5 | os.environ["OPENBLAS_NUM_THREADS"] = "1" # export OPENBLAS_NUM_THREADS=1 6 | os.environ["MKL_NUM_THREADS"] = "1" # export MKL_NUM_THREADS=1 7 | os.environ["VECLIB_MAXIMUM_THREADS"] = "1" # export VECLIB_MAXIMUM_THREADS=1 8 | os.environ["NUMEXPR_NUM_THREADS"] = "1" # export NUMEXPR_NUM_THREADS=1 9 | -------------------------------------------------------------------------------- /src/ppopt/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ["OMP_NUM_THREADS"] = "1" # export OMP_NUM_THREADS=1 4 | os.environ["OPENBLAS_NUM_THREADS"] = "1" # export OPENBLAS_NUM_THREADS=1 5 | os.environ["MKL_NUM_THREADS"] = "1" # export MKL_NUM_THREADS=1 6 | os.environ["VECLIB_MAXIMUM_THREADS"] = "1" # export VECLIB_MAXIMUM_THREADS=1 7 | os.environ["NUMEXPR_NUM_THREADS"] = "1" # export NUMEXPR_NUM_THREADS=1 8 | -------------------------------------------------------------------------------- /src/ppopt/critical_region.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | from typing import List, Optional 3 | 4 | import numpy 5 | 6 | from .utils.chebyshev_ball import chebyshev_ball 7 | 8 | 9 | @dataclass(eq=False) 10 | class CriticalRegion: 11 | r""" 12 | Critical region is a polytope that defines a region in the uncertainty space 13 | with an associated optimal value, active set, lagrange multipliers and 14 | constraints 15 | 16 | .. math:: 17 | 18 | \begin{align} 19 | x(\theta) &= A\theta + b\\ 20 | \lambda(\theta) &= C\theta + d\\ 21 | \Theta &:= \{\forall \theta \in \mathbf{R}^m: E\theta \leq f\} 22 | \end{align} 23 | 24 | equality_indices: numpy array of indices 25 | 26 | constraint_set: if this is an A@x = b + F@theta boundary 27 | 28 | lambda_set: if this is a λ = 0 boundary 29 | 30 | boundary_set: if this is an Eθ <= f boundary 31 | 32 | """ 33 | 34 | A: numpy.ndarray 35 | b: numpy.ndarray 36 | C: numpy.ndarray 37 | d: numpy.ndarray 38 | E: numpy.ndarray 39 | f: numpy.ndarray 40 | active_set: List[int] 41 | 42 | omega_set: List[int] = field(default_factory=list) 43 | lambda_set: List[int] = field(default_factory=list) 44 | regular_set: List[List[int]] = field(default_factory=list) 45 | 46 | y_fixation: Optional[numpy.ndarray] = None 47 | y_indices: Optional[numpy.ndarray] = None 48 | x_indices: Optional[numpy.ndarray] = None 49 | 50 | def __repr__(self): 51 | """Returns a String representation of a Critical Region.""" 52 | 53 | # create the output string 54 | 55 | output = f"Critical region with active set {self.active_set}" 56 | output += f"\nThe Omega Constraint indices are {self.omega_set}" 57 | output += f"\nThe Lagrange multipliers Constraint indices are {self.lambda_set}" 58 | output += f"\nThe Regular Constraint indices are {self.regular_set}" 59 | output += "\n x(θ) = Aθ + b \n λ(θ) = Cθ + d \n Eθ <= f" 60 | output += f"\n A = {self.A} \n b = {self.b} \n C = {self.C} \n d = {self.d} \n E = {self.E} \n f = {self.f}" 61 | 62 | return output 63 | 64 | def evaluate(self, theta: numpy.ndarray) -> numpy.ndarray: 65 | """Evaluates x(θ) = Aθ + b.""" 66 | 67 | # if there are not any binary variables in this problem evaluate and return 68 | if self.y_fixation is None: 69 | return self.A @ theta + self.b 70 | 71 | # otherwise evalute AΘ+b for the continuous variables, then slice in the binaries at the correct locations 72 | cont_vars = self.A @ theta + self.b 73 | 74 | x_star = numpy.zeros((len(self.x_indices) + len(self.y_indices),)) 75 | x_star[self.x_indices] = cont_vars.flatten() 76 | x_star[self.y_indices] = self.y_fixation 77 | return x_star.reshape(-1, 1) 78 | 79 | def lagrange_multipliers(self, theta: numpy.ndarray) -> numpy.ndarray: 80 | """Evaluates λ(θ) = Cθ + d.""" 81 | return self.C @ theta + self.d 82 | 83 | def is_inside(self, theta: numpy.ndarray, tol: float = 1e-5) -> bool: 84 | """Tests if point θ is inside the critical region.""" 85 | # check if all constraints EΘ <= f 86 | return numpy.all(self.E @ theta - self.f < tol) 87 | 88 | # depreciated 89 | def is_full_dimension(self) -> bool: 90 | """Tests dimensionality of critical region. This is done by checking the radius of the chebyshev ball inside 91 | the region 92 | 93 | :return: a boolean value, of whether the critical region is full dimensional 94 | """ 95 | 96 | # solve the chebyshev ball LP 97 | soln = chebyshev_ball(self.E, self.f) 98 | 99 | # if this is infeasible, then it definitely is not full dimension as it is empty and doesn't have a good 100 | # dimensional description 101 | if soln is None: 102 | return False 103 | 104 | # if the chebyshev LP is feasible then we check if the radius is larger than some epsilon value 105 | return soln.sol[-1] > 10 ** -8 106 | 107 | def get_constraints(self): 108 | """ 109 | An assessor function to quickly access the fields of the extends of the critical region 110 | 111 | :return: a list with E, and f as elements 112 | """ 113 | return [self.E, self.f] 114 | -------------------------------------------------------------------------------- /src/ppopt/geometry/__init__.py: -------------------------------------------------------------------------------- 1 | """PPOPT.GEOMETRY INIT FILE - todo fill in.""" 2 | -------------------------------------------------------------------------------- /src/ppopt/geometry/polytope.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | 4 | class Polytope: 5 | """ 6 | This is a basic convex polytope class in n-dimensions. In future releases this will take the place of explicitly passing \\ 7 | around matrix pairs [A, b] and lead to a simplification of the code base. 8 | """ 9 | 10 | def __init__(self, A: numpy.ndarray, b: numpy.ndarray): 11 | """Initializes a Polytope.""" 12 | self.A = A 13 | self.b = b 14 | 15 | def __and__(self, other): # -> Optional[Polytope]: 16 | """Takes the union of two convex polytopes.""" 17 | if other is None: 18 | return Polytope(self.A.copy(), self.b.copy()) 19 | 20 | if isinstance(other, Polytope): 21 | raise TypeError(f"Can not form union of Polytope and type {type(other)}") 22 | 23 | A_prime = numpy.block([[self.A], [other.A]]) 24 | b_prime = numpy.block([[self.b], [other.b]]) 25 | return A_prime, b_prime 26 | -------------------------------------------------------------------------------- /src/ppopt/geometry/polytope_operations.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from ..critical_region import CriticalRegion 4 | from ..mplp_program import MPLP_Program 5 | from ..utils.chebyshev_ball import chebyshev_ball 6 | 7 | 8 | def get_chebyshev_information(region: CriticalRegion, deterministic_solver='gurobi'): 9 | region_constraints = region.get_constraints() 10 | return chebyshev_ball(*region_constraints, deterministic_solver=deterministic_solver) 11 | 12 | 13 | def find_extents(A, b, d, x): 14 | orth_vec = A @ d 15 | point_vec = A @ x 16 | 17 | dist = float('inf') 18 | 19 | for i in range(A.shape[0]): 20 | 21 | if orth_vec[i] <= 0: 22 | continue 23 | 24 | dist = min(dist, (b[i] - point_vec[i]) / orth_vec[i]) 25 | 26 | return dist 27 | 28 | 29 | def hit_and_run(p, x_0, n_steps: int = 10): 30 | # dimension size 31 | size = x_0.size 32 | 33 | prng = numpy.random.default_rng() 34 | 35 | def random_direction(): 36 | vec = prng.standard_normal(size).reshape(size, -1) 37 | return vec / numpy.linalg.norm(vec, 2) 38 | 39 | for _ in range(n_steps): 40 | # generate a random direction 41 | random_direc = random_direction() 42 | 43 | # find the extent in the direction of the random direction and the opposite direction 44 | extent_forward = find_extents(p.A, p.b, random_direc, x_0) 45 | extend_backward = find_extents(p.A, p.b, -random_direc, x_0) 46 | 47 | # sample a delta x from the line 48 | pert = prng.uniform(-extend_backward, extent_forward) * random_direc 49 | 50 | # check if still inside polytope 51 | if not numpy.all(p.A @ (x_0 + pert) <= p.b): 52 | continue 53 | 54 | # make step 55 | x_0 = pert + x_0 56 | 57 | return x_0 58 | 59 | 60 | def sample_program_theta_space(program: MPLP_Program, num_samples: int = 10): 61 | pass 62 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/__init__.py: -------------------------------------------------------------------------------- 1 | """PPOPT.MP_SOLVERS INIT FILE - todo fill in.""" 2 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpmiqp_enumeration.py: -------------------------------------------------------------------------------- 1 | # noinspection PyProtectedMember 2 | from pathos.multiprocessing import ProcessingPool as Pool 3 | 4 | from ..mpmilp_program import MPMILP_Program 5 | from ..solution import Solution 6 | from ..utils.general_utils import num_cpu_cores 7 | 8 | from .mitree import MITree 9 | from .solve_mpqp import mpqp_algorithm, solve_mpqp 10 | 11 | 12 | def solve_mpmiqp_enumeration(program: MPMILP_Program, num_cores: int = -1, 13 | cont_algorithm: mpqp_algorithm = mpqp_algorithm.combinatorial) -> Solution: 14 | """ 15 | The enumeration algorithm is based on the following approach 16 | 17 | 1) Enumerating all feasible binary combinations 18 | 2) Solving the resulting continuous mpQP/mpLP sub-problems for every feasible binary combination 19 | 3) Merging all solutions together 20 | 21 | This algorithm is quite slow in the general sense, but can solve a rich class of problems. 22 | 23 | :param program: An mpQP/mpLP of a problem with the binary variables without added constraints for the binary variables 24 | :param num_cores: the number of cores to use in this calculation to solve the mpLP/mpQP sub-problems 25 | :param cont_algorithm: the algorithm to solve the mpLP/mpQP algorithms (might not be required) 26 | :return: a solution to the mpMILP/mpMIQP (might have overlapping critical regions depending on algorithm choice) 27 | """ 28 | # if core count is unspecified use all available cores 29 | if num_cores == -1: 30 | num_cores = num_cpu_cores() 31 | 32 | # generate problem tree 33 | tree = MITree(program, depth=0) 34 | 35 | # grab all feasible binary combinations 36 | feasible_combinations = [leaf_nodes.fixed_bins for leaf_nodes in tree.get_full_leafs()] 37 | 38 | sols = [] 39 | 40 | # if we only use one core, then we simply solve the problems in sequence otherwise we use of parallel processing 41 | if num_cores == 1: 42 | # generate all substituted problems from these binary combinations to make continuous sub-problems 43 | problems = [program.generate_substituted_problem(fixed_bins) for fixed_bins in feasible_combinations] 44 | 45 | sols = [solve_mpqp(x, cont_algorithm) for x in problems] 46 | else: 47 | # generate all substituted problems from these binary combinations to make continuous sub-problems 48 | 49 | pool = Pool(num_cores) 50 | sols = list(pool.map(lambda x: solve_mpqp(program.generate_substituted_problem(x), cont_algorithm), feasible_combinations)) 51 | 52 | # add the fixed binary values to the critical regions 53 | region_list = [] 54 | for index, sol in enumerate(sols): 55 | for i in range(len(sol.critical_regions)): 56 | # add the fixed binary combination, the binary indices and the continuous variable indices 57 | sol.critical_regions[i].y_fixation = feasible_combinations[index] 58 | sol.critical_regions[i].y_indices = program.binary_indices 59 | sol.critical_regions[i].x_indices = program.cont_indices 60 | region_list.append(sol.critical_regions) 61 | 62 | collected_regions = [item for sublist in region_list for item in sublist] 63 | 64 | return Solution(program, collected_regions, is_overlapping=True) 65 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_ahmadi.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from ..mpqp_program import MPQP_Program 4 | from ..solution import Solution 5 | 6 | 7 | def solve(program: MPQP_Program) -> Optional[Solution]: 8 | """ 9 | This solves a MPQP program with the method proposed by Parisa Ahmadi-Moshkenani et al. This algorithm is similar 10 | to the graph algorithm proposed by Richard Oberdeik. 11 | 12 | The source for the algorithm can be found here. https://ieeexplore.ieee.org/document/8252719 13 | 14 | :param program: a MPQP_Program 15 | :return: The solution of a MPQP Program 16 | """ 17 | 18 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_combi_graph.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from ..mplp_program import MPLP_Program 4 | from ..mpqp_program import MPQP_Program 5 | from ..solution import Solution 6 | from ..utils.constraint_utilities import is_full_rank 7 | from ..utils.mpqp_utils import gen_cr_from_active_set 8 | 9 | 10 | def combinatorial_graph_initialization(program, initial_active_sets): 11 | """ 12 | Initializes the graph algorithm based on input 13 | 14 | :param program: 15 | :param initial_active_sets: 16 | :return: 17 | """ 18 | if initial_active_sets is None: 19 | initial_active_sets = program.sample_theta_space(1) 20 | 21 | # This will contain all the attempted active sets 22 | attempted = set() 23 | 24 | solution = Solution(program, []) 25 | 26 | to_attempt = {sorted_tuple(a_set) for a_set in initial_active_sets} 27 | 28 | return attempted, solution, to_attempt 29 | 30 | 31 | def sorted_tuple(x) -> tuple: 32 | """Helper function to make sure that inclusion tests are O(1)""" 33 | return tuple(sorted(x)) 34 | 35 | 36 | def remove_i(x, i: int) -> tuple: 37 | """helper function to remove an element i from a sorted tuple""" 38 | return sorted_tuple(set(x) - {i}) 39 | 40 | 41 | def add_i(x, i: int) -> tuple: 42 | """helper function to add an element i to a sorted tuple""" 43 | temp_x = set(x) 44 | temp_x.add(i) 45 | return sorted_tuple(temp_x) 46 | 47 | 48 | def feasability_check(program: MPQP_Program, A) -> bool: 49 | A_x, b_x, A_l, b_l = program.optimal_control_law(list(A)) 50 | 51 | # makes the assumption that the equality indices are at the top of the active set 52 | # as these can be any sign, we only care to enforce l(theta) >= 0 53 | A_l = A_l[len(program.equality_indices):] 54 | b_l = b_l[len(program.equality_indices):] 55 | 56 | N = program.num_constraints() 57 | A_ = program.A[[i for i in range(N) if i not in A]] 58 | b_ = program.b[[i for i in range(N) if i not in A]] 59 | F_ = program.F[[i for i in range(N) if i not in A]] 60 | 61 | # top constraints 62 | A_prob = numpy.block([[A_ @ A_x - F_], [-A_l], [program.A_t]]) 63 | b_prob = numpy.block([[b_ - A_ @ b_x], [b_l], [program.b_t]]) 64 | 65 | # build and solve the feasibility LP, if empty it will result in None 66 | return program.solver.solve_lp(None, A_prob, b_prob) is not None 67 | 68 | 69 | def solve(program: MPQP_Program) -> Solution: 70 | """ 71 | Solves the MPQP program with the joint combinatorial based connected graph approach of Arnström et al. 72 | 73 | This method removes the vast majority of the geometry calculations, leaving only the non-empty check of the critical region. 74 | 75 | url: https://arxiv.org/abs/2404.05511 76 | 77 | :param program: MPQP to be solved 78 | :return: the solution of the MPQP 79 | """ 80 | # initialize with and Exclusion set, a base solution, and a set of things to visit 81 | 82 | # initial_active_set = None if program.equality_indices is [] else [program.equality_indices] 83 | 84 | E, solution, S = combinatorial_graph_initialization(program, None) 85 | 86 | # make sure that everything we have added to S is in E 87 | for s in S: 88 | E.add(sorted_tuple(s)) 89 | 90 | def explore_subset(A_): 91 | """ 92 | Explores the subset of the active set A_, does not allow removal of equality indicies 93 | :param A_: The active set that we are taking subsets of 94 | """ 95 | for i in A_: 96 | if i not in program.equality_indices: 97 | A_trial = remove_i(A_, i) 98 | if A_trial not in E: 99 | S.add(A_trial) 100 | E.add(A_trial) 101 | 102 | def explore_superset(A_): 103 | """ 104 | Explores the superset of the active set A_ 105 | :param A_: The active set that we are taking super sets of 106 | """ 107 | for i in range(program.num_constraints()): 108 | if i not in A_: 109 | A_trial = add_i(A_, i) 110 | if A_trial not in E: 111 | S.add(A_trial) 112 | E.add(A_trial) 113 | 114 | # while we have things to explore, we explore 115 | while len(S) > 0: 116 | 117 | # get an active set combination 118 | A = S.pop() 119 | 120 | # if we fail LINQ then we need to remove a constraint to hope to be full rank 121 | if not is_full_rank(program.A, list(A)): 122 | explore_subset(A) 123 | 124 | # if we are full rank, check if the resulting critical region is not empty 125 | elif feasability_check(program, A): 126 | 127 | # in the case of mpLPs the active set must have the same size as the number of x variables 128 | if type(program) is MPLP_Program and len(A) != program.num_x(): 129 | # adds the super, and subsets 130 | explore_subset(A) 131 | explore_superset(A) 132 | continue 133 | 134 | # Attempts to construct the region 135 | cand_region = gen_cr_from_active_set(program, list(A)) 136 | 137 | # if the candidate region is full dimensional 138 | if cand_region is not None: 139 | solution.add_region(cand_region) 140 | 141 | # adds the super, and subsets 142 | explore_subset(A) 143 | explore_superset(A) 144 | 145 | return solution 146 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_combinatorial.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from ..mplp_program import MPLP_Program 4 | from ..mpqp_program import MPQP_Program 5 | from ..solution import Solution 6 | from ..utils.mpqp_utils import gen_cr_from_active_set 7 | from .solver_utils import CombinationTester, generate_children_sets 8 | 9 | 10 | def solve(program: MPQP_Program) -> Solution: 11 | """ 12 | Solves the MPQP program with a modified algorithm described in Gupta et al. 2011. 13 | The algorithm is described in this paper https://www.sciencedirect.com/science/article/pii/S0005109811003190 14 | 15 | :param program: MPQP to be solved 16 | :return: the solution of the MPQP 17 | """ 18 | murder_list = CombinationTester() 19 | 20 | to_check = [] 21 | 22 | solution = Solution(program, []) 23 | 24 | max_depth = max(program.num_x(), program.num_t()) - len(program.equality_indices) 25 | # breath first to optimize the elimination 26 | 27 | root_node = generate_children_sets(program.equality_indices, program.num_constraints(), murder_list) 28 | 29 | to_check.extend(root_node) 30 | 31 | for i in range(max_depth): 32 | # if there are no other active sets to check break out of loop 33 | # print(len(to_check)) 34 | 35 | future_sets = [] 36 | # creates the list of feasible active sets 37 | 38 | # if this is a mpLP we can do a reduction on the active sets 39 | # this is not required but will give a perf boost 40 | if type(program) is MPLP_Program: 41 | condition = lambda child: child[-1] >= len(child) + program.num_constraints() - program.num_x() 42 | to_check = [child for child in to_check if not condition(child)] 43 | 44 | feasible_sets = check_child_feasibility(program, to_check, murder_list) 45 | 46 | for child_set in feasible_sets: 47 | 48 | # soln = check_optimality(program, equality_indices=child_set) 49 | # The active set is optimal try to build a critical region 50 | 51 | # if soln is not None: 52 | if program.check_optimality(child_set): 53 | critical_region = gen_cr_from_active_set(program, child_set) 54 | # Check the dimensions of the critical region 55 | if critical_region is not None: 56 | solution.add_region(critical_region) 57 | 58 | # propagate sets 59 | 60 | if i + 1 != max_depth: 61 | future_sets.extend(generate_children_sets(child_set, program.num_constraints(), murder_list)) 62 | 63 | to_check = future_sets 64 | 65 | if program.check_feasibility(program.equality_indices): 66 | if program.check_optimality(program.equality_indices): 67 | region = gen_cr_from_active_set(program, program.equality_indices) 68 | if region is not None: 69 | if region.is_full_dimension(): 70 | solution.add_region(region) 71 | 72 | return solution 73 | 74 | 75 | def check_child_feasibility(program: MPQP_Program, set_list: List[List[int]], combination_checker: CombinationTester) -> \ 76 | List[List[int]]: 77 | """ 78 | Checks the feasibility of a list of active set combinations, if infeasible add to the combination checker and returns all feasible active set combinations 79 | 80 | :param program: An MPQP Program 81 | :param set_list: The list of active sets 82 | :param combination_checker: The combination checker that prunes 83 | :return: The list of all feasible active sets 84 | """ 85 | output = [] 86 | for child in set_list: 87 | if program.check_feasibility(child): 88 | output.append(child) 89 | else: 90 | combination_checker.add_combo(child) 91 | 92 | return output 93 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_dist_combinatorial.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/src/ppopt/mp_solvers/mpqp_dist_combinatorial.py -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_geometric.py: -------------------------------------------------------------------------------- 1 | 2 | from ..mpqp_program import MPQP_Program 3 | from ..solution import Solution 4 | from ..utils.general_utils import make_column 5 | from ..utils.mpqp_utils import gen_cr_from_active_set 6 | from .solver_utils import fathem_facet, get_facet_centers 7 | 8 | 9 | def solve(program: MPQP_Program, active_set=None) -> Solution: 10 | """ 11 | This solved the multiparametric program using the geometric algorithm described in Spjotvold et al. 12 | 13 | https://www.sciencedirect.com/science/article/pii/S1474667016369154 14 | 15 | :param program: a multiparametric program 16 | :param active_set: an initial optimal active set combination 17 | :return: the solution to the multiparametric optimization problem 18 | """ 19 | if active_set is None: 20 | active_set = program.gen_optimal_active_set() 21 | print(f'Using a found active set {active_set}') 22 | 23 | initial_region = gen_cr_from_active_set(program, active_set, check_full_dim=False) 24 | 25 | if initial_region is None: 26 | return Solution(program, []) 27 | 28 | solution = Solution(program, [initial_region]) 29 | solution_tol = solution.point_location_tolerance 30 | 31 | unchecked_regions = [initial_region] 32 | 33 | indexed_region_as = set() 34 | indexed_region_as.add(tuple(active_set)) 35 | 36 | while len(unchecked_regions) > 0: 37 | 38 | # we want to expand from each region facet 39 | 40 | cur_region = unchecked_regions.pop(0) 41 | A, b = cur_region.E, cur_region.f 42 | 43 | # we want to look at every facet on the region 44 | facet_information = get_facet_centers(A, b, program.solver) 45 | 46 | for center, normal, radius in facet_information: 47 | 48 | # make sure we are pointing in the correct direction 49 | center_c = make_column(center) 50 | normal_c = make_column(normal) 51 | 52 | possible_cr = fathem_facet(center_c, normal_c, radius, program, indexed_region_as, cur_region.active_set, solution) 53 | 54 | if possible_cr is not None: 55 | indexed_region_as.add(tuple(possible_cr.active_set)) 56 | unchecked_regions.append(possible_cr) 57 | solution.add_region(possible_cr) 58 | 59 | solution.point_location_tolerance = solution_tol 60 | 61 | return solution 62 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_graph.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from ..mpqp_program import MPQP_Program 4 | from ..solution import Solution 5 | from ..utils.constraint_utilities import is_full_rank 6 | from ..utils.mpqp_utils import gen_cr_from_active_set 7 | from .solver_utils import CombinationTester, generate_extra, generate_reduce 8 | 9 | 10 | def graph_initialization(program, initial_active_sets): 11 | """ 12 | Initializes the graph algorithm based on input 13 | 14 | :param program: 15 | :param initial_active_sets: 16 | :return: 17 | """ 18 | if initial_active_sets is None: 19 | initial_active_sets = program.sample_theta_space() 20 | 21 | # This will contain all the attempted active sets 22 | attempted = set() 23 | 24 | solution = Solution(program, []) 25 | 26 | murder_list = CombinationTester() 27 | 28 | to_attempt = [tuple(a_set) for a_set in initial_active_sets] 29 | 30 | if len(to_attempt) != 0: 31 | print(f'First region {to_attempt[0]}') 32 | else: 33 | print('Failed to find an initial region!') 34 | 35 | return attempted, solution, murder_list, to_attempt 36 | 37 | 38 | def solve(program: MPQP_Program, initial_active_sets: Optional[List[List[int]]] = None, use_pruning: bool = True) -> Solution: 39 | """ 40 | Solves the MPQP program with a modified algorithm described in Oberdieck et al. 2016 41 | 42 | url: https://www.sciencedirect.com/science/article/pii/S0005109816303971 43 | 44 | :param program: MPQP to be solved 45 | :param initial_active_sets: An initial critical region to start this algorithm with, otherwise one will be found 46 | :param use_pruning: Flag for setting if a pruning list is kept and utilized to remove 47 | :return: the solution of the MPQP 48 | """ 49 | 50 | # TODO: This still misses some Critical Regions. USE Geometric Repair? 51 | 52 | attempted, solution, murder_list, to_attempt = graph_initialization(program, initial_active_sets) 53 | 54 | if not use_pruning: 55 | murder_list = None 56 | 57 | while len(to_attempt) > 0: 58 | 59 | # make sure I am grabbing from the lowest cardinality 60 | to_attempt.sort(key=len) 61 | # step 1: feasibility 62 | 63 | candidate = to_attempt.pop(0) 64 | # print(candidate) 65 | if candidate in attempted: 66 | continue 67 | 68 | attempted.add(candidate) 69 | 70 | # checks for infeasible subsets if so break and go to next candidate 71 | 72 | if not is_full_rank(program.A, list(candidate)): 73 | to_attempt.extend(generate_reduce(candidate, murder_list, attempted, set(program.equality_indices))) 74 | 75 | if murder_list is not None: 76 | murder_list.add_combo(candidate) 77 | continue 78 | 79 | if program.check_feasibility(list(candidate)) is None: 80 | to_attempt.extend(generate_reduce(candidate, murder_list, attempted, set(program.equality_indices))) 81 | 82 | if murder_list is not None: 83 | murder_list.add_combo(candidate) 84 | continue 85 | 86 | if not program.check_optimality(list(candidate)): 87 | to_attempt.extend(generate_reduce(candidate, murder_list, attempted, set(program.equality_indices))) 88 | # not optimal do nothing with this 89 | continue 90 | 91 | region = gen_cr_from_active_set(program, list(candidate), check_full_dim=False) 92 | 93 | if region is None: 94 | continue 95 | 96 | if region.is_full_dimension(): 97 | solution.add_region(region) 98 | 99 | to_attempt.extend(generate_reduce(candidate, murder_list, attempted, set(program.equality_indices))) 100 | 101 | to_attempt.extend(generate_extra(candidate, region.regular_set[1], murder_list, attempted)) 102 | 103 | return solution 104 | # 105 | # 106 | # def solve_no_murder(program: MPQP_Program, initial_active_sets: List[List[int]] = None) -> Solution: 107 | # """ 108 | # Solves the MPQP program with a modified algorithm described in Oberdieck et al. 2016 109 | # 110 | # url: https://www.sciencedirect.com/science/article/pii/S0005109816303971 111 | # 112 | # 113 | # :param program: MPQP to be solved 114 | # :param initial_active_sets: An initial critical region to start this algorithm with, otherwise one will be found 115 | # :return: the solution of the MPQP 116 | # """ 117 | # 118 | # # TODO: This still misses some Critical Regions. USE Geometric Repair? 119 | # 120 | # attempted, solution, _, to_attempt = graph_initialization(program, initial_active_sets) 121 | # 122 | # while len(to_attempt) > 0: 123 | # 124 | # # make sure I am grabbing from the lowest cardinality 125 | # to_attempt.sort(key=len) 126 | # # step 1: feasibility 127 | # 128 | # candidate = to_attempt.pop(0) 129 | # # print(candidate) 130 | # if candidate in attempted: 131 | # continue 132 | # 133 | # attempted.add(candidate) 134 | # 135 | # # checks for infeasible subsets if so break and go to next candidate 136 | # 137 | # if not is_full_rank(program.A, list(candidate)): 138 | # to_attempt.extend(generate_reduce(candidate, None, attempted, set(program.equality_indices))) 139 | # continue 140 | # 141 | # if program.check_feasibility(list(candidate)) is None: 142 | # to_attempt.extend(generate_reduce(candidate, None, attempted, set(program.equality_indices))) 143 | # continue 144 | # 145 | # if not program.check_optimality(list(candidate)): 146 | # to_attempt.extend(generate_reduce(candidate, None, attempted, set(program.equality_indices))) 147 | # # not optimal do nothing with this 148 | # continue 149 | # 150 | # region = gen_cr_from_active_set(program, list(candidate), check_full_dim=False) 151 | # 152 | # if region is None: 153 | # continue 154 | # 155 | # if region.is_full_dimension(): 156 | # solution.add_region(region) 157 | # 158 | # to_attempt.extend(generate_reduce(candidate, None, attempted, set(program.equality_indices))) 159 | # 160 | # to_attempt.extend(generate_extra(candidate, region.regular_set[1], None, attempted)) 161 | # 162 | # return solution 163 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_parallel_combinatorial_exp.py: -------------------------------------------------------------------------------- 1 | import time 2 | from random import shuffle 3 | from typing import List, Tuple 4 | 5 | # noinspection PyProtectedMember 6 | from pathos.multiprocessing import ProcessingPool as Pool 7 | 8 | from ..critical_region import CriticalRegion 9 | from ..mplp_program import MPLP_Program 10 | from ..mpqp_program import MPQP_Program 11 | from ..solution import Solution 12 | from ..utils.general_utils import num_cpu_cores 13 | from ..utils.mpqp_utils import gen_cr_from_active_set 14 | from .solver_utils import generate_children_sets 15 | 16 | 17 | def full_process(program: MPQP_Program, active_set: List[int]) -> Tuple[List[List[int]], List[CriticalRegion]]: 18 | """ 19 | This is the function block that is executed in parallel. This takes a MPQP program as well as an active set combination, and \\ 20 | checks the feasibility of all super sets of cardinality + 1. This is done without using a pruning list as in the other\\ 21 | parallel combinatorial algorithm. This is suited for particularly large problems where an exponential number of pruned\\ 22 | active sets are stored, causing a large memory overhead. 23 | 24 | 25 | :param program: 26 | :param active_set: 27 | :return: 28 | """ 29 | # generate all children nodes 30 | 31 | feasible_children = [] 32 | valid_critical_regions = [] 33 | 34 | children = generate_children_sets(active_set, program.num_constraints()) 35 | 36 | for child in children: 37 | 38 | # mpLP reduction 39 | if type(program) is MPLP_Program: 40 | if child[-1] >= len(child) + program.num_constraints() - program.num_x(): 41 | continue 42 | 43 | if program.check_feasibility(child): # is_feasible(program, child): 44 | feasible_children.append(child) 45 | else: 46 | continue 47 | 48 | if program.check_optimality(child): # is_optimal(program, child): 49 | 50 | region = gen_cr_from_active_set(program, child) 51 | 52 | if region is not None: 53 | valid_critical_regions.append(region) 54 | 55 | is_max_depth = len(active_set) + 1 == max(program.num_t(), program.num_x()) 56 | 57 | if is_max_depth: 58 | feasible_children = [] 59 | 60 | return feasible_children, valid_critical_regions 61 | 62 | 63 | def solve(program: MPQP_Program, num_cores=-1) -> Solution: 64 | """ 65 | Solves the MPQP program with a modified algorithm described in Gupta et al. 2011 66 | 67 | This is the parallel version of the combinatorial. 68 | 69 | url: https://www.sciencedirect.com/science/article/pii/S0005109811003190 70 | 71 | :param num_cores: Sets the number of cores that are allocated to run this algorithm 72 | :param program: MPQP to be solved 73 | :return: the solution of the MPQP 74 | """ 75 | # thread pool that we will be using 76 | start = time.time() 77 | 78 | if num_cores == -1: 79 | num_cores = num_cpu_cores() 80 | 81 | print(f'Spawned threads across {num_cores}') 82 | 83 | pool = Pool(num_cores) 84 | 85 | to_check = [] 86 | 87 | solution = Solution(program, []) 88 | 89 | max_depth = max(program.num_x(), program.num_t()) - len(program.equality_indices) 90 | 91 | if not program.check_feasibility(program.equality_indices): 92 | # this is an infeasible problem 93 | return solution 94 | 95 | if program.check_optimality(program.equality_indices): 96 | region = gen_cr_from_active_set(program, program.equality_indices) 97 | if region is not None: 98 | solution.add_region(region) 99 | 100 | to_check.append(program.equality_indices) 101 | 102 | for i in range(max_depth): 103 | print(f'Time at depth test {i + 1}, {time.time() - start}') 104 | print(f'Number of active sets to be considered is {len(to_check)}') 105 | 106 | depth_time = time.time() 107 | 108 | f = lambda x: full_process(program, x) 109 | 110 | future_list = [] 111 | 112 | shuffle(to_check) 113 | 114 | outputs = pool.map(f, to_check) 115 | 116 | print(f'Time to run all tasks in parallel {time.time() - depth_time}') 117 | depth_time = time.time() 118 | 119 | for output in outputs: 120 | if len(output[0]) != 0: 121 | future_list.extend(output[0]) 122 | if len(output[1]) != 0: 123 | for region in output[1]: 124 | solution.add_region(region) 125 | 126 | print(f'Time to process all depth outputs {time.time() - depth_time}') 127 | 128 | to_check = future_list 129 | 130 | # If there are not more active sets to check we are done 131 | if len(to_check) == 0: 132 | break 133 | 134 | # pool.clear() 135 | 136 | return solution 137 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_parallel_geometric.py: -------------------------------------------------------------------------------- 1 | 2 | import numpy 3 | 4 | # noinspection PyProtectedMember 5 | from pathos.multiprocessing import ProcessingPool as Pool 6 | 7 | from ..mpqp_program import MPQP_Program 8 | from ..solution import Solution 9 | from ..utils.general_utils import num_cpu_cores 10 | from ..utils.mpqp_utils import gen_cr_from_active_set 11 | from .solver_utils import fathem_facet, get_facet_centers 12 | 13 | 14 | def full_process(center: numpy.ndarray, norm: numpy.ndarray, radius: float, program: MPQP_Program, current_active_set, 15 | indexed_region_as): 16 | """ 17 | This is the function block to be executed in parallel. Takes in a facet. Returns the associated CR on the other side of the facet 18 | if it exists, and all the facets associated with the other side of the 19 | 20 | :param center: Chebychev Center of a Critical Region Facet 21 | :param norm: Normal of the Facet 22 | :param radius: Chebychev Radius of the Critical Region Facet 23 | :param program: the multiparametric program being considered 24 | :param indexed_region_as: set of all critical regions found so far 25 | :param current_active_set: list of the active set that we are stepping out of 26 | :return: The identified Critical Region on the other side of the facet, and the facets of this critical region or None of nothing 27 | """ 28 | found_solution = fathem_facet(center, norm, radius, program, current_active_set, indexed_region_as) 29 | 30 | # this will return a None 31 | if found_solution is None: 32 | return None 33 | 34 | return found_solution, get_facet_centers(found_solution.E, found_solution.f) 35 | 36 | 37 | def solve(program: MPQP_Program, active_set=None, num_cores=-1) -> Solution: 38 | """ 39 | This solved the multiparametric program using the geometric algorithm described in Spjotvold et al. 40 | 41 | https://www.sciencedirect.com/science/article/pii/S1474667016369154 42 | 43 | :param program: a multiparametric program 44 | :param active_set: an initial optimal active set combination 45 | :param num_cores: number of cores to run this calculation on, default of -1 means use all available cores 46 | :return: the solution to the multiparametric optimization problem 47 | """ 48 | if active_set is None: 49 | active_set = program.gen_optimal_active_set() 50 | print(f'Using a found active set {active_set}') 51 | 52 | initial_region = gen_cr_from_active_set(program, active_set, check_full_dim=False) 53 | 54 | if initial_region is None: 55 | print('Could not find a valid initial region') 56 | return Solution(program, []) 57 | 58 | if num_cores == -1: 59 | num_cores = num_cpu_cores() 60 | 61 | print(f'Spawned threads across {num_cores}') 62 | 63 | pool = Pool(num_cores) 64 | 65 | solution = Solution(program, [initial_region]) 66 | 67 | indexed_region_as = set() 68 | indexed_region_as.add(tuple(active_set)) 69 | 70 | # initiate by exploring first region 71 | 72 | work_items = [(theta, facet_normal, radius, initial_region.active_set) for theta, facet_normal, radius in 73 | get_facet_centers(initial_region.E, initial_region.f)] 74 | 75 | while len(work_items) > 0: 76 | 77 | print(f' Number of Facets to look at this time {len(work_items)}') 78 | f = lambda x: full_process(x[0], x[1], x[2], program, x[3], indexed_region_as) 79 | 80 | outputs = pool.map(f, work_items) 81 | 82 | # clear out the work queue 83 | work_items = [] 84 | 85 | # process the outputs 86 | for output in outputs: 87 | 88 | if output is None: 89 | continue 90 | 91 | found_cr = output[0] 92 | facets = output[1] 93 | 94 | # check to see if we need to do anything 95 | if found_cr is not None: 96 | # we have a critical region! 97 | 98 | # check to see if we have found this region before 99 | if tuple(found_cr.active_set) not in indexed_region_as: 100 | # if we haven't added it to the active set index 101 | indexed_region_as.add(tuple(found_cr.active_set)) 102 | # add it to the solution 103 | solution.add_region(found_cr) 104 | # add the associated work items from the facets to the queue 105 | work_items.extend( 106 | [(theta, facet_normal, radius, found_cr.active_set) for theta, facet_normal, radius in facets]) 107 | 108 | return solution 109 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_parrallel_combinatorial.py: -------------------------------------------------------------------------------- 1 | import time 2 | from random import shuffle 3 | from typing import List, Optional, Set, Tuple 4 | 5 | # noinspection PyProtectedMember 6 | from pathos.multiprocessing import ProcessingPool as Pool 7 | 8 | from ..critical_region import CriticalRegion 9 | from ..mplp_program import MPLP_Program 10 | from ..mpqp_program import MPQP_Program 11 | from ..solution import Solution 12 | from ..utils.general_utils import num_cpu_cores 13 | from ..utils.mpqp_utils import gen_cr_from_active_set 14 | from .solver_utils import CombinationTester, generate_children_sets 15 | 16 | 17 | def full_process(program: MPQP_Program, active_set: List[int], murder_list, gen_children) -> Tuple[Optional[CriticalRegion], Set[Tuple[int,...]], List[List[int]]]: 18 | """ 19 | 20 | This is the fundamental building block of the parallel combinatorial algorithm, here we branch off of a known feasible active set combination\\ 21 | and then 22 | 23 | 24 | :param program: A multiparametric program 25 | :param active_set: the active set combination that we are expanding on 26 | :param murder_list: the list containing all previously found 27 | :param gen_children: A boolean flag, that determines if we should generate the children subsets 28 | :return: a list of the following form [Optional[CriticalRegion], pruned active set combination,Possibly Feasible Active set combinations] 29 | """ 30 | t_set: Tuple[int, ...] = tuple(active_set) 31 | 32 | candidate_cr: Optional[CriticalRegion] = None 33 | pruned_active_sets: Set[Tuple[int,...]] = set() 34 | child_active_sets: List[List[int]] = [] 35 | 36 | is_feasible_ = program.check_feasibility(active_set) 37 | 38 | if not is_feasible_: 39 | pruned_active_sets.add(t_set) 40 | return candidate_cr, pruned_active_sets, child_active_sets 41 | 42 | is_optimal_ = program.check_optimality(active_set) # is_optimal(program, equality_indices) 43 | 44 | if not is_optimal_: 45 | if gen_children: 46 | child_active_sets = generate_children_sets(active_set, program.num_constraints(), murder_list) 47 | 48 | # filter children 49 | if type(program) is MPLP_Program: 50 | condition = lambda child: child[-1] >= len(child) + program.num_constraints() - program.num_x() 51 | child_active_sets = [child for child in child_active_sets if not condition(child)] 52 | 53 | return candidate_cr, pruned_active_sets, child_active_sets 54 | 55 | candidate_cr = gen_cr_from_active_set(program, active_set) 56 | 57 | if candidate_cr is None: 58 | pruned_active_sets.add(t_set) 59 | return candidate_cr, pruned_active_sets, child_active_sets 60 | 61 | if gen_children: 62 | child_active_sets = generate_children_sets(active_set, program.num_constraints(), murder_list) 63 | 64 | return candidate_cr, pruned_active_sets, child_active_sets 65 | 66 | 67 | def solve(program: MPQP_Program, num_cores=-1) -> Solution: 68 | """ 69 | Solves the MPQP program with a modified algorithm described in Gupta et al. 2011 70 | 71 | This is the parallel version of the combinatorial. 72 | 73 | url: https://www.sciencedirect.com/science/article/pii/S0005109811003190 74 | 75 | :param num_cores: Sets the number of cores that are allocated to run this algorithm 76 | :param program: MPQP to be solved 77 | :return: the solution of the MPQP 78 | """ 79 | # thread pool that we will be using 80 | start = time.time() 81 | 82 | if num_cores == -1: 83 | num_cores = num_cpu_cores() 84 | 85 | print(f'Spawned threads across {num_cores}') 86 | 87 | pool = Pool(num_cores) 88 | 89 | murder_list = CombinationTester() 90 | 91 | to_check = [] 92 | 93 | solution = Solution(program, []) 94 | 95 | max_depth = max(program.num_x(), program.num_t()) - len(program.equality_indices) 96 | 97 | # breath first to increase efficiency of elimination 98 | root_node = generate_children_sets(program.equality_indices, program.num_constraints()) 99 | 100 | to_check.extend(root_node) 101 | 102 | for i in range(max_depth): 103 | print(f'Time at depth test {i + 1}, {time.time() - start}') 104 | print(f'Number of active sets to be considered is {len(to_check)}') 105 | 106 | depth_time = time.time() 107 | 108 | gen_children = i + 1 != max_depth 109 | 110 | f = lambda x: full_process(program, x, murder_list, gen_children) 111 | 112 | future_list = [] 113 | 114 | shuffle(to_check) 115 | 116 | outputs = pool.map(f, to_check) 117 | 118 | print(f'Time to run all tasks in parallel {time.time() - depth_time}') 119 | depth_time = time.time() 120 | 121 | if i + 1 == max_depth: 122 | for output in outputs: 123 | if output[0] is not None: 124 | solution.add_region(output[0]) 125 | break 126 | 127 | for output in outputs: 128 | murder_list.add_combos(output[1]) 129 | future_list.extend(output[2]) 130 | if output[0] is not None: 131 | solution.add_region(output[0]) 132 | 133 | print(f'Time to process all depth outputs {time.time() - depth_time}') 134 | 135 | to_check = future_list 136 | 137 | # If there are not more active sets to check we are done 138 | if len(to_check) == 0: 139 | break 140 | 141 | # we never actually tested the program base active set 142 | if program.check_feasibility(program.equality_indices): 143 | if program.check_optimality(program.equality_indices): 144 | region = gen_cr_from_active_set(program, program.equality_indices) 145 | if region is not None: 146 | solution.add_region(region) 147 | 148 | # pool.clear() 149 | 150 | return solution 151 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/mpqp_parrallel_graph.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Set, Tuple 2 | 3 | # noinspection PyProtectedMember 4 | from pathos.multiprocessing import ProcessingPool as Pool 5 | 6 | from ..mpqp_program import MPQP_Program 7 | from ..solution import Solution 8 | from ..utils.general_utils import num_cpu_cores 9 | from ..utils.mpqp_utils import gen_cr_from_active_set 10 | from .solver_utils import ( 11 | CombinationTester, 12 | generate_extra, 13 | generate_reduce, 14 | manufacture_lambda, 15 | ) 16 | 17 | 18 | def full_process(program, candidate, murder_list): 19 | """ 20 | This function is the main kernel of the parallel graph algorithm. 21 | 22 | :param program: A multiparametric program 23 | :param candidate: the active set combination that we are expanding on 24 | :param murder_list: the list containing all previously found 25 | :return: a list of the following form [List[Active Set combinations], active set to prune, Optional[CriticalRegion]] 26 | """ 27 | to_attempt = [] 28 | to_murder = None 29 | 30 | if not program.check_feasibility(list(candidate)): 31 | to_attempt.extend(generate_reduce(candidate, murder_list, None, set(program.equality_indices))) 32 | to_murder = candidate 33 | return [to_attempt, to_murder, None] 34 | 35 | if not program.check_optimality(list(candidate)): 36 | to_attempt.extend(generate_reduce(candidate, murder_list, None, set(program.equality_indices))) 37 | return [to_attempt, to_murder, None] 38 | 39 | region = gen_cr_from_active_set(program, list(candidate), check_full_dim=False) 40 | 41 | if region.is_full_dimension(): 42 | to_attempt.extend(generate_reduce(candidate, murder_list, None, set(program.equality_indices))) 43 | to_attempt.extend(generate_extra(candidate, region.regular_set[1], murder_list)) 44 | 45 | return [to_attempt, to_murder, region] 46 | 47 | return [to_attempt, to_murder, None] 48 | 49 | 50 | def solve(program: MPQP_Program, initial_active_sets=None, num_cores=-1, use_pruning: bool = True) -> Solution: 51 | """ 52 | Solves the MPQP program with a modified algorithm described in Oberdieck et al. 2016 53 | 54 | url: https://www.sciencedirect.com/science/article/pii/S0005109816303971 55 | 56 | :param program: MPQP to be solved 57 | :param initial_active_sets:An initial critical region to start this algorithm with, otherwise one will be found 58 | :param num_cores: number of cores to run this calculation on, default of -1 means use all available cores 59 | :param use_pruning: if the pruning list should be shared to the other threads 60 | :return: the solution of the MPQP 61 | """ 62 | if initial_active_sets is None: 63 | initial_active_sets = [program.gen_optimal_active_set()] 64 | 65 | # This will contain all the attempted active sets 66 | attempted: Set[Tuple[int]] = set() 67 | in_process = set() 68 | 69 | murder_list: Optional[CombinationTester] = CombinationTester() 70 | 71 | if not use_pruning: 72 | murder_list = None 73 | 74 | to_attempt = [tuple(a_set) for a_set in initial_active_sets] 75 | 76 | solution = Solution(program, []) 77 | 78 | if num_cores == -1: 79 | num_cores = num_cpu_cores() 80 | 81 | pool = Pool(num_cores) 82 | 83 | tiered_to_attempt: List[List] = [[]] * max(program.num_x() + 3, program.num_t() + 3) 84 | tiered_to_attempt[0].extend(to_attempt) 85 | 86 | # loop until there aren't any more candidates 87 | while sum([len(tier) for tier in tiered_to_attempt]) > 0: 88 | 89 | check = manufacture_lambda(attempted, murder_list) 90 | 91 | def f(x): 92 | return full_process(program, x, murder_list) 93 | 94 | to_attempt = [] 95 | 96 | cursor = 0 97 | while len(to_attempt) == 0 and cursor < len(tiered_to_attempt): 98 | to_attempt.extend([x for x in tiered_to_attempt[cursor] if check(x)]) 99 | tiered_to_attempt[cursor] = [] 100 | cursor += 1 101 | 102 | print(f'Processing {len(to_attempt)} in this parallel swap') 103 | outputs = pool.map(f, to_attempt) 104 | 105 | for candidate in to_attempt: 106 | attempted.add(candidate) 107 | 108 | for output in outputs: 109 | for candidate in output[0]: 110 | if candidate not in in_process: 111 | tiered_to_attempt[len(candidate)].append(candidate) 112 | in_process.add(candidate) 113 | if output[1] is not None and murder_list is not None: 114 | murder_list.add_combo(output[1]) 115 | if output[2] is not None: 116 | solution.add_region(output[2]) 117 | 118 | # pool.close() 119 | return solution 120 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/solve_mplp.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from ..mplp_program import MPLP_Program 4 | 5 | 6 | class mplp_solver(Enum): 7 | Dustin = '1' 8 | 9 | 10 | def solve_mplp(problem: MPLP_Program, algorithm: mplp_solver = mplp_solver.Dustin): 11 | """ 12 | This is the main solver interface for MPLP type problems. 13 | 14 | :param problem: 15 | :param algorithm: 16 | :return: 17 | """ 18 | 19 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/solve_mpmiqp.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from ..mpmilp_program import MPMILP_Program 4 | from ..solution import Solution 5 | from .mpmiqp_enumeration import solve_mpmiqp_enumeration 6 | from .solve_mpqp import mpqp_algorithm, solve_mpqp 7 | 8 | import numpy 9 | 10 | from ..utils.region_overlap_utils import reduce_overlapping_critical_regions_1d 11 | 12 | 13 | class mpmiqp_algorithm(Enum): 14 | """ 15 | Enum that selects the mpmiqp algorithm to be used 16 | 17 | This is done by passing the argument mpmiqp_algorithm.algorithm 18 | 19 | This is typically combined in conjunction with a mpqp_algorithm to solve sub-problems when they arise 20 | """ 21 | enumerate = 'enumerate' 22 | 23 | def __str__(self): 24 | return self.name 25 | 26 | @staticmethod 27 | def all_algos(): 28 | output = '' 29 | for algo in mpmiqp_algorithm: 30 | output += f'mpmiqp_algorithm.{algo}\n' 31 | return output 32 | 33 | 34 | def solve_mpmiqp(problem: MPMILP_Program, mpmiqp_algo: mpmiqp_algorithm = mpmiqp_algorithm.enumerate, 35 | cont_algo: mpqp_algorithm = mpqp_algorithm.combinatorial, num_cores=-1, 36 | reduce_overlap=True) -> Solution: 37 | # the case of a continuous problem just solve it and return 38 | if len(problem.binary_indices) == 0: 39 | print("The problem does not have any binary variables, solving as a continuous problem instead.") 40 | # noinspection PyTypeChecker 41 | return solve_mpqp(problem, cont_algo) 42 | 43 | if not isinstance(mpmiqp_algo, mpmiqp_algorithm): 44 | raise TypeError( 45 | f"You must pass an algorithm from mpmiqp_algorithm as the continuous algorithm. These can be found by " 46 | f"importing the following \n\nfrom ppopt.mp_solvers.solve_mpmiqp import mpmiqp_algorithm\n\nWith the " 47 | f"following choices\n{mpmiqp_algorithm.all_algos()}") 48 | 49 | cand_sol = Solution(problem, []) 50 | 51 | # listing of all available algorithms 52 | if mpmiqp_algo == mpmiqp_algorithm.enumerate: 53 | cand_sol = solve_mpmiqp_enumeration(problem, num_cores, cont_algo) 54 | 55 | # see if we can actually reduce the overlaps given current limitations 56 | sum_abs_H = numpy.sum(numpy.abs(problem.H[problem.cont_indices, :])) 57 | is_bilinear_terms: bool = not numpy.isclose(sum_abs_H, 0) 58 | 59 | # we currently only support for pMILP problems no H term 60 | if not (problem.num_t() > 1 or hasattr(problem, 'Q') or not reduce_overlap or is_bilinear_terms): 61 | # For 1D mpMILP case, we remove overlaps 62 | # In case of dual degeneracy we keep all solutions so in this case there could still be overlaps 63 | collected_regions, is_overlapping = reduce_overlapping_critical_regions_1d(problem, cand_sol.critical_regions) 64 | return Solution(problem, collected_regions, is_overlapping) 65 | 66 | return cand_sol 67 | -------------------------------------------------------------------------------- /src/ppopt/mp_solvers/solve_mpqp.py: -------------------------------------------------------------------------------- 1 | # this is the interface for the mpQP and mpLP problem solvers 2 | 3 | from enum import Enum 4 | 5 | import numpy 6 | 7 | from ..mp_solvers import ( 8 | mpqp_combi_graph, 9 | mpqp_combinatorial, 10 | mpqp_graph, 11 | mpqp_parallel_combinatorial_exp, 12 | mpqp_parallel_geometric, 13 | mpqp_parallel_geometric_exp, 14 | mpqp_parrallel_combinatorial, 15 | mpqp_parrallel_graph, 16 | ) 17 | from ..mplp_program import MPLP_Program 18 | from ..mpqp_program import MPQP_Program 19 | from ..solution import Solution 20 | from . import mpqp_geometric 21 | 22 | 23 | class mpqp_algorithm(Enum): 24 | """ 25 | Enum that selects the mpqp algorithm to be used 26 | 27 | This is done by passing the argument mpqp_algorithm.algorithm 28 | """ 29 | combinatorial = 'combinatorial' 30 | combinatorial_parallel = 'p combinatorial' 31 | combinatorial_parallel_exp = 'p combinatorial exp' 32 | graph = 'graph' 33 | graph_exp = 'graph exp' 34 | graph_parallel = 'p graph' 35 | graph_parallel_exp = 'p graph exp' 36 | combinatorial_graph = 'combinatorial graph' 37 | geometric = 'geometric' 38 | geometric_parallel = 'p geometric' 39 | geometric_parallel_exp = 'p geometric exp' 40 | 41 | def __str__(self): 42 | return self.name 43 | 44 | @staticmethod 45 | def all_algos(): 46 | output = '' 47 | for algo in mpqp_algorithm: 48 | output += f'mpqp_algorithm.{algo}\n' 49 | return output 50 | 51 | 52 | def solve_mpqp(problem: MPQP_Program, algorithm: mpqp_algorithm = mpqp_algorithm.combinatorial) -> Solution: 53 | """ 54 | Takes a mpqp programming problem and solves it in a specified manner, In addition this solves MPLPs. The default 55 | solve algorithm is the Combinatorial algorithm by Gupta. et al. 56 | 57 | :param problem: Multiparametric Program to be solved 58 | :param algorithm: Selects the algorithm to be used 59 | :return: the solution of the MPQP, returns an empty solution if there is not an implemented algorithm 60 | """ 61 | 62 | if not isinstance(algorithm, mpqp_algorithm): 63 | raise TypeError( 64 | f"You must pass an algorithm from mpqp_algorithm as the continuous algorithm. These can be found by " 65 | f"importing the following \n\nfrom ppopt.mp_solvers.solve_mpqp import mpqp_algorithm\n\nWith the " 66 | f"following choices\n{mpqp_algorithm.all_algos()}") 67 | 68 | solution = Solution(problem, []) 69 | 70 | if algorithm is mpqp_algorithm.combinatorial: 71 | solution = mpqp_combinatorial.solve(problem) 72 | 73 | if algorithm is mpqp_algorithm.combinatorial_parallel: 74 | solution = mpqp_parrallel_combinatorial.solve(problem) 75 | 76 | if algorithm is mpqp_algorithm.combinatorial_parallel_exp: 77 | solution = mpqp_parallel_combinatorial_exp.solve(problem) 78 | 79 | if algorithm is mpqp_algorithm.graph: 80 | solution = mpqp_graph.solve(problem) 81 | 82 | if algorithm is mpqp_algorithm.graph_exp: 83 | solution = mpqp_graph.solve(problem, use_pruning=False) 84 | 85 | if algorithm is mpqp_algorithm.graph_parallel: 86 | solution = mpqp_parrallel_graph.solve(problem) 87 | 88 | if algorithm is mpqp_algorithm.graph_parallel_exp: 89 | solution = mpqp_parrallel_graph.solve(problem, use_pruning=False) 90 | 91 | if algorithm is mpqp_algorithm.geometric: 92 | solution = mpqp_geometric.solve(problem) 93 | 94 | if algorithm is mpqp_algorithm.geometric_parallel: 95 | solution = mpqp_parallel_geometric.solve(problem) 96 | 97 | if algorithm is mpqp_algorithm.geometric_parallel_exp: 98 | solution = mpqp_parallel_geometric_exp.solve(problem) 99 | 100 | if algorithm is mpqp_algorithm.combinatorial_graph: 101 | solution = mpqp_combi_graph.solve(problem) 102 | 103 | # check if there needs to be a flag thrown in the case of overlapping critical regions 104 | # happens if there are negative or zero eigen values for mpQP (kkt conditions can find a lot of saddle points) 105 | if isinstance(problem, MPQP_Program): 106 | if min(numpy.linalg.eigvalsh(problem.Q)) <= 0: 107 | solution.is_overlapping = True 108 | 109 | # in the case of degenerate problems there are overlapping critical regions, unless a check is performed to prove 110 | # no overlap it is generally safer to consider that the mpLP case is overlapping 111 | if isinstance(problem, MPLP_Program): 112 | solution.is_overlapping = True 113 | 114 | return filter_solution(solution) 115 | 116 | 117 | def filter_solution(solution: Solution) -> Solution: 118 | """ 119 | This is a placeholder function, in the future this will be used to process and operate on the solution before it 120 | is returned to the user. 121 | 122 | :param solution: a multi parametric solution 123 | 124 | :return: A processed solution 125 | """ 126 | 127 | return solution 128 | -------------------------------------------------------------------------------- /src/ppopt/mpmiqp_program.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional, Union 2 | 3 | import numpy 4 | 5 | from .mpmilp_program import MPMILP_Program 6 | from .mpqp_program import MPQP_Program 7 | from .solver import Solver 8 | from .solver_interface.solver_interface_utils import SolverOutput 9 | 10 | 11 | class MPMIQP_Program(MPMILP_Program): 12 | r""" 13 | The standard class for multiparametric mixed integer quadratic programming. 14 | 15 | .. math:: 16 | \min \frac{1}{2}x^T Qx + \theta^T H^T x + c^T x + c_c + c_t^T \theta + \frac{1}{2} \theta^T Q_t\theta 17 | 18 | .. math:: 19 | \begin{align} 20 | A_{eq}x &= b_{eq} + F_{eq}\theta\\ 21 | Ax &\leq b + F\theta\\ 22 | A_\theta \theta &\leq b_\theta\\ 23 | x_i &\in \mathbb{R} \text{ or } \mathbb{B}\\ 24 | \end{align} 25 | 26 | Equality constraints containing only binary variables cannot also be parametric, as that generate a non-convex and 27 | discrete feasible parameter space 28 | 29 | """ 30 | 31 | def __init__(self, A: numpy.ndarray, b: numpy.ndarray, c: numpy.ndarray, H: numpy.ndarray, Q: numpy.ndarray, 32 | A_t: numpy.ndarray, 33 | b_t: numpy.ndarray, F: numpy.ndarray, binary_indices: List, c_c: Optional[numpy.ndarray] = None, 34 | c_t: Optional[numpy.ndarray] = None, Q_t: Optional[numpy.ndarray] = None, 35 | equality_indices: Optional[List[int]] = None, solver: Optional[Solver] = None, 36 | post_process: bool = True): 37 | """Initialized the MPMIQP_Program.""" 38 | # calls MPMILP_Program's constructor to reduce out burden 39 | 40 | if solver is None: 41 | solver = Solver() 42 | 43 | self.Q = Q 44 | super(MPMIQP_Program, self).__init__(A, b, c, H, A_t, b_t, F, binary_indices, c_c, c_t, Q_t, equality_indices, 45 | solver, post_process=False) 46 | 47 | if post_process: 48 | self.post_process() 49 | 50 | def evaluate_objective(self, x: numpy.ndarray, theta_point: numpy.ndarray) -> float: 51 | """Evaluates the objective f(x,theta)""" 52 | obj_val = 0.5 * x.T @ self.Q @ x + theta_point.T @ self.H.T @ x + self.c.T @ x + self.c_c + self.c_t.T @ theta_point + 0.5 * theta_point.T @ self.Q_t @ theta_point 53 | return float(obj_val[0, 0]) 54 | 55 | def solve_theta(self, theta_point: numpy.ndarray) -> Optional[SolverOutput]: 56 | """ 57 | Solves the substituted problem,with the provided theta 58 | 59 | :param theta_point: 60 | :param deterministic_solver: 61 | :return: 62 | """ 63 | soln = self.solver.solve_miqp(self.Q, self.c + self.H @ theta_point, self.A, self.b + self.F @ theta_point, 64 | self.equality_indices, self.binary_indices) 65 | if soln is not None: 66 | const_term = self.c_c + self.c_t.T @ theta_point + 0.5 * theta_point.T @ self.Q_t @ theta_point 67 | soln.obj += float(const_term[0, 0]) 68 | 69 | return soln 70 | 71 | def generate_substituted_problem(self, fixed_combination: Union[numpy.ndarray, List[int]]): 72 | """ 73 | Generates the fixed binary continuous version of the problem e.g. substitute all the binary variables 74 | :param fixed_combination: 75 | :return: 76 | """ 77 | 78 | # handle only the constraint matrices for now 79 | A_cont = self.A[:, self.cont_indices] 80 | A_bin = self.A[:, self.binary_indices] 81 | 82 | fixed_combination = numpy.array(fixed_combination).reshape(-1, 1) 83 | 84 | # helper function to classify constraint types 85 | def is_not_binary_constraint(i: int): 86 | return not (numpy.allclose(A_cont[i], 0 * A_cont[i]) and numpy.allclose(self.F[i], 0 * self.F[i])) 87 | 88 | inequality_indices = [i for i in range(self.num_constraints()) if i not in self.equality_indices] 89 | 90 | kept_equality_constraints = list(filter(is_not_binary_constraint, self.equality_indices)) 91 | kept_ineq_constraints = list(filter(is_not_binary_constraint, inequality_indices)) 92 | 93 | kept_constraints = [*kept_equality_constraints, *kept_ineq_constraints] 94 | 95 | new_equality_set = [i for i in range(len(kept_equality_constraints))] 96 | 97 | A_cont = A_cont[kept_constraints] 98 | A_bin = A_bin[kept_constraints] 99 | b = self.b[kept_constraints] - A_bin @ fixed_combination 100 | F = self.F[kept_constraints] 101 | 102 | Q_c = self.Q[:, self.cont_indices][self.cont_indices] 103 | Q_d = self.Q[:, self.binary_indices][self.binary_indices] 104 | 105 | H_alpha = self.Q[:, self.cont_indices][self.binary_indices] 106 | c = self.c[self.cont_indices] + (H_alpha.T @ fixed_combination) 107 | c_c = self.c_c + self.c[ 108 | self.binary_indices].T @ fixed_combination + 0.5 * fixed_combination.T @ Q_d @ fixed_combination 109 | H_c = self.H[self.cont_indices] 110 | H_d = self.H[self.binary_indices] 111 | 112 | c_t = self.c_t + (fixed_combination.T @ H_d).T 113 | 114 | sub_problem = MPQP_Program(A_cont, b, c, H_c, Q_c, self.A_t, self.b_t, F, c_c, c_t, self.Q_t, new_equality_set, 115 | self.solver) 116 | return sub_problem 117 | 118 | def generate_relaxed_problem(self, process: bool = True) -> MPQP_Program: 119 | """ 120 | Generates the relaxed problem, were all binaries are relaxed to real numbers between [0,1]. 121 | 122 | :param process: if processing should be done 123 | :return: the relaxation of the problem, an mpQP 124 | """ 125 | 126 | # generate 0 <= y <= 1 constraints for every binary variable 127 | 128 | A_ub_add = numpy.zeros((len(self.binary_indices), self.num_x())) 129 | A_lb_add = numpy.zeros((len(self.binary_indices), self.num_x())) 130 | 131 | b_ub_add = numpy.zeros((len(self.binary_indices))).reshape(-1, 1) 132 | b_lb_add = numpy.zeros((len(self.binary_indices))).reshape(-1, 1) 133 | 134 | F_ub_add = numpy.zeros((len(self.binary_indices), self.num_t())) 135 | F_lb_add = numpy.zeros((len(self.binary_indices), self.num_t())) 136 | 137 | for idx, v in enumerate(self.binary_indices): 138 | A_ub_add[idx, v] = 1.0 139 | A_lb_add[idx, v] = -1.0 140 | b_ub_add[idx] = 1.0 141 | b_lb_add[idx] = 0.0 142 | 143 | A = numpy.block([[self.A], [A_ub_add], [A_lb_add]]) 144 | b = numpy.block([[self.b], [b_ub_add], [b_lb_add]]) 145 | F = numpy.block([[self.F], [F_ub_add], [F_lb_add]]) 146 | 147 | return MPQP_Program(A, b, self.c, self.H, self.Q, self.A_t, self.b_t, F, self.c_c, self.c_t, self.Q_t, 148 | self.equality_indices, self.solver, post_process=process) 149 | -------------------------------------------------------------------------------- /src/ppopt/problem_generator.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | import numpy 4 | 5 | from .mplp_program import MPLP_Program 6 | from .mpqp_program import MPQP_Program 7 | 8 | 9 | def generate_mplp(x: int = 2, t: int = 2, m: int = 10, seed: Optional[int] = None) -> MPLP_Program: 10 | """ 11 | Generates a random mpLP problem with of the following characteristics 12 | 13 | :param x: number of parameters 14 | :param t: number of uncertain variables 15 | :param m: number of constraints 16 | :param seed: random seed 17 | :return: A random mpLP of the specified type 18 | """ 19 | 20 | mpqp = generate_mpqp(x, t, m, seed) 21 | 22 | return MPLP_Program(mpqp.A, mpqp.b, mpqp.c, mpqp.H, mpqp.A_t, mpqp.b_t, mpqp.F) 23 | 24 | 25 | def generate_mpqp(x: int = 2, t: int = 2, m: int = 10, seed: Optional[int] = None) -> MPQP_Program: 26 | """ 27 | Generates a random mpQP problem with of the following characteristics 28 | 29 | :param x: number of x dimensions 30 | :param t: number of theta dimensions 31 | :param m: number of constraints 32 | :param seed: random seed 33 | :return: A random mpQP problem of the specified type 34 | """ 35 | 36 | prng = numpy.random.default_rng(seed) 37 | 38 | Q = prng.random((x, x)) 39 | Q = Q.T @ Q + numpy.eye(x) 40 | 41 | rand = lambda: prng.random(1) 42 | 43 | RangeValue = numpy.round(20 * rand() + 5) 44 | XBorder = numpy.round(8 * rand() + 1) / 10 45 | XShift = numpy.round(8 * rand() + 1) / 10 46 | TBorder = numpy.round(8 * rand() + 1) / 10 47 | TShift = numpy.round(8 * rand() + 1) / 10 48 | 49 | c = (prng.random((x, 1)) - .5) / rand() 50 | 51 | eigen_values = numpy.linalg.eigvals(Q) 52 | Range = RangeValue * (max(eigen_values) - min(eigen_values)) 53 | 54 | A = numpy.zeros((m, x)) 55 | F = numpy.zeros((m, t)) 56 | 57 | for i in range(m): 58 | const = False 59 | while not const: 60 | guess = prng.random(x) 61 | idx = guess >= XBorder 62 | A[i][idx] = numpy.floor((prng.random(sum(idx)) - XShift) * Range) 63 | 64 | if any(A[i] != 0): 65 | const = True 66 | guess = prng.random(t) 67 | idx = guess >= TBorder 68 | F[i][idx] = numpy.floor((prng.random(sum(idx)) - TShift) * Range) 69 | 70 | A = numpy.block([[A], [numpy.eye(x)], [-numpy.eye(x)]]) 71 | F = numpy.block([[F], [numpy.zeros((2 * x, t))]]) 72 | 73 | b = numpy.block([[prng.random((m, 1)) / prng.random(1)], [10 ** 7 * numpy.ones((2 * x, 1))]]) 74 | A_t = numpy.block([[numpy.eye(t)], [-numpy.eye(t)]]) 75 | b_t = Range * numpy.ones((2 * t, 1)) 76 | 77 | H = numpy.zeros((F.shape[1], Q.shape[0])).T 78 | return MPQP_Program(A, b, c, H, Q, A_t, b_t, F) 79 | -------------------------------------------------------------------------------- /src/ppopt/solver_interface/__init__.py: -------------------------------------------------------------------------------- 1 | """PPOPT.SOLVER INIT FILE - todo fill in.""" 2 | -------------------------------------------------------------------------------- /src/ppopt/solver_interface/daqp_solver_interface.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence 2 | from ctypes import c_double, c_int 3 | 4 | import numpy 5 | 6 | try: 7 | import daqp 8 | except ImportError: 9 | pass 10 | 11 | from ..solver_interface.solver_interface_utils import SolverOutput 12 | from ..utils.general_utils import make_column 13 | 14 | Matrix = Optional[numpy.ndarray] 15 | 16 | 17 | def solve_qp_daqp(Q: numpy.ndarray, c: Matrix, A: Matrix, b: Matrix, 18 | equality_constraints: Optional[Sequence[int]] = None, 19 | verbose: bool = False, get_duals: bool = True) -> Optional[SolverOutput]: 20 | r""" 21 | Calls DAQP to solve the following optimization problem 22 | 23 | .. math:: 24 | 25 | \min_{x} \frac{1}{2}x^TQx + c^Tx 26 | 27 | .. math:: 28 | \begin{align} 29 | Ax &\leq b\\ 30 | A_{eq}x &= b_{eq}\\ 31 | x &\in R^n\\ 32 | \end{align} 33 | 34 | 35 | :param Q: Square matrix 36 | :param c: Column Vector, can be None 37 | :param A: Constraint LHS matrix, can be None 38 | :param b: Constraint RHS matrix, can be None 39 | :param equality_constraints: List of Equality constraints, can be None 40 | :param verbose: Flag for output of underlying Solver, default False 41 | :param get_duals: Flag for returning dual variable of problem, default True (false for all mixed integer models) 42 | 43 | :return: A SolverOutput object if optima found, otherwise None. 44 | """ 45 | 46 | if equality_constraints is None: 47 | equality_constraints = [] 48 | 49 | num_x = Q.shape[1] 50 | 51 | if c is None: 52 | c = numpy.zeros(num_x).reshape(-1, 1) 53 | 54 | if A is None or b is None: 55 | # this is an unconstrained problem thus we can solve the unconstrained problem in numpy 56 | # we should likely just make this a function 57 | x_sol = numpy.linalg.solve(Q, -c).reshape(-1, 1) 58 | opt_val = float((0.5 * x_sol.T @ Q @ x_sol + c.T @ x_sol)[0, 0]) 59 | return SolverOutput(opt_val, x_sol, numpy.array([]), numpy.array([]), numpy.array([])) 60 | 61 | num_constraints = A.shape[0] 62 | 63 | num_equality_constraints = len(equality_constraints) 64 | num_inequality_constraints = num_constraints - num_equality_constraints 65 | 66 | ineq = [i for i in range(A.shape[0]) if i not in equality_constraints] 67 | 68 | if num_equality_constraints != 0: 69 | new_A = A[[*equality_constraints, *ineq]].astype(c_double) 70 | new_b = b[[*equality_constraints, *ineq]].flatten().astype(c_double) 71 | # equality constraints are labeled as 5, regular inequalities are 0 72 | constraint_sense = numpy.array([*[5 for _ in range(num_equality_constraints)], 73 | *[0 for _ in range(num_inequality_constraints)]]).astype(c_int) 74 | else: 75 | new_A = A.astype(c_double) 76 | new_b = b.flatten().astype(c_double) 77 | constraint_sense = numpy.array([0 for _ in range(num_inequality_constraints)]).astype(c_int) 78 | 79 | Q_ = (0.5 * (Q + Q.T)).astype(c_double) 80 | c_ = c.flatten().astype(c_double) 81 | blower = numpy.full(num_inequality_constraints + num_equality_constraints, -1e30) 82 | x_star, opt, status, info = daqp.solve(H=Q_, f=c_, A=new_A, bupper=new_b, blower=blower, sense=constraint_sense) 83 | 84 | # if there is anything other than an optimal solution found return nothing 85 | if status != 1: 86 | return None 87 | 88 | duals = info['lam'] 89 | lagrange = numpy.zeros(num_constraints) 90 | indexing = [*equality_constraints, *ineq] 91 | lagrange[indexing] = duals 92 | lagrange[ineq] = -lagrange[ineq] 93 | 94 | slack = b - A @ make_column(x_star) 95 | 96 | active = [] 97 | 98 | for i in range(num_constraints): 99 | if abs(slack[i]) <= 10 ** -10 or lagrange[i] != 0: 100 | active.append(i) 101 | 102 | # non_zero_duals = numpy.where(lagrange != 0)[0] 103 | # active_set = numpy.array([i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints]) 104 | 105 | return SolverOutput(opt, x_star, slack.flatten(), numpy.array(active).astype('int64'), lagrange) 106 | -------------------------------------------------------------------------------- /src/ppopt/solver_interface/quad_prog_interface.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Sequence 2 | 3 | import numpy 4 | 5 | try: 6 | import quadprog 7 | except ImportError: 8 | pass 9 | 10 | from ..solver_interface.solver_interface_utils import SolverOutput 11 | from ..utils.general_utils import make_column 12 | 13 | Matrix = Optional[numpy.ndarray] 14 | 15 | 16 | def solve_qp_quadprog(Q: numpy.ndarray, c: numpy.ndarray, A: numpy.ndarray, b: numpy.ndarray, 17 | equality_constraints: Optional[Sequence[int]] = None, verbose=False, 18 | get_duals: bool = True) -> Optional[SolverOutput]: 19 | r""" 20 | Calls Quadprog to solve the following optimization problem 21 | 22 | .. math:: 23 | 24 | \min_{x} \frac{1}{2}x^TQx + c^Tx 25 | 26 | .. math:: 27 | \begin{align} 28 | Ax &\leq b\\ 29 | A_{eq}x &= b_{eq}\\ 30 | x &\in R^n\\ 31 | \end{align} 32 | 33 | 34 | :param Q: Square matrix, can be None 35 | :param c: Column Vector, can be None 36 | :param A: Constraint LHS matrix, can be None 37 | :param b: Constraint RHS matrix, can be None 38 | :param equality_constraints: List of Equality constraints 39 | :param verbose: Flag for output of underlying Solver, default False 40 | :param get_duals: Flag for returning dual variable of problem, default True (false for all mixed integer models) 41 | 42 | :return: A SolverOutput object if optima found, otherwise None. 43 | """ 44 | # the try catch is required as this interface throw exceptions for things like infeasibility and non-symmetry of obj 45 | try: 46 | if equality_constraints is None: 47 | equality_constraints = [] 48 | 49 | num_constraints = A.shape[0] 50 | num_equality_constraints = len(equality_constraints) 51 | ineq = [i for i in range(A.shape[0]) if i not in equality_constraints] 52 | 53 | if num_equality_constraints != 0: 54 | new_A = A[[*equality_constraints, *ineq]] 55 | new_b = b[[*equality_constraints, *ineq]] 56 | else: 57 | new_A = A 58 | new_b = b 59 | 60 | Q_ = .5 * (Q + Q.T) 61 | 62 | sol = quadprog.solve_qp(G=Q_, a=-c.flatten(), C=-new_A.T, b=-new_b.flatten(), meq=len(equality_constraints)) 63 | 64 | if sol is None: 65 | return None 66 | 67 | lagrange = numpy.zeros(num_constraints) 68 | x_star = sol[0] 69 | opt = sol[1] 70 | duals = sol[4] 71 | active = [] 72 | indexing = [*equality_constraints, *ineq] 73 | lagrange[indexing] = duals 74 | lagrange[ineq] = -lagrange[ineq] 75 | 76 | slack = b - A @ make_column(x_star) 77 | for i in range(num_constraints): 78 | if abs(slack[i]) <= 10 ** -10 or lagrange[i] != 0: 79 | active.append(i) 80 | 81 | # non_zero_duals = numpy.where(lagrange != 0)[0] 82 | # active_set = numpy.array( 83 | # [i for i in range(num_constraints) if i in non_zero_duals or i in equality_constraints]) 84 | 85 | return SolverOutput(opt, x_star, slack.flatten(), numpy.array(active).astype('int64'), lagrange) 86 | 87 | except ValueError: 88 | # just swallow the error as something happened Infeasibility or non-symmetry 89 | return None 90 | -------------------------------------------------------------------------------- /src/ppopt/solver_interface/solver_interface_utils.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | import numpy 5 | 6 | 7 | @dataclass 8 | class SolverOutput: 9 | """ 10 | This is the generic Solver information object. This will be the general return object from all the back end 11 | solvers. This was done to remove the need for the user to specialize IO for any particular Solver. It contains 12 | all the information you would need for the optimization solution including, optimal value, optimal solution, 13 | the active set, the value of the slack variables and the largange multipliers associated with every constraint ( 14 | these are listed) as the dual variables. 15 | 16 | Members: 17 | obj: objective value of the optimal solution \n 18 | sol: x*, numpy array \n 19 | 20 | Optional Parameters -> None or numpy.ndarray type 21 | 22 | slack: the slacks associated with every constraint \n 23 | equality_indices: the active set of the solution, including strongly and weakly active constraints \n 24 | dual: the lagrange multipliers associated with the problem\n 25 | 26 | """ 27 | obj: float 28 | sol: numpy.ndarray 29 | 30 | slack: Optional[numpy.ndarray] 31 | active_set: Optional[numpy.ndarray] 32 | dual: Optional[numpy.ndarray] 33 | 34 | def __eq__(self, other): 35 | if not isinstance(other, SolverOutput): 36 | return NotImplemented 37 | 38 | return numpy.allclose(self.slack, other.slack) and numpy.allclose(self.active_set, 39 | other.active_set) and numpy.allclose( 40 | self.dual, other.dual) and numpy.allclose(self.sol, other.sol) and numpy.allclose(self.obj, other.obj) 41 | 42 | 43 | def get_program_parameters(Q: Optional[numpy.ndarray], c: Optional[numpy.ndarray], A: Optional[numpy.ndarray], 44 | b: Optional[numpy.ndarray]): 45 | """ Given a set of possibly None optimization parameters determine the number of variables and constraints """ 46 | num_c = 0 47 | num_v = 0 48 | 49 | if Q is not None: 50 | num_v = Q.shape[0] 51 | 52 | if A is not None: 53 | num_v = A.shape[1] 54 | num_c = A.shape[0] 55 | 56 | if c is not None: 57 | num_v = numpy.size(c) 58 | 59 | return num_v, num_c 60 | -------------------------------------------------------------------------------- /src/ppopt/upop/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | PPOPT.UPOP INIT FILE - todo fill in. 3 | 4 | Currently, UPOP does not support cases in overlapping critical regions or objectives with 5 | 6 | 7 | 8 | 9 | """ 10 | -------------------------------------------------------------------------------- /src/ppopt/upop/language_generation.py: -------------------------------------------------------------------------------- 1 | # language specific generation code for C++ 2 | from typing import Tuple 3 | 4 | 5 | def gen_cpp_array(data: list, name: str, vartype: str, options: Tuple[str] = ("const",)) -> str: 6 | """ 7 | Generates a static C++ array from the provided data in the following format 8 | 9 | vartype name[size] = {data}; 10 | 11 | :param data: 12 | :param name: 13 | :param vartype: 14 | :param options: 15 | :return: 16 | """ 17 | if "const" in options: 18 | vartype = "const " + vartype 19 | 20 | return (f"{vartype} {name} [{len(data)}] = " + "{") + ','.join([str(i) for i in data]) + "};" 21 | 22 | 23 | def gen_cpp_variable(data, name: str, vartype: str, options: Tuple[str] = ("const",)) -> str: 24 | if "const" in options: 25 | vartype = "const " + vartype 26 | 27 | # if string type 28 | if "string" in vartype.lower(): 29 | return f"{vartype} {name} = \"{data}\";" 30 | 31 | return f"{vartype} {name} = {data!s};" 32 | 33 | 34 | # language specific generation code for Python 35 | 36 | 37 | def gen_python_array(data: list, name: str, vartype: str, options: Tuple[str] = ("const",)) -> str: 38 | return f"{name} = " + str(data) 39 | 40 | 41 | def gen_python_variable(data, name: str, vartype: str, options: Tuple[str] = ("const",)) -> str: 42 | data_str = str(data) 43 | 44 | if "string" in vartype.lower(): 45 | data_str = "\"" + data_str + "\"" 46 | 47 | return f"{name} = {data_str}" 48 | 49 | 50 | # language specific code for Javascript 51 | 52 | def gen_js_array(data: list, name: str, vartype: str, options: Tuple[str] = ("const",)) -> str: 53 | if "const" in options: 54 | name = "const " + name 55 | 56 | data_payload = [] 57 | 58 | if "string" in vartype: 59 | data_payload = ["\"" + str(i) + "\"" for i in data] 60 | else: 61 | data_payload = [str(i) for i in data] 62 | 63 | return f"{name} = [" + ",".join(data_payload) + "];" 64 | 65 | 66 | def gen_js_variable(data, name: str, vartype: str, options: Tuple[str] = ("const",)) -> str: 67 | if "const" in options: 68 | name = "const " + name 69 | 70 | data_str = str(data) 71 | 72 | if "string" in vartype.lower(): 73 | data_str = "\"" + data_str + "\"" 74 | 75 | return f"{name} = {data_str};" 76 | 77 | 78 | # general code generation interface 79 | 80 | def gen_array(data: list, name: str, vartype: str, options: Tuple[str] = ("const",), lang='cpp') -> str: 81 | if lang == 'cpp': 82 | return gen_cpp_array(data, name, vartype, options=options) 83 | 84 | if lang == 'python': 85 | return gen_python_array(data, name, vartype, options=options) 86 | 87 | if lang == 'js': 88 | return gen_js_array(data, name, vartype, options=options) 89 | 90 | raise RuntimeError(f"Language {lang} is not a supported lang for array generation") 91 | 92 | 93 | def gen_variable(data, name: str, vartype: str, options: Tuple[str] = ("const",), lang='cpp') -> str: 94 | if lang == 'cpp': 95 | return gen_cpp_variable(data, name, vartype, options=options) 96 | 97 | if lang == 'python': 98 | return gen_python_variable(data, name, vartype, options=options) 99 | 100 | if lang == 'js': 101 | return gen_js_variable(data, name, vartype, options=options) 102 | 103 | raise RuntimeError(f"Language {lang} is not a supported lang for variable generation") 104 | -------------------------------------------------------------------------------- /src/ppopt/upop/lib_upop/upop_cpp_template.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # noinspection SpellCheckingInspection,PyPep8 3 | cpp_upop = """ 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #ifndef UPOP_CPP_HEADER 10 | #define UPOP_CPP_HEADER 11 | 12 | namespace UPOP { 13 | #define NOT_IN_FEASIBLE_SPACE -1 14 | <==PayloadHere==> 15 | template 16 | class Constraints { 17 | public: 18 | // Constructors Destructors 19 | Constraints() {}; 20 | 21 | ~Constraints() {}; 22 | 23 | //computes constraint 24 | const bool get_value(const int i, const float_ t[theta_dim]) { 25 | 26 | int fundamental_plane_index = constraint_indices[i]; 27 | int offset = fundamental_plane_index * theta_dim; 28 | 29 | float_ eval = 0.0; 30 | 31 | if (is_eval[fundamental_plane_index]) { 32 | // get the result 33 | auto result = what_eval[fundamental_plane_index]; 34 | return constraint_parity[i] == result; 35 | } 36 | 37 | for (int j = 0; j < theta_dim; j++) { 38 | eval += t[j] * constraint_matrix_data[j + offset]; 39 | } 40 | 41 | //calculate the constraint 42 | bool value = eval < constraint_vector_data[fundamental_plane_index]; 43 | value = constraint_parity[i] ? value : !value; 44 | 45 | // cache the results for next time 46 | is_eval[fundamental_plane_index] = true; 47 | 48 | // check parity and assing in positive parity refrence 49 | what_eval[fundamental_plane_index] = value == constraint_parity[i]; 50 | 51 | return value; 52 | }; 53 | 54 | private: 55 | std::bitset is_eval; 56 | std::bitset what_eval; 57 | }; 58 | 59 | float_ evaluate_objective(float_* t, float_* x, bool include_theta_terms) { 60 | 61 | float_ obj = c_c; 62 | 63 | if (is_qp) { 64 | // is this is not a qp then we can skip this term 65 | for (int i = 0; i < x_dim; i++) { 66 | for (int j = 0; j < x_dim; j++) 67 | obj += x[i] * x[j] * Q[i*x_dim + j]; 68 | } 69 | } 70 | 71 | // rescale back to 0.5 72 | obj *= float_(.5); 73 | 74 | // calculate and add the cx term 75 | 76 | for (int i = 0; i < x_dim; i++) { 77 | obj += x[i] * c[i]; 78 | } 79 | 80 | // calculate and add the t'H'x term 81 | for (int i = 0; i < x_dim; i++) { 82 | for (int j = 0; j < theta_dim; j++) { 83 | obj += t[j] * x[i] * H[i*x_dim + j]; 84 | } 85 | } 86 | 87 | // this is a fairly common occurance 88 | if (!include_theta_terms) 89 | return obj; 90 | 91 | // calculate and add c_t t term 92 | for (int i = 0; i < theta_dim; i++) { 93 | obj += c_t[i] * t[i]; 94 | } 95 | 96 | float_ tmp = float_(0); 97 | 98 | for (int i = 0; i < theta_dim; i++) { 99 | for (int j = 0; j < theta_dim; j++) 100 | tmp += t[i] * t[j] * Q_t[i*theta_dim + j]; 101 | } 102 | 103 | return obj + float_(0.5)*tmp; 104 | } 105 | __inline bool is_inside_region(float_ * theta, int region_id, Constraints& constraints) { 106 | 107 | for (int constraint_index = region_indicies[region_id]; constraint_index < region_indicies[region_id + 1]; constraint_index++) 108 | if (!constraints.get_value(constraint_index, theta)) 109 | return false; 110 | 111 | return true; 112 | } 113 | 114 | void evaluate(int region_id, float_ theta[theta_dim], float_ output[x_dim]) { 115 | int offset = x_dim * region_id; 116 | 117 | if (region_id == NOT_IN_FEASIBLE_SPACE) { 118 | return; 119 | } 120 | 121 | for (int i = 0; i < x_dim; i++) { 122 | 123 | int function_vector_index = function_indices[i + offset]; 124 | int function_index = function_vector_index * theta_dim; 125 | 126 | 127 | float_ value = 0; 128 | 129 | //std::cout << "Function Values "; 130 | //std::cout << function_index; 131 | //std::cout << " "; 132 | 133 | for (int j = 0; j < theta_dim; j++) { 134 | //std::cout << function_matrix_data[function_index + j] << " "; 135 | value += theta[j] * function_matrix_data[function_index + j]; 136 | } 137 | 138 | value += function_vector_data[function_vector_index]; 139 | //std::cout << function_vector_data[function_vector_index]; 140 | if (function_parity[i + offset] == 0) 141 | value = -value; 142 | 143 | output[i] = value; 144 | 145 | //std::cout << std::endl; 146 | } 147 | } 148 | 149 | int locate_no_overlap(float_* theta) { 150 | // create constraints 151 | auto constraints = Constraints(); 152 | 153 | for (int region_id = 0; region_id < num_regions; region_id++) { 154 | // if no constraint is violated eager return the region_id 155 | if (is_inside_region(theta, region_id, constraints)) 156 | return region_id; 157 | } 158 | 159 | // if not in any region, return special flag 160 | return NOT_IN_FEASIBLE_SPACE; 161 | } 162 | 163 | int locate_with_overlap(float_* theta) { 164 | auto constraints = Constraints(); 165 | 166 | //initialize the tracking variables with an empty selection and the worst possible obtainable objective 167 | auto best_region_id = NOT_IN_FEASIBLE_SPACE; 168 | auto best_obj = std::numeric_limits::max(); 169 | 170 | // initialize the static output array 171 | float_ x_star[x_dim] = {}; 172 | 173 | 174 | for (int region_id = 0; region_id < num_regions; region_id++) { 175 | 176 | // if we are not inside the current region then we can skip the next section 177 | if (!is_inside_region(theta, region_id, constraints)) 178 | continue; 179 | 180 | // only compare the objective of regions that we are inside 181 | evaluate(region_id, theta, x_star); 182 | 183 | // evaluate the objective of the problem 184 | float_ curr_obj = evaluate_objective(theta, x_star, false); 185 | 186 | // swap region ID if better region was found 187 | if (curr_obj <= best_obj) { 188 | best_region_id = region_id; 189 | best_obj = curr_obj; 190 | } 191 | } 192 | 193 | return best_region_id; 194 | } 195 | 196 | //////////////////////////// 197 | // Public Interface 198 | //////////////////////////// 199 | 200 | 201 | // Find the correct region, possibility of missing all regions 202 | int point_location(float_* theta) { 203 | // Calls the correct region location code 204 | if (solution_overlap) 205 | return locate_no_overlap(theta); 206 | else 207 | return locate_with_overlap(theta); 208 | } 209 | 210 | // Evaluate a region for x(theta) at a specific theta 211 | void evaluate_region(int region_id, float_* theta, float_* output) { 212 | 213 | // if we are not in the feasible space then we can return early 214 | if (region_id == NOT_IN_FEASIBLE_SPACE) { 215 | return; 216 | } 217 | 218 | // if we are in a region then we need to evaluate the critical region 219 | evaluate(region_id, theta, output); 220 | } 221 | 222 | } 223 | 224 | #endif // !UPOP_CPP_HEADER 225 | """ 226 | -------------------------------------------------------------------------------- /src/ppopt/upop/lib_upop/upop_js_template.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # noinspection SpellCheckingInspection 3 | js_upop = """ 4 | /* 5 | // Multiparametric solution Exported on <==DATESTAMP==> 6 | 7 | <==PayloadHere==> 8 | 9 | var Constraints = /** @class */ (function () { 10 | function Constraints() { 11 | this.is_value = new Array(num_fundamental_hyper_planes); 12 | this.what_value = new Array(num_fundamental_hyper_planes); 13 | } 14 | Constraints.prototype.get_value = function (i, t) { 15 | // get the real constraint index from the constraint index map 16 | var fundamental_plant_index = constraint_indices[i]; 17 | var offset = fundamental_plant_index * theta_dim; 18 | // check if this is a previously visited constr 19 | if (this.is_value[fundamental_plant_index]) { 20 | // if visited then grab the cached value 21 | var result = this.what_value[fundamental_plant_index]; 22 | return constraint_parity[i] == result; 23 | } 24 | // evaluate 25 | var eval_ = 0; 26 | for (var j = 0; j < theta_dim; j++) { 27 | eval_ += t[j] * constraint_matrix_data[j + offset]; 28 | } 29 | // see if we violated the constraint 30 | var value = eval_ < constraint_vector_data[fundamental_plant_index]; 31 | value = constraint_parity[i] ? value : !value; 32 | // cache the value 33 | this.is_value[fundamental_plant_index] = true; 34 | this.what_value[fundamental_plant_index] = value == constraint_parity[i]; 35 | // return the computed value 36 | return value; 37 | }; 38 | return Constraints; 39 | }()); 40 | function evaluate_objective(t, x, include_theta_therms) { 41 | // the 42 | var obj = c_c * 2.0; 43 | // checks to see if we need to include the Q term in the calulcation 44 | if (is_qp) { 45 | for (var i = 0; i < x_dim; i++) { 46 | for (var j = 0; j < x_dim; j++) { 47 | obj += x[i] * x[j] * Q[i * x_dim + j]; 48 | } 49 | } 50 | } 51 | // rescale back to 0.5 due to the Q term being 0.5x'Qx 52 | obj *= 0.5; 53 | //calculate and add the c'x term 54 | for (var i = 0; i < x_dim; i++) { 55 | obj += x[i] * c[i]; 56 | } 57 | // calculate and add the t'H'x 58 | for (var i = 0; i < x_dim; i++) { 59 | for (var j = 0; j < theta_dim; j++) { 60 | obj += x[i] * t[j] * H[i * x_dim + j]; 61 | } 62 | } 63 | // if we don't need to calculate the f(t) terms then we can return early 64 | if (!include_theta_therms) { 65 | return obj; 66 | } 67 | // calcuate and add the c_t't term 68 | for (var i = 0; i < theta_dim; i++) { 69 | obj += t[i] * c_t[i]; 70 | } 71 | // calculate and add 0.5t'Q_t t 72 | var tmp = 0; 73 | for (var i = 0; i < theta_dim; i++) { 74 | for (var j = 0; j < theta_dim; j++) { 75 | tmp += t[i] * t[j] * Q_t[i * theta_dim + j]; 76 | } 77 | } 78 | return obj + tmp * 0.5; 79 | } 80 | function is_inside_region(t, region_id, constraints) { 81 | for (var constraint_index = region_indices[region_id]; constraint_index < region_indices[region_id + 1]; constraint_index++) { 82 | if (!constraints.get_value(constraint_index, t)) { 83 | return false; 84 | } 85 | } 86 | return true; 87 | } 88 | function evaluate_region(region_id, t) { 89 | var offset = x_dim * region_id; 90 | // check if we aren't in any region 91 | if (region_id == NOT_IN_FEASIBLE_SPACE) { 92 | return null; 93 | } 94 | // make sure we don't grab something over a side 95 | if (region_id >= num_regions) { 96 | return null; 97 | } 98 | // exstatiate the x(theta) array 99 | var x_star = Array(x_dim); 100 | for (var i = 0; i < x_dim; i++) { 101 | var function_vector_index = function_indices[i + offset]; 102 | var function_index = function_vector_index * theta_dim; 103 | var value = 0.0; 104 | for (var j = 0; j < theta_dim; j++) { 105 | value += t[j] * function_matrix_data[function_index + j]; 106 | } 107 | value += function_vector_data[function_vector_index]; 108 | if (function_parity[i + offset] == false) { 109 | value = -value; 110 | } 111 | x_star[i] = value; 112 | } 113 | return x_star; 114 | } 115 | function locate_region(t) { 116 | if (solution_overlap) { 117 | return locate_no_overlap(t); 118 | } 119 | return locate_with_overlap(t); 120 | } 121 | function locate_no_overlap(t) { 122 | var constraints = new Constraints(); 123 | for (var region_id = 0; region_id < num_regions; region_id++) { 124 | if (is_inside_region(t, region_id, constraints)) { 125 | return region_id; 126 | } 127 | } 128 | return NOT_IN_FEASIBLE_SPACE; 129 | } 130 | function locate_with_overlap(t) { 131 | var constraints = new Constraints(); 132 | var best_region_id = NOT_IN_FEASIBLE_SPACE; 133 | var best_obj = Number.MAX_VALUE; 134 | for (var region_id = 0; region_id < num_regions; region_id++) { 135 | if (!is_inside_region(t, region_id, constraints)) { 136 | continue; 137 | } 138 | var x_curr = evaluate_region(region_id, t); 139 | var curr_obj = evaluate_objective(t, x_curr, false); 140 | if (curr_obj <= best_obj) { 141 | best_obj = curr_obj; 142 | best_region_id = region_id; 143 | } 144 | } 145 | return best_region_id; 146 | } 147 | """ -------------------------------------------------------------------------------- /src/ppopt/upop/point_location.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | # Make this optional at some point, so we can run on more general platforms 4 | import numba 5 | import numpy 6 | 7 | from ..solution import Solution 8 | 9 | 10 | class PointLocation: 11 | 12 | def __init__(self, solution: Solution): 13 | """ 14 | Creates a compiled point location solving object for the specified solution. 15 | 16 | This is useful for real time applications on a server or desktop, as it solves the point location problem via 17 | direct enumeration. This is fast; for example a 200 region solution can be evaluated in single digit uSecs on 18 | modern computers. 19 | 20 | :param solution: An explicit solution to a multiparametric program 21 | """ 22 | 23 | # take in the solution 24 | self.solution = solution 25 | 26 | # build the overall matrix block - this is all the region hyper plane constraints stacked on top of each other 27 | 28 | A = numpy.block([[region.E] for region in self.solution.critical_regions]) 29 | b = numpy.block([[region.f] for region in self.solution.critical_regions]) 30 | 31 | # create region idx 32 | num_regions = len(self.solution.critical_regions) 33 | self.num_regions = num_regions 34 | 35 | region_constraints = numpy.zeros((num_regions + 1,)) 36 | 37 | for i, region in enumerate(self.solution.critical_regions): 38 | region_constraints[i + 1] = (region.E.shape[0] + region_constraints[i]) 39 | 40 | self.region_constraints = region_constraints 41 | 42 | # The core point location code is compiled to native instructions this reduces most overheads 43 | @numba.njit 44 | def get_region_overlap(theta: numpy.ndarray) -> numpy.ndarray: 45 | test = A @ theta <= b 46 | 47 | region_indicator = numpy.zeros((num_regions,)) 48 | for j in range(num_regions): 49 | if numpy.all(test[region_constraints[j]:region_constraints[j + 1]]): 50 | region_indicator[j] = 1 51 | 52 | return region_indicator 53 | 54 | @numba.njit 55 | def get_region_no_overlap(theta: numpy.ndarray) -> int: 56 | 57 | test = A @ theta <= b 58 | 59 | for j in range(num_regions): 60 | # if theta is in region 61 | if numpy.all(test[region_constraints[j]:region_constraints[j + 1]]): 62 | return j 63 | 64 | return -1 65 | 66 | if solution.is_overlapping: 67 | self.get_region = get_region_overlap 68 | else: 69 | self.get_region = get_region_no_overlap 70 | 71 | def locate(theta: numpy.ndarray) -> int: 72 | if solution.is_overlapping: 73 | region_indicators = self.get_region(theta) 74 | best_obj = float("inf") 75 | best_region = -1 76 | 77 | for j in range(self.num_regions): 78 | if region_indicators[j] == 1: 79 | obj = self.solution.program.evaluate_objective( 80 | self.solution.critical_regions[j].evaluate(theta), theta) 81 | if obj <= best_obj: 82 | best_region = j 83 | best_obj = obj 84 | return best_region 85 | else: 86 | return self.get_region(theta) 87 | 88 | self.eval_ = locate 89 | 90 | def is_inside(self, theta: numpy.ndarray) -> bool: 91 | """ 92 | Determines if the theta point in inside the feasible space. 93 | 94 | :param theta: A point in the theta space 95 | 96 | :return: True, if theta in region and False, if theta not in region 97 | """ 98 | return self.eval_(theta) != -1 99 | 100 | def locate(self, theta: numpy.ndarray) -> int: 101 | """ 102 | Finds the index of the critical region that theta is inside. 103 | 104 | :param theta: realization of uncertainty 105 | :return: the index of the critical region found 106 | """ 107 | return self.eval_(theta) 108 | 109 | def evaluate(self, theta: numpy.ndarray) -> Optional[numpy.ndarray]: 110 | """ 111 | Evaluates the value of x(theta). 112 | 113 | :param theta: realization of uncertainty 114 | :return: the solution to the optimization problem or None 115 | """ 116 | 117 | idx = self.eval_(theta) 118 | if idx < 0: 119 | return None 120 | return self.solution.critical_regions[idx].evaluate(theta) 121 | -------------------------------------------------------------------------------- /src/ppopt/upop/ucontroller.py: -------------------------------------------------------------------------------- 1 | # The purpose of this is to generate C++ code for to use for microcontroller but 2 | # There are hopes to expand to further outreach such as generating dlls or .so 3 | # or other language output to use solutions in other languages without needing a 4 | # new ppopt in every language 5 | 6 | from typing import List 7 | 8 | import numpy 9 | 10 | from ..critical_region import CriticalRegion 11 | from ..solution import Solution 12 | from ..solver_interface.solver_interface import solve_lp 13 | from ..utils.general_utils import make_column 14 | 15 | 16 | def determine_hyperplane(regions: List[CriticalRegion], hyper_planes: numpy.ndarray): 17 | """ 18 | Finds the 'best' splitting hyper plane for this task. 19 | 20 | In this case best means minimizing the number of intersected regions while also maximizing the difference between 21 | supported and not supported regions. 22 | 23 | :param regions: 24 | :param hyper_planes: 25 | :return: [] 26 | """ 27 | best_index = 0 28 | best_support = [] 29 | best_not_support = [] 30 | best_intersection = [] 31 | 32 | best_diff = 10 * len(regions) 33 | best_over = 10 * len(regions) 34 | 35 | # TODO: Implement redundant hyperplane removal for speedup 36 | 37 | # remove_hyper_plan = list() 38 | 39 | for i in range(hyper_planes.shape[0]): 40 | 41 | support = [] 42 | not_support = [] 43 | intersection = [] 44 | 45 | for j, region in enumerate(regions): 46 | type_ = classify_polytope(region, hyper_planes[i]) 47 | 48 | if type_ == 1: 49 | support.append(j) 50 | elif type_ == 0: 51 | intersection.append(j) 52 | elif type_ == -1: 53 | not_support.append(j) 54 | 55 | diff = abs(len(support) - len(not_support)) 56 | over = len(intersection) 57 | 58 | if diff + over < best_diff + best_over and len(support) > 0 and len(not_support) > 0: 59 | best_index = i 60 | best_diff = diff 61 | best_over = over 62 | best_support = support 63 | best_not_support = not_support 64 | best_intersection = intersection 65 | 66 | return [best_index, best_support, best_not_support, best_intersection] 67 | 68 | 69 | def classify_polytope(region: CriticalRegion, hyper_plane: numpy.ndarray) -> int: 70 | """ 71 | We are going to classify the polytopic critical region by solving 2 LPS \n 72 | 73 | max ||||-d for x in Critical region \n 74 | min ||||-d for x in Critical region \n 75 | 76 | The result of the objective function will tell us the side of the hyper plane the point is on. 77 | 78 | :param region: Critical region 79 | :param hyper_plane: A fundamental hyperplane 80 | :return: -1 if completely not in support, 0 if intersected, 1 if completely in support 81 | """ 82 | 83 | # form the needed matrices 84 | c = make_column(hyper_plane[0:region.E.shape[1]]) 85 | d = hyper_plane[-1] 86 | 87 | # solve the minimization LP 88 | min_sol = solve_lp(c, region.E, region.f) 89 | max_sol = solve_lp(-c, region.E, region.f) 90 | 91 | # this should never happen 92 | if min_sol is None or max_sol is None: 93 | raise ValueError("When solving the LPs in classify_polytope, the solution was None") 94 | 95 | # extract the objective values 96 | min_obj = min_sol.obj - d 97 | max_obj = max_sol.obj - d 98 | 99 | # of both are in support return 1 for in support 100 | if min_obj > 0 and max_obj > 0: 101 | return 1 102 | # if neither is in support return -1 for not in support 103 | elif min_obj < 0 and min_obj < 0: 104 | return -1 105 | # it is intersecting this region return 0 for intersection 106 | else: 107 | return 0 108 | 109 | 110 | class BVH: 111 | """ 112 | This is the Bounding Volume Hierarchy (BVH) class that decomposes the space that allows point location acceleration 113 | """ 114 | 115 | def __init__(self, parent, fundamental_list, region_list, depth, index): 116 | """Initializes the BVH based on a recursive constructor.""" 117 | self.depth = depth 118 | self.is_leaf = False 119 | self.region = -1 120 | self.parent = parent 121 | self.right_pos = -1 122 | self.left_pos = -1 123 | 124 | if len(region_list) == 1: 125 | self.region = region_list[0] 126 | self.is_leaf = True 127 | self.count = 1 128 | else: 129 | pass 130 | 131 | 132 | def generate_code(solution: Solution) -> List[str]: 133 | """ 134 | Generates C++17 code for point location and function evaluation on microcontrollers. This forms a BVH to 135 | accelerate solution times. WARNING: This breaks down at high dimensions. 136 | 137 | :param solution: a solution to a MPLP or MPQP solution 138 | :return: List of the strings of the C++17 datafiles that integrate with uPOP 139 | """ 140 | 141 | # TODO: Finish Implementation 142 | 143 | # fundamental_c, original_c, parity_c = find_unique_region_hyperplanes(solution) 144 | 145 | # fundamental_f, original_f, parity_f = find_unique_region_functions(solution) 146 | 147 | return ["None"] 148 | -------------------------------------------------------------------------------- /src/ppopt/utils/__init__.py: -------------------------------------------------------------------------------- 1 | """PPOPT.UTILS INIT FILE - todo fill in.""" 2 | -------------------------------------------------------------------------------- /src/ppopt/utils/chebyshev_ball.py: -------------------------------------------------------------------------------- 1 | from typing import Iterable, Optional, Sequence 2 | 3 | import numpy 4 | 5 | from ..solver_interface.solver_interface import solve_lp, solve_milp 6 | from ..utils.constraint_utilities import constraint_norm 7 | from ..utils.general_utils import make_column 8 | 9 | 10 | def chebyshev_ball(A: numpy.ndarray, b: numpy.ndarray, equality_constraints: Optional[Sequence[int]] = None, 11 | bin_vars: Optional[Sequence[int]] = None, deterministic_solver: str = 'gurobi'): 12 | r""" 13 | Chebyshev ball finds the largest ball inside a polytope defined by Ax <= b. This is solved by the following LP. 14 | 15 | .. math:: 16 | 17 | \min_{x,r} -r 18 | 19 | .. math:: 20 | 21 | \begin{align*} 22 | \text{s.t. } Ax + ||A_i||_2r &\leq b\\ 23 | A_{eq} x &= b_{eq}\\ 24 | r &\geq 0 25 | \end{align*} 26 | 27 | :param A: LHS Constraint Matrix 28 | :param b: RHS Constraint column vector 29 | :param equality_constraints: indices of rows that have strict equality A[eq] @ x = b[eq] 30 | :param bin_vars: indices of binary variables 31 | :param deterministic_solver: The underlying Solver to use, e.g. gurobi, ect 32 | :return: the SolverOutput object, None if infeasible 33 | """ 34 | if bin_vars is None: 35 | bin_vars = [] 36 | 37 | if equality_constraints is None: 38 | equality_constraints = [] 39 | 40 | # shortcut for chebyshev ball of facet of 1D region 41 | # if A.shape == 1 and len(equality_constraints) == 1: 42 | # x_star = b[equality_constraints[0]] 43 | # is_feasible = numpy.all((A@x_star - b) <= 0) 44 | # if is_feasible: 45 | # return SolverOutput(0, numpy.array([x_star, [0]]), ) 46 | # else: 47 | # return None 48 | 49 | c = numpy.zeros((A.shape[1] + 1, 1)) 50 | c[A.shape[1]][0] = -1 51 | 52 | const_norm = constraint_norm(A) 53 | const_norm = make_column( 54 | [const_norm[i][0] if i not in equality_constraints else 0 for i in range(numpy.size(A, 0))]) 55 | 56 | A_ball = numpy.block([[A, const_norm], [c.T]]) 57 | 58 | b_ball = numpy.concatenate((b, numpy.zeros((1, 1)))) 59 | 60 | if len(bin_vars) == 0: 61 | return solve_lp(c, A_ball, b_ball, equality_constraints, deterministic_solver=deterministic_solver) 62 | else: 63 | return solve_milp(c, A_ball, b_ball, equality_constraints, bin_vars, deterministic_solver=deterministic_solver) 64 | 65 | 66 | # noinspection PyUnusedLocal 67 | def chebyshev_ball_max(A: numpy.ndarray, b: numpy.ndarray, equality_constraints: Optional[Iterable[int]] = None, 68 | bin_vars: Iterable[int] = (), deterministic_solver='glpk'): 69 | r""" 70 | 71 | Chebyshev ball finds the smallest l-infinity ball the contains the polytope defined by Ax <= b. Where A has n 72 | hyper planes and d dimensions. 73 | 74 | This is solved by the following Linear program 75 | 76 | .. math:: 77 | 78 | \min_{x_{c} ,r ,y_{j} ,u_{j}} \quad r 79 | 80 | .. math:: 81 | 82 | \begin{align*} 83 | A^Ty_{j} &= e_{j}, \forall j \in {1, .., d}\\ 84 | A^Tu_{j} &= -e_{j}, \forall j \in {1, .., d}\\ 85 | -x_{cj} + b^Ty_{j} &\leq r\\ 86 | x_{cj} + b^Tu_{j} &\leq r\\ 87 | r &\geq 0\\ 88 | y_{j} &\geq 0\\ 89 | u_{j} &\geq 0\\ 90 | r &\in R\\ 91 | y_{j} &\in R^n\\ 92 | u_{j} &\in R^n\\ 93 | x_c &\in R^d 94 | \end{align*} 95 | 96 | Source: Simon Foucart's excellent book. 97 | 98 | :param A: LHS Constraint Matrix 99 | :param b: RHS Constraint column vector 100 | :param equality_constraints: indices of rows that have strict equality A[eq] @ x = b[eq] 101 | :param bin_vars: indices of binary variables 102 | :param deterministic_solver: The underlying Solver to use, e.g. gurobi, ect 103 | :return: the SolverOutput object, None if infeasible 104 | """ 105 | -------------------------------------------------------------------------------- /src/ppopt/utils/general_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | from typing import Iterable, List, Union 3 | 4 | import numpy 5 | 6 | 7 | def make_column(x: Union[List, numpy.ndarray]) -> numpy.ndarray: 8 | """ 9 | Makes x into a column vector 10 | 11 | :param x: a list or a numpy array 12 | :return: a numpy array that is a column vector 13 | """ 14 | if isinstance(x, numpy.ndarray): 15 | return x.reshape(x.size, 1) 16 | return (numpy.array(x)).reshape(len(x), 1) 17 | 18 | 19 | def make_row(x: Union[List, numpy.ndarray]) -> numpy.ndarray: 20 | """ 21 | Makes x into a row vector 22 | 23 | :param x: a list or a numpy array 24 | :return: a numpy array that is a row column 25 | """ 26 | if isinstance(x, numpy.ndarray): 27 | return x.reshape(1, x.size) 28 | return (numpy.array(x)).reshape(1, len(x)) 29 | 30 | 31 | def select_not_in_list(A: numpy.ndarray, coll: Iterable[int]) -> numpy.ndarray: 32 | """ 33 | Filters a numpy array to select all rows that are not in a list 34 | 35 | :param A: a numpy array 36 | :param coll: a list of indices that you want to remove 37 | :return: return a numpy array of A[not in coll] 38 | """ 39 | return A[[i for i in range(A.shape[0]) if i not in coll]] 40 | 41 | 42 | def render_number(x, trade_off=1e-4) -> str: 43 | if isinstance(x, str): 44 | return x 45 | 46 | if abs(x) < 10 ** -14: 47 | return "0" 48 | elif abs(x) > trade_off: 49 | return f"{float(x):.4}" 50 | else: 51 | base_10 = int(numpy.floor(numpy.log10(abs(x)))) 52 | 53 | exponent = 10 ** base_10 54 | x_scaled = x / exponent 55 | 56 | return f"{x_scaled:.4} " + "10^{" + f"{base_10}" + "}" 57 | 58 | 59 | def latex_matrix(A: Union[List[str], numpy.ndarray]) -> str: 60 | """ 61 | Creates a latex string for a given numpy array 62 | 63 | :param A: A numpy array 64 | :return: A latex string for the matrix A 65 | """ 66 | 67 | # beginning and ending of a matrix in latex 68 | start = "\\left[\\begin{matrix}" 69 | end = "\\end{matrix}\\right]" 70 | 71 | # generate an empty list of rows 72 | rows = [] 73 | 74 | # if A is a matrix then make a matrix like object 75 | if isinstance(A, numpy.ndarray): 76 | for i in range(A.shape[0]): 77 | rows.append(" & ".join([render_number(j) for j in A[i]])) 78 | return start + "\\\\".join(rows) + end 79 | 80 | # default lists as column matrix 81 | if isinstance(A, list): 82 | return start + "\\\\".join([render_number(x) for x in A]) + end 83 | 84 | raise TypeError(f"When attempting to generate the latex rep of an object A, unsupported type {type(A)} was passed") 85 | 86 | 87 | def remove_size_zero_matrices(list_matrices: List[numpy.ndarray]) -> List[numpy.ndarray]: 88 | """ 89 | Removes size zero matrices from a list 90 | 91 | :param list_matrices: A list of numpy arrays 92 | :return: returns all matrices from the list that do not have a dimension of 0 in any index 93 | """ 94 | return [i for i in list_matrices if i.shape[0] > 0 and i.shape[1] > 0] 95 | 96 | 97 | def num_cpu_cores(): 98 | """ 99 | Finds the number of allocated cores,with different behavior in windows and linux. 100 | 101 | In Windows, returns number of physical cpu cores 102 | 103 | In Linux, returns number of available cores for processing (this is for running on cluster or managed environment) 104 | 105 | :return: number of cores 106 | """ 107 | 108 | cores = os.cpu_count() 109 | 110 | # noinspection SpellCheckingInspection 111 | if 'sched_getaffinity' in dir(os): 112 | cores = len(os.sched_getaffinity(0)) 113 | 114 | return cores 115 | 116 | 117 | def ppopt_block(mat_list): 118 | """ 119 | This is an internal utility function that was created for internal use only for performance reasons. This is a 120 | replacement of ``numpy.block`` for performance sensitive sections of the codebase. This is approximately 3x faster 121 | for the matrices that are typically used here. 122 | 123 | :param mat_list: a list of matrices to concatenate in the same format as ``numpy.block`` 124 | :return: the concatenated matrix 125 | """ 126 | 127 | # if the matrix list is of the form [A, F] transform it to [[A, F]] to simplify downstream logic 128 | if not isinstance(mat_list[0], list): 129 | mat_list = [mat_list] 130 | 131 | # find the size of the output matrix on the assumption that everything is properly sized 132 | x_size = sum(el.shape[1] for el in mat_list[0]) 133 | y_size = sum(el[0].shape[0] for el in mat_list) 134 | 135 | # create the output buffer 136 | output_data = numpy.zeros((y_size, x_size)) 137 | 138 | # set initial coordinates to start placing matrices 139 | x_cursor = 0 140 | y_cursor = 0 141 | 142 | # loop over all the matrix rows in the matrix list [[A, B, C], [D, E, F], ..., [Q, W, P]] 143 | for mat_row in mat_list: 144 | y_offset = 0 145 | 146 | # write out the matrix row [..., [A, B, ..., Z], ....] into the row of the output buffer 147 | for matrix_ in mat_row: 148 | shape_ = matrix_.shape 149 | output_data[y_cursor: y_cursor + shape_[0], x_cursor: x_cursor + shape_[1]] = matrix_ 150 | x_cursor += shape_[1] 151 | y_offset = shape_[0] 152 | 153 | # we are done with this row, move to the next row and reset the x coordinate 154 | y_cursor += y_offset 155 | x_cursor = 0 156 | 157 | # return the output buffer 158 | return output_data 159 | -------------------------------------------------------------------------------- /src/ppopt/utils/geometric.py: -------------------------------------------------------------------------------- 1 | # import numpy 2 | # 3 | # from numba import jit 4 | # 5 | # 6 | # def make_subdomains(points): 7 | # return numpy.array([0]) 8 | # # return scipy.spatial.Delaunay(points).simplices 9 | # 10 | # 11 | # @jit(nopython=True) 12 | # def make_simplex(n: int): 13 | # a = numpy.zeros(shape=(n + 1, n)) 14 | # for i in range(n): 15 | # a[i + 1][i] = 1 16 | # return a 17 | # 18 | # 19 | # @jit(nopython=True) 20 | # def gen_tess_points_simplex(simplex): 21 | # """ 22 | # 23 | # :param simplex: 24 | # :return: 25 | # """ 26 | # width = simplex.shape[1] 27 | # length = simplex.shape[0] 28 | # new_length = length * (length + 1) // 2 29 | # tess = numpy.zeros(shape=(new_length, width)) 30 | # 31 | # for i in range(length): 32 | # tess[i] = simplex[i] 33 | # 34 | # index = length 35 | # extent = len(simplex) 36 | # for i in range(extent): 37 | # for j in range(i + 1, extent): 38 | # tess[index] = .5 * (simplex[i] + simplex[j]) 39 | # index += 1 40 | # return tess 41 | # 42 | # 43 | # # this adds only one subdivision point but in a pseudo optimal spot 44 | # @jit(nopython=True) 45 | # def revised_tess_simplex(simplex, half_split=False): 46 | # # find the longest and shortest edges 47 | # # ~O(n^2) where n is number of dimensions 48 | # # this runs in the order of 10 usec 49 | # 50 | # shortest_so_far = float('inf') 51 | # longest_so_far = -1 52 | # longest_index_i = 0 53 | # longest_index_j = 0 54 | # 55 | # for i in range(simplex.shape[0]): 56 | # for j in range(i + 1, simplex.shape[0]): 57 | # edge_ij = numpy.linalg.norm(simplex[i] - simplex[j]) 58 | # if edge_ij <= shortest_so_far: 59 | # shortest_so_far = edge_ij 60 | # elif edge_ij >= longest_so_far: 61 | # longest_so_far = edge_ij 62 | # longest_index_i = i 63 | # longest_index_j = j 64 | # 65 | # # enforce that I do not want a simplex more bent than ~30 60 90 66 | # # cuts the simplex into 2 based on the longest edge 67 | # 68 | # if longest_so_far >= 1.7 * shortest_so_far or half_split: 69 | # split_point = .5 * (simplex[longest_index_i] + simplex[longest_index_j]) 70 | # left_split = [i for i in range(simplex.shape[0]) if i is not longest_index_j] 71 | # right_split = [i for i in range(simplex.shape[0]) if i is not longest_index_i] 72 | # 73 | # # the simplex is sufficiently regular to take a piece out of the center 74 | # else: 75 | # split_point = numpy.sum(simplex, axis=0) 76 | # combinations = [[0] * simplex.shape[0]] * simplex.shape[0] 77 | # for i in range(simplex.shape[0]): 78 | # combinations[i] = [j for j in range(simplex.shape[0]) if j is not i] 79 | # # TODO: implement the rest of the algorithm 80 | # pass 81 | 82 | # def make_domain_subdivision(A_t, b_t): 83 | # print(A_t) 84 | # print(b_t) 85 | # 86 | # x = pypoman.compute_polytope_vertices(A_t, b_t) 87 | # print(x) 88 | # 89 | # x.append(numpy.sum(x, axis=0)) 90 | # 91 | # simplices = numpy.array(make_subdomains(x)) 92 | # x_ = numpy.array(x) 93 | # return [x_[i] for i in simplices] 94 | -------------------------------------------------------------------------------- /src/requires.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | scipy 4 | numba 5 | gurobipy 6 | pathos 7 | plotly 8 | daqp -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Main __init__.py for the ppopt package""" 2 | import os 3 | 4 | os.environ["OMP_NUM_THREADS"] = "1" # export OMP_NUM_THREADS=1 5 | os.environ["OPENBLAS_NUM_THREADS"] = "1" # export OPENBLAS_NUM_THREADS=1 6 | os.environ["MKL_NUM_THREADS"] = "1" # export MKL_NUM_THREADS=1 7 | os.environ["VECLIB_MAXIMUM_THREADS"] = "1" # export VECLIB_MAXIMUM_THREADS=1 8 | os.environ["NUMEXPR_NUM_THREADS"] = "1" # export NUMEXPR_NUM_THREADS=1 -------------------------------------------------------------------------------- /tests/geometry_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/geometry_tests/__init__.py -------------------------------------------------------------------------------- /tests/geometry_tests/test_polytope.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/geometry_tests/test_polytope.py -------------------------------------------------------------------------------- /tests/mpmiqp_solver_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/mpmiqp_solver_tests/__init__.py -------------------------------------------------------------------------------- /tests/mpmiqp_solver_tests/test_mpmiqp.py: -------------------------------------------------------------------------------- 1 | # simple test, just for coverage 2 | import numpy 3 | 4 | from src.ppopt.mp_solvers.solve_mpmiqp import solve_mpmiqp 5 | from src.ppopt.mp_solvers.solve_mpqp import mpqp_algorithm, solve_mpqp 6 | from tests.test_fixtures import simple_mpMILP, simple_mpMIQP, mpMILP_market_problem, mpMIQP_market_problem, \ 7 | bard_mpMILP_adapted, bard_mpMILP_adapted_2, bard_mpMILP_adapted_degenerate, mpMILP_1d, acevedo_mpmilp, \ 8 | pappas_multi_objective, pappas_multi_objective_2 9 | from src.ppopt.utils.mpqp_utils import get_bounds_1d 10 | 11 | 12 | def test_mpmilp_process_constraints(simple_mpMILP): 13 | simple_mpMILP.process_constraints([0, 1]) 14 | 15 | 16 | def test_mpmiqp_process_constraints(simple_mpMIQP): 17 | simple_mpMIQP.process_constraints([0, 1]) 18 | 19 | 20 | def test_mpmilp_sub_problem(simple_mpMILP): 21 | sub_problem = simple_mpMILP.generate_substituted_problem([0, 1]) 22 | 23 | assert (sub_problem.A.shape == (2, 1)) 24 | assert (sub_problem.equality_indices == [0]) 25 | 26 | 27 | def test_mpmilp_partial_feasibility(simple_mpMILP): 28 | assert (simple_mpMILP.check_bin_feasibility([0, 0])) 29 | assert (simple_mpMILP.check_bin_feasibility([1, 0])) 30 | assert (simple_mpMILP.check_bin_feasibility([0, 1])) 31 | assert (not simple_mpMILP.check_bin_feasibility([1, 1])) 32 | 33 | # this should generate the following determanistic problem 34 | # min -3x_1 s.t. x = 0, x <= theta, |theta| <= 2 35 | 36 | 37 | def test_mpmiqp_partial_feasibility(simple_mpMIQP): 38 | assert (simple_mpMIQP.check_bin_feasibility([0, 0])) 39 | assert (simple_mpMIQP.check_bin_feasibility([1, 0])) 40 | assert (simple_mpMIQP.check_bin_feasibility([0, 1])) 41 | assert (not simple_mpMIQP.check_bin_feasibility([1, 1])) 42 | 43 | # this should generate the following determanistic problem 44 | # min -3x_1 s.t. x = 0, x <= theta, |theta| <= 2 45 | 46 | 47 | def test_mpmilp_enumeration_solve(simple_mpMILP): 48 | sol = solve_mpmiqp(simple_mpMILP, num_cores=1) 49 | 50 | 51 | def test_mpmilqp_enumeration_solve(simple_mpMIQP): 52 | sol = solve_mpmiqp(simple_mpMIQP, num_cores=1) 53 | 54 | 55 | def test_mpmilqp_enumeration_solve_2(mpMIQP_market_problem): 56 | sol = solve_mpmiqp(mpMIQP_market_problem, cont_algo=mpqp_algorithm.combinatorial, num_cores=1) 57 | 58 | 59 | def test_mpmilp_evaluate(mpMILP_market_problem): 60 | # find the explicit solution to the mpMILP market problem 61 | sol = solve_mpmiqp(mpMILP_market_problem, num_cores=1) 62 | 63 | # simple test that we are not finding a hole in the middle of two regions 64 | theta_point = numpy.array([[0.0], [500.0]]) 65 | 66 | # get the solution 67 | ppopt_solution = sol.evaluate(theta_point).flatten() 68 | ppopt_value = sol.evaluate_objective(theta_point) 69 | 70 | # get the deterministic solution 71 | det_solution = mpMILP_market_problem.solve_theta(theta_point) 72 | det_primal_sol = numpy.array(det_solution.sol) 73 | 74 | assert (numpy.isclose(det_solution.obj, ppopt_value)) 75 | assert (all(numpy.isclose(det_primal_sol, ppopt_solution.flatten()))) 76 | 77 | 78 | def test_mpmiqp_evaluate(simple_mpMIQP): 79 | sol = solve_mpmiqp(simple_mpMIQP, num_cores=1) 80 | 81 | sol.evaluate(numpy.array([[1.2]])) 82 | 83 | 84 | def test_mpmilp_incorrect_algo(simple_mpMILP): 85 | try: 86 | sol = solve_mpmiqp(simple_mpMILP, "enum") 87 | assert (False) 88 | except TypeError as e: 89 | print(e) 90 | assert (True) 91 | 92 | 93 | def test_mpmilp_cr_removal_1D(bard_mpMILP_adapted): 94 | sol = solve_mpmiqp(bard_mpMILP_adapted, num_cores=1) 95 | assert (len(sol) == 2) 96 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[2.]])), 2)) 97 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[3.]])), 1)) 98 | 99 | 100 | def test_mpmilp_cr_removal_1D(bard_mpMILP_adapted_degenerate): 101 | sol = solve_mpmiqp(bard_mpMILP_adapted_degenerate, num_cores=1) 102 | assert (len(sol) == 5) 103 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[2.]])), 2)) 104 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[3.]])), 1)) 105 | 106 | 107 | def test_mpmilp_cr_removal_1D_2(bard_mpMILP_adapted_2): 108 | sol = solve_mpmiqp(bard_mpMILP_adapted_2, num_cores=1) 109 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[2.]])), 2)) 110 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[3.]])), 1)) 111 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[8.]])), 1)) 112 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[9.]])), 3)) 113 | 114 | 115 | def test_small_mpmilp_1d(mpMILP_1d): 116 | sol = solve_mpmiqp(mpMILP_1d, num_cores=1) 117 | assert (len(sol) == 3) 118 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[2.]])), 2)) 119 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[45.]])), 40)) 120 | assert (numpy.isclose(sol.evaluate_objective(numpy.array([[60.]])), 50)) 121 | 122 | expected_bounds = [(0, 40), (40, 50), (50, 100)] 123 | for cr in sol.critical_regions: 124 | lb, ub = get_bounds_1d(cr.E, cr.f) 125 | assert any(numpy.isclose(lb, expected_lb) and numpy.isclose(ub, expected_ub) for expected_lb, expected_ub in 126 | expected_bounds) 127 | 128 | 129 | def test_acevedo_mpmilp(acevedo_mpmilp): 130 | # for small problems the cost of running the parallel pool is higher than the cost of solving the problem serially 131 | sol = solve_mpmiqp(acevedo_mpmilp, num_cores=1) 132 | 133 | theta_point = numpy.array([[0.5] * 3]).reshape(-1, 1) 134 | det_sol = acevedo_mpmilp.solve_theta(theta_point) 135 | 136 | assert (numpy.allclose(sol.evaluate(theta_point).flatten(), det_sol.sol)) 137 | assert (numpy.allclose(sol.evaluate_objective(theta_point), det_sol.obj)) 138 | 139 | 140 | def test_pappas_mpmilp(pappas_multi_objective): 141 | sol = solve_mpmiqp(pappas_multi_objective, num_cores=1) 142 | 143 | assert (len(sol) == 3) 144 | 145 | 146 | def test_pappas_mpmilp_2(pappas_multi_objective_2): 147 | sol = solve_mpmiqp(pappas_multi_objective_2, num_cores=1) 148 | 149 | theta_point = numpy.array([[90.0]]) 150 | 151 | theta_sol = sol.evaluate(theta_point) 152 | theta_obj = sol.evaluate_objective(theta_point) 153 | 154 | # deterministic solution at theta = 90 155 | det_sol = pappas_multi_objective_2.solve_theta(theta_point) 156 | 157 | assert numpy.allclose(theta_sol.flatten(), det_sol.sol) 158 | assert numpy.allclose(theta_obj, det_sol.obj) 159 | -------------------------------------------------------------------------------- /tests/mpmodel_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/mpmodel_test/__init__.py -------------------------------------------------------------------------------- /tests/mpqp_solver_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/mpqp_solver_tests/__init__.py -------------------------------------------------------------------------------- /tests/mpqp_solver_tests/test_mpqp_combinatorial.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from src.ppopt.mp_solvers.mpqp_combinatorial import * 4 | from src.ppopt.mp_solvers.solver_utils import generate_children_sets 5 | from src.ppopt.mpqp_program import MPQP_Program 6 | 7 | from ..test_fixtures import * 8 | 9 | 10 | def test_check(filled_combo_tester): 11 | assert not filled_combo_tester.check([1]) 12 | assert not filled_combo_tester.check([2]) 13 | assert not filled_combo_tester.check([3]) 14 | assert not filled_combo_tester.check([1, 5]) 15 | assert not filled_combo_tester.check([1, 5, 2]) 16 | 17 | assert filled_combo_tester.check([0, 4]) 18 | assert filled_combo_tester.check([5]) 19 | assert filled_combo_tester.check([5, 6]) 20 | assert filled_combo_tester.check([5, 8]) 21 | 22 | 23 | # simple test, just for coverage 24 | def test_add_1(blank_combo_tester): 25 | num_items = len(blank_combo_tester.combos) 26 | blank_combo_tester.add_combo([]) 27 | assert len(blank_combo_tester.combos) == num_items + 1 28 | 29 | 30 | # simple test, just for coverage 31 | def test_add_2(filled_combo_tester): 32 | num_items = len(filled_combo_tester.combos) 33 | filled_combo_tester.add_combo([]) 34 | assert len(filled_combo_tester.combos) == num_items + 1 35 | 36 | 37 | def test_generate_children_1(blank_combo_tester): 38 | output = generate_children_sets([], 8, blank_combo_tester) 39 | assert output == [[0], [1], [2], [3], [4], [5], [6], [7]] 40 | 41 | 42 | def test_generate_children_2(blank_combo_tester): 43 | output = generate_children_sets([1, 2, 3, 5], 8, blank_combo_tester, ) 44 | assert output == [[1, 2, 3, 5, 6], [1, 2, 3, 5, 7]] 45 | 46 | 47 | def test_generate_children_3(filled_combo_tester): 48 | output = generate_children_sets([], 8, filled_combo_tester) 49 | assert output == [[0], [4], [5], [6], [7]] 50 | 51 | 52 | def test_generate_children_4(filled_combo_tester): 53 | output = generate_children_sets([0], 8, filled_combo_tester) 54 | assert output == [[0, 4], [0, 5], [0, 6], [0, 7]] 55 | 56 | 57 | def test_generate_children_5(blank_combo_tester): 58 | output = generate_children_sets([0], 3, blank_combo_tester) 59 | assert output == [[0, 1], [0, 2]] 60 | 61 | 62 | def test_generate_children_6(blank_combo_tester): 63 | output = generate_children_sets([0, 1], 3, blank_combo_tester) 64 | assert output == [[0, 1, 2]] 65 | 66 | 67 | def test_generate_children_7(blank_combo_tester): 68 | A = numpy.array( 69 | [[1, 1, 0, 0], [0, 0, 1, 1], [-1, 0, -1, 0], [0, -1, 0, -1], [-1, 0, 0, 0], [0, -1, 0, 0], [0, 0, -1, 0], 70 | [0, 0, 0, -1]]) 71 | b = numpy.array([350, 600, 0, 0, 0, 0, 0, 0]).reshape(8, 1) 72 | c = 25 * numpy.array([[1], [1], [1], [1]]) 73 | F = numpy.array([[0, 0], [0, 0], [-1, 0], [0, -1], [0, 0], [0, 0], [0, 0], [0, 0]]) 74 | Q = numpy.diag([153, 162, 162, 126]) 75 | 76 | CRa = numpy.vstack((numpy.eye(2), -numpy.eye(2))) 77 | CRb = numpy.array([1000, 1000, 0, 0]).reshape(-1, 1) 78 | H = numpy.zeros((F.shape[1], Q.shape[0])) 79 | program = MPQP_Program(A, b, c, H, Q, CRa, CRb, F, equality_indices=[0]) 80 | 81 | output = generate_children_sets(program.equality_indices, program.num_constraints(), blank_combo_tester) 82 | assert output == [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [0, 7]] 83 | 84 | 85 | def test_check_feasibility_1(quadratic_program, blank_combo_tester): 86 | output = check_child_feasibility(quadratic_program, [[], [1], [2], [0, 1, 2, 3, 4]], blank_combo_tester) 87 | assert output == [[], [1], [2]] 88 | -------------------------------------------------------------------------------- /tests/mpqp_solver_tests/test_solution.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import pytest 3 | 4 | from tests.test_fixtures import blank_solution, filled_solution, region 5 | 6 | 7 | def test_add_region_1(blank_solution, region): 8 | assert len(blank_solution.critical_regions) == 0 9 | blank_solution.add_region(region) 10 | assert len(blank_solution.critical_regions) == 1 11 | 12 | 13 | def test_add_region_2(blank_solution, region): 14 | num_items = len(blank_solution.critical_regions) 15 | blank_solution.add_region(region) 16 | assert len(blank_solution.critical_regions) == num_items + 1 17 | 18 | 19 | def test_evaluate_1(blank_solution): 20 | theta = numpy.array([[.5], [.5]]) 21 | assert blank_solution.evaluate(theta) is None 22 | 23 | 24 | def test_evaluate_2(filled_solution): 25 | theta = numpy.array([[.5], [.5]]) 26 | assert filled_solution.evaluate(theta) is not None 27 | assert numpy.allclose(filled_solution.evaluate(theta), theta) 28 | 29 | 30 | def test_get_region_1(filled_solution): 31 | theta = numpy.array([[.5], [.5]]) 32 | assert filled_solution.get_region(theta) is not None 33 | 34 | 35 | def test_get_region_2(blank_solution): 36 | theta = numpy.array([[.5], [.5]]) 37 | assert blank_solution.get_region(theta) is None 38 | 39 | @pytest.mark.skip(reason="I am scaling the matrix array, expected output has changed") 40 | def test_verify_1(factory_solution): 41 | theta = numpy.random.random((2, 1)) * 100 42 | 43 | region = factory_solution.get_region(theta) 44 | 45 | print('') 46 | print(region.evaluate(theta)) 47 | print(region.lagrange_multipliers(theta)) 48 | print(region.equality_indices) 49 | print(factory_solution.program.solve_theta(theta)) 50 | 51 | assert factory_solution.verify_solution() 52 | -------------------------------------------------------------------------------- /tests/other_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/other_tests/__init__.py -------------------------------------------------------------------------------- /tests/other_tests/test_chebyshev_ball.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from src.ppopt.utils.chebyshev_ball import chebyshev_ball 4 | 5 | 6 | def test_chebyshev_ball_1(): 7 | A = numpy.vstack((numpy.eye(5), -numpy.eye(5))) 8 | b = numpy.ones((10, 1)) 9 | chebyshev_soln = chebyshev_ball(A, b, deterministic_solver='gurobi') 10 | # make sure it solved 11 | # solution is [0,0,0,0,0,1] -> y = [0,0,0,0,0], r = 1 12 | assert numpy.allclose(numpy.array([0.0, 0.0, 0.0, 0.0, 0.0, 1.0]), chebyshev_soln.sol) 13 | 14 | 15 | def test_chebyshev_ball_2(): 16 | A = numpy.vstack((numpy.eye(5), -numpy.eye(5))) 17 | b = numpy.ones((10, 1)) 18 | chebyshev_soln = chebyshev_ball(A, b, bin_vars=None, deterministic_solver='gurobi') 19 | # make sure it solved 20 | # solution is [0,0,0,0,0,1] -> y = [0,0,0,0,0], r = 1 21 | assert numpy.allclose(numpy.array([0.0, 0.0, 0.0, 0.0, 0.0, 1.0]), chebyshev_soln.sol) 22 | -------------------------------------------------------------------------------- /tests/other_tests/test_constraint_utilities.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | import pytest 3 | 4 | from src.ppopt.utils.constraint_utilities import ( 5 | cheap_remove_redundant_constraints, 6 | find_implicit_equalities, 7 | is_full_rank, 8 | process_program_constraints, 9 | process_region_constraints, 10 | remove_duplicate_rows, 11 | remove_strongly_redundant_constraints, 12 | remove_zero_rows, 13 | row_equality, 14 | scale_constraint, 15 | ) 16 | from src.ppopt.utils.general_utils import make_row 17 | 18 | 19 | def test_constraint_norm_1(): 20 | A = numpy.random.random((10, 10)) 21 | b = numpy.random.random((10, 1)) 22 | 23 | [As, _] = scale_constraint(A, b) 24 | 25 | results = numpy.linalg.norm(As, axis=1) 26 | assert numpy.allclose(numpy.ones(10), results) 27 | 28 | 29 | def test_constraint_norm_2(): 30 | A = -numpy.random.random((10, 10)) 31 | b = numpy.random.random((10, 1)) 32 | [_, _] = scale_constraint(A, b) 33 | 34 | def test_scale_constraint(): 35 | A = 2 * numpy.eye(3) 36 | b = numpy.ones(3) 37 | 38 | A, b = scale_constraint(A, b) 39 | 40 | assert numpy.allclose(A, numpy.eye(3)) 41 | assert numpy.allclose(b, .5 * numpy.ones(3)) 42 | 43 | 44 | def test_remove_zero_rows(): 45 | A = numpy.random.random((10, 10)) 46 | b = numpy.random.random((10, 1)) 47 | A[3] = 0 48 | A[7] = 0 49 | index = [0, 1, 2, 4, 5, 6, 8, 9] 50 | 51 | [A_, b_] = remove_zero_rows(A, b) 52 | assert numpy.allclose(A_, A[index]) 53 | assert numpy.allclose(b_, b[index]) 54 | assert A_.shape == A[index].shape 55 | assert b_.shape == b[index].shape 56 | 57 | 58 | def test_row_equality_1(): 59 | a = numpy.array([1, 2, 4]) 60 | b = numpy.array([1, 2, 3]) 61 | 62 | assert not row_equality(a, b) 63 | 64 | 65 | def test_row_equality_2(): 66 | a = numpy.array([1, 2, 3]) 67 | b = numpy.array([1, 2, 3]) 68 | 69 | assert row_equality(a, b) 70 | 71 | 72 | def test_remove_duplicate_rows(): 73 | A = numpy.array([[1, 2, 3], [1, 2, 3], [1, 2, 3], [1, 1, 1]]) 74 | b = numpy.array([[1], [2], [1], [1]]) 75 | [A, b] = remove_duplicate_rows(A, b) 76 | 77 | assert A.shape == (3, 3) 78 | assert b.shape == (3, 1) 79 | 80 | 81 | def test_is_full_rank_1(): 82 | A = numpy.eye(5) 83 | assert is_full_rank(A) 84 | 85 | 86 | def test_is_full_rank_2(): 87 | A = numpy.array([[1, 2, 3], [1, 0, 3]]) 88 | assert is_full_rank(A) 89 | 90 | 91 | def test_is_full_rank_3(): 92 | A = numpy.eye(10) 93 | A[-1, -1] = 0 94 | assert not is_full_rank(A) 95 | 96 | 97 | def test_is_full_rank_4(): 98 | A = numpy.eye(4) 99 | assert is_full_rank(A, [1, 2, 3]) 100 | 101 | 102 | def test_is_full_rank_5(): 103 | A = numpy.array([[1, 0], [1, 0], [0, 1]]) 104 | assert not is_full_rank(A) 105 | assert not is_full_rank(A, [0, 1]) 106 | assert is_full_rank(A, [1, 2]) 107 | 108 | 109 | def test_is_full_rank_6(): 110 | A = numpy.eye(2) 111 | assert is_full_rank(A, []) 112 | 113 | 114 | def test_remove_redundant_constraints(): 115 | A = numpy.array([[-1, 0], [0, -1], [-1, 1], [-1, 1]]) 116 | b = numpy.array([[0], [0], [1], [20]]) 117 | 118 | # [As, bs] = process_region_constraints(A, b) 119 | As, bs = cheap_remove_redundant_constraints(A, b) 120 | As, bs = remove_strongly_redundant_constraints(As, bs) 121 | # As, bs = facet_ball_elimination(As, bs) 122 | 123 | A_ss, b_ss = scale_constraint(A, b) 124 | 125 | assert numpy.allclose(As, A_ss[[0, 1, 2]]) 126 | assert numpy.allclose(bs, b_ss[[0, 1, 2]]) 127 | 128 | 129 | def test_process_region_constraints(): 130 | A = numpy.block([[numpy.eye(3)], [-numpy.eye(3)], [make_row([1, 1, 1])]]) 131 | 132 | b = numpy.block([[numpy.ones((3, 1))], [numpy.zeros((3, 1))], [numpy.array([[1]])]]) 133 | 134 | [A, b] = process_region_constraints(A, b) 135 | 136 | assert A.shape == (4, 3) 137 | assert b.shape == (4, 1) 138 | 139 | 140 | @pytest.mark.skip(reason="I am scaling the matrix array, expected output has changed") 141 | def test_facet_ball_elimination(): 142 | A = numpy.block([[numpy.eye(2)], [-numpy.eye(2)]]) 143 | b = numpy.array([[1], [1], [0], [0]]) 144 | 145 | A_t = numpy.block([[numpy.eye(2)], [-numpy.eye(2)], [numpy.array([[1, 1]])]]) 146 | b_t = numpy.array([[2], [2], [0], [0], [1]]) 147 | 148 | A_r = numpy.block([[A], [A_t]]) 149 | b_r = numpy.block([[b], [b_t]]) 150 | 151 | [_, _] = process_region_constraints(A_r, b_r) 152 | 153 | def test_process_program_constraints(): 154 | 155 | A = numpy.array([[0,0,0,0],[1,2,3,5],[0,0,0,0]]) 156 | b = numpy.array([[0],[1],[1]]).reshape(-1,1) 157 | F = numpy.array([[1,2],[3,4],[0,0]]) 158 | 159 | A_t = numpy.block([[numpy.eye(2)],[-numpy.eye(2)]]) 160 | b_t = numpy.array([[1],[1],[1],[1]]).reshape(-1,1) 161 | 162 | A, b, F, A_t, b_t = process_program_constraints(A, b, F, A_t, b_t) 163 | 164 | A_test = numpy.array([[1,2,3,5]]) 165 | b_test = numpy.array([[1]]) 166 | F_test = numpy.array([[3, 4]]) 167 | A_t_test = numpy.block([[numpy.eye(2)], [-numpy.eye(2)], [numpy.array([[0.0,0.0]])],[numpy.array([[-1.0,-2.0]])]]) 168 | b_t_test = numpy.array([[1], [1], [1], [1], [1], [0]]) 169 | 170 | assert numpy.allclose(A_test, A) 171 | assert numpy.allclose(b_test, b) 172 | assert numpy.allclose(F_test, F) 173 | assert numpy.allclose(A_t_test, A_t) 174 | assert numpy.allclose(b_t_test, b_t) 175 | 176 | def test_find_implicit_equalities(): 177 | A = numpy.array([[0, 0, 0, 0], [1, 2, 3, 5], [0, 0, 0, 0], [-1,-2,-3,-5]]) 178 | b = numpy.array([[0], [1], [1],[-1]]).reshape(-1, 1) 179 | F = numpy.array([[1, 2], [3, 4], [0, 0],[-3,-4]]) 180 | 181 | A_t = numpy.block([[numpy.eye(2)],[-numpy.eye(2)]]) 182 | b_t = numpy.array([[1],[1],[1],[1]]).reshape(-1,1) 183 | 184 | A, b, F, eq = find_implicit_equalities(A, b, F, []) 185 | 186 | A_test = numpy.array([[1,2,3,5],[0,0,0,0],[0,0,0,0]]) 187 | b_test = numpy.array([[1],[0],[1]]) 188 | F_test = numpy.array([[3,4],[1,2],[0,0]]) 189 | eq_test = [0] 190 | 191 | assert numpy.allclose(A_test, A) 192 | assert numpy.allclose(b_test, b) 193 | assert numpy.allclose(F_test, F) 194 | assert numpy.allclose(eq_test, eq) 195 | 196 | # print(process_program_constraints(A, b, F, A_t, b_t)) 197 | -------------------------------------------------------------------------------- /tests/other_tests/test_critical_region.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import numpy 4 | import pytest 5 | 6 | from src.ppopt.critical_region import CriticalRegion 7 | from src.ppopt.utils.general_utils import make_column 8 | 9 | 10 | @pytest.fixture() 11 | def region() -> CriticalRegion: 12 | """A square critical region with predictable properties.""" 13 | A = numpy.eye(2) 14 | b = numpy.zeros((2, 1)) 15 | C = numpy.eye(2) 16 | d = numpy.zeros((2, 1)) 17 | E = numpy.block([[numpy.eye(2)], [-numpy.eye(2)]]) 18 | f = make_column([1, 1, 0, 0]) 19 | return CriticalRegion(A, b, C, d, E, f, []) 20 | 21 | def test_docs(region): 22 | assert CriticalRegion.__doc__ is not None 23 | 24 | 25 | def test_repr(region): 26 | assert len(region.__repr__()) > 0 27 | 28 | 29 | def test_critical_region_construction(region): 30 | assert region is not None 31 | 32 | 33 | def test_evaluate(region): 34 | theta = numpy.ones((2, 1)) 35 | assert numpy.allclose(region.evaluate(theta), theta) 36 | 37 | 38 | def test_lagrange_multipliers(region): 39 | theta_point = numpy.array([[1], [1]]) 40 | assert numpy.allclose(theta_point, region.lagrange_multipliers(theta_point)) 41 | 42 | 43 | def test_is_inside_1(region): 44 | num_tests = 10 45 | for _ in range(num_tests): 46 | theta = numpy.random.random((2, 1)) 47 | assert region.is_inside(theta) 48 | 49 | 50 | def test_is_inside_2(region): 51 | num_tests = 10 52 | for _ in range(num_tests): 53 | theta = numpy.random.random((2, 1)) - numpy.array([[1], [1]]) 54 | assert not region.is_inside(theta) 55 | 56 | 57 | def test_is_full_dimension_1(region): 58 | assert region.is_full_dimension() 59 | 60 | 61 | def test_is_full_dimension_2(region): 62 | region_2 = copy.deepcopy(region) 63 | region_2.f = make_column([0, 1, 0, 0]) 64 | assert not region_2.is_full_dimension() 65 | 66 | 67 | def test_get_constraints_1(region): 68 | region_2 = copy.deepcopy(region) 69 | assert len(region_2.get_constraints()) == 2 70 | -------------------------------------------------------------------------------- /tests/other_tests/test_general_utils.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from src.ppopt.utils.general_utils import ( 4 | make_column, 5 | make_row, 6 | remove_size_zero_matrices, 7 | select_not_in_list, 8 | ) 9 | 10 | 11 | def test_make_column_1(): 12 | test_case = make_column([1, 1, 1, 1]) 13 | correct_result = numpy.array([[1], [1], [1], [1]]) 14 | 15 | assert numpy.allclose(correct_result, test_case) 16 | assert correct_result.shape == test_case.shape 17 | 18 | 19 | def test_make_column_2(): 20 | k = numpy.ones((2, 2)) 21 | assert make_column(k).shape == (4, 1) 22 | 23 | 24 | def test_make_column_3(): 25 | k = numpy.ones((2,)) 26 | assert make_column(k).shape == (2, 1) 27 | 28 | 29 | def test_make_row_1(): 30 | test_case = make_row([1, 1, 1, 1]) 31 | correct_result = numpy.array([[1, 1, 1, 1]]) 32 | 33 | assert numpy.allclose(correct_result, test_case) 34 | assert correct_result.shape == test_case.shape 35 | 36 | 37 | def test_make_row_2(): 38 | k = numpy.ones((2, 2)) 39 | assert make_row(k).shape == (1, 4) 40 | 41 | 42 | def test_make_row_3(): 43 | k = numpy.ones((2,)) 44 | assert make_row(k).shape == (1, 2) 45 | 46 | 47 | def test_select_not_in_list_1(): 48 | A = numpy.eye(5) 49 | B = select_not_in_list(A, [0]) 50 | assert numpy.allclose(A[[1, 2, 3, 4]], B) 51 | 52 | 53 | def test_select_not_in_list_2(): 54 | A = numpy.eye(5) 55 | B = select_not_in_list(A, [0, 1, 2, 3, 4]) 56 | assert B.size == 0 57 | 58 | 59 | def test_remove_size_zero_matrices(): 60 | A = [numpy.eye(0), numpy.eye(1), numpy.zeros((2, 0))] 61 | assert remove_size_zero_matrices(A) == [numpy.eye(1)] 62 | 63 | -------------------------------------------------------------------------------- /tests/other_tests/test_mpqp_utils.py: -------------------------------------------------------------------------------- 1 | from src.ppopt.utils.mpqp_utils import gen_cr_from_active_set 2 | from tests.test_fixtures import qp_problem, quadratic_program, simple_mpqp_problem 3 | 4 | 5 | def test_check_feasibility_1(quadratic_program): 6 | assert quadratic_program.check_feasibility([]) 7 | 8 | 9 | def test_check_feasibility_2(quadratic_program): 10 | assert quadratic_program.check_feasibility([0]) 11 | 12 | 13 | def test_check_feasibility_3(quadratic_program): 14 | assert not quadratic_program.check_feasibility([6, 7, 8], False) 15 | 16 | 17 | def test_check_optimality(simple_mpqp_problem): 18 | assert simple_mpqp_problem.check_optimality([]) is not None 19 | assert simple_mpqp_problem.check_optimality([0]) is None 20 | assert simple_mpqp_problem.check_optimality([1]) is not None 21 | assert simple_mpqp_problem.check_optimality([0, 1]) is None 22 | 23 | 24 | def test_build_cr_from_active_set(qp_problem): 25 | qp_problem.scale_constraints() 26 | 27 | assert gen_cr_from_active_set(qp_problem, [2, 3]) is not None 28 | assert gen_cr_from_active_set(qp_problem, [0, 2, 3]) is not None 29 | assert gen_cr_from_active_set(qp_problem, [0, 2, 3, 4]) is not None 30 | assert gen_cr_from_active_set(qp_problem, [0, 2, 3, 4]) is not None 31 | 32 | 33 | def test_process_constraints_1(qp_problem): 34 | qp_problem.scale_constraints() 35 | 36 | -------------------------------------------------------------------------------- /tests/other_tests/test_plot.py: -------------------------------------------------------------------------------- 1 | from src.ppopt.mp_solvers.solve_mpqp import solve_mpqp 2 | from src.ppopt.plot import parametric_plot, parametric_plot_1D, plotly_plot 3 | from tests.test_fixtures import factory_solution, simple_mpqp_problem 4 | 5 | import pytest 6 | 7 | @pytest.mark.skip(reason="no way of currently testing this") 8 | def test_matplotlib_plot(factory_solution): 9 | parametric_plot(factory_solution, show=False) 10 | 11 | @pytest.mark.skip(reason="no way of currently testing this") 12 | def test_plotly_plot(factory_solution): 13 | plotly_plot(factory_solution, show=False) 14 | 15 | @pytest.mark.skip(reason="no way of currently testing this") 16 | def test_matplotlib_plot_1d(simple_mpqp_problem): 17 | parametric_plot_1D(solve_mpqp(simple_mpqp_problem), show=False) 18 | -------------------------------------------------------------------------------- /tests/other_tests/test_problem_generator.py: -------------------------------------------------------------------------------- 1 | from src.ppopt.problem_generator import generate_mplp, generate_mpqp 2 | 3 | 4 | def test_mplp_problem_generator(): 5 | # check that this won't give out infeasible problems 6 | assert generate_mplp(2, 2, 40).feasible_theta_point() is not None 7 | 8 | 9 | def test_mpqp_problem_generator(): 10 | # check that this won't give out infeasible problems 11 | assert generate_mpqp(2, 2, 40).feasible_theta_point() is not None 12 | -------------------------------------------------------------------------------- /tests/other_tests/test_solve_mpqp.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from src.ppopt.utils.chebyshev_ball import chebyshev_ball 4 | from src.ppopt.mp_solvers import mpqp_parrallel_combinatorial 5 | from src.ppopt.mp_solvers.solve_mpqp import mpqp_algorithm, solve_mpqp 6 | from tests.test_fixtures import qp_problem, simple_mpLP, portfolio_problem_analog, non_negative_least_squares, over_determined_as_mplp 7 | 8 | 9 | def test_solve_mpqp_combinatorial(qp_problem): 10 | solution = solve_mpqp(qp_problem, mpqp_algorithm.combinatorial) 11 | 12 | assert solution is not None 13 | assert len(solution.critical_regions) == 4 14 | 15 | 16 | def test_solve_mpqp_gupta_parallel_exp(qp_problem): 17 | # solution = solve_mpqp(qp_problem, mpqp_algorithm.combinatorial_parallel_exp) 18 | 19 | solution = mpqp_parrallel_combinatorial.solve(qp_problem, 4) 20 | 21 | assert solution is not None 22 | assert len(solution.critical_regions) == 4 23 | 24 | 25 | def test_solve_mpqp_geometric(qp_problem): 26 | solution = solve_mpqp(qp_problem, mpqp_algorithm.geometric) 27 | 28 | assert solution is not None 29 | assert len(solution.critical_regions) == 4 30 | 31 | 32 | def test_solve_mpqp_geometric_parallel(qp_problem): 33 | solution = solve_mpqp(qp_problem, mpqp_algorithm.geometric_parallel) 34 | 35 | assert solution is not None 36 | assert len(solution.critical_regions) == 4 37 | 38 | 39 | def test_solve_mpqp_geometric_parallel_exp(qp_problem): 40 | solution = solve_mpqp(qp_problem, mpqp_algorithm.geometric_parallel_exp) 41 | 42 | assert solution is not None 43 | assert len(solution.critical_regions) == 4 44 | 45 | 46 | def test_solve_mpqp_graph(qp_problem): 47 | solution = solve_mpqp(qp_problem, mpqp_algorithm.graph) 48 | 49 | assert solution is not None 50 | assert len(solution.critical_regions) == 4 51 | 52 | 53 | def test_solve_mpqp_graph_exp(qp_problem): 54 | solution = solve_mpqp(qp_problem, mpqp_algorithm.graph_exp) 55 | 56 | assert solution is not None 57 | assert len(solution.critical_regions) == 4 58 | 59 | 60 | def test_solve_mpqp_graph_parallel(qp_problem): 61 | solution = solve_mpqp(qp_problem, mpqp_algorithm.graph_parallel) 62 | 63 | assert solution is not None 64 | assert len(solution.critical_regions) == 4 65 | 66 | 67 | def test_solve_mpqp_graph_parallel_exp(qp_problem): 68 | qp_problem.solver.solvers['lp'] = 'glpk' 69 | solution = solve_mpqp(qp_problem, mpqp_algorithm.graph_parallel_exp) 70 | assert solution is not None 71 | assert len(solution.critical_regions) == 4 72 | 73 | 74 | def test_solve_mpqp_combinatorial_graph(qp_problem): 75 | qp_problem.solver.solvers['lp'] = 'glpk' 76 | solution = solve_mpqp(qp_problem, mpqp_algorithm.combinatorial_graph) 77 | 78 | assert solution is not None 79 | assert len(solution.critical_regions) == 4 80 | 81 | 82 | def test_solve_mplp_combinatorial(simple_mpLP): 83 | solution = solve_mpqp(simple_mpLP, mpqp_algorithm.combinatorial) 84 | assert solution is not None 85 | assert len(solution.critical_regions) == 4 86 | 87 | def tesT_solve_mplp_overdetermined_active_set(over_determined_as_mplp): 88 | 89 | solution = solve_mpqp(over_determined_as_mplp, mpqp_algorithm.combinatorial) 90 | assert solution is not None 91 | assert len(solution.critical_regions) == 5 92 | 93 | 94 | def test_solve_missing_algorithm(qp_problem): 95 | try: 96 | solution = solve_mpqp(qp_problem, algorithm="cambinatorial") 97 | assert False 98 | except TypeError as e: 99 | print(e) 100 | assert True 101 | 102 | 103 | def test_solve_geometric_portfolio(portfolio_problem_analog): 104 | sol_geo = solve_mpqp(portfolio_problem_analog, mpqp_algorithm.geometric) 105 | sol_combi = solve_mpqp(portfolio_problem_analog, mpqp_algorithm.combinatorial) 106 | 107 | # they should have the same number of critical regions 108 | assert (len(sol_geo) == len(sol_combi)) 109 | 110 | # test the center of each critical region 111 | for cr in sol_geo.critical_regions: 112 | 113 | chev_sol = chebyshev_ball(cr.E, cr.f, deterministic_solver=portfolio_problem_analog.solver.solvers['lp']) 114 | center = chev_sol.sol[0].reshape(-1, 1) 115 | 116 | geo_ans = sol_geo.evaluate(center) 117 | combi_ans = sol_combi.evaluate(center) 118 | 119 | if not numpy.allclose(geo_ans, combi_ans): 120 | assert False 121 | 122 | def test_solve_geometric_nnls(non_negative_least_squares): 123 | sol_geo = solve_mpqp(non_negative_least_squares, mpqp_algorithm.geometric) 124 | sol_combi = solve_mpqp(non_negative_least_squares, mpqp_algorithm.combinatorial) 125 | 126 | # they should have the same number of critical regions 127 | assert(len(sol_geo) == len(sol_combi)) 128 | 129 | # test the center of each critical region 130 | for cr in sol_geo.critical_regions: 131 | 132 | chev_sol = chebyshev_ball(cr.E, cr.f, deterministic_solver=non_negative_least_squares.solver.solvers['lp']) 133 | center = chev_sol.sol[0].reshape(-1,1) 134 | 135 | geo_ans = sol_geo.evaluate(center) 136 | combi_ans = sol_combi.evaluate(center) 137 | 138 | if not numpy.allclose(geo_ans, combi_ans): 139 | assert False 140 | -------------------------------------------------------------------------------- /tests/simple_fixtures/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/simple_fixtures/__init__.py -------------------------------------------------------------------------------- /tests/solver_interface_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/solver_interface_tests/__init__.py -------------------------------------------------------------------------------- /tests/solver_interface_tests/test_glpk_solver.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from src.ppopt.solver_interface.cvxopt_interface import solve_lp_cvxopt 4 | 5 | 6 | def test_solve_lp_1(): 7 | A = numpy.array([[1.0, 1.0], [1.0, -1.0]]) 8 | b = numpy.array([[0.0], [0.0]]) 9 | c = numpy.array([[0.0], [0.0]]) 10 | 11 | soln = solve_lp_cvxopt(c, A, b, [0, 1]) 12 | assert numpy.allclose(numpy.zeros(2), soln.sol) 13 | 14 | 15 | def test_solve_lp_2(): 16 | A = numpy.array([[-1, 0], [0, -1], [-1, 1]], dtype='float64') 17 | b = numpy.array([[0], [0], [1]], dtype='float64') 18 | c = numpy.array([[1], [1]], dtype='float64') 19 | 20 | soln = solve_lp_cvxopt(c, A, b) 21 | assert numpy.allclose(numpy.zeros(2), soln.sol) 22 | print(soln) 23 | 24 | def test_infeasfible_lp(): 25 | A = numpy.array([[1], [-1]], dtype='float64') 26 | b = numpy.array([[-1], [-1]], dtype='float64') 27 | c = None 28 | soln = solve_lp_cvxopt(c, A, b) 29 | assert soln is None 30 | 31 | 32 | def test_indefinite_lp(): 33 | assert solve_lp_cvxopt(None, None, None) is None 34 | 35 | 36 | def test_indefinite_lp_2(): 37 | assert solve_lp_cvxopt(None, numpy.zeros((0, 0)), None) is None 38 | -------------------------------------------------------------------------------- /tests/solver_interface_tests/test_gurobi_solver.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | 3 | from src.ppopt.solver_interface.gurobi_solver_interface import ( 4 | solve_lp_gurobi, 5 | solve_milp_gurobi, 6 | solve_miqp_gurobi, 7 | solve_qp_gurobi, 8 | ) 9 | 10 | 11 | def test_solve_lp_1(): 12 | A = numpy.array([[1.0, 1.0], [1.0, -1.0]]) 13 | b = numpy.array([[0.0], [0.0]]) 14 | c = numpy.array([[0.0], [0.0]]) 15 | 16 | soln = solve_lp_gurobi(c, A, b, [0, 1]) 17 | assert numpy.allclose(numpy.zeros(2), soln.sol) 18 | 19 | 20 | def test_solve_lp_2(): 21 | A = numpy.array([[-1, 0], [0, -1], [-1, 1]], dtype='float64') 22 | b = numpy.array([[0], [0], [1]], dtype='float64') 23 | c = numpy.array([[1], [1]], dtype='float64') 24 | 25 | soln = solve_lp_gurobi(c, A, b) 26 | assert numpy.allclose(numpy.zeros(2), soln.sol) 27 | 28 | 29 | def test_solve_qp_1(): 30 | rand_dim = numpy.random.randint(1, 10) 31 | Q = numpy.eye(rand_dim) 32 | soln = solve_qp_gurobi(Q, None, None, None) 33 | assert numpy.allclose(numpy.zeros(rand_dim), soln.sol) 34 | 35 | 36 | def test_solve_qp_2(): 37 | rand_dim = numpy.random.randint(1, 10) 38 | Q = numpy.eye(rand_dim) 39 | A = numpy.vstack((numpy.eye(rand_dim), -numpy.eye(rand_dim))) 40 | b = numpy.ones((rand_dim * 2, 1)) 41 | 42 | soln = solve_qp_gurobi(Q, None, A, b) 43 | 44 | assert numpy.allclose(numpy.zeros(rand_dim), soln.sol) 45 | 46 | 47 | def test_solve_qp_3(): 48 | rand_dim = numpy.random.randint(1, 10) 49 | Q = None 50 | A = numpy.vstack((numpy.eye(rand_dim), -numpy.eye(rand_dim))) 51 | b = numpy.ones(rand_dim * 2) 52 | c = numpy.ones((rand_dim, 1)) 53 | soln = solve_qp_gurobi(Q, c, A, b, [0]) 54 | assert soln.sol[0] == 1 55 | assert numpy.allclose(soln.sol[1:], -numpy.ones(rand_dim - 1)) 56 | 57 | 58 | def test_solve_miqp_1(): 59 | rand_dim = numpy.random.randint(1, 10) 60 | Q = None 61 | A = None 62 | b = None 63 | c = numpy.ones((rand_dim, 1)) 64 | 65 | soln = solve_miqp_gurobi(Q, c, A, b, [], [0]) 66 | print(soln) 67 | assert soln is None 68 | 69 | 70 | def test_solve_miqp_2(): 71 | rand_dim = numpy.random.randint(1, 10) 72 | Q = numpy.eye(rand_dim) 73 | A = numpy.vstack((numpy.eye(rand_dim), -numpy.eye(rand_dim))) 74 | b = numpy.block([[numpy.ones((rand_dim, 1))], [numpy.zeros((rand_dim, 1))]]) 75 | c = numpy.zeros((rand_dim, 1)) 76 | soln = solve_miqp_gurobi(Q, c, A, b, [], [0]) 77 | assert numpy.allclose(numpy.zeros(rand_dim), soln.sol) 78 | 79 | 80 | def test_solve_milp(): 81 | rand_dim = numpy.random.randint(1, 10) 82 | A = numpy.vstack((numpy.eye(rand_dim), -numpy.eye(rand_dim))) 83 | b = numpy.ones(rand_dim * 2) 84 | c = None 85 | 86 | soln = solve_milp_gurobi(c, A, b, [], [0]) 87 | assert numpy.allclose(numpy.zeros(rand_dim), soln.sol) 88 | 89 | 90 | def test_infeasfible_lp(): 91 | A = numpy.array([[1], [-1]], dtype='float64') 92 | b = numpy.array([[-1], [-1]], dtype='float64') 93 | c = None 94 | soln = solve_lp_gurobi(c, A, b) 95 | assert soln is None 96 | 97 | 98 | def test_indefinite_lp_1(): 99 | assert solve_lp_gurobi(None, None, None) is None 100 | 101 | 102 | def test_indefinite_lp_2(): 103 | assert solve_lp_gurobi(None, numpy.zeros((0, 0)), None) is None 104 | 105 | 106 | def test_infeasfible_qp(): 107 | A = numpy.array([[1], [-1]]) 108 | b = numpy.array([[-1], [-1]]) 109 | c = numpy.zeros((1, 1)) 110 | soln = solve_qp_gurobi(None, c, A, b) 111 | assert soln is None 112 | -------------------------------------------------------------------------------- /tests/solver_interface_tests/test_solver.py: -------------------------------------------------------------------------------- 1 | from src.ppopt.solver import Solver 2 | 3 | 4 | def test_solver_constructor_1(): 5 | _ = Solver() 6 | assert True 7 | 8 | 9 | def test_solver_constructor_2(): 10 | _ = Solver({'lp': 'gurobi', 'qp': 'gurobi'}) 11 | assert True 12 | 13 | 14 | def test_solver_wrong_solver_1(): 15 | try: 16 | _ = Solver({'mlp': 'python'}) 17 | assert False 18 | except RuntimeError: 19 | assert True 20 | except Exception: 21 | assert False 22 | 23 | 24 | def test_solver_wrong_solver_2(): 25 | try: 26 | _ = Solver({'lp': 'python'}) 27 | assert False 28 | except RuntimeError: 29 | assert True 30 | except Exception: 31 | assert False 32 | 33 | 34 | def test_solver_defined_problem_1(): 35 | solver = Solver({'lp': 'gurobi', 'qp': 'gurobi'}) 36 | try: 37 | solver.check_supported_problem('miqp') 38 | except RuntimeError: 39 | assert True 40 | -------------------------------------------------------------------------------- /tests/solver_interface_tests/test_solver_consistency.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | import numpy 4 | 5 | from src.ppopt.solver_interface.daqp_solver_interface import solve_qp_daqp 6 | from src.ppopt.solver_interface.cvxopt_interface import solve_lp_cvxopt 7 | from src.ppopt.solver_interface.gurobi_solver_interface import ( 8 | solve_lp_gurobi, 9 | solve_qp_gurobi, 10 | ) 11 | from src.ppopt.solver_interface.quad_prog_interface import solve_qp_quadprog 12 | 13 | 14 | def test_lp_consistency(): 15 | # test 100 random LPs 16 | num_lp = 100 17 | 18 | for _ in range(num_lp): 19 | 20 | dim = numpy.random.randint(3, 20) 21 | num_constraints = 3 * dim 22 | 23 | A = numpy.random.random((num_constraints, dim)) 24 | b = numpy.random.random((num_constraints, 1)) 25 | c = numpy.random.random((dim)) 26 | num_equals = numpy.random.randint(0, dim // 2) 27 | equality_constraints = random.sample(range(num_constraints), num_equals) 28 | 29 | glpk_sol = solve_lp_cvxopt(c, A, b, equality_constraints) 30 | gurobi_sol = solve_lp_gurobi(c, A, b, equality_constraints) 31 | 32 | if glpk_sol != gurobi_sol: 33 | print(glpk_sol) 34 | print(gurobi_sol) 35 | assert False 36 | 37 | 38 | def test_qp_interface_consistency(): 39 | 40 | num_qp = 100 41 | 42 | for i in range(num_qp): 43 | dim = numpy.random.randint(3, 20) 44 | num_constraints = 3 * dim 45 | 46 | A = numpy.random.random((num_constraints, dim)) 47 | b = numpy.random.random((num_constraints, 1)) 48 | c = numpy.random.random((dim)) 49 | Q = numpy.eye(dim) 50 | equality_constraints = [] 51 | quadprog_sol = solve_qp_quadprog(Q, c, A, b, equality_constraints) 52 | gurobi_sol = solve_qp_gurobi(Q, c, A, b, equality_constraints) 53 | daqp_sol = solve_qp_daqp(Q, c, A, b, equality_constraints) 54 | 55 | if quadprog_sol != gurobi_sol: 56 | print(f'On problem {i} there was a disagreement between Gurobi and Quadprog') 57 | print(quadprog_sol) 58 | print(gurobi_sol) 59 | if numpy.linalg.norm(quadprog_sol.sol - gurobi_sol.sol, 2) > 10**(-4): 60 | assert False 61 | 62 | if quadprog_sol != daqp_sol: 63 | print(f'On problem {i} there was a disagreement between Daqp and Quadprog') 64 | print(quadprog_sol) 65 | print(daqp_sol) 66 | if numpy.linalg.norm(quadprog_sol.sol - daqp_sol.sol, 2) > 10 ** (-4): 67 | assert False 68 | 69 | if gurobi_sol != daqp_sol: 70 | print(f'On problem {i} there was a disagreement between Daqp and Gurobi') 71 | print(gurobi_sol) 72 | print(daqp_sol) 73 | if numpy.linalg.norm(gurobi_sol.sol - daqp_sol.sol, 2) > 10 ** (-4): 74 | assert False -------------------------------------------------------------------------------- /tests/solver_interface_tests/test_solver_interface.py: -------------------------------------------------------------------------------- 1 | from src.ppopt.solver_interface.solver_interface import ( 2 | solve_lp, 3 | solve_milp, 4 | solve_miqp, 5 | solve_qp, 6 | ) 7 | 8 | 9 | def test_solver_not_supported_1(): 10 | try: 11 | solve_lp(None, None, None, deterministic_solver='MyBigBrain') 12 | assert False 13 | except RuntimeError: 14 | assert True 15 | 16 | 17 | def test_solver_not_supported_2(): 18 | try: 19 | solve_qp(None, None, None, None, deterministic_solver='MyBigBrain') 20 | assert False 21 | except RuntimeError: 22 | assert True 23 | 24 | 25 | def test_solver_not_supported_3(): 26 | try: 27 | solve_miqp(None, None, None, None, deterministic_solver='MyBigBrain') 28 | assert False 29 | except RuntimeError: 30 | assert True 31 | 32 | 33 | def test_solver_not_supported_4(): 34 | try: 35 | solve_milp(None, None, None, deterministic_solver='MyBigBrain') 36 | assert False 37 | except RuntimeError: 38 | assert True 39 | -------------------------------------------------------------------------------- /tests/upop_tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TAMUparametric/PPOPT/cdc79a31965dbf7a0139667affa5c52076c9dcae/tests/upop_tests/__init__.py -------------------------------------------------------------------------------- /tests/upop_tests/test_language_generation.py: -------------------------------------------------------------------------------- 1 | from src.ppopt.upop.language_generation import gen_array, gen_variable 2 | 3 | 4 | def test_array_cpp_1(): 5 | value = gen_array([i for i in range(10)], 'my_data', 'double') 6 | print(value) 7 | 8 | 9 | def test_array_js_1(): 10 | value = gen_array([i for i in range(10)], 'my_data', 'double', lang='js') 11 | print(value) 12 | 13 | 14 | def test_array_python_1(): 15 | value = gen_array([i for i in range(10)], 'my_data', 'double', lang='python') 16 | print(value) 17 | 18 | 19 | def test_variable_cpp_1(): 20 | value = gen_variable(3.75, 'variable', 'double', lang='cpp') 21 | print(value) 22 | 23 | 24 | def test_variable_cpp_2(): 25 | value = gen_variable('3.75', 'variable', 'std::string', lang='cpp') 26 | print(value) 27 | 28 | 29 | def test_variable_js_1(): 30 | value = gen_variable(3.75, 'variable', 'double', lang='js') 31 | print(value) 32 | 33 | 34 | def test_variable_js_2(): 35 | value = gen_variable(3.75, 'variable', 'string', lang='js') 36 | print(value) 37 | 38 | 39 | def test_variable_python_1(): 40 | value = gen_variable(3.75, 'variable', 'double', lang='python') 41 | print(value) 42 | 43 | 44 | def test_variable_python_2(): 45 | value = gen_variable(3.75, 'variable', 'string', lang='python') 46 | print(value) 47 | -------------------------------------------------------------------------------- /tests/upop_tests/test_linear_code_generation.py: -------------------------------------------------------------------------------- 1 | from src.ppopt.upop.linear_code_gen import ( 2 | generate_code_cpp, 3 | generate_code_js, 4 | generate_code_matlab, 5 | ) 6 | from tests.test_fixtures import factory_solution 7 | 8 | 9 | def test_generate_js_export(factory_solution): 10 | _ = generate_code_js(factory_solution) 11 | 12 | 13 | def test_generate_cpp_export(factory_solution): 14 | _ = generate_code_cpp(factory_solution) 15 | 16 | 17 | def test_generate_matlab_export(factory_solution): 18 | generate_code_matlab(factory_solution) 19 | -------------------------------------------------------------------------------- /tests/upop_tests/test_point_location.py: -------------------------------------------------------------------------------- 1 | import copy 2 | 3 | import numpy 4 | 5 | from src.ppopt.upop.point_location import PointLocation 6 | from tests.test_fixtures import factory_solution 7 | 8 | 9 | def test_point_location(factory_solution): 10 | 11 | pl = PointLocation(factory_solution) 12 | pl.is_overlapping = False 13 | 14 | theta = numpy.array([[200.0], [200.0]]) 15 | print(pl.evaluate(theta)) 16 | print(factory_solution.evaluate(theta)) 17 | print(factory_solution.program.solve_theta(theta)) 18 | assert numpy.allclose(pl.evaluate(theta), factory_solution.evaluate(theta)) 19 | 20 | 21 | def test_point_location_overlap(factory_solution): 22 | new_sol = copy.deepcopy(factory_solution) 23 | new_sol.is_overlapping = True 24 | pl = PointLocation(factory_solution) 25 | 26 | theta = numpy.array([[200.0], [200.0]]) 27 | print(pl.evaluate(theta)) 28 | print(factory_solution.evaluate(theta)) 29 | print(factory_solution.program.solve_theta(theta)) 30 | assert numpy.allclose(pl.evaluate(theta), factory_solution.evaluate(theta)) 31 | -------------------------------------------------------------------------------- /tests/upop_tests/test_upop.py: -------------------------------------------------------------------------------- 1 | def test_upop(): 2 | assert True 3 | --------------------------------------------------------------------------------