# Introduction to reproducibility and power issues

## Some Definitions

* $H_0$ : null hypothesis: The hypotheis that the effect we are testing for is null

* $H_A$ : alternative hypothesis : Not $H_0$, so there is some signal

* $T$ : The random variable that takes value "significant" or "not significant"

* $T_S$ : Value of T when test is significant (eg $T = T_S$)

* $T_N$ : Value of T when test is not significant (eg $T = T_N$)

* $\alpha$ : false positive rate - probability to reject $H_0$ when $H_0$ is true (therefore $H_A$ is false)

* $\beta$ : false negative rate - probability to accept $H_0$ when $H_A$ is true (i.e. $H_0$ is false)

power = $1-\beta$

where $\beta$ is the risk of *false negative*

So, to compute power, *we need to know what is the risk of false negative*, ie, the risk to not show a significant effect while we have some signal (null is false).

```python
from __future__ import division
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import scipy.stats as sst
```

```python
from sympy import symbols, Eq, solve, simplify, lambdify, init_printing, latex
init_printing(use_latex=True, order='old')
```

```python
from IPython.display import HTML
# Code to make HTML for a probability table
def association_table(assocs, title):
    latexed = {'title': title}
    for key, value in assocs.items():
        latexed[key] = latex(value)
    latexed['s_total'] = latex(assocs['t_s'] + assocs['f_s'])
    latexed['ns_total'] = latex(assocs['t_ns'] + assocs['f_ns'])
    return """<h3>{title}</h3>
              <TABLE><TR><TH>$H/T$<TH>$T_S$<TH>$T_N$
              <TR><TH>$H_A$<TD>${t_s}$<TD>${t_ns}$
              <TR><TH>$H_0$<TD>${f_s}$<TD>${f_ns}$
              <TR><TH>Total<TD>${s_total}$<TD>${ns_total}$
              </TABLE>""".format(**latexed)
```

```python
from sympy.abc import alpha, beta # get alpha, beta symbolic variables
assoc = dict(t_s = 1 - beta, # H_A true, test significant = true positives
             t_ns = beta, # true, not significant = false negatives
             f_s = alpha, # false, significant = false positives
             f_ns = 1 - alpha) # false, not sigificant = true negatives
HTML(association_table(assoc, 'Not considering prior'))
```

Not considering prior

$H/T$$T_S$$T_N$

$H_A$$- \beta + 1$$\beta$

$H_0$$\alpha$$- \alpha + 1$

Total$\alpha - \beta + 1$$- \alpha + \beta + 1$

## How do we compute power ?

### What is the effect ?

$$\hspace{3cm}\mu = \mu_1 - \mu_2$$

### What is the standardized effect ? (eg Cohen's d)

$$\hspace{3cm}d = \frac{\mu_1 - \mu_2}{\sigma} = \frac{\mu}{\sigma}$$

### "Z" : Effect accounting for the sample size

$$\hspace{3cm}Z = \frac{\mu}{\sigma / \sqrt{n}}$$

### Cohen's d value:

```python
# print some cohen values
# %pylab inline
muse = (.05, .1,.2,.3,.4,.5);
sigmas = np.linspace(1.,.5,len(muse))
cohenstr = ["For sigma = %3.2f and m = %3.2f Cohen d = %3.2f" %(sig,mu,coh)
       for (sig,mu,coh) in zip(sigmas,muse, np.asarray(muse)/sigmas)]
for s in cohenstr:
    print(s)
```

    For sigma = 1.00 and m = 0.05 Cohen d = 0.05
    For sigma = 0.90 and m = 0.10 Cohen d = 0.11
    For sigma = 0.80 and m = 0.20 Cohen d = 0.25
    For sigma = 0.70 and m = 0.30 Cohen d = 0.43
    For sigma = 0.60 and m = 0.40 Cohen d = 0.67
    For sigma = 0.50 and m = 0.50 Cohen d = 1.00

We have to estimate the effect $\mu$, say under some normal noise. Our statistic will be:

$$
t = \frac{\hat{\mu}}{\hat{\sigma_{\mu}}} = \frac{\hat{\mu}}{\hat{{SE}_{\mu}}}
$$

Power is the probability that the observed t is greater than $t_{.05}$, computing $t_{.05}$ by assuming that we are under the null.

So, we compute $t_{.05}$, and want to compute $P(t > t_{.05})$.

To compute this, __we need the distribution of our measured t - therefore we need to know the signal / effect size !__

Let's assume we know this and call it $t_{nc}$, and $F_{nc}$ for the cumulative distribution (more on this in the appendix).

$\mbox{Power} = 1 - \beta =  P(t > t_{.05}) = 1 - F_{nc}(t_{.05})$

__This power will depend on 4 parameters :__

$$ \mbox{The non standardized effect : } \mu$$

$$\mbox{The standard deviation of the data : } \sigma$$

$$\mbox{The number of subjects : } n$$

$$\mbox{The type I risk of error : } \alpha$$

And on the distribution of the statistic under the alternative hypothesis. Here, we assume our original data are normals, and the $t = \frac{\hat{\mu}}{\hat{{SE}_{\mu}}}$ statistics follows a non central t distribution with non centrality parameter

$$\theta = \mu \sqrt{n}/\sigma$$

and $n-1$ degrees of freedom.

### What is the effect size of APOE on the hippocampal volume ?

Authors find p value of 6.63e-10

They had 733 subjects

```python
n01 = sst.norm(0,1.)
z = n01.isf(6.6311e-10)
d = n01.isf(6.6311e-10)/np.sqrt(733)
print("z = %4.3f d = %4.3f " %(z,d))
```

    z = 6.064 d = 0.224

```python
import scipy.stats as sst
import numpy as np
import matplotlib.pyplot as plt

```

```python
# plot power as a function of n : define a little function that
# takes n, mu, sigma, alpha, and report n.
# Optionally plot power as a function of nfrom matplotlib.patches import Polygon

def stat_power(n=16, mu=1., sigma=1., alpha=0.05):
    """
    This function computes the statistical power of an analysis assuming a normal
    distribution of the data with a one sample t-test

    Parameters:
    -----------
    n: int,
        The number of sample in the experiment
    mu: float
        The mean of the alternative
    sigma: float
        The standard deviation of the alternative
    plot: bool
        Plot something
    alpha: float
        The risk of error (type I)
    xlen: int
        Number of points for the display

    Returns:
    --------
    float
        The statistical power for this number of sample, mu, sigma, alpha
    """
    df = n-1
    theta = np.sqrt(n)*mu/sigma
    t_alph_null = sst.t.isf(alpha, df)
    ncrv = sst.nct(df, theta)
    spow = 1 - ncrv.cdf(t_alph_null)

    return spow

```

```python
from matplotlib.patches import Polygon

def plot_power(n=16, mu=1., sigma=1., alpha=0.05, xlen=500):

    df = n-1
    theta = np.sqrt(n)*mu/sigma
    ncrv = sst.nct(df, theta)
    t_alph_null = sst.t.isf(alpha, df)
    spow = 1 - ncrv.cdf(t_alph_null)

    # define the domain of the plot
    norv = sst.norm(0, 1.)
    bornesnc = ncrv.isf([0.001, .999])
    bornesn = norv.isf([0.001, .999])

    # because the nc t will have higher max borne, and the H0 normal will be on the left
    x = np.linspace(np.min(bornesn), np.max(bornesnc), xlen)
    t_line = np.zeros_like(x)

    # define the line
    x_t_line = np.argmin((x-t_alph_null)**2)
    y_t_line = np.max(np.hstack((ncrv.pdf(x), norv.pdf(x))))
    t_line[x_t_line] = y_t_line

    fig, ax = plt.subplots()
    plt.plot(x, ncrv.pdf(x), 'g', x, norv.pdf(x), 'b', x, t_line, 'r')

    # Make the shaded region
    # http://matplotlib.org/xkcd/examples/showcase/integral_demo.html
    a = x[x_t_line]; b =  np.max(bornesnc);
    ix = np.linspace(a,b)
    iy = ncrv.pdf(ix)
    verts = [(a, 0)] + list(zip(ix, iy)) + [(b, 0)]
    poly = Polygon(verts, facecolor='0.9', edgecolor='0.5')
    ax.add_patch(poly)

    ax.set_xlabel("t-value - H1 centred on " + r"$\theta $" + " = %4.2f;  " %theta
                      + r"$\mu$" + " = %4.2f" %mu);
    ax.set_ylabel("Probability(t)");
    ax.set_title('H0 and H1 sampling densities '
                     + r'$\beta$' + '= %3.2f' %spow + ' n = %d' %n)
    plt.show()

```

```python
n = 17
mu = .5
sigma = 2.
pwr = stat_power(n, mu, sigma, alpha=0.05)
plot_power(n, mu, sigma, alpha=0.05, xlen=500)

print ("Power = ", pwr, "      Z effect (Non centrality parameter) = ", mu*np.sqrt(n)/sigma)
```

```python
n = 50
mu = .5
sigma = 1.
pwr = stat_power(n, mu, sigma, alpha=0.05)
plot_power(n, mu, sigma, alpha=0.05, xlen=500)

print("Power = ", pwr, "      Z effect (Non centrality parameter): ", mu*np.sqrt(n)/sigma)
```

### Plot power as a function of the number of subject in the study

```python
#

def pwr_funcofsubj(muse, nses, alpha=.05, sigma=1):
    """
    Function to plot power as a function of the number of subjects (the nses)
    for an array of effect size (muses)

    muse: array of float
          The effect sizes : mu
    nses: array of integer
          The number of subjects
    alpha: float,
          type I risk
    sigma: float,
          data sigma
    """
    mstr = [ 'd='+str(m) for m in np.asarray(muse)/sigma]
    lines=[]
    for mu in (muse):
        pw = [stat_power(n, mu, sigma, alpha=alpha) for n in nses]
        (pl,) = plt.plot(nses, pw)
        lines.append(pl)
    plt.legend( lines, mstr, loc='upper right', shadow=True)
    plt.xlabel(" Number of subjects ")
    plt.ylabel(" Power ");

    return None

```

### **** For a small number of subjects ****

```python
mus = (.05, .1,.2,.3,.4,.5, .6);
#nse = range(70, 770, 20)
nse = range(7, 77, 2)
alph = 1.e-3

pwr_funcofsubj(mus, nse, alph)
```

### **** For a larger number of subjects ****

```python
mus = (.05,.1,.2,.3,.4,.5,.6);
nse = range(10, 330, 20)
#nse = range(7, 77, 2)
alph = 0.001

pwr_funcofsubj(mus, nse, alph)
```

### Here - stricter $\alpha$

```python
mus = (.05, .1,.2,.3,.4,.5, .6);
#nse = range(10, 770, 20)
nse = range(7, 77, 2)
alph = 0.05/30000
alph = 0.05

pwr_funcofsubj(mus, nse, alph)
```

```python

```

```python
#Example:
n = 259; mu= 230; sigma = 1487
stat_power(n, mu, sigma, alpha=0.05)
```

```python

```


```julia
using Catalyst

# NOTE: both models MUST preserve the same ordering of reactions in order to detect
# how the nonlinear reactions are to be transformed using LMA

rn_nonlinear = @reaction_network begin
      σ_b, g + p → 0
      σ_u*(1-g), 0 ⇒ g + p
      ρ_u, g → g + p
      ρ_b*(1-g), 0 ⇒ p
      1, p → 0
end σ_b σ_u ρ_b ρ_u

rn_linear = @reaction_network begin
      σ_b_LMA, g → 0      # typing ̄σ_b is not allowed it seems
      σ_u*(1-g), 0 ⇒ g
      ρ_u, g → g+p
      (ρ_b*(1-g)), 0 ⇒ p
      1, p → 0
end σ_b_LMA σ_u ρ_b ρ_u
```

\begin{align}
\require{mhchem}
\ce{ g &<=>[\sigma_{b\_LMA}][\sigma_{u} \left( 1 - g\left( t \right) \right)] \varnothing}\\
\ce{ g &->[\rho_{u}] g + p}\\
\ce{ \varnothing &<=>[\rho_{b} \left( 1 - g\left( t \right) \right)][1] p}
\end{align}

```julia
using MomentClosure

# NOTE: we have to provide the indices of binary variables in the system
# as they are ordered in the *nonlinear* GRN.
# The distinction here between linear and nonlinear GRNs is important as in some cases
# the internal ordering of variables of the two Catalyst models can differ
@parameters t
@variables g(t)
binary_vars = [speciesmap(rn_nonlinear)[g]]

LMA_eqs, effective_params = linear_mapping_approximation(rn_nonlinear, rn_linear, binary_vars, combinatoric_ratelaw=false)
display(effective_params)
```

    OrderedCollections.OrderedDict{Any, Any} with 1 entry:
      σ_b_LMA => σ_b*μ₁₁(t)*(μ₁₀(t)^-1)

```julia
using Latexify
latexify(LMA_eqs)
```

\begin{align*}
\frac{d\mu{_{10}}}{dt} =& \sigma_{u} - \sigma_{b} \mu{_{11}} - \sigma_{u} \mu{_{10}} \\
\frac{d\mu{_{01}}}{dt} =& \rho_{b} + \rho_{u} \mu{_{10}} - \mu{_{01}} - \rho_{b} \mu{_{10}} \\
\frac{d\mu{_{11}}}{dt} =& \rho_{u} \mu{_{10}} + \sigma_{u} \mu{_{01}} - \mu{_{11}} - \sigma_{u} \mu{_{11}} - \sigma_{b} \mu{_{10}}^{-1} \mu{_{11}}^{2} \\
\frac{d\mu{_{02}}}{dt} =& \rho_{b} + \rho_{u} \mu{_{10}} + 2 \rho_{b} \mu{_{01}} + 2 \rho_{u} \mu{_{11}} + \mu{_{01}} - 2 \mu{_{02}} - \rho_{b} \mu{_{10}} - 2 \rho_{b} \mu{_{11}}
\end{align*}

```julia
println(latexify(LMA_eqs))
```

    \begin{align*}
    \frac{d\mu{_{10}}}{dt} =& \sigma_{u} - \sigma_{b} \mu{_{11}} - \sigma_{u} \mu{_{10}} \\
    \frac{d\mu{_{01}}}{dt} =& \rho_{b} + \rho_{u} \mu{_{10}} - \mu{_{01}} - \rho_{b} \mu{_{10}} \\
    \frac{d\mu{_{11}}}{dt} =& \rho_{u} \mu{_{10}} + \sigma_{u} \mu{_{01}} - \mu{_{11}} - \sigma_{u} \mu{_{11}} - \sigma_{b} \mu{_{10}}^{-1} \mu{_{11}}^{2} \\
    \frac{d\mu{_{02}}}{dt} =& \rho_{b} + \rho_{u} \mu{_{10}} + 2 \rho_{b} \mu{_{01}} + 2 \rho_{u} \mu{_{11}} + \mu{_{01}} - 2 \mu{_{02}} - \rho_{b} \mu{_{10}} - 2 \rho_{b} \mu{_{11}}
    \end{align*}

```julia
using OrdinaryDiffEq, Sundials, Plots

# [g, p] as in species(rn_nonlinear)
u₀ = [1.0, 0.001]
p = [0.004, 0.25, 25.0, 60.0]
tspan = (0., 15.)
dt = 0.1

u₀map = deterministic_IC(u₀, LMA_eqs)
oprob_LMA = ODEProblem(LMA_eqs, u₀map, tspan, p)
sol_LMA = solve(oprob_LMA, CVODE_BDF(), saveat=dt)

plot(sol_LMA, vars=(0, [2]), label="LMA", ylabel="⟨p⟩", xlabel="time", fmt="svg", guidefontsize=12)
```

    (process:8552): GLib-GIO-WARNING **: 17:16:47.816: Unexpectedly, UWP app `KDEe.V.Okular_20.1202.546.0_x64__7vt06qxq7ptv8' (AUMId `KDEe.V.Okular_7vt06qxq7ptv8!KDEe.V.Okular') supports 5 extensions but has no verbs

```julia
#savefig("../docs/src/assets/LMA_feedback_loop_mean_protein_number.svg")
```

```julia
using FiniteStateProjection

fsp_sys = FSPSystem(rn_nonlinear, combinatoric_ratelaw=false)
# Truncate the state space of the system
# The gene has two states (G or G*) whereas we consider protein number from 0 to 100
state_space = [2, 201]

# The initial condition is the matrix of probabilities representing the state of the system
# We assume zero protein and the gene to be in the state G, hence the probability of this
# specific state should be set to 1 initially
u0 = zeros(state_space...)
u0[2, 1] = 1.0

# construct an ODE problem from the FSPSystem and solve it
fsp_prob = ODEProblem(fsp_sys, u0, tspan, p)
sol_FSP = solve(fsp_prob, CVODE_BDF(), saveat=dt)

# extract the 1st order raw moments from the FSP solution
μ_FSP = get_moments_FSP(sol_FSP, 1, "raw")
plot!(sol_FSP.t, μ_FSP[(0,1)], label="FSP", legend=:bottomright)
```

```julia
#savefig("../docs/src/assets/LMA+FSP_feedback_loop_mean_protein_number.svg")
```

```julia
using TaylorSeries, HypergeometricFunctions

function t_pFq(α::AbstractVector, β::AbstractVector, a::Taylor1)
    order = a.order
    aux = pFq(α, β, constant_term(a))
    c = Taylor1(aux, order)

    iszero(order) && return c

    coeffs = t_pFq(α.+1, β.+1, Taylor1(a[0:end-1], a.order-1))
    factor = prod(α)/prod(β)
    for k in 1:length(a)-1
        c[k] = sum(i * a[i] * coeffs[k-i] for i in 1:k) * factor / k
    end

    return c

end
```

    t_pFq (generic function with 1 method)

```julia
# calculate the raw moments up to time t at a fine temporal resolution
T = 15.0
tspan = (0., T)
dt = 0.001
oprob_LMA = remake(oprob_LMA, tspan=tspan)
sol_LMA = solve(oprob_LMA, CVODE_BDF(), saveat=dt)

# rebuild the symbolic expression for the effective parameter as a function of raw moments
μ_sym = LMA_eqs.odes.states
p_sub = Pair.(LMA_eqs.odes.ps, p)
avg_σ_b_sym = collect(values(effective_params))[1]
fn = build_function(substitute(avg_σ_b_sym, p_sub), μ_sym)
avg_σ_b = eval(fn)
# evaluate the time-averaged value of the effective parameter
@time σ_b_avg = sum(avg_σ_b.(sol_LMA[:])) * dt / T
```

      0.166009 seconds (303.68 k allocations: 20.226 MiB, 23.01% gc time, 99.56% compilation time)

    0.20985247655841632

```julia
# need higher-precision numerics as Float64 can be unstable here due to very small numbers
# DoubleFloats is sufficient for this example and much more efficient than BigFloat
using DoubleFloats

# define the numerical values of the parameters
σ_u = p[2]; ρ_b = p[3]; ρ_u = p[4]
Σ = 1 + σ_b_avg + σ_u
ρ_Δ = ρ_b - ρ_u

n = 100 # expansion order (or max protein number to evaluate)
w₀ = -1 # value around which to expand

# compute the Taylor expansion (note the use of Double64)
w = w₀ + Taylor1(Double64, n)
@time f = σ_b_avg/(Σ-1)*exp(-T*(Σ-1))*exp(-ρ_u*w*exp(-T))*t_pFq([σ_u], [Σ], -ρ_Δ*w*exp(-T))
@time g = σ_u/(Σ-1)*exp(-ρ_u*w*exp(-T))*t_pFq([-σ_b_avg], [2-Σ], -ρ_Δ*w*exp(-T))

@time G00 = exp(ρ_b*w)*(f * t_pFq([1-σ_b_avg], [2-Σ], -ρ_Δ*w) +
                  g * t_pFq([1+σ_u], [Σ], -ρ_Δ*w) )

@time G11 = σ_u^(-1) * exp(ρ_b*w) * (-σ_u*f*t_pFq([-σ_b_avg], [2-Σ], -ρ_Δ*w) +
                                σ_b_avg*g*t_pFq([σ_u], [Σ], -ρ_Δ*w))

probs = (G00+G11).coeffs

# check that the probability distribution is more or less normalised to 1
# need higher numerical precision if not
isapprox(sum(probs), 1.0, rtol=1e-2)
```

      4.622658 seconds (7.25 M allocations: 377.657 MiB, 2.09% gc time, 99.73% compilation time)
      0.110327 seconds (42.19 k allocations: 1.676 MiB, 77.20% gc time, 12.72% compilation time)
      0.081298 seconds (85.06 k allocations: 3.613 MiB)
      0.085832 seconds (85.81 k allocations: 3.670 MiB, 3.77% compilation time)

    true

```julia
plot(0:n, probs, xlabel="n", ylabel="P(n, t=4)", label="LMA", fmt="svg")
# plot the FSP probability of protein number by marginalising over the gene states
plot!(0:n, sum(sol_FSP[:, 151], dims=1)'[1:n+1], label="FSP")
```

```julia
#savefig("../docs/src/assets/LMA+FSP_feedback_loop_distribution.svg")
```


Matrices

From

```julia
    >>> from sympy import *
    >>> init_printing(use_unicode=True)
```

In Julia:

In SymPy, matrices can be store using Julia's generic Matrix{T} type where T <: Sym or using SymPy's matrix type, wrapped in a SymMatrix type by SymPy. This tutorial shows how to use the underlying SymMatrix values. To construct a matrix of symbolic values is identical to construction a matrix of numeric values within Julia, and will be illustrated at the end.

methods for SymMatrix objects use the dot call syntax. As a convenience, this will also work for Array{Sym} objects. The returned value may be a SymMatrix, not an Array{Sym}.

The matrix constructor in SymPy using a vector of row vectors does not work in SymPy, as of newer versions (it does not work with version 1.5.1 of sympy and PyCall). This style is used in this document. The user of SymPy can eaesily avoid this specification, using Julia's matrix construction techniques.

```julia
using SymPy
```

To make a matrix in SymPy, use the Matrix object.  A matrix is constructed by providing a list of row vectors that make up the matrix.  For example, to construct the matrix

$$
   \left[\begin{array}{cc}1 & -1\\3 & 4\\0 & 2\end{array}\right]
$$

use

```julia
    >>> Matrix([[1, -1], [3, 4], [0, 2]])
    ⎡1  -1⎤
    ⎢     ⎥
    ⎢3  4 ⎥
    ⎢     ⎥
    ⎣0  2 ⎦
```

In Julia:

In Julia, the Matrix constructor is not exported, so must be qualified. Here we avoid thte vector of row vectors above:

```julia
sympy.Matrix([1 -1; 3 4; 0 2])
```

\[\left[ \begin{array}{rr}1&-1\\3&4\\0&2\end{array}\right]\]

However, through the magic of PyCall, such matrices are converted into Julia matrices, of type Array{Sym}, so the familiar matrix operations for Julia users are available.

In fact, the above could be done in the more Julian manner through

```julia
Sym[1 -1; 3 4; 0 2]
```

\[\left[ \begin{array}{rr}1&-1\\3&4\\0&2\end{array}\right]\]

using an annotation to ensure the type. Alternatively, through promotion, just a single symbolic object will result in the same:

```julia
[Sym(1) -1; 3 4; 0 2]
```

\[\left[ \begin{array}{rr}1&-1\\3&4\\0&2\end{array}\right]\]

To make it easy to make column vectors, a list of elements is considered to be a column vector.

```julia
    >>> Matrix([1, 2, 3])
    ⎡1⎤
    ⎢ ⎥
    ⎢2⎥
    ⎢ ⎥
    ⎣3⎦
```

In Julia:

For ths use, sympy.Matrix does work, but again its usage is discouraged:

```julia
sympy.Matrix([1, 2, 3])
```

\[\left[ \begin{array}{r}1\\2\\3\end{array}\right]\]

Again, this is converted into a Vector{Sym} object or entered directly:

```julia
Sym[1,2,3]
```

\[ \left[ \begin{array}{r}1\\2\\3\end{array} \right] \]

And again:

Using sympy.Matrix is strongly discouraged.

Matrices are manipulated just like any other object in SymPy or Python.

```julia
    >>> M = Matrix([[1, 2, 3], [3, 2, 1]])
    >>> N = Matrix([0, 1, 1])
    >>> M*N
    ⎡5⎤
    ⎢ ⎥
    ⎣3⎦
```

In Julia:

In Julia, matrices are just matrices, and inherit all of the operations defined on them:

```julia
M = Sym[1 2 3; 3 2 1]
N = Sym[0, 1, 1]
M*N
```

    2-element Array{Any,1}:
     5
     3

One important thing to note about SymPy matrices is that, unlike every other object in SymPy, they are mutable.  This means that they can be modified in place, as we will see below.  The downside to this is that Matrix cannot be used in places that require immutability, such as inside other SymPy expressions or as keys to dictionaries.  If you need an immutable version of Matrix, use ImmutableMatrix.

In Julia:

A distinction is made between ImmutableMatrix and a mutable one. Mutable ones are mapped to Julia arrays, immutable ones are left as a symbolic object of type SymMatrix. The usual infix mathematical operations (but not dot broadcasting), 0-based indexing, and dot call syntax for methods are used with these objects.

Basic Operations

Shape

Here are some basic operations on Matrix.  To get the shape of a matrix use shape

```julia
    >>> M = Matrix([[1, 2, 3], [-2, 0, 4]])
    >>> M
    ⎡1   2  3⎤
    ⎢        ⎥
    ⎣-2  0  4⎦
    >>> M.shape
    (2, 3)
```

In Julia:

```julia
M = Sym[1 2 3; -2 0 4]
M
```

\[\left[ \begin{array}{rrr}1&2&3\\-2&0&4\end{array}\right]\]

```julia
M.shape
```

    (2, 3)

Or, the Julian counterpart:

```julia
size(M)
```

    (2, 3)

Accessing Rows and Columns

To get an individual row or column of a matrix, use row or col.  For example, M.row(0) will get the first row. M.col(-1) will get the last column.

```julia
    >>> M.row(0)
    [1  2  3]
    >>> M.col(-1)
    ⎡3⎤
    ⎢ ⎥
    ⎣4⎦
```

In Julia:

these 0-based operations are supported:

```julia
M.row(0)
M.col(-1)
```

\[\left[ \begin{array}{r}3\\4\end{array}\right]\]

The more familiar counterparts would be:

```julia
M[1,:], M[:, end]
```

    (Sym[1, 2, 3], Sym[3, 4])

Deleting and Inserting Rows and Columns

To delete a row or column, use row_del or col_del.  These operations will modify the Matrix in place.

```julia
    >>> M.col_del(0)
    >>> M
    ⎡2  3⎤
    ⎢    ⎥
    ⎣0  4⎦
    >>> M.row_del(1)
    >>> M
    [2  3]
```

In Julia:

These methods do not work on Array{Sym} objects, use Julia's indexing notation to remove a row or column.

However, these methods do work on the ImmutableMatrix class:

```julia
M = sympy.ImmutableMatrix([1 2 3; -2 0 4])  # avoid vector of row vector construction
M.col_del(0)
```

\begin{equation*}\left[\begin{matrix}2 & 3\\0 & 4\end{matrix}\right]\end{equation*}

```julia
M.row_del(1)
```

\begin{equation*}\left[\begin{matrix}1 & 2 & 3\end{matrix}\right]\end{equation*}

!!! Alert

For older versions of sympy, the following did not work (using symbolic  values as matrix entries without reverting to  their PyObjects  had shape issues) but  this should work  now:

```julia
@vars x
sympy.ImmutableMatrix([x 1;  1  x])
```

\begin{equation*}\left[\begin{matrix}x & 1\\1 & x\end{matrix}\right]\end{equation*}

TODO

This is a mess. See issue 6992.

To insert rows or columns, use row_insert or col_insert.  These operations do not operate in place.

```julia
    >>> M
    [2  3]
    >>> M = M.row_insert(1, Matrix([[0, 4]]))
    >>> M
    ⎡2  3⎤
    ⎢    ⎥
    ⎣0  4⎦
    >>> M = M.col_insert(0, Matrix([1, -2]))
    >>> M
    ⎡1   2  3⎤
    ⎢        ⎥
    ⎣-2  0  4⎦
```

In Julia:

```julia
M
M = M.row_insert(1, Sym[0 4])
M
```

\begin{equation*}\left[\begin{matrix}1 & 2 & 3\\-2 & 0 & 4\end{matrix}\right]\end{equation*}

```julia
M = M.col_insert(0, Matrix([1, -2]))
M
```

\begin{equation*}\left[\begin{matrix}1 & 2 & 3\\-2 & 0 & 4\end{matrix}\right]\end{equation*}

Unless explicitly stated, the methods mentioned below do not operate in place. In general, a method that does not operate in place will return a new Matrix and a method that does operate in place will return None.

In Julia

This would be the case for the immutable matrices.

Basic Methods

As noted above, simple operations like addition and multiplication are done just by using +, *, and **.  To find the inverse of a matrix, just raise it to the -1 power.

```julia
    >>> M = Matrix([[1, 3], [-2, 3]])
    >>> N = Matrix([[0, 3], [0, 7]])
    >>> M + N
    ⎡1   6 ⎤
    ⎢      ⎥
    ⎣-2  10⎦
    >>> M*N
    ⎡0  24⎤
    ⎢     ⎥
    ⎣0  15⎦
    >>> 3*M
    ⎡3   9⎤
    ⎢     ⎥
    ⎣-6  9⎦
    >>> M**2
    ⎡-5  12⎤
    ⎢      ⎥
    ⎣-8  3 ⎦
    >>> M**-1
    ⎡1/3  -1/3⎤
    ⎢         ⎥
    ⎣2/9  1/9 ⎦
    >>> N**-1
    Traceback (most recent call last):
    ...
    ValueError: Matrix det == 0; not invertible.
```

In Julia:

```julia
M = Sym[1 3; -2 3]
M1 = Sym[0 3; 0 7]
M + M1
```

\[\left[ \begin{array}{rr}1&6\\-2&10\end{array}\right]\]

```julia
M*M1
```

    2×2 Array{Any,2}:
     0  24
     0  15

```julia
3*M
```

\[\left[ \begin{array}{rr}3&9\\-6&9\end{array}\right]\]

```julia
M^2
```

    2×2 Array{Any,2}:
     -5  12
     -8   3

```julia
M^-1
```

\[\left[ \begin{array}{rr}\frac{1}{3}&- \frac{1}{3}\\\frac{2}{9}&\frac{1}{9}\end{array}\right]\]

```julia
M1^-1
```

    PyError ($(Expr(:escape, :(ccall(#= /Users/verzani/.julia/packages/PyCall/ttONZ/src/pyfncall.jl:44 =# @pysym(:PyObject_Call), PyPtr, (PyPtr, PyPtr, PyPtr), o, pyargsptr, kw))))) <class 'sympy.matrices.common.NonInvertibleMatrixError'>
    NonInvertibleMatrixError('Matrix det == 0; not invertible.')
      File "/Users/verzani/.julia/conda/3/lib/python3.7/site-packages/sympy/matrices/matrices.py", line 3672, in inv
        return self._eval_inverse(**kwargs)
      File "/Users/verzani/.julia/conda/3/lib/python3.7/site-packages/sympy/matrices/dense.py", line 265, in _eval_inverse
        rv = M.inverse_GE(iszerofunc=iszerofunc)
      File "/Users/verzani/.julia/conda/3/lib/python3.7/site-packages/sympy/matrices/matrices.py", line 3587, in inverse_GE
        raise NonInvertibleMatrixError("Matrix det == 0; not invertible.")

The above (except for the inverses) are using generic Julia definitions. For immutable matrices, we would have:

```julia
M = sympy.ImmutableMatrix([1 3; -2 3])
M1 = sympy.ImmutableMatrix([0 3; 0 7])
M + M1
```

\begin{equation*}\left[\begin{matrix}1 & 6\\-2 & 10\end{matrix}\right]\end{equation*}

```julia
M*M1
```

\begin{equation*}\left[\begin{matrix}0 & 24\\0 & 15\end{matrix}\right]\end{equation*}

```julia
3*M
```

\begin{equation*}\left[\begin{matrix}3 & 9\\-6 & 9\end{matrix}\right]\end{equation*}

```julia
M^2
```

\begin{equation*}\left(\left[\begin{matrix}1 & 3\\-2 & 3\end{matrix}\right]\right)^{2}\end{equation*}

```julia
M^-1
```

\begin{equation*}\left[\begin{matrix}\frac{1}{3} & - \frac{1}{3}\\\frac{2}{9} & \frac{1}{9}\end{matrix}\right]\end{equation*}

```julia
M1^-1
```

    PyError ($(Expr(:escape, :(ccall(#= /Users/verzani/.julia/packages/PyCall/ttONZ/src/pyfncall.jl:44 =# @pysym(:PyObject_Call), PyPtr, (PyPtr, PyPtr, PyPtr), o, pyargsptr, kw))))) <class 'sympy.matrices.common.NonInvertibleMatrixError'>
    NonInvertibleMatrixError('Matrix det == 0; not invertible.')
      File "/Users/verzani/.julia/conda/3/lib/python3.7/site-packages/sympy/matrices/matrices.py", line 3672, in inv
        return self._eval_inverse(**kwargs)
      File "/Users/verzani/.julia/conda/3/lib/python3.7/site-packages/sympy/matrices/dense.py", line 265, in _eval_inverse
        rv = M.inverse_GE(iszerofunc=iszerofunc)
      File "/Users/verzani/.julia/conda/3/lib/python3.7/site-packages/sympy/matrices/matrices.py", line 3587, in inverse_GE
        raise NonInvertibleMatrixError("Matrix det == 0; not invertible.")

There is no broadcasting defined for the SymMatrix type.

To take the transpose of a Matrix, use T.

```julia
    >>> M = Matrix([[1, 2, 3], [4, 5, 6]])
    >>> M
    ⎡1  2  3⎤
    ⎢       ⎥
    ⎣4  5  6⎦
    >>> M.T
    ⎡1  4⎤
    ⎢    ⎥
    ⎢2  5⎥
    ⎢    ⎥
    ⎣3  6⎦
```

In Julia:

```julia
M = Sym[1 2 3; 4 5 6]
M
```

\[\left[ \begin{array}{rrr}1&2&3\\4&5&6\end{array}\right]\]

```julia
M.T
```

\[\left[ \begin{array}{rr}1&4\\2&5\\3&6\end{array}\right]\]

Matrix Constructors

Several constructors exist for creating common matrices.  To create an identity matrix, use eye.  The command eye(n) will create an n x n identity matrix:

```julia
    >>> eye(3)
    ⎡1  0  0⎤
    ⎢       ⎥
    ⎢0  1  0⎥
    ⎢       ⎥
    ⎣0  0  1⎦
    >>> eye(4)
    ⎡1  0  0  0⎤
    ⎢          ⎥
    ⎢0  1  0  0⎥
    ⎢          ⎥
    ⎢0  0  1  0⎥
    ⎢          ⎥
    ⎣0  0  0  1⎦
```

In Julia:

eye is not exported so must qualified:

```julia
sympy.eye(3)
sympy.eye(4)
```

\[\left[ \begin{array}{rrrr}1&0&0&0\\0&1&0&0\\0&0&1&0\\0&0&0&1\end{array}\right]\]

To create a matrix of all zeros, use zeros.  zeros(n, m) creates an n x m matrix of 0s.

```julia
    >>> zeros(2, 3)
    ⎡0  0  0⎤
    ⎢       ⎥
    ⎣0  0  0⎦
```

In Julia:

zeros is exported but the method expects a symbolic first argument.  Either qualify it:

```julia
sympy.zeros(2, 3)
```

\[\left[ \begin{array}{rrr}0&0&0\\0&0&0\end{array}\right]\]

or create a symbolic first value:

```julia
zeros(Sym(2), 3)
```

\[\left[ \begin{array}{rrr}0&0&0\\0&0&0\end{array}\right]\]

or use the Julia constructor:

```julia
zeros(Sym, 2, 3)
```

\[\left[ \begin{array}{rrr}0&0&0\\0&0&0\end{array}\right]\]

Similarly, ones creates a matrix of ones.

```julia
    >>> ones(3, 2)
    ⎡1  1⎤
    ⎢    ⎥
    ⎢1  1⎥
    ⎢    ⎥
    ⎣1  1⎦
```

In Julia:

Similarly with ones:

```julia
sympy.ones(3, 2)
```

\[\left[ \begin{array}{rr}1&1\\1&1\\1&1\end{array}\right]\]

To create diagonal matrices, use diag.  The arguments to diag can be either numbers or matrices.  A number is interpreted as a 1 x 1 matrix. The matrices are stacked diagonally.  The remaining elements are filled with 0\ s.

```julia
    >>> diag(1, 2, 3)
    ⎡1  0  0⎤
    ⎢       ⎥
    ⎢0  2  0⎥
    ⎢       ⎥
    ⎣0  0  3⎦
    >>> diag(-1, ones(2, 2), Matrix([5, 7, 5]))
    ⎡-1  0  0  0⎤
    ⎢           ⎥
    ⎢0   1  1  0⎥
    ⎢           ⎥
    ⎢0   1  1  0⎥
    ⎢           ⎥
    ⎢0   0  0  5⎥
    ⎢           ⎥
    ⎢0   0  0  7⎥
    ⎢           ⎥
    ⎣0   0  0  5⎦
```

In Julia:

similarly with diag:

```julia
sympy.diag(1, 2, 3)
```

\[\left[ \begin{array}{rrr}1&0&0\\0&2&0\\0&0&3\end{array}\right]\]

```julia
sympy.diag(-1, sympy.ones(2, 2), sympy.Matrix([5, 7, 5]))
```

\[\left[ \begin{array}{rrrr}-1&0&0&0\\0&1&1&0\\0&1&1&0\\0&0&0&5\\0&0&0&7\\0&0&0&5\end{array}\right]\]

The first one, could also use Julia's diagm function:

```julia
using LinearAlgebra
diagm(0 => Sym[1,2,3])
```

\[\left[ \begin{array}{rrr}1&0&0\\0&2&0\\0&0&3\end{array}\right]\]

Advanced Methods

Determinant

To compute the determinant of a matrix, use det.

```julia
    >>> M = Matrix([[1, 0, 1], [2, -1, 3], [4, 3, 2]])
    >>> M
    ⎡1  0   1⎤
    ⎢        ⎥
    ⎢2  -1  3⎥
    ⎢        ⎥
    ⎣4  3   2⎦
    >>> M.det()
    -1
```

In Julia:

```julia
using LinearAlgebra
M = Sym[1 0 1; 2 -1 3; 4 3 2]
M
```

\[\left[ \begin{array}{rrr}1&0&1\\2&-1&3\\4&3&2\end{array}\right]\]

```julia
M.det()
```

\begin{equation*}-1\end{equation*}

Let

```julia
@vars x
A = Sym[x 1; 1 x]
```

\[\left[ \begin{array}{rr}x&1\\1&x\end{array}\right]\]

Then we can compute the determinant using Julia's generic implementation:

```julia
det(A)
```

\begin{equation*}x^{2} - 1\end{equation*}

or using SymPy's:

```julia
A.det()
```

\begin{equation*}x^{2} - 1\end{equation*}

The answer is identical, though not necessarily being done in a similar manner.

RREF

To put a matrix into reduced row echelon form, use rref.  rref returns a tuple of two elements. The first is the reduced row echelon form, and the second is a tuple of indices of the pivot columns.

```julia
    >>> M = Matrix([[1, 0, 1, 3], [2, 3, 4, 7], [-1, -3, -3, -4]])
    >>> M
    ⎡1   0   1   3 ⎤
    ⎢              ⎥
    ⎢2   3   4   7 ⎥
    ⎢              ⎥
    ⎣-1  -3  -3  -4⎦
    >>> M.rref()
    ⎛⎡1  0   1    3 ⎤        ⎞
    ⎜⎢              ⎥        ⎟
    ⎜⎢0  1  2/3  1/3⎥, (0, 1)⎟
    ⎜⎢              ⎥        ⎟
    ⎝⎣0  0   0    0 ⎦        ⎠
```

In Julia:

```julia
M = Sym[1 0 1 3; 2 3 4 7; -1 -3 -3 -4]
M
```

\[\left[ \begin{array}{rrrr}1&0&1&3\\2&3&4&7\\-1&-3&-3&-4\end{array}\right]\]

```julia
M.rref()
```

    (Sym[Sym(PyObject 1) Sym(PyObject 0) Sym(PyObject 1) Sym(PyObject 3); Sym(PyObject 0) Sym(PyObject 1) Sym(PyObject 2/3) Sym(PyObject 1/3); Sym(PyObject 0) Sym(PyObject 0) Sym(PyObject 0) Sym(PyObject 0)], (0, 1))

Note

The first element of the tuple returned by rref is of type Matrix. The second is of type tuple.

Nullspace

To find the nullspace of a matrix, use nullspace. nullspace returns a list of column vectors that span the nullspace of the matrix.

```julia
    >>> M = Matrix([[1, 2, 3, 0, 0], [4, 10, 0, 0, 1]])
    >>> M
    ⎡1  2   3  0  0⎤
    ⎢              ⎥
    ⎣4  10  0  0  1⎦
    >>> M.nullspace()
    ⎡⎡-15⎤  ⎡0⎤  ⎡ 1  ⎤⎤
    ⎢⎢   ⎥  ⎢ ⎥  ⎢    ⎥⎥
    ⎢⎢ 6 ⎥  ⎢0⎥  ⎢-1/2⎥⎥
    ⎢⎢   ⎥  ⎢ ⎥  ⎢    ⎥⎥
    ⎢⎢ 1 ⎥, ⎢0⎥, ⎢ 0  ⎥⎥
    ⎢⎢   ⎥  ⎢ ⎥  ⎢    ⎥⎥
    ⎢⎢ 0 ⎥  ⎢1⎥  ⎢ 0  ⎥⎥
    ⎢⎢   ⎥  ⎢ ⎥  ⎢    ⎥⎥
    ⎣⎣ 0 ⎦  ⎣0⎦  ⎣ 1  ⎦⎦
```

In Julia:

the list is mapped to an array of vectors, otherwise this is identical:

```julia
M = Sym[1 2 3 0 0; 4 10 0 0 1]
M
```

\[\left[ \begin{array}{rrrrr}1&2&3&0&0\\4&10&0&0&1\end{array}\right]\]

```julia
M.nullspace()
```

    3-element Array{Array{Sym,2},1}:
     [-15; 6; 1; 0; 0]
     [0; 0; 0; 1; 0]
     [1; -1/2; 0; 0; 1]

Columnspace

To find the columnspace of a matrix, use columnspace. columnspace returns a list of column vectors that span the columnspace of the matrix.

```julia
    >>> M = Matrix([[1, 1, 2], [2 ,1 , 3], [3 , 1, 4]])
    >>> M
    ⎡1  1  2⎤
    ⎢       ⎥
    ⎢2  1  3⎥
    ⎢       ⎥
    ⎣3  1  4⎦
    >>> M.columnspace()
    ⎡⎡1⎤  ⎡1⎤⎤
    ⎢⎢ ⎥  ⎢ ⎥⎥
    ⎢⎢2⎥, ⎢1⎥⎥
    ⎢⎢ ⎥  ⎢ ⎥⎥
    ⎣⎣3⎦  ⎣1⎦⎦
```

In Julia:

as with nullspace, the return value is a vector of vectors:

```julia
M = Sym[1 1 2; 2 1 3; 3 1 4]
M
```

\[\left[ \begin{array}{rrr}1&1&2\\2&1&3\\3&1&4\end{array}\right]\]

```julia
M.columnspace()
```

    2-element Array{Array{Sym,2},1}:
     [1; 2; 3]
     [1; 1; 1]

Eigenvalues, Eigenvectors, and Diagonalization

To find the eigenvalues of a matrix, use eigenvals.  eigenvals returns a dictionary of eigenvalue:algebraic multiplicity pairs (similar to the output of :ref:roots <tutorial-roots>).

```julia
    >>> M = Matrix([[3, -2,  4, -2], [5,  3, -3, -2], [5, -2,  2, -2], [5, -2, -3,  3]])
    >>> M
    ⎡3  -2  4   -2⎤
    ⎢             ⎥
    ⎢5  3   -3  -2⎥
    ⎢             ⎥
    ⎢5  -2  2   -2⎥
    ⎢             ⎥
    ⎣5  -2  -3  3 ⎦
    >>> M.eigenvals()
    {-2: 1, 3: 1, 5: 2}
```

In Julia:

```julia
M = Sym[3 -2  4 -2; 5  3 -3 -2; 5 -2  2 -2; 5 -2 -3  3]
M
```

\[\left[ \begin{array}{rrrr}3&-2&4&-2\\5&3&-3&-2\\5&-2&2&-2\\5&-2&-3&3\end{array}\right]\]

```julia
M.eigenvals()
```

    Dict{Any,Any} with 3 entries:
      3 => 1
      -2 => 1
      5 => 2

This means that M has eigenvalues -2, 3, and 5, and that the eigenvalues -2 and 3 have algebraic multiplicity 1 and that the eigenvalue 5 has algebraic multiplicity 2.

To find the eigenvectors of a matrix, use eigenvects.  eigenvects returns a list of tuples of the form (eigenvalue:algebraic multiplicity, [eigenvectors]).

```julia
    >>> M.eigenvects()
    ⎡⎛       ⎡⎡0⎤⎤⎞  ⎛      ⎡⎡1⎤⎤⎞  ⎛      ⎡⎡1⎤  ⎡0 ⎤⎤⎞⎤
    ⎢⎜       ⎢⎢ ⎥⎥⎟  ⎜      ⎢⎢ ⎥⎥⎟  ⎜      ⎢⎢ ⎥  ⎢  ⎥⎥⎟⎥
    ⎢⎜       ⎢⎢1⎥⎥⎟  ⎜      ⎢⎢1⎥⎥⎟  ⎜      ⎢⎢1⎥  ⎢-1⎥⎥⎟⎥
    ⎢⎜-2, 1, ⎢⎢ ⎥⎥⎟, ⎜3, 1, ⎢⎢ ⎥⎥⎟, ⎜5, 2, ⎢⎢ ⎥, ⎢  ⎥⎥⎟⎥
    ⎢⎜       ⎢⎢1⎥⎥⎟  ⎜      ⎢⎢1⎥⎥⎟  ⎜      ⎢⎢1⎥  ⎢0 ⎥⎥⎟⎥
    ⎢⎜       ⎢⎢ ⎥⎥⎟  ⎜      ⎢⎢ ⎥⎥⎟  ⎜      ⎢⎢ ⎥  ⎢  ⎥⎥⎟⎥
    ⎣⎝       ⎣⎣1⎦⎦⎠  ⎝      ⎣⎣1⎦⎦⎠  ⎝      ⎣⎣0⎦  ⎣1 ⎦⎦⎠⎦
```

In Julia:

the output is less than desirable, as there is no special show method

the eigvals and eigvecs methods present the output in the manner that Julia's generic functions do:

```julia
M.eigenvects()
```

    3-element Array{Tuple{Sym,Int64,Array{Array{Sym,2},1}},1}:
     (-2, 1, [[0; 1; 1; 1]])
     (3, 1, [[1; 1; 1; 1]])
     (5, 2, [[1; 1; 1; 0], [0; -1; 0; 1]])

compare with

```julia
eigvecs(M)
```

\[\left[ \begin{array}{rrrr}0&1&1&0\\1&1&1&-1\\1&1&1&0\\1&1&0&1\end{array}\right]\]

This shows us that, for example, the eigenvalue 5 also has geometric multiplicity 2, because it has two eigenvectors.  Because the algebraic and geometric multiplicities are the same for all the eigenvalues, M is diagonalizable.

To diagonalize a matrix, use diagonalize. diagonalize returns a tuple (P, D), where D is diagonal and M = PDP^{-1}.

```julia
    >>> P, D = M.diagonalize()
    >>> P
    ⎡0  1  1  0 ⎤
    ⎢           ⎥
    ⎢1  1  1  -1⎥
    ⎢           ⎥
    ⎢1  1  1  0 ⎥
    ⎢           ⎥
    ⎣1  1  0  1 ⎦
    >>> D
    ⎡-2  0  0  0⎤
    ⎢           ⎥
    ⎢0   3  0  0⎥
    ⎢           ⎥
    ⎢0   0  5  0⎥
    ⎢           ⎥
    ⎣0   0  0  5⎦
    >>> P*D*P**-1
    ⎡3  -2  4   -2⎤
    ⎢             ⎥
    ⎢5  3   -3  -2⎥
    ⎢             ⎥
    ⎢5  -2  2   -2⎥
    ⎢             ⎥
    ⎣5  -2  -3  3 ⎦
    >>> P*D*P**-1 == M
    True
```

In Julia:

```julia
P, D = M.diagonalize()
P
```

\[\left[ \begin{array}{rrrr}0&1&1&0\\1&1&1&-1\\1&1&1&0\\1&1&0&1\end{array}\right]\]

```julia
D
```

\[\left[ \begin{array}{rrrr}-2&0&0&0\\0&3&0&0\\0&0&5&0\\0&0&0&5\end{array}\right]\]

```julia
P*D*P^-1
```

    4×4 Array{Any,2}:
     3  -2   4  -2
     5   3  -3  -2
     5  -2   2  -2
     5  -2  -3   3

```julia
P*D*P^-1 == M
```

    true

Quick Tip

lambda is a reserved keyword in Python, so to create a Symbol called    $\lambda$, while using the same names for SymPy Symbols and Python    variables, use lamda (without the b).  It will still pretty print    as $\lambda$.

Note that since eigenvects also includes the eigenvalues, you should use it instead of eigenvals if you also want the eigenvectors. However, as computing the eigenvectors may often be costly, eigenvals should be preferred if you only wish to find the eigenvalues.

If all you want is the characteristic polynomial, use charpoly.  This is more efficient than eigenvals, because sometimes symbolic roots can be expensive to calculate.

```julia
    >>> lamda = symbols('lamda')
    >>> p = M.charpoly(lamda)
    >>> factor(p)
           2
    (λ - 5) ⋅(λ - 3)⋅(λ + 2)
```

In Julia:

note missing b is not needed with Julia:

```julia
lambda = symbols("lambda")
p = M.charpoly(lambda)
factor(p)
```

\begin{equation*}\left(\lambda - 5\right)^{2} \left(\lambda - 3\right) \left(\lambda + 2\right)\end{equation*}

TODO

Add an example for jordan_form, once it is fully implemented.

Possible Issues

Zero Testing

If your matrix operations are failing or returning wrong answers, the common reasons would likely be from zero testing. If there is an expression not properly zero-tested, it can possibly bring issues in finding pivots for gaussian elimination, or deciding whether the matrix is inversible, or any high level functions which relies on the prior procedures.

Currently, the SymPy's default method of zero testing _iszero is only guaranteed to be accurate in some limited domain of numerics and symbols, and any complicated expressions beyond its decidability are treated as None, which behaves similarly to logical False.

The list of methods using zero testing procedures are as followings.

echelon_form , is_echelon , rank , rref , nullspace , eigenvects , inverse_ADJ , inverse_GE , inverse_LU , LUdecomposition , LUdecomposition_Simple , LUsolve

They have property iszerofunc opened up for user to specify zero testing method, which can accept any function with single input and boolean output, while being defaulted with _iszero.

Here is an example of solving an issue caused by undertested zero. [#zerotestexampleidea-fn]_ [#zerotestexamplediscovery-fn]_

```julia
    >>> from sympy import *
    >>> q = Symbol("q", positive = True)
    >>> m = Matrix([
    ... [-2*cosh(q/3),      exp(-q),            1],
    ... [      exp(q), -2*cosh(q/3),            1],
    ... [           1,            1, -2*cosh(q/3)]])
    >>> m.nullspace()
    []
```

In Julia:

```julia
q = sympy.Symbol("q", positive = true)
m = Sym[
-2*cosh(q/3)      exp(-q)            1;
      exp(q) -2*cosh(q/3)            1;
           1            1 -2*cosh(q/3)]
m.nullspace()
```

    0-element Array{Any,1}

You can trace down which expression is being underevaluated, by injecting a custom zero test with warnings enabled.

```julia
    >>> import warnings
    >>>
    >>> def my_iszero(x):
    ...     try:
    ...         result = x.is_zero
    ...     except AttributeError:
    ...         result = None
    ...
    ...     # Warnings if evaluated into None
    ...     if result == None:
    ...         warnings.warn("Zero testing of {} evaluated into {}".format(x, result))
    ...     return result
    ...
    >>> m.nullspace(iszerofunc=my_iszero) # doctest: +SKIP
    __main__:9: UserWarning: Zero testing of 4*cosh(q/3)**2 - 1 evaluated into None
    __main__:9: UserWarning: Zero testing of (-exp(q) - 2*cosh(q/3))*(-2*cosh(q/3) - exp(-q)) - (4*cosh(q/3)**2 - 1)**2 evaluated into None
    __main__:9: UserWarning: Zero testing of 2*exp(q)*cosh(q/3) - 16*cosh(q/3)**4 + 12*cosh(q/3)**2 + 2*exp(-q)*cosh(q/3) evaluated into None
    __main__:9: UserWarning: Zero testing of -(4*cosh(q/3)**2 - 1)*exp(-q) - 2*cosh(q/3) - exp(-q) evaluated into None
    []
```

In Julia:

Is this available??

In this case, (-exp(q) - 2*cosh(q/3))*(-2*cosh(q/3) - exp(-q)) - (4*cosh(q/3)**2 - 1)**2 should yield zero, but the zero testing had failed to catch. possibly meaning that a stronger zero test should be introduced. For this specific example, rewriting to exponentials and applying simplify would make zero test stronger for hyperbolics, while being harmless to other polynomials or transcendental functions.

```julia
    >>> def my_iszero(x):
    ...     try:
    ...         result = x.rewrite(exp).simplify().is_zero
    ...     except AttributeError:
    ...         result = None
    ...
    ...     # Warnings if evaluated into None
    ...     if result == None:
    ...         warnings.warn("Zero testing of {} evaluated into {}".format(x, result))
    ...     return result
    ...
    >>> m.nullspace(iszerofunc=my_iszero) # doctest: +SKIP
    __main__:9: UserWarning: Zero testing of -2*cosh(q/3) - exp(-q) evaluated into None
    ⎡⎡  ⎛   q         ⎛q⎞⎞  -q         2⎛q⎞    ⎤⎤
    ⎢⎢- ⎜- ℯ  - 2⋅cosh⎜─⎟⎟⋅ℯ   + 4⋅cosh ⎜─⎟ - 1⎥⎥
    ⎢⎢  ⎝             ⎝3⎠⎠              ⎝3⎠    ⎥⎥
    ⎢⎢─────────────────────────────────────────⎥⎥
    ⎢⎢          ⎛      2⎛q⎞    ⎞     ⎛q⎞       ⎥⎥
    ⎢⎢        2⋅⎜4⋅cosh ⎜─⎟ - 1⎟⋅cosh⎜─⎟       ⎥⎥
    ⎢⎢          ⎝       ⎝3⎠    ⎠     ⎝3⎠       ⎥⎥
    ⎢⎢                                         ⎥⎥
    ⎢⎢           ⎛   q         ⎛q⎞⎞            ⎥⎥
    ⎢⎢          -⎜- ℯ  - 2⋅cosh⎜─⎟⎟            ⎥⎥
    ⎢⎢           ⎝             ⎝3⎠⎠            ⎥⎥
    ⎢⎢          ────────────────────           ⎥⎥
    ⎢⎢                   2⎛q⎞                  ⎥⎥
    ⎢⎢             4⋅cosh ⎜─⎟ - 1              ⎥⎥
    ⎢⎢                    ⎝3⎠                  ⎥⎥
    ⎢⎢                                         ⎥⎥
    ⎣⎣                    1                    ⎦⎦
```

In Julia:

Is this available?

You can clearly see nullspace returning proper result, after injecting an alternative zero test.

Note that this approach is only valid for some limited cases of matrices containing only numerics, hyperbolics, and exponentials. For other matrices, you should use different method opted for their domains.

Possible suggestions would be either taking advantage of rewriting and simplifying, with tradeoff of speed [#zerotestsimplifysolution-fn]_ , or using random numeric testing, with tradeoff of accuracy [#zerotestnumerictestsolution-fn]_ .

If you wonder why there is no generic algorithm for zero testing that can work with any symbolic entities, it's because of the constant problem stating that zero testing is undecidable [#constantproblemwikilink-fn]_ , and not only the SymPy, but also other computer algebra systems [#mathematicazero-fn]_ [#matlabzero-fn]_ would face the same fundamental issue.

However, discovery of any zero test failings can provide some good examples to improve SymPy, so if you have encountered one, you can report the issue to SymPy issue tracker [#sympyissues-fn]_ to get detailed help from the community.

Footnotes

[#zerotestexampleidea-fn] Inspired by https://gitter.im/sympy/sympy?at=5b7c3e8ee5b40332abdb206c

[#zerotestexamplediscovery-fn] Discovered from https://github.com/sympy/sympy/issues/15141

[#zerotestsimplifysolution-fn] Suggested from https://github.com/sympy/sympy/issues/10120

[#zerotestnumerictestsolution-fn] Suggested from https://github.com/sympy/sympy/issues/10279

[#constantproblemwikilink-fn] https://en.wikipedia.org/wiki/Constant_problem

[#mathematicazero-fn] How mathematica tests zero https://reference.wolfram.com/language/ref/PossibleZeroQ.html

[#matlabzero-fn] How matlab tests zero https://www.mathworks.com/help/symbolic/mupad_ref/iszero.html

[#sympyissues-fn] https://github.com/sympy/sympy/issues

return to index


# Modelo simplificado de la dinámica lateral

## Modelo dinámico

La dinámica de inclinación de una moto se puede aproximar al comportamiento de un péndulo invertido, si consideramos el punto de contacto de la rueda con el suelo $O$ como una articulación con el eje paralelo a la dirección de la marcha. La siguiente figura muestra un esquema del modelo, que se puede interpretar como una vista de la moto desde atrás, donde $G$ es el centro de masas:

Durante el movimiento de rodadura, el punto de contacto entre la rueda y el suelo corresponderá en cada instante a puntos materiales diferentes, tanto de la rueda como del suelo. Si la rodadura se produce sin deslizamiento, dicho punto de contacto tendrá siempre velocidad nula, por lo que constituirá el *centro instantáneo de rotación* o CIR de la rueda. Aunque el punto de contacto físico tenga velocidad nula en cada instante, la localización del contacto irá cambiando a lo largo del tiempo, siguiendo una trayectoria sobre el suelo. La velocidad a la que se produce este desplazamiento del punto de contacto se denomina *velocidad de sucesión*, y es fácil demostrar que, cuando hay rodadura sin deslizamiento y el desplazamiento es rectilíneo, coincide con la velocidad de traslación del centro de la rueda o, lo que es lo mismo, con la velocidad de avance de la moto $v$. Si el movimiento no es rectilíneo pero el radio de curvatura $\rho$ de la trayectoria es suficientemente grande, $v$ sigue siendo una buena estimación de la velocidad de sucesión.

En nuestro modelo, por lo tanto, vamos a considerar que la articulación $O$ se mueve sobre el suelo a una velocidad $v$ (que consideraremos constante para simplificar el estudio), a lo largo de una trayectoria curvilínea con radio de curvatura $\rho$, que dependerá del ángulo de dirección $u$ y de la distancia entre ejes de la moto $l$, como se verá a continuación.

## Efecto del ángulo de dirección

Para estimar la relación entre el ángulo de dirección $u$ y el radio de curvatura de la trayectoria $\rho$, consideraremos un modelo simplificado de la moto como el que se muestra en la figura, en este caso visto desde arriba:

En este modelo, a diferencia de lo que ocurre en una moto real, el eje de dirección es vertical y pasa por el centro de la rueda delantera. Por este motivo, cuando la moto no está inclinada, la distancia entre ejes $l$ permanecerá constante al girar la dirección un ángulo cualquiera $u$. En estas condiciones, para cumplir el campo de velocidades de sólido rígido del chasis sin que las ruedas tengan deslizamiento, los centros de curvatura de sus trayectorias deben coincidir en un mismo punto, que será a su vez el CIR del chasis. Se demuestra fácilmente que este punto es la intersección de los ejes de ambas ruedas, como muestra la figura anterior.

De este modo, se puede establecer la relación geométrica entre el ángulo de dirección y los radios de curvatura $\rho_f$ y $\rho_r$ de las trayectorias de las ruedas delantera y trasera, respectivamente:

$$
    \rho_f = \frac{l}{\sin u} \qquad \rho_r = \frac{l}{\tan u}
$$

Para ángulos de dirección $u$ pequeños, ambos radios se pueden aproximar a un mismo valor $\rho$:

$$
    \rho \approx \frac{l}{u}
$$

## Ecuación del movimiento

Para estudiar el movimiento del péndulo, se consideran unos ejes móviles $\bar x \bar y \bar z$ con origen en $O$, tales que $\bar y$ apunta siempre en la dirección de la marcha (es decir, es tangente a la trayectoria) y $\bar z$ apunta siempre hacia arriba, coincidiendo con el eje global $z$. Para que los ejes locales se mantengan siempre tangentes a la trayectoria, deberán tener una velocidad angular $\mathbf\Omega$:

$$
    \mathbf \Omega = \begin{bmatrix} 0 \\ 0 \\ \Omega \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \\ \frac{v}{\rho} \end{bmatrix}
$$

que, al ser en dirección $\bar z$, coincidirá con su expresión en ejes locales $\bar{\mathbf\Omega}$. Consideraremos que el radio de curvatura $r$ es positivo cuando la moto gira hacia la izquierda (velocidad angular positiva sobre el eje $z$). La velocidad angular del péndulo, expresada en los ejes locales $\bar x \bar y \bar z$, será el resultado de sumar esta velocidad angular de arrastre $\bar{\mathbf\Omega}$, a una velocidad angular relativa según el eje $\bar y$, causada por las variaciones del ángulo $\varphi$:

$$
    \bar{\mathbf\omega} = \begin{bmatrix} 0 \\ \dot\varphi \\ \frac{v}{\rho} \end{bmatrix}
$$

Para obtener la aceleración angular, hay que derivar este vector en la base móvil usando la fórmula de Boure:

$$
    \dot{\bar{\mathbf\omega}} = \left.\frac{d{\bar{\mathbf\omega}}}{dt}\right|_m + \bar{\mathbf\Omega}\times\bar{\mathbf\omega} = \begin{bmatrix} -\frac{v}{\rho}\dot\varphi \\ \ddot\varphi \\ 0 \end{bmatrix}
$$

En esta derivación, se considera que el radio de curvatura es constante, ya que técnicamente no es un grado de libertad del movimiento (no se puede variar arbitrariamente, depende de la trayectoria). Aplicando el campo de aceleraciones del sólido rígido, la aceleración del centro de masas en ejes locales $\bar{\mathbf a}_G$ será igual a:

$$
    \bar{\mathbf a}_G = \bar{\mathbf a}_O + \bar{\mathbf\omega}\times\left(\bar{\mathbf\omega}\times\bar{\mathbf{OG}}\right) + \dot{\bar{\mathbf\omega}}\times\bar{\mathbf{OG}}
$$

donde donde $\bar{\mathbf a}_O$ es la aceleración del punto de contacto $O$, que será la que le corresponde por tener una trayectoria curvilínea de radio $r$ a velocidad constante $v$:

$$
    \bar{\mathbf a}_O = \begin{bmatrix} -\frac{v^2}{\rho} \\ 0 \\ 0 \end{bmatrix}
$$

Sustituyendo esto en la expresión anterior y evaluando los productos vectoriales, se obtiene la aceleración del centro de masas en ejes locales:

$$
    \bar{\mathbf a}_G = \begin{bmatrix} -\frac{v^2}{\rho^2}\left(\rho + h\sin\varphi\right) - h\dot\varphi^2\sin\varphi + h\ddot\varphi\cos\varphi \\
                 2h\frac{v}{\rho}\dot\varphi\cos\varphi \\ - h\dot\varphi^2\cos\varphi - h\ddot\varphi\sin\varphi \end{bmatrix}
$$

Ahora que ya tenemos la aceleración del centro de masas, podemos plantear las ecuaciones de Newton-Euler para nuestro sistema, considerando únicamente la dinámica en el plano $\bar x\bar z$. Según la primera figura, donde se muestran las componentes de la aceleración de $G$ en azul, se puede ver que:

$$
    \begin{align}
        & R_x = m\bar a_{Gx} \\
        & R_z - mg = m\bar a_{Gz} \\
        & R_zh\sin\varphi - R_xh\cos\varphi = I_G\ddot\varphi
    \end{align}
$$

Estas ecuaciones son no lineales, así que hay que linealizarlas alrededor de la posición de equilibrio ($\varphi = 0$) para poder expresar la dinámica del sistema en forma *state-space*. Si se consideran valores pequeños de $\varphi$ y $\dot\varphi$, se puede asumir que $\sin\varphi \approx \varphi$, $\cos\varphi \approx 1$, $\varphi^2 \approx 0$ y $\dot\varphi^2 \approx 0$. Además, si $\rho$ es grande, podemos considerar que $\rho^2$ tiende a infinito. Aplicando estas simplificaciones, y sustituyendo los valores de $R_x$ y $R_z$ de las dos primeras ecuaciones en la tercera, se obtiene la ecuación del movimiento linealizada:

$$
    \left(I_G + mh^2\right)\ddot\varphi = mgh\varphi + mh\frac{v^2}{\rho}
$$

Sustituyendo la expresión de $\rho$ en función de $u$, se obtiene la ecuación que se utilizará para diseñar el controlador:

$$
    \left(I_G + mh^2\right)\ddot\varphi = mgh\varphi + mh\frac{v^2}{l}u
$$

Entonces, los parámetros que vamos a necesitar para diseñar el controlador son la masa total $m$, el momento de inercia respecto a un eje longitudinal que pasa por el centro de masas $I_G$, la altura del centro de masas $h$ y la distancia entre ejes $l$, además de la velocidad de avance $v$.

## Sistema linealizado en forma *state-space*

Ya tenemos la ecuación del movimiento linealizada alrededor de la posición de equilibrio, en forma de ecuación diferencial lineal con coeficientes constantes. Para poder diseñar el controlador, queremos reescribirla usando la representación *state-space*, que para un sistema SISO  (*single input, single output*) genérico, con una entrada $u$ y una salida $y$, tiene la forma:

$$
    \begin{align}
        & \dot{\mathbf x}=\mathbf{Ax}+\mathbf{B}u \\
        & y = \mathbf{Cx} + Du
    \end{align}
$$

La primera ecuación describe la dinámica del sistema, que se modeliza a través de una serie de *estados* contenidos en un vector $\mathbf x$, que son controlados por medio de una entrada o *input* $u$ (en nuestro caso, el ángulo de dirección). La matriz $\mathbf A$ y el vector $\mathbf B$ son constantes, de modo que se trata de un sistema de ecuaciones diferenciales lineales con coeficientes constantes. La segunda ecuación relaciona la salida o *output* $y$, es decir, la magnitud del sistema que queremos conocer y/o controlar (el ángulo de inclinación $\varphi$ en nuestro sistema), con los estados $\mathbf x$ y el *input* $u$, a través de una matriz $\mathbf C$ y un escalar $D$, también constantes.

La ecuación del movimiento de nuestro sistema es de segundo orden, así que tendremos que establecer el vector de estados de forma que se pueda expresar como un sistema de primer orden. Podemos elegir los estados $\mathbf x$ de la siguiente manera:

$$
    \mathbf{x}=
    \begin{bmatrix}
        \varphi \\ \dot\varphi
    \end{bmatrix}
    \implies \dot{\mathbf{x}}=
    \begin{bmatrix}
        \dot\varphi \\ \ddot\varphi
    \end{bmatrix}
$$

Esto nos permite reescribir la ecuación del movimiento en la forma $\dot{\mathbf x}=\mathbf{Ax}+\mathbf{B}u$:

$$
    \begin{bmatrix}
        \dot\varphi \\ \ddot\varphi
    \end{bmatrix} =
    \begin{bmatrix}
        0 && 1 \\ \frac{mgh}{\left(I_G+mh^2\right)} && 0
    \end{bmatrix}
    \begin{bmatrix}
        \varphi \\ \dot\varphi
    \end{bmatrix} +
    \begin{bmatrix}
        0 \\ \frac{mhv^2}{l\left(I_G+mh^2\right)}
    \end{bmatrix}u
$$

Como se mencionó más arriba, la salida del sistema $y$ es el propio ángulo de inclinación $\varphi$, por lo que los cuatro términos $\mathbf A$, $\mathbf B$, $\mathbf C$ y $D$ resultan finalmente:

$$
    \mathbf A = \begin{bmatrix} 0 && 1 \\ \frac{mgh}{\left(I_G+mh^2\right)} && 0 \end{bmatrix} \qquad
    \mathbf B = \begin{bmatrix} 0 \\ \frac{mhv^2}{l\left(I_G+mh^2\right)} \end{bmatrix} \qquad
    \mathbf C = \begin{bmatrix} 1 && 0 \end{bmatrix} \qquad
    D = 0
$$

Para poder continuar con el diseño del controlador, se asignarán valores numéricos a los parámetros. En este caso se utilizarán unos valores genéricos, cuando se tenga la moto construida habrá que estimar los valores reales y sustituirlos. Cada vez que se modifique una celda de código, hay que volver a ejecutarla usando SHIFT+ENTER. Además, habrá que ejecutar también todas las que haya debajo, o al menos las que se vean afectadas por los cambios. Se pueden usar las opciones que hay en el menú de Jupyter, para ejecutar una celda y las siguientes, o todas desde el principio, etc.

```python
import numpy as np

m = 100.0 # Masa en kg
I = 10.0  # Momento de inercia en kg·m2
v = 10.0  # Velocidad en m/s
l = 1.0   # Distancia entre ejes en m
h = 1.0   # Altura del CDG en m
g = 9.81  # Aceleración de la gravedad en m/s2

A = np.array([[0.0, 1.0], [m*g*h/(I + m*h**2), 0.0]])
B = np.array([[0], [m*h*v**2/l/(I + m*h**2)]])
C = np.array([[1.0, 0.0]])
D = np.array([[0]])

# Mostramos en la salida el contenido de A y B
print(A)
print(B)
```

# Diseño del controlador

## Estabilidad del sistema

En la bibliografía sobre Control es frecuente referirse al sistema que queremos controlar, de modo general, como *planta*. Cuando no hay realimentación, el sistema se conoce como *planta en lazo abierto*. Si consideramos el sistema libre, sin introducir ningún *input*, resultará una ecuación diferencial de la forma:

$$
    \dot{\mathbf x} = \mathbf A\mathbf x
$$

Se dice que un sistema es *asintóticamente estable* si, para unas condiciones iniciales cualesquiera, el vector de estados $\mathbf x$ tiende a cero cuando el tiempo tiende a infinito. Se puede demostrar que la solución de un sistema de este tipo es estable cuando todos los autovalores de $\mathbf A$ tienen parte real negativa. Como se puede ver, nuestro sistema no es estable, ya que tiene un autovalor con parte real positiva:

```python
# Comprobamos los autovalores de 'A'
print(np.real(np.linalg.eig(A)[0]))
```

## Primera aproximación: regulador PID

Cuando no se dispone de un modelo dinámico del sistema, la forma más simple de estabilizarlo es utilizando un regulador PID. En este caso, se podría utilizar un PID en el que la entrada fuese el error entre el ángulo de inclinación deseado y el real, y la salida el ángulo de dirección. En general, lo que hay que hacer para estabilizar una moto es girar el manillar en la dirección de la caída ([*steer into the fall*](https://youtu.be/o7nSQ2ycGX4)).

Para eso, es necesario tener una estimación del ángulo de la moto, y la aproximación más simple es utilizar el ángulo de equilibrio estático, es decir, el que mantiene la moto en equilibrio al girar con velocidad y radio constantes. Este ángulo se puede estimar a partir de la ecuación del movimiento, haciendo nulas las derivadas del ángulo $\varphi$:

$$
    mgh\varphi + mh\frac{v^2}{\rho} = 0 \implies \varphi \approx - \frac{v^2}{\rho g} = -\frac{v}{\rho}\frac{v}{g} = -\Omega\frac{v}{g}
$$

donde $\Omega$, que como recordaremos es la velocidad de rotación del sistema de referencia $\bar x\bar y\bar z$, se puede aproximar para inclinaciones pequeñas al valor proporcionado por el eje vertical del giróscopo, mientras que $v$ se puede estimar colocando un *encoder* en la rueda trasera.

El problema de utilizar este tipo de controladores es que suelen ser difíciles de ajustar, por lo que, si es posible, es mejor recurrir a métodos más avanzados, como la realimentación de estados, que se describe a continuación.

## Estabilización mediante realimentación de estados

La realimentación de estados se basa en definir la entrada del sistema $u$ como una realimentación negativa del estado, de la siguiente forma:

$$
    u = -\mathbf K\mathbf x
$$

Con este tipo de realimentación, se puede determinar de forma analítica una matriz de ganancias $\mathbf K$ que estabilice el sistema, es decir, que haga que todos los autovalores sean negativos. Para verlo más claramente, se puede introducir la realimentación en las ecuaciones del sistema:

$$
    \dot{\mathbf x} = \mathbf{Ax} - \mathbf{BKx} = \left(\mathbf A - \mathbf{BK}\right)\mathbf x
$$

Se observa que, ahora, la estabilidad del sistema la determinan los autovalores de $\mathbf A - \mathbf{BK}$. Esto significa que se puede alterar la dinámica del sistema a conveniencia, ajustando la matriz $\mathbf K$ para colocar los autovalores donde nos interese. Este método de control se puede esquematizar de la forma siguiente:

## Regulador cuadrático lineal (LQR)

El esquema de control de realimentación de estados deja abierta una decisión complicada al diseñador: ¿dónde colocar los autovalores del sistema? Existen diversos métodos descritos en la bibliografía, dedicados a determinar dónde se deben colocar los autovalores para conseguir que la respuesta del sistema presente unas características determinadas. Estas técnicas se denominan *asignación de polos* o *pole placement*.

En nuestro proyecto, vamos a utilizar un método un poco más elaborado, pero a la vez sencillo de utilizar: el LQR (*Linear Quadratic Regulator*). El LQR no es más que otro método para calcular la matriz $\mathbf K$, pero en lugar de tener que decidir dónde colocamos los polos del sistema, lo que hacemos es buscar el valor único de $\mathbf K$ que minimiza la siguiente función:

$$
    J = \int_0^\infty\left(\mathbf x^T\mathbf Q\mathbf x + Ru^2\right)dt
$$

Esta función (expresada aquí en forma específica para un sistema con un solo *inupt*) combina los valores cuadráticos, integrados a lo largo del tiempo, de las dos magnitudes que queremos minimizar: el error en los estados y la magnitud de la entrada de control. Cada término va ponderado por una matriz de pesos, $\mathbf Q$ para los estados y $R$ para el *input*, de modo que podemos decidir qué variables queremos penalizar más en el controlador. Por ejemplo, asignar un valor elevado al elemento $Q_{ii}$ de la diagonal de $\mathbf Q$ hará que los errores en el estado $x_i$ sean fuertemente penalizados en la función objetivo $J$, así que el controlador se esforzará en manternerlos bajos. Por el contrario, si se hace lo mismo con $R$, se indica al controlador que debe utilizar lo menos posible el actuador correspondiente al *input* $u$.

Modificando los elementos de $\mathbf Q$ y $R$ se puede ajustar el comportamiento del controlador a nuestros requerimientos, dando mayor relevancia al error en los estados o al esfuerzo de control. Lo primero puede ser importante si en algún estado es crítico mantener errores bajos, y lo segundo, por ejemplo, si es prioritario obtener una alta eficiencia energética, aunque sea al coste de menor precisión en el control. En nuestro caso, se podría utilizar para evitar que el controlador use ángulos de dirección demasiado grandes.

Una vez dererminadas las matrices de pesos, la función `lqr` del módulo de control de Python resuelve el problema de optimización y nos devuelve directamente la matriz de ganancias $\mathbf K$ que minimiza la función $J$:

```python
from control import ss, lqr, ss2tf, minreal, forced_response

sys = ss(A, B, C, D)

Q = np.eye(2) # Aproximación incial para 'Q'
Q[0, 0] = 10  # Ajustamos peso del ángulo 'phi'
R = 1         # Peso del ángulo de dirección 'u'

K = lqr(sys, Q, R)[0]
```

Para evaluar el comportamiento del regulador, se simula el comportamiento del sistema cuando partimos de un estado inicial fuera de equilibrio. Por ejemplo, colocamos la moto con un ángulo inicial $\varphi_0$ de 20º, mientras circula en línea recta a la velocidad prefijada $v$ (10 m/s), de modo que el controlador generará un movimiento de dirección que la estabilice en vertical.

La entrada de referencia $r$ que se ve en el diagrama del controlador se dejará a cero (se explicará más adelante para qué sirve esta entrada). Como ejercicio, es interesante comprobar cómo varía la repuesta al modificar los pesos del LQR:

```python
%%capture
from matplotlib.animation import FuncAnimation
import plotfunctions as pf
```

```python
# Ensamblamos el sistema en lazo cerrado
clsys = ss(A - B*K, B, C, D)

tf = 4.00         # Duración de la simulación en segundos
dt = 0.01         # Paso de tiempo de simulación en segundos

# Simulamos respuesta para ángulo incial de 20 grados
t = np.arange(0, tf, dt)
r = np.zeros(t.shape)

T, Y, X = forced_response(clsys, t, r, X0=[np.radians(20), 0])

pf.plot_response(T, X)
```

Como se ve, el ángulo $\varphi$ converge rápidamente a cero, estabilizando el sistema.

Podemos verificar que, al aplicar la realimentación, todos los autovalores del sistema tienen ahora parte real negativa:

```python
print(np.real(np.linalg.eig(A - B*K)[0]))
```

A continuación se muestra una animación con el movimiento resultante:

```python
FuncAnimation(pf.fg1, pf.anim, init_func=pf.init, frames=len(T), fargs=(X[[0, 1]], dt, h), interval=dt*1000, blit=True)
```

## Control de la inclinación

Cuando se aplica realimentación de estados, el controlador resultante siempre tratará de llevarlos todos al equilibrio, y eso es lo que se conoce como *regulador*. Hasta ahora, esto nos ha servido para estabilizar el sistema, manteniéndolo equilibrado en vertical. Si lo que queremos es asignar a algún *output* un valor arbitrario, hay que convertir nuestro regulador en un *servomecanismo*. El objetivo ahora es conseguir que el *output* (en nuestro caso el ángulo de inclinación $\varphi$) siga a un valor de referencia $r$, en lugar de converger hacia cero. De este modo, podremos controlar la dirección de la moto sin perder la estabilidad, ya que controlar el ángulo de inclinación implica controlar indirectamente el radio de curvatura de la trayectoria.

Antes de explicar cómo se consigue esto, es importante introducir un nuevo concepto: el *tipo* de sistema o planta. Por definición, una planta es de tipo $N$ cuando, al representarla como un sistema con realimentación unitaria (como el que se muestra en la figura), el término de grado más bajo del denominador de la función de transferencia $G$ es de grado $N$.

Es decir, si la función de transferencia en lazo abierto $G$ se escribe en la forma:

$$
    G(s) = \frac{K\left(T_as + 1\right)\left(T_bs + 1\right)\cdots\left(T_ms + 1\right)}
    {s^N\left(T_1s + 1\right)\left(T_2s + 1\right)\cdots\left(T_ps + 1\right)}
$$

el tipo de sistema corresponderá al exponente $N$ que aparece en el denominador. El tipo de un sistema determina su error de seguimiento $e$ en régimen permanente para distintos tipos de entrada. Por ejemplo, en un sistema tipo 1, el error tenderá a cero para una entrada escalón, a un valor finito para una entrada rampa, y a infinito para una entrada cuadrática o de orden superior. En cambio, un sistema tipo 0 ya mostrará un error finito en régimen permanente para una entrada escalón, y será inestable frente a una entrada rampa o superior.

Para saber el tipo de nuestro sistema, primero tenemos que representarlo según la estructura de la figura. La función de transferencia $H$ del sistema completo, entre la entrada $r$ y la salida $y$, se puede obtener sustituyendo el error de seguimiento $e$ por su valor $r - y$:

$$
    y = G\left(r - y\right) \implies H(s) = \frac{y}{r} = \frac{G}{1 + G}
$$

Entonces, se puede hacer el cambio inverso para expresar cualquier sistema con función de transferencia $H$ como sistema con realimentación unitaria:

$$
    G(s) = \frac{H}{1 - H}
$$

Comprobamos el tipo de nuestro sistema, incluyendo la realimentación, cuando consideramos que nuestro único *output* es la inclinación. La función `ss2tf` devuelve la función de transferencia $H$ de un sistema *state-space*, y `minreal` (*minimal realization*) cancela los factores comunes del numerador y el denominador:

```python
# Función de transferencia del sistema con realimentación
H = ss2tf(clsys)

# Calculamos G para transformar en sistema con realimentación unitaria
minreal(H/(1 - H))
```

Mirando el denominador, se ve claramente que nuestro sistema realimentado es de tipo 0. Eso significa que, para una entrada escalón, la salida $\varphi$ nunca alcanzará el valor de referencia $r$ en régimen permanente. Si queremos controlar la inclinación correctamente, tendremos que convertir el sistema en uno de tipo 1.

## Servomecanismo tipo 1 para controlar la inclinación

Existen varios métodos para convertir un sistema de tipo 0 en uno de tipo 1. Lo más sencillo es escalar la entrada de referencia $r$ mediante una ganancia de precompensación $\bar N$, que haga que el *output* que queremos controlar (en nuestro caso la inclinación $\varphi$) alcance el valor solicitado en régimen permanente. En la figura se muestra un diagrama del sistema original, con la precompensación aplicada en la entrada:

Al añadir la precompensación, la entrada del sistema en lazo abierto pasa a ser:

$$
    u = -\mathbf{Kx} + \bar Nr
$$

Vamos a estudiar lo que ocurre con $\mathbf x$ y $u$ cuando tenemos una entrada de referencia $r$ de tipo escalón unitario. Supondremos que, en régimen permanente, la salida $y$ alcanza efectivamente el valor de referencia (es decir, la unidad). Como en régimen estacionario las derivadas de los estados $\dot{\mathbf x}$ serán nulas, podemos escribir:

$$
    \begin{bmatrix} \mathbf 0 \\ 1 \end{bmatrix} =
    \begin{bmatrix} \mathbf A && \mathbf B \\ \mathbf C && D \end{bmatrix}
    \begin{bmatrix} \mathbf{x_\infty} \\ u_\infty \end{bmatrix}
$$

Si la matriz de este sistema lineal es invertible, resolviéndolo obtendremos el valor de $\mathbf x_\infty$ y $u_\infty$. Como la referencia $r$ es un escalón unitario, la entrada del sistema en lazo abierto alcanzará el siguiente valor:

$$
    u_\infty = -\mathbf{Kx}_\infty + \bar N
$$

de donde obtenemos $\bar N$ como:

$$
    \bar N = \mathbf{Kx}_\infty + u_\infty
$$

```python
# Precompensación para salida 'xp' en régimen permanente
xuinf = np.linalg.solve(np.block([[A, B], [C, D]]), [0, 0, 1])
Nb = K@xuinf[:-1] + xuinf[-1]
```

Antes de simular la respuesta, podemos montar el sistema en lazo cerrado con precompensación en la entrada, y comprobar su tipo:

```python
# Sistema en lazo cerrado, incluyendo la precompensación
clsyspc = ss(A - B*K, B*Nb, C, D)

# Función de transferencia del sistema con realimentación
H = ss2tf(clsyspc)

# Calculamos G(s) para sistema con realimentación unitaria
minreal(H/(1 - H))
```

Vemos que el sistema se puede considerar prácticamente de tipo 1, ya que el término de grado 0 del denominador es cero o despreciable (varía según la plataforma donde se ejecute el código).

Para evaluar el comportamiento del controlador, se simulará una maniobra simple, equivalente a tomar una curva hacia la derecha. Partiendo del equilibrio en línea recta, se pasará el ángulo deseado de 0 a 20 grados, se mantendrá así durante dos segundos, y se volverá a poner a cero después.

```python
# Simulamos respuesta a escalón
t = np.arange(0, tf, dt)
r = np.zeros(len(t))
r[t < 2] = np.radians(20)

T, Y, X = forced_response(clsyspc, t, r)

pf.plot_response(T, X)
```

Se observa que el sistema alcanza efectivamente el ángulo objetivo, y vuelve a la vertical de forma controlada.

Antes de implementar este controlador, hay que tener cuidado con los valores del *input*, ya que al estar el modelo linealizado, pueden tomar cualquier valor. Esto quiere decir que nos pueden salir ángulos de dirección poco realistas, como 120 grados. Es importante entonces observar los ángulos que está utilizando el controlador:

```python
pf.plot_steering(T, (Nb*r - K@X).T)
```

Vemos que, para alcanzar el ángulo solicitado de 20 grados ($\varphi$ positivo, hacia la derecha), la dirección gira brevemente 60 grados ($u$ positivo, hacia la izquierda), y cuando alcanza la inclinación deseada se mantiene con un ligero ángulo hacia la derecha. Después, cuando se le pide volver a la vertical al cabo de 2 segundos, da un toque aún más hacia la derecha, que hace que la moto se vaya dirigiendo de nuevo a la vertical. Esto es bien conocido por cualquiera que haya montado en moto, y es lo que se conoce como *contramanillar*: para girar hacia un lado, antes hay que dar un toque al manillar hacia el lado contrario, lo que ayuda a inclinar la moto en la dirección buscada.

El problema es que un giro de 60 grados se sale de la zona de validez del modelo lineal, aparte de que un golpe de manillar de esa magnitud en una moto que circula a 10 m/s no parece muy razonable. Para corregir esto, hay que reajustar los pesos en el LQR, aumentando el peso del esfuerzo de control $\mathbf R$ para penalizar el uso de ángulos grandes, con la contrapartida de que la maniobra será más lenta. Es interesante probar diferentes valores, volviendo a ejecutar el código para ver cómo varía la respuesta.

A continuación se muestra una animación del movimiento obtenido:

```python
FuncAnimation(pf.fg1, pf.anim, init_func=pf.init, frames=len(T), fargs=(X[[0, 1]], dt, h), interval=dt*1000, blit=True)
```

Los parámetros obtenidos ya sirven para controlar la inclinación de la moto. Los valores mostrados más abajo se pueden copiar y pegar directamente en un *sketch* de Arduino, de forma que, para implementar el controlador, sólo habría que calcular el ángulo de dirección como $u = -\mathbf{Kx} + \bar Nr$.

```python
print('const float K1 =%10.6f;  // Ganancia phi'    % K[0, 0])
print('const float K2 =%10.6f;  // Ganancia phip'   % K[0, 1])
print('const float Nb =%10.6f;  // Precompensación' % Nb)
```

Una de las principales desventajas de los métodos de realimentación de estados es que, para calcular $u$, necesitamos conocer el valor de todos los estados (además de los parámetros del sistema). Esto es un problema, porque normalmente no todos los estados se pueden medir fácilmente con precisión. En estos casos, lo que se hace es utilizar un *observador*, que consiste en un modelo del sistema que se va integrando en el tiempo, corrigiendo los errores de integración a partir de los datos de los sensores.

Como el uso de observadores embebidos en el controlador queda fuera del ámbito de este proyecto, es usará una estimación de estados simplificada. En nuestro sistema, el vector $\mathbf x$ contiene dos estados: $\varphi$ y $\dot\varphi$. El primero se puede estimar usando el método descrito más arriba, utilizando las condiciones de equilibrio estático. El segundo es muy fácil de medir, ya que nos lo proporciona directamente la lectura del eje longitudinal del giróscopo. Si esto no resultase válido, se podría plantear el uso de un observador más avanzado.

## Servomecanismo tipo 1 con integrador en la entrada

El problema de usar precompensación es que la respuesta es muy sensible a errores en los parámetros del sistema. Cualquier desviación en el valor de $\bar N$ hará que el sistema vuelva a ser de tipo 0, y por lo tanto tenga error en régimen permanente. La manera de resolver esto de forma más robusta es añadir a la entrada un integrador, en lugar de una simple ganancia:

Para utilizar este esquema de control, hay que aumentar el sistema añadiéndole un estado adicional $\xi$, que será la integral del error de seguimiento $r - y$. Por lo tanto, su derivada es:

$$
    \dot\xi = r - y = r - \left(\mathbf{Cx} + Du\right)
$$

y la entrada $u$ pasa a ser ahora:

$$
    u = -\mathbf K \mathbf x + k_I\xi
$$

La ecuación completa de la dinámica del sistema aumentado, sin incluir la realimentación, se puede escribir como:

$$
    \begin{bmatrix} \dot{\mathbf x} \\ \dot\xi \end{bmatrix} =
    \begin{bmatrix} \mathbf A && \mathbf 0 \\ -\mathbf C && 0 \end{bmatrix}
    \begin{bmatrix} \mathbf{x} \\ \xi\end{bmatrix} +
    \begin{bmatrix} \mathbf B \\ -D \end{bmatrix}u +
    \begin{bmatrix} \mathbf 0 \\ 1 \end{bmatrix}r
$$

Estudiaremos la respuesta de este sistema a una entrada escalón, cuando el tiempo tiende a infinito. Para ello, definiremos las siguientes magnitudes:

$$
    \mathbf e = \begin{bmatrix} \mathbf{x} - \mathbf{x}_\infty \\ \xi - \xi_\infty \end{bmatrix} \qquad
    u_e = u - u_\infty
$$

que representan las desviaciones del estado y la entrada respecto sus propios valores en régimen permanente. Como $r$ es un escalón, su valor es siempre el mismo para $t>0$, de forma que podemos escribir la dinámica del error como:

$$
    \dot{\mathbf e} =
    \begin{bmatrix} \mathbf A && \mathbf 0 \\ -\mathbf C && 0 \end{bmatrix}
    \mathbf e +
    \begin{bmatrix} \mathbf B \\ -D \end{bmatrix}u_e = \hat{\mathbf A}\mathbf e + \hat{\mathbf B}u_e
$$

Se puede observar que, de forma análoga a lo que ocurría al principio, tenemos un sistema en forma *state-space* cuyos estados tienen que converger asintóticamente a cero. Eso quiere decir que existirán unas ganancias de realimentación $\hat{\mathbf K}$ que estabilicen el sistema:

$$
    \hat{\mathbf K} = \begin{bmatrix} \mathbf K && -k_I \end{bmatrix}
$$

Estas ganancias se pueden calcular, como antes, utilizando el algoritmo LQR, sabiendo que ahora tenemos tres estados, ya que el último corresponde a la integral del error de seguimiento en la velocidad:

```python
# Ecuaciones del sistema con los errores como estados
# Se añade la integral del error en ángulo como tercer estado
Ah = np.block([[A, np.zeros((2, 1))], [-C, 0]])
Bh = np.block([[B], [-D]])

# Controlador LQR para la dinámica del error
Q = np.eye(3)  # Aproximación inicial para 'Q'
Q[0, 0] = 1.0  # Ajustamos peso del ángulo 'phi'
Q[1, 1] = 0.0  # Ajustamos peso de la derivada del ángulo 'phip'
Q[2, 2] = 25   # Peso de la integral del error en velocidad 'xi'
R = 1          # Peso del ángulo de dirección 'u'

# La función 'LQR' también admite usar sólo las matrices A y B
Kh = lqr(Ah, Bh, Q, R)[0]
Kn = Kh[0, :-1]
Ki = -Kh[0, -1]
```

Para simular el sistema, utilizaremos un modelo aumentado de la siguiente forma, que se obtiene al sustituir en la ecuación del sistema el valor de $u$:

$$
    \begin{bmatrix} \dot{\mathbf x} \\ \dot\xi \end{bmatrix} =
    \begin{bmatrix} \mathbf A - \mathbf{BK} && \mathbf Bk_I \\ D\mathbf K - \mathbf C && -Dk_I \end{bmatrix}
    \begin{bmatrix} \mathbf{x} \\ \xi\end{bmatrix} +
    \begin{bmatrix} \mathbf 0 \\ 1 \end{bmatrix}r
$$

Antes de simular, verificamos que este sistema es de tipo 1:

```python
# Sistema en lazo cerrado con las ganancias obtenidas
An = np.block([[A - B*Kn, B*Ki], [D*Kn - C, -D*Ki]])
Bn = [[0], [0], [1]]
Cn = [1, 0, 0]
Dn = 0

clisysint = ss(An, Bn, Cn, Dn)

# Función de transferencia del sistema con realimentación
H = ss2tf(clisysint)

# Calculamos G(s) para sistema con realimentación unitaria
minreal(H/(1 - H))
```

Para comprobar que este método es mucho más robusto que la precompensación, se puede hacer una prueba sencilla. En el modelo con precompensación, si antes de ensamblar el sistema *state-space* se simula un error en los parámetros del modelo multiplicando $\bar N$ por un factor, por ejemplo 0.9, se verá que el sistema pasa a ser tipo 0, incluso con valores del factor muy cercanos a la unidad. En cambio, si se hace lo mismo con $k_I$, se observa que el término de grado 0 del denominador se mantiene cercano a cero para cualquier valor de $k_I$.

A continuación se muestra gráficamente la respuesta, comparándola con la obtenida usando precompensación (en línea de puntos). Igual que antes, es interesante probar diferentes pesos en el LQR, para ver cómo varía el comportamiento del sistema.

```python
# Respuesta con misma entrada que en el modelo con precompensación
T, Y, Xn = forced_response(clisysint, t, r)

pf.plot_response(T, Xn, X)
```

Antes de implementar el controlador, hay que asegurarse de que no estamos saturando el actuador:

```python
pf.plot_steering(T, (Ki*Xn[2, :] - Kn@Xn[:2, :]).T)
```

Vemos que los ángulos de dirección se mantienen dentro de valores mucho más realistas. Como siempre, es interesante ver cómo afectan los pesos del LQR, buscando un equilibrio entre una respuesta rápida (pero más brusca) y un comportamiento suave (pero más lento).

A continuación se muestra una animación del nuevo movimiento:

```python
FuncAnimation(pf.fg1, pf.anim, init_func=pf.init, frames=len(T), fargs=(Xn[[0, 2]], dt, h), interval=dt*1000, blit=True)
```

Como todo es correcto, ya podemos utilizar las ganancias para la implementación física del controlador. Se pueden copiar y pegar directamente en un sketch de Arduino, siendo ahora el ángulo de dirección $u= -\mathbf{Kx} + k_I\xi$. Esto significa que, además de los estados anteriores, habrá que ir calculando la integral del error de inclinación, que no es más que ir acumulando en una variable dicho error $\xi = r - \varphi$, multiplicado por el paso de tiempo.

```python
print('const float K1 =%10.6f;  // Ganancia phi'      % Kn[0, 0])
print('const float K2 =%10.6f;  // Ganancia phip'     % Kn[0, 1])
print('const float Ki =%10.6f;  // Ganancia integral' % Ki)
```

## Funcionamiento a diferentes velocidades

Todo lo que se ha hecho hasta ahora ha implicado considerar la velocidad de avance $v$ como un parámetro más del modelo, como la masa o el momento de inercia. Eso quiere decir que las ganancias del controlador que obtengamos estarán optimizadas para funcionar a esa velocidad. En la práctica, lo más probable es que el controlador funcione correctamente en un rango determinado de velocidades, pero si nos alejamos mucho de $v$, empezará a fallar. Una forma de comprobar la sensibilidad del sistema es calcular las ganancias para diferentes velocidades: cuanto menos varíen las ganancias al cambiar la velocidad, más robusto será el controlador.

Si la variabilidad de ganancias es alta y se quiere un controlador que pueda funcionar a diferentes velocidades, habrá que calcular varios vectores de ganancias $\mathbf K_i$, de modo que cada uno tendrá validez en un rango de velocidades. Incluso se puede estudiar la variación de las ganancias en función de la velocidad, e implementar alguna estrategia de interpolación.

# Bibliografía
[1] Ogata, K. *Modern Control Engineering, 5th Ed.*, Pearson, 2010
[2] Williams II, R. L., Lawrence, D. A. *Linear State-Space Control Systems*, John Wiley & Sons, 2007
[3] Hespanha, J. P. *Linear Systems Theory, 2nd Ed.*, Princeton University Press, 2018
[4] Åström, K. J., Murray, R. M., [*Feedback Systems*](http://www.cds.caltech.edu/~murray/amwiki/index.php), Princeton University Press, 2008
[5] Messner, B. et al. [*Control Tutorials for Matlab and Simulink*](http://ctms.engin.umich.edu)

# Acerca de este documento
Autor: Urbano Lugrís Armesto
[Laboratorio de Ingeniería Mecánica](http://lim.ii.udc.es/)
[Escuela Politécnica Superior](https://eps.udc.es/)
[Universidad de A Coruña](https://www.udc.es/)

Este documento pertenece a la asignatura *Proyecto Interdisciplinar II*, de cuarto curso de los Grados en Ingeniería Mecánica e Ingeniería en Tecnologías Industriales, impartidos en la Escuela Politécnica Superior de la Universidad de A Coruña.


# Lecture 07

## Integration II:

### Integration by Parts and by Partial Fractions

```python
import numpy as np
import sympy as sp
sp.init_printing()
##################################################
##### Matplotlib boilerplate for consistency #####
##################################################
from ipywidgets import interact
from ipywidgets import FloatSlider
from matplotlib import pyplot as plt

%matplotlib inline

from IPython.display import set_matplotlib_formats
set_matplotlib_formats('svg')

global_fig_width = 10
global_fig_height = global_fig_width / 1.61803399
font_size = 12

plt.rcParams['axes.axisbelow'] = True
plt.rcParams['axes.edgecolor'] = '0.8'
plt.rcParams['axes.grid'] = True
plt.rcParams['axes.labelpad'] = 8
plt.rcParams['axes.linewidth'] = 2
plt.rcParams['axes.titlepad'] = 16.0
plt.rcParams['axes.titlesize'] = font_size * 1.4
plt.rcParams['figure.figsize'] = (global_fig_width, global_fig_height)
plt.rcParams['font.sans-serif'] = ['Computer Modern Sans Serif', 'DejaVu Sans', 'sans-serif']
plt.rcParams['font.size'] = font_size
plt.rcParams['grid.color'] = '0.8'
plt.rcParams['grid.linestyle'] = 'dashed'
plt.rcParams['grid.linewidth'] = 2
plt.rcParams['lines.dash_capstyle'] = 'round'
plt.rcParams['lines.dashed_pattern'] = [1, 4]
plt.rcParams['xtick.labelsize'] = font_size
plt.rcParams['xtick.major.pad'] = 4
plt.rcParams['xtick.major.size'] = 0
plt.rcParams['ytick.labelsize'] = font_size
plt.rcParams['ytick.major.pad'] = 4
plt.rcParams['ytick.major.size'] = 0
##################################################
```

## Wake Up Exercise

$\displaystyle \int_1^2 x~dx=$

$\displaystyle \int_1^{\sqrt[3]3} t^2~dt =$

$\displaystyle \int_2^4 \frac{1}{\sqrt z}~dz=$

## Integration reverses the process of differentiation

### In general:

$${d(a\thinspace x^{n+1})\over dx}=(n+1)a\thinspace~x^n~~~~~
\Rightarrow ~~~~\int_{x_1}^{x_2} a\thinspace x^n~dx = \biggr[{a\over
(n+1)} \thinspace x^{(n+1)}\biggl]_{x_1}^{x_2}$$

### Reminder: method 1: integration by substitution

This method can be thought of as an integral version of the chain rule.
Suppose we wish to integrate:

$$I=\int f(g(x))~dx= \int f(u)~dx$$

$$I =\int f(u){dx\over du}~du$$

## Integration Method 2: Integration by Parts

Recall that:
$${d\over dx}f(x)~g(x)=f(x)~g'(x)+ g(x)~f'(x)$$
Integrate and rearrange to get:

$$ \int\limits_a^b
f(x)~g'(x)~dx=\biggr[\;f(x)~g(x)\;\biggl]_a^b-\int\limits_a^b
g(x)~f'(x)~dx $$

 This is known as the formula for
**'integrating by parts'**

can also be written:

$$\int\limits_a^b u~{dv\over dx}~dx ~~= ~~
\biggr[uv\biggl]^b_a-\int\limits_a^b v~{du\over dx}~dx$$

or

$$\int\limits_a^b u~v'~dx~~ =~~
\biggr[uv\biggl]^b_a-\int\limits_a^b v~u'~dx$$ with $f(x) \equiv u$ and $g(x) \equiv v$

## Example 1:

$$\int\limits_a^b x~\sqrt{(x+1)}~dx=\int\limits_a^b x~(x+1)^{1/2}~dx$$

Choose
$$u=x\qquad\mbox{and}\qquad v'=\sqrt{(x+1)}$$

so that:
$$u'=1\qquad\mbox{and}\qquad v={2 \over 3}(x+1)^{3/2}$$

Choose
$$u=x\qquad\mbox{and}\qquad v'=\sqrt{(x+1)}$$

so that:
$$u'=1\qquad\mbox{and}\qquad v={2 \over 3}(x+1)^{3/2}$$

$$\int\limits_a^b x~\sqrt{(x+1)}~dx =\biggr[x~{2\over 3}(x+1)^{3/2}\biggl]_a^b - \int\limits_a^b 1 \times {2\over 3}(x+1)^{3/2}~dx$$

$$=\biggr[{2\over 3}~x~(x+1)^{3/2}\biggl]_a^b-
\biggr[{2\over 3}\cdot {2\over 5}(x+1)^{5/2}\biggl]_a^b$$

If choose the other option for $u$ and $v'$,
get:

$$\biggr[(x+1)^{1/2}~{x^2\over 2}\biggl]_a^b
- \int\limits_a^b {1\over 2}~{1\over \sqrt{(x+1)}}~{x^2\over
2}~dx$$

The second term is **worse** than the integral we started with!
It's important to choose $u$ and $v$ carefully.
This comes with practice.

## Example 2:

Let us calculate the indefinite integral,

$$\int \ln x~dx$$

We can do a bit of a trick.  Let:

$$u=\ln x \quad\Longrightarrow\quad u'={1\over x}\qquad\mbox{and}\qquad v'=1\Longrightarrow v=x$$

Putting these into our equation:
$$\int \ln x~dx\quad=\quad\ln x~\cdot x - \int x~\frac{1}{x}dx\quad=\quad x \ln x - \int 1 dx\quad=\quad x \ln x - x + C$$

```python
x = sp.symbols('x')
sp.integrate(sp.log(x),x)

```

## Example 3: $\int\limits^{\infty}_0 x^3~e^{-x}~dx$:

$$\biggl(\int u\thinspace v'\thinspace dx = [u\thinspace v]-\int v\thinspace u'\thinspace dx\biggr)$$

Choose
$$u=x^3\qquad\mbox{and}\qquad v'=e^{-x}$$

so that:
$$u'=3x^2\qquad\mbox{and}\qquad v=-e^{-x}$$

$$\int\limits^{\infty}_0 x^3~e^{-x}~dx=-\biggr[x^3~e^{-x}\biggl]^{\infty}_0
+\int\limits^{\infty}_0 3x^2~e^{-x}~dx$$

Now apply integration by parts to the right-hand side:

Choose
$$u=3x^2\qquad\mbox{and}\qquad v'=e^{-x}$$

so that:
$$u'=6x\qquad\mbox{and}\qquad v=-e^{-x}$$

$$\int\limits^{\infty}_0 3x^2~e^{-x}~dx=-\biggr[3x^2~e^{-x}\biggl]^{\infty}_0 +\int\limits^{\infty}_0 6x~e^{-x}~dx$$

And, once more:

Choose
$$u=6x\qquad\mbox{and}\qquad v'=e^{-x}$$

so that:
$$u'=6\qquad\mbox{and}\qquad v=-e^{-x}$$

$$\int\limits^{\infty}_0 6x~e^{-x}~dx =-\biggr[6x~e^{-x}\biggl]^{\infty}_0 +\int\limits^{\infty}_0 6~e^{-x}~dx$$

$$\Longrightarrow \int\limits^{\infty}_0 6~e^{-x}~dx=
-\biggr[6~e^{-x}\biggl]^{\infty}_0=-6e^{-\infty}+6e^0=6$$

(Since $e^{-\infty}=0$ and $e^0=1$)

The other terms all go to zero:

$$-\bigr[x^3~e^{-x}\bigl]^{\infty}_0 =-{\infty}^3~e^{-\infty} + 0 =0$$

$$-\bigr[3x^2~e^{-x}\bigl]^{\infty}_0 =-3{\infty}^2~e^{-\infty} + 0 =0$$

So, to answer our original question:
$$\int\limits^{\infty}_0 x^3~e^{-x}~dx=6$$

```python
sp.integrate(x**3 * sp.exp(-x),(x,0,sp.oo))

```

and in general:
$$\int\limits^{\infty}_0 x^n~e^{-x}~dx=n!$$

## Example 4. Integrating Trig functions by parts.

Recall that:
$${d\over dx}(\sin x)=\cos x$$

$${d\over dx}(\cos x)=-\sin x$$

$$\int\limits^b_a~\cos x\;e^{-x}~dx = I$$

Choose
$$u=\cos x\qquad\mbox{and}\qquad v'=e^{-x}$$

so that:
$$u'=-\sin x\qquad\mbox{and}\qquad v=-e^{-x}$$

$$I =\biggr[-\cos x~\; e^{-x}\biggl]^b_a
-\int\limits^b_a ~(-)\sin x~(-)e^{-x}~dx$$

$$I =\biggr[-\cos x~\; e^{-x}\biggl]^b_a~~-~~\int\limits^b_a ~(-)\sin x~(-)e^{-x}~dx$$

Choose
$$u=\sin x\qquad\mbox{and}\qquad v'=e^{-x}$$

so that:
$$u'=\cos x\qquad\mbox{and}\qquad v=-e^{-x}$$

$$I =\biggr[-\cos x~\;~e^{-x}\biggl]^b_a~~-~~\biggr[\sin x~(-)e^{-x}\biggl]^b_a ~~+~~\int\limits^b_a ~\cos x~(-)e^{-x}~dx$$

The last term is the integral we started with:
$$\Longrightarrow~~~2~\int\limits^b_a ~\cos x~e^{-x}~dx~ =~\biggr[\sin x~\; e^{-x}\biggl]^b_a~~ -~~\biggr[\cos x~\; e^{-x}\biggl]^b_a$$

## Integration Method 3: Partial Fractions

**Example 7**

$$\int {dx \over (2x+1)(x-5)}$$
$${\rm Let~~~~~} {1 \over (2x+1)(x-5)}={A\over (2x+1)} + {B\over (x-5)}$$

Multiply both sides by $(2x+1)(x-5)$ to get:
$A(x-5)+B(2x+1)=1$ so $Ax-5A+B2x+B=1$

 Equate coefficients of $x$:
$$A+2B=0~~~~{\rm thus~~} A=-2B~~~~~~~~~~\rm (A)$$
Equate units (coefficients of $x^0$):
$$-5A+B=1 ~~{\rm~~so~from~(A):~~} 10B+B=1,~~B={1\over 11}~,~~A=-{2\over 11}$$

Thus:
$$\int {dx \over (2x+1)(x-5)}=-\int {2 dx\over 11(2x+1)} + \int {dx\over 11(x-5)}$$
Now use method of substitution on this:

$u=2x+1$
and $du/dx=2$ so $dx/du=1/2$

 $w=x-5$ so $dw/dx=1$ and $dx/dw=1$

$$\int {2 du \over 2 \times 11 \times u} + \int {dw\over 11w}= -{\ln u\over 11} + {\ln w \over 11}$$

$$=-{\ln |2x+1|\over 11} + {\ln |x-5| \over 11}$$

```python
sp.integrate(1/((2*x + 1)*(x - 5)),x) # answer seems different because of the arbitrary constant!

```


```python
%matplotlib widget
```

```python
from sympy import *
x, y, z = symbols('x y z')
init_printing(use_unicode=True, wrap_line=False)
```

```python
from sympy.physics.continuum_mechanics.beam import Beam
E, I = symbols('E, I')
b = Beam(9, E, I)
```

```python
b.apply_load(-12, 9, -1)
b.apply_load(50, 5, -2)
b.apply_load(-8, 0, 0, end=5)
```

```python
b.bc_deflection.append((0, 0))
b.bc_slope.append((0, 0))
```

```python
R, M = symbols('R, M')
b.apply_load(R, 0, -1)
b.apply_load(M, 0, -2)
b.load
```

```python
b.solve_for_reaction_loads(R, M)
b.reaction_loads
```

```python
b.load
```

```python
b.shear_force()
```

```python
b.bending_moment()
```

```python
b.slope()
```

```python
b.deflection()
```

```python
b.plot_shear_force()
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    C:\Users\spart\miniconda3\envs\bmcs_env\lib\site-packages\sympy\plotting\plot.py:1486: RuntimeWarning: invalid value encountered in double_scalars
      cos_theta = dot_product / (vector_a_norm * vector_b_norm)

    <sympy.plotting.plot.Plot at 0x1fd4c670940>

```python
b.plot_bending_moment()
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    <sympy.plotting.plot.Plot at 0x1fd4c5be7c0>

```python
b.plot_slope(subs={E: 20E9, I: 3.25E-6})
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    <sympy.plotting.plot.Plot at 0x1fd4d991cd0>

```python
b.plot_deflection(subs={E: 20E9, I: 3.25E-6})
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    <sympy.plotting.plot.Plot at 0x1fd4de00ac0>

```python
b.plot_loading_results(subs={E: 20E9, I: 3.25E-6})
```

    Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …

    <sympy.plotting.plot.PlotGrid at 0x1fd4e4f4af0>


```julia
using JuMP, Cbc, Plots
```

# MS-E2121 - Linear optimization
## Exercise session 1

First, we warm up with two small optimization problems. After that, you are expected to solve a small transportation problem.
#'
### Demo exercise a)
\begin{align}
    \max \ & x_1 + 2x_2 + 5x_3 \\
    \text{s.t. } & x_1 - x_2 -3x_3 && \geq 5 \\
    & x_1 + 3x_2 - 7x_3 && \leq 10 \\
    & x_1 && \leq 10 \\
    & x_i \geq 0, \ \forall i \in \{1,2,3\}
\end{align}

```julia
model_a = Model(Cbc.Optimizer);
```

```julia
@variable(model_a, x[1:3] >= 0)
```

```julia
@objective(model_a, Max, x[1] + 2*x[2] + 5*x[3])
```

```julia
@constraint(model_a, x[1] - x[2] - 3*x[3] >= 5)
@constraint(model_a, x[1] + 3*x[2] - 7*x[3] <= 10)
@constraint(model_a, x[1] <= 10)
```

```julia
println(model_a)
```

```julia
optimize!(model_a)
```

#### Extracting the solutions:
- termination_status(model) gives the reason for stopping the optimization process.
- objective_value(model) gives the final objective value
- value(model[:variable]) gives the final value of the given variable in the model. We could also do value(x) instead, since we only have one model. If we have multiple models each with a variable named x, value(x) would return the values corresponding to the variable that was defined last.

```julia
println(termination_status(model_a))
println(objective_value(model_a))
println(value.(model_a[:x]))        # the dot notation value.() means the function value() element-wise to collection type input.
```

```julia
if termination_status(model_a) == MOI.OPTIMAL
    println("Found optimal solution $(objective_value(model_a)) at x=$(value.(model_a[:x]))")
else
    println("Optimization terminated with status $(termination_status(model_a))")
end
```

### Demo exercise b)
\begin{align}
    \max \ & 2x_1 + 4x_2 \\
    \text{s.t. } & x_1 + x_2 && \leq 5 \\
    & -x_1 + 3x_2 && \leq 1 \\
    & x_1 && \leq 5 \\
    & x_2 && \leq 5 \\
    & x_i \geq 0, \ \forall i \in \{1,2\}
\end{align}

$~$
$~$

While it is possible to write out the constraints individually, it is sometimes preferable to use the matrix form:
\begin{align}
    \max \ & c^\top x \\
    \text{s.t. } & Ax && \leq b \\
    & x_i \geq 0, \ \forall i \in \{1,2\},
\end{align}

$~$
$~$

where for this problem, $c = \begin{bmatrix}
2 \\
4
\end{bmatrix}$, $A = \begin{bmatrix}
1 & 1 \\
-1 & 3 \\
1 & 0 \\
0 & 1
\end{bmatrix}$ and $b = \begin{bmatrix}
5 \\
1 \\
5 \\
5
\end{bmatrix}$

```julia
c = [2 4]
A = [1 1; -1 3; 1 0; 0 1]
b = [5 1 5 5]'
model_b = Model(Cbc.Optimizer);
```

```julia
@variable(model_b, x[i=1:2] >= 0)
```

```julia
@objective(model_b, Max, sum(c[i]*x[i] for i in 1:2))
```

```julia
@constraint(model_b, [j in 1:4], sum(A[j,i]*x[i] for i in 1:2) .<= b[j])
```

```julia
println(model_b)
```

```julia
optimize!(model_b)
```

```julia
println(termination_status(model_b))
println(objective_value(model_b))
println(value.(model_b[:x]))
```

```julia
if termination_status(model_b) == MOI.OPTIMAL
    println("Found optimal solution $(objective_value(model_b)) at x=$(value.(model_b[:x]))")
else
    println("Optimization terminated with status $(termination_status(model_b))")
end
```

We can also plot the constraints, the corresponding feasible region and the objective function at the optimal point to verify our result. We can see that the objective function indeed touches a vertex of the feasible region at x = (3.5, 1.5).

```julia
function plot_model(A, b, c; sol=nothing, xlim=5, ylim=5)

    # Check problem dimensions and throw an error if not 2D
    m,n = size(A)
    if n != 2
        throw(ArgumentError("Model is not 2-dimensional"))
    end

    # Some ranges to help with plotting, these are similar to what you may have used in Matlab
    x1_range = collect(0:0.1:5)
    x2_range = collect(0:0.1:5)

    # Create empty plot
    plot(xlim=(-1,6), ylim=(-1,6), xlabel="x1", ylabel="x2")

    # Add lines Ax=b to the plot
    for con in 1:m
        a_x1 = A[con,1]
        a_x2 = A[con,2]
        if a_x1!=0 && a_x2!=0
            plot!(x1_range, (-a_x1*x1_range.+b[con])/a_x2, label="$(a_x1==1 ? "" : a_x1)x1+$(a_x2==1 ? "" : a_x2)x2=$(b[con])")
        elseif a_x1!=0 && a_x2==0
            vline!([b[con]], label="$(a_x1==1 ? "" : a_x1)x1=$(b[con])")
        elseif a_x1==0 && a_x2!=0
            hline!([b[con]], label="$(a_x2==1 ? "" : a_x2)x2=$(b[con])")
        end
    end
    vline!([0], label="x1=0")
    hline!([0], label="x2=0")

    # Collect the points in the feasible region using a grid
    feasible_region = []
    for x1 in x1_range
        for x2 in x2_range
            if all(A*[x1;x2].<=b)
                push!(feasible_region, [x1, x2])
            end
        end
    end

    # Turn the array of arrays into a 2D-array and plot the points
    feasible_region = hcat(feasible_region...)
    scatter!(feasible_region[1,:], feasible_region[2,:], ms=1, label=false)

    # If objective function value was given as an argument, plot the objective function with that value
    if !isnothing(sol)
        plot!(x1_range, (-c[1]*x1_range.+sol)/c[2], linestyle=:dash, linewidth=2, color=:black, label="Objective function")
    end

    # This is how you would add a title to your plot
    title!("")
end

plot_model(A,b,c; sol=13)
```

### Student exercise

_Source: Julia Programming for Operations Research 2$^{nd}$ edition (Online)._
_Available at: http://www.chkwon.net/julia - published by Changhyun Kwon_

$~$
$~$

Consider the following network where the combined supplies from the Austin and Buffalo nodes need to meet the demands coming from Chicago, Denver, and Erie. The supplies available are represented by $S$ and the demands by $D$, the costs of transporting in each arc connecting supply and demand nodes are shown as $c$.

Solve the transportation problem to the minimum cost. The data is given, you will need to:

- create decision variables (What are the decisions in this problem? The amount transported from city i to city j.)
- formulate the objective function (What are we minimizing? The total transportation cost.)
- write constraints (What constraints need to be satisfied? Suppliers cannot send out more than their supply capacity and the demand in each node must be fulfilled.)

The correct optimal cost is 251.

#### Mathematical Fomularion of Transportation Problem
##### Algebaric (Symbolic) Form
For sets $I = \{ \text{Austin}, \text{Buffalo} \}$ and $J = \{ \text{Chicago}, \text{Denver}, \text{Erie} \}$ of the supply and demand cities, respectively,
\begin{align}
    \min. \ \ & z = \sum_{i \in I} \sum_{j \in J} c_{ij} x_{ij} \\
    \text{s.t. } & \sum_{j \in J} x_{ij} \leq C_i, \forall i \\
    & \sum_{i \in I} x_{ij} \geq D_j, \forall j \\
    & x_{ij} \geq 0, \forall i \in I, \forall j \in J
\end{align}

$~$
$~$

where for this particular problem,
$c = \begin{bmatrix}
10 & 7 & 9 \\
4 & 9 & 8
\end{bmatrix}$,
$C = \begin{bmatrix}
15 \\
25
\end{bmatrix}$, and
$D = \begin{bmatrix}
15 \\
12 \\
13
\end{bmatrix}$.

#### Loose Format

```julia
using JuMP, Cbc

# Supply nodes and their capacities
supply_nodes = ["Austin" "Buffalo"]
S = [15 25]
# Demand nodes and corresponding demands
demand_nodes = ["Chicago" "Denver" "Erie"]
D = [15 12 13]
# Transportation unit costs
c = [10 7 9; 4 9 8]

# Converting arrays to dictionaries
s_dict = Dict(supply_nodes .=> S)
d_dict = Dict(demand_nodes .=> D)
c_dict = Dict( (supply_nodes[i], demand_nodes[j]) => c[i,j]
          for i in 1:length(supply_nodes), j in 1:length(demand_nodes) );
```

```julia
# Set the model
model_transport = Model(Cbc.Optimizer);
```

```julia
# Variables
@variable(model_transport, x[supply_nodes, demand_nodes] ≥ 0);
```

```julia
# Formulate objective function
@objective(model_transport, Min, sum(c_dict[i,j]*x[i,j] for i in supply_nodes for j in demand_nodes));
```

```julia
# Formulate constraints
@constraint(model_transport, supply_cap[i in supply_nodes], sum(x[i,j] for j in demand_nodes) ≤ s_dict[i]);
@constraint(model_transport, demand_limit[j in demand_nodes], sum(x[i,j] for i in supply_nodes) ≥ d_dict[j]);
```

```julia
# Print the model and optimize it
println(model_transport)
optimize!(model_transport)

x_star = value.(x)
for s in supply_nodes, d in demand_nodes
    println("from $s to $d: $(x_star[s,d]) units")
end
```