├── .github └── workflows │ ├── dist.yml │ ├── docs.yml │ └── test.yml ├── .gitignore ├── .gitlab-ci.yml ├── .pre-commit-config.yaml ├── README.rst ├── design ├── aesara_sellar.py ├── casadi_rocket.py ├── condor.pptx ├── prototype_condor_sgm.py └── solver_codegen_pseudocode.py ├── docs ├── Makefile ├── _static │ └── overrides.css ├── api │ ├── backend.rst │ ├── contrib.rst │ ├── fields.rst │ ├── implementations.rst │ └── index.rst ├── conf.py ├── howto_src │ ├── index.rst │ ├── parallel_processing.py │ └── table_basics.py ├── images │ ├── architecture.png │ ├── architecture.svg │ ├── math-model-process.png │ └── math-model-process.svg ├── index.rst ├── make.bat ├── topics │ ├── design.rst │ ├── glossary.rst │ └── index.rst └── tutorial_src │ ├── index.rst │ ├── polar_transform.py │ ├── sellar.py │ └── trajectory.py ├── examples ├── LinCovCW.py ├── P_aug_0.mat ├── condor_sellar.py ├── condor_sellar_2.py ├── condor_sellar_3.py ├── ct_lqr.py ├── custom_model_template_with_metaprogramming.py ├── explicit_system.py ├── mwe_external.py ├── orbital_1.py ├── orbital_2.py ├── orbital_3.py ├── orbital_4.py ├── orbital_debugging.py ├── results ├── rosenbrock.py ├── sellar.py ├── sgm_test_util.py ├── sp_lqr.py ├── state_switched.py ├── test_sgm.py ├── test_tables.py └── time_switch.py ├── license.pdf ├── pyproject.toml ├── src └── condor │ ├── __init__.py │ ├── backend │ ├── __init__.py │ ├── _get_backend.py │ └── operators.py │ ├── backends │ ├── casadi │ │ ├── __init__.py │ │ └── operators.py │ ├── default.py │ └── element_mixin.py │ ├── conf.py │ ├── contrib.py │ ├── fields.py │ ├── implementations │ ├── __init__.py │ ├── iterative.py │ ├── sgm_trajectory.py │ ├── simple.py │ └── utils.py │ ├── models.py │ └── solvers │ ├── __init__.py │ ├── casadi_warmstart_wrapper.py │ ├── newton.py │ └── sweeping_gradient_method.py └── tests ├── modules └── configured_model.py ├── test_algebraic_system.py ├── test_custom_model_template.py ├── test_fields.py ├── test_model_api.py ├── test_model_config.py ├── test_operators.py ├── test_optimization.py ├── test_partial.py ├── test_placeholders.py └── test_trajectory_analysis.py /.github/workflows/dist.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish dist artifacts 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: 7 | - 'v*' 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Build dist 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Build sdist and wheel 23 | run: pipx run build 24 | 25 | - name: Upload artifacts 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: dist 29 | path: dist/ 30 | 31 | publish: 32 | name: Publish to PyPI 33 | needs: [build] 34 | runs-on: ubuntu-latest 35 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') 36 | environment: 37 | name: pypi 38 | url: https://pypi.org/p/condor 39 | permissions: 40 | id-token: write 41 | steps: 42 | - name: Download artifacts 43 | uses: actions/download-artifact@v4 44 | with: 45 | name: dist 46 | path: dist/ 47 | 48 | - name: List files 49 | run: ls -R 50 | 51 | - name: Publish to PyPI 52 | uses: pypa/gh-action-pypi-publish@v1.12.4 53 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | build: 10 | name: Docs build 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: '3.12' 22 | 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | python -m pip install --group docs . 27 | 28 | - name: Build documentation 29 | run: make -C docs html 30 | 31 | - name: Upload artifact 32 | uses: actions/upload-artifact@v4 33 | with: 34 | name: docs 35 | path: ./docs/_build/ 36 | 37 | - name: Deploy to gh pages 38 | uses: JamesIves/github-pages-deploy-action@v4 39 | if: ${{ github.ref == 'refs/heads/main' }} 40 | with: 41 | branch: gh-pages 42 | folder: ./docs/_build/html 43 | 44 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | lint: 10 | name: Lint 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - uses: pre-commit/action@v3.0.1 16 | with: 17 | extra_args: --all-files 18 | 19 | test: 20 | name: Test 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 26 | os: [ubuntu-latest, windows-latest, macos-latest] 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - uses: actions/setup-python@v5 32 | with: 33 | python-version: ${{ matrix.python-version }} 34 | 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | python -m pip install --group test . 39 | 40 | - name: Run tests 41 | run: pytest --cov=src 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | tags 3 | reports/ 4 | *.egg-info/ 5 | .venv/ 6 | _version.py 7 | _build/ 8 | build/ 9 | dist/ 10 | .coverage 11 | 12 | # sphinx-gallery outputs 13 | tutorial/ 14 | howto/ 15 | sg_execution_times.rst 16 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # The Docker image that will be used to build your app 2 | # https://hub.docker.com/r/library/python/tags/ 3 | image: python:3.11 4 | 5 | # Change pip's cache directory to be inside the project directory since we can 6 | # only cache local items. 7 | variables: 8 | PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" 9 | 10 | # https://pip.pypa.io/en/stable/topics/caching/ 11 | cache: 12 | paths: 13 | - .cache/pip 14 | 15 | before_script: 16 | - python --version ; pip --version 17 | - python -m venv .venv 18 | - source .venv/bin/activate 19 | 20 | test: 21 | script: 22 | - pip install -e .[test] 23 | - pytest 24 | 25 | docs: 26 | script: 27 | - pip install -e .[docs] 28 | - make -C docs html 29 | artifacts: 30 | paths: 31 | - docs/_build/html 32 | 33 | pages: 34 | needs: ['docs'] 35 | script: 36 | - ls -l docs/_build/html 37 | - mv docs/_build/html/ public/ 38 | artifacts: 39 | paths: 40 | - public 41 | rules: 42 | - if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH 43 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/astral-sh/ruff-pre-commit 3 | rev: v0.11.12 4 | hooks: 5 | - id: ruff-check 6 | args: ["--fix", "--show-fixes"] 7 | - id: ruff-format 8 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Condor 2 | ====== 3 | 4 | .. image:: https://github.com/nasa/simupy-flight/actions/workflows/docs.yml/badge.svg 5 | :target: https://nasa.github.io/condor 6 | .. image:: https://img.shields.io/badge/License-NOSA-green.svg 7 | :target: https://github.com/nasa/condor/blob/master/license.pdf 8 | .. image:: https://img.shields.io/github/release/nasa/condor.svg 9 | :target: https://github.com/nasa/condor/releases 10 | 11 | 12 | Condor is a new mathematical modeling framework for Python, developed at 13 | NASA's Ames Research Center. Initial development began in April 2023 to 14 | address model implementation challenges for aircraft synthesis and 15 | robust orbital trajectory design. The goal is for Condor to help 16 | evaluate numerical models and then get out of the way. 17 | 18 | One key aspect to achieve this goal was to create an API that looked as 19 | much like the mathematical description as possible with as little 20 | distraction from programming cruft as possible. To best understand 21 | this approach, we can consider a simple benchmark problem which consists 22 | of a set of coupled algebraic expressions. This can be represented as a 23 | system of algebraic equations: 24 | 25 | .. code-block:: python 26 | 27 | class Coupling(co.AlgebraicSystem): 28 | x = parameter(shape=3) 29 | y1 = variable(initializer=1.) 30 | y2 = variable(initializer=1.) 31 | 32 | residual(y1 == x[0] ** 2 + x[1] + x[2] - 0.2 * y2) 33 | residual(y2 == y1**0.5 + x[0] + x[1]) 34 | 35 | This parametric model can be evaluated by providing the values for the 36 | parameters; the resulting object has values for its inputs and outputs 37 | bound, so the solved values for ``y1`` and ``y2`` can be accessed easily: 38 | 39 | .. code-block:: python 40 | 41 | coupling = Coupling([5., 2., 1]) # evaluate the model numerically 42 | print(coupling.y1, coupling.y2) # individual elements are bound numerically 43 | print(coupling.variable) # fields are bound as a dataclass 44 | 45 | Models can also be seamlessly built-up, with parent models accessing any 46 | input or output of models embedded within them. For example, we can 47 | optimize this system of algebraic equations by embedding it within an 48 | optimization problem: 49 | 50 | .. code-block:: python 51 | 52 | class Sellar(co.OptimizationProblem): 53 | x = variable(shape=3, lower_bound=0, upper_bound=10) 54 | coupling = Coupling(x) 55 | y1, y2 = coupling 56 | 57 | objective = x[2]**2 + x[1] + y1 + exp(-y2) 58 | constraint(y1 >= 3.16) 59 | constraint(24. >= y2) 60 | 61 | After the model is solved, the embedded model can be accessed directly: 62 | 63 | .. code-block:: python 64 | 65 | Sellar.set_initial(x=[5,2,1]) 66 | sellar = Sellar() 67 | print(sellar.objective) # scalar value 68 | print(sellar.constraint) # field 69 | print(sellar.coupling.y1) # sub-model element 70 | 71 | NASA's Condor is a framework for mathematical modeling of engineering 72 | systems in Python, written for engineers with a deadline. 73 | 74 | Installation 75 | ------------ 76 | 77 | Condor is available on `PyPI `_, so you can 78 | install with pip: 79 | 80 | .. code:: bash 81 | 82 | $ pip install condor 83 | 84 | 85 | License 86 | ------- 87 | 88 | This software is released under the `NASA Open Source Agreement Version 1.3 `_. 89 | 90 | Notices 91 | ------- 92 | 93 | Copyright © 2024 United States Government as represented by the Administrator of the National Aeronautics and Space Administration. All Rights Reserved. 94 | 95 | Disclaimers 96 | ----------- 97 | 98 | No Warranty: THE SUBJECT SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY OF ANY KIND, EITHER EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR FREEDOM FROM INFRINGEMENT, ANY WARRANTY THAT THE SUBJECT SOFTWARE WILL BE ERROR FREE, OR ANY WARRANTY THAT DOCUMENTATION, IF PROVIDED, WILL CONFORM TO THE SUBJECT SOFTWARE. THIS AGREEMENT DOES NOT, IN ANY MANNER, CONSTITUTE AN ENDORSEMENT BY GOVERNMENT AGENCY OR ANY PRIOR RECIPIENT OF ANY RESULTS, RESULTING DESIGNS, HARDWARE, SOFTWARE PRODUCTS OR ANY OTHER APPLICATIONS RESULTING FROM USE OF THE SUBJECT SOFTWARE. FURTHER, GOVERNMENT AGENCY DISCLAIMS ALL WARRANTIES AND LIABILITIES REGARDING THIRD-PARTY SOFTWARE, IF PRESENT IN THE ORIGINAL SOFTWARE, AND DISTRIBUTES IT "AS IS." 99 | 100 | Waiver and Indemnity: RECIPIENT AGREES TO WAIVE ANY AND ALL CLAIMS AGAINST THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT. IF RECIPIENT'S USE OF THE SUBJECT SOFTWARE RESULTS IN ANY LIABILITIES, DEMANDS, DAMAGES, EXPENSES OR LOSSES ARISING FROM SUCH USE, INCLUDING ANY DAMAGES FROM PRODUCTS BASED ON, OR RESULTING FROM, RECIPIENT'S USE OF THE SUBJECT SOFTWARE, RECIPIENT SHALL INDEMNIFY AND HOLD HARMLESS THE UNITED STATES GOVERNMENT, ITS CONTRACTORS AND SUBCONTRACTORS, AS WELL AS ANY PRIOR RECIPIENT, TO THE EXTENT PERMITTED BY LAW. RECIPIENT'S SOLE REMEDY FOR ANY SUCH MATTER SHALL BE THE IMMEDIATE, UNILATERAL TERMINATION OF THIS AGREEMENT. 101 | -------------------------------------------------------------------------------- /design/aesara_sellar.py: -------------------------------------------------------------------------------- 1 | import aesara.tensor as at 2 | from aesara import function as fn 3 | from scipy import optimize 4 | 5 | xx = at.vector("xx") 6 | x, z1, z2, y2_in = [xx[i] for i in range(4)] 7 | 8 | @sysin(x, z1, z2, y2) 9 | @sysout(y1) 10 | def y1__y2(x, z1, z2, y2): 11 | return z1**2 + z2 + x - 0.2 * y2 12 | 13 | class sys1(...): 14 | def compute?(x, z1, z2, y2): 15 | return ... 16 | 17 | y1 = Outputs 18 | y2,y3 = compute 19 | 20 | def sys0(x, z1, z2, y2): 21 | return z1**2 + z2 + x - 0.2 * y2 22 | 23 | copy1 = system(sys0, "y1") 24 | copy2 = system(sys0, "y1") 25 | 26 | @system("y1") 27 | def sys1(x, z1, z2, y2): 28 | return z1**2 + z2 + x - 0.2 * y2 29 | 30 | @system("y2") 31 | def sys2(y1, z1, z2): 32 | return at.sqrt(y1) + z1 + z2 33 | 34 | @system("obj") 35 | def objective(x, y1, y2, z2) 36 | return x**2 + z2 + y1 + at.exp(-y2) 37 | 38 | @system("obj", "constr1", "constr2") 39 | def sellar(x, z1, z2): 40 | y1 = sys1(x=x, z1=z1, z2=z2, y2=sys2.y2) 41 | y2 = sys2(x=x, z1=z1, z2=z2, y1=sys1.y1) 42 | 43 | obj = x**2 + z2 + y1 + at.exp(-y2) 44 | constr1 = y1 > 3.16 45 | constr2 = y2 < 24. 46 | return obj, constr1, constr2 47 | 48 | prob.add_objective(sellar.obj) 49 | prob.add_constraints(sellar.constr1:) 50 | 51 | objective = cp.Minimize(sellar.obj) 52 | prob = cp.Problem(obj, sellar.constr1:) 53 | 54 | def block_diagram(t, x): 55 | xdot, y = plant(t, x, u=controller.u) 56 | u = controller(t, y=plant.y) 57 | return xdot, (y, u) 58 | 59 | 60 | class Sellar(...): 61 | subsystems = [sys1, sys2] # do we need this? 62 | x = fw.designvar #basically declaring inputs 63 | z1 = ... 64 | 65 | fw.connect(x, [sys1.x, sys2.x])# can programmatically do this 66 | fw.connect(z1, [sys1.z1, sys2.z1]) 67 | 68 | 69 | 70 | 71 | obj = x + sys1.y2 72 | 73 | def objective(self): 74 | x**2 + z2 + y1 + at.exp(-y2_in) 75 | self.x 76 | 77 | 78 | obj = x**2 + z2 + y1 + at.exp(-y2_in) 79 | con1 = y1 - 3.16 80 | con2 = 24.0 - y2_out 81 | con_y2 = y2_out - y2_in 82 | 83 | ins = [xx] 84 | 85 | res = optimize.minimize( 86 | fn(ins, obj), 87 | [1.0, 5.0, 2.0, 1.0], 88 | method="SLSQP", 89 | #jac=fn(ins, at.grad(obj, xx)), 90 | jac=None, 91 | bounds=[(0, 10), (0, 10), (0, 10), (None, None)], 92 | constraints=[ 93 | {"type": "ineq", "fun": fn(ins, con1),}, # "jac": fn(ins, at.grad(con1, ins))}, 94 | {"type": "ineq", "fun": fn(ins, con2),}, # "jac": fn(ins, at.grad(con2, ins))}, 95 | {"type": "eq", "fun": fn(ins, con_y2),}, # "jac": fn(ins, at.grad(con_y2, ins))}, 96 | ], 97 | tol=1e-8, 98 | ) 99 | print(res) 100 | 101 | print("checking...") 102 | print("y1 =", y1.eval({xx: res.x})) 103 | print("y2 =", y2_out.eval({xx: res.x})) 104 | -------------------------------------------------------------------------------- /design/casadi_rocket.py: -------------------------------------------------------------------------------- 1 | # 2 | # This file is part of CasADi. 3 | # 4 | # CasADi -- A symbolic framework for dynamic optimization. 5 | # Copyright (C) 2010-2014 Joel Andersson, Joris Gillis, Moritz Diehl, 6 | # K.U. Leuven. All rights reserved. 7 | # Copyright (C) 2011-2014 Greg Horn 8 | # 9 | # CasADi is free software; you can redistribute it and/or 10 | # modify it under the terms of the GNU Lesser General Public 11 | # License as published by the Free Software Foundation; either 12 | # version 3 of the License, or (at your option) any later version. 13 | # 14 | # CasADi is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # Lesser General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU Lesser General Public 20 | # License along with CasADi; if not, write to the Free Software 21 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 22 | # 23 | # 24 | import casadi 25 | import matplotlib.pyplot as plt 26 | 27 | class CasadiNLPWithWarmstart(casadi.Callback): 28 | 29 | def __init__(self, name, *solver_args, cb_opts={}): 30 | casadi.Callback.__init__(self) 31 | 32 | self.solver = casadi.nlpsol(name + "_solver", *solver_args) 33 | 34 | self.construct(name, cb_opts) 35 | 36 | def get_n_in(self): 37 | return casadi.nlpsol_n_in() 38 | 39 | def get_n_out(self): 40 | return casadi.nlpsol_n_out() 41 | 42 | def eval(self, args): 43 | pass 44 | 45 | 46 | use_cb_for_jac = True 47 | 48 | 49 | class StatefulFunction(casadi.Callback): 50 | 51 | def __init__(self, name, func, opts={}): 52 | casadi.Callback.__init__(self) 53 | self.func = func 54 | self.name = name 55 | self.construct(name, opts) 56 | 57 | def init(self): 58 | pass 59 | 60 | def finalize(self): 61 | pass 62 | 63 | def get_n_in(self): 64 | return self.func.n_in() 65 | 66 | def get_n_out(self): 67 | return self.func.n_out() 68 | 69 | def eval(self, args): 70 | out = self.func(*args) 71 | print(self.name) 72 | return [out] if self.func.n_out() == 1 else out 73 | 74 | def get_sparsity_in(self, i): 75 | return self.func.sparsity_in(i) 76 | 77 | def get_sparsity_out(self, i): 78 | return self.func.sparsity_out(i) 79 | 80 | def has_jacobian(self): 81 | return True 82 | 83 | def get_jacobian(self, name, inames, onames, opts): 84 | print(name, inames, onames, opts) 85 | if use_cb_for_jac: 86 | jcb = StatefulFunction(name, self.func.jacobian()) 87 | self.jcb = jcb 88 | return jcb 89 | else: 90 | return self.func.jacobian() 91 | 92 | 93 | # Control 94 | u = casadi.MX.sym("u") 95 | 96 | # State 97 | x = casadi.MX.sym("x", 3) 98 | s = x[0] # position 99 | v = x[1] # speed 100 | m = x[2] # mass 101 | 102 | # ODE right hand side 103 | sdot = v 104 | vdot = (u - 0.05 * v * v) / m 105 | mdot = -0.1 * u * u 106 | xdot = casadi.vertcat(sdot, vdot, mdot) 107 | 108 | # ODE right hand side function 109 | f = casadi.Function("f", [x, u], [xdot]) 110 | 111 | # Integrate with Explicit Euler over 0.2 seconds 112 | dt = 0.01 # Time step 113 | xj = x 114 | for j in range(20): 115 | fj = f(xj, u) 116 | xj += dt * fj 117 | 118 | # Discrete time dynamics function 119 | F = casadi.Function("F", [x, u], [xj]) 120 | 121 | 122 | # Number of control segments 123 | nu = 50 124 | 125 | # Control for all segments 126 | U = casadi.MX.sym("U", nu) 127 | 128 | # Initial conditions 129 | X0 = casadi.MX([0, 0, 1]) 130 | 131 | # Integrate over all intervals 132 | X = X0 133 | for k in range(nu): 134 | X = F(X, U[k]) 135 | 136 | # Objective function and constraints 137 | J = casadi.mtimes(U.T, U) # u'*u in Matlab 138 | G_expr = X[0:2] # x(1:2) in Matlab 139 | G_func = casadi.Function("G_func", [U], [G_expr]) 140 | G = StatefulFunction("G", G_func) 141 | 142 | 143 | 144 | # NLP 145 | nlp = {"x": U, "f": J, "g": G(U)} 146 | # Allocate an NLP solver 147 | opts = {"ipopt.tol": 1e-10, "expand": False} 148 | 149 | opts = {"expand": False, "ipopt": {"tol": 1e-10, "hessian_approximation":"limited-memory"}} 150 | 151 | solver = casadi.nlpsol("solver", "ipopt", nlp, opts) 152 | 153 | arg = {} 154 | # Bounds on u and initial condition 155 | arg["lbx"] = -0.5 156 | arg["ubx"] = 0.5 157 | arg["x0"] = 0.0 158 | 159 | # Bounds on g 160 | arg["lbg"] = [10, 0] 161 | arg["ubg"] = [10, 0] 162 | 163 | # Solve the problem 164 | res = solver(**arg) 165 | 166 | # Get the solution 167 | plt.plot(res["x"], label="x") 168 | plt.plot(res["lam_x"], label="lam_x") 169 | plt.legend() 170 | plt.grid() 171 | plt.show() 172 | -------------------------------------------------------------------------------- /design/condor.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/condor/2976ecae44fa94b3b882acc3b40c050126f1403e/design/condor.pptx -------------------------------------------------------------------------------- /design/solver_codegen_pseudocode.py: -------------------------------------------------------------------------------- 1 | # 2 | # in loop over om systems starting at prob.model 3 | # 4 | for omsys in model.system_iter: 5 | upsys = UpcycyleSystem() 6 | 7 | upsys.inputs = omsys.inputs 8 | internal_connections = upsys.solver.get_internal_connections(upsys.inputs) 9 | # solver only gets external connections to this system, could happen in get_internal_connections 10 | upsys.solver.inputs += ~internal_connections 11 | 12 | if omsys is explicit and omssys.is_forced_explicit: 13 | for output_name in omsys: 14 | upsys.outputs[output_name] = omsys.compute() # residiual expr + output symbol 15 | 16 | else omsys is implicit: 17 | upsys.solver.residuals += omsys.residuals 18 | 19 | # 20 | # once a solver is built by the loop above 21 | # 22 | 23 | # Generated code: 24 | def solver_system({external_inputs}): 25 | p = external_inputs 26 | resid_guesses_initial = warm_start() # initial pass pulls defaults from om metadata 27 | 28 | def resid(external_inputs, resid_guesses): # guesses are vals for solver.residuals.keys() 29 | # template generating explicit_output_var = rhs expr 30 | for varname, expr in solver.outputs.items(): 31 | varname = expr 32 | 33 | for varname, expr in solver.residuals.items(): 34 | varname + "_resid" = expr 35 | 36 | return varnames, varnames_resid 37 | 38 | resid_with_external_inputs = partial(resid, external_inputs=p) 39 | 40 | lbg = [-inf] * len(solver.outputs) + [0] * len(solver.residuals) 41 | ubg = [inf] * len(solver.outputs) + [0] * len(solver.residuals) 42 | 43 | out = solve( 44 | f=0, 45 | g=resid_with_external_inputs, 46 | x0=resid_guesses_initial, 47 | lbx=prob_meta[resid_guesses], 48 | ubx=prob_meta[resid_gueses], 49 | lbg=lbg, 50 | ubg=ubg 51 | ) 52 | 53 | return out["g"][:len(solver.outputs) + out["x"] 54 | -------------------------------------------------------------------------------- /docs/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 clean 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 | 22 | clean: 23 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 24 | rm -rf $(SOURCEDIR)/tutorial $(SOURCEDIR)/howto 25 | -------------------------------------------------------------------------------- /docs/_static/overrides.css: -------------------------------------------------------------------------------- 1 | div.sphx-glr-download-link-note { 2 | display: none; 3 | } 4 | 5 | div.sphx-glr-footer { 6 | display: none; 7 | } 8 | 9 | div.sphx-glr-thumbnails { 10 | grid: none; 11 | grid-template-columns: none; 12 | display: block; 13 | position: static; 14 | } 15 | 16 | div.sphx-glr-thumbcontainer { 17 | box-shadow: none; 18 | display: block; 19 | position: relative; 20 | } 21 | 22 | div.sphx-glr-thumbcontainer img { 23 | display: none; 24 | } 25 | 26 | .sphx-glr-thumbcontainer a.internal { 27 | padding: 0px 10px; 28 | padding: 0px 0px; 29 | border-bottom: none; 30 | } 31 | 32 | .sphx-glr-thumbcontainer[tooltip]:hover::after { 33 | padding: 10px 0px; 34 | } 35 | 36 | table { 37 | font-size: 10pt; 38 | } 39 | -------------------------------------------------------------------------------- /docs/api/backend.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Backend Modules 3 | =============== 4 | 5 | 6 | Backend Shim 7 | ============= 8 | 9 | .. automodule:: condor.backend 10 | :member-order: bysource 11 | :undoc-members: 12 | :members: 13 | 14 | Operators Shim 15 | ================ 16 | .. automodule:: condor.backend.operators 17 | :member-order: bysource 18 | :undoc-members: 19 | :members: 20 | 21 | CasADi Realization 22 | ==================== 23 | 24 | Backend Module 25 | --------------- 26 | .. automodule:: condor.backends.casadi 27 | :member-order: bysource 28 | :members: 29 | :exclude-members: WrappedSymbol 30 | 31 | Operators Module 32 | ------------------- 33 | .. automodule:: condor.backends.casadi.operators 34 | :member-order: bysource 35 | :members: 36 | 37 | -------------------------------------------------------------------------------- /docs/api/contrib.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Contrib Models 3 | ============== 4 | 5 | .. automodule:: condor.contrib 6 | 7 | .. autoclass:: condor.contrib.ExplicitSystem 8 | :member-order: bysource 9 | :members: 10 | :undoc-members: 11 | :exclude-members: placeholder 12 | 13 | .. autoclass:: condor.contrib.TableLookup 14 | :member-order: bysource 15 | :members: __original_init__ 16 | :undoc-members: 17 | :exclude-members: placeholder, user_model_metaclass 18 | 19 | .. autoclass:: condor.contrib.AlgebraicSystem 20 | :member-order: bysource 21 | :members: 22 | :undoc-members: 23 | :exclude-members: placeholder, user_model_metaclass 24 | 25 | .. autoclass:: condor.contrib.OptimizationProblem 26 | :member-order: bysource 27 | :members: 28 | :undoc-members: 29 | :exclude-members: placeholder, user_model_metaclass 30 | 31 | .. autoclass:: condor.contrib.ODESystem 32 | :member-order: bysource 33 | :members: 34 | :undoc-members: 35 | :exclude-members: placeholder, user_model_metaclass 36 | 37 | .. autoclass:: condor.contrib.Event 38 | :member-order: bysource 39 | :members: 40 | :undoc-members: 41 | :exclude-members: placeholder, user_model_metaclass 42 | 43 | .. autoclass:: condor.contrib.Mode 44 | :member-order: bysource 45 | :members: 46 | :undoc-members: 47 | :exclude-members: placeholder, user_model_metaclass 48 | 49 | .. autoclass:: condor.contrib.TrajectoryAnalysis 50 | :member-order: bysource 51 | :members: 52 | :undoc-members: 53 | :exclude-members: placeholder, user_model_metaclass 54 | -------------------------------------------------------------------------------- /docs/api/fields.rst: -------------------------------------------------------------------------------- 1 | =================== 2 | Fields and Elements 3 | =================== 4 | 5 | .. automodule:: condor.fields 6 | :member-order: bysource 7 | :members: 8 | :exclude-members: log 9 | -------------------------------------------------------------------------------- /docs/api/implementations.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Implementations 3 | =============== 4 | 5 | .. automodule:: condor.implementations.simple 6 | :member-order: bysource 7 | :members: 8 | 9 | .. automodule:: condor.implementations.sgm_trajectory 10 | :member-order: bysource 11 | :undoc-members: 12 | :members: 13 | :exclude-members: get_state_setter 14 | 15 | 16 | Iterative Solvers 17 | ------------------- 18 | 19 | .. automodule:: condor.implementations.iterative 20 | :member-order: bysource 21 | :members: 22 | :undoc-members: 23 | :exclude-members: InitializerMixin, SciPyIterCallbackWrapper 24 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | ================= 2 | Condor API 3 | ================= 4 | 5 | .. toctree:: 6 | :maxdepth: 2 7 | 8 | contrib 9 | fields 10 | implementations 11 | backend 12 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import condor 10 | 11 | project = "Condor" 12 | copyright = "2023, Benjamin W. L. Margolis" 13 | author = "Benjamin W. L. Margolis" 14 | version = condor.__version__ 15 | release = version 16 | 17 | # -- General configuration --------------------------------------------------- 18 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 19 | 20 | extensions = [ 21 | "sphinx.ext.napoleon", 22 | "sphinx_gallery.gen_gallery", 23 | "sphinx.ext.autodoc", 24 | ] 25 | 26 | # templates_path = ['_templates'] 27 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**/README.rst"] 28 | 29 | gallery_src_dirs = ["tutorial_src", "howto_src"] 30 | exclude_patterns.extend(gallery_src_dirs) 31 | 32 | 33 | # -- Options for HTML output ------------------------------------------------- 34 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 35 | 36 | html_theme = "furo" 37 | html_static_path = ["_static"] 38 | html_css_files = ["overrides.css"] 39 | 40 | 41 | sphinx_gallery_conf = { 42 | "examples_dirs": gallery_src_dirs, 43 | "gallery_dirs": [name.split("_")[0] for name in gallery_src_dirs], 44 | "filename_pattern": ".*.py", 45 | "within_subsection_order": "FileNameSortKey", 46 | "download_all_examples": False, 47 | "copyfile_regex": r".*\.rst", 48 | "reference_url": { 49 | "sphinx_gallery": None, 50 | }, 51 | } 52 | 53 | napoleon_custom_sections = [("Options", "params_style")] 54 | -------------------------------------------------------------------------------- /docs/howto_src/index.rst: -------------------------------------------------------------------------------- 1 | ============= 2 | How-To Guide 3 | ============= 4 | 5 | Here we will provide how-to guides (recipes) for certain tasks. 6 | 7 | .. toctree:: 8 | :maxdepth: 1 9 | 10 | table_basics 11 | parallel_processing 12 | 13 | .. 14 | [ ] how to learn about condor 15 | [x] parallel_processing - quick guide on parallel processing 16 | [ ] external_solver 17 | 18 | [ ] optimization_callback 19 | [ ] from_values - or part of callback? 20 | [ ] pareto_front - using a constraint to sweep? 21 | 22 | [ ] external solver 23 | 24 | [ ] variable -- warmstart, initializer, etc. 25 | [ ] set_initial - set initial for iterative solvers 26 | 27 | [ ] options (or to topic) -- inspect implementation, basic usage and philosophy? so actually topic 28 | [ ] how to access model metadata 29 | 30 | [ ] how to dry-up 31 | [ ] alias_dict - use dictionaries for manging IO 32 | [ ] functions operating on fields 33 | 34 | [ ] model_generators - not sure what this was for? maybe just a header for the two below?? 35 | [ ] functional_class_factory - using metaprogramming machinery in a function 36 | [ ] configuration_class_factory - using configuration to make a dynamic model 37 | [ ] dynamic link? 38 | 39 | 40 | 41 | 42 | [ ] new_model_template -- 43 | [ ] extend existing model with pre-populated fields/using placeholders 44 | [ ] Defining fields/creating new fields 45 | [ ] Creating a new implementaiton vs using existing 46 | [ ] custom metaclass for construction/model behaviors 47 | [ ] staticmethod vs classmethod vs (instance)method -- or is this in topics 48 | 49 | 50 | 51 | [ ] new_solver -- create new implementation to re-route ?? :( 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /docs/howto_src/parallel_processing.py: -------------------------------------------------------------------------------- 1 | """ 2 | =================== 3 | Parallel Processing 4 | =================== 5 | """ 6 | 7 | # %% 8 | # You should be able to treat models like any other function in terms of 9 | # parallelization. This example shows using the built-in :mod:`multiprocessing` to do 10 | # process-based parallelization of an explicit system. 11 | 12 | from multiprocessing import Pool 13 | 14 | import condor 15 | 16 | 17 | class Model(condor.ExplicitSystem): 18 | x = input() 19 | output.y = -(x**2) + 2 * x + 1 20 | 21 | 22 | if __name__ == "__main__": 23 | with Pool(5) as p: 24 | models = p.map(Model, [1, 2, 3]) 25 | 26 | for model in models: 27 | print(model.input, model.output) 28 | -------------------------------------------------------------------------------- /docs/howto_src/table_basics.py: -------------------------------------------------------------------------------- 1 | """ 2 | ============ 3 | Tabular Data 4 | ============ 5 | """ 6 | 7 | # %% 8 | # It is often useful to interpolate pre-existing data. For this, the 9 | # :class:`~condor.contrib.TableLookup` model provides a convenient way to specify the 10 | # interpolant input and output data. This model also provids an example of using a 11 | # :class:`~condor.contrib.ExternalSolverWrapper` by wrapping uses the `ndsplines 12 | # `_ library to perform the interpolation and 13 | # compute derivatives as needed for tensor-product B-splines. Note that this table 14 | # model assumes fixed input and output data, but a model with variable input and output 15 | # data could be defined as needs arise. 16 | # 17 | # Because :class:`TableLookup` is an :class:`ExternalSolverWrapper`, the declaration of 18 | # a model quite different from a standard :class:`ModelTemplate`, 19 | # with the relevant data is passed in in a way that appears more similar to a standard 20 | # Python object instantiation with arguments for the input data, output data, degree, 21 | # and boundary conditions. Condor supports any number of inputs, and automatically 22 | # computes the derivatives :math:`\frac{dy_i}{dx_j}` as needed. 23 | # 24 | # Here we demonstrate the construction of a single-input, single-output table for the 25 | # :math:`sin` function 26 | 27 | import numpy as np 28 | 29 | import condor 30 | from condor.backend import operators as ops 31 | 32 | # input and output data are dictionaries with keys for the name of the element and 33 | # values to construct the interpolant. 34 | data_x = dict(x=np.linspace(-1, 1, 5) * ops.pi) 35 | data_y = dict(y=ops.sin(data_x["x"])) 36 | SinTable = condor.TableLookup(data_x, data_y) 37 | 38 | 39 | out = SinTable(np.pi / 2) 40 | print(out.y) 41 | assert np.isclose(out.y, 1) 42 | 43 | # %% 44 | # Next, we construct a table with two inputs and a single output 45 | 46 | Table = condor.TableLookup( 47 | dict( 48 | x1=[-1, -0.5, 0, 0.5, 1], 49 | x2=[0, 1, 2, 3], 50 | ), 51 | dict( 52 | y1=[ 53 | [0, 1, 2, 3], 54 | [3, 4, 5, 6], 55 | [6, 7, 8, 9], 56 | [8, 7, 6, 5], 57 | [4, 3, 2, 1], 58 | ] 59 | ), 60 | ) 61 | 62 | tab_out = Table(x1=0.5, x2=0.1) 63 | print(tab_out.output) 64 | 65 | # %% 66 | # Next we demonstrate specifying the degrees (and boundary conditions) for the 67 | # :code:`SinTable`. Note that these can be specified for each input (and boundary) 68 | # independently, or a single custom value can be broadcast to each input (and boundary). 69 | 70 | from matplotlib import pyplot as plt 71 | 72 | eval_x = np.linspace(-1.1, 1.1, 100) * np.pi 73 | 74 | fig, ax = plt.subplots(constrained_layout=True) 75 | for k in [0, 1, 3]: 76 | # for cubic polynomial, use constant slope (constant first derivative, 0 second 77 | # derivative) boundary condition instead of default not-a-knot (constant, non-zero, 78 | # second derivative) 79 | bcs = (2, 0) if k == 3 else (-1, 0) 80 | 81 | SinTable = condor.TableLookup(data_x, data_y, degrees=k, bcs=bcs) 82 | y = np.array([SinTable(x).y for x in eval_x]).squeeze() 83 | 84 | plt.plot(eval_x, y, label=f"k={k}") 85 | 86 | plt.plot(data_x["x"], data_y["y"], "ko") 87 | plt.plot(eval_x, np.sin(eval_x), "k--", label="true") 88 | 89 | plt.grid(True) 90 | plt.legend() 91 | 92 | plt.show() 93 | -------------------------------------------------------------------------------- /docs/images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/condor/2976ecae44fa94b3b882acc3b40c050126f1403e/docs/images/architecture.png -------------------------------------------------------------------------------- /docs/images/math-model-process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/condor/2976ecae44fa94b3b882acc3b40c050126f1403e/docs/images/math-model-process.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Condor documentation master file, created by 2 | sphinx-quickstart on Mon Sep 18 09:55:53 2023. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | 7 | 8 | Welcome to Condor's documentation! 9 | ================================== 10 | .. 11 | .. include:: ../README.rst 12 | 13 | The documentation is organized as follows: 14 | 15 | * :doc:`Tutorials ` have several small examples that show you the basics of translating a mathematical model into condor code, evaluating the model, and using it other scientific python tools. Start here if you're new to condor. 16 | 17 | * :doc:`Topic guides ` discuss key topics and concepts at a 18 | fairly high level and provide useful background information and explanation. 19 | 20 | * :doc:`API docs ` contain technical reference for APIs and other aspects of Condor's machinery. They describe how it works and how to use it but assume that you have a basic understanding of key concepts. 21 | 22 | * :doc:`How-to guides ` are recipes. They guide you through the 23 | steps involved in addressing key problems and use-cases. They are more 24 | advanced than tutorials and assume some knowledge of how Condor works. 25 | 26 | .. toctree:: 27 | :hidden: 28 | :maxdepth: 2 29 | :caption: Contents: 30 | 31 | tutorial/index 32 | howto/index 33 | api/index 34 | topics/index 35 | 36 | 37 | Indices and tables 38 | ================== 39 | 40 | * :ref:`genindex` 41 | * :ref:`modindex` 42 | * :ref:`search` 43 | -------------------------------------------------------------------------------- /docs/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 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 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 | -------------------------------------------------------------------------------- /docs/topics/glossary.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | Glossary of Terms 3 | ======================= 4 | 5 | Condor Specific Terms 6 | ====================== 7 | 8 | .. glossary:: 9 | 10 | computational engine 11 | the external library that is used to perform symbolic differentiation and generate numeric functions 12 | 13 | backend 14 | The Condor "shim" submodule that provides a consistent interface to any supported computational engine 15 | 16 | ``backend_repr`` 17 | an expression representable by the computational engine 18 | 19 | element 20 | a wrapper around a ``backend_repr`` that includes relevant metadata such as shape and name 21 | 22 | field 23 | organization of elements according to purpose, e.g. variable field on Optimization Problem models. 24 | 25 | model 26 | mathematical representation of an engineering system that the user writes by using Condor's mathematical 27 | domain-specific language in Python's class declaration format 28 | 29 | solver 30 | a piece of software "code" that can evaluate a type of model represented in a particular canonical form 31 | 32 | implementation 33 | a class that uses the backend (1) to transform the model as written by the engineer into the canonical form expected 34 | by the solver, call the solver, and transform the solver results back into the form of the model, and (2) provide 35 | the symbolic metadata for differentiation rules back to the computational engine 36 | 37 | embedded model 38 | a model being evaluated as part of another model definition 39 | 40 | model instance 41 | an evaluation of the model with the specified input values bound; may be symbolic when embedded into another model 42 | 43 | bind 44 | attach specific values to the inputs and outputs of a model to create a model instance 45 | 46 | model template 47 | a class that defines what fields and placeholder values a particular model type can use; a model subclasses a template 48 | 49 | model metaclass 50 | a metaclass that processes the model declaration so it can be evalauted into a model instance 51 | 52 | model metadata class 53 | a dataclass for holding metadata for a model 54 | 55 | placeholder 56 | a field provided to model templates to define singleton reserved word values, like ``t0`` and ``tf`` for a trajectory 57 | or the ``objective`` for an optimization problem 58 | 59 | submodel 60 | a model (template) for defining models that is intrinsically tied to primary model, e.g., events, modes, and trajectory 61 | analysis models are submodels to the primary ODE System model 62 | 63 | 64 | General Object-Oriented and Metaprogramming Terms 65 | ===================================================== 66 | 67 | .. glossary:: 68 | 69 | base 70 | A relatively complete class to inherit from; inheritors will generally make behavior more specific by overwriting methods. 71 | Inheritors can re-use base's methods in python by using super() 72 | 73 | mixin 74 | A class that provides specific behavior, an inheritor may also inherit from other mixins and even a "base" to maximize 75 | code reuse 76 | 77 | type 78 | The class of a class (i.e., a class is an object of type, type); used as a suffix for a metaclass 79 | 80 | metaclass 81 | The class of a particular class, does name space preparation before user's class declaration and processing at closure 82 | 83 | contrib 84 | Included implementations of a library's capability, the batteries in "batteries included" 85 | 86 | MRO 87 | the "method resolution order" which defines the order of classes to resolve an attribute definition 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /docs/topics/index.rst: -------------------------------------------------------------------------------- 1 | =========================== 2 | Topics 3 | =========================== 4 | 5 | Here we'll discuss advanced topics to understand the design of condor and best-practices 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | design 11 | glossary 12 | 13 | .. 14 | expectations_for_library - functional vs configuration-based class factories, custom models 15 | 16 | -------------------------------------------------------------------------------- /docs/tutorial_src/index.rst: -------------------------------------------------------------------------------- 1 | ========= 2 | Tutorials 3 | ========= 4 | 5 | We provide a few simple tutorial examples to illustrate modeling with Condor. 6 | 7 | .. toctree:: 8 | :maxdepth: 2 9 | 10 | /tutorial/sellar 11 | /tutorial/polar_transform 12 | /tutorial/trajectory 13 | 14 | -------------------------------------------------------------------------------- /docs/tutorial_src/polar_transform.py: -------------------------------------------------------------------------------- 1 | """ 2 | ========================= 3 | Polar Transformation 4 | ========================= 5 | """ 6 | # %% 7 | # As another example, if we were interested in transforming Cartesian coordinates to 8 | # polar form: 9 | # 10 | # .. math:: 11 | # \begin{align} 12 | # p_r &= \sqrt{x^2 + y^2} \\ 13 | # p_{\theta} &= \tan^{-1}\left(\frac{y}{x}\right) 14 | # \end{align} 15 | # 16 | # We can implement this with an ``ExplicitSystem`` by declaring the inputs and outputs 17 | # of this system as follows: 18 | 19 | import condor as co 20 | from condor.backend import operators as ops 21 | 22 | 23 | class PolarTransform(co.ExplicitSystem): 24 | x = input() 25 | y = input() 26 | 27 | output.r = ops.sqrt(x**2 + y**2) 28 | # output.theta = ops.atan2(y, x) 29 | output.theta = ops.atan(y / x) 30 | 31 | 32 | # %% 33 | # In general, once you've defined any system in Condor, you can just evaulate it 34 | # numerically by passing in numbers: 35 | 36 | p = PolarTransform(x=3, y=4) 37 | print(p) 38 | 39 | # %% 40 | # The output returned by such a call is designed for inspection to the extent that we 41 | # recommend working in an interactive session or debugger, especially when getting 42 | # accustomed to Condor features. 43 | # 44 | # For example, the outputs of an explicit system are accessible directly: 45 | 46 | print(p.r) 47 | 48 | # %% 49 | # They can also be retrieved collectively: 50 | 51 | print(p.output) 52 | 53 | # %% 54 | # You can of course call it again with different arguments 55 | 56 | print(PolarTransform(x=1, y=0).output.asdict()) 57 | 58 | 59 | # %% 60 | # While the *binding* of the results in a datastructure is nice, the real benefit of 61 | # constructing condor models is in calling iterative solvers. For example, we could 62 | # perform symbolic manipulation to define another ``ExplicitSystem`` with :math:`x = 63 | # r\cos\theta` and :math:`y = r\sin\theta`. Or we can we use Condor to 64 | # numerically solve this algebraic system of equations using an ``AlgebraicSystem`` by 65 | # declaring the input radius and angle as ``parameter``\s and the solving variables for 66 | # :math:`x` and :math:`y`. Mathematically, we are defining the system of algebraic 67 | # equations 68 | # 69 | # .. math:: 70 | # r &= p_r (x^*, y^*) \\ 71 | # \theta &= p_{\theta} (x^*, y^*) 72 | # 73 | # and letting an iterative solver find the solution :math:`x^*,y^*` satisfying both 74 | # residual equations given parameters :math:`r` and :math:`\theta`. In Condor, 75 | 76 | 77 | class CartesianTransform(co.AlgebraicSystem): 78 | # r and theta are input parameters 79 | r = parameter() 80 | theta = parameter() 81 | 82 | # solver will vary x and y to satisfy the residuals 83 | x = variable(initializer=1) 84 | y = variable(initializer=0) 85 | 86 | # get r, theta from solver's x, y 87 | p = PolarTransform(x=x, y=y) 88 | 89 | # residuals to converge to 0 90 | residual(r == p.r) 91 | residual(theta == p.theta) 92 | 93 | 94 | out = CartesianTransform(r=1, theta=ops.pi / 4) 95 | print(out.x, out.y) 96 | 97 | # %% 98 | # Note also that passing the inputs (or any intermediates) to plain numeric functions 99 | # that can handle symbolic objects as well as pure numerical objects (float or numpy 100 | # arrays) could work for this simple example. However, since we *embedded* the 101 | # ``PolarTransform`` model in this solver, the system evaluated with the solved variable 102 | # values is directly accessible if the ``bind_embedded_models`` option is ``True`` 103 | # (which it is by default), as in: 104 | 105 | 106 | print(out.p.output) 107 | 108 | # %% 109 | # Note that this has multiple solutions due to the form of the algebraic relationship of 110 | # the polar/rectangular transformation. The :class:`AlgebraicSystem` uses Newton's 111 | # method as the solver, so the solution that is found depends on the initial conditions. 112 | # The :attr:`initializer` attribute on the :attr:`variable` field determines the initial 113 | # position. For example, 114 | 115 | CartesianTransform.set_initial(x=-1, y=-1) 116 | out = CartesianTransform(r=1, theta=ops.pi / 4) 117 | print(out.variable) 118 | 119 | 120 | # %% 121 | # An additional :attr:`warm_start` attribute determines whether the initializer is 122 | # over-wrriten. Since the default is true, we can inspect the initializer values, 123 | 124 | print(CartesianTransform.x.initializer, CartesianTransform.y.initializer) 125 | 126 | # %% 127 | # and re-solve with attr:`warm_start` False 128 | 129 | CartesianTransform.y.warm_start = False 130 | -------------------------------------------------------------------------------- /docs/tutorial_src/sellar.py: -------------------------------------------------------------------------------- 1 | """ 2 | ====================== 3 | Introduction to Condor 4 | ====================== 5 | """ 6 | 7 | # %% 8 | # We wanted to have an API that looks as much like a mathematical description as 9 | # possible with as little distraction from programming cruft as possible. For example, 10 | # an arbitrary system of equations like from Sellar [sellar]_, 11 | # 12 | # .. math:: 13 | # \begin{align} 14 | # y_{1}&=x_{0}^{2}+x_{1}+x_{2}-0.2\,y_{2} \\ 15 | # y_{2}&=\sqrt{y_{1}}+x_{0}+x_{1} 16 | # \end{align} 17 | # 18 | # should be writable as 19 | # 20 | # .. code-block:: python 21 | # 22 | # y1 == x[0] ** 2 + x[1] + x[2] - 0.2 * y2 23 | # y2 == y1**0.5 + x[0] + x[1] 24 | # 25 | # Of course, in both the mathematical and programmatic description, the source of each 26 | # symbol must be defined. In an engineering memo, we might say "where :math:`y_1,y_2` 27 | # are the variables to solve and :math:`x \in \mathbb{R}^3` parameterizes the system of 28 | # equations," which suggests the API for an algebraic system of equations as 29 | 30 | import condor 31 | 32 | 33 | class Coupling(condor.AlgebraicSystem): 34 | x = parameter(shape=3) 35 | y1 = variable(initializer=1.0) 36 | y2 = variable(initializer=1.0) 37 | 38 | residual(y1 == x[0] ** 2 + x[1] + x[2] - 0.2 * y2) 39 | residual(y2 == y1**0.5 + x[0] + x[1]) 40 | 41 | 42 | # %% 43 | # which can be evaluated by instantiating the model with numerical values for the 44 | # parameters: 45 | 46 | coupling = Coupling([5.0, 2.0, 1]) 47 | 48 | # %% 49 | # Once the model is finished running, the model *binds* the numerical results from the 50 | # iterative solver to the named *element* and *field* attributes on the instance. That 51 | # is, elements of fields accessible directly: 52 | 53 | print(coupling.y1, coupling.y2) 54 | 55 | # %% 56 | # Fields are bound as dataclasses 57 | 58 | print(coupling.variable) 59 | 60 | 61 | # %% 62 | # Models can be used recursively, building up more sophisticated models by *embedding* 63 | # models within another. However, system encapsulation is enforced so only elements from 64 | # input and output fields are accessible after the model has been defined. For example, 65 | # we may wish to optimize Sellar's algebraic system of equations. Mathematically, we can 66 | # define the optimization as 67 | # 68 | # .. math:: 69 | # \begin{aligned} 70 | # \operatorname*{minimize}_{x \in \mathbb{R}^3} & \quad x_2^2+x_1+y_1+e^{-y_{2}} \\ 71 | # \text{subject to} & \quad 3.16 \le y_1 \\ 72 | # & \quad y_2 \le 24.0 73 | # \end{aligned} 74 | # 75 | # where :math:`y_1` and :math:`y_2` are the solution to the system of algebraic 76 | # equations described above. In condor, we can write this as 77 | 78 | from condor.backend import operators as ops 79 | 80 | 81 | class Sellar(condor.OptimizationProblem): 82 | x = variable(shape=3, lower_bound=0, upper_bound=10) 83 | coupling = Coupling(x) 84 | y1, y2 = coupling 85 | 86 | objective = x[2] ** 2 + x[1] + y1 + ops.exp(-y2) 87 | constraint(y1 >= 3.16) 88 | constraint(y2 <= 24.0) 89 | 90 | 91 | # %% 92 | # As with the system of algebraic equations, we can numerically solve this optimization 93 | # problem by providing an initial value for the variables and instantiating the model. 94 | 95 | Sellar.set_initial(x=[5, 2, 1]) 96 | sellar = Sellar() 97 | 98 | # %% 99 | # The resulting object will have a dot-able data structure with the bound results, 100 | # including the embedded ``Coupling`` model: 101 | 102 | print("objective value:", sellar.objective) # scalar value 103 | print(sellar.constraint) # field 104 | print(sellar.coupling.y1) # embedded-model element 105 | 106 | # %% 107 | # .. rubric:: References 108 | # .. [sellar] Sellar, R., Batill, S., and Renaud, J., "Response Surface Based, 109 | # Concurrent Subspace Optimization for Multidisciplinary System Design," 1996. 110 | # https://doi.org/10.2514/6.1996-714 111 | -------------------------------------------------------------------------------- /docs/tutorial_src/trajectory.py: -------------------------------------------------------------------------------- 1 | """ 2 | ========================= 3 | Working with trajectories 4 | ========================= 5 | """ 6 | 7 | # %% 8 | # Condor is not intended to be an optimal control library per se, but we often end up 9 | # working with trajectories a great deal and spent considerable effort to make modeling 10 | # dynamical systems nice. 11 | # 12 | # Glider Model 13 | # ------------ 14 | # 15 | # For this tutorial, we will consider a simplified model of a glider with some form of 16 | # angle-of-attack control. We can represent this as a system of ordinary differential 17 | # equations (ODEs) given by 18 | # 19 | # .. math:: 20 | # \begin{align} 21 | # \dot{r} &= v \cos \gamma \\ 22 | # \dot{h} &= v \sin \gamma \\ 23 | # \dot{\gamma} &= (CL(\alpha) \cdot v^2 - g \cos \gamma) / v \\ 24 | # \dot{v} &= - CD(\alpha) \cdot v^2 - g \sin \gamma \\ 25 | # \end{align} 26 | # 27 | # where :math:`r` is the range, or horizontal position, :math:`h` is the altitude, or 28 | # vertical position, :math:`v` is the velocity, :math:`\gamma` is the flight-path 29 | # angle, and :math:`\alpha` is the angle-of-attack, which modulates the coefficients of 30 | # lift, :math:`CL`, and drag, :math:`CD`, and :math:`g` is the acceleration due to 31 | # gravity. Simple models of the lift and drag are given by 32 | # 33 | # .. math:: 34 | # \begin{align} 35 | # CL(\alpha) &= CL_{\alpha} \cdot \alpha \\ 36 | # CD(\alpha) &= CD_0 + CD_{i,q} \cdot CL^2 \\ 37 | # \end{align} 38 | # 39 | # where :math:`CL_{\alpha}` is the lift slope, :math:`CD_0` is the 0-lift drag, and 40 | # :math:`CD_{i,q}` is the quadratic coefficient for the lift-induced drag. In Condor, we 41 | # can implement this as, 42 | 43 | import condor 44 | from condor.backend import operators as ops 45 | 46 | 47 | class Glider(condor.ODESystem): 48 | r = state() 49 | h = state() 50 | gamma = state() 51 | v = state() 52 | 53 | alpha = modal() 54 | 55 | CL_alpha = parameter() 56 | CD_0 = parameter() 57 | CD_i_q = parameter() 58 | g = parameter() 59 | 60 | CL = CL_alpha * alpha 61 | CD = CD_0 + CD_i_q * CL**2 62 | 63 | dot[r] = v * ops.cos(gamma) 64 | dot[h] = v * ops.sin(gamma) 65 | dot[gamma] = (CL * v**2 - g * ops.cos(gamma)) / v 66 | dot[v] = -CD * v**2 - g * ops.sin(gamma) 67 | 68 | initial[r] = 0.0 69 | initial[h] = 1.0 70 | initial[v] = 15.0 71 | initial[gamma] = 30 * ops.pi / 180.0 72 | 73 | 74 | # %% 75 | # The :attr:`modal` field is used to define elements with deferred and possibly varying 76 | # behavior, so we use this for the angle-of-attack so we can simulate multiple 77 | # behaviors. To simulate this model, we create a 78 | # :class:`~condor.contrib.TrajectoryAnalysis`, a sub-model to an ODE Sysstem, which is 79 | # ultimately responsible for defining the specifics of integrating the ODE. In this 80 | # example, the :class:`TrajectoryAnalysis` model only specifies the final simulation 81 | # time of the model. It is more mathematically consistent to have the initial values 82 | # defined in the trajectory analysis, but for convenience we declared it as part of the 83 | # ODE system. 84 | 85 | 86 | class FirstSim(Glider.TrajectoryAnalysis): 87 | tf = 20.0 88 | 89 | 90 | # %% 91 | # The fields of the original :class:`Glider` simulation are copied to the 92 | # :class:`TrajectoryAnalysis` so the parameter values must be supplied to evaluate the 93 | # model numerically. 94 | # 95 | 96 | A = 3e-1 97 | first_sim = FirstSim(CL_alpha=0.11 * A, CD_0=0.05 * A, CD_i_q=0.05, g=1.0) 98 | 99 | # %% 100 | # In addition to binding static parameters like the other built-in models, the 101 | # time-histories for the :attr:`state` and :attr:`dynamic_output` are bound and can be 102 | # accessed for plotting. For example, we can plot time histories 103 | 104 | from matplotlib import pyplot as plt 105 | 106 | state_data = first_sim.state.asdict() 107 | 108 | fig, axs = plt.subplots(nrows=len(state_data), constrained_layout=True, sharex=True) 109 | for ax, (state_name, state_hist) in zip(axs, state_data.items()): 110 | ax.plot(first_sim.t, state_hist) 111 | ax.set_ylabel(state_name) 112 | ax.grid(True) 113 | ax.set_xlabel("t") 114 | 115 | # %% 116 | # and the flight path 117 | 118 | import numpy as np 119 | 120 | 121 | def flight_path_plot(sims, **plot_kwargs): 122 | fig, ax = plt.subplots(constrained_layout=True, figsize=(6.4, 3.2)) 123 | plt.ylabel("altitude") 124 | plt.xlabel("range") 125 | # reverse zorders to show progression of sims more nicely 126 | zorders = np.linspace(2.1, 2.5, len(sims))[::-1] 127 | marker = plot_kwargs.pop("marker", "o") 128 | for sim, zorder in zip(sims, zorders): 129 | ax.plot(sim.r, sim.h, marker=marker, zorder=zorder, **plot_kwargs) 130 | plt.grid(True) 131 | ax.set_aspect("equal") 132 | ax.set_ylim(-3, 30) 133 | ax.set_xlim(-3, 90) 134 | return ax 135 | 136 | 137 | flight_path_plot([first_sim]) 138 | 139 | # %% 140 | # Modeling the Ground with an Event 141 | # --------------------------------- 142 | # 143 | # Notice the glider eventually flies straight through the ground. We can fix that with 144 | # an :class:`Event` sub-model that detects the altitude zero-crossing and flips the 145 | # descent to an ascent with a simple parametrized loss model. 146 | 147 | 148 | class Bounce(Glider.Event): 149 | function = h 150 | update[gamma] = -gamma 151 | mu = parameter() 152 | update[v] = mu * v 153 | 154 | 155 | # %% 156 | # We still need to create a new :class:`TrajectoryAnalysis` since the :class:`FirstSim` 157 | # was bound to the :class:`Glider` model at the time of creation (without the bounce 158 | # event). 159 | 160 | 161 | class BounceSim(Glider.TrajectoryAnalysis): 162 | tf = 20.0 163 | 164 | 165 | bounce_sim = BounceSim(**first_sim.parameter.asdict(), mu=0.9) 166 | 167 | flight_path_plot([first_sim, bounce_sim]) 168 | plt.legend(["original sim", "with bounce"]) 169 | 170 | # %% 171 | # Angle of Attack Control with a Mode 172 | # ----------------------------------- 173 | # 174 | # We can also add a behavior for the angle of attack using a mode, in this case 175 | # holding a constant angle of attack after reaching peak altitude to reduce 176 | # rate of descent. 177 | # 178 | # To ensure proper numerical behavior, we follow [orbital ref] and use an accumulator 179 | # state to encode the flight controller logic. In this case, we create an event to 180 | # detect the switch from ascent to descent and perform a state update. 181 | 182 | 183 | class MaxAlt(Glider.Event): 184 | function = gamma 185 | max_alt = state() 186 | update[max_alt] = h 187 | 188 | 189 | # %% 190 | # The mode can now be triggered by the accumulator state update, where we set 191 | # :math:`\alpha` to a new constant parameter. 192 | 193 | 194 | class DescentAlphaHold(Glider.Mode): 195 | condition = max_alt > 0 196 | hold_alpha = parameter() 197 | action[alpha] = hold_alpha 198 | 199 | 200 | # %% 201 | # The glider now travels a little further with this control behavior. 202 | 203 | 204 | class AlphaSim(Glider.TrajectoryAnalysis): 205 | tf = 20.0 206 | 207 | 208 | alpha_sim = AlphaSim(**bounce_sim.parameter.asdict(), hold_alpha=0.5) 209 | 210 | ax = flight_path_plot([first_sim, bounce_sim, alpha_sim]) 211 | ax.legend(["original sim", "with bounce", "gradual descent"]) 212 | 213 | 214 | # %% 215 | # Trajectory Outputs 216 | # ------------------ 217 | # 218 | # So far, we have only used the :class:`TrajectoryAnalysis` to simulate the ODE System. 219 | # In order to use the ODE system as part of other condor models, we must declare 220 | # :attr:`trajectory_output`. Condor computes the gradient of the 221 | # :attr:`trajectory_output` using the Sweeping Gradient method. Each trajectory output 222 | # has the form 223 | # 224 | # .. math:: 225 | # J = \phi\left(t_{f},x\left(t_{f}\right),p\right) + 226 | # \int_{t_{0}}^{t_{f}}L\left(\tau,x\left(\tau\right),p\right)\,d\tau 227 | # 228 | # where :math:`\phi` is the terminal term, :math:`L\left(\cdot\right)` is the integrand 229 | # term, and :math:`x\left(t\right)` is the solution to the system of ODEs with events 230 | # and modes. 231 | # 232 | # First we'll change the control behavior to use a constant angle of attack through the 233 | # whole trajectory to get the peak altitude to vary. We'll also make the bounce event 234 | # terminal since we're interested in the flown range. 235 | 236 | 237 | class ConstantAlphaHold(Glider.Mode): 238 | condition = 1 239 | action[alpha] = 1 * DescentAlphaHold.hold_alpha 240 | 241 | 242 | Bounce.terminate = True 243 | 244 | # %% 245 | # We can form the area under the flight-path curve by taking the derivative 246 | # :math:`\dot{r}` and using it to form the integrand. We can also just take the final 247 | # max altitude state we added with the ``MaxAlt`` event and the final range. 248 | 249 | 250 | class AlphaSim(Glider.TrajectoryAnalysis): 251 | initial[r] = 0.0 252 | initial[h] = 1.0 253 | initial[v] = 15.0 254 | initial[gamma] = 30 * ops.pi / 180.0 255 | tf = 100.0 256 | 257 | area = trajectory_output(integrand=dot[r] * h) 258 | max_h = trajectory_output(max_alt) 259 | max_r = trajectory_output(r) 260 | 261 | class Options: 262 | state_rtol = 1e-12 263 | state_atol = 1e-15 264 | adjoint_rtol = 1e-12 265 | adjoint_atol = 1e-15 266 | 267 | 268 | # %% 269 | # Then we can compare areas with different hold angles of attack: 270 | 271 | params = bounce_sim.parameter.asdict() 272 | results = { 273 | "alpha = +0.5 deg": AlphaSim(**params, hold_alpha=0.5), 274 | "alpha = 0.0 deg": AlphaSim(**params, hold_alpha=0.0), 275 | "alpha = -0.5 deg": AlphaSim(**params, hold_alpha=-0.5), 276 | } 277 | 278 | print(*[f"{k}: {v.area}" for k, v in results.items()], sep="\n") 279 | 280 | # %% 281 | # 282 | 283 | ax = flight_path_plot(results.values()) 284 | ax.legend([k.replace("alpha", r"$\alpha$") for k in results]) 285 | 286 | 287 | # %% 288 | # Embedding 289 | # --------- 290 | # 291 | # With several :attr:`trajectory_output` elements declared, we can embed the trajectory 292 | # within other Condor models, for example to maximize a combination of the peak height 293 | # and flown range. 294 | # 295 | 296 | 297 | class GlideOpt(condor.OptimizationProblem): 298 | alpha = variable( 299 | initializer=0.001, 300 | lower_bound=-1.0, 301 | upper_bound=1, 302 | warm_start=False, 303 | ) 304 | sim = AlphaSim(**bounce_sim.parameter.asdict(), hold_alpha=alpha) 305 | trade_off = parameter() 306 | objective = -(trade_off * sim.max_h + (1 - trade_off) * sim.max_r) 307 | 308 | class Options: 309 | exact_hessian = False 310 | print_level = 0 311 | tol = 1e-3 312 | max_iter = 8 313 | 314 | 315 | opt_range = GlideOpt(trade_off=0) 316 | 317 | ax = flight_path_plot([opt_range.sim]) 318 | ax.text( 319 | *(0.05, 0.92), 320 | f"max range: {opt_range.sim.max_r} ($\\alpha={opt_range.alpha}$", 321 | transform=ax.transAxes, 322 | ) 323 | 324 | # %% 325 | # We can store the iteration history using :attr:`iter_callback` option on 326 | # :class:`OptimizationProblem`, pointing it to the method of a class to store the 327 | # simulation data on each call. Since it only gets information relevant to the 328 | # optimization problem itself, we use 329 | # :meth:`~condor.contrib.OptimizationProblem.from_values` to reconstruct the internals 330 | # of the analysis with the simulation outputs bound to the ``sim`` attribute. 331 | 332 | 333 | class IterStore: 334 | def __init__(self): 335 | self.parameter = None 336 | 337 | def init_callback(self, parameter, impl_opts): 338 | self.parameter = parameter 339 | self.iters = [] 340 | 341 | def iter_callback(self, i, variable, objective, constraint): 342 | iter_opt_res = GlideOpt.from_values( 343 | **variable.asdict(), 344 | **self.parameter.asdict(), 345 | ) 346 | self.iters.append(iter_opt_res) 347 | 348 | 349 | hist = IterStore() 350 | GlideOpt.Options.init_callback = hist.init_callback 351 | GlideOpt.Options.iter_callback = hist.iter_callback 352 | 353 | opt_alt = GlideOpt(trade_off=1) 354 | 355 | ax = flight_path_plot([it.sim for it in hist.iters], marker=None) 356 | ax.legend([f"iter {idx}, alpha={sim.alpha}" for idx, sim in enumerate(hist.iters)]) 357 | ax.set_title("max altitude iterations") 358 | ax.set_ylim(-3, 45) 359 | -------------------------------------------------------------------------------- /examples/LinCovCW.py: -------------------------------------------------------------------------------- 1 | import casadi as ca 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from scipy.interpolate import make_interp_spline 5 | from scipy.io import loadmat 6 | 7 | import condor as co 8 | 9 | I6 = ca.MX.eye(6) 10 | Z6 = ca.MX(6, 6) 11 | W = ca.vertcat(I6, Z6) 12 | 13 | I3 = ca.MX.eye(3) 14 | Z3 = ca.MX(3, 3) 15 | V = ca.vertcat(Z3, I3, ca.MX(6, 3)) 16 | 17 | Wonb = ca.MX.eye(6) 18 | Vonb = ca.vertcat(Z3, I3) 19 | 20 | 21 | class LinCovCW(co.ODESystem): 22 | omega = parameter() 23 | scal_w = parameter(shape=6) # covariance elements for propagation 24 | scal_v = parameter(shape=3) # covariance elements for control update 25 | # estimated covariance elements for navigation covariance propagation and control 26 | scalhat_w = parameter(shape=6) 27 | scalhat_v = parameter(shape=3) 28 | 29 | initial_x = parameter(shape=6) 30 | initial_C = parameter(shape=(12, 12)) 31 | initial_P = parameter(shape=(6, 6)) 32 | 33 | Acw = ca.MX(6, 6) 34 | 35 | """[ 36 | 37 | [0, 0, 0, 1, 0, 0], 38 | [0, 0, 0, 0, 1, 0], 39 | [0, 0, 0, 0, 0, 1], 40 | 41 | [0, 0, 0, 0, 0, 2*omega], 42 | [0, -omega**2, 0, 0, 0, 0], 43 | [0, 0, 3*omega**2, -2*omega, 0, 0] 44 | 45 | ] """ 46 | 47 | Acw[0, 3] = 1 48 | Acw[1, 4] = 1 49 | Acw[2, 5] = 1 50 | 51 | Acw[3, 5] = 2 * omega 52 | Acw[4, 1] = -(omega**2) 53 | Acw[5, 2] = 3 * omega**2 54 | Acw[5, 3] = -2 * omega 55 | 56 | x = state(shape=6) # true state position and velocity 57 | C = state(shape=(12, 12)) # augmented covariance 58 | P = state(shape=(6, 6)) # onboard covariance for navigation system (Kalman filter) 59 | 60 | tt = state() 61 | 62 | Scal_w = ca.diag(scal_w) 63 | Cov_prop_offset = W @ Scal_w @ W.T 64 | 65 | Scal_v = ca.diag(scal_v) 66 | Cov_ctrl_offset = V @ Scal_v @ V.T 67 | 68 | Scalhat_w = ca.diag(scalhat_w) 69 | P_prop_offset = Wonb @ Scalhat_w @ Wonb.T 70 | 71 | Scalhat_v = ca.diag(scalhat_v) 72 | P_ctrl_offset = Vonb @ Scalhat_v @ Vonb.T 73 | 74 | Fcal = ca.MX(12, 12) 75 | Fcal[:6, :6] = Acw 76 | Fcal[6:, 6:] = Acw 77 | 78 | initial[x] = initial_x 79 | initial[C] = initial_C 80 | initial[P] = initial_P 81 | 82 | dot[x] = Acw @ x 83 | dot[C] = Fcal @ C + C @ Fcal.T + Cov_prop_offset 84 | dot[tt] = 1.0 85 | 86 | # TODO: in generla case, this should be a dfhat/dx(hat) instead of exact Acw 87 | # and should be reflected in bottom right corner of Fcal as well 88 | dot[P] = Acw @ P + P @ Acw.T + P_prop_offset 89 | 90 | 91 | sin = ca.sin 92 | cos = ca.cos 93 | 94 | burns = [] 95 | 96 | 97 | def make_burn( 98 | rd, 99 | tig, 100 | tem, 101 | ): 102 | # burn_num = (1 + sum([ 103 | # event.__name__.startswith("Burn") for event in LinCovCW.Event.subclasses 104 | # ])) 105 | burn_num = 1 + len(burns) 106 | burn_name = f"Burn{burn_num:d}" 107 | attrs = co.InnerModelType.__prepare__(burn_name, (LinCovCW.Event,)) 108 | update = attrs["update"] 109 | x = attrs["x"] 110 | C = attrs["C"] 111 | P = attrs["P"] 112 | omega = attrs["omega"] 113 | Cov_ctrl_offset = attrs["Cov_ctrl_offset"] 114 | P_ctrl_offset = attrs["P_ctrl_offset"] 115 | 116 | Delta_v_disp = attrs[f"Delta_v_disp_{burn_num:d}"] = attrs["state"]() 117 | Delta_v_mag = attrs[f"Delta_v_mag_{burn_num:d}"] = attrs["state"]() 118 | 119 | if not LinCovCW.parameter.get(backend_repr=rd).name: 120 | rdnum = 1 + sum( 121 | [name.startswith("rd_") for name in LinCovCW.parameter.list_of("name")] 122 | ) 123 | attrs[f"rd_{rdnum:d}"] = rd 124 | 125 | if not LinCovCW.parameter.get(backend_repr=tig).name: 126 | tignum = 1 + sum( 127 | [name.startswith("tig_") for name in LinCovCW.parameter.list_of("name")] 128 | ) 129 | attrs[f"tig_{tignum:d}"] = tig 130 | 131 | if not LinCovCW.parameter.get(backend_repr=tem).name: 132 | temnum = 1 + sum( 133 | [name.startswith("tem_") for name in LinCovCW.parameter.list_of("name")] 134 | ) 135 | attrs[f"tem_{temnum:d}"] = tem 136 | # attrs["function"] = t - tig 137 | attrs["at_time"] = [tig] 138 | 139 | t_d = tem - tig 140 | stm = ca.MX(6, 6) 141 | stm[0, 0] = 1 142 | stm[0, 2] = 6 * omega * t_d - 6 * sin(omega * t_d) 143 | stm[0, 3] = -3 * t_d + 4 * sin(omega * t_d) / omega 144 | stm[0, 5] = 2 * (1 - cos(omega * t_d)) / omega 145 | stm[1, 1] = cos(omega * t_d) 146 | stm[1, 4] = sin(omega * t_d) / omega 147 | stm[2, 2] = 4 - 3 * cos(omega * t_d) 148 | stm[2, 3] = 2 * (cos(omega * t_d) - 1) / omega 149 | stm[2, 5] = sin(omega * t_d) / omega 150 | stm[3, 2] = 6 * omega * (1 - cos(omega * t_d)) 151 | stm[3, 3] = 4 * cos(omega * t_d) - 3 152 | stm[3, 5] = 2 * sin(omega * t_d) 153 | stm[4, 1] = -omega * sin(omega * t_d) 154 | stm[4, 4] = cos(omega * t_d) 155 | stm[5, 2] = 3 * omega * sin(omega * t_d) 156 | stm[5, 3] = -2 * sin(omega * t_d) 157 | stm[5, 5] = cos(omega * t_d) 158 | T_pp = stm[:3, :3] 159 | T_pv = stm[:3, 3:] 160 | T_pv_inv = ca.solve(T_pv, ca.MX.eye(3)) 161 | 162 | Delta_v = (T_pv_inv @ rd - T_pv_inv @ T_pp @ x[:3, 0]) - x[3:, 0] 163 | update[Delta_v_mag] = Delta_v_mag + ca.norm_2(Delta_v) 164 | # update[Delta_v_mag] = ca.norm_2(Delta_v) 165 | update[x] = x + ca.vertcat(Z3, I3) @ (Delta_v) 166 | 167 | DG = ca.vertcat(ca.MX(3, 6), ca.horzcat(-(T_pv_inv @ T_pp), -I3)) 168 | Dcal = ca.vertcat( 169 | ca.horzcat(I6, DG), 170 | ca.horzcat(Z6, I6 + DG), 171 | ) 172 | 173 | update[C] = Dcal @ C @ Dcal.T + Cov_ctrl_offset 174 | # TODO: in general case, this requires something like the bottom right corner of 175 | # Dcal which should use onboard models of control instead of exact control 176 | update[P] = P + P_ctrl_offset 177 | 178 | Mc = DG @ ca.horzcat(Z6, I6) 179 | sigma_Dv__2 = ca.trace(Mc @ C @ Mc.T) 180 | 181 | update[Delta_v_disp] = Delta_v_disp + ca.sqrt(sigma_Dv__2) 182 | # update[Delta_v_disp] = ca.sqrt(sigma_Dv__2) 183 | Burn = co.InnerModelType(burn_name, (LinCovCW.Event,), attrs=attrs) 184 | burns.append(Burn) 185 | 186 | Burn.rd = rd 187 | Burn.tig = tig 188 | Burn.tem = tem 189 | Burn.Delta_v_mag = Delta_v_mag 190 | Burn.Delta_v_disp = Delta_v_disp 191 | 192 | Burn.DG = DG 193 | Burn.Dcal = Dcal 194 | Burn.sigma_Dv__2 = sigma_Dv__2 195 | Burn.Mc = Mc 196 | return Burn 197 | 198 | 199 | make_burn.burns = burns 200 | 201 | 202 | def make_sim(sim_name="Sim"): 203 | attrs = co.InnerModelType.__prepare__(sim_name, (LinCovCW.TrajectoryAnalysis,)) 204 | C = attrs["C"] 205 | trajectory_output = attrs["trajectory_output"] 206 | 207 | Mr = ca.horzcat(I3, ca.MX(3, 9)) 208 | sigma_r__2 = ca.trace(Mr @ C @ Mr.T) 209 | attrs["final_pos_disp"] = trajectory_output(ca.sqrt(sigma_r__2)) 210 | 211 | Mv = ca.horzcat(Z3, I3, ca.MX(3, 6)) 212 | sigma_vf__2 = ca.trace(Mv @ C @ Mv.T) 213 | attrs["final_vel_disp"] = trajectory_output(ca.sqrt(sigma_vf__2)) 214 | 215 | attrs["final_vel_mag"] = trajectory_output(ca.norm_2(attrs["x"][3:, 0])) 216 | 217 | Delta_v_mags = [] 218 | Delta_v_disps = [] 219 | for burn in make_burn.burns: 220 | Dv_mag_state = LinCovCW.state.get(backend_repr=burn.Delta_v_mag) 221 | attrs[Dv_mag_state.name] = trajectory_output(burn.Delta_v_mag) 222 | Delta_v_mags.append(attrs[Dv_mag_state.name]) 223 | 224 | Dv_disp_state = LinCovCW.state.get(backend_repr=burn.Delta_v_disp) 225 | attrs[Dv_disp_state.name] = trajectory_output(burn.Delta_v_disp) 226 | Delta_v_disps.append(attrs[Dv_disp_state.name]) 227 | 228 | # attrs["tot_Delta_v_mag"] = trajectory_output( 229 | # sum([burn.Delta_v_mag for burn in make_burn.burns]) 230 | # ) 231 | # attrs["tot_Delta_v_disp"] = trajectory_output( 232 | # sum([burn.Delta_v_disp for burn in make_burn.burns]) 233 | # ) 234 | 235 | class Casadi(co.Options): 236 | # state_rtol = 1E-9 237 | # state_atol = 1E-15 238 | # adjoint_rtol = 1E-9 239 | # adjoint_atol = 1E-15 240 | # state_max_step_size = 30. 241 | 242 | state_adaptive_max_step_size = 4 # 16 243 | adjoint_adaptive_max_step_size = 4 244 | 245 | # integrator_options = dict( 246 | # max_step = 1., 247 | # #atol = 1E-15, 248 | # #rtol = 1E-12, 249 | # nsteps = 10000, 250 | # ) 251 | 252 | attrs["Casadi"] = Casadi 253 | 254 | Sim = co.InnerModelType(sim_name, (LinCovCW.TrajectoryAnalysis,), attrs=attrs) 255 | Sim.Delta_v_mags = tuple(Delta_v_mags) 256 | Sim.Delta_v_disps = tuple(Delta_v_disps) 257 | Sim.implementation.callback.get_jacobian( 258 | f"jac_{Sim.implementation.callback.name}", None, None, {} 259 | ) 260 | 261 | return Sim 262 | 263 | 264 | Cov_0_matlab = loadmat("P_aug_0.mat")["P_aug_0"][0] 265 | 266 | sim_kwargs = dict( 267 | omega=0.00114, 268 | # env.translation_process_noise_disp from IC_Traj_demo_000c.m 269 | scal_w=[0.0] * 3 + [4.8e-10] * 3, 270 | # env.translation_maneuvers.var_noise_disp 271 | scal_v=[2.5e-7] * 3, 272 | # env.translation_process_noise_err from IC_Traj_demo_000c.m 273 | scalhat_w=[0.0] * 3 + [4.8e-10] * 3, 274 | # scalhat_w=[0.]*6, 275 | # env.translation_maneuvers.var_noise_err 276 | scalhat_v=[2.5e-7] * 3, 277 | # io.R = env.sensors.rel_pos.measurement_var; from rel_pos.m which is set by 278 | # sig = 1e-3*(40/3); pos_var = [(sig)^2 (sig)^2 (sig)^2]; in rel_pos_ic.m 279 | rcal=[(1e-3 * (40 / 3)) ** 2] * 3, 280 | # io.R_onb = io.onboard.sensors.rel_pos.measurement_var from rel_pos.m 281 | # in runRelMotionSetup.m, io.onboard = io.environment 282 | rcalhat=[(1e-3 * (40 / 3)) ** 2] * 3, 283 | rd_1=[500.0, 0.0, 0.0], 284 | ) 285 | sim_kwargs.update( 286 | dict( 287 | initial_x=[ 288 | -2000.0, 289 | 0.0, 290 | 1000.0, 291 | sim_kwargs["omega"] * 1000.0 * 3 / 2, 292 | 0.0, 293 | 0.0, 294 | ], 295 | initial_C=Cov_0_matlab, 296 | initial_P=Cov_0_matlab[-6:, -6:] - Cov_0_matlab[:6, :6], 297 | ) 298 | ) 299 | 300 | 301 | def deriv_check_plots(indep_var, output_vars, sims, title_prefix="", interp_k=2): 302 | sims1 = [simout[0] for simout in sims] 303 | jac = np.stack([simout[1] for simout in sims]) 304 | # Dv_mags = [sim.tot_Delta_v_mag for sim in sims1] 305 | # Dv_disps = [sim.tot_Delta_v_disp for sim in sims1] 306 | 307 | xgrid = [getattr(sim, indep_var.name) for sim in sims1] 308 | xidx = indep_var.field_type.flat_index(indep_var) 309 | 310 | ordinate_names = [output_var.name.replace("_", " ") for output_var in output_vars] 311 | field = output_vars[0].field_type 312 | ordinates = [ 313 | [getattr(sim, output_var.name) for sim in sims1] for output_var in output_vars 314 | ] 315 | ord_idxs = [field.flat_index(output_var) for output_var in output_vars] 316 | 317 | # Sim = field._model 318 | # Dv_mag_idx = Sim.trajectory_output.flat_index(Sim.Delta_v_mag_1) 319 | # Dv_disp_idx = Sim.trajectory_output.flat_index(Sim.Delta_v_disp_1) 320 | # pos_disp_idx = Sim.trajectory_output.flat_index(Sim.final_pos_disp) 321 | 322 | # Dv_mags = [sim.Delta_v_mag_1[-1] for sim in sims1] 323 | # Dv_disps = [sim.Delta_v_disp_1[-1] for sim in sims1] 324 | # pos_disps = [sim.final_pos_disp for sim in sims1] 325 | # breakpoint() 326 | # ord_idxs = [Dv_mag_idx, Dv_disp_idx, pos_disp_idx] 327 | for ord_idx, ord_name, ord_val in zip(ord_idxs, ordinate_names, ordinates): 328 | interp = make_interp_spline(xgrid, ord_val, k=interp_k) 329 | derinterp = interp.derivative() 330 | fig, axes = plt.subplots(2, constrained_layout=True, sharex=True) 331 | plt.suptitle(" ".join([title_prefix, ord_name])) 332 | axes[0].plot(xgrid, ord_val) 333 | axes[0].grid(True) 334 | axes[1].plot(xgrid, derinterp(xgrid), label="numerical") 335 | axes[1].plot(xgrid, jac[:, ord_idx, xidx], "--", label="SGM") 336 | axes[1].grid(True) 337 | axes[1].legend() 338 | return xgrid, ordinates 339 | -------------------------------------------------------------------------------- /examples/P_aug_0.mat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/condor/2976ecae44fa94b3b882acc3b40c050126f1403e/examples/P_aug_0.mat -------------------------------------------------------------------------------- /examples/condor_sellar.py: -------------------------------------------------------------------------------- 1 | from casadi import exp 2 | 3 | import condor as co 4 | 5 | 6 | class Resid(co.AlgebraicSystem): 7 | x = parameter() 8 | z = parameter(shape=2) 9 | y1 = variable(initializer=1.0) 10 | y2 = variable(initializer=1.0) 11 | 12 | residual(-y1 == -(z[0] ** 2) - z[1] - x + 0.2 * y2) 13 | residual(-y2 == -(y1**0.5) - z[0] - z[1]) 14 | 15 | 16 | class Obj(co.ExplicitSystem): 17 | x = input() 18 | z = input(shape=2) 19 | 20 | y1, y2 = Resid(x, z) 21 | 22 | output.obj = x**2 + z[1] + y1 + exp(-y2) 23 | 24 | 25 | class Constr(co.ExplicitSystem): 26 | x = input() 27 | z = input(shape=2) 28 | 29 | y1, y2 = Resid(x, z) 30 | 31 | output.con1 = 3.16 - y1 32 | output.con2 = y2 - 24.0 33 | 34 | 35 | class Sellar(co.OptimizationProblem): 36 | x = variable(lower_bound=0, upper_bound=10) 37 | z = variable(shape=2, lower_bound=0, upper_bound=10) 38 | 39 | objective = Obj(x, z).obj 40 | constrs = Constr(x, z) 41 | constraint(constrs.con1, upper_bound=0.0) 42 | constraint(constrs.con2, upper_bound=0.0) 43 | 44 | class Options: 45 | # method = ( 46 | # co.backends.casadi.implementations.OptimizationProblem.Method.scipy_slsqp 47 | # ) 48 | # disp = True 49 | iprint = 2 50 | tol = 1e-8 51 | maxiter = 0 52 | 53 | # @staticmethod 54 | # def iter_callback(idx, variable, objective, constraints): 55 | # print() 56 | # print(f"iter {idx}: {variable}") 57 | # for k, v in constraints.asdict().items(): 58 | # elem = Design.constraint.get(name=k) 59 | # print(" "*4, f"{k}: {elem.lower_bound} < {v} < {elem.upper_bound}") 60 | 61 | 62 | # resid_sol = Resid(1, [5., 2.]) 63 | Sellar.set_initial( 64 | x=1.0, 65 | z=[ 66 | 5.0, 67 | 2.0, 68 | ], 69 | ) 70 | # Sellar.implementation.set_initial(x=0, z=[3.15, 0.,]) 71 | sellar_opt = Sellar() 72 | resid_at_opt = Resid(*sellar_opt) 73 | obj_at_opt = Obj(*sellar_opt) 74 | -------------------------------------------------------------------------------- /examples/condor_sellar_2.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from casadi import exp 4 | 5 | import condor as co 6 | 7 | tic = time.perf_counter() 8 | 9 | 10 | class Coupling(co.AlgebraicSystem): 11 | x = parameter() 12 | z = parameter(shape=2) 13 | y1 = implicit_output(initializer=1.0) 14 | y2 = implicit_output(initializer=1.0) 15 | 16 | residual.y1 = y1 - z[0] ** 2 - z[1] - x + 0.2 * y2 17 | residual.y2 = y2 - y1**0.5 - z[0] - z[1] 18 | 19 | 20 | coupling = Coupling(1, [5.0, 2.0]) 21 | 22 | 23 | class Sellar(co.OptimizationProblem): 24 | x = variable(lower_bound=0, upper_bound=10) 25 | z = variable(shape=2, lower_bound=0, upper_bound=10) 26 | coupling = Coupling(x, z) 27 | y1, y2 = coupling 28 | 29 | objective = x**2 + z[1] + y1 + exp(-y2) 30 | constraint(y1, lower_bound=3.16, name="con1") 31 | constraint(y2, upper_bound=24.0) 32 | constraint(y1 + y2) 33 | 34 | # constraint(x, lower_bound=0, upper_bound=10) 35 | # constraint(z, lower_bound=0, upper_bound=10) 36 | 37 | 38 | Sellar.implementation.set_initial( 39 | x=1.0, 40 | z=[ 41 | 5.0, 42 | 2.0, 43 | ], 44 | ) 45 | Sellar._meta.bind_embedded_models = False 46 | sellar_opt = Sellar() 47 | 48 | 49 | toc = time.perf_counter() 50 | print("total time:", toc - tic) 51 | 52 | print(sellar_opt.coupling, sellar_opt.coupling.implicit_output) 53 | -------------------------------------------------------------------------------- /examples/condor_sellar_3.py: -------------------------------------------------------------------------------- 1 | from casadi import exp 2 | 3 | import condor as co 4 | 5 | 6 | class Resid(co.AlgebraicSystem): 7 | x = parameter() 8 | z = parameter(shape=2) 9 | y1 = implicit_output(initializer=1.0) 10 | y2 = implicit_output(initializer=1.0) 11 | 12 | residual.y1 = y1 - z[0] ** 2 - z[1] - x + 0.2 * y2 13 | residual.y2 = y2 - y1**0.5 - z[0] - z[1] 14 | 15 | 16 | class Obj(co.ExplicitSystem): 17 | x = input() 18 | z = input(shape=2) 19 | 20 | y1, y2 = Resid(x, z) 21 | 22 | output.obj = x**2 + z[1] + y1 + exp(-y2) 23 | 24 | 25 | class Constr(co.ExplicitSystem): 26 | x = input() 27 | z = input(shape=2) 28 | 29 | resid = Resid(x, z) 30 | 31 | output.con1 = 3.16 - resid.y1 32 | output.con2 = resid.y2 - 24.0 33 | 34 | 35 | do_bind = True 36 | 37 | 38 | class Sellar(co.OptimizationProblem, bind_submodels=do_bind): 39 | x = variable(lower_bound=0, upper_bound=10) 40 | z = variable(shape=2, lower_bound=0, upper_bound=10) 41 | 42 | objective = Obj(x, z).obj 43 | constrs = Constr(x, z) 44 | constraint(constrs.con1, upper_bound=0.0) 45 | constraint(constrs.con2, upper_bound=0.0) 46 | 47 | 48 | resid_sol = Resid(1, [5.0, 2.0]) 49 | Sellar.implementation.set_initial( 50 | x=1.0, 51 | z=[ 52 | 5.0, 53 | 2.0, 54 | ], 55 | ) 56 | sellar_opt = Sellar() 57 | resid_at_opt = Resid(*sellar_opt) 58 | obj_at_opt = Obj(*sellar_opt) 59 | 60 | if do_bind: 61 | print("should get real numbers") 62 | else: 63 | print("should get symbolic result from model definition") 64 | 65 | print(sellar_opt.constrs.resid.y1, sellar_opt.constrs.resid.y2) 66 | -------------------------------------------------------------------------------- /examples/ct_lqr.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from scipy import linalg 6 | from sgm_test_util import LTI_plot 7 | 8 | import condor as co 9 | 10 | dblintA = np.array([[0, 1], [0, 0]]) 11 | dblintB = np.array([[0, 1]]).T 12 | 13 | DblInt = co.LTI(a=dblintA, b=dblintB, name="DblInt") 14 | # class Terminate(DblInt.Event): 15 | # at_time = 32., 16 | # terminate = True 17 | 18 | 19 | class DblIntLQR(DblInt.TrajectoryAnalysis): 20 | initial[x] = [1.0, 0.1] 21 | Q = np.eye(2) 22 | R = np.eye(1) 23 | # tf = None 24 | tf = 32.0 25 | u = dynamic_output.u 26 | cost = trajectory_output(integrand=(x.T @ Q @ x + u.T @ R @ u) / 2) 27 | 28 | class Options: 29 | state_rtol = 1e-8 30 | adjoint_rtol = 1e-8 31 | pass 32 | 33 | 34 | ct_sim = DblIntLQR([1, 0.1]) 35 | LTI_plot(ct_sim) 36 | # ct_sim = DblIntLQR([0., 0.,]) 37 | # LTI_plot(ct_sim) 38 | 39 | 40 | class CtOptLQR(co.OptimizationProblem): 41 | K = variable(shape=DblIntLQR.K.shape) 42 | objective = DblIntLQR(K).cost 43 | 44 | class Options: 45 | exact_hessian = False 46 | __implementation__ = co.implementations.ScipyCG 47 | 48 | 49 | t_start = perf_counter() 50 | lqr_sol = CtOptLQR() 51 | t_stop = perf_counter() 52 | 53 | S = linalg.solve_continuous_are(dblintA, dblintB, DblIntLQR.Q, DblIntLQR.R) 54 | K = linalg.solve(DblIntLQR.R, dblintB.T @ S) 55 | 56 | lqr_are = DblIntLQR(K) 57 | 58 | print(lqr_sol._stats) 59 | print(lqr_are.cost, lqr_sol.objective) 60 | print(lqr_are.cost > lqr_sol.objective) 61 | print(" ARE sol:", K, "\niterative sol:", lqr_sol.K) 62 | print("time to run:", t_stop - t_start) 63 | 64 | plt.show() 65 | -------------------------------------------------------------------------------- /examples/custom_model_template_with_metaprogramming.py: -------------------------------------------------------------------------------- 1 | import condor as co 2 | 3 | 4 | class CustomMetaprogrammedType(co.ModelType): 5 | @classmethod 6 | def process_placeholders(cls, new_cls, attrs): 7 | print( 8 | f"CustomMetaprogrammedType.processs_placeholders for {new_cls} is a good " 9 | "place for manipulating substitutions" 10 | ) 11 | 12 | @classmethod 13 | def __prepare__(cls, *args, new_kwarg=None, **kwargs): 14 | """ 15 | custom processing behavior use cases: 16 | - condor-flight cross-substitution of vehicle state to environment models 17 | (atmosphere/wind/gravity/etc) 18 | - lincov generation of augmented covariance state, propagation/correction/update 19 | equations, etc. 20 | 21 | 22 | catching custom metaclass arguments use cases: 23 | big gascon: component inputs and sub-assembly declaration (like a primary model) 24 | then singleton submodels for "outputs", but these are not condor built-in 25 | submodels, gascon will define a template for each: 26 | - weight with "subtotal" placheolder 27 | - aero with lift-curve slope, equivalent wetted area, etc, placeholder + 28 | potentially dynamic inputs? 29 | - propulsion with dynamic inputs (maybe base aircraft provides all the core 30 | state plus flight conditions, etc.) and placeholder for thrust, incidence 31 | angle, and state field for adding new state 32 | so primary component model metaclass will have to have a slot for these gascon 33 | output submodels 34 | so the gascon output submodel will at least bind to the primary model template 35 | its when the primary is inserted into a parent assembly that the output 36 | submodels really get processed, and then combined into the parents as needed? 37 | will the assembly user model 38 | 39 | class Component(ComponentTemplate): 40 | input = field() 41 | 42 | class Weight(GasconComponentOutput): 43 | subtotal = placeholder() 44 | 45 | class Aero(GasconComponentOutput): 46 | lift_curve_slope = placeholder() 47 | flat_plate_equivalent_area = placeholder() 48 | 49 | class Propulsion(GasconComponentOutput): 50 | state = field() 51 | thrust_lbf = placeholder() 52 | incidence_angle = placeholder() 53 | 54 | 55 | # this is a bad example since we will use a custom metaclass for lots of the 56 | # propulsion models for the engine decks... 57 | class Turbofan(Component): 58 | fan_diameter_ft = input() 59 | sls_airflow = input() 60 | # require location? 61 | 62 | class Performance(Turbofan): 63 | thrust = ... 64 | 65 | class EngineOutDrag(Turbofan): 66 | drag_lbf = ... 67 | 68 | # I kinda like this syntax, use the name of the "subclass" to figure out which 69 | # template to use? 70 | 71 | 72 | 73 | 74 | 75 | 76 | """ 77 | print(f"CustomMetaprogrammedType.__prepare__ with new_kwarg={new_kwarg}") 78 | return super().__prepare__(*args, **kwargs) 79 | 80 | def __new__(cls, *args, new_kwarg=None, **kwargs): 81 | print(f"CustomMetaprogrammedType.__new__ with new_kwarg={new_kwarg}") 82 | new_cls = super().__new__(cls, *args, **kwargs) 83 | return new_cls 84 | 85 | 86 | class CustomMetaprogrammed(co.ModelTemplate, model_metaclass=CustomMetaprogrammedType): 87 | pass 88 | 89 | 90 | class MyModel0(CustomMetaprogrammed): 91 | pass 92 | 93 | 94 | class MyModel1(CustomMetaprogrammed, new_kwarg="handle a string"): 95 | pass 96 | 97 | 98 | class ModelsCouldAlwaysTakeNonCondorInputs(co.ExplicitSystem): 99 | x = input() 100 | output.y = x**2 101 | 102 | def __init__(self, *args, my_kwarg=None, **kwargs): 103 | print( 104 | "Use for something like ReferenceFrame, it's also possible to modify the " 105 | "kwargs" 106 | ) 107 | # store frame state as condor elements, then can use normal python type logic to 108 | # compose bigger expressions like finding path from arbitrary frames -- but 109 | # would need to do something to make sure that the extra kwargs were saved and 110 | # re-bound as needed... or maybe it's okay if they're not? the final expression 111 | # which will get used with the bound thing will be right? not sure. 112 | 113 | # it's also possible to manipulate the (k)wargs that get passed to super based 114 | # on my_kwarg -- is that a way this could work? 115 | super().__init__(*args, **kwargs) 116 | 117 | 118 | ModelsCouldAlwaysTakeNonCondorInputs(1.2) 119 | -------------------------------------------------------------------------------- /examples/explicit_system.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | import numpy as np 4 | 5 | import condor as co 6 | 7 | rng = np.random.default_rng(123) 8 | 9 | 10 | class instancemethod: # noqa: N801 11 | def __init__(self, func): 12 | print("creating wrapper with func", func, "on", self) 13 | self.func = func 14 | 15 | def __get__(self, obj, cls): 16 | print("returning self", self, " with", self.func, obj, cls) 17 | if obj is None: 18 | return self 19 | else: 20 | return functools.partial(self, obj) 21 | 22 | def __call__(self, *args, **kwargs): 23 | print("calling self", self, " with", self.func, *args, **kwargs) 24 | return self.func(*args, **kwargs) 25 | 26 | 27 | class Class: 28 | def __init__(self, x): 29 | self.x = x 30 | 31 | @instancemethod 32 | def test(self, y): 33 | return self.x + y 34 | 35 | 36 | cls = Class(2.0) 37 | print(cls.test(3.0)) 38 | 39 | 40 | class ComponentRaw(co.models.ModelTemplate): 41 | """Raw Component base""" 42 | 43 | input = co.FreeField(co.Direction.input) 44 | output = co.AssignedField(co.Direction.output) 45 | 46 | x = placeholder(default=2.0) 47 | y = placeholder(default=1.0) 48 | 49 | output.z = x**2 + y 50 | 51 | def hello(self): 52 | print("world", self.z, self.x, self.y, self.input, self.output) 53 | 54 | 55 | class ComponentImplementation(co.implementations.ExplicitSystem): 56 | pass 57 | 58 | 59 | co.implementations.ComponentRaw = ComponentImplementation 60 | 61 | 62 | class ComponentAT(co.ExplicitSystem, as_template=True): 63 | """AT component base""" 64 | 65 | x = placeholder(default=2.0) 66 | y = placeholder(default=1.0) 67 | 68 | output.z = x**2 + y 69 | 70 | def hello(self): 71 | print("world", self.x, self.y, self.z) 72 | 73 | 74 | class MyComponentR(ComponentRaw): 75 | """my component R""" 76 | 77 | u = input() 78 | output.w = z + u 79 | 80 | def hello2(self): 81 | print("world", self.z) 82 | 83 | 84 | class MyComponentA(ComponentAT): 85 | """my component A""" 86 | 87 | u = input() 88 | output.w = z + u 89 | 90 | 91 | assert MyComponentR(u=1.23).z == MyComponentA(u=1.23).z # noqa 92 | 93 | # comp = MyComponentA(u=1., z=5.) 94 | 95 | 96 | class MyComponent1(ComponentRaw): 97 | pass 98 | 99 | 100 | comp1 = MyComponent1() 101 | 102 | 103 | class MyComponent2(ComponentAT): 104 | u = input() 105 | # output.xx = z+u 106 | output.x = u + 2.0 107 | # output.x = z+u # this should produce an error because it's overwriting x but didnt 108 | 109 | 110 | comp2 = MyComponent2(u=1.0) 111 | 112 | 113 | class MatSys(co.ExplicitSystem): 114 | A = input(shape=(3, 4)) 115 | B = input(shape=(4, 2)) 116 | output.C = A @ B 117 | 118 | 119 | ms = MatSys(rng.random(size=(3, 4)), rng.random(size=(4, 2))) 120 | 121 | 122 | class SymMatSys(co.ExplicitSystem): 123 | A = input(shape=(3, 3), symmetric=True) 124 | B = input(shape=(3, 3)) 125 | output.C = A @ B + B.T @ A 126 | 127 | 128 | a = rng.random(size=(3, 3)) 129 | sms = SymMatSys(a + a.T, rng.random(size=(3, 3))) 130 | 131 | 132 | class Sys(co.ExplicitSystem): 133 | x = input() 134 | y = input() 135 | v = y**2 136 | output.w = x**2 + y**2 137 | output.z = x**2 + y 138 | 139 | 140 | sys = Sys(1.2, 3.4) 141 | print(sys, sys.output) 142 | 143 | 144 | class Opt(co.OptimizationProblem): 145 | x = variable() 146 | y = variable() 147 | 148 | sys = Sys(x=x, y=y) 149 | 150 | objective = (sys.w - 1) ** 2 - sys.z 151 | 152 | 153 | Opt.set_initial(x=3.0, y=4.0) 154 | opt = Opt() 155 | -------------------------------------------------------------------------------- /examples/mwe_external.py: -------------------------------------------------------------------------------- 1 | import casadi 2 | import numpy as np 3 | 4 | import condor as co 5 | 6 | try: 7 | import spiceypy as spice 8 | except ImportError: 9 | pass 10 | else: 11 | 12 | def left_quaternion_product_matrix(q): 13 | return np.array( 14 | [ 15 | [-q[1], -q[2], -q[3]], 16 | [q[0], -q[3], q[2]], 17 | [q[3], q[0], -q[1]], 18 | [-q[2], q[1], q[0]], 19 | ] 20 | ).squeeze() 21 | 22 | # class SpiceReferenceFrame(type): 23 | class SpiceReferenceFrame(co.ExternalSolverWrapper, parameterized_IO=False): 24 | def __init__(self, inertial_frame_name, name, start_time_string): 25 | self.input(name="dt") 26 | self.output(name="q", shape=4) 27 | self.output(name="omega", shape=3) 28 | 29 | self.inertial_frame_name = inertial_frame_name 30 | self.name = name 31 | self.start_time_string = start_time_string 32 | self.et_start = spice.str2et(start_time_string) 33 | # self.create_model() 34 | 35 | def function(self, dt): 36 | # need self ref to access things like inertial_frame_name, etc 37 | et = self.et_start + dt 38 | SS = spice.sxform(self.inertial_frame_name, self.name, et) 39 | RR, omega_other = spice.xf2rav(SS) 40 | ang_vel = RR @ omega_other 41 | # quaternion = spice.m2q(RR.T.copy()) 42 | # quaternion = spice.m2q(RR.copy()) 43 | quaternion = spice.m2q(RR) 44 | quaternion[1:] *= -1 45 | return quaternion, ang_vel 46 | 47 | # allow jac_vec_prod, hessian, etc 48 | # okay, I guess a finite_difference_mixin would be easy to add -- 49 | def jacobian(self, t): 50 | q, ang_vel = self.function(t) 51 | qjac = left_quaternion_product_matrix(q) @ ang_vel / 2 52 | return qjac, np.zeros(3) 53 | 54 | spice_reference_frame = SpiceReferenceFrame( 55 | "J2000", "moon_pa", "2026-1-28 6:42:03.51 UTC" 56 | ) 57 | at_0 = spice_reference_frame(0) 58 | print(at_0.input, at_0.output) 59 | 60 | class Solve(co.AlgebraicSystem): 61 | dt = implicit_output(initializer=-50_000) 62 | sys = spice_reference_frame(dt) 63 | residual.qq = casadi.norm_2(at_0.q - casadi.vertcat(*sys.q.squeeze().tolist())) 64 | # residual.qq = casadi.DM(at_0.q) - sys.q 65 | 66 | solve = Solve() 67 | print(solve.dt) 68 | 69 | 70 | data_yy = dict( 71 | sigma=np.array( 72 | [ 73 | [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 74 | [0.2, 0.1833, 0.1621, 0.1429, 0.1256, 0.1101, 0.0966], 75 | [0.4, 0.3600, 0.3186, 0.2801, 0.2454, 0.2147, 0.1879], 76 | [0.6, 0.5319, 0.4654, 0.4053, 0.3526, 0.3070, 0.2681], 77 | [0.8, 0.6896, 0.5900, 0.5063, 0.4368, 0.3791, 0.3309], 78 | [1.0, 0.7857, 0.6575, 0.5613, 0.4850, 0.4228, 0.3712], 79 | ] 80 | ), 81 | sigstr=np.array( 82 | [ 83 | [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], 84 | [0.04, 0.7971, 0.9314, 0.9722, 0.9874, 0.9939, 0.9969], 85 | [0.16, 0.7040, 0.8681, 0.9373, 0.9688, 0.9839, 0.9914], 86 | [0.36, 0.7476, 0.8767, 0.9363, 0.9659, 0.9812, 0.9893], 87 | [0.64, 0.8709, 0.9338, 0.9625, 0.9778, 0.9865, 0.9917], 88 | [1.0, 0.9852, 0.9852, 0.9880, 0.9910, 0.9935, 0.9954], 89 | ] 90 | ), 91 | ) 92 | data_xx = dict( 93 | xbbar=np.linspace(0, 1.0, data_yy["sigma"].shape[0]), 94 | xhbar=np.linspace(0, 0.3, data_yy["sigma"].shape[1]), 95 | ) 96 | Table = co.TableLookup(data_xx, data_yy, 1) 97 | tt = Table(0.5, 0.5) 98 | print(tt.input, tt.output) 99 | 100 | 101 | class MyOpt3(co.OptimizationProblem): 102 | xx = variable(warm_start=False) 103 | yy = variable(warm_start=False) 104 | interp = Table(xx, yy) 105 | objective = (interp.sigma - 0.2) ** 2 + (interp.sigstr - 0.7) ** 2 106 | 107 | class Options: 108 | exact_hessian = False 109 | exact_hessian = True 110 | print_level = 0 111 | 112 | 113 | opt3 = MyOpt3() 114 | print("first call") 115 | print(opt3.implementation.callback._stats["iter_count"]) 116 | 117 | MyOpt3.Options.exact_hessian = False 118 | 119 | opt3 = MyOpt3() 120 | print("call w/o hessian") 121 | print(opt3.implementation.callback._stats["iter_count"]) 122 | 123 | MyOpt3.Options.exact_hessian = True 124 | 125 | opt3 = MyOpt3() 126 | print("with hessian again") 127 | print(opt3.implementation.callback._stats["iter_count"]) 128 | -------------------------------------------------------------------------------- /examples/orbital_1.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | import casadi as ca 4 | import numpy as np 5 | from scipy.io import loadmat 6 | 7 | import condor as co 8 | from condor.backends.casadi.implementations import OptimizationProblem 9 | 10 | I6 = ca.MX.eye(6) 11 | Z6 = ca.MX(6, 6) 12 | W = ca.vertcat(I6, Z6) 13 | 14 | # I3 = np.eye(3) 15 | # Z3 = np.zeros((3,3)) 16 | # V = np.vstack((Z3, I3, np.zeros((6,3)))) 17 | # V = ca.sparsify(V) 18 | 19 | I3 = ca.MX.eye(3) 20 | Z3 = ca.MX(3, 3) 21 | V = ca.vertcat(Z3, I3, ca.MX(6, 3)) 22 | 23 | 24 | numeric_constants = False 25 | 26 | 27 | class LinCovCW(co.ODESystem): 28 | if numeric_constants: 29 | # omega = 0.0011 30 | omega = 0.00114 31 | scal_w = ca.MX.ones(6) 32 | scal_v = ca.MX.ones(3) 33 | else: 34 | omega = parameter() 35 | scal_w = parameter(shape=6) 36 | scal_v = parameter(shape=3) 37 | 38 | initial_x = parameter(shape=6) 39 | initial_C = parameter(shape=(12, 12)) 40 | 41 | Acw = ca.MX(6, 6) 42 | 43 | """[ 44 | 45 | [0, 0, 0, 1, 0, 0], 46 | [0, 0, 0, 0, 1, 0], 47 | [0, 0, 0, 0, 0, 1], 48 | 49 | [0, 0, 0, 0, 0, 2*omega], 50 | [0, -omega**2, 0, 0, 0, 0], 51 | [0, 0, 3*omega**2, -2*omega, 0, 0] 52 | 53 | ] """ 54 | 55 | Acw[0, 3] = 1 56 | Acw[1, 4] = 1 57 | Acw[2, 5] = 1 58 | 59 | Acw[3, 5] = 2 * omega 60 | Acw[4, 1] = -(omega**2) 61 | Acw[5, 2] = 3 * omega**2 62 | Acw[5, 3] = -2 * omega 63 | 64 | x = state(shape=6) 65 | C = state(shape=(12, 12)) 66 | Delta_v_mag = state() 67 | Delta_v_disp = state() 68 | 69 | Scal_w = ca.diag(scal_w) 70 | Cov_prop_offset = W @ Scal_w @ W.T 71 | 72 | Scal_v = ca.diag(scal_v) 73 | Cov_ctrl_offset = V @ Scal_v @ V.T 74 | 75 | Fcal = ca.MX(12, 12) 76 | Fcal[:6, :6] = Acw 77 | Fcal[6:, 6:] = Acw 78 | 79 | initial[x] = initial_x 80 | initial[C] = initial_C 81 | 82 | dot[x] = Acw @ x 83 | dot[C] = Fcal @ C + C @ Fcal.T + Cov_prop_offset 84 | 85 | 86 | sin = ca.sin 87 | cos = ca.cos 88 | 89 | 90 | class MajorBurn(LinCovCW.Event): 91 | rd = parameter(shape=3) # desired position 92 | tig = parameter() # time ignition 93 | tem = parameter() # time end maneuver 94 | 95 | # function = t - tig 96 | at_time = [tig] 97 | 98 | t_d = tem - tig 99 | 100 | stm = ca.MX(6, 6) 101 | stm[0, 0] = 1 102 | stm[0, 2] = 6 * omega * t_d - 6 * sin(omega * t_d) 103 | stm[0, 3] = -3 * t_d + 4 * sin(omega * t_d) / omega 104 | stm[0, 5] = 2 * (1 - cos(omega * t_d)) / omega 105 | stm[1, 1] = cos(omega * t_d) 106 | stm[1, 4] = sin(omega * t_d) / omega 107 | stm[2, 2] = 4 - 3 * cos(omega * t_d) 108 | stm[2, 3] = 2 * (cos(omega * t_d) - 1) / omega 109 | stm[2, 5] = sin(omega * t_d) / omega 110 | stm[3, 2] = 6 * omega * (1 - cos(omega * t_d)) 111 | stm[3, 3] = 4 * cos(omega * t_d) - 3 112 | stm[3, 5] = 2 * sin(omega * t_d) 113 | stm[4, 1] = -omega * sin(omega * t_d) 114 | stm[4, 4] = cos(omega * t_d) 115 | stm[5, 2] = 3 * omega * sin(omega * t_d) 116 | stm[5, 3] = -2 * sin(omega * t_d) 117 | stm[5, 5] = cos(omega * t_d) 118 | T_pp = stm[:3, :3] 119 | T_pv = stm[:3, 3:] 120 | T_pv_inv = ca.solve(T_pv, ca.MX.eye(3)) 121 | 122 | Delta_v = (T_pv_inv @ rd - T_pv_inv @ T_pp @ x[:3, 0]) - x[3:, 0] 123 | 124 | update[Delta_v_mag] = Delta_v_mag + ca.norm_2(Delta_v) 125 | update[x] = x + ca.vertcat(Z3, I3) @ (Delta_v) 126 | 127 | DG = ca.vertcat(ca.MX(3, 6), ca.horzcat(-(T_pv_inv @ T_pp), -I3)) 128 | Dcal = ca.vertcat( 129 | ca.horzcat(I6, DG), 130 | ca.horzcat(Z6, I6 + DG), 131 | ) 132 | 133 | update[C] = Dcal @ C @ Dcal.T + Cov_ctrl_offset 134 | 135 | Mc = DG @ ca.horzcat(Z6, I6) 136 | sigma_Dv__2 = ca.trace(Mc @ C @ Mc.T) 137 | 138 | update[Delta_v_disp] = Delta_v_disp + ca.sqrt(sigma_Dv__2) 139 | 140 | 141 | class Terminate(LinCovCW.Event): 142 | terminate = True 143 | # TODO: how to make a symbol like this just provide the backend repr? or is this 144 | # correct? 145 | # function = t - MajorBurn.tem.backend_repr 146 | at_time = [MajorBurn.tem.backend_repr] 147 | 148 | 149 | class Sim(LinCovCW.TrajectoryAnalysis): 150 | # TODO: add final burn Delta v (assume final relative v is 0, can get magnitude and 151 | # dispersion) 152 | tot_Delta_v_mag = trajectory_output(Delta_v_mag) 153 | tot_Delta_v_disp = trajectory_output(Delta_v_disp) 154 | 155 | # tf = parameter() 156 | 157 | Mr = ca.horzcat(I3, ca.MX(3, 9)) 158 | sigma_r__2 = ca.trace(Mr @ C @ Mr.T) 159 | final_pos_disp = trajectory_output(ca.sqrt(sigma_r__2)) 160 | 161 | class Casadi(co.Options): 162 | # state_rtol = 1E-9 163 | # state_atol = 1E-15 164 | # adjoint_rtol = 1E-9 165 | # adjoint_atol = 1E-15 166 | # state_max_step_size = 30. 167 | 168 | state_adaptive_max_step_size = 4 # 16 169 | adjoint_adaptive_max_step_size = 4 170 | 171 | 172 | Cov_0_matlab = loadmat("P_aug_0.mat")["P_aug_0"][0] 173 | 174 | sim_kwargs = dict( 175 | omega=0.00114, 176 | scal_w=[0.0] * 3 + [4.8e-10] * 3, 177 | scal_v=[2.5e-7] * 3, 178 | initial_x=[ 179 | -2000.0, 180 | 0.0, 181 | 1000.0, 182 | 1.71, 183 | 0.0, 184 | 0.0, 185 | ], 186 | initial_C=Cov_0_matlab, 187 | rd=[500.0, 0.0, 0.0], 188 | ) 189 | 190 | 191 | class Hohmann(co.OptimizationProblem): 192 | tig = variable(initializer=200.0) 193 | tf = variable(initializer=500.0) 194 | constraint(tf - tig, lower_bound=30.0) 195 | constraint(tig, lower_bound=0.1) 196 | sim = Sim(tig=tig, tem=tf, **sim_kwargs) 197 | 198 | objective = sim.tot_Delta_v_mag 199 | 200 | class Casadi(co.Options): 201 | exact_hessian = False 202 | method = OptimizationProblem.Method.scipy_trust_constr 203 | 204 | 205 | class TotalDeltaV(co.OptimizationProblem): 206 | tig = variable(initializer=200.0) 207 | tf = variable(initializer=500.0) 208 | constraint(tf - tig, lower_bound=30.0) 209 | constraint(tig, lower_bound=0.0) 210 | sim = Sim(tig=tig, tem=tf, **sim_kwargs) 211 | 212 | # TODO: adding a parameter and constraint to existing problem SHOULD be done by 213 | # inheritance... I suppose the originally Hohmann model could easily be written to 214 | # include more parameters to solve all permutations of this problem... weights for 215 | # each output, upper bounds for each output (and combinations?) 216 | # what about including a default for a paremter at a model level? no, just make a 217 | # dict like unbounded_kwargs to fill in with a large number/inf 218 | pos_disp_max = parameter() 219 | constraint(sim.final_pos_disp - pos_disp_max, upper_bound=0.0) 220 | 221 | objective = sim.tot_Delta_v_mag + 3 * sim.tot_Delta_v_disp 222 | 223 | class Casadi(co.Options): 224 | exact_hessian = False 225 | method = OptimizationProblem.Method.scipy_trust_constr 226 | 227 | 228 | ############## 229 | 230 | DV_idx = Sim.trajectory_output.flat_index(Sim.tot_Delta_v_mag) 231 | tig_idx = Sim.parameter.flat_index(Sim.tig) 232 | tem_idx = Sim.parameter.flat_index(Sim.tem) 233 | 234 | init_sim = Sim(**sim_kwargs, tig=200.0, tem=500.0) 235 | init_jac = Sim.implementation.callback.jac_callback(Sim.implementation.callback.p, []) 236 | print("init grad wrt tig", init_jac[DV_idx, tig_idx]) 237 | print("init grad wrt tem", init_jac[DV_idx, tem_idx]) 238 | """ 239 | init grad wrt tig 0.0209833 240 | init grad wrt tem -0.0260249 241 | """ 242 | 243 | hoh_start = perf_counter() 244 | hohmann = Hohmann() 245 | hoh_stop = perf_counter() 246 | 247 | hohmann_sim = Sim(**sim_kwargs, tig=hohmann.tig, tem=hohmann.tf) 248 | opt_jac = Sim.implementation.callback.jac_callback(Sim.implementation.callback.p, []) 249 | 250 | 251 | total_delta_v = TotalDeltaV(pos_disp_max=1000) 252 | tot_delta_v_sim = Sim(**sim_kwargs, tig=total_delta_v.tig, tem=total_delta_v.tf) 253 | 254 | 255 | total_delta_v_constrained = TotalDeltaV(pos_disp_max=10.0) 256 | tot_delta_v_constrained_sim = Sim( 257 | **sim_kwargs, tig=total_delta_v_constrained.tig, tem=total_delta_v_constrained.tf 258 | ) 259 | 260 | print("\n" * 2, "hohmann") 261 | print(hohmann._stats) 262 | print((hohmann.tf - hohmann.tig) * hohmann.sim.omega * 180 / np.pi) 263 | print(hohmann_sim.tot_Delta_v_disp) 264 | print(hohmann_sim.final_pos_disp) 265 | print(hohmann.tig, hohmann.tf) 266 | print("time:", hoh_stop - hoh_start) 267 | 268 | print("opt grad wrt tig", opt_jac[DV_idx, tig_idx]) 269 | print("opt grad wrt tem", opt_jac[DV_idx, tem_idx]) 270 | """ 271 | opt grad wrt tig -4.48258e-09 272 | opt grad wrt tem -1.47125e-09 273 | """ 274 | 275 | print("\n" * 2, "unconstrained Delta v") 276 | print(total_delta_v._stats) 277 | print((total_delta_v.tf - total_delta_v.tig) * total_delta_v.sim.omega * 180 / np.pi) 278 | print(tot_delta_v_sim.final_pos_disp) 279 | 280 | print("\n" * 2, "constrained Delta v") 281 | print(total_delta_v_constrained._stats) 282 | print( 283 | (total_delta_v_constrained.tf - total_delta_v_constrained.tig) 284 | * total_delta_v_constrained.sim.omega 285 | * 180 286 | / np.pi 287 | ) 288 | print(tot_delta_v_constrained_sim.final_pos_disp) 289 | -------------------------------------------------------------------------------- /examples/orbital_2.py: -------------------------------------------------------------------------------- 1 | import casadi as ca 2 | from scipy.io import loadmat 3 | 4 | import condor as co 5 | from condor.backends.casadi.implementations import OptimizationProblem 6 | 7 | I6 = ca.MX.eye(6) 8 | Z6 = ca.MX(6, 6) 9 | W = ca.vertcat(I6, Z6) 10 | 11 | # I3 = np.eye(3) 12 | # Z3 = np.zeros((3,3)) 13 | # V = np.vstack((Z3, I3, np.zeros((6,3)))) 14 | # V = ca.sparsify(V) 15 | 16 | I3 = ca.MX.eye(3) 17 | Z3 = ca.MX(3, 3) 18 | V = ca.vertcat(Z3, I3, ca.MX(6, 3)) 19 | 20 | 21 | numeric_constants = False 22 | 23 | 24 | class LinCovCW(co.ODESystem): 25 | if numeric_constants: 26 | omega = 0.0011 27 | scal_w = ca.MX.ones(6) 28 | scal_v = ca.MX.ones(3) 29 | else: 30 | omega = parameter() 31 | scal_w = parameter(shape=6) 32 | scal_v = parameter(shape=3) 33 | 34 | initial_x = parameter(shape=6) 35 | initial_C = parameter(shape=(12, 12)) 36 | 37 | Acw = ca.MX(6, 6) 38 | 39 | """[ 40 | 41 | [0, 0, 0, 1, 0, 0], 42 | [0, 0, 0, 0, 1, 0], 43 | [0, 0, 0, 0, 0, 1], 44 | 45 | [0, 0, 0, 0, 0, 2*omega], 46 | [0, -omega**2, 0, 0, 0, 0], 47 | [0, 0, 3*omega**2, -2*omega, 0, 0] 48 | 49 | ] """ 50 | 51 | Acw[0, 3] = 1 52 | Acw[1, 4] = 1 53 | Acw[2, 5] = 1 54 | 55 | Acw[3, 5] = 2 * omega 56 | Acw[4, 1] = -(omega**2) 57 | Acw[5, 2] = 3 * omega**2 58 | Acw[5, 3] = -2 * omega 59 | 60 | x = state(shape=6) 61 | C = state(shape=(12, 12)) 62 | Delta_v_mag = state() 63 | Delta_v_disp = state() 64 | 65 | Scal_w = ca.diag(scal_w) 66 | Cov_prop_offset = W @ Scal_w @ W.T 67 | 68 | Scal_v = ca.diag(scal_v) 69 | Cov_ctrl_offset = V @ Scal_v @ V.T 70 | 71 | Fcal = ca.MX(12, 12) 72 | Fcal[:6, :6] = Acw 73 | Fcal[6:, 6:] = Acw 74 | 75 | initial[x] = initial_x 76 | initial[C] = initial_C 77 | 78 | dot[x] = Acw @ x 79 | dot[C] = Fcal @ C + C @ Fcal.T + Cov_prop_offset 80 | 81 | 82 | sin = ca.sin 83 | cos = ca.cos 84 | 85 | 86 | def make_burn(rd, tig, tem): 87 | burn_num = 1 + sum( 88 | [event.__name__.startswith("Burn") for event in LinCovCW.Event.subclasses] 89 | ) 90 | burn_name = f"Burn{burn_num:d}" 91 | attrs = co.InnerModelType.__prepare__(burn_name, (LinCovCW.Event,)) 92 | update = attrs["update"] 93 | x = attrs["x"] 94 | C = attrs["C"] 95 | omega = attrs["omega"] 96 | Cov_ctrl_offset = attrs["Cov_ctrl_offset"] 97 | Delta_v_mag = attrs["Delta_v_mag"] 98 | Delta_v_disp = attrs["Delta_v_disp"] 99 | 100 | if not LinCovCW.parameter.get(backend_repr=rd).name: 101 | rdnum = 1 + sum( 102 | [name.startswith("rd_") for name in LinCovCW.parameter.list_of("name")] 103 | ) 104 | attrs[f"rd_{rdnum:d}"] = rd 105 | 106 | if not LinCovCW.parameter.get(backend_repr=tig).name: 107 | tignum = 1 + sum( 108 | [name.startswith("tig_") for name in LinCovCW.parameter.list_of("name")] 109 | ) 110 | attrs[f"tig_{tignum:d}"] = tig 111 | 112 | if not LinCovCW.parameter.get(backend_repr=tem).name: 113 | temnum = 1 + sum( 114 | [name.startswith("tem_") for name in LinCovCW.parameter.list_of("name")] 115 | ) 116 | attrs[f"tem_{temnum:d}"] = tem 117 | 118 | # attrs["function"] = t - tig 119 | attrs["at_time"] = [tig] 120 | 121 | t_d = tem - tig 122 | stm = ca.MX(6, 6) 123 | stm[0, 0] = 1 124 | stm[0, 2] = 6 * omega * t_d - 6 * sin(omega * t_d) 125 | stm[0, 3] = -3 * t_d + 4 * sin(omega * t_d) / omega 126 | stm[0, 5] = 2 * (1 - cos(omega * t_d)) / omega 127 | stm[1, 1] = cos(omega * t_d) 128 | stm[1, 4] = sin(omega * t_d) / omega 129 | stm[2, 2] = 4 - 3 * cos(omega * t_d) 130 | stm[2, 3] = 2 * (cos(omega * t_d) - 1) / omega 131 | stm[2, 5] = sin(omega * t_d) / omega 132 | stm[3, 2] = 6 * omega * (1 - cos(omega * t_d)) 133 | stm[3, 3] = 4 * cos(omega * t_d) - 3 134 | stm[3, 5] = 2 * sin(omega * t_d) 135 | stm[4, 1] = -omega * sin(omega * t_d) 136 | stm[4, 4] = cos(omega * t_d) 137 | stm[5, 2] = 3 * omega * sin(omega * t_d) 138 | stm[5, 3] = -2 * sin(omega * t_d) 139 | stm[5, 5] = cos(omega * t_d) 140 | T_pp = stm[:3, :3] 141 | T_pv = stm[:3, 3:] 142 | T_pv_inv = ca.solve(T_pv, ca.MX.eye(3)) 143 | 144 | Delta_v = (T_pv_inv @ rd - T_pv_inv @ T_pp @ x[:3, 0]) - x[3:, 0] 145 | update[Delta_v_mag] = Delta_v_mag + ca.norm_2(Delta_v) 146 | update[x] = x + ca.vertcat(Z3, I3) @ (Delta_v) 147 | 148 | DG = ca.vertcat(ca.MX(3, 6), ca.horzcat(-(T_pv_inv @ T_pp), -I3)) 149 | Dcal = ca.vertcat( 150 | ca.horzcat(I6, DG), 151 | ca.horzcat(Z6, I6 + DG), 152 | ) 153 | 154 | update[C] = Dcal @ C @ Dcal.T + Cov_ctrl_offset 155 | 156 | Mc = DG @ ca.horzcat(Z6, I6) 157 | sigma_Dv__2 = ca.trace(Mc @ C @ Mc.T) 158 | 159 | update[Delta_v_disp] = Delta_v_disp + ca.sqrt(sigma_Dv__2) 160 | Burn = co.InnerModelType(burn_name, (LinCovCW.Event,), attrs=attrs) 161 | 162 | Burn.rd = rd 163 | Burn.tig = tig 164 | Burn.tem = tem 165 | 166 | Burn.DG = DG 167 | Burn.Dcal = Dcal 168 | Burn.sigma_Dv__2 = sigma_Dv__2 169 | Burn.Mc = Mc 170 | return Burn 171 | 172 | 173 | MajorBurn = make_burn( 174 | rd=LinCovCW.parameter(shape=3), # desired position 175 | tig=LinCovCW.parameter(), # time ignition 176 | tem=LinCovCW.parameter(), # time end maneuver 177 | ) 178 | 179 | 180 | MinorBurn = make_burn( 181 | rd=MajorBurn.rd, # desired position 182 | tig=LinCovCW.parameter(), # time ignition 183 | tem=MajorBurn.tem, # time end maneuver 184 | ) 185 | 186 | 187 | class Terminate(LinCovCW.Event): 188 | terminate = True 189 | # TODO: how to make a symbol like this just provide the backend repr? or is this 190 | # correct? 191 | # function = t - MajorBurn.tem 192 | at_time = [MajorBurn.tem] 193 | 194 | 195 | class Sim(LinCovCW.TrajectoryAnalysis): 196 | # TODO: add final burn Delta v (assume final relative v is 0, can get magnitude and 197 | # dispersion) 198 | tot_Delta_v_mag = trajectory_output(Delta_v_mag) 199 | tot_Delta_v_disp = trajectory_output(Delta_v_disp) 200 | 201 | # tf = parameter() 202 | 203 | Mr = ca.horzcat(I3, ca.MX(3, 9)) 204 | sigma_r__2 = ca.trace(Mr @ C @ Mr.T) 205 | final_pos_disp = trajectory_output(ca.sqrt(sigma_r__2)) 206 | 207 | # class Casadi(co.Options): 208 | 209 | 210 | Cov_0_matlab = loadmat("P_aug_0.mat")["P_aug_0"][0] 211 | 212 | sim_kwargs = dict( 213 | omega=0.00114, 214 | scal_w=[0.0] * 3 + [4.8e-10] * 3, 215 | scal_v=[2.5e-7] * 3, 216 | initial_x=[ 217 | -2000.0, 218 | 0.0, 219 | 1000.0, 220 | 1.71, 221 | 0.0, 222 | 0.0, 223 | ], 224 | initial_C=Cov_0_matlab, 225 | rd_1=[500.0, 0.0, 0.0], 226 | ) 227 | 228 | 229 | class Geller2006(co.OptimizationProblem): 230 | t1 = 10.0 231 | tf = 1210.0 232 | t2 = variable(initializer=700.0) 233 | sigma_r_weight = parameter() 234 | sigma_Dv_weight = parameter() 235 | 236 | constraint(t2, lower_bound=t1 + 600.0, upper_bound=tf - 120.0) 237 | 238 | sim = Sim(tig_1=t1, tig_2=t2, tem_1=tf, **sim_kwargs) 239 | objective = ( 240 | sigma_Dv_weight * sim.tot_Delta_v_disp + sigma_r_weight * sim.final_pos_disp 241 | ) 242 | 243 | class Casadi(co.Options): 244 | exact_hessian = False 245 | method = OptimizationProblem.Method.scipy_trust_constr 246 | 247 | 248 | ############## 249 | 250 | r = Geller2006(sigma_r_weight=1.0, sigma_Dv_weight=0.0) 251 | v = Geller2006(sigma_r_weight=0.0, sigma_Dv_weight=500.0) 252 | 253 | 254 | sim = Sim(tig_1=Geller2006.t1, tig_2=700.0, tem_1=Geller2006.tf, **sim_kwargs) 255 | # jac = sim.implementation.callback.jac_callback(sim.implementation.callback.p, []) 256 | print("\n" * 3, "minimize position dispersions:") 257 | print(r._stats) 258 | 259 | print("\n" * 3, "minimize Delta-v dispersions:") 260 | print(v._stats) 261 | -------------------------------------------------------------------------------- /examples/orbital_debugging.py: -------------------------------------------------------------------------------- 1 | import casadi as ca 2 | import matplotlib.pyplot as plt 3 | import numpy as np 4 | from LinCovCW import ( 5 | I3, 6 | I6, 7 | Z3, 8 | Z6, 9 | LinCovCW, 10 | deriv_check_plots, 11 | make_burn, 12 | make_sim, 13 | sim_kwargs, 14 | ) 15 | 16 | import condor as co 17 | from condor.backends.casadi.implementations import OptimizationProblem 18 | 19 | MajorBurn = make_burn( 20 | rd=LinCovCW.parameter(shape=3), # desired position 21 | tig=LinCovCW.parameter(), # time ignition 22 | tem=LinCovCW.parameter(), # time end maneuver 23 | ) 24 | 25 | 26 | class Terminate(LinCovCW.Event): 27 | terminate = True 28 | # TODO: how to make a symbol like this just provide the backend repr? or is this 29 | # correct? 30 | # function = t - MajorBurn.tem 31 | at_time = [ 32 | MajorBurn.tem, 33 | ] 34 | 35 | 36 | class Measurements(LinCovCW.Event): 37 | rcal = parameter(shape=3) 38 | rcalhat = parameter(shape=3) 39 | 40 | Rcal = ca.diag(rcal) 41 | Rcalhat = ca.diag(rcalhat) 42 | 43 | Hcal = ca.horzcat(I3, Z3) 44 | Hcalhat = ca.horzcat(I3, Z3) 45 | 46 | Khat = (P @ Hcalhat.T) @ ca.solve(Hcalhat @ P @ Hcalhat.T + Rcalhat, ca.MX.eye(3)) 47 | Acalhat = I6 - Khat @ Hcalhat 48 | update[P] = Acalhat @ P @ Acalhat.T + Khat @ Rcalhat @ Khat.T 49 | 50 | M01 = Khat @ Hcal 51 | M = ca.vertcat(ca.horzcat(I6, Z6), ca.horzcat(M01, Acalhat)) 52 | N = ca.vertcat(Z3, Z3, Khat) 53 | update[C] = M @ C @ M.T + N @ Rcal @ N.T 54 | 55 | # update[Delta_v_disp] = Delta_v_disp# + ca.sqrt(sigma_Dv__2) 56 | # update[Delta_v_mag] = Delta_v_mag# + ca.sqrt(sigma_Dv__2) 57 | # update[x] = x 58 | 59 | meas_dt = parameter() 60 | meas_t_offset = parameter() 61 | 62 | # function = ca.sin(np.pi*(t-meas_t_offset)/meas_dt) 63 | # this re-normalizes the derivative to be ~1 at the 0-crossings which may or may not 64 | # be helpful 65 | # function = meas_dt*ca.sin(np.pi*(t-meas_t_offset)/meas_dt)/np.pi 66 | # function = t - meas_t_offset 67 | # at_time = [meas_t_offset, None, meas_dt] 68 | at_time = [ 69 | meas_t_offset, 70 | ] 71 | 72 | 73 | sim_kwargs.update( 74 | dict( 75 | meas_dt=2300.0, 76 | meas_t_offset=851.0, 77 | # meas_dt = 100., 78 | # meas_t_offset = 51., 79 | ) 80 | ) 81 | 82 | # 1-burn sim 83 | # class Sim(LinCovCW.TrajectoryAnalysis): 84 | # # TODO: add final burn Delta v (assume final relative v is 0, can get magnitude and 85 | # # dispersion) 86 | # tot_Delta_v_mag = trajectory_output( 87 | # sum([burn.Delta_v_mag for burn in make_burn.burns]) 88 | # ) 89 | # #tot_Delta_v_disp = trajectory_output(Delta_v_disp) 90 | # tot_Delta_v_disp = trajectory_output( 91 | # sum([burn.Delta_v_disp for burn in make_burn.burns]) 92 | # ) 93 | # 94 | # Mr = ca.horzcat(I3, ca.MX(3,9)) 95 | # sigma_r__2 = ca.trace(Mr @ C @ Mr.T) 96 | # final_pos_disp = trajectory_output(ca.sqrt(sigma_r__2)) 97 | # 98 | # class Casadi(co.Options): 99 | # integrator_options = dict( 100 | # max_step = 1., 101 | # #atol = 1E-15, 102 | # #rtol = 1E-12, 103 | # nsteps = 10000, 104 | # ) 105 | # Sim.implementation.callback.get_jacobian( 106 | # f"jac_{Sim.implementation.callback.name}", None, None, {} 107 | # ) 108 | 109 | 110 | Sim = make_sim() 111 | output_vars = Sim.Delta_v_mag_1, Sim.Delta_v_disp_1, Sim.final_pos_disp 112 | sim_kwargs.update( 113 | dict( 114 | tem_1=3800.0, 115 | # meas_dt = 200., 116 | # meas_t_offset = 7.5, 117 | # meas_t_offset = 851., 118 | ) 119 | ) 120 | 121 | 122 | sim_kwargs.pop("meas_t_offset", None) 123 | meas_times = np.arange(300, 1100, 5.0) 124 | meas_times = np.arange(300, 900, 5.0) 125 | meas_times = np.arange(0.0, 150.0, 20.0) 126 | meas_times = np.r_[1:151:20, 153:203:20] 127 | meas_sims = [ 128 | ( 129 | Sim(tig_1=152.0, meas_t_offset=meas_time, **sim_kwargs), 130 | Sim.implementation.callback.jac_callback(Sim.implementation.callback.p, []), 131 | ) 132 | for meas_time in meas_times 133 | ] 134 | indep_var = Sim.meas_t_offset 135 | xx, yy = deriv_check_plots(Sim.meas_t_offset, output_vars, meas_sims, title_prefix="mt") 136 | 137 | 138 | # plt.show() 139 | # import sys 140 | # sys.exit() 141 | 142 | tigs = np.arange(600, 1000.0, 50) 143 | # tigs = np.arange(10, 3000., 5) 144 | tig_sims = [ 145 | ( 146 | Sim(tig_1=tig, meas_t_offset=851.0, **sim_kwargs), 147 | Sim.implementation.callback.jac_callback(Sim.implementation.callback.p, []), 148 | ) 149 | for tig in tigs 150 | ] 151 | indep_var = Sim.tig_1 152 | 153 | 154 | deriv_check_plots(Sim.tig_1, output_vars, tig_sims, title_prefix="tig") 155 | 156 | # plt.show() 157 | # import sys 158 | # sys.exit() 159 | 160 | 161 | # 2-burn sim 162 | sim_kwargs.update( 163 | dict( 164 | tem_1=1210.0, 165 | tig_1=10.0, 166 | ) 167 | ) 168 | MinorBurn = make_burn( 169 | rd=MajorBurn.rd, # desired position 170 | tig=LinCovCW.parameter(), # time ignition 171 | tem=MajorBurn.tem, # time end maneuver 172 | ) 173 | 174 | # class Sim2(LinCovCW.TrajectoryAnalysis): 175 | # # TODO: add final burn Delta v (assume final relative v is 0, can get magnitude and 176 | # # dispersion) 177 | # tot_Delta_v_mag = trajectory_output(Delta_v_mag) 178 | # tot_Delta_v_disp = trajectory_output(Delta_v_disp) 179 | # 180 | # Mr = ca.horzcat(I3, ca.MX(3,9)) 181 | # sigma_r__2 = ca.trace(Mr @ C @ Mr.T) 182 | # final_pos_disp = trajectory_output(ca.sqrt(sigma_r__2)) 183 | 184 | # Sim2.implementation.callback.get_jacobian( 185 | # f"jac_{Sim2.implementation.callback.name}", None, None, {} 186 | # ) 187 | Sim2 = make_sim() 188 | 189 | tigs = np.arange(600, 1000.0, 50) 190 | sims = [ 191 | ( 192 | Sim2(tig_2=tig, meas_t_offset=1500.0, **sim_kwargs), 193 | Sim2.implementation.callback.jac_callback(Sim2.implementation.callback.p, []), 194 | ) 195 | for tig in tigs 196 | ] 197 | sims2 = sims 198 | deriv_check_plots(Sim2.tig_2, output_vars, sims2, title_prefix="mcc tig") 199 | """ 200 | turning debug_level to 0 for shooting_gradient_method moves from 200s to 190s. 201 | 202 | """ 203 | plt.show() 204 | 205 | import sys # noqa 206 | 207 | sys.exit() 208 | 209 | 210 | # was attempting to optimize measurement time given fixed burn schedule: converse of 211 | # orbital_3 212 | class Meas1(co.OptimizationProblem): 213 | t1 = variable(initializer=100.0) 214 | sigma_r_weight = parameter() 215 | sigma_Dv_weight = parameter() 216 | mag_Dv_weight = parameter() 217 | sim = Sim(**sim_kwargs, meas_t_offset=t1, tig_1=900.0) 218 | 219 | constraint(t1, lower_bound=30.0, upper_bound=sim_kwargs["tem_1"] - 30.0) 220 | objective = ( 221 | sigma_Dv_weight * sim.tot_Delta_v_disp 222 | + sigma_r_weight * sim.final_pos_disp 223 | + mag_Dv_weight * sim.tot_Delta_v_mag 224 | ) 225 | 226 | class Casadi(co.Options): 227 | exact_hessian = False 228 | method = OptimizationProblem.Method.scipy_trust_constr 229 | 230 | 231 | # opt = Meas1(sigma_Dv_weight=0, mag_Dv_weight=1, sigma_r_weight=1) 232 | # sim = Sim(**sim_kwargs, meas_t_offset=100.) 233 | 234 | fig, axes = plt.subplots(3, constrained_layout=True, sharex=True) 235 | for ax, ordinate in zip(axes, ordinates): 236 | ax.plot(tigs, ordinate) 237 | ax.grid(True) 238 | 239 | 240 | Dv_mags = [sim.tot_Delta_v_mag for sim in sims] 241 | Dv_disps = [sim.tot_Delta_v_disp for sim in sims] 242 | pos_disps = [sim.final_pos_disp for sim in sims] 243 | ordinates = [Dv_mags, Dv_disps, pos_disps] 244 | 245 | fig, axes = plt.subplots(3, constrained_layout=True, sharex=True) 246 | for ax, ordinate in zip(axes, ordinates): 247 | ax.plot(tigs, ordinate) 248 | ax.grid(True) 249 | 250 | 251 | plt.show() 252 | 253 | 254 | # sim1 = Sim( 255 | # tem_1=opt.tf, 256 | # tig_1=850, 257 | # **sim_kwargs 258 | # ) 259 | # 260 | # 261 | # sim2 = Sim( 262 | # tem_1=opt.tf, 263 | # tig_1=852, 264 | # **sim_kwargs 265 | # ) 266 | -------------------------------------------------------------------------------- /examples/rosenbrock.py: -------------------------------------------------------------------------------- 1 | import condor 2 | 3 | 4 | def rosenbrock(x, y, a=1, b=100): 5 | return (a - x) ** 2 + b * (y - x**2) ** 2 6 | 7 | 8 | call_from_count = [] 9 | param_count = [] 10 | 11 | # these two callback data arrays end up being 7 elements long, 12 | # 3 calls as a top level (1 with warm start off, 2 with warm start on) 13 | # then Outer gets called twice (once with warm start off, once off) 14 | # each outer results in two elements to the callback data arrays, 15 | # 1 is used by outer to solve the embedded problem (initiated with a variable, but then 16 | # called at least once for each iter), which only goes through casadi infrastructure as 17 | # Callback, and doesn't hit implementation after binding the variable 18 | # 1 is used by the bind_embedded_models with the final solution, which is essentially 19 | # "top level" and goes through implementation 20 | # currently not able to link the final solution of the embedded to the implementation -- 21 | # might be able to fix that? 22 | # definitely have access to the embedded solution, in bind_embedded_model, the 23 | # embedded_model.implementation.callback definitely has the correct last_x. e.g., line 24 | # 1333 of models.py 25 | # need an API for using the callback effectively -- probably applies to SGM as well. 26 | # maybe if the implementation and model API was more defined, with more hooks for the 27 | # call process. 28 | 29 | # ok, figured out a decent API I think. iter_count for the two bind_embedded_model calls 30 | # is 0. can use call_from_count [-4], [-2] to show warm start worked? 31 | 32 | 33 | class RosenbrockOnCircle(condor.OptimizationProblem): 34 | r = parameter() 35 | x = variable(warm_start=False) 36 | y = variable(warm_start=False) 37 | 38 | objective = rosenbrock(x, y) 39 | 40 | constraint(x**2 + y**2 == r**2) 41 | 42 | class Options: 43 | print_level = 0 44 | 45 | @staticmethod 46 | def init_callback(parameter, opts): 47 | print(" inner init:", parameter) 48 | call_from_count.append(0) 49 | param_count.append(parameter) 50 | 51 | @staticmethod 52 | def iter_callback(i, variable, objective, constraint): 53 | print(" inner: ", i, variable, objective) 54 | call_from_count[-1] += 1 55 | 56 | 57 | print("=== Call twice, should see same iters") 58 | out1 = RosenbrockOnCircle(r=2) # **0.5) 59 | print("---") 60 | 61 | RosenbrockOnCircle.x.warm_start = True 62 | RosenbrockOnCircle.y.warm_start = True 63 | 64 | out2 = RosenbrockOnCircle(r=2) # **0.5) 65 | 66 | print(3 * "\n") 67 | 68 | print("=== From warm start") 69 | out3 = RosenbrockOnCircle(r=2) # **0.5) 70 | 71 | print(3 * "\n") 72 | 73 | # print("=== Set warm start, should see fewer iters on second call") 74 | # RosenbrockOnCircle.x.warm_start = True 75 | # RosenbrockOnCircle.y.warm_start = True 76 | # RosenbrockOnCircle(r=2) 77 | # print("---") 78 | # RosenbrockOnCircle(r=2) 79 | # 80 | # print(3*"\n") 81 | 82 | 83 | for use_warm_start in [False, True]: 84 | print("=== with warm_start =", use_warm_start) 85 | RosenbrockOnCircle.x.warm_start = use_warm_start 86 | RosenbrockOnCircle.y.warm_start = use_warm_start 87 | 88 | print("=== Embed within optimization over disk radius") 89 | 90 | class Outer(condor.OptimizationProblem): 91 | # r = variable(initializer=2+(5/16)+(1/64)) 92 | r = variable(initializer=1.5, warm_start=False) 93 | 94 | out = RosenbrockOnCircle(r=r) 95 | 96 | objective = rosenbrock(out.x, out.y) 97 | 98 | class Options: 99 | print_level = 0 100 | exact_hessian = False 101 | # with exact_hessian = False means more outer iters and also a larger 102 | # percentage of calls correctly going through #the warm start -- I assume 103 | # the ones where it is re-starting is because of the jacobian?, 104 | # produces about a 16 iter difference 105 | 106 | @staticmethod 107 | def init_callback(parameter, opts): 108 | print("outer init:", parameter) 109 | 110 | @staticmethod 111 | def iter_callback(i, variable, objective, constraint): 112 | print("outer: ", i, variable, objective) 113 | 114 | out = Outer() 115 | print(out.r) 116 | # break 117 | 118 | 119 | print(call_from_count) 120 | print(param_count) 121 | -------------------------------------------------------------------------------- /examples/sellar.py: -------------------------------------------------------------------------------- 1 | import condor as co 2 | from condor.backend import operators as ops 3 | 4 | warm_start = False 5 | 6 | 7 | class Coupling(co.AlgebraicSystem): 8 | x = parameter(shape=3) 9 | y1 = variable(initializer=1.0, warm_start=warm_start) 10 | y2 = variable(initializer=1.0, warm_start=warm_start) 11 | 12 | residual(y1 == x[0] ** 2 + x[1] + x[2] - 0.2 * y2) 13 | residual(y2 == y1**0.5 + x[0] + x[1]) 14 | 15 | 16 | coupling = Coupling([5.0, 2.0, 1]) # evaluate the model numerically 17 | print(coupling.y1, coupling.y2) # individual elements are bound numerically 18 | print(coupling.variable) # fields are bound as a dataclass 19 | 20 | 21 | class Sellar(co.OptimizationProblem): 22 | x = variable(shape=3, lower_bound=0, upper_bound=10, warm_start=False) 23 | coupling = Coupling(x) 24 | y1, y2 = coupling 25 | 26 | objective = x[2] ** 2 + x[1] + y1 + ops.exp(-y2) 27 | constraint(y1 >= 3.16) 28 | constraint(y2 <= 24.0) 29 | 30 | class Options: 31 | __implementation__ = co.implementations.OptimizationProblem 32 | print_level = 0 33 | 34 | @staticmethod 35 | def iter_callback(idx, variable, objective, constraints, instance=None): 36 | print() 37 | # print(f"iter {idx}: {variable}") 38 | # for k, v in constraints.asdict().items(): 39 | # elem = Sellar.constraint.get(name=k) 40 | # # print(" "*4, f"{k}: {elem.lower_bound} < {v} < {elem.upper_bound}") 41 | # # if instance is not None: 42 | # # print(" ", f"{instance.coupling.variable}") 43 | 44 | 45 | Sellar.set_initial(x=[5, 2, 1]) 46 | for _ in range(2): 47 | sellar = Sellar() 48 | print() 49 | print("objective value:", sellar.objective) # scalar value 50 | print(sellar.constraint) # field 51 | print(sellar.coupling.y1) # embedded-model element 52 | print( 53 | "embedded solver iter count:", 54 | Sellar.coupling.implementation.callback.total_iters, 55 | ) 56 | self = Sellar.coupling.implementation.callback 57 | self.update_warmstart(self.warm_start + True) 58 | Sellar.coupling.implementation.callback.total_iters = 0 59 | 60 | 61 | Sellar.Options.__implementation__ = co.implementations.OptimizationProblem 62 | Sellar.set_initial(x=[5, 2, 1]) 63 | # ipopt_s = Sellar() 64 | -------------------------------------------------------------------------------- /examples/sgm_test_util.py: -------------------------------------------------------------------------------- 1 | from dataclasses import asdict 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | 6 | def LTI_plot(sim, t_slice=None): 7 | if t_slice is None: 8 | t_slice = slice(None, None) 9 | field = sim.state 10 | # for field in [sim.state, sim.output]: 11 | for sym_name, symbol in asdict(field).items(): 12 | n = symbol.shape if symbol.ndim > 1 else 1 13 | fig, axes = plt.subplots(n, 1, constrained_layout=True, sharex=True) 14 | plt.suptitle(f"{sim.__class__.__name__} {field.__class__.__name__}.{sym_name}") 15 | if n > 1: 16 | for ax, x in zip(axes, symbol): 17 | ax.plot(sim.t[t_slice], x.squeeze()[t_slice]) 18 | ax.grid(True) 19 | else: 20 | plt.plot(sim.t[t_slice], symbol.squeeze()[t_slice]) 21 | plt.grid(True) 22 | -------------------------------------------------------------------------------- /examples/sp_lqr.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from scipy import linalg, signal 6 | from sgm_test_util import LTI_plot 7 | 8 | import condor as co 9 | 10 | dblintA = np.array([[0, 1], [0, 0]]) 11 | dblintB = np.array([[0, 1]]).T 12 | dt = 0.5 13 | 14 | 15 | DblIntSampled = co.LTI(a=dblintA, b=dblintB, name="DblIntSampled", dt=dt) 16 | 17 | 18 | class DblIntSampledLQR(DblIntSampled.TrajectoryAnalysis): 19 | initial[x] = [1.0, 0.1] 20 | # initial[u] = -K@initial[x] 21 | Q = np.eye(2) 22 | R = np.eye(1) 23 | tf = 32.0 # 12 iters, 21 calls 1E-8 jac 24 | # tf = 16. # 9 iters, 20 calls, 1E-7 25 | cost = trajectory_output(integrand=(x.T @ Q @ x + u.T @ R @ u) / 2) 26 | 27 | class Casadi(co.Options): 28 | adjoint_adaptive_max_step_size = False 29 | state_max_step_size = 0.5 / 8 30 | adjoint_max_step_size = 0.5 / 8 31 | 32 | 33 | class SampledOptLQR(co.OptimizationProblem): 34 | K = variable(shape=DblIntSampledLQR.K.shape) 35 | objective = DblIntSampledLQR(K).cost 36 | 37 | class Casadi(co.Options): 38 | exact_hessian = False 39 | # method = OptimizationProblem.Method.scipy_cg 40 | # method = OptimizationProblem.Method.scipy_trust_constr 41 | 42 | 43 | sim = DblIntSampledLQR([1.00842737, 0.05634044]) 44 | 45 | sim = DblIntSampledLQR([0.0, 0.0]) 46 | sim.implementation.callback.jac_callback(sim.implementation.callback.p, []) 47 | 48 | 49 | t_start = perf_counter() 50 | lqr_sol_samp = SampledOptLQR() 51 | t_stop = perf_counter() 52 | 53 | 54 | # sampled_sim = DblIntSampledLQR([0., 0.]) 55 | # sampled_sim.implementation.callback.jac_callback([0., 0.,], [0.]) 56 | 57 | Q = DblIntSampledLQR.Q 58 | R = DblIntSampledLQR.R 59 | A = dblintA 60 | B = dblintB 61 | 62 | Ad, Bd = signal.cont2discrete((A, B, None, None), dt)[:2] 63 | S = linalg.solve_discrete_are( 64 | Ad, 65 | Bd, 66 | Q, 67 | R, 68 | ) 69 | K = linalg.solve(Bd.T @ S @ Bd + R, Bd.T @ S @ Ad) 70 | 71 | # sim = DblIntSampledLQR([1.00842737, 0.05634044]) 72 | sim = DblIntSampledLQR(K) 73 | 74 | jac = sim.implementation.callback.jac_callback(sim.implementation.callback.p, []) 75 | LTI_plot(sim) 76 | plt.show() 77 | 78 | # sim = DblIntSampledLQR([0., 0.]) 79 | 80 | 81 | # sampled_sim = DblIntSampledLQR([0., 0.]) 82 | # sampled_sim.implementation.callback.jac_callback([0., 0.,], [0.]) 83 | 84 | 85 | sampled_sim = DblIntSampledLQR(K) 86 | jac_cb = sampled_sim.implementation.callback.jac_callback 87 | jac_cb(K, [0.0]) 88 | 89 | print(lqr_sol_samp._stats) 90 | print(lqr_sol_samp.objective < sampled_sim.cost) 91 | print(lqr_sol_samp.objective, sampled_sim.cost) 92 | print(" ARE sol:", K, "\niterative sol:", lqr_sol_samp.K) 93 | print("time to run:", t_stop - t_start) 94 | -------------------------------------------------------------------------------- /examples/state_switched.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from sgm_test_util import LTI_plot 6 | 7 | import condor as co 8 | 9 | 10 | class DblInt(co.ODESystem): 11 | A = np.array( 12 | [ 13 | [0, 1], 14 | [0, 0], 15 | ] 16 | ) 17 | B = np.array([[0, 1]]).T 18 | x = state(shape=A.shape[0]) 19 | mode = state() 20 | p1 = parameter() 21 | p2 = parameter() 22 | u = modal() 23 | dot[x] = A @ x + B * u 24 | 25 | 26 | class Accel(DblInt.Mode): 27 | condition = mode == 0.0 28 | action[u] = 1.0 29 | 30 | 31 | class Switch1(DblInt.Event): 32 | function = x[0] - p1 33 | update[mode] = 1.0 34 | 35 | 36 | class Decel(DblInt.Mode): 37 | condition = mode == 1.0 38 | action[u] = -1.0 39 | 40 | 41 | class Switch2(DblInt.Event): 42 | function = x[0] - p2 43 | # update[mode] = 2. 44 | terminate = True 45 | 46 | 47 | class Transfer(DblInt.TrajectoryAnalysis): 48 | initial[x] = [-9.0, 0.0] 49 | xd = [1.0, 2.0] 50 | Q = np.eye(2) 51 | cost = trajectory_output(((x - xd).T @ (x - xd)) / 2) 52 | tf = 20.0 53 | 54 | class Options: 55 | state_max_step_size = 0.25 56 | state_atol = 1e-15 57 | state_rtol = 1e-12 58 | adjoint_atol = 1e-15 59 | adjoint_rtol = 1e-12 60 | # state_solver = co.backend.implementations.TrajectoryAnalysis.Solver.CVODE 61 | # adjoint_solver = co.backend.implementations.TrajectoryAnalysis.Solver.CVODE 62 | 63 | 64 | p0 = -4.0, -1.0 65 | sim = Transfer(*p0) 66 | # sim.implementation.callback.jac_callback(sim.implementation.callback.p, []) 67 | 68 | 69 | class MinimumTime(co.OptimizationProblem): 70 | p1 = variable() 71 | p2 = variable() 72 | sim = Transfer(p1, p2) 73 | objective = sim.cost 74 | 75 | class Options: 76 | exact_hessian = False 77 | __implementation__ = co.implementations.ScipyCG 78 | 79 | 80 | MinimumTime.set_initial(p1=p0[0], p2=p0[1]) 81 | 82 | 83 | t_start = perf_counter() 84 | opt = MinimumTime() 85 | t_stop = perf_counter() 86 | print("time to run:", t_stop - t_start) 87 | print(opt.p1, opt.p2) 88 | print(opt._stats) 89 | 90 | LTI_plot(opt.sim) 91 | plt.show() 92 | -------------------------------------------------------------------------------- /examples/test_sgm.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | 6 | import condor as co 7 | 8 | dblintA = np.array( 9 | [ 10 | [0, 1], 11 | [0, 0], 12 | ] 13 | ) 14 | dblintB = np.array([[0, 1]]).T 15 | 16 | DblInt = co.LTI(a=dblintA, b=dblintB, name="DblInt") 17 | 18 | 19 | class DblIntLQR(DblInt.TrajectoryAnalysis): 20 | initial[x] = [1.0, 0.1] 21 | Q = np.eye(2) 22 | R = np.eye(1) 23 | tf = 32.0 24 | cost = trajectory_output(integrand=(x.T @ Q @ x + (K @ x).T @ R @ (K @ x)) / 2) 25 | # cost = trajectory_output(integrand= x.T@Q@x + u.T @ R @ u) 26 | 27 | class Casadi(co.Options): 28 | nsteps = 5000 29 | atol = 1e-15 30 | # max_step = 1. 31 | 32 | 33 | # ct_sim = DblIntLQR([1, .1]) 34 | # LTI_plot(ct_sim) 35 | 36 | ct_sim = DblIntLQR( 37 | [ 38 | 0.0, 39 | 0.0, 40 | ] 41 | ) 42 | LTI_plot(ct_sim) 43 | # plt.show() 44 | 45 | 46 | sys.exit() 47 | 48 | sampled_sim = DblIntSampledLQR([0.7, 0.2]) 49 | LTI_plot(sampled_sim) 50 | plt.show() 51 | 52 | lqr_sol_samp = SampledOptLQR() 53 | 54 | # ------- 55 | 56 | DblIntSampled = co.LTI(a=dblintA, b=dblintB, name="DblIntSampled", dt=5.0) 57 | 58 | 59 | class DblIntSampledLQR(DblIntSampled.TrajectoryAnalysis): 60 | initial[x] = [1.0, 0.0] 61 | initial[u] = -K @ [1.0, 0.0] 62 | Q = np.eye(2) 63 | R = np.eye(1) 64 | tf = 100.0 65 | cost = trajectory_output(integrand=x.T @ Q @ x + u.T @ R @ u) 66 | 67 | class Casadi(co.Options): 68 | max_step = 1.0 69 | 70 | 71 | sampled_sim = DblIntSampledLQR([5e-3, 0.1]) 72 | LTI_plot(sampled_sim) 73 | plt.show() 74 | 75 | 76 | DblIntDt = co.LTI(a=dblintA, b=dblintB, name="DblIntDt", dt=5.0, dt_plant=True) 77 | 78 | 79 | class DblIntDtLQR(DblIntDt.TrajectoryAnalysis): 80 | initial[x] = [1.0, 0.0] 81 | Q = np.eye(2) 82 | R = np.eye(1) 83 | tf = 32.0 84 | # cost = trajectory_output(integrand= x.T@Q@x + u.T @ R @ u) 85 | cost = trajectory_output(integrand=x.T @ Q @ x + (K @ x).T @ R @ (K @ x)) 86 | 87 | class Casadi(co.Options): 88 | max_step = 1.0 89 | 90 | 91 | dt_sim = DblIntDtLQR([0.005, 0.1]) 92 | LTI_plot(dt_sim) 93 | plt.show() 94 | -------------------------------------------------------------------------------- /examples/test_tables.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | import condor as co 4 | 5 | sig1 = np.array( 6 | [ 7 | [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 8 | [0.2, 0.1833, 0.1621, 0.1429, 0.1256, 0.1101, 0.0966], 9 | [0.4, 0.3600, 0.3186, 0.2801, 0.2454, 0.2147, 0.1879], 10 | [0.6, 0.5319, 0.4654, 0.4053, 0.3526, 0.3070, 0.2681], 11 | [0.8, 0.6896, 0.5900, 0.5063, 0.4368, 0.3791, 0.3309], 12 | [1.0, 0.7857, 0.6575, 0.5613, 0.4850, 0.4228, 0.3712], 13 | ] 14 | ) 15 | sig2 = np.array( 16 | [ 17 | [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], 18 | [0.04, 0.7971, 0.9314, 0.9722, 0.9874, 0.9939, 0.9969], 19 | [0.16, 0.7040, 0.8681, 0.9373, 0.9688, 0.9839, 0.9914], 20 | [0.36, 0.7476, 0.8767, 0.9363, 0.9659, 0.9812, 0.9893], 21 | [0.64, 0.8709, 0.9338, 0.9625, 0.9778, 0.9865, 0.9917], 22 | [1.0, 0.9852, 0.9852, 0.9880, 0.9910, 0.9935, 0.9954], 23 | ] 24 | ) 25 | xbbar = np.linspace(0, 1.0, sig1.shape[0]) 26 | xhbar = np.linspace(0, 0.3, sig1.shape[1]) 27 | 28 | Interp = co.TableLookup( 29 | dict(bbar=xbbar, hbar=xhbar), 30 | dict(sigma=sig1, sigstr=sig2), 31 | 1, 32 | ) 33 | interp = Interp(0.5, 0.5) 34 | 35 | 36 | class MyOpt3(co.OptimizationProblem): 37 | xx = variable() 38 | yy = variable() 39 | interp = Interp(xx, yy) 40 | objective = (interp.sigma - 0.2) ** 2 + (interp.sigstr - 0.7) ** 2 41 | 42 | class Casadi(co.Options): 43 | exact_hessian = False 44 | 45 | 46 | opt3 = MyOpt3() 47 | 48 | 49 | adelfd = np.array( 50 | [ 51 | 0.0, 52 | 5.0, 53 | 10.0, 54 | 15.0, 55 | 20.0, 56 | 25.0, 57 | 30.0, 58 | 35.0, 59 | 38.0, 60 | 40.0, 61 | 42.0, 62 | 44.0, 63 | 50.0, 64 | 55.0, 65 | 60.0, 66 | ] 67 | ) 68 | # flap angle correction of oswald efficiency factor 69 | adel6 = np.array( 70 | [ 71 | 1.0, 72 | 0.995, 73 | 0.99, 74 | 0.98, 75 | 0.97, 76 | 0.955, 77 | 0.935, 78 | 0.90, 79 | 0.875, 80 | 0.855, 81 | 0.83, 82 | 0.80, 83 | 0.70, 84 | 0.54, 85 | 0.30, 86 | ] 87 | ) 88 | # induced drag correction factors 89 | asigma = np.array( 90 | [ 91 | 0.0, 92 | 0.16, 93 | 0.285, 94 | 0.375, 95 | 0.435, 96 | 0.48, 97 | 0.52, 98 | 0.55, 99 | 0.575, 100 | 0.58, 101 | 0.59, 102 | 0.60, 103 | 0.62, 104 | 0.635, 105 | 0.65, 106 | ] 107 | ) 108 | 109 | CDIFlapInterp = co.TableLookup( 110 | dict( 111 | flap_defl=adelfd, 112 | ), 113 | dict( 114 | dCL_flaps_coef=asigma, 115 | CDI_factor=adel6, 116 | ), 117 | 1, 118 | ) 119 | 120 | 121 | flap_defl = 7.0 122 | cdi_flap_interp = CDIFlapInterp(flap_defl=flap_defl) 123 | 124 | input_data = dict(x=np.arange(10) * 0.1) 125 | 126 | MyInterp = co.TableLookup( 127 | input_data, 128 | dict(y=(input_data["x"] - 0.8) ** 2), 129 | ) 130 | 131 | 132 | class MyOpt(co.OptimizationProblem): 133 | xx = variable() 134 | my_interp = MyInterp(xx) 135 | objective = my_interp.y 136 | 137 | class Options: 138 | exact_hessian = False 139 | 140 | 141 | mysol = MyOpt().xx 142 | 143 | VDEL3_interp = co.TableLookup( 144 | dict( 145 | flap_span_ratio=[0.0, 0.2, 0.4, 0.6, 0.7, 0.8, 0.9, 1.0], 146 | taper_ratio=[0.0, 0.33, 1.0], 147 | ), 148 | dict( 149 | VDEL3=np.array( 150 | [ 151 | [0.0, 0.0, 0.0], 152 | [0.4, 0.28, 0.2], 153 | [0.67, 0.52, 0.4], 154 | [0.86, 0.72, 0.6], 155 | [0.92, 0.81, 0.7], 156 | [0.96, 0.88, 0.8], 157 | [0.99, 0.95, 0.9], 158 | [1.0, 1.0, 1.0], 159 | ] 160 | ) 161 | ), 162 | degrees=1, 163 | ) 164 | 165 | VDEL3_interp_obj = VDEL3_interp(flap_span_ratio=0.3, taper_ratio=0.2) 166 | 167 | 168 | class MyOpt2(co.OptimizationProblem): 169 | xx = variable() 170 | yy = variable() 171 | objective = (VDEL3_interp(flap_span_ratio=xx, taper_ratio=yy).VDEL3 - 0.3) ** 2 172 | 173 | class Casadi(co.Options): 174 | exact_hessian = False 175 | 176 | 177 | mysol2 = MyOpt2() 178 | -------------------------------------------------------------------------------- /examples/time_switch.py: -------------------------------------------------------------------------------- 1 | from time import perf_counter 2 | 3 | import matplotlib.pyplot as plt 4 | import numpy as np 5 | from sgm_test_util import LTI_plot 6 | 7 | import condor as co 8 | 9 | with_time_state = False 10 | # either include time as state or increase tolerances to ensure sufficient ODE solver 11 | # accuracy 12 | 13 | 14 | class DblInt(co.ODESystem): 15 | A = np.array( 16 | [ 17 | [0, 1], 18 | [0, 0], 19 | ] 20 | ) 21 | B = np.array([[0, 1]]).T 22 | 23 | x = state(shape=A.shape[0]) 24 | mode = state() 25 | pos_at_switch = state() 26 | 27 | t1 = parameter() 28 | t2 = parameter() 29 | u = modal() 30 | dot[x] = A @ x + B * u 31 | 32 | if with_time_state: 33 | tt = state() 34 | dot[tt] = 1.0 35 | 36 | 37 | class Accel(DblInt.Mode): 38 | condition = mode == 0.0 39 | action[u] = 1.0 40 | 41 | 42 | class Switch1(DblInt.Event): 43 | # function = t - t1 44 | at_time = t1 45 | update[mode] = 1.0 46 | 47 | update[pos_at_switch] = x[0] 48 | 49 | 50 | class Decel(DblInt.Mode): 51 | condition = mode == 1.0 52 | action[u] = -1.0 53 | 54 | 55 | class Switch2(DblInt.Event): 56 | # function = t - t2 - t1 57 | # mode == 2. 58 | 59 | at_time = t2 + t1 60 | # update[mode] = 2. 61 | terminate = True 62 | 63 | 64 | class Transfer(DblInt.TrajectoryAnalysis): 65 | initial[x] = [-9.0, 0.0] 66 | Q = np.eye(2) 67 | cost = trajectory_output((x.T @ Q @ x) / 2) 68 | 69 | if not with_time_state: 70 | 71 | class Casadi(co.Options): 72 | state_adaptive_max_step_size = 4 73 | 74 | 75 | class AccelerateTransfer(DblInt.TrajectoryAnalysis, exclude_events=[Switch1]): 76 | initial[x] = [-9.0, 0.0] 77 | Q = np.eye(2) 78 | cost = trajectory_output((x.T @ Q @ x) / 2) 79 | 80 | if not with_time_state: 81 | 82 | class Casadi(co.Options): 83 | state_adaptive_max_step_size = 4 84 | 85 | 86 | sim = Transfer( 87 | t1=1.0, 88 | t2=4.0, 89 | ) 90 | print(sim.pos_at_switch) 91 | # jac = sim.implementation.callback.jac_callback(sim.implementation.callback.p, []) 92 | 93 | 94 | class MinimumTime(co.OptimizationProblem): 95 | t1 = variable(lower_bound=0) 96 | t2 = variable(lower_bound=0) 97 | transfer = Transfer(t1, t2) 98 | objective = transfer.cost 99 | 100 | # class Casadi(co.Options): 101 | class Options: 102 | exact_hessian = False 103 | __implementation__ = co.implementations.ScipyCG 104 | 105 | 106 | """ 107 | old: 108 | eval jacobian for jac_Transfer 109 | args [DM([1, 4]), DM(00)] 110 | p=[1, 4] 111 | [[-56.9839, 0]] 112 | 113 | 114 | 115 | """ 116 | 117 | MinimumTime.set_initial(t1=2.163165480675697, t2=4.361971866705403) 118 | 119 | t_start = perf_counter() 120 | opt = MinimumTime() 121 | t_stop = perf_counter() 122 | 123 | print("time to run:", t_stop - t_start) 124 | print(opt.t1, opt.t2) 125 | # print(jac) 126 | print(opt._stats) 127 | LTI_plot(opt.transfer) 128 | 129 | sim_accel = AccelerateTransfer(**opt.transfer.parameter.asdict()) 130 | assert sim_accel._res.e[0].rootsfound.size == opt.transfer._res.e[0].rootsfound.size - 1 # noqa 131 | 132 | plt.show() 133 | -------------------------------------------------------------------------------- /license.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/condor/2976ecae44fa94b3b882acc3b40c050126f1403e/license.pdf -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "setuptools_scm[toml]>=6.2"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "condor" 7 | description = "A package for disciplined systems modeling on a deadline" 8 | readme = "README.rst" 9 | dynamic = ["version"] 10 | authors = [ 11 | { name="Benjamin W. L. Margolis", email="benjamin.margolis@nasa.gov" }, 12 | { name="Kenneth R. Lyons", email="kenneth.r.lyons@nasa.gov" }, 13 | ] 14 | requires-python = ">=3.9" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "Operating System :: OS Independent", 18 | ] 19 | 20 | dependencies = [ 21 | "numpy", 22 | "scipy", 23 | "casadi", 24 | "ndsplines>=0.2.0post0", 25 | ] 26 | 27 | [dependency-groups] 28 | lint = ["ruff"] 29 | test = ["pytest", "pytest-cov"] 30 | docs = [ 31 | "sphinx", 32 | "sphinx-gallery", 33 | "matplotlib", 34 | "furo", 35 | ] 36 | 37 | [project.urls] 38 | Documentation = "https://nasa.github.io/condor" 39 | 40 | [tool.setuptools_scm] 41 | write_to = "src/condor/_version.py" 42 | 43 | [tool.ruff] 44 | exclude = ["design/*"] 45 | 46 | [tool.ruff.lint] 47 | select = [ 48 | "S", # flake8-bandit 49 | "B", # flake8-bugbear 50 | "EM", # flake8-errmsg 51 | "PT", # flake8-pytest-style 52 | "SIM", # flake8-simplify 53 | "I", # isort 54 | "NPY", # numpy-specific-rules 55 | "N", # pep8-naming 56 | "PERF", # perflint 57 | "E","W", # pycodestyle 58 | # "D", # pydocstyle 59 | "F", # pyflakes 60 | "PL", # pylint 61 | "UP", # pyupgrade 62 | ] 63 | ignore = [ 64 | "F821", # Undefined name ... (too dynamic) 65 | "D1", # Missing docstring in ... (use doc coverage for now) 66 | "PLR09", # Too many statements/arguments/etc (preference) 67 | "PLR2004", # Magic value used in comparison (preference) 68 | ] 69 | 70 | [tool.ruff.lint.per-file-ignores] 71 | "docs/*" = [ 72 | "E402", # Module level import not at top of file, ok in tutorials 73 | "E305", # Expected 2 blank lines afer class or function definition 74 | "S101", # asserts ok in doc examples 75 | ] 76 | "examples/*" = [ 77 | "N802", # Non-lowercase functions ok in mathy examples 78 | "N806", # Non-lowercase variables 79 | "N815", # Mixed case variables 80 | "N816", # Mixed case variables 81 | ] 82 | "tests/*" = [ 83 | "F841", # Local variable assigned but never used, ok in smoke tests 84 | "S101", # asserts ok in tests 85 | ] 86 | 87 | [tool.isort] 88 | profile = "black" 89 | 90 | [tool.pytest.ini_options] 91 | testpaths = ["tests"] 92 | addopts = "--pdbcls=IPython.terminal.debugger:TerminalPdb" 93 | -------------------------------------------------------------------------------- /src/condor/__init__.py: -------------------------------------------------------------------------------- 1 | if False: 2 | import logging 3 | 4 | logging.getLogger("condor").setLevel(logging.DEBUG) 5 | logging.basicConfig() 6 | 7 | 8 | from condor._version import __version__ 9 | from condor.conf import settings 10 | from condor.contrib import ( 11 | LTI, 12 | AlgebraicSystem, 13 | DeferredSystem, 14 | ExplicitSystem, 15 | ExternalSolverModel, 16 | ODESystem, 17 | OptimizationProblem, 18 | TableLookup, 19 | ) 20 | from condor.fields import ( 21 | AssignedField, 22 | BaseElement, 23 | BoundedAssignmentField, 24 | Direction, 25 | Field, 26 | FreeElement, 27 | FreeField, 28 | InitializedField, 29 | MatchedField, 30 | TrajectoryOutputField, 31 | WithDefaultField, 32 | ) 33 | from condor.models import ModelTemplate, ModelType, Options 34 | 35 | __all__ = [ 36 | "__version__", 37 | "ModelType", 38 | "ModelTemplate", 39 | "settings", 40 | "Options", 41 | "DeferredSystem", 42 | "ExplicitSystem", 43 | "ExternalSolverModel", 44 | "AlgebraicSystem", 45 | "ODESystem", 46 | "LTI", 47 | "TableLookup", 48 | "OptimizationProblem", 49 | "AssignedField", 50 | "BaseElement", 51 | "BoundedAssignmentField", 52 | "Direction", 53 | "Field", 54 | "FreeElement", 55 | "FreeField", 56 | "InitializedField", 57 | "MatchedField", 58 | "TrajectoryOutputField", 59 | "WithDefaultField", 60 | ] 61 | 62 | ################## 63 | """ 64 | ModelTemplate flag for user_model (default True) 65 | If false, can add placeholders, returns a tempalte instead of model 66 | 67 | 68 | 69 | 70 | ModelTemplate class creation 71 | creation of placeholder field 72 | that's it? 73 | 74 | model_template class creation (e.g., AlgebraicSystem) 75 | inherit (copy) placeholder field 76 | creation of placeholder elements for users to fill-in 77 | creation of model template specific fields (e.g., implicit_output, residual) 78 | 79 | extended template (for libraries): 80 | creation of elements that must get re-created for each user model 81 | specification of (some) parent template placeholder elements 82 | creation of new placeholder elements 83 | 84 | or is this just normal template creation? 85 | does this need to get "flattened" somehow? 86 | 87 | submodel template: 88 | same as model/extended template and... 89 | specify primary 90 | and whether fields are 91 | copied or accessed 92 | --> attaching submodel template from template to user model is a special version of 93 | extension?? kwargs need to be optional, then just another layer of dispatch. 94 | 95 | assembly/component model template: 96 | specify rules for acceptable child/parent (but also methods for later modificaiton) 97 | template extension classes by default 98 | 99 | user assembly/component model creation: 100 | provide methods for operating on tree (maybe python multi-inherit) 101 | must take kwarg 102 | 103 | user model creation 104 | (condor-)inherit template-specific fields 105 | define model: 106 | creation of elements from template specific fields 107 | fill in (optional) values for placeholder elements 108 | embed models for sub-calculations 109 | create (extended) templates for submodels 110 | 111 | (python )inherit methods for binding, accessing IO data, etc.--not part of class 112 | creation 113 | 114 | 115 | so __prepare__ has to handle all the accesible element and field injection 116 | (accessing/copying/etc), 117 | CondorClassDict has to handle assignment for elements/submodels/etc 118 | __new__ is cleanup of standard class attributes before passing to type().__new__ and 119 | finalization after -- I guess it's possible the finalization could happen in __init__ or 120 | even __init_subclass__? 121 | 122 | customize meta so only have relevant attriburtes 123 | too hard to use different metaclasses for base/template/user ? times special types like 124 | submodel, assemblycomponent. 125 | if template inherits from base, user inherits from template, what happens? special types 126 | can multi-inherit? 127 | 128 | need to be able to make it easy to build library components (extended templates) 129 | how to "socket" something? e.g., quickly create a new assembly component like an 130 | impulsive correction and implement __new__ finalize logic to modify ODE... similar to 131 | condor-flight. 132 | clear pattern (or even better, simple API to automate) 133 | if over-writing __new__, __prepare__, class_dict.__set_item__, is enough, that's OK. 134 | class_dict can use meta to access anything and then adding some generality to class_dict 135 | filtering, maybe even adding little callback hooks or something. 136 | 137 | above sketch is still just datastructure. Need to trace through construction of a simple 138 | model. 139 | 140 | LinCov is directional SGM, sone number of number to construct covariance matrix or 141 | something 142 | 143 | 144 | ModelTemplate: 145 | provide placeholder, extend_template 146 | 147 | Model: 148 | provide a bunch of user-model methods 149 | 150 | a template: inherit ModelTemplate 151 | define fields and pre-loaded symbols (placeholders, independent variables, etc) and get 152 | ready for inheritance to a user model -- 153 | 154 | a submodel template: inherit SubmodelTemplate 155 | primary to a template, define fields and pre-loaded symbols 156 | 157 | a user model: inherit from a template 158 | "read" user-defined elements, including output from embeded models 159 | inherit user-copies of fields, prepare to be be runnable -- substitue placeholder 160 | elements with their subs (or defaults) 161 | copy & extend any submodel templates 162 | 163 | a user submodel: inherit from a user model's extended submodel template 164 | 165 | 166 | 167 | 168 | 169 | currently... 170 | __new__ is: 171 | some processing specific to submodel: 172 | don't add attrs that were originally from primary to submodel 173 | if it was an embedded model, also remove from meta embedded models list 174 | 175 | create _parent_name attr for __repr__ 176 | 177 | filter attrs for: 178 | strip dynamic link 179 | options 180 | submodel template (which get added as a ref so users can create them) 181 | independent symbols (or their backend repr) that don't belong to a field bound to 182 | actual model -- processed by iterating over field's elements below 183 | 184 | process fields (and independentsymbol elements) 185 | - to update reference name which isn't known until now? 186 | - for input and output fields: 187 | name validation 188 | replace attribute name with element's name (from independent name assignment OR 189 | name kwarg on element creation) 190 | replace backend repr VALUE with element 191 | add to IO meta 192 | - dataclass creation for internal and output fields (why not input fields??) 193 | 194 | modify docstring 195 | 196 | create class using super().__new__ 197 | 198 | re-attach meta 199 | 200 | if primary (is submodel): 201 | improper separation, so needs to see if it is a submodel template or a valid 202 | submodel, and get added appropriately or not 203 | 204 | if not submodel template and not direct descendent of model, register? 205 | I think this is registering user models for each template 206 | 207 | final bind fields 208 | process inheriting submodel templates 209 | if implementaiton exists 210 | create input field dataclasses (why not earlier??) 211 | bind implementation 212 | bind dataclasses (attach qualname etc) 213 | 214 | So a lot is happening that could happen in condorclassdict, new should be finalize-y 215 | stuff. filter stuff from namespace that we want available for class declaration (user 216 | convenience) but not on final class, call model, then finalize binding etc. 217 | 218 | where does placeholder substitution go? this will specify where hooks for other 219 | extensions go. And need to figure out assembly too 220 | 221 | """ 222 | -------------------------------------------------------------------------------- /src/condor/backend/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shim module for non-operators. This is moer utility-like functionality, outside the 3 | scope of the array API standard. 4 | """ 5 | 6 | from ._get_backend import get_backend 7 | 8 | backend_mod = get_backend() 9 | 10 | 11 | # non-operators -- more utility-like, ~outside scope of array API 12 | symbol_class = backend_mod.symbol_class #: class for expressions 13 | symbol_generator = backend_mod.symbol_generator 14 | get_symbol_data = backend_mod.get_symbol_data 15 | symbol_is = backend_mod.symbol_is 16 | BackendSymbolData = backend_mod.BackendSymbolData 17 | callables_to_operator = backend_mod.callables_to_operator 18 | expression_to_operator = backend_mod.expression_to_operator 19 | 20 | process_relational_element = backend_mod.process_relational_element 21 | is_constant = backend_mod.is_constant 22 | evalf = backend_mod.evalf 23 | SymbolCompatibleDict = backend_mod.SymbolCompatibleDict 24 | symbols_in = backend_mod.symbols_in 25 | # list of symbols in an expression, casadi.symvar, aesara.graph_inputs, possibly in 26 | # jax.jax_expr stuff? 27 | -------------------------------------------------------------------------------- /src/condor/backend/_get_backend.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | 3 | 4 | def get_backend(module="condor.backends.casadi"): 5 | mod = importlib.import_module(module) 6 | return mod 7 | -------------------------------------------------------------------------------- /src/condor/backend/operators.py: -------------------------------------------------------------------------------- 1 | """ 2 | Shim module for operators, extending the array API standard with several calculus 3 | operators 4 | """ 5 | 6 | from ._get_backend import get_backend 7 | 8 | backend_mod = get_backend() 9 | # operators should be... 10 | # ~ array API 11 | # algebra and trig binary/unary ops 12 | # set reduction: (f)min/max, sum?? 13 | # limited manipulation: concat, stack, split?, reshape? 14 | # concat = backend_mod.concat 15 | 16 | # 17 | # ~ calculus 18 | # - jacobian 19 | # - jacobian_product? hessp? later 20 | # symbolic operators 21 | # - if_else 22 | # - substitute? 23 | 24 | # - NOT callable/expression to operator 25 | 26 | # constants 27 | pi = backend_mod.operators.pi #: constant pi 28 | inf = backend_mod.operators.inf #: constant inf 29 | nan = backend_mod.operators.nan #: constant nan 30 | 31 | # calculus & symbolic 32 | jacobian = backend_mod.operators.jacobian #: create dense jacobian expression 33 | if_else = backend_mod.operators.if_else #: function for creating 34 | substitute = backend_mod.operators.substitute 35 | 36 | # creation functions 37 | zeros = backend_mod.operators.zeros 38 | eye = backend_mod.operators.eye 39 | ones = backend_mod.operators.ones 40 | diag = backend_mod.operators.diag # possibly not part of array API? 41 | 42 | # "manipulation functions" 43 | concat = backend_mod.operators.concat 44 | # stack? 45 | unstack = backend_mod.operators.unstack 46 | 47 | 48 | # "element-wise functions" 49 | def wrap(f): 50 | """wrap function :attr:`f` to allow elements, symbolic, and numeric values to be 51 | usable""" 52 | 53 | def _(*args, **kwargs): 54 | new_args = [getattr(arg, "backend_repr", arg) for arg in args] 55 | new_kwargs = {k: getattr(v, "backend_repr", v) for k, v in kwargs.items()} 56 | return f(*new_args, **new_kwargs) 57 | 58 | return _ 59 | 60 | 61 | min = wrap(backend_mod.operators.min) #: array API standard for min 62 | max = wrap(backend_mod.operators.max) 63 | mod = wrap(backend_mod.operators.mod) 64 | 65 | tan = wrap(backend_mod.operators.tan) 66 | atan = wrap(backend_mod.operators.atan) 67 | atan2 = wrap(backend_mod.operators.atan2) 68 | sin = wrap(backend_mod.operators.sin) 69 | cos = wrap(backend_mod.operators.cos) 70 | asin = wrap(backend_mod.operators.asin) 71 | acos = wrap(backend_mod.operators.acos) 72 | exp = wrap(backend_mod.operators.exp) 73 | log = wrap(backend_mod.operators.log) 74 | log10 = wrap(backend_mod.operators.log10) 75 | sqrt = wrap(backend_mod.operators.sqrt) 76 | 77 | vector_norm = wrap(backend_mod.operators.vector_norm) 78 | solve = wrap(backend_mod.operators.solve) 79 | -------------------------------------------------------------------------------- /src/condor/backends/casadi/operators.py: -------------------------------------------------------------------------------- 1 | # from numpy import * 2 | import contextlib 3 | 4 | import numpy as np 5 | 6 | import casadi 7 | import condor.backends.casadi as backend 8 | 9 | # useful but not sure if all backends would have: 10 | # symvar -- list all symbols present in expression 11 | # depends_on 12 | # 13 | 14 | pi = casadi.pi 15 | inf = casadi.inf 16 | nan = np.nan 17 | 18 | mod = casadi.fmod 19 | 20 | atan = casadi.atan 21 | atan2 = casadi.atan2 22 | tan = casadi.tan 23 | sin = casadi.sin 24 | cos = casadi.cos 25 | asin = casadi.asin 26 | acos = casadi.acos 27 | exp = casadi.exp 28 | log = casadi.log 29 | log10 = casadi.log10 30 | sqrt = casadi.sqrt 31 | 32 | eye = casadi.MX.eye 33 | ones = casadi.MX.ones 34 | 35 | 36 | def diag(v, k=0): 37 | if k != 0: 38 | msg = "Not supported for this backend" 39 | raise ValueError(msg) 40 | if not hasattr(v, "shape"): 41 | # try to concat list/tuple of elements 42 | v = concat(v) 43 | return casadi.diag(v) 44 | 45 | 46 | def vector_norm(x, ord=2): 47 | if ord == 2: 48 | return casadi.norm_2(x) 49 | if ord == 1: 50 | return casadi.norm_1(x) 51 | if ord == inf: 52 | return casadi.norm_inf(x) 53 | 54 | 55 | solve = casadi.solve 56 | 57 | 58 | def concat(arrs, axis=0): 59 | """implement concat from array API for casadi""" 60 | if not arrs: 61 | return arrs 62 | if np.any([isinstance(arr, backend.symbol_class) for arr in arrs]): 63 | if axis == 0: 64 | return casadi.vcat(arrs) 65 | elif axis in (1, -1): 66 | return casadi.hcat(arrs) 67 | else: 68 | msg = "Casadi only supports matrices" 69 | raise ValueError(msg) 70 | else: 71 | return np.concat([np.atleast_2d(arr) for arr in arrs], axis=axis) 72 | 73 | 74 | def unstack(arr, axis=0): 75 | if axis == 0: 76 | return casadi.vertsplit(arr) 77 | elif axis in (1, -1): 78 | return casadi.horzsplit(arr) 79 | 80 | 81 | def zeros(shape=(1, 1)): 82 | return backend.symbol_class(*shape) 83 | 84 | 85 | def min(x, axis=None): 86 | if not isinstance(x, backend.symbol_class): 87 | x = concat(x) 88 | if axis is not None: 89 | msg = "Only axis=None supported" 90 | raise ValueError(msg) 91 | return casadi.mmin(x) 92 | 93 | 94 | def max(x, axis=None): 95 | if not isinstance(x, backend.symbol_class): 96 | x = concat(x) 97 | if axis is not None: 98 | msg = "Only axis=None supported" 99 | raise ValueError(msg) 100 | return casadi.mmax(x) 101 | 102 | 103 | def jacobian(of, wrt): 104 | """jacobian of expression `of` with respect to symbols `wrt`""" 105 | """ 106 | we can apply jacobian to ExternalSolverWrapper but it's a bit clunky because need 107 | symbol_class expressions for IO, and to evalaute need to create a Function. Not sure 108 | how to create a backend-generic interface for this. When do we want an expression vs 109 | a callable? Maybe the overall process is right (e.g., within an optimization 110 | problem, will have a variable flat input, and might just want the jac_expr) 111 | 112 | Example to extend from docs/howto_src/table_basics.py 113 | 114 | flat_inp = SinTable.input.flatten() 115 | wrap_inp = SinTable.input.wrap(flat_inp) 116 | instance = SinTable(**wrap_inp.asdict()) # needed so callback obj isn't destroyed 117 | wrap_out = instance.output 118 | flat_out = wrap_out.flatten() 119 | jac_expr = ops.jacobian(flat_out, flat_inp) 120 | from condor import backend 121 | jac = backend.expression_to_operator(flat_inp, jac_expr, "my_jac") 122 | #jac = casadi.Function("my_jac", [flat_inp], [jac_expr]) 123 | jac(0.) 124 | """ 125 | if of.size: 126 | return casadi.jacobian(of, wrt) 127 | else: 128 | return casadi.MX() 129 | 130 | 131 | def jac_prod(of, wrt, rev=True): 132 | """create directional derivative""" 133 | return casadi.jtimes(of, wrt, not rev) 134 | 135 | 136 | def substitute(expr, subs): 137 | for key, val in subs.items(): 138 | expr = casadi.substitute(expr, key, val) 139 | 140 | # if expr is the output of a single call, try to to eval it 141 | if isinstance(expr, backend.symbol_class) and ( 142 | ( 143 | expr.op() == casadi.OP_GETNONZEROS 144 | and expr.dep().op() == -1 145 | and expr.dep().dep().is_call() 146 | ) 147 | or (expr.op() == -1 and expr.dep().is_call()) 148 | ): 149 | with contextlib.suppress(RuntimeError): 150 | expr = casadi.evalf(expr) 151 | 152 | return expr 153 | 154 | 155 | def if_else(*conditions_actions): 156 | """ 157 | symbolic representation of a if/else control flow 158 | 159 | Parameters 160 | --------- 161 | *conditions_actions : list of (condition, value) pairs, ending with else_value 162 | 163 | Example 164 | -------- 165 | 166 | The expression:: 167 | 168 | value = if_else( 169 | (condition0, value0), 170 | (codnition1, value1), 171 | ... 172 | else_value 173 | ) 174 | 175 | 176 | is equivalent to the numerical code:: 177 | 178 | if condition0: 179 | value = value0 180 | elif condition1: 181 | value = value1 182 | ... 183 | else: 184 | value = else_value 185 | 186 | """ 187 | if len(conditions_actions) == 1: 188 | else_action = conditions_actions[0] 189 | if isinstance(else_action, (list, tuple)): 190 | msg = "if_else requires an else_action to be provided" 191 | raise ValueError(msg) 192 | return else_action 193 | condition, action = conditions_actions[0] 194 | remainder = if_else(*conditions_actions[1:]) 195 | return casadi.if_else(condition, action, remainder) 196 | -------------------------------------------------------------------------------- /src/condor/backends/default.py: -------------------------------------------------------------------------------- 1 | """ 2 | vcat/vertcat -- combine multiple backend_symbol 3 | flatten -- always used in cnojuction with vertcat? to make differently shaped 4 | backend_reprs 5 | 6 | vertsplit -- opposite of vertcat 7 | wrap -- opposite of flatten? 8 | --> these two pairs are really single functions of fields 9 | 10 | Function -- create a numerical/symbolic callable 11 | 12 | ??? -- create a backend-compatible Op from functions for 0th derivative and more 13 | currently CasadiFunctionCallback, but needs to be generalized 14 | 15 | if_else -- symbolic control flow 16 | ??? -- something for symbolic array stuffing if forecasting JAX compatibility; 17 | may work just to use elements everywhere? or use a wrapped JAX array as the 18 | backend_repr? assume replacing with elements works and JAX backend element mixin can 19 | include __setitem__ overwrite to replace backend_repr with .at[...].set(...) result 20 | would be nice if numpy ufunc dispatch mechanism worked and could be applied to elements. 21 | 22 | nlpsol -- should get moved to the solvers (uses casadi, scipy solvers) 23 | ideally, backend-compatible Op would see nlpsol is already an op, 24 | Interface >>> performance hit of nlpsol native vs wrapped, presumably 25 | 26 | 27 | 28 | 29 | 30 | """ 31 | 32 | """ 33 | Need to keep in a separate file so backend.get_symbol_data (required by fields which 34 | generate the symbols) can return filled dataclass without causing a circular import 35 | 36 | """ 37 | 38 | """ 39 | Backend: 40 | [x] provide symbol_generator for creating backend symbol repr 41 | [x] symbol_class for isinstance(model_attr, backend.symbol_class 42 | [x] name which is how backend options on model is identified 43 | Do we allow more complicated datastructures? like models, etc. 44 | 45 | optional implementations which has a __dict__ that allows assignment 46 | Or, should the implementations just live in the main backend? 47 | 48 | 49 | Backend Implementations 50 | [x] must be able to flatten model symbols to backend arrays, 51 | [x] wrap backend arrays to model symbol, matching shape -- 52 | [ ] wrap and flatten must handle model numerics (float/numpy array) and backend numerics 53 | (if different, eg casadi DM) and backend symbols 54 | [ ] ideally, handle special case symmetric and dynamic flags for FreeSymbol and 55 | [ ] MatchedSymbol if matched to symmetric/diagonal FreeSymbol 56 | setting the values for outputs and intermediates 57 | 58 | Couples to Model types -- knows all fields 59 | 60 | who is responsible for: 61 | filling in fields/._dataclass? 62 | filling in model input/output attrs? 63 | 64 | 65 | 66 | 67 | """ 68 | 69 | """ 70 | Figure out how to add DB storage -- maybe expect that to be a user choice (a decorator 71 | or something)? Then user defined initializer could use it. Yeah, and ORM aspect just 72 | takes advantage of model attributes just like backend implementation 73 | 74 | 75 | I assume a user model/library code could inject an implementation to the backend? 76 | not sure how to assign special numeric stuff, probably an submodel class on the model 77 | based on NPSS discussion it's not really needed if it's done right 78 | 79 | For injecting a default implementation (e.g., new backend) this does work: 80 | 81 | import casadi_implementations 82 | casadi_implementations.ODESystem = 'a reference to check exists' 83 | 84 | import condor as co 85 | 86 | but probably should just figure out hooks to do that? Could create local dict of backend 87 | that gets updated with backend.implementations at the top of this file, then libary/user 88 | code could update it (add/overwrite) 89 | """ 90 | -------------------------------------------------------------------------------- /src/condor/backends/element_mixin.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field 2 | 3 | import numpy as np 4 | 5 | """ 6 | vcat/vertcat -- combine multiple backend_symbol 7 | flatten -- always used in cnojuction with vertcat? to make differently shaped 8 | backend_reprs 9 | 10 | vertsplit -- opposite of vertcat 11 | wrap -- opposite of flatten? 12 | --> these two pairs are really single functions of fields 13 | 14 | Function -- create a numerical/symbolic callable 15 | 16 | ??? -- create a backend-compatible Op from functions for 0th derivative and more 17 | currently CasadiFunctionCallback, but needs to be generalized 18 | 19 | if_else -- symbolic control flow 20 | ??? -- something for symbolic array stuffing if forecasting JAX compatibility; 21 | may work just to use elements everywhere? or use a wrapped JAX array as the 22 | backend_repr? assume replacing with elements works and JAX backend element mixin can 23 | include __setitem__ overwrite to replace backend_repr with .at[...].set(...) result 24 | would be nice if numpy ufunc dispatch mechanism worked and could be applied to elements. 25 | 26 | nlpsol -- should get moved to the solvers (uses casadi, scipy solvers) 27 | ideally, backend-compatible Op would see nlpsol is already an op, 28 | Interface >>> performance hit of nlpsol native vs wrapped, presumably 29 | 30 | 31 | 32 | 33 | 34 | """ 35 | 36 | """ 37 | Need to keep in a separate file so backend.get_symbol_data (required by fields which 38 | generate the symbols) can return filled dataclass without causing a circular import 39 | 40 | """ 41 | 42 | """ 43 | Backend: 44 | [x] provide symbol_generator for creating backend symbol repr 45 | [x] symbol_class for isinstance(model_attr, backend.symbol_class 46 | [x] name which is how backend options on model is identified 47 | Do we allow more complicated datastructures? like models, etc. 48 | 49 | optional implementations which has a __dict__ that allows assignment 50 | Or, should the implementations just live in the main backend? 51 | 52 | 53 | Backend Implementations 54 | [x] must be able to flatten model symbols to backend arrays, 55 | [x] wrap backend arrays to model symbol, matching shape -- 56 | [ ] wrap and flatten must handle model numerics (float/numpy array) and backend numerics 57 | (if different, eg casadi DM) and backend symbols 58 | [ ] ideally, handle special case symmetric and dynamic flags for FreeSymbol and 59 | [ ] MatchedSymbol if matched to symmetric/diagonal FreeSymbol 60 | setting the values for outputs and intermediates 61 | 62 | Couples to Model types -- knows all fields 63 | 64 | who is responsible for: 65 | filling in fields/._dataclass? 66 | filling in model input/output attrs? 67 | 68 | 69 | 70 | 71 | """ 72 | 73 | """ 74 | Figure out how to add DB storage -- maybe expect that to be a user choice (a decorator 75 | or something)? Then user defined initializer could use it. Yeah, and ORM aspect just 76 | takes advantage of model attributes just like backend implementation 77 | 78 | 79 | I assume a user model/library code could inject an implementation to the backend? 80 | not sure how to assign special numeric stuff, probably an submodel class on the model 81 | based on NPSS discussion it's not really needed if it's done right 82 | 83 | For injecting a default implementation (e.g., new backend) this does work: 84 | 85 | import casadi_implementations 86 | casadi_implementations.ODESystem = 'a reference to check exists' 87 | 88 | import condor as co 89 | 90 | but probably should just figure out hooks to do that? Could create local dict of backend 91 | that gets updated with backend.implementations at the top of this file, then libary/user 92 | code could update it (add/overwrite) 93 | """ 94 | 95 | 96 | @dataclass 97 | class BackendSymbolDataMixin: 98 | shape: tuple # TODO: tuple of ints 99 | symmetric: bool 100 | diagonal: bool 101 | size: int = field(init=False) 102 | # TODO: mark size as computed? or replace with @property? can that be trivially 103 | # cached? currently computed by actual backend... 104 | 105 | def __post_init__(self, *args, **kwargs): 106 | n = self.shape[0] 107 | size = np.prod(self.shape) 108 | if self.symmetric: 109 | size = int(n * (n + 1) / 2) 110 | if self.diagonal: 111 | size = n 112 | 113 | if self.diagonal and size != self.shape[0]: 114 | msg = f"Diagonal symbol should have size {self.shape[0]}, got {size}" 115 | raise ValueError(msg) 116 | elif self.symmetric: 117 | if len(self.shape) != 2: 118 | msg = f"Symmetric symbol must have dimension 2, got {len(self.shape)}" 119 | raise ValueError(msg) 120 | if self.shape[0] != self.shape[1]: 121 | msg = f"Symmetric symbol must be square, got shape {self.shape}" 122 | raise ValueError(msg) 123 | self.size = size 124 | -------------------------------------------------------------------------------- /src/condor/conf.py: -------------------------------------------------------------------------------- 1 | """Module configuration""" 2 | 3 | import importlib 4 | import sys 5 | 6 | 7 | class Settings: 8 | """Configuration manager""" 9 | 10 | def __init__(self): 11 | # settings is a stack of dicts which should allow arbitrary nested deferred 12 | # modules 13 | self.settings = [{}] 14 | 15 | def get_module(self, module, **kwargs): 16 | """Load a module by path with specified options set 17 | 18 | Parameters 19 | ---------- 20 | module : str 21 | Module path that would be used in an import statement, e.g. 22 | ``"my_package.module"`` 23 | **kwargs 24 | Settings declared by `module` (see :meth:`get_settings`). 25 | 26 | Returns 27 | ------- 28 | mod : module 29 | Configured module object. 30 | """ 31 | self.settings.append(kwargs) 32 | 33 | if module not in sys.modules: 34 | mod = importlib.import_module(module) 35 | else: 36 | mod = sys.modules[module] 37 | mod = importlib.reload(mod) 38 | self.settings.pop() 39 | return mod 40 | 41 | def get_settings(self, **defaults): 42 | """Declare available settings with default values 43 | 44 | Parameters 45 | ---------- 46 | **defaults 47 | Declared available settings with default values. 48 | 49 | Returns 50 | ------- 51 | settings : dict 52 | Module configuration as set by :meth:`get_module`. 53 | """ 54 | configured_kwargs = {k: self.settings[-1].get(k, defaults[k]) for k in defaults} 55 | extra_kwargs = {k: v for k, v in self.settings[-1].items() if k not in defaults} 56 | if extra_kwargs: 57 | # TODO warn instead? 58 | msg = f"Extra keyword arguments provided to configuration {extra_kwargs}" 59 | raise ValueError(msg) 60 | 61 | return configured_kwargs 62 | 63 | 64 | #: singleton :class:`Settings` instance 65 | settings = Settings() 66 | -------------------------------------------------------------------------------- /src/condor/implementations/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module uses the backend to process a Model with values for input fields to call 3 | solvers. 4 | """ 5 | # TODO: make SGM and SolverWithWarmStart (really, back-tracking solver and possibly only 6 | # needed if broyden doesn't resolve it?) generic and figure out how to separate the 7 | # algorithm from the casadi callback. I think SWWS already does this pretty well 8 | 9 | # Implementations should definitely be the owners of expressions used to generate 10 | # functions -- maybe own all intermediates used to interface with solver? I think some 11 | # casadi built-in solvers+symbolic representation take either (or both) expressions and 12 | # ca.Function`s (~ duck-typed functions that can operate on either symbols or numerics, 13 | # kind of an arechetype of callback?) but even when they take expressions, I don't think 14 | # they allow access to them after creation so it's useful for the implementation to keep 15 | # it 16 | 17 | # would like implementation to be the interface between model field and the callback, 18 | # but some of the helpers (eg initializer, state settter, etc) generate functions, not 19 | # just collect expressions. 20 | 21 | # TODO figure out how to use names for casadi callback layer 22 | # TODO function generation is primarily responsibility of callback (e.g., nlpsol takes 23 | # expressions, not functions) 24 | # TODO if we provide a backend reference to vertcat, implementations can be 25 | # backend-agnostic and just provide appropriate flattening + binding of fields! may 26 | # also/instead create callback even for OptimizationProblem, ExplicitSystem to provide 27 | # consistent interface 28 | # --> move some of the helper functions that are tightly coupled to backend to utils, ? 29 | # and generalize, eg state setter 30 | # TODO for custom solvers like SGM, table, does the get_jacobian arguments allow you to 31 | # avoid computing wrt particular inputs/outputs if possible? 32 | 33 | from condor.implementations.iterative import ( 34 | AlgebraicSystem, 35 | ScipyCG, 36 | ScipySLSQP, 37 | ScipyTrustConstr, 38 | ) 39 | from condor.implementations.iterative import ( 40 | CasadiNlpsolImplementation as OptimizationProblem, 41 | ) 42 | from condor.implementations.sgm_trajectory import TrajectoryAnalysis 43 | from condor.implementations.simple import ( 44 | DeferredSystem, 45 | ExplicitSystem, 46 | ExternalSolverModel, 47 | ) 48 | 49 | __all__ = [ 50 | "DeferredSystem", 51 | "ExplicitSystem", 52 | "ExternalSolverModel", 53 | "AlgebraicSystem", 54 | "OptimizationProblem", 55 | "ScipyCG", 56 | "ScipySLSQP", 57 | "ScipyTrustConstr", 58 | "TrajectoryAnalysis", 59 | ] 60 | -------------------------------------------------------------------------------- /src/condor/implementations/simple.py: -------------------------------------------------------------------------------- 1 | from condor.backend import callables_to_operator, expression_to_operator 2 | 3 | from .utils import options_to_kwargs 4 | 5 | 6 | class DeferredSystem: 7 | def construct(self, model): 8 | self.symbol_inputs = model.input.flatten() 9 | self.symbol_outputs = model.output.flatten() 10 | self.model = model 11 | 12 | def __init__(self, model_instance): 13 | self.construct(model_instance.__class__) 14 | self(model_instance) 15 | 16 | def __call__(self, model_instance): 17 | model_instance.bind_field(self.model.output.wrap(self.symbol_outputs)) 18 | 19 | 20 | class ExplicitSystem: 21 | """Implementation for :class:`ExplicitSystem` model. 22 | 23 | No :class:`Options` expected. 24 | """ 25 | 26 | def __init__(self, model_instance): 27 | self.construct(model_instance.__class__) 28 | self(model_instance) 29 | 30 | def construct(self, model): 31 | self.symbol_inputs = model.input.flatten() 32 | self.symbol_outputs = model.output.flatten() 33 | 34 | self.model = model 35 | self.func = expression_to_operator( 36 | [self.symbol_inputs], 37 | self.symbol_outputs, 38 | name=model.__name__, 39 | ) 40 | 41 | def __call__(self, model_instance): 42 | self.args = model_instance.input.flatten() 43 | self.out = self.func(self.args) 44 | model_instance.bind_field(self.model.output.wrap(self.out)) 45 | 46 | 47 | class ExternalSolverModel: 48 | """Implementation for External Solver models. 49 | 50 | No :class:`Options` expected. 51 | """ 52 | 53 | def __init__(self, model_instance): 54 | model = model_instance.__class__ 55 | model_instance.options_dict = options_to_kwargs(model) 56 | self.construct(model, **model_instance.options_dict) 57 | self(model_instance) 58 | 59 | def construct(self, model): 60 | self.model = model 61 | self.wrapper = model._meta.external_wrapper 62 | self.input = model.input.flatten() 63 | self.output = model.output.flatten() 64 | wrapper_funcs = [self.wrapper.function] 65 | if hasattr(self.wrapper, "jacobian"): 66 | wrapper_funcs.append(self.wrapper.jacobian) 67 | if hasattr(self.wrapper, "hessian"): 68 | wrapper_funcs.append(self.wrapper.hessian) 69 | self.callback = callables_to_operator( 70 | wrapper_funcs, 71 | self, 72 | jacobian_of=None, 73 | input_symbol=self.input, 74 | output_symbol=self.output, 75 | ) 76 | self.callback.construct() 77 | 78 | def __call__(self, model_instance): 79 | use_args = model_instance.input.flatten() 80 | out = self.callback(use_args) 81 | model_instance.bind_field(self.model.output.wrap(out)) 82 | -------------------------------------------------------------------------------- /src/condor/implementations/utils.py: -------------------------------------------------------------------------------- 1 | def options_to_kwargs(new_cls): 2 | """Process a model clasa and create the kwarg dictionary for the :class:`Options`""" 3 | opts = getattr(new_cls, "Options", None) 4 | if opts is not None: 5 | backend_option = { 6 | k: v for k, v in opts.__dict__.items() if not k.startswith("_") 7 | } 8 | else: 9 | backend_option = {} 10 | return backend_option 11 | -------------------------------------------------------------------------------- /src/condor/solvers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nasa/condor/2976ecae44fa94b3b882acc3b40c050126f1403e/src/condor/solvers/__init__.py -------------------------------------------------------------------------------- /src/condor/solvers/newton.py: -------------------------------------------------------------------------------- 1 | import casadi 2 | import numpy as np 3 | from scipy import linalg 4 | 5 | 6 | def wrap_ls_func(f): 7 | def func(x, p): 8 | out = f(x, p).toarray().reshape(-1) 9 | if out.size == 1: 10 | out = out[0] 11 | return out 12 | 13 | return func 14 | 15 | 16 | class Newton: 17 | """ 18 | :: 19 | 20 | x = casadi.vertcat(*[casadi.MX.sym(f"x{i}", 1) for i in range(4)]) 21 | p = casadi.vertcat(*[casadi.MX.sym(f"p{i}", 1) for i in range(2)]) 22 | resid = casadi.vertcat(x[0] + p[0], x[1] * p[1], x[2]**2, x[3] / 2) 23 | Newton(x, p, resids) 24 | 25 | :: 26 | 27 | f = casadi.Function("f", [x, p], [resid]) 28 | fprime = casadi.Function("fprime", [x, p], [casadi.jacobian(resid, x)]) 29 | 30 | """ 31 | 32 | def __init__( 33 | self, 34 | x, 35 | p, 36 | resids, 37 | lbx=None, 38 | ubx=None, 39 | max_iter=1000, 40 | tol=1e-10, 41 | ls_type=None, 42 | error_on_fail=True, 43 | ): 44 | self.max_iter = max_iter 45 | self.tol = tol 46 | self.ls_type = ls_type 47 | 48 | self.lbx = lbx 49 | if lbx is None: 50 | self.lbx = np.full(x.size()[0], -np.inf) 51 | self.ubx = ubx 52 | if ubx is None: 53 | self.ubx = np.full(x.size()[0], np.inf) 54 | 55 | # residuals for newton 56 | self.f = casadi.Function("f", [x, p], [resids]) 57 | self.fprime = casadi.Function("fprime", [x, p], [casadi.jacobian(resids, x)]) 58 | 59 | # obj for linesearch, TODO: performance testing for sumsqr vs norm_2 60 | # resids_norm = casadi.sumsqr(resids) 61 | resids_norm = casadi.norm_2(resids) 62 | self.ls_f = wrap_ls_func(casadi.Function("ls_f", [x, p], [resids_norm])) 63 | self.ls_fprime = wrap_ls_func( 64 | casadi.Function("ls_fprime", [x, p], [casadi.jacobian(resids_norm, x)]) 65 | ) 66 | self.error_on_fail = error_on_fail 67 | 68 | def __call__(self, x, p): 69 | x = np.asarray(x, dtype=float).reshape(-1) 70 | p = np.asarray(p, dtype=float).reshape(-1) 71 | 72 | itr = 0 73 | while True: 74 | if itr >= self.max_iter: 75 | print("newton failed, max iters reached") 76 | print(f"x: {x}\np: {p}") 77 | breakpoint() 78 | break 79 | 80 | # eval residuals func 81 | f = self.f(x, p).toarray().reshape(-1) 82 | 83 | # check tol 84 | if np.linalg.norm(f, ord=np.inf) < self.tol: 85 | break 86 | 87 | # eval jacobian 88 | jac = self.fprime(x, p) 89 | 90 | # newton step calculation jac @ d = -f 91 | try: 92 | dx = linalg.solve(jac, -f) 93 | except linalg.LinAlgError: 94 | print( 95 | "solver failed, jacobian is singular. itr:", 96 | itr, 97 | ) 98 | break 99 | except ValueError: 100 | print("some other error. itr:", itr) 101 | breakpoint() 102 | pass 103 | break 104 | 105 | # TODO check stall 106 | 107 | if self.ls_type is None: 108 | x += dx 109 | elif "AG" in self.ls_type: 110 | # om sequence: 111 | # init: given current x, full newton step dx 112 | # take the step x += dx 113 | # enforce bounds, modify step 114 | # line search from bound-enforced point back to x? 115 | 116 | # TODO ArmijoGoldsteinLS takes an option for initial alpha, default 1.0 117 | 118 | xb, dxb = _enforce_bounds_scalar(x + dx, dx, 1.0, self.lbx, self.ubx) 119 | 120 | fx = self.ls_f(x, p) 121 | gx = self.ls_fprime(x, p) 122 | 123 | alpha, fc, gc, fout, _, _ = line_search( 124 | self.ls_f, 125 | self.ls_fprime, 126 | x, 127 | dxb, 128 | gfk=gx, 129 | old_fval=fx, 130 | c1=0.1, 131 | c2=0.9, 132 | args=(p,), 133 | maxiter=2, 134 | ) 135 | 136 | # alpha, fc, fout = line_search_armijo( 137 | # self.ls_f, x, dxb, gx, fx, args=(p,), c1=0.1, alpha0=1 138 | # ) 139 | 140 | if alpha is None: 141 | # TODO: performance tuning, alpha = 0 [x = x], alpha = 1 [x=xb], 142 | # alpha=0.5, or something else? 0 and 1 work for simple turbojet 143 | # design cycle only 144 | alpha = 0.5 145 | print("line search failed!", xb) 146 | print("setting alpha =", alpha) 147 | break 148 | 149 | x += alpha * dxb 150 | else: 151 | x += dx 152 | x, dx = _enforce_bounds_scalar(x, dx, 1.0, self.lbx, self.ubx) 153 | 154 | itr += 1 155 | 156 | # TODO 157 | # self.resid_vals = f 158 | self.iters = itr 159 | return x 160 | 161 | 162 | def _enforce_bounds_scalar(u, du, alpha, lower_bounds, upper_bounds): 163 | # from openmdao/solvers/linesearch/backtracking.py 164 | 165 | # The assumption is that alpha * step has been added to this vector 166 | # just prior to this method being called. We are currently in the 167 | # initialization of a line search, and we're trying to ensure that 168 | # the initial step does not violate bounds. If it does, we modify 169 | # the step vector directly. 170 | 171 | # If u > lower, we're just adding zero. Otherwise, we're adding 172 | # the step required to get up to the lower bound. 173 | # For du, we normalize by alpha since du eventually gets 174 | # multiplied by alpha. 175 | change_lower = 0.0 if lower_bounds is None else np.maximum(u, lower_bounds) - u 176 | 177 | # If u < upper, we're just adding zero. Otherwise, we're adding 178 | # the step required to get down to the upper bound, but normalized 179 | # by alpha since du eventually gets multiplied by alpha. 180 | change_upper = 0.0 if upper_bounds is None else np.minimum(u, upper_bounds) - u 181 | 182 | if lower_bounds is not None: 183 | mask = u < lower_bounds 184 | if np.any(mask): 185 | print("vals exceeding lower bounds") 186 | print("\tval:", u[mask]) 187 | print("\tlower:", lower_bounds[mask]) 188 | 189 | if upper_bounds is not None: 190 | mask = u > upper_bounds 191 | if np.any(mask): 192 | print("vals exceeding upper bounds") 193 | print("\tval:", u[mask]) 194 | print("\tupper:", upper_bounds[mask]) 195 | 196 | change = change_lower + change_upper 197 | 198 | # TODO don't modify in place for now while testing 199 | # u += change 200 | # du += change / alpha 201 | return u + change, du + change / alpha 202 | -------------------------------------------------------------------------------- /tests/modules/configured_model.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from scipy.signal import cont2discrete 3 | 4 | import condor as co 5 | 6 | settings = co.settings.get_settings( 7 | a=None, 8 | b=None, 9 | dt=0.0, 10 | dt_plant=False, 11 | ) 12 | 13 | 14 | class LTI(co.ODESystem): 15 | a = settings["a"] 16 | b = settings["b"] 17 | 18 | x = state(shape=a.shape[0]) 19 | xdot = a @ x 20 | 21 | if settings["dt"] <= 0.0 and settings["dt_plant"]: 22 | raise ValueError 23 | 24 | if b is not None: 25 | K = parameter(shape=b.T.shape) 26 | 27 | if settings["dt"] and not settings["dt_plant"]: 28 | u = state(shape=b.shape[1]) 29 | 30 | else: 31 | # feedback control matching system 32 | u = -K @ x 33 | dynamic_output.u = u 34 | 35 | xdot += b @ u 36 | 37 | if not (settings["dt_plant"] and settings["dt"]): 38 | dot[x] = xdot 39 | 40 | 41 | if settings["dt"]: 42 | 43 | class DT(LTI.Event): 44 | function = np.sin(t * np.pi / settings["dt"]) 45 | if settings["dt_plant"]: 46 | if b is None: 47 | b = np.zeros((a.shape[0], 1)) 48 | Ad, Bd, *_ = cont2discrete((a, b, None, None), dt=settings["dt"]) 49 | update[x] = (Ad - Bd @ K) @ x 50 | elif b is not None: 51 | update[u] = -K @ x 52 | -------------------------------------------------------------------------------- /tests/test_algebraic_system.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import condor as co 5 | 6 | # TODO test with actual bounds 7 | 8 | 9 | @pytest.fixture 10 | def sellar_system(): 11 | class Coupling(co.AlgebraicSystem): 12 | x = parameter() 13 | z = parameter(shape=2) 14 | y1 = variable(initializer=1.0) 15 | y2 = variable(initializer=1.0) 16 | 17 | y1_agreement = residual(y1 == z[0] ** 2 + z[1] + x - 0.2 * y2) 18 | residual(y2 == y1**0.5 + z[0] + z[1]) 19 | 20 | return Coupling 21 | 22 | 23 | def test_sellar_solvers(sellar_system): 24 | out = sellar_system(x=1, z=[5, 2]) 25 | 26 | # residuals bound, get by name 27 | assert np.isclose(out.residual.y1_agreement, 0, atol=1e-6) 28 | 29 | assert np.isclose(out.variable.y1, 25.5883, rtol=1e-6) 30 | assert np.isclose(out.variable.y2, 12.0585, rtol=1e-6) 31 | 32 | 33 | def test_set_initial(sellar_system): 34 | sellar_system.set_initial(y1=1.1, y2=1.5) 35 | out = sellar_system(x=1, z=[5.0, 2.0]) 36 | for resid in out.residual.asdict().values(): 37 | assert np.isclose(resid, 0, atol=1e-6) 38 | 39 | 40 | def test_set_initial_typo(sellar_system): 41 | with pytest.raises(ValueError, match="variables"): 42 | sellar_system.set_initial(x=1) 43 | -------------------------------------------------------------------------------- /tests/test_custom_model_template.py: -------------------------------------------------------------------------------- 1 | import condor as co 2 | 3 | 4 | def test_defaults(): 5 | class CustomModelType(co.ModelType): 6 | pass 7 | 8 | class CustomModelTemplate(co.ModelTemplate, model_metaclass=CustomModelType): 9 | pass 10 | 11 | class MyModel(CustomModelTemplate): 12 | pass 13 | 14 | 15 | def test_new_kwarg(): 16 | class CustomModelType(co.ModelType): 17 | def __new__(cls, *args, new_kwarg=None, **kwargs): 18 | assert new_kwarg == "handle a string" 19 | 20 | class CustomModelTemplate(co.ModelTemplate, model_metaclass=CustomModelType): 21 | pass 22 | 23 | class MyModel(CustomModelTemplate, new_kwarg="handle a string"): 24 | pass 25 | -------------------------------------------------------------------------------- /tests/test_fields.py: -------------------------------------------------------------------------------- 1 | import condor 2 | 3 | 4 | def test_create_from(): 5 | class Sys1(condor.ExplicitSystem): 6 | a = input() 7 | b = input() 8 | 9 | output.c = a + b 10 | 11 | class Sys2(condor.ExplicitSystem): 12 | a_lias = input() 13 | 14 | sys1_in = input.create_from(Sys1.input, a=a_lias) 15 | sys1_out = Sys1(**sys1_in) 16 | 17 | output.d = sys1_out.c + 1 18 | 19 | out = Sys2(a_lias=10, b=2) 20 | assert out.d == 10 + 2 + 1 21 | 22 | 23 | def test_create_from_different_field_types(): 24 | class Sys1(condor.ExplicitSystem): 25 | a = input() 26 | b = input() 27 | 28 | output.c = a + b 29 | 30 | class Sys2(condor.AlgebraicSystem): 31 | a = variable() 32 | c_target = parameter() 33 | 34 | sys1_in = parameter.create_from(Sys1.input, a=a) 35 | sys1_out = Sys1(**sys1_in) 36 | 37 | residual(sys1_out.c == c_target) 38 | 39 | out = Sys2(b=3, c_target=10) 40 | assert out.a == 7 41 | 42 | 43 | def test_dict_unpack(): 44 | class Sys(condor.ExplicitSystem): 45 | a = input() 46 | b = input() 47 | 48 | output.c = a + b 49 | 50 | assert dict(**Sys.input) == {"a": Sys.a.backend_repr, "b": Sys.b.backend_repr} 51 | assert dict(**Sys.output) == {"c": Sys.c.backend_repr} 52 | 53 | sys = Sys(a=1, b=-2) 54 | assert dict(**sys.input) == {"a": 1, "b": -2} 55 | assert dict(**sys.output) == {"c": -1} 56 | -------------------------------------------------------------------------------- /tests/test_model_api.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import condor as co 5 | 6 | 7 | def test_output_ref(): 8 | class OutputRefCheck(co.ExplicitSystem): 9 | x = input() 10 | output.y = x**2 11 | output.z = y + 1 12 | 13 | chk = OutputRefCheck(3.0) 14 | assert chk.y == 9.0 15 | 16 | 17 | def test_placeholder_on_explicitsystem(): 18 | with pytest.raises(NameError, match="name 'placeholder' is not defined"): 19 | 20 | class ShouldFail(co.ExplicitSystem): 21 | x = input() 22 | output.y = x**2 23 | z = placeholder() 24 | 25 | 26 | def test_reserved_word_input(): 27 | with pytest.raises(ValueError, match="Attempting to set _meta"): 28 | 29 | class ShouldFail(co.ExplicitSystem): 30 | _meta = input() 31 | output.y = _meta**2 32 | 33 | 34 | def test_reserved_word_output(): 35 | with pytest.raises(ValueError, match="Attempting to set _meta"): 36 | 37 | class ShouldFail(co.ExplicitSystem): 38 | x = input() 39 | output._meta = x**2 40 | 41 | 42 | def test_objective_shape(): 43 | class Check(co.OptimizationProblem): 44 | assert objective.shape == (1, 1) 45 | 46 | 47 | def test_ode_system_event_api(): 48 | class MySystem(co.ODESystem): 49 | ts = list() 50 | for i in range(3): 51 | ts.append(parameter(name=f"t_{i}")) # noqa: PERF401 52 | 53 | n = 2 54 | m = 1 55 | x = state(shape=n) 56 | C = state(shape=(n, n)) # symmetric=True) 57 | A = np.array([[0, 1], [0, 0]]) 58 | 59 | # A = parameter(n,n) 60 | # B = parameter(n,m) 61 | # K = parameter(m,n) 62 | 63 | W = C.T @ C 64 | 65 | # indexing an output/computation by state 66 | dot[x] = A @ x # (A - B @ K) @ x 67 | dot[C] = A @ C + C @ A.T 68 | # dot naming, although have to do more work to minimize other setattr's 69 | dynamic_output.y = C.T @ C 70 | 71 | class MyEvent(MySystem.Event): 72 | function = MySystem.t - 100.0 73 | update[x] = x - 2 74 | z = state() 75 | 76 | assert MyEvent._meta.primary is MySystem 77 | assert co.models.Submodel in MyEvent.__bases__ 78 | assert MySystem.Event not in MyEvent.__bases__ 79 | assert MySystem.Event in MySystem._meta.submodels 80 | assert MyEvent in MySystem.Event 81 | assert MyEvent.update._elements[0].match in MySystem.state 82 | 83 | class MySim(MySystem.TrajectoryAnalysis): 84 | # this over-writes which is actually really nice 85 | initial[x] = 1.0 86 | initial[C] = np.eye(2) * parameter() 87 | 88 | out1 = trajectory_output(integrand=x.T @ x) 89 | out2 = trajectory_output(x.T @ x) 90 | out3 = trajectory_output(C[0, 0], C[1, 1]) 91 | 92 | with pytest.raises(ValueError, match="Incompatible terminal term shape"): 93 | # incompatible shape 94 | out4 = trajectory_output(C[0, 0], x) 95 | 96 | 97 | def test_embedded_system(): 98 | class Sys1out(co.ExplicitSystem): 99 | x = input() 100 | y = input() 101 | output.z = x**2 + y 102 | 103 | class Sys2out(co.ExplicitSystem): 104 | x = input() 105 | y = input() 106 | v = y**2 107 | output.w = x**2 + y**2 108 | output.z = x**2 + y 109 | 110 | class Sys3out(co.ExplicitSystem): 111 | z = input() 112 | sys2 = Sys2out(z**2, z) 113 | output.x = sys2.w 114 | output.y = sys2.z 115 | 116 | with pytest.raises(AttributeError, match="no attribute 'v'"): 117 | # v is not a bound output of sys2 118 | output.v = sys2.v 119 | 120 | 121 | def test_algebraic_system(): 122 | class MySolver(co.AlgebraicSystem): 123 | x = parameter() 124 | z = parameter() 125 | y2 = variable(lower_bound=0.0, initializer=1.0) 126 | y1 = variable(lower_bound=0.0) 127 | 128 | residual(y2 + x**2 == 0) 129 | residual(y1 - x + z == 0) 130 | 131 | class Options: 132 | warm_start = False 133 | 134 | mysolution = MySolver(10, 1) 135 | assert mysolution.y2 == -100 136 | assert mysolution.y1 == 9 137 | -------------------------------------------------------------------------------- /tests/test_model_config.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | 4 | import condor as co 5 | 6 | 7 | def test_model_config(): 8 | a = np.array([[0, 1], [0, 0]]) 9 | b = np.array([[0], [1]]) 10 | 11 | ct_mod = co.settings.get_module("modules.configured_model", a=a, b=b) 12 | dbl_int = ct_mod.LTI 13 | # no events 14 | assert len(dbl_int.Event._meta.subclasses) == 0 15 | 16 | sp_mod = co.settings.get_module("modules.configured_model", a=a, b=b, dt=0.5) 17 | sp_dbl_int = sp_mod.LTI 18 | # one DT event 19 | assert len(sp_dbl_int.Event._meta.subclasses) == 1 20 | 21 | assert dbl_int is not sp_dbl_int 22 | 23 | with pytest.raises(ValueError, match="Extra keyword arguments"): 24 | co.settings.get_module("modules.configured_model", a=a, b=b, extra="something") 25 | -------------------------------------------------------------------------------- /tests/test_operators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import condor as co 4 | 5 | backend = co.backend 6 | ops = backend.operators 7 | 8 | 9 | def test_min_max(): 10 | class TestMax(co.ExplicitSystem): 11 | x = input() 12 | output.y = ops.max([0.3, x]) 13 | output.z = ops.min([0.3, x]) 14 | output.yy = ops.min(ops.concat([0.3, x])) 15 | output.zz = ops.max(ops.concat([0.3, x])) 16 | 17 | chk1 = TestMax(0.5) 18 | chk2 = TestMax(0.1) 19 | 20 | assert chk1.y == 0.5 21 | assert chk1.z == 0.3 22 | 23 | assert chk2.y == 0.3 24 | assert chk2.z == 0.1 25 | 26 | 27 | def test_if_(): 28 | class Check(co.ExplicitSystem): 29 | catd = input() 30 | output.emlf = ops.if_else( 31 | (catd == 0, 3.8), # normal design FAR Part 23 32 | (catd == 1, 4.4), # utility design FAR 23 33 | (catd == 2, 6.0), # aerobatic design FAR 23 34 | (catd == 3, 2.5), # transports FAR 25 35 | (catd > 3, catd), # input design limit load factor 36 | 1.234, # else 37 | ) 38 | 39 | assert Check(2.2).emlf == 1.234 40 | assert Check(1).emlf == 4.4 41 | assert Check(2).emlf == 6.0 42 | assert Check(12).emlf == 12 43 | 44 | with pytest.raises(ValueError, match="if_else requires an else_action"): 45 | 46 | class Check(co.ExplicitSystem): 47 | catd = input() 48 | output.emlf = ops.if_else( 49 | (catd == 0, 3.8), # normal design FAR Part 23 50 | (catd == 1, 4.4), # utility design FAR 23 51 | (catd == 2, 6.0), # aerobatic design FAR 23 52 | (catd == 3, 2.5), # transports FAR 25 53 | (catd > 3, catd), # input design limit load factor 54 | ) 55 | 56 | 57 | def test_jacobian_empty(): 58 | class TestJacobian(co.ExplicitSystem): 59 | x = input() 60 | 61 | ops.jacobian(TestJacobian.output.flatten(), TestJacobian.input.flatten()) 62 | -------------------------------------------------------------------------------- /tests/test_optimization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import condor as co 4 | from condor.backend.operators import exp 5 | 6 | # TODO test from_values 7 | 8 | 9 | @pytest.mark.parametrize( 10 | "method", 11 | [ 12 | co.implementations.OptimizationProblem, 13 | co.implementations.ScipySLSQP, 14 | co.implementations.ScipyTrustConstr, 15 | ], 16 | ) 17 | def test_sellar(method): 18 | class Coupling(co.AlgebraicSystem): 19 | x = parameter() 20 | z = parameter(shape=2) 21 | y1 = variable(initializer=1.0) 22 | y2 = variable(initializer=1.0) 23 | 24 | y1_agreement = residual(y1 == z[0] ** 2 + z[1] + x - 0.2 * y2) 25 | residual(y2 == y1**0.5 + z[0] + z[1]) 26 | 27 | # coupling = Coupling(1, [5.0, 2.0]) 28 | 29 | class Sellar(co.OptimizationProblem): 30 | x = variable(lower_bound=0, upper_bound=10) 31 | z = variable(shape=2, lower_bound=0, upper_bound=10) 32 | 33 | coupling = Coupling(x, z) 34 | y1, y2 = coupling 35 | 36 | objective = x**2 + z[1] + y1 + exp(-y2) 37 | 38 | constraint(y1, upper_bound=10, lower_bound=3.16, name="y1_bound") 39 | y2_bound = constraint(y2 < 20) 40 | 41 | # Sellar.implementation.set_initial(x=1., z=[5., 2.,]) 42 | Sellar.x.initializer = 1.0 43 | Sellar.z.initializer = [5.0, 2.0] 44 | Sellar.Options.__implementation__ = method 45 | Sellar() 46 | 47 | # TODO meaningful asserts? 48 | 49 | 50 | def test_callback(): 51 | class Opt(co.OptimizationProblem): 52 | p = parameter() 53 | x = variable() 54 | 55 | objective = x**2 + p 56 | 57 | class Callback: 58 | def __init__(self): 59 | self.count = 0 60 | self.objectives = [] 61 | self.parameter = None 62 | 63 | def init_callback(self, parameter, impl_opts): 64 | self.parameter = parameter 65 | 66 | def iter_callback(self, i, variable, objective, constraint): 67 | self.count += 1 68 | self.objectives.append(objective) 69 | 70 | callback = Callback() 71 | Opt.Options.iter_callback = callback.iter_callback 72 | 73 | xinit = 10 74 | p = 4 75 | Opt.x.initializer = xinit 76 | Opt(p=p) 77 | 78 | assert callback.parameter is None 79 | assert callback.count > 0 80 | assert callback.objectives[0] == xinit**2 + p 81 | assert callback.objectives[-1] == p 82 | 83 | # add init callback 84 | Opt.Options.init_callback = callback.init_callback 85 | Opt(p=p) 86 | assert callback.parameter is not None 87 | 88 | 89 | def test_callback_scipy_no_instance(): 90 | class Opt(co.OptimizationProblem): 91 | p = parameter() 92 | x = variable() 93 | 94 | objective = x**2 + p 95 | 96 | class Options: 97 | __implementation__ = co.implementations.ScipySLSQP 98 | 99 | class Callback: 100 | def __init__(self): 101 | self.count = 0 102 | self.objectives = [] 103 | self.parameter = None 104 | 105 | def init_callback(self, parameter, impl_opts): 106 | self.parameter = parameter 107 | 108 | def iter_callback(self, i, variable, objective, constraint): 109 | self.count += 1 110 | self.objectives.append(objective) 111 | 112 | callback = Callback() 113 | Opt.Options.iter_callback = callback.iter_callback 114 | 115 | xinit = 10 116 | p = 4 117 | Opt.x.initializer = xinit 118 | Opt(p=p) 119 | 120 | assert callback.parameter is None 121 | assert callback.count > 0 122 | assert callback.objectives[0] == xinit**2 + p 123 | assert callback.objectives[-1] == p 124 | 125 | # add init callback 126 | Opt.Options.init_callback = callback.init_callback 127 | Opt(p=p) 128 | assert callback.parameter is not None 129 | 130 | 131 | def test_callback_scipy_instance(): 132 | class Opt(co.OptimizationProblem): 133 | p = parameter() 134 | x = variable() 135 | 136 | objective = x**2 + p 137 | 138 | class Options: 139 | __implementation__ = co.implementations.ScipySLSQP 140 | 141 | class Callback: 142 | def __init__(self): 143 | self.count = 0 144 | self.objectives = [] 145 | self.parameter = None 146 | self.instances = [] 147 | 148 | def init_callback(self, parameter, impl_opts): 149 | self.parameter = parameter 150 | 151 | def iter_callback(self, i, variable, objective, constraint, instance): 152 | self.count += 1 153 | self.objectives.append(objective) 154 | self.instances.append(instance) 155 | 156 | callback = Callback() 157 | Opt.Options.iter_callback = callback.iter_callback 158 | 159 | xinit = 10 160 | p = 4 161 | Opt.x.initializer = xinit 162 | Opt(p=p) 163 | 164 | assert callback.parameter is None 165 | assert callback.count > 0 166 | assert callback.objectives[0] == xinit**2 + p 167 | assert callback.objectives[-1] == p 168 | assert len(callback.instances) == callback.count 169 | assert isinstance(callback.instances[-1], Opt) 170 | 171 | # add init callback 172 | Opt.Options.init_callback = callback.init_callback 173 | Opt(p=p) 174 | assert callback.parameter is not None 175 | -------------------------------------------------------------------------------- /tests/test_partial.py: -------------------------------------------------------------------------------- 1 | """Exercise wrapping different systems overriding some inputs with constants""" 2 | 3 | import condor 4 | 5 | 6 | def test_embedded_explicit(): 7 | class EmbSys(condor.ExplicitSystem): 8 | a = input() 9 | b = input() 10 | output.c = a + b 11 | 12 | class WrapSys(condor.ExplicitSystem): 13 | a = input() 14 | out = EmbSys(a=a, b=4) 15 | output.c = out.c 16 | 17 | assert WrapSys(a=3).c == 7 18 | 19 | 20 | def test_embedded_algebraic(): 21 | class EmbSys(condor.AlgebraicSystem): 22 | a = parameter() 23 | b = variable() 24 | residual(a**2 == b) 25 | 26 | assert EmbSys(a=4).b == 16 27 | 28 | class WrapSys(condor.ExplicitSystem): 29 | out = EmbSys(a=3) 30 | output.b = out.b 31 | 32 | assert WrapSys().b == 9 33 | 34 | 35 | def test_embedded_optimization(): 36 | class EmbSys(condor.OptimizationProblem): 37 | a = parameter() 38 | b = variable() 39 | objective = (a**2 - b) ** 2 40 | 41 | class Options: 42 | print_level = 0 43 | 44 | assert EmbSys(a=4).b == 16 45 | 46 | class WrapSys(condor.ExplicitSystem): 47 | out = EmbSys(a=3) 48 | output.b = out.b 49 | 50 | assert WrapSys().b == 9 51 | -------------------------------------------------------------------------------- /tests/test_placeholders.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import condor as co 4 | 5 | # TODO test as_template 6 | 7 | 8 | class ComponentRaw(co.models.ModelTemplate): 9 | input = co.FreeField(co.Direction.input) 10 | output = co.AssignedField(co.Direction.output) 11 | 12 | x = placeholder(default=2.0) 13 | y = placeholder(default=1.0) 14 | 15 | output.z = x**2 + y 16 | 17 | 18 | class ComponentImplementation(co.implementations.ExplicitSystem): 19 | pass 20 | 21 | 22 | co.implementations.ComponentRaw = ComponentImplementation 23 | 24 | 25 | class ComponentAsTemplate(co.ExplicitSystem, as_template=True): 26 | x = placeholder(default=2.0) 27 | y = placeholder(default=1.0) 28 | 29 | output.z = x**2 + y 30 | 31 | 32 | @pytest.mark.parametrize("component", [ComponentRaw, ComponentAsTemplate]) 33 | class TestPlaceholders: 34 | def test_default_impl(self, component): 35 | class MyComp0(component): 36 | pass 37 | 38 | assert MyComp0().z == 5 39 | 40 | def test_new_io(self, component): 41 | class MyComp5(component): 42 | u = input() 43 | output.v = u**2 + 2 * u + 1 44 | 45 | out = MyComp5(3.0) 46 | assert out.z == 5 47 | assert out.v == 16 48 | 49 | def test_use_placeholders(self, component): 50 | class MyComp1(component): 51 | x = input() 52 | y = input() 53 | 54 | out = MyComp1(x=2.0, y=3.0) 55 | assert out.z == 7 56 | 57 | def test_partial_placeholder(self, component): 58 | class MyComp2(component): 59 | x = input() 60 | y = 3.0 61 | 62 | assert MyComp2(x=1.0).z == 1**2 + MyComp2.y 63 | 64 | # TODO currently this works, but probably shouldn't 65 | # keyword args x=..., y=... does fail 66 | assert MyComp2(3.0, 4.0).z == 3**2 + 3 67 | 68 | def test_override_placeholders(self, component): 69 | class MyComp3(component): 70 | x = 3.0 71 | y = 4.0 72 | 73 | assert MyComp3().z == 3**2 + 4 74 | 75 | def test_computed_placeholder(self, component): 76 | class MyComp4(component): 77 | u = input() 78 | x = u**0.5 79 | y = 0 80 | 81 | output.v = x + 5 82 | 83 | out = MyComp4(4.0) 84 | assert out.v == 4**0.5 + 5 85 | assert out.z == (4**0.5) ** 2 + 0 86 | -------------------------------------------------------------------------------- /tests/test_trajectory_analysis.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pytest 3 | from scipy import linalg 4 | 5 | import condor as co 6 | 7 | 8 | def test_ct_lqr(): 9 | # continuous-time LQR 10 | dblint_a = np.array([[0, 1], [0, 0]]) 11 | dblint_b = np.array([[0], [1]]) 12 | 13 | DblInt = co.LTI(a=dblint_a, b=dblint_b, name="DblInt") # noqa: N806 14 | 15 | class DblIntLQR(DblInt.TrajectoryAnalysis): 16 | initial[x] = [1.0, 0.1] 17 | q = np.eye(2) 18 | r = np.eye(1) 19 | tf = 32.0 20 | u = dynamic_output.u 21 | cost = trajectory_output(integrand=(x.T @ q @ x + u.T @ r @ u) / 2) 22 | 23 | class Options: 24 | state_rtol = 1e-8 25 | adjoint_rtol = 1e-8 26 | 27 | class CtOptLQR(co.OptimizationProblem): 28 | k = variable(shape=DblIntLQR.k.shape) 29 | sim = DblIntLQR(k) 30 | objective = sim.cost 31 | 32 | class Options: 33 | exact_hessian = False 34 | __implementation__ = co.implementations.ScipyCG 35 | 36 | lqr_sol = CtOptLQR() 37 | 38 | s = linalg.solve_continuous_are(dblint_a, dblint_b, DblIntLQR.q, DblIntLQR.r) 39 | k = linalg.solve(DblIntLQR.r, dblint_b.T @ s) 40 | 41 | lqr_are = DblIntLQR(k) 42 | 43 | # causes an AttributeError, I guess becuase the Jacobian hasn't been requested? 44 | # jac_callback = lqr_are.implementation.callback.jac_callback 45 | # jac_callback(k, [0]) 46 | 47 | assert lqr_sol._stats.success 48 | np.testing.assert_allclose(lqr_are.cost, lqr_sol.objective) 49 | np.testing.assert_allclose(k, lqr_sol.k, rtol=1e-4) 50 | 51 | 52 | @pytest.mark.skip(reason="Need to fix LTI function") 53 | def test_sp_lqr(): 54 | # sampled LQR 55 | dblint_a = np.array([[0, 1], [0, 0]]) 56 | dblint_b = np.array([[0], [1]]) 57 | dt = 0.5 58 | 59 | DblIntSampled = co.LTI( # noqa: N806 60 | a=dblint_a, b=dblint_b, name="DblIntSampled", dt=dt 61 | ) 62 | 63 | class DblIntSampledLQR(DblIntSampled.TrajectoryAnalysis): 64 | initial[x] = [1.0, 0.1] 65 | # initial[u] = -k@initial[x] 66 | q = np.eye(2) 67 | r = np.eye(1) 68 | tf = 32.0 # 12 iters, 21 calls 1E-8 jac 69 | # tf = 16. # 9 iters, 20 calls, 1E-7 70 | cost = trajectory_output(integrand=(x.T @ q @ x + u.T @ r @ u) / 2) 71 | 72 | class Casadi(co.Options): 73 | adjoint_adaptive_max_step_size = False 74 | state_max_step_size = dt / 8 75 | adjoint_max_step_size = dt / 8 76 | 77 | class SampledOptLQR(co.OptimizationProblem): 78 | k = variable(shape=DblIntSampledLQR.k.shape) 79 | sim = DblIntSampledLQR(k) 80 | objective = sim.cost 81 | 82 | class Casadi(co.Options): 83 | exact_hessian = False 84 | 85 | # sim = DblIntSampledLQR([1.00842737, 0.05634044]) 86 | 87 | sim = DblIntSampledLQR([0.0, 0.0]) 88 | sim.implementation.callback.jac_callback(sim.implementation.callback.p, []) 89 | 90 | lqr_sol_samp = SampledOptLQR() 91 | 92 | # sampled_sim = DblIntSampledLQR([0., 0.]) 93 | # sampled_sim.implementation.callback.jac_callback([0., 0.,], [0.]) 94 | 95 | q = DblIntSampledLQR.q 96 | r = DblIntSampledLQR.r 97 | a = dblint_a 98 | b = dblint_b 99 | 100 | ad, bd = signal.cont2discrete((a, b, None, None), dt)[:2] 101 | s = linalg.solve_discrete_are( 102 | ad, 103 | bd, 104 | q, 105 | r, 106 | ) 107 | k = linalg.solve(bd.T @ s @ bd + r, bd.T @ s @ ad) 108 | 109 | # sim = DblIntSampledLQR([1.00842737, 0.05634044]) 110 | sim = DblIntSampledLQR(k) 111 | 112 | sim.implementation.callback.jac_callback(sim.implementation.callback.p, []) 113 | LTI_plot(sim) 114 | plt.show() 115 | 116 | # sim = DblIntSampledLQR([0., 0.]) 117 | 118 | # sampled_sim = DblIntSampledLQR([0., 0.]) 119 | # sampled_sim.implementation.callback.jac_callback([0., 0.,], [0.]) 120 | 121 | sampled_sim = DblIntSampledLQR(k) 122 | jac_cb = sampled_sim.implementation.callback.jac_callback 123 | jac_cb(k, [0.0]) 124 | 125 | assert lqr_sol_samp._stats.success 126 | print(lqr_sol_samp._stats) 127 | print(lqr_sol_samp.objective < sampled_sim.cost) 128 | print(lqr_sol_samp.objective, sampled_sim.cost) 129 | print(" ARE sol:", k, "\niterative sol:", lqr_sol_samp.k) 130 | 131 | 132 | def test_time_switched(): 133 | # optimal transfer time with time-based events 134 | 135 | class DblInt(co.ODESystem): 136 | a = np.array([[0, 1], [0, 0]]) 137 | b = np.array([[0], [1]]) 138 | 139 | x = state(shape=a.shape[0]) 140 | mode = state() 141 | pos_at_switch = state() 142 | 143 | t1 = parameter() 144 | t2 = parameter() 145 | u = modal() 146 | 147 | dot[x] = a @ x + b * u 148 | 149 | class Accel(DblInt.Mode): 150 | condition = mode == 0 151 | action[u] = 1.0 152 | 153 | class Switch(DblInt.Event): 154 | at_time = t1 155 | update[mode] = 1 156 | # TODO should it be possible to add a state here? 157 | # pos_at_switch = state() 158 | update[pos_at_switch] = x[0] 159 | 160 | class Decel(DblInt.Mode): 161 | condition = mode == 1 162 | action[u] = -1.0 163 | 164 | class Terminate(DblInt.Event): 165 | at_time = t2 + t1 166 | terminate = True 167 | 168 | class Transfer(DblInt.TrajectoryAnalysis): 169 | initial[x] = [-9.0, 0.0] 170 | q = np.eye(2) 171 | cost = trajectory_output((x.T @ q @ x) / 2) 172 | 173 | class Casadi(co.Options): 174 | state_adaptive_max_step_size = 4 175 | 176 | class MinimumTime(co.OptimizationProblem): 177 | t1 = variable(lower_bound=0) 178 | t2 = variable(lower_bound=0) 179 | transfer = Transfer(t1, t2) 180 | objective = transfer.cost 181 | 182 | class Options: 183 | exact_hessian = False 184 | __implementation__ = co.implementations.ScipyCG 185 | 186 | MinimumTime.set_initial(t1=2.163165480675697, t2=4.361971866705403) 187 | opt = MinimumTime() 188 | 189 | assert opt._stats.success 190 | np.testing.assert_allclose(opt.t1, 3.0, rtol=1e-5) 191 | np.testing.assert_allclose(opt.t2, 3.0, rtol=1e-5) 192 | 193 | class AccelerateTransfer(DblInt.TrajectoryAnalysis, exclude_events=[Switch]): 194 | initial[x] = [-9.0, 0.0] 195 | q = np.eye(2) 196 | cost = trajectory_output((x.T @ q @ x) / 2) 197 | 198 | class Casadi(co.Options): 199 | state_adaptive_max_step_size = 4 200 | 201 | sim_accel = AccelerateTransfer(**opt.transfer.parameter.asdict()) 202 | 203 | assert ( 204 | sim_accel._res.e[0].rootsfound.size 205 | == opt.transfer._res.e[0].rootsfound.size - 1 206 | ) 207 | 208 | 209 | def test_state_switched(): 210 | # optimal transfer time with state-based events 211 | 212 | class DblInt(co.ODESystem): 213 | a = np.array([[0, 1], [0, 0]]) 214 | b = np.array([[0], [1]]) 215 | 216 | x = state(shape=a.shape[0]) 217 | 218 | mode = state() 219 | 220 | p1 = parameter() 221 | p2 = parameter() 222 | 223 | u = modal() 224 | 225 | dot[x] = a @ x + b * u 226 | 227 | class Accel(DblInt.Mode): 228 | condition = mode == 0 229 | action[u] = 1.0 230 | 231 | class Switch(DblInt.Event): 232 | function = x[0] - p1 233 | update[mode] = 1 234 | 235 | class Decel(DblInt.Mode): 236 | condition = mode == 1 237 | action[u] = -1.0 238 | 239 | class Terminate(DblInt.Event): 240 | function = x[0] - p2 241 | terminate = True 242 | 243 | class Transfer(DblInt.TrajectoryAnalysis): 244 | initial[x] = [-9.0, 0.0] 245 | xd = [1.0, 2.0] 246 | q = np.eye(2) 247 | cost = trajectory_output(((x - xd).T @ (x - xd)) / 2) 248 | tf = 20.0 249 | 250 | class Options: 251 | state_max_step_size = 0.25 252 | state_atol = 1e-15 253 | state_rtol = 1e-12 254 | adjoint_atol = 1e-15 255 | adjoint_rtol = 1e-12 256 | 257 | class MinimumTime(co.OptimizationProblem): 258 | p1 = variable() 259 | p2 = variable() 260 | sim = Transfer(p1, p2) 261 | objective = sim.cost 262 | 263 | class Options: 264 | exact_hessian = False 265 | __implementation__ = co.implementations.ScipyCG 266 | 267 | MinimumTime.set_initial(p1=-4, p2=-1) 268 | 269 | opt = MinimumTime() 270 | 271 | assert opt._stats.success 272 | np.testing.assert_allclose(opt.p1, -3, rtol=1e-5) 273 | np.testing.assert_allclose(opt.p2, 1, rtol=1e-5) 274 | 275 | 276 | @pytest.fixture 277 | def odesys(): 278 | class MassSpring(co.ODESystem): 279 | x = state() 280 | v = state() 281 | wn = parameter() 282 | u = modal() 283 | dot[x] = v 284 | dot[v] = u - wn**2 * x 285 | initial[x] = 1 286 | 287 | return MassSpring 288 | 289 | 290 | def test_event_state_to_mode(odesys): 291 | # verify you can reference a state created in an event from a mode 292 | 293 | class Event(odesys.Event): 294 | function = v 295 | count = state(name="count_") 296 | update[count] = count + 1 297 | 298 | class Mode(odesys.Mode): 299 | condition = Event.count > 0 300 | action[u] = 1 301 | 302 | class Sim(odesys.TrajectoryAnalysis): 303 | total_count = trajectory_output(Event.count) 304 | tf = 10 305 | 306 | print(Sim(wn=10).total_count) 307 | 308 | 309 | def test_mode_param_to_mode(odesys): 310 | # verify you can reference a parameter created in a mode in another mode 311 | 312 | class ModeA(odesys.Mode): 313 | condition = v > 0 314 | u_hold = parameter() 315 | action[u] = u_hold 316 | 317 | class ModeB(odesys.Mode): 318 | condition = 1 319 | action[u] = ModeA.u_hold 320 | 321 | class Sim(odesys.TrajectoryAnalysis): 322 | tf = 10 323 | 324 | Sim(wn=10, u_hold=0.8) 325 | --------------------------------------------------------------------------------