.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "tutorial/trajectory.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end <sphx_glr_download_tutorial_trajectory.py>` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_tutorial_trajectory.py: ========================= Working with trajectories ========================= .. GENERATED FROM PYTHON SOURCE LINES 8-42 Condor is not intended to be an optimal control library per se, but we often end up working with trajectories a great deal and spent considerable effort to make modeling dynamical systems nice. Glider Model ------------ For this tutorial, we will consider a simplified model of a glider with some form of angle-of-attack control. We can represent this as a system of ordinary differential equations (ODEs) given by .. math:: \begin{align} \dot{r} &= v \cos \gamma \\ \dot{h} &= v \sin \gamma \\ \dot{\gamma} &= (CL(\alpha) \cdot v^2 - g \cos \gamma) / v \\ \dot{v} &= - CD(\alpha) \cdot v^2 - g \sin \gamma \\ \end{align} where :math:`r` is the range, or horizontal position, :math:`h` is the altitude, or vertical position, :math:`v` is the velocity, :math:`\gamma` is the flight-path angle, and :math:`\alpha` is the angle-of-attack, which modulates the coefficients of lift, :math:`CL`, and drag, :math:`CD`, and :math:`g` is the acceleration due to gravity. Simple models of the lift and drag are given by .. math:: \begin{align} CL(\alpha) &= CL_{\alpha} \cdot \alpha \\ CD(\alpha) &= CD_0 + CD_{i,q} \cdot CL^2 \\ \end{align} where :math:`CL_{\alpha}` is the lift slope, :math:`CD_0` is the 0-lift drag, and :math:`CD_{i,q}` is the quadratic coefficient for the lift-induced drag. In Condor, we can implement this as, .. GENERATED FROM PYTHON SOURCE LINES 42-74 .. code-block:: Python import condor from condor.backend import operators as ops class Glider(condor.ODESystem): r = state() h = state() gamma = state() v = state() alpha = modal() CL_alpha = parameter() CD_0 = parameter() CD_i_q = parameter() g = parameter() CL = CL_alpha * alpha CD = CD_0 + CD_i_q * CL**2 dot[r] = v * ops.cos(gamma) dot[h] = v * ops.sin(gamma) dot[gamma] = (CL * v**2 - g * ops.cos(gamma)) / v dot[v] = -CD * v**2 - g * ops.sin(gamma) initial[r] = 0.0 initial[h] = 1.0 initial[v] = 15.0 initial[gamma] = 30 * ops.pi / 180.0 .. GENERATED FROM PYTHON SOURCE LINES 75-84 The :attr:`modal` field is used to define elements with deferred and possibly varying behavior, so we use this for the angle-of-attack so we can simulate multiple behaviors. To simulate this model, we create a :class:`~condor.contrib.TrajectoryAnalysis`, a sub-model to an ODE Sysstem, which is ultimately responsible for defining the specifics of integrating the ODE. In this example, the :class:`TrajectoryAnalysis` model only specifies the final simulation time of the model. It is more mathematically consistent to have the initial values defined in the trajectory analysis, but for convenience we declared it as part of the ODE system. .. GENERATED FROM PYTHON SOURCE LINES 84-90 .. code-block:: Python class FirstSim(Glider.TrajectoryAnalysis): tf = 20.0 .. GENERATED FROM PYTHON SOURCE LINES 91-95 The fields of the original :class:`Glider` simulation are copied to the :class:`TrajectoryAnalysis` so the parameter values must be supplied to evaluate the model numerically. .. GENERATED FROM PYTHON SOURCE LINES 95-99 .. code-block:: Python A = 3e-1 first_sim = FirstSim(CL_alpha=0.11 * A, CD_0=0.05 * A, CD_i_q=0.05, g=1.0) .. GENERATED FROM PYTHON SOURCE LINES 100-103 In addition to binding static parameters like the other built-in models, the time-histories for the :attr:`state` and :attr:`dynamic_output` are bound and can be accessed for plotting. For example, we can plot time histories .. GENERATED FROM PYTHON SOURCE LINES 103-115 .. code-block:: Python from matplotlib import pyplot as plt state_data = first_sim.state.asdict() fig, axs = plt.subplots(nrows=len(state_data), constrained_layout=True, sharex=True) for ax, (state_name, state_hist) in zip(axs, state_data.items()): ax.plot(first_sim.t, state_hist) ax.set_ylabel(state_name) ax.grid(True) ax.set_xlabel("t") .. image-sg:: /tutorial/images/sphx_glr_trajectory_001.png :alt: trajectory :srcset: /tutorial/images/sphx_glr_trajectory_001.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Text(0.5, 18.167, 't') .. GENERATED FROM PYTHON SOURCE LINES 116-117 and the flight path .. GENERATED FROM PYTHON SOURCE LINES 117-139 .. code-block:: Python import numpy as np def flight_path_plot(sims, **plot_kwargs): fig, ax = plt.subplots(constrained_layout=True, figsize=(6.4, 3.2)) plt.ylabel("altitude") plt.xlabel("range") # reverse zorders to show progression of sims more nicely zorders = np.linspace(2.1, 2.5, len(sims))[::-1] marker = plot_kwargs.pop("marker", "o") for sim, zorder in zip(sims, zorders): ax.plot(sim.r, sim.h, marker=marker, zorder=zorder, **plot_kwargs) plt.grid(True) ax.set_aspect("equal") ax.set_ylim(-3, 30) ax.set_xlim(-3, 90) return ax flight_path_plot([first_sim]) .. image-sg:: /tutorial/images/sphx_glr_trajectory_002.png :alt: trajectory :srcset: /tutorial/images/sphx_glr_trajectory_002.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none <Axes: xlabel='range', ylabel='altitude'> .. GENERATED FROM PYTHON SOURCE LINES 140-146 Modeling the Ground with an Event --------------------------------- Notice the glider eventually flies straight through the ground. We can fix that with an :class:`Event` sub-model that detects the altitude zero-crossing and flips the descent to an ascent with a simple parametrized loss model. .. GENERATED FROM PYTHON SOURCE LINES 146-155 .. code-block:: Python class Bounce(Glider.Event): function = h update[gamma] = -gamma mu = parameter() update[v] = mu * v .. GENERATED FROM PYTHON SOURCE LINES 156-159 We still need to create a new :class:`TrajectoryAnalysis` since the :class:`FirstSim` was bound to the :class:`Glider` model at the time of creation (without the bounce event). .. GENERATED FROM PYTHON SOURCE LINES 159-170 .. code-block:: Python class BounceSim(Glider.TrajectoryAnalysis): tf = 20.0 bounce_sim = BounceSim(**first_sim.parameter.asdict(), mu=0.9) flight_path_plot([first_sim, bounce_sim]) plt.legend(["original sim", "with bounce"]) .. image-sg:: /tutorial/images/sphx_glr_trajectory_003.png :alt: trajectory :srcset: /tutorial/images/sphx_glr_trajectory_003.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none <matplotlib.legend.Legend object at 0x7f65b59051f0> .. GENERATED FROM PYTHON SOURCE LINES 171-181 Angle of Attack Control with a Mode ----------------------------------- We can also add a behavior for the angle of attack using a mode, in this case holding a constant angle of attack after reaching peak altitude to reduce rate of descent. To ensure proper numerical behavior, we follow [orbital ref] and use an accumulator state to encode the flight controller logic. In this case, we create an event to detect the switch from ascent to descent and perform a state update. .. GENERATED FROM PYTHON SOURCE LINES 181-189 .. code-block:: Python class MaxAlt(Glider.Event): function = gamma max_alt = state() update[max_alt] = h .. GENERATED FROM PYTHON SOURCE LINES 190-192 The mode can now be triggered by the accumulator state update, where we set :math:`\alpha` to a new constant parameter. .. GENERATED FROM PYTHON SOURCE LINES 192-200 .. code-block:: Python class DescentAlphaHold(Glider.Mode): condition = max_alt > 0 hold_alpha = parameter() action[alpha] = hold_alpha .. GENERATED FROM PYTHON SOURCE LINES 201-202 The glider now travels a little further with this control behavior. .. GENERATED FROM PYTHON SOURCE LINES 202-214 .. code-block:: Python class AlphaSim(Glider.TrajectoryAnalysis): tf = 20.0 alpha_sim = AlphaSim(**bounce_sim.parameter.asdict(), hold_alpha=0.5) ax = flight_path_plot([first_sim, bounce_sim, alpha_sim]) ax.legend(["original sim", "with bounce", "gradual descent"]) .. image-sg:: /tutorial/images/sphx_glr_trajectory_004.png :alt: trajectory :srcset: /tutorial/images/sphx_glr_trajectory_004.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none <matplotlib.legend.Legend object at 0x7f65b5997a40> .. GENERATED FROM PYTHON SOURCE LINES 215-234 Trajectory Outputs ------------------ So far, we have only used the :class:`TrajectoryAnalysis` to simulate the ODE System. In order to use the ODE system as part of other condor models, we must declare :attr:`trajectory_output`. Condor computes the gradient of the :attr:`trajectory_output` using the Sweeping Gradient method. Each trajectory output has the form .. math:: J = \phi\left(t_{f},x\left(t_{f}\right),p\right)+\int_{t_{0}}^{t_{f}}L\left(\tau,x\left(\tau\right),p\right)\,d\tau where :math:`\phi` is the terminal term, :math:`L\left(\cdot\right)` is the integrand term, and :math:`x\left(t\right)` is the solution to the system of ODEs with events and modes. First we'll change the control behavior to use a constant angle of attack through the whole trajectory to get the peak altitude to vary. We'll also make the bounce event terminal since we're interested in the flown range. .. GENERATED FROM PYTHON SOURCE LINES 234-243 .. code-block:: Python class ConstantAlphaHold(Glider.Mode): condition = 1 action[alpha] = 1 * DescentAlphaHold.hold_alpha Bounce.terminate = True .. GENERATED FROM PYTHON SOURCE LINES 244-247 We can form the area under the flight-path curve by taking the derivative :math:`\dot{r}` and using it to form the integrand. We can also just take the final max altitude state we added with the ``MaxAlt`` event and the final range. .. GENERATED FROM PYTHON SOURCE LINES 247-267 .. code-block:: Python class AlphaSim(Glider.TrajectoryAnalysis): initial[r] = 0.0 initial[h] = 1.0 initial[v] = 15.0 initial[gamma] = 30 * ops.pi / 180.0 tf = 100.0 area = trajectory_output(integrand=dot[r] * h) max_h = trajectory_output(max_alt) max_r = trajectory_output(r) class Options: state_rtol = 1e-12 state_atol = 1e-15 adjoint_rtol = 1e-12 adjoint_atol = 1e-15 .. GENERATED FROM PYTHON SOURCE LINES 268-269 Then we can compare areas with different hold angles of attack: .. GENERATED FROM PYTHON SOURCE LINES 269-279 .. code-block:: Python params = bounce_sim.parameter.asdict() results = { "alpha = +0.5 deg": AlphaSim(**params, hold_alpha=0.5), "alpha = 0.0 deg": AlphaSim(**params, hold_alpha=0.0), "alpha = -0.5 deg": AlphaSim(**params, hold_alpha=-0.5), } print(*[f"{k}: {v.area}" for k, v in results.items()], sep="\n") .. rst-class:: sphx-glr-script-out .. code-block:: none alpha = +0.5 deg: [1459.42002699] alpha = 0.0 deg: [825.36407495] alpha = -0.5 deg: [214.23621474] .. GENERATED FROM PYTHON SOURCE LINES 281-286 .. code-block:: Python ax = flight_path_plot(results.values()) ax.legend([k.replace("alpha", r"$\alpha$") for k in results]) .. image-sg:: /tutorial/images/sphx_glr_trajectory_005.png :alt: trajectory :srcset: /tutorial/images/sphx_glr_trajectory_005.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none <matplotlib.legend.Legend object at 0x7f65b4758710> .. GENERATED FROM PYTHON SOURCE LINES 287-294 Embedding --------- With several :attr:`trajectory_output` elements declared, we can embed the trajectory within other Condor models, for example to maximize a combination of the peak height and flown range. .. GENERATED FROM PYTHON SOURCE LINES 294-323 .. code-block:: Python class GlideOpt(condor.OptimizationProblem): alpha = variable( initializer=0.001, lower_bound=-1.0, upper_bound=1, warm_start=False, ) sim = AlphaSim(**bounce_sim.parameter.asdict(), hold_alpha=alpha) trade_off = parameter() objective = -(trade_off * sim.max_h + (1 - trade_off) * sim.max_r) class Options: exact_hessian = False print_level = 0 tol = 1e-3 max_iter = 8 opt_range = GlideOpt(trade_off=0) ax = flight_path_plot([opt_range.sim]) ax.text( *(0.05, 0.92), f"max range: {opt_range.sim.max_r} ($\\alpha={opt_range.alpha}$", transform=ax.transAxes, ) .. image-sg:: /tutorial/images/sphx_glr_trajectory_006.png :alt: trajectory :srcset: /tutorial/images/sphx_glr_trajectory_006.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none Text(0.05, 0.92, 'max range: [81.82500377] ($\\alpha=[0.22725794]$') .. GENERATED FROM PYTHON SOURCE LINES 324-330 We can store the iteration history using :attr:`iter_callback` option on :class:`OptimizationProblem`, pointing it to the method of a class to store the simulation data on each call. Since it only gets information relevant to the optimization problem itself, we use :meth:`~condor.contrib.OptimizationProblem.from_values` to reconstruct the internals of the analysis with the simulation outputs bound to the ``sim`` attribute. .. GENERATED FROM PYTHON SOURCE LINES 330-358 .. code-block:: Python class IterStore: def __init__(self): self.parameter = None def init_callback(self, parameter, impl_opts): self.parameter = parameter self.iters = [] def iter_callback(self, i, variable, objective, constraint): iter_opt_res = GlideOpt.from_values( **variable.asdict(), **self.parameter.asdict(), ) self.iters.append(iter_opt_res) hist = IterStore() GlideOpt.Options.init_callback = hist.init_callback GlideOpt.Options.iter_callback = hist.iter_callback opt_alt = GlideOpt(trade_off=1) ax = flight_path_plot([it.sim for it in hist.iters], marker=None) ax.legend([f"iter {idx}, alpha={sim.alpha}" for idx, sim in enumerate(hist.iters)]) ax.set_title("max altitude iterations") ax.set_ylim(-3, 45) .. image-sg:: /tutorial/images/sphx_glr_trajectory_007.png :alt: max altitude iterations :srcset: /tutorial/images/sphx_glr_trajectory_007.png :class: sphx-glr-single-img .. rst-class:: sphx-glr-script-out .. code-block:: none (-3.0, 45.0) .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 28.553 seconds) .. _sphx_glr_download_tutorial_trajectory.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: trajectory.ipynb <trajectory.ipynb>` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: trajectory.py <trajectory.py>` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: trajectory.zip <trajectory.zip>` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery <https://sphinx-gallery.github.io>`_