File size: 4,512 Bytes
89f83d9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import marimo

__generated_with = "0.11.0"
app = marimo.App()


@app.cell
def _():
    import marimo as mo
    return (mo,)


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        r"""
        # Minimal fuel optimal control

        This notebook includes an application of linear programming to controlling a
        physical system, adapted from [Convex
        Optimization](https://web.stanford.edu/~boyd/cvxbook/) by Boyd and Vandenberghe.

        We consider a linear dynamical system with state $x(t) \in \mathbf{R}^n$, for $t = 0, \ldots, T$. At each time step $t = 0, \ldots, T - 1$, an actuator or input signal $u(t)$ is applied, affecting the state. The dynamics
        of the system is given by the linear recurrence

        \[
            x(t + 1) = Ax(t) + bu(t), \quad t = 0, \ldots, T - 1,
        \]

        where $A \in \mathbf{R}^{n \times n}$ and $b \in \mathbf{R}^n$ are given and encode how the system evolves. The initial state $x(0)$ is also given.

        The _minimum fuel optimal control problem_ is to choose the inputs $u(0), \ldots, u(T - 1)$ so as to achieve
        a given desired state $x_\text{des} = x(T)$ while minimizing the total fuel consumed

        \[
        F = \sum_{t=0}^{T - 1} f(u(t)).
        \]

        The function $f : \mathbf{R} \to \mathbf{R}$ tells us how much fuel is consumed as a function of the input, and is given by

        \[
            f(a) = \begin{cases}
            |a| & |a| \leq 1 \\
            2|a| - 1 & |a| > 1.
            \end{cases}
        \]

        This means the fuel use is proportional to the magnitude of the signal between $-1$ and $1$, but for larger signals the marginal fuel efficiency is half.

        **This notebook.** In this notebook we use CVXPY to formulate the minimum fuel optimal control problem as a linear program. The notebook lets you play with the initial and target states, letting you see how they affect the planned trajectory of inputs $u$.

        First, we create the **problem data**.
        """
    )
    return


@app.cell
def _():
    import numpy as np
    return (np,)


@app.cell
def _():
    n, T = 3, 30
    return T, n


@app.cell
def _(np):
    A = np.array([[-1, 0.4, 0.8], [1, 0, 0], [0, 1, 0]])
    b = np.array([[1, 0, 0.3]]).T
    return A, b


@app.cell(hide_code=True)
def _(mo, n, np):
    import wigglystuff

    x0_widget = mo.ui.anywidget(wigglystuff.Matrix(np.zeros((1, n))))
    xdes_widget = mo.ui.anywidget(wigglystuff.Matrix(np.array([[7, 2, -6]])))

    _a = mo.md(
        rf"""

        Choose a value for $x_0$ ...
        
        {x0_widget}
        """
    )

    _b = mo.md(
        rf"""
        ... and for $x_\text{{des}}$

        {xdes_widget}
        """
    )

    mo.hstack([_a, _b], justify="space-around")
    return wigglystuff, x0_widget, xdes_widget


@app.cell
def _(x0_widget, xdes_widget):
    x0 = x0_widget.matrix
    xdes = xdes_widget.matrix
    return x0, xdes


@app.cell(hide_code=True)
def _(mo):
    mo.md(r"""**Next, we specify the problem as a linear program using CVXPY.** This problem is linear because the objective and constraints are affine. (In fact, the objective is piecewise affine, but CVXPY rewrites it to be affine for you.)""")
    return


@app.cell
def _():
    import cvxpy as cp
    return (cp,)


@app.cell
def _(A, T, b, cp, mo, n, x0, xdes):
    X, u = cp.Variable(shape=(n, T + 1)), cp.Variable(shape=(1, T))

    objective = cp.sum(cp.maximum(cp.abs(u), 2 * cp.abs(u) - 1))
    constraints = [
        X[:, 1:] == A @ X[:, :-1] + b @ u,
        X[:, 0] == x0,
        X[:, -1] == xdes,
    ]

    fuel_used = cp.Problem(cp.Minimize(objective), constraints).solve()
    mo.md(f"Achieved a fuel usage of {fuel_used:.02f}. 🚀")
    return X, constraints, fuel_used, objective, u


@app.cell(hide_code=True)
def _(mo):
    mo.md(
        """
        Finally, we plot the chosen inputs over time.

        **🌊 Try it!** Change the initial and desired states; how do fuel usage and controls change? Can you explain what you see? You can also try experimenting with the value of $T$.
        """
    )
    return


@app.cell
def _(plot_solution, u):
    plot_solution(u)
    return


@app.cell
def _(T, cp, np):
    def plot_solution(u: cp.Variable):
        import matplotlib.pyplot as plt

        plt.step(np.arange(T), u.T.value)
        plt.axis("tight")
        plt.xlabel("$t$")
        plt.ylabel("$u$")
        return plt.gca()
    return (plot_solution,)


if __name__ == "__main__":
    app.run()