#   Copyright statement:
#### The initial code was provided by Dr. Sandoval (Assitant professor of Electrical Engineering, New Mexico State University) as a starting point for project. https://web.nmsu.edu/~spsandov/
#### The algorithm in current form is developed by Sina Zamen.

#   Objective:
## In this problem, convergence of Levenberg-Marquardt method method for Himmelblau’s cost function, is considered. The initial weight vectors drawn from a Gaussian distribution with mean 0 and variance equal to 1.

### Loading Required Modules:

```python
import math
import numpy as np
import sympy as sym
from PIL import Image
from numpy import array
from matplotlib import style
from numpy.linalg import inv
import matplotlib.pyplot as plt
style.use('ggplot')
```

### Defining Gaussian Dataset Generator Function:

```python
def circGuass(N,Me,VAR):
    dimension = len(Me)
    a = np.array(Me)
    p = np.random.randn(dimension,N)
    h = np.tile(a, (2, N//2))
    x = math.sqrt(Var) * p + h
    return x
```

```python
M1 = 0
Var = 1
N = 2
firstmean = [M1, M1]
Me = firstmean;
X = circGuass(N, Me, Var)
```

### Setting up countour map:

```python
HX ,HY  = 50,50 #number of x,y  points for countour
xmin,xmax = -15,15
ymin,ymax = -12,12
x1 = np.linspace(xmin,xmax,HX)
x2 = np.linspace(ymin,ymax,HY)
X1,X2 = np.meshgrid(x1,x2) # genertate mesh grid
w1=sym.Symbol('w1') # define symbols
w2=sym.Symbol('w2')
```

```python
bowl = 'bowl'
case2= ''
case = bowl #case select
if(case==bowl):
    j=(w1**2 + w2 - 11)**2 + (w1 + w2**2 - 11)**2 # define equation
elif(case==case2):
    #define other surfaces here
    pass
else:
    print('case not recognized')
```

```python
#compute gradient
j_grad1=sym.diff(j,w1)
j_grad2=sym.diff(j,w2)
#compute hessian
hess11=sym.diff(j_grad1,w1)
hess12=sym.diff(j_grad1,w2)
hess21=sym.diff(j_grad2,w1)
hess22=sym.diff(j_grad2,w2)

#generate contour map
ConMap=np.zeros((HX,HY))
for i in range(HX):
    for k in range(HY):
        ConMap[i,k]=j.subs({w1:x1[i],w2:x2[k]})

w=X[0,:] # starting point
wStar1=[3,2] # ending point
wStar2=[-2.8,3.13] # ending point
wStar3=[-3.78,-3.28] # ending point
wStar4=[3.58,-1.85] # ending point
distance1 = math.sqrt(sum([(a - b) ** 2 for a, b in zip(w, wStar1)]))
distance2 = math.sqrt(sum([(a - b) ** 2 for a, b in zip(w, wStar2)]))
distance3 = math.sqrt(sum([(a - b) ** 2 for a, b in zip(w, wStar3)]))
distance4 = math.sqrt(sum([(a - b) ** 2 for a, b in zip(w, wStar4)]))
if distance1 < distance2 and distance1 < distance3 and distance1 < distance4:
    wStar = wStar1
elif distance2 < distance1 and distance2 < distance3 and distance2 < distance4:
    wStar = wStar2
elif distance3 < distance1 and distance3 < distance2 and distance3 < distance4:
    wStar = wStar3
else:
    wStar = wStar4
ew=array([5])
jw=array([250])
eta = 0.05 # learning rate
lim=400 # number of iterations
count= 0
line=[]
h = jw.astype(int)
h = jw[-1:].astype(int)
while (h > 0.05):
    h = ew[-1:].astype(int)
    #compute gradient matrix and hessian matrix
    g= np.array([float(j_grad1.subs({w1:w[0],w2:w[1]})),float(j_grad2.subs({w1:w[0],w2:w[1]}))])
    H= np.array([[float(hess11.subs({w1:w[0],w2:w[1]})),float(hess12.subs({w1:w[0],w2:w[1]}))],                 [float(hess21.subs({w1:w[0],w2:w[1]})),float(hess22.subs({w1:w[0],w2:w[1]}))]])

    wnew1 =  w-eta*((inv(H+50*np.identity(2))*np.asmatrix(g).T).T)
    wnew = np.squeeze(np.asarray(wnew1))
    #loop check
    if( count>lim ):
        print('Count Break')
        break
    elif(np.isnan(g).any()):
        print('nan break')
        break
    else:
        count=count +1
        wprev=w.copy()

        line.append(w)
        w=wnew.copy()

        ew1 = (np.linalg.norm(w-wStar))
        jw1 = (j.subs({w1:w[0],w2:w[1]}))
        ew = np.append(ew, ew1)
        jw = np.append(jw, jw1)
line=np.array(line)
```

```python
print("Number of Iterations:")
print(count)
```

    Number of Iterations:
    33

```python
plt.figure(figsize=(10,10))
plt.contour(X1,X2,ConMap)
plt.plot(line[:,1],line[:,0])
plt.scatter(line[:,1],line[:,0])
plt.plot(wStar[1],wStar[0],'go')
plt.show()
```

```python
plt.figure(figsize=(10,10))
plt.title('J(w) vs iterations')
plt.ylabel('J(w)')
plt.xlabel('iteration')
plt.plot(range(len(jw)),jw)
plt.show()
```

```python
plt.figure(figsize=(10,10))
plt.title('E(w) vs iterations')
plt.ylabel('E(w)')
plt.xlabel('iteration')
plt.plot(range(len(ew)),ew)
plt.show()
```


```python
%matplotlib inline
```

# FMAS

This example demonstrates photon number conservation for the forward model for
the analytic signal (FMAS).

The considered propagation model provides a proper conservation law as class
method `claw`. However, for clarity, we here re-implement the conservation
law and explicitly pass this  user-defined function to the solver class upon
initialization.

As exemplary propagation scenario, the setup used in the step-by-step demo

`sphx_glr_auto_tutorials_specific_g_spectrogram.py`

is used.

.. codeauthor:: Oliver Melchert <melchert@iqo.uni-hannover.de>

```python
import fmas
import numpy as np
from fmas.solver import IFM_RK4IP
from fmas.analytic_signal import AS
from fmas.grid import Grid
from fmas.propagation_constant import PropConst, define_beta_fun_ESM
from fmas.tools import sech, change_reference_frame, plot_claw

beta_fun = define_beta_fun_ESM()
pc = PropConst(beta_fun)

grid = Grid(t_max=5500.0, t_num=2 ** 14)  # (fs)  # (-)

Ns = 8.0  # (-)
t0 = 7.0  # (fs)
w0 = 1.7  # (rad/fs)
n2 = 3.0e-8  # (micron^2/W)
chi = 8.0 * n2 * pc.beta(w0) * pc.c0 / w0 / 3.0
gam0 = 3 * w0 * w0 * chi / (8 * pc.c0 * pc.c0 * pc.beta(w0))
A0 = Ns * np.sqrt(abs(pc.beta2(w0)) / gam0) / t0
E_0t_fun = lambda t: np.real(A0 * sech(t / t0) * np.exp(1j * w0 * t))
Eps_0w = AS(E_0t_fun(grid.t)).w_rep
```

As model we here consider the forward model for the analytic signal (FMAS)

```python
from fmas.models import FMAS
model = FMAS(w=grid.w, beta_w=pc.beta(grid.w), chi=chi)
```

For the FMAS $z$-propagation model we consider a conserved quantity
that is related to the classical analog of the photon number, see Eq. (24) of
Ref. [AD2010] below. In particular we here implement

\begin{align}C_p(z) = \sum_{\omega>0} \omega^{-2} \beta(\omega) |u_\omega(z)|^2,\end{align}

which is, by default, provided as method `model.claw` .

```python
beta_w = pc.beta(grid.w)  # pre-compute beta(w) for convenience

def Cp(i, zi, w, uw):
    _a2_w = np.divide(
        np.abs(beta_w) * np.abs(uw) ** 2,
        w * w,
        out=np.zeros(w.size, dtype="float"),
        where=w > 0,
    )
    return np.sum(_a2_w)
```

As shown below, this conserved quantity can be provided when an instance of
the desired solver is initialized. Here, for simply monitoring the
conservation law we use the Runge-Kutta in the ineraction picture method.
However, a proper conserved quantity is especially important when the
conservation quantity error method (CQE) is used, see, e.g., demo

`sphx_glr_auto_tutorials_tests_g_performance_CQE.py`

```python
solver = IFM_RK4IP(model.Lw, model.Nw, user_action=Cp)
solver.set_initial_condition(grid.w, Eps_0w)
solver.propagate(z_range=0.01e6, n_steps=4000, n_skip=8)  # (micron)  # (-)  # (-)
```

The figure below shows the dynamic evolution of the pulse in the time domain
(top subfigure) and in the frequency domain (center subfigure). The subfigure
at the bottom shows the conservation law (c-law) given by the normalized
photon number variation

\begin{align}\delta_{\rm{Ph}}(z) = \frac{ C_p(z)-C_p(0)}{C_p(0)}\end{align}

as function of the proapgation coordinate $z$. For the considered
discretization of the computational domain the normalized photon number
variation is of the order $\delta_{\rm{Ph}}\approx 10^{-7}$ and thus
very small. The value can be still decreased by decreasing the stepsize
$\Delta z$.

```python
utz = change_reference_frame(solver.w, solver.z, solver.uwz, pc.vg(w0))

plot_claw(
    solver.z, grid.t, utz, solver.ua_vals, t_lim=(-25, 125), w_lim=(0.5, 4.5)
)
```

**References:**

[AD2010] Sh. Amiranashvili, A. Demircan, Hamiltonian structure of propagation
equations for ultrashort optical pulses, Phys. Rev. E 10 (2010) 013812,
http://dx.doi.org/10.1103/PhysRevA.82.013812.


#  NMA Model Types Tutorial 2: "How" models

In this tutorial we will explore models that can potentially explain *how* the spiking data we have observed is produced. That is, the models will tell us something about the *mechanism* underlying the physiological phenomenon.

Our objectives:
- Write code to simulate a  simple "leaky integrate-and-fire" neuron model
- Make the model more complicated — but also more realistic — by adding more physiologically-inspired details

```python
#@title Video: "How" models
from IPython.display import YouTubeVideo
video = YouTubeVideo(id='yWPQsBud4Cc', width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=yWPQsBud4Cc

## Setup

**Don't forget to execute the hidden cells!**

```python
#@title Imports
import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as stats

import ipywidgets as widgets
```

```python
#@title Helper Functions
def histogram(counts, bins, vlines=(), ax=None, ax_args=None, **kwargs):
  """Plot a step histogram given counts over bins."""
  if ax is None:
    _, ax = plt.subplots()

  # duplicate the first element of `counts` to match bin edges
  counts = np.insert(counts, 0, counts[0])

  ax.fill_between(bins, counts, step="pre", alpha=0.4, **kwargs)  # area shading
  ax.plot(bins, counts, drawstyle="steps", **kwargs)  # lines

  for x in vlines:
    ax.axvline(x, color='r', linestyle='dotted')  # vertical line

  if ax_args is None:
    ax_args = {}

  # heuristically set max y to leave a bit of room
  ymin, ymax = ax_args.get('ylim', [None, None])
  if ymax is None:
    ymax = np.max(counts)
    if ax_args.get('yscale', 'linear') == 'log':
      ymax *= 1.5
    else:
      ymax *= 1.1
      if ymin is None:
        ymin = 0

  if ymax == ymin:
    ymax = None

  ax_args['ylim'] = [ymin, ymax]

  ax.set(**ax_args)
  ax.autoscale(enable=False, axis='x', tight=True)

def plot_neuron_stats(v, spike_times):
  fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(12, 5))

  # membrane voltage trace
  ax1.plot(v[0:100])
  ax1.set(xlabel='Time', ylabel='Voltage')
  # plot spike events
  for x in spike_times:
    if x >= 100:
      break
    ax1.axvline(x, color='limegreen')

  # ISI distribution
  isi = np.diff(spike_times)
  n_bins = bins = np.arange(isi.min(), isi.max() + 2) - .5
  counts, bins = np.histogram(isi, bins)
  vlines = []
  if len(isi) > 0:
    vlines = [np.mean(isi)]
  xmax = max(20, int(bins[-1])+5)
  histogram(counts, bins, vlines=vlines, ax=ax2, ax_args={
    'xlabel': 'Inter-spike interval',
    'ylabel': 'Number of intervals',
    'xlim': [0, xmax]
  })
```

## The Linear Integrate-and-Fire Neuron

One of the simplest models of spiking neuron behavior is the linear integrate-and-fire model neuron. In this model, the neuron increases its membrane potential $V_m$ over time in response to excitatory input currents $I$ scaled by some factor $\alpha$:

\begin{align}
  dV_m = {\alpha}I
\end{align}

Once $V_m$ reaches a threshold value of 1, a spike is emitted, the neuron resets $V_m$ back to 0, and the process continues.

#### Spiking Inputs

We now have a model for the neuron dynamics. Next we need to consider what form the input $I$ will take. How should we represent the presynaptic neuron firing behavior providing the input coming into our model nueuron? We learned previously that a good approximation of spike timing is a Poisson random variable, so we can do that here as well

\begin{align}
  I \sim Poisson(\lambda)
\end{align}

where $\lambda$ is the average rate of incoming spikes.

### Exercise: Compute $dV_m$

For your first exercise, you will write the code to compute the change in voltage $dV_m$ of the linear integrate-and-fire model neuron. The rest of the code to handle the neuron dynamics are provided for you, so you just need to fill in a definition for `dv` in the `lif_neuron` method below. The value for $\lambda$ needed for the Poisson random variable is named `rate`.

TIP: The [`scipy.stats`](https://docs.scipy.org/doc/scipy/reference/stats.html) package is a great resource for working with and sampling from various probability distributions.

```python
def lif_neuron(n_steps=1000, alpha=0.01, rate=10):
  """ Simulate a linear integrate-and-fire neuron.

  Args:
    n_steps (int): The number of time steps to simulate the neuron's activity.
    alpha (float): The input scaling factor
    rate (int): The mean rate of incoming spikes

  """
  v = np.zeros(n_steps)
  spike_times = []
  for i in range(1, n_steps):

    #######################################################################
    ## TODO for students: compute dv, then remove the NotImplementedError #
    #######################################################################
    # dv = ...
    raise NotImplementedError("Student excercise: compute the change in membrane potential")

    v[i] = v[i-1] + dv
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0

  return v, spike_times

# Uncomment these lines after completing the lif_neuron function
# v, spike_times = lif_neuron()
# plot_neuron_stats(v, spike_times)
```

**Example output:**

### Parameter Exploration

Here's an interactive demo that shows how the model behavior changes for different parameter values.

**Remember to enable the demo by running the cell.**

```python
#@title Linear Integrate-and-Fire Model Neuron Explorer

def _lif_neuron(n_steps=1000, alpha=0.01, rate=10):
  exc = stats.poisson(rate).rvs(n_steps)
  v = np.zeros(n_steps)
  spike_times = []
  for i in range(1, n_steps):
    dv = alpha * exc[i]
    v[i] = v[i-1] + dv
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0
  return v, spike_times

@widgets.interact(
  n_steps=widgets.FloatLogSlider(1000.0, min=2, max=4),
  alpha=widgets.FloatLogSlider(0.01, min=-4, max=-1),
  rate=widgets.IntSlider(10, min=1, max=20)
)
def plot_lif_neuron(n_steps=1000, alpha=0.01, rate=10):
  v, spike_times = _lif_neuron(int(n_steps), alpha, rate)
  plot_neuron_stats(v, spike_times)
```

## Inhibitory signals

Our linear integrate-and-fire neuron from the previous section was indeed able to produce spikes, but the actual spiking behavior did not line up with our expectations of exponentially distributed ISIs. This means we need to refine our model!

In the previous model we only considered excitatory behavior -- the only way the membrane potential could decrease is upon a spike event. We know, however, that there are other factors that can drive $V_m$ down. First is the natural tendancy of the neuron to return to some steady state or resting potential. We can update our previous model as follows:

\begin{align}
  dV_m = -{\beta}V_m + {\alpha}I
\end{align}

where $V_m$ is the current membrane potential and $\beta$ is some leakage factor. This is a basic form of the popular Leaky Integrate-and-Fire model neuron (for a more detailed discussion of the LIF Neuron, see the Appendix).

We also know that in addition to excitatory presynaptic neurons, we can have inhibitory presynaptic neurons as well. We can model these inhibitory neurons with another Poisson random variable, giving us

\begin{align}
I = I_{exc} - I_{inh} \\
I_{exc} \sim Poisson(\lambda_{exc}) \\
I_{inh} \sim Poisson(\lambda_{inh})
\end{align}

where $\lambda_{exc}$ and $\lambda_{inh}$ are the rates of the excitatory and inhibitory presynaptic neurons, respectively.

### Exercise: Compute $dV_m$ with inhibitory signals

For your second exercise, you will again write the code to compute the change in voltage $dV_m$, though now of the LIF model neuron described above. Like last time, the rest of the code needed to handle the neuron dynamics are provided for you, so you just need to fill in a definition for `dv` below.

```python
def lif_neuron_inh(n_steps=1000, alpha=0.5, beta=0.1, exc_rate=10, inh_rate=10):
  """ Simulate a simplified leaky integrate-and-fire neuron with both excitatory
  and inhibitory inputs.

  Args:
    n_steps (int): The number of time steps to simulate the neuron's activity.
    alpha (float): The input scaling factor
    beta (float): The membrane potential leakage factor
    exc_rate (int): The mean rate of the incoming excitatory spikes
    inh_rate (int): The mean rate of the incoming inhibitory spikes
  """
  v = np.zeros(n_steps)
  spike_times = []
  for i in range(1, n_steps):

    ############################################################
    ## Students: compute dv and remove the NotImplementedError #
    ############################################################
    # dv = ...
    raise NotImplementedError("Student excercise: compute the change in membrane potential")

    v[i] = v[i-1] + dv
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0

  return v, spike_times

# Uncomment these lines do make the plot once you've completed the function
# v, spike_times = lif_neuron_inh()
# plot_neuron_stats(v, spike_times)
```

```python
v, spike_times = lif_neuron_inh()
plot_neuron_stats(v, spike_times)
```

### Parameter Exploration

Like last time, you can now explore how you LIF model behaves when the various parameters of the system are changed.

```python
#@title LIF Model Neuron with Inhibitory Inputs Explorer

def _lif_neuron_inh(n_steps=1000, alpha=0.5, beta=0.1, exc_rate=10, inh_rate=10):
  """ Simulate a simplified leaky integrate-and-fire neuron with both excitatory
  and inhibitory inputs.

  Args:
    n_steps (int): The number of time steps to simulate the neuron's activity.
    alpha (float): The input scaling factor
    beta (float): The membrane potential leakage factor
    exc_rate (int): The mean rate of the incoming excitatory spikes
    inh_rate (int): The mean rate of the incoming inhibitory spikes
  """
  # precompute Poisson samples for speed
  exc = stats.poisson(exc_rate).rvs(n_steps)
  inh = stats.poisson(inh_rate).rvs(n_steps)

  v = np.zeros(n_steps)
  spike_times = []
  for i in range(1, n_steps):
    dv = -beta * v[i-1] + alpha * (exc[i] - inh[i])
    v[i] = v[i-1] + dv
    if v[i] > 1:
      spike_times.append(i)
      v[i] = 0

  return v, spike_times

@widgets.interact(n_steps=widgets.FloatLogSlider(1000.0, min=2, max=4),
                  alpha=widgets.FloatLogSlider(0.5, min=-2, max=1),
                  beta=widgets.FloatLogSlider(0.1, min=-2, max=0),
                  exc_rate=widgets.IntSlider(10, min=1, max=20),
                  inh_rate=widgets.IntSlider(10, min=1, max=20))
def plot_lif_neuron(n_steps=1000, alpha=0.5, beta=0.1, exc_rate=10, inh_rate=10):
  v, spike_times = _lif_neuron_inh(int(n_steps), alpha, beta, exc_rate, inh_rate)
  plot_neuron_stats(v, spike_times)
```

```python
#@title Video: Balanced inputs
from IPython.display import YouTubeVideo
video = YouTubeVideo(id='buXEQPp9LKI', width=854, height=480, fs=1)
print("Video available at https://youtube.com/watch?v=" + video.id)
video
```

    Video available at https://youtube.com/watch?v=buXEQPp9LKI

## Appendix

### Why do neurons spike?

A neuron stores energy in an electric field across its cell membrane, by controlling the distribution of charges (ions) on either side of the membrane. This energy is rapidly discharged to generate a spike when the field potential (or membrane potential) crosses a threshold. The membrane potential may be driven toward or away from this threshold, depending on inputs from other neurons: excitatory or inhibitory, respectively. The membrane potential tends to revert to a resting potential, for example due to the leakage of ions across the membrane, so that reaching the spiking threshold depends not only on the amount of input ever received following the last spike, but also the timing of the inputs.

The storage of energy by maintaining a field potential across an insulating membrane can be modeled by a capacitor. The leakage of charge across the membrane can be modeled by a resistor. This is the basis for the leaky integrate-and-fire neuron model.

### The LIF Model Neuron

The full equation for the LIF neuron is

\begin{align}
C_{m}\frac{dV_m}{dt} = -(V_m - V_{rest})/R_{m} + I
\end{align}

where $C_m$ is the membrane capacitance, $R_M$ is the membrane resistance, $𝑉_{𝑟𝑒𝑠𝑡}$ is the resting potential, and  𝐼  is some input current (from other neurons, an electrode, ...).

In our above examples we set many of these properties to convenient values ($C_m = R_m = dt = 1$, $V_{rest} = 0$) to focus more on the overall behavior, though these too can be manipulated to achieve different dynamics.


<figure>
  <IMG SRC="gfx/Logo_norsk_pos.png" WIDTH=100 ALIGN="right">
</figure>

# Time evolution in quantum mechanics. The particle in a box model
# ! WORK IN PROGRESS !
*Roberto Di Remigio*, *Luca Frediani*

In this notebook, we will consider how the time evolution of physical systems is treated in quantum mechanics.
We will again make use of the simple particle in a box model to highlight the basic ideas.

The equation of motion for quantum mechanical systems is the time-dependent Schrödinger equation:

\begin{equation}
H\Psi(x, t) = \mathrm{i}\hbar\frac{\partial\Psi(x, t)}{\partial t}
\end{equation}

where $H$ is the Hamiltonian operator and $\Psi(x, t)$ is the wavefunction describing the system.
For a general, time-dependent Hamiltonian the solution of the time-dependent Schrödinger equation is far from trivial and is usually achieved using [perturbation theory].

[perturbation theory]: https://en.wikipedia.org/wiki/Perturbation_theory_(quantum_mechanics)

In case the Hamiltonian is not time-dependent, we can solve the time-dependent Schrödinger equation by separation of the space ($x$) and time variables ($t$):

\begin{equation}
\Psi(x, t) = \psi(x)\theta(t)
\end{equation}

Inserting this _Ansatz_ into the equation above:

\begin{equation}
[H\psi(x)]\theta(t) = \psi(x)\mathrm{i}\hbar\frac{\partial\theta(t)}{\partial t}
\end{equation}

followed by division by $\psi(x)$ and $\theta(t)$ yields:

\begin{equation}
\frac{H\psi(x)}{\psi(x)}  = \frac{1}{\theta(t)}\mathrm{i}\hbar\frac{\partial\theta(t)}{\partial t}
\end{equation}

This means that the left- and right-hand sides have to be equal to a constant value, which is the energy of the system:

\begin{equation}
\frac{H\psi(x)}{\psi(x)} = E = \frac{1}{\theta(t)}\mathrm{i}\hbar\frac{\partial\theta(t)}{\partial t}
\end{equation}

What we have obtained is thus two equations.
The first one is time-independent Schrödinger equation:

\begin{equation}
H\psi_n(x) = E_n\psi_n(x)
\end{equation}

whose eigenvalues indeed are the allowed energies for the system.
The second equation determines the time evolution of the eigenfunctions:

\begin{equation}
-\frac{\mathrm{i}E_n}{\hbar}\theta(t) = \frac{\partial\theta(t)}{\partial t}
\end{equation}

this is **not** an eigenvalue equation. The solution to this differential equation is given by the exponential function:

\begin{equation}
\theta(t) = \exp(-\frac{\mathrm{i}E_nt}{\hbar}) = \cos(\frac{E_nt}{\hbar}) -\mathrm{i}\sin(\frac{E_nt}{\hbar})
\end{equation}

Eventually, the solution to the time-dependent problem is:

\begin{equation}
\Psi_n(x, t) = \psi_n(x)\theta(t) = \exp(-\frac{\mathrm{i}E_nt}{\hbar}) \psi_n(x)
\end{equation}

Notice that this form of the solution is general. It is the same for all quantum systems were the Hamiltonian doesn't depend explicitly on time. You will also notice that the time evolving part of the wavefunction is given as a simpe phase factor depending on the energy of the eigenfunction to the time-independent problem.
This means that the probability density for the eigenstates is unchanged with the passing of time:

\begin{equation}
|\Psi_n(x, t)|^2 = \Psi_n^*(x, t)\Psi_n(x, t) = \exp(+\frac{\mathrm{i}E_nt}{\hbar}) \psi^*_n(x) \exp(-\frac{\mathrm{i}E_nt}{\hbar}) \psi_n(x) = \psi^*_n(x)\psi_n(x) =|\psi(x)|^2
\end{equation}

which also means that expectation values of time-independent operators are **unchanged** with the passing of time:

\begin{equation}
\langle O \rangle =
\int\mathrm{d} x \Psi_n^*(x, t)O(x)\Psi_n(x, t)  =
\int\mathrm{d} x
\exp(+\frac{\mathrm{i}E_nt}{\hbar}) \psi^*_n(x)O(x) \exp(-\frac{\mathrm{i}E_nt}{\hbar}) \psi_n(x)
=
\int\mathrm{d} x \psi^*_n(x)O(x)\psi_n(x)
\end{equation}

States for which all the observables are unchanged by time evolution are called **stationary**. All eigenvectors of a time-independent Hamiltonian are stationary states.

In the following, our toy system will be the particle in a box (both one-dimensional and two-dimensional)
For the one-dimensional model the (normalized) eigenfunctions are:
\begin{equation}
\psi_n(x) = \sqrt{\frac{2}{L}}\sin(\frac{n\pi x}{L}) \quad\quad \forall n \neq 0
\end{equation}
with energies:
\begin{equation}
E_n = \frac{h^2n^2}{8ML^2}  \quad\quad \forall n \neq 0
\end{equation}

For the two-dimensional, square model the (normalized) eigenfunctions are:
\begin{equation}
\psi_{nm}(x,y) = \frac{2}{L}\sin(\frac{n\pi x}{L})\sin(\frac{m\pi y}{L}) \quad\quad \forall n,m  \neq 0
\end{equation}
with energies:
\begin{equation}
E_{nm} = \frac{h^2}{8ML^2}(n^2 + m^2)  \quad\quad \forall n, m \neq 0
\end{equation}

[particle in a box]: https://en.wikipedia.org/wiki/Particle_in_a_box

## Eigenfunctions and probability densities

The first thing we can try is to plot wavefunctions and the associated probability densities at different times. Start with the one-dimensional model and look at the eigenfunctions for the ground and first excited state at different times. Use the functions defined below to achieve this.

At what time points should we make the plots? Given that these are stationary states and that the time-evolving factor in the wavefunction is an oscillatory function, there will be a characteristic oscillation period for each eigenfunction. At what time $T$ (the period) does a given eigenfunction become equal to its starting value?
In other words, for which $T$ is the following equation satisfied:
\begin{equation}
\Psi(x, t = 0) = \Psi(x, t = T)
\end{equation}
or more explicitly:
\begin{equation}
\psi_n(x) = \exp(-\frac{\mathrm{i}E_nT}{\hbar}) \psi_n(x)
\end{equation}
Once you have find the value of the period, you can plot at time points that are multiples of the period.

**Warning** The eigenfunctions are now complex functions! Plot both the real and imaginary part to get something meaningful! Use the NumPy `real` and `imag` functions to get them.

```python
import numpy as np
from scipy.constants import *

def theta(En, t):
    """ The time evolving factor for a stationary state.
    This is a complex function!

    En -- the energy of the stationary state
    t  -- time point
    """
    return np.exp(-1j * (En / hbar) * t)

def eigenfunction1D(n, L, x):
    """ Normalized eigenfunction for the 1D particle in a box.

    n -- the NumPy array with the x values
    L -- the quantum number
    x -- the size of the box
    """
    if n <= 0:
        raise TypeError('Quantum number has to be greater than 0!')
    normalization = np.sqrt(2.0 / L)
    return normalization * np.sin(n * pi * x / L)

def eigenvalue1D(n, M, L):
    """ Eigenvalue for the 1D particle in a box.
    Returned values is in Joule.

    n -- the quantum number
    M -- the mass of the particle
    L -- the size of the box
    """
    if n <= 0:
        raise TypeError('Quantum number has to be greater than 0!')
    gs_energy = h**2 / (8 * M * L**2)
    return n**2 * gs_energy
```

The example below shows how things would look for the ground state eigenfunction (once you fill in how to calculate the period ;))

```python
import matplotlib.pyplot as plt
# make sure we see it on this notebook
%matplotlib inline

# The mass will be that of an electron
M = m_e
# The length of the box is pi
L = pi

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

axes[0].set_xlabel(r'$x$')
axes[0].set_ylabel(r'$\psi$')
axes[0].set_title(r'1D particle in a box wavefunctions')
axes[0].set_xlim([0, L])

axes[0].spines['right'].set_color('none')
axes[0].spines['top'].set_color('none')
axes[0].xaxis.set_ticks_position('bottom')
axes[0].spines['bottom'].set_position(('data',0)) # set position of x spine to x=0

axes[0].yaxis.set_ticks_position('left')
axes[0].spines['left'].set_position(('data',0))   # set position of y spine to y=0

E1 = eigenvalue1D(1, M, L)
print('Eigenvalue 1 is {} Joule'.format(E1))
# What's the period?
T1 =
print('The oscillation period is {} seconds'. format(T1))

x = np.linspace(0, L, 1000)
axes[0].plot(x, np.real(theta(E1, 0)*eigenfunction1D(1, L, x)), label=r'$\Re(\psi_1(t=0))$')
axes[0].plot(x, np.imag(theta(E1, 0)*eigenfunction1D(1, L, x)), label=r'$\Im(\psi_1(t=0))$')
axes[0].plot(x, np.real(theta(E1, 0.1*T1)*eigenfunction1D(1, L, x)), label=r'$\Re(\psi_1(t=0.1T_1))$')
axes[0].plot(x, np.imag(theta(E1, 0.1*T1)*eigenfunction1D(1, L, x)), label=r'$\Im(\psi_1(t=0.1T_1))$')

axes[0].legend()

axes[1].set_xlabel(r'$x$')
axes[1].set_ylabel(r'$|\psi|^2$')
axes[1].set_title(r'1D particle in a box probability densities')
axes[1].set_xlim([0, pi])

axes[1].spines['right'].set_color('none')
axes[1].spines['top'].set_color('none')
axes[1].xaxis.set_ticks_position('bottom')
axes[1].spines['bottom'].set_position(('data',0)) # set position of x spine to x=0

axes[1].yaxis.set_ticks_position('left')
axes[1].spines['left'].set_position(('data',0))   # set position of y spine to y=0

# We need to use the absolute function from NumPy to get the correct probability
# distribution since the wavefunction is now complex
axes[1].plot(x, (np.absolute(theta(E1, 0)*eigenfunction1D(1, L, x)))**2, label=r'$|\psi_1(t=0)|^2$')
axes[1].plot(x, (np.absolute(theta(E1, 0.1*T1)*eigenfunction1D(1, L, x)))**2, label=r'$|\psi_1(t=0.1T_1)|^2$')

axes[1].legend()
```

## Linear combinations

The next step is to look at the time evolution of a linear combination of eigenfunctions. Let us consider the following, normalized, linear combination:
\begin{equation}
\Psi(x, t) = \frac{1}{\sqrt{2}}(\exp(-\frac{\mathrm{i}E_1t}{\hbar})\psi_1(x) + \exp(-\frac{\mathrm{i}E_2t}{\hbar})\psi_2(x))
\end{equation}
From the expression of the eigenvalues:
\begin{equation}
E_n = \frac{h^2n^2}{8ML^2}
\end{equation}
we can see that the energy of the excited states is a multiple of that for the ground state:
\begin{equation}
E_\mathrm{gs} = E_1 = \frac{h^2}{8ML^2} \quad\quad E_n = n^2E_\mathrm{gs}
\end{equation}
We can thus rewrite the linear combination above as:
\begin{equation}
\Psi(x, t) = \frac{1}{\sqrt{2}}(\exp(-\frac{\mathrm{i}E_1t}{\hbar})\psi_1(x) + \exp(-\frac{\mathrm{i}4E_1t}{\hbar})\psi_2(x))
=\frac{1}{\sqrt{2}}\exp(-\frac{\mathrm{i}E_1t}{\hbar})\left[\psi_1(x) + \exp(-\frac{\mathrm{i}3E_1t}{\hbar})\psi_2(x)\right]
\end{equation}

## Animated plots to visualize time evolution

`matplotlib` can be used to create animated plots.

```python

```


```
# default_exp transfer_functions
```

```
#hide
%load_ext autoreload
%autoreload 2
```

```
#hide
from IPython.display import Image
from IPython.display import HTML
```

```
#export
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
```

```
#hide
%matplotlib inline
```

------------------------

# Transfer Functions

It is key to be able to describe the system in an efficient and useful manner.

- To do this, we describe the system mathematically writing the equations of motion in the form of differential equations.
- We have seen how to model the simplified case of a car driving uphill.

The two most popular representations:
- state space representation
- transfer functions.

Loosely speaking, **transfer functions** are a Laplace domain representation of your system and they are commonly associated with the era of control techniques labeled classical control theory.

**State space** is a time domain representation, packaged in matrix form, and they are commonly associated with the era labeled modern control theory.

- Each representation has its own set of benefits and drawbacks and as a control engineer we need to use what is best for our problem.
- We will focus on Transfer Functions.

## Definition of Transfer Function
> A transfer function is the Laplace transform of the impulse response of a linear, time-invariant system with a single input and single output when you set the initial conditions to zero.

### The _s_ domain and the Laplace Transform

- The Laplace Transform takes the Fourier Transform one step forward
- Decomposes a time domain signal into both cosines and exponential functions
- For the Laplace transform we need a symbol that represents more than just frequency, $\omega$
- Need to also account for the exponential aspect of the signal.
- This is where the variable $s$ comes in.

$s$ is a complex number, which means that it contains values for two dimensions:
- one dimension that describes the frequency of a cosine wave
- the second dimension that describes the exponential term.

It is defined as $s = \sigma + j\omega$.

Exponential functions that have imaginary exponents, such as $e^{j2t}$, produce two-dimensional sinusoids through Euler’s formula:

$$e^{j\omega t} = cos(\omega t) + jsin(\omega t)$$

For exponential functions that have **real** numbers for exponents:
- Negative real numbers give us exponentially decaying signals
- Positive real numbers give us exponentially growing signals.

Two examples are $e^{2t}$, which grows exponentially for all positive time, and $e^{−5t}$, which decays exponentially for all positive time.

```
fig = plt.figure()

t = np.linspace(-0.2, 1, 20)
plt.plot(t, np.exp(2*t), linewidth=6)
plt.plot(t, np.exp(-5*t), linewidth=6)
plt.grid()
plt.xlabel('time (s)')
```

Now let’s think about our new variable $s$ which has both a real and imaginary component.

- The equation $e^{st}$ is really just an exponential function multiplied by a sinusoid

$$e^{st} = e^{(\sigma+j\omega)t} = e^{\sigma t}e^{j\omega t}$$

```
import cmath
```

```
fig, axs = plt.subplots(1,3, figsize=(20,5))

t = np.linspace(-0.2, 10, 50) # time range

# s = 1 + j2
s = complex(1, 2)

axs[0].plot(t[:10], np.exp(t[:10]), linewidth=6); axs[0].set_title('exp(t)')
axs[1].plot(t, np.exp(complex(0,2)*t), linewidth=6); axs[1].set_title('exp(j2t)')
axs[2].plot(t, np.exp(t)*np.exp(complex(0,2)*t), linewidth=6); axs[2].set_title('exp(st)')

plt.grid()
plt.xlabel('time (s)')
fig.tight_layout()
```

```
fig, axs = plt.subplots(1,3, figsize=(20,5))

t = np.linspace(-5, 5, 50)

# s = -2 + j
s = complex(-2, 1)

axs[0].plot(t[:10], np.exp(-2*t[:10]), linewidth=6); axs[0].set_title('exp(-2t)')
axs[1].plot(t, np.exp(complex(0, 1)*t), linewidth=6); axs[1].set_title('exp(jt)')
axs[2].plot(t, np.exp(-2*t)*np.exp(complex(0, 1)*t), linewidth=6); axs[2].set_title('exp(st)')

plt.grid()
plt.xlabel('time (s)')
fig.tight_layout()
```

- We  can combine the two parts of $s = \sigma + j\omega$ into a two-dimensional plane where the real axis is the exponential line and the imaginary axis is the frequency line.

- The value of $s$ provides a location in this plane and describes the resulting signal, $e^{st}$, as function of the selected $\omega$ and $\sigma$.

--------------------------------

### The Laplace Transform

The Laplace transform is an integral transform that converts a function of a real variable
$t$ (e.g., $time$) to a function of a complex variable $s = \sigma + j\omega \in \mathbb{C}$ (complex frequency).

$${\displaystyle F(s)={\mathcal {L}}\{f\}(s)=\int _{0}^{\infty }f(t)e^{-st}\,dt.}$$

The inverse Laplace transform is:

$${\displaystyle f(t)={\mathcal {L}}^{-1}\{F(s)\}(t)={\frac {1}{2\pi j}}\lim _{\omega\to \infty }\int _{\sigma -j\omega}^{\sigma +j\omega}e^{st}F(s)\,ds}$$

The Laplace transform is particularly important because it is a tool for solving differential equations: _it transforms linear differential equations into algebraic equations, and convolution into multiplication!_

- Derivatives and integrals become algebric operations

From a system perspective, the Transfer function expresses the relation between the Laplace Transform of the input and that of the output:

$$ Y(s)=\mathbf{c}^T(s\mathbf{I}-\mathbf{A})^{-1} \mathbf{b} U(s) $$

--------------------

### Properties of the Laplace Transform: relationships between time and frequency

The Laplace transform has a number of properties. These are really useful to calculate the transforms without using its integral definition.

| Property | Time domain | Frequency domain (s) |
| ---------| --------------| --------------|
| Linearity | $af(t)$ + $bg(t)$ | $aF(s)+bG(s)$ |
| Time shift (delay) | $f(t-\tau)$ | $e^{-\tau s}F(s)$
| Frequency shift | $f(t)e^{\alpha t}$ | $F(s-\alpha)$
| Derivative | $\frac{df}{dt}(t)$ | $sF(s)$ |
| Second Derivative | $\frac{df^2}{d^2t}(t)$ | $s^2F(s) - f(0^-)$ |
| Integral | $\int _{0}^{t}f(\tau )\,d\tau$ = $(u*f)(t)$ | $\frac{1}{s}F(s)$ |
| Convolution | $(f*g)(t)$ = $\int _{0}^{t} f(\tau )g(t$-$\tau )d\tau$ | $F(s)G(s)$ |
|  | $t\rightarrow\inf$ | $s\rightarrow 0$ |
|  | $t\rightarrow 0$ | $s\rightarrow \inf$ |

From this table it is easier to calculate Laplace transforms of known functions:

```
#export
def step(t, step_time=0):
    """Heaviside step function"""
    return 1 * (t >= step_time) #np.ones((1,len(t))).squeeze()

def ramp(t):
    return t * (t >= 0)

def delta(t, delta_t=0, eps=None):
    if eps is None and len(t) > 1:
        _eps=t[1]-t[0]
    else:
        _eps = eps
    return step(t, delta_t)-step(t, delta_t+_eps)
```

```
fig, axs = plt.subplots(1,4, figsize=(10, 5))

t = np.linspace(-10,10,1000)

axs[0].plot(t, delta(t), linewidth=3)
axs[1].plot(t, step(t), linewidth=3)
axs[2].plot(t, ramp(t), linewidth=3)
axs[3].plot(t, ramp(t)**2, linewidth=3)

fig.tight_layout()
```

Note that the `step` function is the derivative of the `ramp`, and that the `ramp` is the derivative of the `quadratic` function.

--------------------------------------------

### Transforms of known functions

### Delta function

If we then want to calculate the Laplace transform of the `delta` $\delta(t)$ function:

$${\displaystyle F(s)={\mathcal {L}}\{f\}(s)=\int _{0}^{\infty }\delta(t)e^{-st}\,dt.}$$

where

$$\delta(t)=0, \forall t \neq 0 $$
$$\int _{-\infty}^{\infty } \delta(t)dt=1$$

$${\displaystyle F(s)={\mathcal {L}}\{f\}(s)=\int _{0}^{\infty }\delta(t)e^{-st}\,dt.}=e^{-s0}=1$$

### Step function

Or of the `step` $1(t)$ function:

$${\displaystyle F(s)={\mathcal {L}}\{f\}(s)=\int _{0}^{\infty }1(t)e^{-st}\,dt.}$$

where

$$1(t)=0, \forall t < 0 $$
$$1(t)=1, \forall t \geq 0 $$

$${\displaystyle F(s)={\mathcal {L}}\{f\}(s)=\int _{0}^{\infty }1(t)e^{-st}\,dt.} = \int _{0}^{\infty}e^{-st}\,dt. = -\frac{e^{-st}}{s} \Bigg|^\infty_0 = -\frac{1}{s} \bigg( \lim_{t\rightarrow \infty} e^{-st} - \lim_{t\rightarrow 0} e^{-st} \bigg) = \frac{1}{s}$$

A great thing about working with linear systems is that usually we don’t have to perform the Laplace transform integration by hand.

Most the Laplace transforms for most functions that we will encounter have been solved many times and are available already collected into tables.

### Putting this all Together

Let's consider the rolling cart example:

We can model this cart as:

$$m\ddot{x} = F_{input}(t) - F_{dumper}(t) - F_{spring}(t)$$

And since: $F_{spring}(t)=kx(t)$, $F_{dumper}(t)=b\dot{x}(t)$

We obtain:

$$m\ddot{x} + kx(t) + b\dot{x}(t) -  F_{input}(t) = 0$$

We can convert this system model into a transfer function:
- it’s a linear,
- time-invariant system,
- there is a single input, $F_{input}$,
- and a single output, $x$.

To calculate the Transfer Function we need to take the Laplace Transform of the impulse response of the system.

To do so, let's set our input to $F_{input}(t)=\delta(t)$ and solve for the response $x(t)$.

$$m\ddot{x} + kx(t) + b\dot{x}(t) -  \delta(t) = 0$$

- Solving linear, ordinary differential equations in the time domain can be time consuming.
- We can make the task easier by taking the Laplace transform of the entire differential equation, one term at a time, and solve for the impulse response in the $s$ domain directly.
- Simply take each term and replace with the corresponding $s$ domain equivalent.

| | |
|---|---|
| time domain | $\large m\ddot{x} + kx(t) + b\dot{x}(t) -  \delta(t) = 0$ |
| $s$ domain  | $\large (ms^2 + bs + k)X(s) - 1 = 0 $|
| $X(s)$ is the impulse response in the $s$ domain  | $\large X(s) = \frac{1}{m*s^2 + bs + k}$|

*note that the initial conditions are zeros*

To go back to the time domain we could preform the inverse Laplace transform on $X(s)$.

------------------------------------

## Inverse Laplace Transform: Partial fraction decomposition

Given:

$$Y(s) = G(s)U(s) = \frac{N(s)}{D(s)}U(s)$$

We want to expand $G(s)$ into the sum of functions for which we already know the inverse transform, then thanks to the linearity we can simply sum them all up to obtain the inverse of the entire function:

Let's suppose we have all **distinct poles**:

$$ D(s) = \prod^{n}_{k=1}{(s-p_k)}$$

we want to find the coefficient $P_k$ such that:

$$ \frac{N(s)}{\prod^{n}_{k=1}{(s-p_k)}} = \sum^{n}_{k=1}\frac{P_k}{s-p_k}$$

Multiplying for $(s-p_i)$:

$$ (s-p_i)\frac{N(s)}{\prod^{n}_{k=1}{(s-p_k)}} = (s-p_i)\sum^{n}_{k=1}\frac{P_k}{s-p_k}$$

we can obtain:

$$P_i = [(s-p_i)G(s)] \big|_{s=p_i}$$

and finally:

$$ g(t) = \mathcal {L}^{-1}[G(s)]=\mathcal {L}^{-1} \bigg[\sum^{n}_{k=1}\frac{P_k}{s-p_k}\bigg] = \sum^{n}_{k=1} P_k e^{p_kt}$$

If we have **muliple poles** the decomposition is similar.

For example:

$$G(s) = \frac{s-10}{(s+2)(s+5)}$$

$$P_1=(s+2)\frac{s-10}{(s+2)(s+5)}\bigg|_{s=-2}=-\frac{12}{3}=-4$$
$$P_2=(s+5)\frac{s-10}{(s+2)(s+5)}\bigg|_{s=-5}=\frac{-15}{-3}=5$$

which means that:

$$G(s) = \frac{-4}{(s+2)} + \frac{5}{(s+5)}$$

and finally:

$$ g(t) = \mathcal {L}^{-1}[G(s)] = -4e^{-2t} + 5e^{-5t}$$

--------------------

### Some more on the Laplace transform

- Convert dfferential problems into algebric ones (finding the poles of the system)
- Makes it possible to understand the system output for a given input
- Using the transfer function it is possible to represent the input/output relationship
- Interconnected systems can be represented using block diagrams
- However, _it does not allow analysing the controllability and observability of each single block!_

--------------------

## [Sympy](https://dynamics-and-control.readthedocs.io/en/latest/1_Dynamics/3_Linear_systems/Laplace%20transforms.html)

The sympy Python module makes it easier to work with Laplace transforms. Let's import it:

```
import sympy
sympy.init_printing()

# Let's also ignore some warnings here due to sympy using an old matplotlib function to render Latex equations.
import warnings
warnings.filterwarnings('ignore')
```

And let’s define the symbols with need to work with.

```
t, s = sympy.symbols('t, s')
a = sympy.symbols('a', real=True, positive=True)
```

Sympy provides a function called `laplace_transform` to easily calculate Laplace transforms:

For example, if we want to know the Laplace transform of $e^{\alpha t}$

```
f = sympy.exp(a*t)
f
```

```
F = sympy.laplace_transform(f, t, s, noconds=True)
F
```

We can define a function to make it easier:

```
def L(f):
    return sympy.laplace_transform(f, t, s, noconds=True)

def invL(F):
    return sympy.inverse_laplace_transform(F, s, t)
```

```
L(f)
```

```
invL(L(f))
```

where $\theta(t)$ is the name used by Sympy for the unit step function.

More transforms:

```
omega = sympy.Symbol('omega', real=True)
exp = sympy.exp
sin = sympy.sin
cos = sympy.cos
functions = [1,
         t,
         t**2,
         exp(-a*t),
         t*exp(-a*t),
         t**2*exp(-a*t),
         sin(omega*t),
         cos(omega*t),
         1 - exp(-a*t),
         exp(-a*t)*sin(omega*t),
         exp(-a*t)*cos(omega*t),
         ]
functions
```

```
Fs = [L(f) for f in functions]
```

```
Fs
```

## Table of Laplace transforms

------------------------------------------------------------

## Transfer function matrix

- We start from the state equations and use the derivate operator of $s$
- We also consider only the forced output of the system

$$\dot{x} = Ax + Bu;\; y = Cx$$

$$sX = AX + BU;\; Y = CX$$

$$(sI-A)X=BU;\; Y = CX$$

$$X = (sI-A)^{-1}BU;$$ $$Y = C(sI-A)^{-1}BU$$

Matrix $G(s)=C(sI-A)^{-1}B$ is the _Transfer Function Matrix_(or _Transfer Matrix_).

Each element of $G(s)$ expresses the **dynamic** relation (there is $s$) between an input channel and an output channel.

$$
\begin{equation}
G(s)=
\begin{bmatrix}
g_{11}(s) & ... & g_{1m}(s)\\
... & ... & ... \\
g_{p1}(s) & ... & g_{pm}(s)
\end{bmatrix}
\end{equation}
$$

$g_{ij}(s)$: Transfer function between $u_j$ and $y_i$, each  $g_{ij}(s)=\frac{N(s)}{D(s)}$

### Matrix transfer function and poles

$$G(s) = C(sI-A)^{-1}BU$$

- we need to calculate the inverse of a matrix:

$$A^{-1} = \frac{Adjugate(A)}{det(A)}$$

$$\Downarrow$$

$$G(s) = \frac{CAdjugate(sI-A)B}{det(sI-A)}$$

- Roots of the denominator of $G(s)$ (values of $s$ that make $den(G)=0$) are called _poles_ of the system.
- A pole is a value of $s$ that causes $H(s)\rightarrow \inf$ (e.g. $\frac{1}{s}$, when $s=0$, $H(s)\rightarrow \inf$)
- A zero is a value of $s$ that causes $H(s)\rightarrow 0$  (e.g. $s$, when $s=0$, $H(s)=0)$

- The poles of the system $G(s)$ are, in general, a _subset_ of the roots of the eq. $det(sI-A)=0$

- which are the same as the roots of the characteristic equation: $det(A-\lambda I)=0$

- this means that the poles of the system are a subset of the eigenvalue of the matrix $A$.

## Transfer function, controllability and observability

- For each $g_{ij}(s)$, the poles of the transfer function are only the poles that are **controllable** from input $u_j$ and **observable** from the output $y_i$.

## Single Input, Single Output (SISO)

It is similar to what we had before, but now we only have one input and one output:

$$\dot{x} = Ax + bu;\; y = c^Tx$$

$$G(s) = c^T(sI-A)^{-1}b$$

Again, the transfer function only includes poles that are controllable and observable.

--------------------------------------------

## Stability in Classical Control

- Stability is the property of the system to stay close to its equilibrium $\bar{x}$ when perturbed
$$ \forall ||x_0 - \bar{x} || \le \delta \rightarrow ||x(t) - \bar{x}||\le\epsilon,\;\; \forall t\ge 0$$

- Asymptotic stability: the property of the system to go back to its equilibrium when perturbed: $||x(t)|| \rightarrow \bar{x}$, for $t\rightarrow \inf$.

**BIBO stability**
- A system is BIBO stable (Bounded Input, Bounded Output) if for each limited input, there is a limited output
- For linear systems: BIBO stability if and only if poles of the transfer functions have Re $<0$

**Intuition:** You can think of stability as meaning that your system’s response decays over time - instability implies the opposite is true, that a finite input will cause your system to respond with increasing amplitude over time, leading to an unstable system.

Note that it is possible to be maginally stable: the system has an infinite, oscillatory response (e.g., sinusoid at constant frequency and amplitude).

**Stability margins:**
- Even if your system is stable, unmodelled dynamics or parameter variations could make it unstable.
- Stability margins make it possible to evaluate the robustness of the system to these variations.

Note that too much stability is not necessarily a good thing: a system that is too stable means that will tend not to move from its equilibrium even when you desired it to do so.

## System stability and system matrix

System stability depends on the eigenvalues of the system matrix $A$ or on the poles of the transfer function:
- eigenvalues with $Re < 0$, system is asymptotically stable
- eigenvalues with $Re \le 0$, and $Re=0$ have multiplicity 1 then the system is stable

When we analyse the stability of linearised systems things can be more complicated.
- If the linear system is aymptotically stable, then the non-linear system is stable (around the equilibrium): we can always move close enough to the equilibrium to be inside its region of stability)

- If the linear system is marginally stable, we cannot say anything on the non-linear system (other non-linear dynamics become key)

## System stability and transfer function

- The stability of a linear system may be determined directly from its transfer function.
- An nth order
linear system is asymptotically stable only if all of the components in the response
from a finite set of initial conditions decay to zero as time increases, or:
$$
\lim \limits_{t\rightarrow\inf} \sum_{i=1}^{n} C_ie^{p_it}
$$

where the $p_i$ are the system poles.

For linear systems modelled through their transfer function $G(s)=\frac{N(s)}{D(s)}$, we need to analyse the roots of the characteristic equation $D(s)=0$:
- the system is stable if all the roots of the characteristic equation have negative real part
- the system is stable if all its poles have negative real part.

_In order for a linear system to be stable, all of its poles must have negative real parts,
that is they must all lie within the left-half of the s-plane. An "unstable" pole, lying in
the right half of the s-plane, generates a component in the system homogeneous response
that increases without bound from any finite initial conditions. A system having one
or more poles lying on the imaginary axis of the s-plane has non-decaying oscillatory
components in its homogeneous response, and is defined to be marginally stable._

------------------------

## More structural properties
- Stability only depends on the structure of the system (matrix A)

- Other aspects:
   - What input do we need to take the system into a given state?
   - Given the output of a system, how can we determine the state?

### Controllability
- A state $x$ is controllable if exists an input $u$ that can take the trajectory of the system from state $0$ to $x$ in finite time.
- A system is controllable if all its states are controllable

### Observability
- An initial state is observable if it is possible to determine $x$ measuring the system output $y$
- A system is observable if all its states are observable

--------------------

## Laplace Transforms Examples

### Example 1

$$F(s)=\frac{10}{s^2+12s+40}$$

- poles: $-6 \pm \sqrt{36-40} = -6 \pm \sqrt{-4} = -6 \pm j2$

- let's call $\alpha_1=-6 + j2$, $\alpha^*_1=-6 - j2$

We can then re-write:
$$
F(s) = \frac{10}{[s - (-6+j2)][s - (-6-j2)]} = \frac{10}{(s-\alpha_1)(s-\alpha^*_1)}
$$

To calculate the inverse transform we need to find the coefficients:

$$
F(s) = \frac{10}{(s-\alpha_1)(s-\alpha^*_1)} = \frac{A}{s-\alpha_1} + \frac{B}{s-\alpha^*_1}
$$

where $A$ and $B$ must be complex conjugated: $B=A^*$ (or we would not get real cofficient in $F(s)$).

$$
F(s) = \frac{10}{(s-\alpha_1)(s-\alpha^*_1)} = \frac{A}{s-\alpha_1} + \frac{A^*}{s-\alpha^*_1}
$$

$$
A = \lim \limits_{s\rightarrow \alpha_1}  \frac{10}{(s-\alpha_1)(s-\alpha^*_1)} (s-\alpha_1) = \frac{10}{\alpha_1-\alpha_1^*} = \frac{10}{2j \text{Img(}\alpha_1)}
$$

this is because $a + jb - (a - jb) = 2jb$

$$
B = \lim \limits_{s\rightarrow \alpha^*_1}  \frac{10}{(s-\alpha_1)(s-\alpha^*_1)} (s-\alpha^*_1) = \frac{10}{\alpha^*_1-\alpha_1} = -\frac{10}{2j \text{Img(}\alpha_1)}
$$

Let's see now the inverse transform:

$$
F(s) = \frac{A}{s-\alpha_1} + \frac{A^*}{s-\alpha^*_1} \rightarrow \mathcal{L^{-1}}  \rightarrow Ae^{\alpha_1t} + A^*e^{\alpha^*_1t}
$$

where we could explicit $\alpha_1 = \sigma + j\omega$, and $A=|A|e^{j\Phi_A}$, $A^*=|A|e^{-j\Phi_A}$

$$
f(t) = Ae^{\alpha_1t} + A^*e^{\alpha^*_1t} = |A|e^{j\Phi_A}e^{(\sigma +j\omega)t} + |A|e^{-j\Phi_A}e^{(\sigma -j\omega)t} = |A|e^{j\Phi_A}e^{\sigma t} e^{+j\omega t} + |A|e^{-j\Phi_A}e^{\sigma t}e^{-j\omega t}
$$

we can group things together:

$$
f(t) = |A|e^{j\Phi_A}e^{\sigma t} e^{+j\omega t} + |A|e^{-j\Phi_A}e^{\sigma t}e^{-j\omega t} = |A|e^{\sigma t} \big [   e^{j\Phi_A}e^{j\omega t} + e^{-j\Phi_A}e^{-j\omega t}  \big ] = |A|e^{\sigma t} \big [   e^{j(\Phi_A+\omega t)} + e^{-j(\Phi_A+\omega t)}  \big ]
$$

and since $\frac{e^{jx}+e^{-jx}}{2}=cos(x)$, we can write the abov expression as:

$$
f(t) = |A|e^{\sigma t} \big [   e^{j(\Phi_A+\omega t)} + e^{-j(\Phi_A+\omega t)}  \big ] = 2|A|e^{\sigma t}\cos(\Phi_A+\omega t)
$$

As expected we have an oscillatory term that could decrease or increase depending on the value of $\sigma$ (real part of the poles).

-------

### Example 2

$$F(s) = \frac{100}{(s+1)(s^2+4s+13)}$$

- poles: $s=-1, s=-2\pm j3$

We can write:

$$
F(s) = \frac{100}{(s+1)(s^2+4s+13)} = \frac{A_1}{(s+1)} + \frac{B_1}{(s-\alpha_1)} + \frac{B^*_1}{(s-\alpha^*_1)}
$$

Where we can calculate the value of the coefficient $A_1$:

- for the pole in $s=-1$
   $$
   A_1 = \frac{100}{s^2+4s+13} \bigg |_{s=-1} = \frac{100}{1-4+13} = 10
   $$

- for the pole in $s=\alpha_1=-2+j3$
   $$
   B_1 = (s-\alpha_1)\frac{100}{(s-\alpha_1)(s+1)(s-\alpha^*_1)} \bigg |_{s=-2+j3} =
   \frac{100}{(-2+j3+1)(-2+j3-(-2+j3)} = \frac{100}{(-1+j3)6j} = \frac{100}{-18-6j} \\
   = \frac{100}{-18-6j}\frac{-18+6j}{-18+6j} = \frac{100}{|-18-6j|^2}(-18+6j) = -\frac{100}{360}6(3-j)=-\frac{5}{3}(3-j)
   $$

- note: $z^{-1} = \frac{\bar{z}}{|z|^2}$

and the inverse transform is:

$$
f(t) = 10e^{-t} + 2|B_1|e^{-2t}cos(3t+\arg(B_1))
$$

### Example 3

$$
F(s) = \frac{s+3}{s(s+7)(s+1)} = \frac{A}{s} + \frac{B}{s+7} + \frac{C}{s+1}
$$

Note that when we apply partial fraction decomposition we do:

$$
\bigg [ \frac{A}{s} + \frac{B}{s+7} + \frac{C}{s+1} \bigg ]  (s+7) = \frac{A(s+7)}{s} + \frac{B(s+7)}{s+7} + \frac{C(s+7)}{s+1} \bigg | _{s=-7} = B
$$

so if we calculate the limits of both sides we can exactly what we want.

we can then

$$
B = \lim \limits_{s\rightarrow-7} \frac{s+3}{s(s+7)(s+1)} (s+7) = \frac{-4}{-7(-6)} = \frac{-4}{42} = \frac{-2}{21}
$$

we can then calculate the other terms:

$$
A = \lim \limits_{s\rightarrow 0} \frac{s+3}{s(s+7)(s+1)} (s) = \frac{3}{7}
$$

$$
C = \lim \limits_{s\rightarrow -1} \frac{s+3}{s(s+7)(s+1)} (s+1) = \frac{2}{-6} = -\frac{1}{3}
$$

Now we can do the inverse transform of:

$$
F(s) = \frac{s+3}{s(s+7)(s+1)} = \frac{3}{7}\frac{1}{s} - \frac{2}{21}\frac{1}{s+7} - \frac{1}{3}\frac{1}{s+1}
$$

$$\Downarrow$$

$$
f(t) = \frac{3}{7} 1(t) - \frac{2}{21}e^{-7t} - \frac{1}{3}e^{-t}
$$

---------------------


# Problem Sheet 2 Question 1a - 2nd Order Taylor

The general form of the population growth differential equation
\begin{equation} y^{'}=t-y, \ \ (0 \leq t \leq 4), \end{equation}
with the initial condition
\begin{equation}y(0)=1\end{equation}
For N=4
with the analytic (exact) solution
\begin{equation} y= 2e^{-t}+t+1.\end{equation}

Apply __2nd Order Taylor Method__ to approximate the solution of the given initial value problems using the indicated number of time steps. Compare the approximate solution with the given exact solution, and compare the actual error with the theoretical error.

```python
## Library
import numpy as np
import math

%matplotlib inline
import matplotlib.pyplot as plt # side-stepping mpl backend
import matplotlib.gridspec as gridspec # subplots
import warnings
import pandas as pd

warnings.filterwarnings("ignore")
```

## General Discrete Interval
The continuous time $a\leq t \leq b $ is discretised into $N$ points seperated by a constant stepsize
\begin{equation} h=\frac{b-a}{N}.\end{equation}
## Specific Discrete Interval
Here the interval is $0\leq t \leq 4$ with $N=4$
\begin{equation} h=\frac{4-0}{4}=1.\end{equation}
This gives the 5 discrete points with stepsize h=1:
\begin{equation} t_0=0, \ t_1=1, \ ... t_{4}=4. \end{equation}
This is generalised to
\begin{equation} t_i=0+i1, \ \ \ i=0,1,...,4.\end{equation}
The plot below illustrates the discrete time steps from 0 to 4.

```python
### Setting up time
t_end=4
t_start=0
N=4
h=(t_end-t_start)/(N)
t=np.arange(t_start,t_end+0.01,h)
fig = plt.figure(figsize=(10,4))
plt.plot(t,0*t,'o:',color='red')
plt.plot(t[0],0*t[0],'o',color='green')

plt.title('Illustration of discrete time points for h=%s'%(h))
plt.plot();
```

## 2nd Order Taylor Solution

The 2nd Order Taylor difference equation is given by
\begin{equation} w_{i+1}=w_i+h\left[f(t_i,w_i)+\frac{h}{2}f'(t_i,w_i)\right],\end{equation}
where
\begin{equation} f(t_i,w_i)=t_i-w_i\end{equation}
and
\begin{equation} f'(t_i,w_i)=1-t_i+w_i,\end{equation}
which gives
\begin{equation} w_{i+1}=w_i+h(t_i-w_i)+\frac{h^2}{2}(1-t_i+w_i) \end{equation}
for $i=0,1,2,3$ with the initial condition,
\begin{equation}w_0=1.\end{equation}

```python
IC=1

INTITIAL_CONDITION=IC
w=np.zeros(N+1)
w[0]=INTITIAL_CONDITION
for i in range (0,N):
    w[i+1]=w[i]+h*(t[i]-w[i])+h*h/2*(1-t[i]+w[i])
```

```python
y=2*np.exp(-t)+t-1 # Exact Solution
```

## Global Error
The upper bound of the global error is:
\begin{equation} |y(t_i)-w_i|\leq\frac{Mh^2}{6}\frac{1}{L} |e^{Lt_i}-1|, \end{equation}
where
$|y'''(t)|\leq M $ for $t \in (0,4)$ and $L$ is the Lipschitz constant.
Generally we do not have access to $M$ or $L$, but as we have the exact solution we can find $M$ and $L$.
### Local Error
The global error consistent of an exponential term and the local error.
The local error for the 2nd Order Taylor method is
\begin{equation} |y(t_1)-w_1|<\tau=\frac{Mh^2}{6}, \end{equation}
by differentiation the exact solution
$ y= 2e^{-t}+t+1$
three times
\begin{equation}y'=-2e^{-t}+1,\end{equation}
\begin{equation}y''=2e^{-t},\end{equation}
and
\begin{equation}y'''=-2e^{-t}.\end{equation}
Then we choose the largest possible value of $y''$ on the interval of interest $(0,4)$, which gives,

\begin{equation}|y''(t)|\leq |-2e^{0}|=2, \end{equation}
therefore
\begin{equation}M=2.\end{equation}
This gives the local error
\begin{equation} |y(t_1)-w_1|<\tau=\frac{Mh^2}{6}=\frac{2h^2}{6}=\frac{h^2}{3}.\end{equation}
### Lipschitz constant
The Lipschitz constant $L$ is from the Lipschitz condition,
\begin{equation}\left| \frac{\partial f(t,y)}{\partial t}\right|\leq L. \end{equation}
The constant can be found by taking partical derivative of $f(t,y)=t-y$ with respect to $y$
\begin{equation}\frac{\partial f(t,y)}{\partial y}=-1\end{equation}
\begin{equation}L=|-1|=1.\end{equation}

### Specific Global Upper Bound Formula
Puting these values into the upper bound gives:
\begin{equation} |y(t_i)-w_i|\leq\frac{h^2}{3} |e^{t_i}-1|. \end{equation}

```python
Upper_bound=h*h/(3*1)*(np.exp(t)-1) # Upper Bound
```

## Results

```python
fig = plt.figure(figsize=(12,5))
# --- left hand plot
ax = fig.add_subplot(1,4,1)
plt.plot(t,w,'o:',color='k')
plt.plot(t[0],w[0],'o',color='green')

#ax.legend(loc='best')
plt.title('Numerical Solution (w) h=%s'%(h))

# --- right hand plot
ax = fig.add_subplot(1,4,2)
plt.plot(t,y,color='blue')
plt.title('Analytic Solution (y)')

#ax.legend(loc='best')
ax = fig.add_subplot(1,4,3)
plt.plot(t,np.abs(w-y),'o:',color='red')
plt.title('Exact Error |w-y|')

# --- title, explanatory text and save

ax = fig.add_subplot(1,4,4)
plt.plot(t,np.abs(w-y),'o:',color='red')
plt.plot(t,Upper_bound,color='purple')
plt.title('Upper Bound')

# --- title, explanatory text and save
fig.suptitle(r"$y'=t-y$", fontsize=20)
plt.tight_layout()
plt.subplots_adjust(top=0.85)
```

```python

d = {'time t_i': t,    ' 2nd Order Taylor (w_i) ':w,    'Exact (y_i) ':y,    'Exact Error( |y_i-w_i|) ':np.abs(y-w),'Upper Bound':Upper_bound}
df = pd.DataFrame(data=d)
df
```

      time t_i
      2nd Order Taylor (w_i)
      Exact (y_i)
      Exact Error( |y_i-w_i|)
      Upper Bound

      0
      0.0
      1.000
      1.000000
      0.000000
      0.000000

      1
      1.0
      1.000
      0.735759
      0.264241
      0.572761

      2
      2.0
      1.500
      1.270671
      0.229329
      2.129685

      3
      3.0
      2.250
      2.099574
      0.150426
      6.361846

      4
      4.0
      3.125
      3.036631
      0.088369
      17.866050

```python

```

Chapter 02 - Higher Order Methods/2_Problem Sheet/201a_Problem_Sheet.ipynb


# Comparing Data
You'll often want to compare data in your dataset, to see if you can discern trends or relationships.

## Univariate Data
*Univariate* data is data that consist of only one variable or feature. While it may initially seem as though there's not much we can do to analyze univariate data, we've already seen that we can explore its distribution in terms of measures of central tendency and measures of variance. We've also seen how we can visualize this distribution using histograms and box plots.

Here's a reminder of how you can visualize the distribution of univariate data, using our student grade data with a few additional observations in the sample:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

plt.figure()
df['Grade'].plot( kind='box', title='Grade Distribution')
plt.figure()
df['Grade'].hist(bins=9)
plt.show()
print(df.describe())
print('median: ' + str(df['Grade'].median()))
```

## Bivariate and Multivariate Data
It can often be useful to compare *bivariate* data; in other words, compare two variables, or even more (in which case we call it *multivariate* data).

For example, our student data includes three numeric variables for each student: their salary, the number of hours they work per week, and their final school grade. Run the following code to see an enlarged sample of this data as a table:

```python
import pandas as pd

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

df[['Name', 'Salary', 'Hours', 'Grade']]
```

      Name
      Salary
      Hours
      Grade

      0
      Dan
      50000
      41
      50

      1
      Joann
      54000
      40
      50

      2
      Pedro
      50000
      36
      46

      3
      Rosie
      189000
      17
      95

      4
      Ethan
      55000
      35
      50

      5
      Vicky
      40000
      39
      5

      6
      Frederic
      59000
      40
      57

      7
      Jimmie
      42000
      45
      42

      8
      Rhonda
      47000
      41
      26

      9
      Giovanni
      78000
      35
      72

      10
      Francesca
      119000
      30
      78

      11
      Rajab
      95000
      33
      60

      12
      Naiyana
      49000
      38
      40

      13
      Kian
      29000
      47
      17

      14
      Jenny
      130000
      24
      85

Let's suppose you want to compare the distributions of these variables. You might simply create a boxplot for each variable, like this:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

df.plot(kind='box', title='Distribution', figsize = (6,4))
plt.show()
```

Hmm, that's not particularly useful is it?

The problem is that the data are all measured in different scales. Salaries are typically in tens of thousands, while hours and grades are in single or double digits.

### Normalizing Data
When you need to compare data in different units of measurement, you can *normalize* or *scale* the data so that the values are measured in the same proportional scale. For example, in Python you can use a MinMax scaler to normalize multiple numeric variables to a proportional value between 0 and 1 based on their minimum and maximum values. Run the following cell to do this:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.preprocessing import MinMaxScaler

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Normalize the data
scaler = MinMaxScaler()
df[['Salary', 'Hours', 'Grade']] = scaler.fit_transform(df[['Salary', 'Hours', 'Grade']])

# Plot the normalized data
df.plot(kind='box', title='Distribution', figsize = (10,8))
plt.show()
```

Now the numbers on the y axis aren't particularly meaningful, but they're on a similar scale.

### Comparing Bivariate Data with a Scatter Plot
When you need to compare two numeric values, a scatter plot can be a great way to see if there is any apparent relationship between them so that changes in the value of one variable affect the value of the other.

Let's look at a scatter plot of *Salary* and *Grade*:

```python
%matplotlib inline
import pandas as pd
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Create a scatter plot of Salary vs Grade
df.plot(kind='scatter', title='Grade vs Hours', x='Grade', y='Salary')
plt.show()

```

Look closely at the scatter plot. Can you see a diagonal trend in the plotted points, rising up to the right? It looks as though the higher the student's grade is, the higher their salary is.

You can see the trend more clearly by adding a *line of best fit* (sometimes called a *trendline*) to the plot:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Create a scatter plot of Salary vs Grade
df.plot(kind='scatter', title='Grade vs Salary', x='Grade', y='Salary')

# Add a line of best fit
plt.plot(np.unique(df['Grade']), np.poly1d(np.polyfit(df['Grade'], df['Salary'], 1))(np.unique(df['Grade'])))

plt.show()
```

The line of best fit makes it clearer that there is some apparent *colinearity* between these variables (the relationship is *colinear* if one variable's value increases or decreases in line with the other).

### Correlation
The apparently colinear relationship you saw in the scatter plot can be verified by calculating a statistic that quantifies the relationship between the two variables. The statistic usually used to do this is *correlation*, though there is also a statistic named *covariance* that is sometimes used. Correlation is generally preferred because the value it produces is more easily interpreted.

A correlation value is always a number between ***-1*** and ***1***.
- A positive value indicates a positive correlation (as the value of variable *x* increases, so does the value of variable *y*).
- A negative value indicates a negative correlation (as the value of variable *x* increases, the value of variable *y* decreases).
- The closer to zero the correlation value is, the weaker the correlation between *x* and *y*.
- A correlation of exactly zero means there is no apparent relationship between the variables.

The formula to calculate correlation is:

\begin{equation}r_{x,y} = \frac{\displaystyle\sum_{i=1}^{n} (x_{i} -\bar{x})(y_{i} -\bar{y})}{\sqrt{\displaystyle\sum_{i=1}^{n} (x_{i} -\bar{x})^{2}(y_{i} -\bar{y})^{2}}}\end{equation}

**rx, y** is the notation for the *correlation between x and y*.

The formula is pretty complex, but fortunately Python makes it very easy to calculate the correlation by using the ***corr*** function:

```python
import pandas as pd

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000],
                   'Hours':[41,40,36,17,35,39,40],
                   'Grade':[50,50,46,95,50,5,57]})

# Calculate the correlation between *Salary* and *Grade*
print(df['Grade'].corr(df['Salary']))
```

In this case, the correlation is just over 0.8; making it a reasonably high positive correlation that indicates salary increases in line with grade.

Let's see if we can find a correlation between *Grade* and *Hours*:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

r = df['Grade'].corr(df['Hours'])
print('Correlation: ' + str(r))

# Create a scatter plot of Salary vs Grade
df.plot(kind='scatter', title='Grade vs Hours', x='Grade', y='Hours')

# Add a line of best fit-
plt.plot(np.unique(df['Grade']), np.poly1d(np.polyfit(df['Grade'], df['Hours'], 1))(np.unique(df['Grade'])))
plt.show()

```

In this case, the correlation value is just under -0.8; meaning a fairly strong negative correlation in which the number of hours worked decreases as the grade increases. The line of best fit on the scatter plot corroborates this statistic.

It's important to remember that *correlation* **is not** *causation*. In other words, even though there's an apparent relationship, you can't say for sure that one variable is the cause of the other. In this example, we can say that students who achieved higher grades tend to work shorter hours; but we ***can't*** say that those who work shorter hours do so *because* they achieved a high grade!

## Least Squares Regression
In the previous examples, we drew a line on a scatter plot to show the *best fit* of the data. In many cases, your initial attempts to identify any colinearity might involve adding this kind of line by hand (or just mentally visualizing it); but as you may suspect from the use of the *numpy.**polyfit*** function in the code above, there are ways to calculate the coordinates for this line mathematically. One of the most commonly used techniques is *least squares regression*, and that's what we'll look at now.

Cast your mind back to when you were learning how to solve linear equations, and recall that the *slope-intercept* form of a linear equation lookes like this:

\begin{equation}y = mx + b\end{equation}

In this equation, *y* and *x* are the coordinate variables, *m* is the slope of the line, and *b* is the y-intercept of the line.

In the case of our scatter plot for our former-student's working hours, we already have our values for *x* (*Grade*) and *y* (*Hours*), so we just need to calculate the intercept and slope of the straight line that lies closest to those points. Then we can form a linear equation that calculates the a new *y* value on that line for each of our *x* (*Grade*) values - to avoid confusion, we'll call this new *y* value *f(x)* (because it's the output from a linear equation function based on *x*). The difference between the original *y* (*Hours*) value and the *f(x)* value is the *error* between our regression line of best fit and the actual *Hours* worked by the former student. Our goal is to calculate the slope and intercept for a line with the lowest overall error.

Specifically, we define the overall error by taking the error for each point, squaring it, and adding all the squared errors together. The line of best fit is the line that gives us the lowest value for the sum of the squared errors - hence the name *least squares regression*.

So how do we accomplish this? First we need to calculate the slope (*m*), which we do using this formula (in which *n* is the number of observations in our data sample):

\begin{equation}m = \frac{n(\sum{xy}) - (\sum{x})(\sum{y})}{n(\sum{x^{2}})-(\sum{x})^{2}}\end{equation}

After we've calculated the slope (*m*), we can use is to calculate the intercept (*b*) like this:

\begin{equation}b = \frac{\sum{y} - m(\sum{x})}{n}\end{equation}

Let's look at a simple example that compares the number of hours of nightly study each student undertook with the final grade the student achieved:

| Name     | Study | Grade |
|----------|-------|-------|
| Dan      | 1     | 50    |
| Joann    | 0.75  | 50    |
| Pedro    | 0.6   | 46    |
| Rosie    | 2     | 95    |
| Ethan    | 1     | 50    |
| Vicky    | 0.2   | 5     |
| Frederic | 1.2   | 57    |

First, let's take each *x* (Study) and *y* (Grade) pair and calculate *x2* and *xy*, because we're going to need these to work out the slope:

| Name     | Study | Grade | x2 | xy   |
|----------|-------|-------|------|------|
| Dan      | 1     | 50    | 1    | 50   |
| Joann    | 0.75  | 50    | 0.55 | 37.5 |
| Pedro    | 0.6   | 46    | 0.36 | 27.6 |
| Rosie    | 2     | 95    | 4    | 190  |
| Ethan    | 1     | 50    | 1    | 50   |
| Vicky    | 0.2   | 5     | 0.04 | 1    |
| Frederic | 1.2   | 57    | 1.44 | 68.4 |

Now we'll sum *x*, *y*, *x2*, and *xy*:

| Name     | Study | Grade | x2 | xy   |
|----------|-------|-------|------|------|
| Dan      | 1     | 50    | 1    | 50   |
| Joann    | 0.75  | 50    | 0.55 | 37.5 |
| Pedro    | 0.6   | 46    | 0.36 | 27.6 |
| Rosie    | 2     | 95    | 4    | 190  |
| Ethan    | 1     | 50    | 1    | 50   |
| Vicky    | 0.2   | 5     | 0.04 | 1    |
| Frederic | 1.2   | 57    | 1.44 | 68.4 |
| **Σ**      | **6.75**  | **353**   | **8.4025**| **424.5**  |

OK, now we're ready to calculate the slope for our *7* observations:

\begin{equation}m = \frac{(7\times 424.5) - (6.75\times353)}{(7\times8.4025)-6.75^{2}}\end{equation}

Which is:

\begin{equation}m = \frac{2971.5 - 2382.75}{58.8175-45.5625}\end{equation}

So:

\begin{equation}m = \frac{588.75}{13.255} \approx 44.4172\end{equation}

Now we can calculate *b*:

\begin{equation}b = \frac{353 - (44.4172\times6.75)}{7}\end{equation}

Which simplifies to:

\begin{equation}b = \frac{53.18389}{7} = 7.597699\end{equation}

Now we have our linear function:

\begin{equation}f(x) = 44.4172x + 7.597699\end{equation}

We can use this for each *x* (Study) value to calculate the *y* values for the regression line (*f(x)*), and we can subtract the original *y* (Grade) from these to calculate the error for each point:

| Name     | Study | Grade | *f(x)* | Error |
|----------|-------|-------|------|------ |
| Dan      | 1     | 50    |52.0149 |2.0149 |
| Joann    | 0.75  | 50    |40.9106 |-9.0894|
| Pedro    | 0.6   | 46    |34.2480 |-11.752|
| Rosie    | 2     | 95    |96.4321 |1.4321 |
| Ethan    | 1     | 50    |52.0149 |2.0149 |
| Vicky    | 0.2   | 5     |16.4811 |11.4811|
| Frederic | 1.2   | 57    |60.8983 |3.8983 |

As you can see, the *f(x)* values are mostly quite close to the actual *Grade* values, and the errors (which when we're comparing estimated values from a function with actual known values we we often call *residuals*) are generally pretty small.

Let's plot the least squares regression line with the actual values:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic'],
                   'Study':[1,0.75,0.6,2,1,0.2,1.2],
                   'Grade':[50,50,46,95,50,5,57],
                   'fx':[52.0159,40.9106,34.2480,96.4321,52.0149,16.4811,60.8983]})

# Create a scatter plot of Study vs Grade
df.plot(kind='scatter', title='Study Time vs Grade Regression', x='Study', y='Grade', color='red')

# Plot the regression line
plt.plot(df['Study'],df['fx'])

plt.show()
```

In this case, the line fits the middle values fairly well, but is less accurate for the outlier at the low end. This is often the case, which is why statisticians and data scientists often *treat* outliers by removing them or applying a threshold value; though in this example there are too few data points to conclude that the data points are really outliers.

Let's look at a slightly larger dataset and apply the same approach to compare *Grade* and *Salary*:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Calculate least squares regression line
df['x2'] = df['Grade']**2
df['xy'] = df['Grade'] * df['Salary']
x = df['Grade'].sum()
y = df['Salary'].sum()
x2 = df['x2'].sum()
xy = df['xy'].sum()
n = df['Grade'].count()
m = ((n*xy) - (x*y))/((n*x2)-(x**2))
b = (y - (m*x))/n
df['fx'] = (m*df['Grade']) + b
df['error'] = df['fx'] - df['Salary']

print('slope: ' + str(m))
print('y-intercept: ' + str(b))

# Create a scatter plot of Grade vs Salary
df.plot(kind='scatter', title='Grade vs Salary Regression', x='Grade', y='Salary', color='red')

# Plot the regression line
plt.plot(df['Grade'],df['fx'])

plt.show()

# Show the original x,y values, the f(x) value, and the error
df[['Grade', 'Salary', 'fx', 'error']]
```

In this case, we used Python expressions to calculate the *slope* and *y-intercept* using the same approach and formula as before. In practice, Python provides great support for statistical operations like this; and you can use the ***linregress*** function in the *scipy.stats* package to retrieve the *slope* and *y-intercept* (as well as the *correlation*, *p-value*, and *standard error*) for a matched array of *x* and *y* values (we'll discuss *p-values* later!).

Here's the Python code to calculate the regression line variables using the ***linregress*** function:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from scipy import stats

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

# Get the regression line slope and intercept
m, b, r, p, se = stats.linregress(df['Grade'], df['Salary'])

df['fx'] = (m*df['Grade']) + b
df['error'] = df['fx'] - df['Salary']

print('slope: ' + str(m))
print('y-intercept: ' + str(b))

# Create a scatter plot of Grade vs Salary
df.plot(kind='scatter', title='Grade vs Salary Regression', x='Grade', y='Salary', color='red')

# Plot the regression line
plt.plot(df['Grade'],df['fx'])

plt.show()

# Show the original x,y values, the f(x) value, and the error
df[['Grade', 'Salary', 'fx', 'error']]

```

Note that the *slope* and *y-intercept* values are the same as when we worked them out using the formula.

Similarly to the simple study hours example, the regression line doesn't fit the outliers very well. In this case, the extremes include a student who scored only 5, and a student who scored 95. Let's see what happens if we remove these students from our sample:

```python
%matplotlib inline
import pandas as pd
import numpy as np
from matplotlib import pyplot as plt
from scipy import stats

df = pd.DataFrame({'Name': ['Dan', 'Joann', 'Pedro', 'Rosie', 'Ethan', 'Vicky', 'Frederic', 'Jimmie', 'Rhonda', 'Giovanni', 'Francesca', 'Rajab', 'Naiyana', 'Kian', 'Jenny'],
                   'Salary':[50000,54000,50000,189000,55000,40000,59000,42000,47000,78000,119000,95000,49000,29000,130000],
                   'Hours':[41,40,36,17,35,39,40,45,41,35,30,33,38,47,24],
                   'Grade':[50,50,46,95,50,5,57,42,26,72,78,60,40,17,85]})

df = df[(df['Grade'] > 5) & (df['Grade'] < 95)]

# Get the regression line slope and intercept
m, b, r, p, se = stats.linregress(df['Grade'], df['Salary'])

df['fx'] = (m*df['Grade']) + b
df['error'] = df['fx'] - df['Salary']

print('slope: ' + str(m))
print('y-intercept: ' + str(b))

# Create a scatter plot of Grade vs Salary
df.plot(kind='scatter', title='Grade vs Salary Regression', x='Grade', y='Salary', color='red')

# Plot the regression line
plt.plot(df['Grade'],df['fx'])

plt.show()

# Show the original x,y values, the f(x) value, and the error
df[['Grade', 'Salary', 'fx', 'error']]

```

With the outliers removed, the line is a slightly better overall fit to the data.

One of the neat things about regression is that it gives us a formula and some constant values that we can use to estimate a *y* value for any *x* value. We just need to apply the linear function using the *slope* and *y-intercept* values we've calculated from our sample data. For example, suppose a student named Fabio graduates from our school with a final grade of **62**. We can use our linear function with the *slope* and *y-intercept* values we calculated with Python to estimate what salary he can expect to earn:

\begin{equation}f(x) = (1424.50\times62) - 7822.24 \approx 80,497 \end{equation}

MathsToML/Module04-Statistics and Probability/04-03-Comparing Data.ipynb


---
title: Triple Pendulum Example
type: submodule
---

```python
%matplotlib inline
```

Try running with this variable set to true and to false and see the difference in the resulting equations of motion

```python
use_constraints = True
```

Import all the necessary modules

```python
# -*- coding: utf-8 -*-
"""
Written by Daniel M. Aukes
Email: danaukes<at>gmail.com
Please see LICENSE for full license.
"""

import pynamics
from pynamics.frame import Frame
from pynamics.variable_types import Differentiable,Constant
from pynamics.system import System
from pynamics.body import Body
from pynamics.dyadic import Dyadic
from pynamics.output import Output,PointsOutput
from pynamics.particle import Particle
from pynamics.constraint import AccelerationConstraint
import pynamics.integration
import numpy
import sympy
import matplotlib.pyplot as plt
plt.ion()
from math import pi
```

The next two lines create a new system object and set that system as the global system within the module so that other variables can use and find it.

```python
system = System()
pynamics.set_system(__name__,system)
```

## Parameterization

### Constants

Declare constants and seed them with their default value.  This can be changed at integration time but is often a nice shortcut when you don't want the value to change but you want it to be represented symbolically in calculations

```python
lA = Constant(1,'lA',system)
lB = Constant(1,'lB',system)
lC = Constant(1,'lC',system)

mA = Constant(1,'mA',system)
mB = Constant(1,'mB',system)
mC = Constant(1,'mC',system)

g = Constant(9.81,'g',system)
b = Constant(1e1,'b',system)
k = Constant(1e1,'k',system)

preload1 = Constant(0*pi/180,'preload1',system)
preload2 = Constant(0*pi/180,'preload2',system)
preload3 = Constant(0*pi/180,'preload3',system)

Ixx_A = Constant(1,'Ixx_A',system)
Iyy_A = Constant(1,'Iyy_A',system)
Izz_A = Constant(1,'Izz_A',system)
Ixx_B = Constant(1,'Ixx_B',system)
Iyy_B = Constant(1,'Iyy_B',system)
Izz_B = Constant(1,'Izz_B',system)
Ixx_C = Constant(1,'Ixx_C',system)
Iyy_C = Constant(1,'Iyy_C',system)
Izz_C = Constant(1,'Izz_C',system)

torque = Constant(0,'torque',system)
freq = Constant(3e0,'freq',system)
```

### Differentiable State Variables

Define your differentiable state variables that you will use to model the state of the system.  In this case $qA$, $qB$, and $qC$ are the rotation angles of a three-link mechanism

```python
qA,qA_d,qA_dd = Differentiable('qA',system)
qB,qB_d,qB_dd = Differentiable('qB',system)
qC,qC_d,qC_dd = Differentiable('qC',system)
```

### Initial Values
Define a set of initial values for the position and velocity of each of your state variables.  It is necessary to define a known.  This code create a dictionary of initial values.

```python
initialvalues = {}
initialvalues[qA]=0*pi/180
initialvalues[qA_d]=0*pi/180
initialvalues[qB]=0*pi/180
initialvalues[qB_d]=0*pi/180
initialvalues[qC]=0*pi/180
initialvalues[qC_d]=0*pi/180
```

These two lines of code order the initial values in a list in such a way that the integrator can use it in the same order that it expects the variables to be supplied

```python
statevariables = system.get_state_variables()
ini = [initialvalues[item] for item in statevariables]
```

## Kinematics

### Frames
Define the reference frames of the system

```python
N = Frame('N',system)
A = Frame('A',system)
B = Frame('B',system)
C = Frame('C',system)
```

### Newtonian Frame

It is important to define the Newtonian reference frame as a reference frame that is not accelerating, otherwise the dynamic equations will not be correct

```python
system.set_newtonian(N)
```

Rotate each successive frame by amount q<new> from the last.  This approach can produce more complex equations but is representationally simple (Minimal Representation)

```python
A.rotate_fixed_axis(N,[0,0,1],qA,system)
B.rotate_fixed_axis(A,[0,0,1],qB,system)
C.rotate_fixed_axis(B,[0,0,1],qC,system)
```

### Vectors
Define the vectors that describe the kinematics of a series of connected lengths

* pNA - This is a vector with position at the origin.
* pAB - This vector is length $l_A$ away from the origin along the A.x unit vector
* pBC - This vector is length $l_B$ away from the pAB along the B.x unit vector
* pCtip - This vector is length $l_C$ away from the pBC along the C.x unit vector

Define my rigid body kinematics

```python
pNA=0*N.x
pAB=pNA+lA*A.x
pBC = pAB + lB*B.x
pCtip = pBC + lC*C.x
```

## Centers of Mass

It is important to define the centers of mass of each link.  In this case, the center of mass of link A, B, and C is halfway along the length of each

```python
pAcm=pNA+lA/2*A.x
pBcm=pAB+lB/2*B.x
pCcm=pBC+lC/2*C.x
```

## Calculating Velocity

The angular velocity between frames, and the time derivatives of vectors are extremely useful in calculating the equations of motion and for determining many of the forces that need to be applied to your system (damping, drag, etc).  Thus, it is useful, once kinematics have been defined, to take or find the derivatives of some of those vectors for calculating  linear or angular velocity vectors

### Angular Velocity
The following three lines of code computes and returns the angular velocity between frames N and A (${}^N\omega^A$), A and B (${}^A\omega^B$), and B and C (${}^B\omega^C$).  In other cases, if the derivative expression is complex or long,  you can supply pynamics with a given angular velocity between frames to speed up computation time.

```python
wNA = N.get_w_to(A)
wAB = A.get_w_to(B)
wBC = B.get_w_to(C)
```

### Vector derivatives
The time derivatives of vectors may also be

vCtip = pCtip.time_derivative(N,system)

### Define Inertias and Bodies
The next several lines compute the inertia dyadics of each body and define a rigid body on each frame.  In the case of frame C, we represent the mass as a particle located at point pCcm.

```python
IA = Dyadic.build(A,Ixx_A,Iyy_A,Izz_A)
IB = Dyadic.build(B,Ixx_B,Iyy_B,Izz_B)
IC = Dyadic.build(C,Ixx_C,Iyy_C,Izz_C)

BodyA = Body('BodyA',A,pAcm,mA,IA,system)
BodyB = Body('BodyB',B,pBcm,mB,IB,system)
BodyC = Body('BodyC',C,pCcm,mC,IC,system)
#BodyC = Particle(pCcm,mC,'ParticleC',system)
```

## Forces and Torques
Forces and torques are added to the system with the generic ```addforce``` method.  The first parameter supplied is a vector describing the force applied at a point or the torque applied along a given rotational axis.  The second parameter is the  vector describing the linear speed (for an applied force) or the angular velocity(for an applied torque)

```python
system.addforce(torque*sympy.sin(freq*2*sympy.pi*system.t)*A.z,wNA)
```

    <pynamics.force.Force at 0x7f40ebd2bcd0>

### Damper

```python
system.addforce(-b*wNA,wNA)
system.addforce(-b*wAB,wAB)
system.addforce(-b*wBC,wBC)
```

    <pynamics.force.Force at 0x7f40ebdbf1f0>

### Spring Forces

Spring forces are a special case because the energy stored in springs is conservative and should be considered when calculating the system's potential energy.  To do this, use the ```add_spring_force``` command.  In this method, the first value is the linear spring constant.  The second value is the "stretch" vector, indicating the amount of deflection from the neutral point of the spring.  The final parameter is, as above, the linear or angluar velocity vector (depending on whether your spring is a linear or torsional spring)

In this case, the torques applied to each joint are dependent upon whether qA, qB, and qC are absolute or relative rotations, as defined above.

```python
system.add_spring_force1(k,(qA-preload1)*N.z,wNA)
system.add_spring_force1(k,(qB-preload2)*A.z,wAB)
system.add_spring_force1(k,(qC-preload3)*B.z,wBC)
```

    (<pynamics.force.Force at 0x7f40ebcbef10>,
     <pynamics.spring.Spring at 0x7f40ebcbe5b0>)

### Gravity
Again, like springs, the force of gravity is conservative and should be applied to all bodies.  To globally apply the force of gravity to all particles and bodies, you can use the special ```addforcegravity``` method, by supplying the acceleration due to gravity as a vector.  This will get applied to all bodies defined in your system.

```python
system.addforcegravity(-g*N.y)
```

## Constraints
Constraints may be defined that prevent the motion of certain elements.  Try turning on the constraints flag at the top of the script to see what happens.

```python
if use_constraints:
    eq = []
    eq.append(pCtip)
    eq_d=[item.time_derivative() for item in eq]
    eq_dd=[item.time_derivative() for item in eq_d]
    eq_dd_scalar = []
    eq_dd_scalar.append(eq_dd[0].dot(N.y))
    constraint = AccelerationConstraint(eq_dd_scalar)
    system.add_constraint(constraint)
```

## F=ma
This is where the symbolic expressions for F and ma are calculated.  This must be done after all parts of the system have been defined.  The ```getdynamics``` function uses Kane's method to derive the equations of motion.

```python
f,ma = system.getdynamics()
```

    2022-01-11 14:47:26,505 - pynamics.system - INFO - getting dynamic equations

```python
f
```

    [-b*qA_d - g*lA*mA*cos(qA)/2 - g*lA*mB*cos(qA) - g*lA*mC*cos(qA) + g*lB*mB*sin(qA)*sin(qB)/2 - g*lB*mB*cos(qA)*cos(qB)/2 + g*lB*mC*sin(qA)*sin(qB) - g*lB*mC*cos(qA)*cos(qB) - g*lC*mC*(-sin(qB)*sin(qC) + cos(qB)*cos(qC))*cos(qA)/2 - g*lC*mC*(-sin(qB)*cos(qC) - sin(qC)*cos(qB))*sin(qA)/2 - k*(-preload1 + qA) + torque*sin(2*freq*pi*t), -b*qB_d + g*lB*mB*sin(qA)*sin(qB)/2 - g*lB*mB*cos(qA)*cos(qB)/2 + g*lB*mC*sin(qA)*sin(qB) - g*lB*mC*cos(qA)*cos(qB) - g*lC*mC*(-sin(qB)*sin(qC) + cos(qB)*cos(qC))*cos(qA)/2 - g*lC*mC*(-sin(qB)*cos(qC) - sin(qC)*cos(qB))*sin(qA)/2 - k*(-preload2 + qB), -b*qC_d - g*lC*mC*(-sin(qB)*sin(qC) + cos(qB)*cos(qC))*cos(qA)/2 - g*lC*mC*(-sin(qB)*cos(qC) - sin(qC)*cos(qB))*sin(qA)/2 - k*(-preload3 + qC)]

```python
ma
```

    [Izz_A*qA_dd + Izz_B*(qA_dd + qB_dd) + Izz_C*(qA_dd + qB_dd + qC_dd) + lA**2*mA*qA_dd/4 + lA**2*mB*qA_dd + lA**2*mC*qA_dd + lA*lB*mB*qA_d**2*sin(qB)/2 + lA*lB*mB*qA_dd*cos(qB)/2 - lA*lB*mB*(qA_d + qB_d)**2*sin(qB)/2 + lA*lB*mC*qA_d**2*sin(qB) + lA*lB*mC*qA_dd*cos(qB) - lA*lB*mC*(qA_d + qB_d)**2*sin(qB) + lA*mB*(lB*qA_dd/2 + lB*qB_dd/2)*cos(qB) + lA*mC*(lB*qA_dd + lB*qB_dd)*cos(qB) + lA*(-lC*mC*(qA_d + qB_d + qC_d)**2*sin(qC)/2 + mC*(lC*qA_dd/2 + lC*qB_dd/2 + lC*qC_dd/2)*cos(qC))*cos(qB) + lA*(-lC*mC*(qA_d + qB_d + qC_d)**2*cos(qC)/2 - mC*(lC*qA_dd/2 + lC*qB_dd/2 + lC*qC_dd/2)*sin(qC))*sin(qB) + lB*lC*mC*(qA_d + qB_d)**2*sin(qC)/2 - lB*lC*mC*(qA_d + qB_d + qC_d)**2*sin(qC)/2 + lB*mB*(lB*qA_dd/2 + lB*qB_dd/2)/2 + lB*mC*(lB*qA_dd + lB*qB_dd) + lB*mC*(lC*qA_dd/2 + lC*qB_dd/2 + lC*qC_dd/2)*cos(qC) + lC*mC*(lB*qA_dd + lB*qB_dd)*cos(qC)/2 + lC*mC*(lC*qA_dd/2 + lC*qB_dd/2 + lC*qC_dd/2)/2 + lC*(lA*mC*qA_d**2*sin(qB) + lA*mC*qA_dd*cos(qB))*cos(qC)/2 - lC*(-lA*mC*qA_d**2*cos(qB) + lA*mC*qA_dd*sin(qB))*sin(qC)/2, Izz_B*(qA_dd + qB_dd) + Izz_C*(qA_dd + qB_dd + qC_dd) + lA*lB*mB*qA_d**2*sin(qB)/2 + lA*lB*mB*qA_dd*cos(qB)/2 + lA*lB*mC*qA_d**2*sin(qB) + lA*lB*mC*qA_dd*cos(qB) + lB*lC*mC*(qA_d + qB_d)**2*sin(qC)/2 - lB*lC*mC*(qA_d + qB_d + qC_d)**2*sin(qC)/2 + lB*mB*(lB*qA_dd/2 + lB*qB_dd/2)/2 + lB*mC*(lB*qA_dd + lB*qB_dd) + lB*mC*(lC*qA_dd/2 + lC*qB_dd/2 + lC*qC_dd/2)*cos(qC) + lC*mC*(lB*qA_dd + lB*qB_dd)*cos(qC)/2 + lC*mC*(lC*qA_dd/2 + lC*qB_dd/2 + lC*qC_dd/2)/2 + lC*(lA*mC*qA_d**2*sin(qB) + lA*mC*qA_dd*cos(qB))*cos(qC)/2 - lC*(-lA*mC*qA_d**2*cos(qB) + lA*mC*qA_dd*sin(qB))*sin(qC)/2, Izz_C*(qA_dd + qB_dd + qC_dd) + lB*lC*mC*(qA_d + qB_d)**2*sin(qC)/2 + lC*mC*(lB*qA_dd + lB*qB_dd)*cos(qC)/2 + lC*mC*(lC*qA_dd/2 + lC*qB_dd/2 + lC*qC_dd/2)/2 + lC*(lA*mC*qA_d**2*sin(qB) + lA*mC*qA_dd*cos(qB))*cos(qC)/2 - lC*(-lA*mC*qA_d**2*cos(qB) + lA*mC*qA_dd*sin(qB))*sin(qC)/2]

## Solve for Acceleration

The next line of code solves the system of equations F=ma plus any constraint equations that have been added above.  It returns one or two variables.  func1 is the function that computes the velocity and acceleration given a certain state, and lambda1(optional) supplies the function that computes the constraint forces as a function of the resulting states

There are a few ways of solveing for a.  The below function inverts the mass matrix numerically every time step.  This can be slower because the matrix solution has to be solved for, but is sometimes more tractable than solving the highly nonlinear symbolic expressions that can be generated from the previous step.  The other options would be to use ```state_space_pre_invert```, which pre-inverts the equations symbolically before generating a numerical function, or ```state_space_post_invert2```, which adds Baumgarte's method for intermittent constraints.

```python
func1,lambda1 = system.state_space_post_invert(f,ma,return_lambda = True)
```

    2022-01-11 14:47:27,064 - pynamics.system - INFO - solving a = f/m and creating function
    2022-01-11 14:47:27,205 - pynamics.system - INFO - substituting constrained in Ma-f.
    2022-01-11 14:47:28,124 - pynamics.system - INFO - done solving a = f/m and creating function
    2022-01-11 14:47:28,125 - pynamics.system - INFO - calculating function for lambdas

## Integration Tolerance
Specify the precision of the integration

```python
tol = 1e-5
```

### Time
Define variables for time that can be used throughout the script.  These get used to create the t array, a list of every time value that is solved for during integration

```python
tinitial = 0
tfinal = 10
fps = 30
tstep = 1/fps
t = numpy.r_[tinitial:tfinal:tstep]
```

## Integrate

The next line of code integrates the function calculated

```python
states=pynamics.integration.integrate(func1,ini,t,rtol=tol,atol=tol, args=({'constants':system.constant_values},))
```

    2022-01-11 14:47:29,513 - pynamics.integration - INFO - beginning integration
    2022-01-11 14:47:29,515 - pynamics.system - INFO - integration at time 0000.00
    2022-01-11 14:47:29,858 - pynamics.integration - INFO - finished integration

## Outputs

The next section simply calculates and plots a variety of data from the previous simulation
### States

```python
plt.figure()
artists = plt.plot(t,states[:,:3])
plt.legend(artists,['qA','qB','qC'])
```

### Energy

```python
KE = system.get_KE()
PE = system.getPEGravity(pNA) - system.getPESprings()
energy_output = Output([KE-PE],system)
energy_output.calc(states,t)
energy_output.plot_time()
```

### Constraint Forces

This line of code computes the constraint forces once the system's states have been solved for.

```python
if use_constraints:
    lambda2 = numpy.array([lambda1(item1,item2,system.constant_values) for item1,item2 in zip(t,states)])
    plt.figure()
    plt.plot(t, lambda2)
```

### Motion

```python
points = [pNA,pAB,pBC,pCtip]
points_output = PointsOutput(points,system)
y = points_output.calc(states,t)
points_output.plot_time(20)
```

#### Motion Animation
in normal Python the next lines of code produce an animation using matplotlib
points_output.animate(fps = fps,movie_name = 'triple_pendulum.mp4',lw=2,marker='o',color=(1,0,0,1),linestyle='-')
To plot the animation in jupyter you need a couple extra lines of code...
from matplotlib import animation, rc
from IPython.display import HTML
HTML(points_output.anim.to_html5_video())