├── .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 |
--------------------------------------------------------------------------------