## Mechanical system with spring, mass and damping
Consider a mechanical oscillator including a spring
$$ F_{c}(t) = c ~ x(t) $$
with $ c > 0 $, a damping
$$
F_{D}(t) = D ~ v(t) = D ~ \dot{x}(t)
$$
with D > 0, and a mass
$$ F_{m}(t) = m ~ a(t) = m ~ \ddot{x}(t) $$

with m > 0. This system is excited by an external force $ F_{ex}(t) $ and all forces are summarized to

$$ F_{m}(t) + F_{D}(t) + F_{c}(t) = m ~ \ddot{x}(t) + D ~ \dot{x}(t) + c ~ x(t) = F_{ex}(t) \text{.} $$

The complete mechanical system is portrayed in the figure below.

```julia
foo = """
<svg height="210" width="700">
    <defs>
    <!-- arrowhead marker definition -->
    <marker id="arrow" viewBox="0 0 10 10" refX="5" refY="5"
        markerWidth="6" markerHeight="6"
        orient="auto-start-reverse">
      <path d="M 0 0 L 10 5 L 0 10 z" />
    </marker>
    </defs>
  <line x1="0" y1="0" x2="0" y2="150" style="stroke:rgb(0,0,0);stroke-width:4" />
  <!-- Damping -->
  <line x1="0" y1="30" x2="40" y2="30" style="stroke:rgb(0,0,0);stroke-width:2" />
  <line x1="40" y1="10" x2="40" y2="50" style="stroke:rgb(0,0,0);stroke-width:2" />
  <line x1="40" y1="10" x2="160" y2="10" style="stroke:rgb(0,0,0);stroke-width:2" />
  <line x1="40" y1="50" x2="160" y2="50" style="stroke:rgb(0,0,0);stroke-width:2" />
  <line x1="100" y1="15" x2="100" y2="45" style="stroke:rgb(0,0,0);stroke-width:2" />
  <line x1="100" y1="30" x2="200" y2="30" style="stroke:rgb(0,0,0);stroke-width:2" />
  <text x="120" y="70" fill="black">Damping D</text>
  <!-- Spring -->
  <line x1="0" y1="120" x2="40" y2="120" style="stroke:rgb(0,0,0);stroke-width:2" />
  <polyline points="40,120 50,100 60,140 70,100 80,140 90,100 100,140 110,100 120,140 130,100 140,140 150,100 160,140 170,100 180,120"
  style="fill:none;stroke:black;stroke-width:2" />
  <line x1="180" y1="120" x2="200" y2="120" style="stroke:rgb(0,0,0);stroke-width:2" />
  <text x="120" y="160" fill="black">Spring c</text>
  <!-- Mass -->
  <rect x="200" y="10" width="50" height="130" style="fill:rgb(255,255,255);stroke-width:3;stroke:rgb(0,0,0)" />
  <text x="210" y="160" fill="black">Mass m</text>
  <!-- External Force -->
  <line x1="250" y1="75" x2="300" y2="75" style="stroke:rgb(0,0,0);stroke-width:2" marker-end="url(#arrow)" />
  <text x="260" y="100" fill="black">Ext. Force F_ex</text>
  <!-- Overview Forces -->
  <line x1="500" y1="10" x2="500" y2="160" style="stroke:rgb(0,0,0);stroke-width:3" />
  <line x1="430" y1="25" x2="500" y2="25" style="stroke:rgb(0,0,0);stroke-width:2" marker-start="url(#arrow)" />
  <line x1="430" y1="85" x2="500" y2="85" style="stroke:rgb(0,0,0);stroke-width:2" marker-start="url(#arrow)" />
  <line x1="430" y1="145" x2="500" y2="145" style="stroke:rgb(0,0,0);stroke-width:2" marker-start="url(#arrow)" />
  <line x1="500" y1="75" x2="570" y2="75" style="stroke:rgb(0,0,0);stroke-width:2" marker-end="url(#arrow)" />
  <text x="450" y="50" fill="black">F_D</text>
  <text x="450" y="110" fill="black">F_m</text>
  <text x="450" y="170" fill="black">F_c</text>
  <text x="520" y="100" fill="black">F_ex</text>
</svg>
"""
display("image/svg+xml", foo)
```

The second order differential equation
$$ \ddot{x}(t) + \frac{D}{m} \dot{x}(t) + \frac{c}{m} x(t) ~=~ \frac{1}{m} F_{ex}(t) $$
is transfered with

$$ \frac{D}{m} = 2 ~ d ~ \omega_{0} \quad \text{,} \quad \frac{c}{m} = \omega_{0}^{2} \quad \text{,} \quad \frac{1}{m} = K ~ \omega_{0}^{2} \quad \text{and} \quad F_{ex}(t) = u(t) $$

to the general oscillation equation

$$
\begin{align}
\ddot{y}(t) + 2 d ~ \omega_{0} ~ \dot{y}(t) + \omega_{0}^{2} ~ y(t) ~=~ K ~ \omega_{0}^{2} ~ u(t) \text{.} \label{eq:gen_oscil} \tag{1}
\end{align}
$$

Equation $ \eqref{eq:gen_oscil} $ is noted with $ x_{1}(t) = y(t) $ and $ x_{2}(t) = \dot{y}(t) $ as the first order differential equation

$$
\begin{align}
\begin{pmatrix}
\dot{x}_{1}(t) \\
\dot{x}_{2}(t)
\end{pmatrix}
=
\begin{pmatrix}
0 & 1 \\
- \omega_{0}^{2} & - 2 d ~ \omega_{0}
\end{pmatrix}
\begin{pmatrix}
x_{1}(t) \\
x_{2}(t)
\end{pmatrix}
+
\begin{pmatrix}
0 \\
K ~ \omega_{0}^{2}
\end{pmatrix}
u(t)
\end{align}
$$

### Stability

The uncontrolled system (for $u(t) = 0$) is stable if matrix

$$
A = \begin{pmatrix}
0 & 1 \\
- \omega_{0}^{2} & - 2 d ~ \omega_{0}
\end{pmatrix}
$$

has only eigenvalues in the left complex space. The eigenvalues are calculated with

$$ det(\lambda I - A) = \lambda ~ (\lambda + 2 d \omega_{0}) + \omega_{0}^2 = \lambda^2 + 2 d ~ \omega_{0} \lambda + \omega_{0}^{2} = 0 $$

and thus one holds
$$ \lambda = -d ~ \omega_{0} \pm j \omega_{0} ~ \sqrt{1 - d^{2}} \text{.} $$

The uncontrolled system is always stable if $d > 0$ and $\omega_{0} > 0$, which is both guaranteed for usual mechanical systems. Furthermore, a damping $d < 0$ implies complex eigenvalues and leads to an oscillating behaviour of the solution trajectory $y(t)$.

### Numerical solution

The controlled mechancial oscillator $ \dot{x}(t) = A ~ x(t) + b ~ u(t) $ is simulated with a numerical integration method instead of calculating the solution via eigenvalues and eigenvectors. For further information about numerical integration see [Wikipedia](https://en.wikipedia.org/wiki/Numerical_methods_for_ordinary_differential_equations) and the [DifferentialEquations documentation](http://docs.juliadiffeq.org/latest/index.html).

The initial values are defined with

```julia
x₀ = [2.0, 1.0]; # Inital values
tspan = (0.0, 50.0); # Time range
d = 0.5; # Damping
ω₀ = 0.7; # Eigenfrequency
param = [d, ω₀];
```

The right-hand side $ A ~ x(t) + b ~ u(t) $  with an arbitrary input - here $u(t) = sign( sin(t))$ is chosen as a periodic excitation - and gain $K$ is defined as a function. The input is generated by the external force in the sense of a mechanical oscillator.

```julia
function mechanical_oscillator(dx, x, p, t)
    u = sign(sin(t)) # Input: periodic excitation
    K = 0.3 # Gain or amplification

    dx[1] = x[2]
    dx[2] = -(p[2]^2)*x[1] - 2*p[1]*p[2]*x[2] + K*p[2]^2 * u
end
```

    mechanical_oscillator (generic function with 1 method)

Differential equations are solved in two steps in Julia. Firstly, the mathematical problem is formulated and secondly the mathematical problem is solved with a "numerical integration" method. Tolerances are set to specify the precision of the solution.

```julia
using DifferentialEquations
mech_osc_problem = ODEProblem(mechanical_oscillator,x₀,tspan, param); # Build of the ODE problem
mech_osc_solution = solve(mech_osc_problem,Tsit5(),reltol=1e-8,abstol=1e-8); # Solution of the ODE problem
```

The calculated solution is figured in a plot.

```julia
using Plots
plot(mech_osc_solution, title="Mechanical Oscillator", label=["x1" "x2"])
```


# 4.4 Logistic Regression

The model has the form (4.17):
$$
\begin{equation}
\log \cfrac{Pr(G = 1 | X = x)}{Pr(G = k | X =x)} = \beta_{10} + \beta_1^Tx\\
\log \cfrac{Pr(G = 2 | X = x)}{Pr(G = k | X =x)} = \beta_{20} + \beta_2^Tx\\
\vdots\\
\log \cfrac{Pr(G = K-1 | X = x)}{Pr(G = k | X =x)} = \beta_{(K-1)0} + \beta_{K-1}^Tx\\
\end{equation}
$$

The model is specified in terms of K - 1 log-odds or logit transformations. A simple calculation shows that (4.18):
$$
\begin{equation}
Pr(G=k|X=x) = \cfrac{exp(\beta_{k0} + \beta_k^Tx)}{1+\sum_{l=1}^{K-1}exp(\beta_{l0} + \beta_l^Tx)},
k = 1,...,K - 1,\\
Pr(G=K|X=x) = \cfrac{1}{1+\sum_{l=1}^{K-1}exp(\beta_{l0} + \beta_l^Tx)}
\end{equation}
$$

and they clearly sum to one.


## Trajectory equations:

```python
%matplotlib inline
import matplotlib.pyplot as plt
```

```python
from sympy import *
init_printing()
```

```python
Bx, By, Bz, B = symbols("B_x, B_y, B_z, B")
x, y, z = symbols("x, y, z" )
x_0, y_0, z_0 = symbols("x_0, y_0, z_0")
vx, vy, vz, v = symbols("v_x, v_y, v_z, v")
vx_0, vy_0, vz_0 = symbols("v_x0, v_y0, v_z0")
t = symbols("t")
q, m = symbols("q, m")
c, eps0 = symbols("c, epsilon_0")
```

The equation of motion:
$$
\begin{gather*}
  m \frac{d^2 \vec{r} }{dt^2} = \frac{q}{c} [ \vec{v} \vec{B} ]
\end{gather*}
$$

For the case of a uniform magnetic field along the $z$-axis:
$$ \vec{B} = B_z = B, \quad B_x = 0, \quad B_y = 0 $$

In Cortesian coordinates:

```python
eq_x = Eq( Derivative(x(t), t, 2), q / c / m * Bz * Derivative(y(t),t) )
eq_y = Eq( Derivative(y(t), t, 2), - q / c / m * Bz * Derivative(x(t),t) )
eq_z = Eq( Derivative(z(t), t, 2), 0 )
display( eq_x, eq_y, eq_z )
```

Motion is uniform along the $z$-axis:

```python
z_eq = dsolve( eq_z, z(t) )
vz_eq = Eq( z_eq.lhs.diff(t), z_eq.rhs.diff(t) )
display( z_eq, vz_eq )
```

The constants of integration can be found from the initial conditions $z(0) = z_0$ and $v_z(0) = v_{z0}$:

```python
c1_c2_system = []
initial_cond_subs = [(t, 0), (z(0), z_0), (diff(z(t),t).subs(t,0), vz_0) ]
c1_c2_system.append( z_eq.subs( initial_cond_subs ) )
c1_c2_system.append( vz_eq.subs( initial_cond_subs ) )

c1, c2 = symbols("C1, C2")
c1_c2 = solve( c1_c2_system, [c1, c2] )
c1_c2
```

So that

```python
z_sol = z_eq.subs( c1_c2 )
vz_sol = vz_eq.subs( c1_c2 ).subs( [( diff(z(t),t), vz(t) ) ] )
display( z_sol, vz_sol )
```

For some reason I have not been able to solve the system of differential equations for $x$ and $y$ directly
with Sympy's `dsolve` function:

```python
#dsolve( [eq_x, eq_y], [x(t),y(t)] )
```

It is necessary to resort to the manual solution. The method is to differentiate one of them over
time and substitute the other. This will result in oscillator-type second-order equations for $v_y$ and $v_x$. Their solution is known. Integrating one more time, it is possible to obtain laws of motion $x(t)$ and $y(t)$.

```python
v_subs = [ (Derivative(x(t),t), vx(t)), (Derivative(y(t),t), vy(t)) ]
eq_vx = eq_x.subs( v_subs )
eq_vy = eq_y.subs( v_subs )
display( eq_vx, eq_vy )

eq_d2t_vx = Eq( diff(eq_vx.lhs,t), diff(eq_vx.rhs,t))
eq_d2t_vx = eq_d2t_vx.subs( [(eq_vy.lhs, eq_vy.rhs)] )
display( eq_d2t_vx )
```

The solution of the last equation is

```python
C1, C2, Omega = symbols( "C1, C2, Omega" )
vx_eq = Eq( vx(t), C1 * cos( Omega * t ) + C2 * sin( Omega * t ))
display( vx_eq )
omega_eq = Eq( Omega, Bz * q / c / m )
display( omega_eq )
```

where $\Omega$ is a cyclotron frequency.

```python
display( vx_eq )

vy_eq = Eq( vy(t), solve( Eq( diff(vx_eq.rhs,t), eq_vx.rhs ), ( vy(t) ) )[0] )
vy_eq = vy_eq.subs( [(Omega*c*m / Bz / q, omega_eq.rhs * c * m / Bz / q)]).simplify()
display( vy_eq )
```

For initial conditions $v_x(0) = v_{x0}, v_y(0) = v_{y0}$:

```python
initial_cond_subs = [(t,0), (vx(0), vx_0), (vy(0), vy_0) ]
vx0_eq = vx_eq.subs( initial_cond_subs )
vy0_eq = vy_eq.subs( initial_cond_subs )
display( vx0_eq, vy0_eq )

c1_c2 = solve( [vx0_eq, vy0_eq] )
c1_c2_subs = [ ("C1", c1_c2[c1]), ("C2", c1_c2[c2]) ]
vx_eq = vx_eq.subs( c1_c2_subs )
vy_eq = vy_eq.subs( c1_c2_subs )
display( vx_eq, vy_eq )
```

These equations can be integrated to obtain the laws of motion:

```python
x_eq = vx_eq.subs( vx(t), diff(x(t),t))
x_eq = dsolve( x_eq )
y_eq = vy_eq.subs( vy(t), diff(y(t),t))
y_eq = dsolve( y_eq ).subs( C1, C2 )
display( x_eq, y_eq )
```

$$x{\left (t \right )} = C_{1} + v_{x0} \left(\begin{cases} t & \text{for}\: \Omega = 0 \\\frac{1}{\Omega} \sin{\left (\Omega t \right )} & \text{otherwise} \end{cases}\right) + v_{y0} \left(\begin{cases} 0 & \text{for}\: \Omega = 0 \\- \frac{1}{\Omega} \cos{\left (\Omega t \right )} & \text{otherwise} \end{cases}\right)$$

$$y{\left (t \right )} = C_{2} - v_{x0} \left(\begin{cases} 0 & \text{for}\: \Omega = 0 \\- \frac{1}{\Omega} \cos{\left (\Omega t \right )} & \text{otherwise} \end{cases}\right) + v_{y0} \left(\begin{cases} t & \text{for}\: \Omega = 0 \\\frac{1}{\Omega} \sin{\left (\Omega t \right )} & \text{otherwise} \end{cases}\right)$$

For nonzero $\Omega$:

```python
x_eq = x_eq.subs( [(Omega, 123)] ).subs( [(123, Omega)] ).subs( [(Rational(1,123), 1/Omega)] )
y_eq = y_eq.subs( [(Omega, 123)] ).subs( [(123, Omega)] ).subs( [(Rational(1,123), 1/Omega)] )
display( x_eq, y_eq )
```

For initial conditions $x(0) = x_0, y(0) = y_0$:

```python
initial_cond_subs = [(t,0), (x(0), x_0), (y(0), y_0) ]
x0_eq = x_eq.subs( initial_cond_subs )
y0_eq = y_eq.subs( initial_cond_subs )
display( x0_eq, y0_eq )

c1_c2 = solve( [x0_eq, y0_eq] )
c1_c2_subs = [ ("C1", c1_c2[0][c1]), ("C2", c1_c2[0][c2]) ]
x_eq = x_eq.subs( c1_c2_subs )
y_eq = y_eq.subs( c1_c2_subs )
display( x_eq, y_eq )
```

```python
x_eq = x_eq.simplify()
y_eq = y_eq.simplify()
x_eq = x_eq.expand().collect(Omega)
y_eq = y_eq.expand().collect(Omega)
display( x_eq, y_eq )
```

Finally

```python
display( x_eq, y_eq, z_sol )
display( vx_eq, vy_eq, vz_sol )
display( omega_eq )
```

```python

```

examples/single_particle_in_magnetic_field/Single Particle in Uniform Magnetic Field.ipynb


Here, we outline a viscoelastic modification of the viscous contact
problem in the repository: https://github.com/agstub/grounding-line-methods.

Momentum balance and incompressibility are given by
\begin{eqnarray}
-\nabla \cdot \mathbf{\sigma}(\mathbf{u},p) &&= \rho_i\mathbf{g}  \\
\nabla \cdot \mathbf{u} &&= 0.
\end{eqnarray}
The stress is related to pressure and the deviatoric stress through
\begin{eqnarray}
\mathbf{\sigma}(\mathbf{u},p)&&= -p\mathsf{I} + \mathbf{\tau},
\end{eqnarray}
where $\mathbf{\tau}$ is the deviatoric stress tensor. Conservation of angular momentum
implies that the deviatoric stress tensor must satisfy
\begin{equation}
\tau = \tau^T.
\end{equation}

The upper-convected Maxwell viscoelastic model takes the form
\begin{equation}
\mathbf{\tau} + \frac{\eta}{G} \overset{\triangledown}{\tau} = 2\eta(\tau)\mathsf{D}(\mathbf{u}) \label{maxwell}
\end{equation}
where
\begin{eqnarray}
\mathsf{D}(\mathbf{u}) &&= \frac{1}{2}\left[\nabla \mathbf{u} + (\nabla \mathbf{u})^\mathrm{T}  \right]
\end{eqnarray}
is the strain rate and $\eta / G$ is the relaxation time
($G$ is the shear modulus and $\eta$ is viscosity).

The upper-convected time derivative is defined by
\begin{equation}
\overset{\triangledown}{\tau} = \frac{\partial \tau}{\partial t} + \mathbf{u}\cdot\nabla\tau -(\nabla\mathbf{u})\tau -\tau(\nabla\mathbf{u})^T.
\end{equation}

The viscosity is related to the deviatoric stress through Glen's law
\begin{equation}
\eta(\mathbf{\tau}) = \frac{1}{2A|\mathbf{\tau}|^{n-1}}.
\end{equation}

In the upper-convected time derivative, we make the backward Euler approximation
\begin{equation}
\frac{\partial \mathbf{\tau}}{\partial t}(\mathbf{x},t)
\approx \frac{\mathbf{\tau}(\mathbf{x},t)-\mathbf{\tau}(\mathbf{x},t-\Delta t) }{\Delta t}.
\end{equation}

For brevity, we let $(\cdot,\cdot)$ denote the inner product (or duality pairing) of scalars, vectors, and tensors over the domain $\Omega$.
We use a mixed finite element method to solve the above problem: Find $(\tau,\mathbf{u},p)$ such that
\begin{equation}
 (\tau,\nabla\mathbf{v})-(\rho_i\mathbf{g},\mathbf{v}) - (p,\nabla\cdot\mathbf{v})+(\nabla\cdot\mathbf{u},q) + (\mathbf{\tau} + \frac{\eta(\tau)}{G} \overset{\triangledown}{\tau} - 2\eta(\tau)\mathsf{D}(\mathbf{u}),\mu) + \text{BC's} = 0
\end{equation}
for all $(\mu,\mathbf{v},q)$ (where $\mu$ is a tensor-valued test function).

The symmetry constraint $\tau=\tau^T$
is incorporated directly in the finite element function space.
The "BC's" terms include a sliding law at the base, water pressure condition at the ice-water boundary, and a penalty term for enforcing the contact conditions. See the following reference for more details:
>Stubblefield, A. G., Spiegelman, M., & Creyts, T. T. (2021). Variational formulation of marine ice-sheet and subglacial-lake grounding-line dynamics. Journal of Fluid Mechanics, 919.


# Signal processing course 2018/2019-1 @ ELTE
# Assignment 4
## 10.15.2018

```python
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import random
import scipy
```

```python
sns.set(style='darkgrid')
```

## Task 1

### Z-transform

The Z-transform converts a discrete-time signal, which is a sequence of real or complex numbers, into a complex frequency domain representation.

It can be defined as either a one-sided or two-sided transform:

#### Bilateral Z-transform
The bilateral or two-sided Z-transform of a discrete-time signal $x[n]$ is the formal power series $X(z)$ defined as

$$
X(z) = \mathcal{Z} \left \{ x[n] \right \} = \sum_{n\ =\ -\infty}^{\infty} x[n] z^{-n}
$$

where $n$ is an integer and $z$ is, in general, a complex number:

$$
z = A e^{j \phi} = A \cdot (\cos{\phi} + j \sin{\phi})
$$

where $A$ is the magnitude of $z$, $j$ is the imaginary unit, and $\phi$ is the complex argument (also referred to as angle or phase) in radians.

#### Unilateral Z-transform
Alternatively, in cases where $x[n]$ is defined only for $n \geq 0$, the single-sided or unilateral Z-transform is defined as

$$
X(z) = \mathcal{Z} \left \{ x[n] \right \} =  \sum_{n\ =\ 0}^{\infty} x[n] z^{-n}
$$

In signal processing, this definition can be used to evaluate the Z-transform of the unit impulse response of a discrete-time causal system.

An important example of the unilateral Z-transform is the probability-generating function, where the component $x[n]$ is the probability that a discrete random variable takes the value $n$, and the function $X(z)$ is usually written as $X(s)$, in terms of $s = z − 1$. The properties of Z-transforms have useful interpretations in the context of probability theory.

### Inverse Z-transform
The inverse Z-transform is

$$
x[n] = \mathcal{Z}^{-1} \left \{ X(z) \right \} = \frac{1}{2 \pi j} \oint_{C} X(z) z^{n-1} dz
$$

where $C$ is a counterclockwise closed path encircling the origin and entirely in the region of convergence (ROC). In the case where the ROC is causal, this means the path $C$ must encircle all of the poles of $X(z)$.

### Region of convergence
The region of convergence (ROC) is the set of points in the complex plane for which the Z-transform summation converges:

$$
\mathrm{ROC} = \left \{ z : \left|\sum_{n\ =\ -\infty}^{\infty}x[n]z^{-n}\right| < \infty \right \}
$$

### Task 1 problems
#### General step function
The $u[n-a]$ symbols in the tasks indicate the step function at $n = a$.

```python
def u_step_function(t, a):

    if(t >= a):
        signal = 1

    else:
        signal = 0

    return(signal)
```

#### Convergence testing

```python
def TestCovergence(series_max, z):

    series_conv = np.empty(series_max)
    series_args = np.empty(series_max)

    series_sum = 0
    for i in range(0,series_max):
        series_sum += (1/(2*z)) ** i + (1/(-3*z)) ** i
        series_conv[i] = series_sum
        series_args[i] = i

    fig_series_conv, axes_series_conv = plt.subplots(figsize=(14,7))

    axes_series_conv.scatter(series_args, series_conv, c='red')
    axes_series_conv.set_title("Series converges to {0:.6f}".format(series_sum))

    plt.show()
```

#### Signal 1

$$
x[n] = {\left( \frac{1}{2} \right)}^{n} \left( u[n] - u[n - 10] \right)
$$

```python
def f_1(t):

    # Sampling rate: 1000 Hz/second
    SampRate = t * 1000
    time = np.linspace(0, t, SampRate, endpoint=True)

    # np.array for containting values of current function
    signal = np.empty(SampRate)

    i = 0
    while(i < SampRate):
        signal[i] = (1/2) ** (time[i]) * (u_step_function(time[i], 0) - u_step_function(time[i], 10))
        i += 1

    return(time, signal)
```

```python
t_1 = 15

time_1, signal_1 = f_1(t_1)
```

#### Plot signal

```python
nrows = 2
ncols = 1
fig_1, axes_1 = plt.subplots(nrows=nrows, ncols=ncols, figsize=(ncols*14, nrows*7))

axes_1[0].plot(time_1, signal_1, c='red')
axes_1[0].set_title("First causal signal")
axes_1[0].set_xlabel("Time (s)")
axes_1[0].set_ylabel("Signal value")

axes_1[1].plot(time_1, signal_1, c='red')
axes_1[1].set_title("First causal signal around 10")
axes_1[1].set_xlabel("Time (s)")
axes_1[1].set_ylabel("Signal value")
axes_1[1].set_xlim(9.9,10.1)
axes_1[1].set_ylim(-0.00025,0.00125)

plt.show()
```

#### Evaluate the Z-transform

We'll use the unilateral Z-transform, because it's a causal signal:

$$
X(z) = \mathcal{Z} \left \{ x[n] \right \} = \sum_{n\ =\ 0}^{\infty} x[n] z^{-n} = \sum_{n\ =\ 0}^{\infty} \left( {\left( \frac{1}{2} \right)}^{n} \left( u[n] - u[n - 10] \right) \cdot z^{-n} \right)
$$

Because the discrete function has zero value for all arguments, starting from $x = 10$, the Z-transform takes the following form:

$$
\begin{align}
X(z) &= \\
&= \mathcal{Z} \left \{ x[n] \right \} = \sum_{n\ =\ 0}^{9} \left( {\left( \frac{1}{2} \right)}^{n} z^{-n} \right) = \sum_{n\ =\ 0}^{9} {\left( 2z \right)}^{-n} \\
&= \frac{1}{2^{0}} \cdot \frac{1}{z^{0}} + \frac{1}{2^{1}} \cdot \frac{1}{z^{1}} + \frac{1}{2^{2}} \cdot \frac{1}{z^{2}} + \dots + \frac{1}{2^{9}} \cdot \frac{1}{z^{9}}
\end{align}
$$

Sum of the first $n$ element of a geometric series:

$$
S_{n} = a_{1} \frac{q^{n} - 1}{q - 1}
$$

Where $a_1$ is the first element of the series, and $q$ is the constant that each term is multiplied by to get the next term.
Thus:

$$
X(z) = \frac{1}{2^{0}} \cdot \frac{1}{z^{0}} \cdot \frac{{\tfrac{1}{2z}}^{n} - 1}{\tfrac{1}{2z} - 1} \Biggr\rvert_{\ n\ =\ 9} = \frac{{\tfrac{1}{2z}}^{n} - 1}{\tfrac{1}{2z} - 1} \Biggr\rvert_{\ n\ =\ 9} = \frac{{\tfrac{1}{512z^{9}}} - 1}{\tfrac{1}{2z} - 1}
$$

#### Calculate ROC

$$
\mathrm{ROC} = \left\{ \left| z \right| : \mathrm{R} \text{\\} \{0\} \right\}
$$

#### Signal 2

$$
x[n] = {\left( \frac{1}{2} \right)}^{\left| n \right|}
$$

```python
def f_2(t):

    # Sampling rate: 1000 Hz/second
    SampRate = t * 1000
    time = np.linspace(0, t, SampRate, endpoint=True)

    # np.array for containting values of current function
    signal = np.empty(SampRate)

    i = 0
    while(i < SampRate):
        signal[i] = (1/2) ** np.abs((time[i]))
        i += 1

    return(time, signal)
```

```python
t_2 = 15

time_2, signal_2 = f_2(t_2)
```

#### Plot signal

```python
nrows = 1
ncols = 1
fig_2, axes_2 = plt.subplots(nrows=nrows, ncols=ncols, figsize=(ncols*14, nrows*7))

axes_2.plot(time_2, signal_2, c='red')
axes_2.set_title("Second causal signal")
axes_2.set_xlabel("Time (s)")
axes_2.set_ylabel("Signal value")

plt.show()
```

#### Evaluate the Z-transform

We'll use the unilateral Z-transform, because it's also a causal signal, like in the task before. Therefore all $\left| n \right|$ simplifies to $n$:

$$
\begin{align}
X(z) &= \\
&= \mathcal{Z} \left \{ x[n] \right \} = \sum_{n\ =\ 0}^{\infty} x[n] z^{-n} = \sum_{n\ =\ 0}^{\infty} \left( {\left( \frac{1}{2} \right)}^{\left| n \right|} \cdot z^{-n} \right) = \\
&= \sum_{n\ =\ 0}^{\infty} {\left( \frac{1}{2z} \right)}^{\left| n \right|} = \sum_{n\ =\ 0}^{\infty} {\left( 2z \right)}^{-n}
\end{align}
$$

Sum of the first $n$ element of a geometric series:

$$
S_{n} = a_{1} \frac{q^{n} - 1}{q - 1}
$$

Where $a_1$ is the first element of the series, and $q$ is the constant that each term is multiplied by to get the next term.
Thus:

$$
X(z) = \frac{1}{2^{0}} \cdot \frac{1}{z^{0}} \cdot \frac{{\tfrac{1}{2z}}^{n} - 1}{\tfrac{1}{2z} - 1} \Biggr\rvert_{\ n\ =\ \infty} = \frac{{\tfrac{1}{2z}}^{n} - 1}{\tfrac{1}{2z} - 1} \Biggr\rvert_{\ n\ =\ \infty} = \frac{{\tfrac{1}{2^{\infty} \cdot z^{\infty}}} - 1}{\tfrac{1}{2z} - 1} = - \frac{1}{\tfrac{1}{2z} - 1}
$$

#### Calculate ROC

$$
\mathrm{ROC} = \left\{ \left| z \right| : \left( 0.5,\ \infty \right) \right\}
$$

#### Signal 3

$$
x[n] = {\left( 5 \right)}^{\left| n \right|}
$$

```python
def f_3(t):

    # Sampling rate: 1000 Hz/second
    SampRate = t * 1000
    time = np.linspace(0, t, SampRate, endpoint=True)

    # np.array for containting values of current function
    signal = np.empty(SampRate)

    i = 0
    while(i < SampRate):
        signal[i] = 5 ** np.abs((time[i]))
        i += 1

    return(time, signal)
```

```python
t_3 = 15

time_3, signal_3 = f_3(t_3)
```

#### Plot signal

```python
nrows = 1
ncols = 1
fig_3, axes_3 = plt.subplots(nrows=nrows, ncols=ncols, figsize=(ncols*14, nrows*7))

axes_3.plot(time_3, signal_3, c='red')
axes_3.set_title("Third causal signal")
axes_3.set_xlabel("Time (s)")
axes_3.set_ylabel("Signal value")

plt.show()
```

#### Evaluate the Z-transform

We'll use the unilateral Z-transform, because it's also a causal signal, like in the task before. Therefore all $\left| n \right|$ simplifies to $n$:

$$
\begin{align}
X(z) &= \\
&= \mathcal{Z} \left \{ x[n] \right \} = \sum_{n\ =\ 0}^{\infty} x[n] z^{-n} = \sum_{n\ =\ 0}^{\infty} \left( {\left( 5 \right)}^{\left| n \right|} \cdot z^{-n} \right) = \sum_{n\ =\ 0}^{\infty} {\left( \frac{5}{z} \right)}^{\left| n \right|}
\end{align}
$$

Sum of the first $n$ element of a geometric series:

$$
S_{n} = a_{1} \frac{q^{n} - 1}{q - 1}
$$

Where $a_1$ is the first element of the series, and $q$ is the constant that each term is multiplied by to get the next term.
Thus:

$$
X(z) = \frac{5^{0}}{z^{0}} \cdot \frac{{\tfrac{5}{z}}^{n} - 1}{\tfrac{5}{z} - 1} \Biggr\rvert_{\ n\ =\ \infty} = \frac{{\tfrac{5}{z}}^{n} - 1}{\tfrac{5}{z} - 1} \Biggr\rvert_{\ n\ =\ \infty} = \frac{{\tfrac{5^{\infty}}{z^{\infty}}} - 1}{\tfrac{5}{z} - 1} \longrightarrow \infty \quad \text{if $\frac{5}{\left| z \right|} > 1$}
$$

#### Calculate ROC

But!
The series converges, if

$$
\frac{5}{\left| z \right|} \leq 1
$$

Thus the value of ROC:

$$
\mathrm{ROC} = \left\{ \left| z \right| : \left[ 0,\ 5 \right] \right\}
$$

#### Signal 4

$$
x[n] = {\left( \frac{1}{2} \right)}^{n} \cdot \cos{\left( \frac{\pi n}{3} \right)} \cdot u[n]
$$

```python
def f_4(t):

    # Sampling rate: 1000 Hz/second
    SampRate = t * 1000
    time = np.linspace(0, t, SampRate, endpoint=True)

    # np.array for containting values of current function
    signal = np.empty(SampRate)

    i = 0
    while(i < SampRate):
        signal[i] = (1/2) ** (time[i]) * np.cos(np.pi * time[i] / 3) * u_step_function(time[i], 0)
        i += 1

    return(time, signal)
```

```python
t_4 = 15

time_4, signal_4 = f_4(t_4)
```

#### Plot signal

```python
nrows = 1
ncols = 1
fig_4, axes_4 = plt.subplots(nrows=nrows, ncols=ncols, figsize=(ncols*14, nrows*7))

axes_4.plot(time_4, signal_4, c='red')
axes_4.set_title("Third causal signal")
axes_4.set_xlabel("Time (s)")
axes_4.set_ylabel("Signal value")

plt.show()
```

#### Evaluate the Z-transform

We'll use the unilateral Z-transform, because it's also a causal signal, like in the task before. Therefore all $\left| n \right|$ simplifies to $n$:

$$
\begin{align}
X(z) &= \\
&= \mathcal{Z} \left \{ x[n] \right \} = \sum_{n\ =\ 0}^{\infty} x[n] z^{-n} = \sum_{n\ =\ 0}^{\infty} {\left( \frac{1}{2} \right)}^{n} \cdot \cos{\left( \frac{\pi n}{3} \right)} \cdot u[n] \cdot z^{-n} = \\
&= \sum_{n\ =\ 0}^{\infty} {\left( \frac{1}{2} \right)}^{n} \cdot \cos{\left( \frac{\pi n}{3} \right)} \cdot z^{-n} = \sum_{n\ =\ 0}^{\infty} {\left( \frac{1}{2z} \right)}^{n} \cdot \cos{\left( \frac{\pi n}{3} \right)}
\end{align}
$$

Sum of the first $n$ element of a geometric series:

$$
S_{n} = a_{1} \frac{q^{n} - 1}{q - 1}
$$

Where $a_1$ is the first element of the series, and $q$ is the constant that each term is multiplied by to get the next term.

```python
series_max_testconv_task1_4 = 100
z_testconv_task1_4 = 0.5

TestCovergence(series_max_testconv_task1_4, z_testconv_task1_4)
```

Thus the Z-trandors evaluates as follows:

$$
\begin{align}
X(z) &= \\
&= {\left( \frac{1}{2z} \right)}^{0} \cdot \cos{\left( \frac{\pi \cdot 0}{3} \right)}
\cdot
\frac{\left( \left( \frac{1}{2z} \right) \cdot \cos{\left( \frac{\pi}{3} \right)} \right)^{n} - 1}{{\left( \frac{1}{2z} \right)} \cdot \cos{\left( \frac{\pi}{3} \right)} - 1} \Biggr\rvert_{\ n\ =\ \infty} = \\
&= \frac{\left( \left( \frac{1}{2z} \right) \cdot \cos{\left( \frac{\pi}{3} \right)} \right)^{n} - 1}{{\left( \frac{1}{2z} \right)} \cdot \cos{\left( \frac{\pi}{3} \right)} - 1} \Biggr\rvert_{\ n\ =\ \infty}
=
\frac{\left( \left( \frac{1}{2z} \right) \cdot \tfrac{1}{2} \right)^{n} - 1}{{\left( \frac{1}{2z} \right)} \cdot \tfrac{1}{2} - 1} \Biggr\rvert_{\ n\ =\ \infty} = \\
&= \frac{\left( \frac{1}{4z} \right)^{n} - 1}{{\left( \frac{1}{4z} \right)} - 1} \Biggr\rvert_{\ n\ =\ \infty}
=
\frac{\frac{4}{3} \cdot \frac{1}{z^{\infty}} - 1}{{\left( \frac{1}{4z} \right)} - 1}
=
\frac{\frac{4}{3} - 1}{{\left( \frac{1}{4z} \right)} - 1} = \\
&= \frac{1}{3} \cdot \frac{1}{{\left( \frac{1}{4z} \right)} - 1}
=
\frac{1}{{\frac{3}{4z}} - 3}
\end{align}
$$

#### Calculate ROC

$$
\mathrm{ROC} = \left\{ \left| z \right| : \left( 0.25,\ +\infty \right) \right\}
$$

## Task 2

### Part 1

Formalism for Python:

```python
y = scipy.signal.lfilter(b, a, x, axis=-1, zi=None)
```

```python

```

### Part 2

#### Input discrete signal

$$
x[n] = \left[ \left( \frac{1}{2} \right)^{n} + \left( -\frac{1}{3} \right)^{n} \right] \cdot u[n]
$$

#### Z-transform of signal

$$
\begin{align}
X(z) &= \\
&=
\mathcal{Z} \left \{ x[n] \right \}
=
\sum_{n\ =\ 0}^{\infty} x[n] z^{-n}
=
\sum_{n\ =\ 0}^{\infty} \left\{ \left[ \left( \frac{1}{2} \right)^{n} + \left( -\frac{1}{3} \right)^{n} \right] \cdot u[n] \cdot z^{-n} \right\} = \\
&=
\sum_{n\ =\ 0}^{\infty} \left\{ \left[ \left( \frac{1}{2} \right)^{n} + \left( -\frac{1}{3} \right)^{n} \right] \cdot z^{-n} \right\}
=
\sum_{n\ =\ 0}^{\infty} \left\{ \left( \frac{1}{2z} \right)^{n} + \left( -\frac{1}{3z} \right)^{n} \right\}
= \\
&=
\frac{6^{-m} \cdot \left( 2^{m + 2} \cdot 3^{m + 1} \cdot z^{2} - 3^{m + 1} \left( \frac{1}{z} \right)^{m - 1} - 3^{m} \left( \frac{1}{z} \right)^{m} - 2^{m} \left( -\frac{1}{z} \right)^{m} - 6^{m} \cdot z + 2^{m + 1} \cdot z \left( -\frac{1}{z} \right)^{m} \right)}{\left( 2z - 1 \right) \cdot \left( 3z + 1 \right)} \quad \text{where m $\to \infty$}
\end{align}
$$

#### Calculate ROC

$$
\mathrm{ROC} = \left\{ \left| z \right| : \left[ 0.5,\ \infty \right) \right\}
$$

### Part 3

Test convergence numercially

```python
series_max_testconv_task2 = 100
z_testconv_task2 = 0.5

TestCovergence(series_max_testconv_task2, z_testconv_task2)
```

## Task 4

Prove, that if

$$
x \left[ n \right] = \left( r^{n} \sin{\left( \omega_{0} n \right)} \right) \cdot u \left[ n \right]
$$

then the Z-transform evaluates as follows:

$$
X \left( z \right) = \frac{r \sin{\left( \omega_{0} \right)} \cdot z^{-1}}{1 - 2 r \sin{\left( \omega_{0} \right)} \cdot z^{-1} + r^{2} \cdot z^{-2}}
$$

and

$$
\mathrm{ROC} = \left\{ \left| z \right| > r \right\}
$$

$$
\begin{align}
X(z) &= \\
&=
\mathcal{Z} \left \{ x[n] \right \}
=
\sum_{n\ =\ 0}^{\infty} x[n] z^{-n}
=
\sum_{n\ =\ 0}^{\infty} \left\{ \left( r^{n} \sin{\left( \omega_{0} n \right)} \right) \cdot u \left[ n \right] \cdot z^{-n} \right\}
= \\
&=
\sum_{n\ =\ 0}^{\infty} \frac{r^{n} \sin{\left( \omega_{0} n \right)}}{z^{n}}
=
\end{align}
$$

```python

```


Basic Symbolic Quantum Mechanics

```
%load_ext sympyprinting
```

```
from sympy import sqrt, symbols, Rational
from sympy import expand, Eq, Symbol, simplify, exp, sin
from sympy.physics.quantum import *
from sympy.physics.quantum.qubit import *
from sympy.physics.quantum.gate import *
from sympy.physics.quantum.grover import *
from sympy.physics.quantum.qft import QFT, IQFT, Fourier
from sympy.physics.quantum.circuitplot import circuit_plot
```

Bras and Kets

Create symbolic states

```
phi, psi = Ket('phi'), Ket('psi')
alpha, beta = symbols('alpha beta', complex=True)
```

Create a superposition

```
state = alpha*psi + beta*phi; state

```

Dagger the superposition and multiply the original

```
ip = Dagger(state)*state; ip

```

Distribute

```
qapply(expand(ip))

```

Operators

Create symbolic operators

```
A = Operator('A')
B = Operator('B')
C = Operator('C')
```

Test commutativity

```
A*B == B*A

```

    False

Distribute A+B squared

```
expand((A+B)**2)
```

Create a commutator

```
comm = Commutator(A,B); comm

```

Carry out the commutator

```
comm.doit()
```

Create a more fancy commutator

```
comm = Commutator(A*B,B+C); comm
```

Expand the commutator

```
comm.expand(commutator=True)
```

Carry out and expand the commutators

```
_.doit().expand()

```

Take the dagger

```
Dagger(_)
```


# 3D backends

Three backends support 3D plotting: `PlotlyBackend`, `K3DBackend` and `MatplotlibBackend`.

```python
%matplotlib widget
from sympy import *
from spb import *
from spb.backends.plotly import PB
from spb.backends.k3d import KB
from spb.backends.matplotlib import MB
import numpy as np
var("u, v, x, y")
```

First, let's examine a plot created with Matplotlib:

```python
r = 2 + sin(7 * u + 5 * v)
expr = (
    r * cos(u) * sin(v),
    r * sin(u) * sin(v),
    r * cos(v)
)
plot3d_parametric_surface(*expr, (u, 0, 2 * pi), (v, 0, pi), backend=MB)
```

Here, we can guess what the exact shape of the surface is going to be. We could increase the number of discretization points, in the `u` and `v` directions, but we are not going to do that with Matplotlib (it would be a suicide). As always, we can use the toolbar buttons to zoom in and out. Now, try to click and drag the surface: there is a lot of lag. Matplotlib is not designed to be interactive.

Let's plot the same surface with `K3DBackend`. Since we are at it, let's also bump up the number of discretization points to 500 on both parameters. The resulting mesh will have 250 thousand points, therefore the computation may take a few seconds (depending on our machine). Note one major difference with SymPy's plotting module: to specify the same numer of discretization points on both directions we can use the keyword argument `n`. Alternatively, we could use `n1` and `n2` to specify different numbers of discretization points.

```python
n = 500
plot3d_parametric_surface(*expr, (u, 0, 2 * pi), (v, 0, pi),
                          backend=KB, n=n)
```

To interact with the plot:
* Left click and drag: rotate the plot.
* Scroll with the mouse wheel: zoom in and out.
* Right click and drag: pan.

Note how smooth the interaction is!!!

On the top right corner there is a menu with a few entries:
* **Controls**: we can play with a few options, like hiding the grids, going full screen, ..., add and remove clipping planes.
* **Objects**: we can see the objects displayed on the plot. Let's click the `Mesh #1` entry: we can hide/show the object, its color legend, we can turn on wireframe view (don't do it with such a high number of points, it will slows things down a lot!). Note that by default a color map is applied to the surface, hence we cannot change its color. We will see in the next notebook how to do it.
* **Info**: useful information for debug purposes.

It is left to the Reader to play with the controls and learn what they do.

Note that the name of the surface displayed under **Objects** is `Mesh #1`. If we plot multiple expressions, the names will be `Mesh #1`, `Mesh #2`, ... This is the default behaviour for `K3DBackend`. We can also chose to display the string representation of the expression by setting `show_label=True`, but it is safe to assume that the label won't fit the small amount of width of the **Controls** user interface, therefore it makes sense to leave that option unset.

Let's now look at a simple example which illustrate that no matter what backend we are using, we ultimately have to decide if the plot is correct or not:

```python
expr = re(atan(x + I*y))
n = 200
plot3d(expr, backend=KB, n=n)
```

Here, the vertical wall at $x=0$ is rendered, but should not!!! There is a discontinuity there.

As we can see above, by default a color map is applied to the surface, ranging from the minimum and maximum value of the z-coordinate. If we wish to switch to an automatic solid coloring, we need to set the `use_cm=False`. Also, keep in mind that `K3D` natively support latex. Let's try it:

```python
n = 500
expr = cos(2 * pi * x * y)
plot3d(expr, (x, -2, 2), (y, -2, 2), backend=KB,
       n=n, use_cm=False,
       title=r"\text{K3D - Latex support} \qquad f(x, y) = " + latex(expr))
```

```python
n = 100
expr = (
    2 * (1 - exp(u / (6 * pi))) * cos(u) * cos(v / 2)**2,
    2 * (-1 + exp(u / (6 * pi))) * sin(u) * cos(v / 2)**2,
    1 - exp(u / (3 * pi)) - sin(v) + exp(u / (6 * pi)) * sin(v)
)
plot3d_parametric_surface(*expr, (u, 0, 6*pi), (v, 0, 2*pi),
                          backend=KB, n=n, use_cm=True)
```

Let's now try `PlotlyBackend`. The main difference between `PlotlyBackend` and `K3DBackend` is that the former can stretch the axis, whereas the latter (being more engineering-oriented) uses a fixed aspect ratio representing reality. We can control this behaviour by setting an appropriate value for the `aspect` keyword:

```python
plot3d_parametric_surface(*expr, (u, 0, 2**pi), (v, 0, 2*pi),
                          backend=PB, n=n, aspect="cube")
```

Note that the z axis is stretched!

The two other major differences are:
* `PlotlyBackend` is consistently slower at rendering 3D objects than the other two backends.
* `PlotlyBackend` doesn't natively support wireframe. While it is possible to draw wireframe by setting `wireframe=True`, it will significantly slow down the rendering process, even for a small number of discretization points. Therefore, it is advisable not to use wireframes with Plotly.
* By moving the cursor over the surface, we can actually see the coordinates of the "selected" point. This is not currently possible with `K3DBackend` or `MayaviBackend`.

```python
n = 100
expr = (
    (-(2/15) * cos(u) * (3 * cos(v) - 30 * sin(u) + 90 * cos(u)**4 * sin(u) -
    60 * cos(u)**6 * sin(u) + 5 * cos(u) * cos(v) * sin(u))),
    (-(1/15) * sin(u) * (3 * cos(v) - 3 * cos(u)**2 * cos(v) - 48 * cos(u)**4 * cos(v) +
    48 * cos(u)**6 * cos(v) - 60 * sin(u) + 5 * cos(u) * cos(v) * sin(u)
    - 5 * cos(u)**3 * cos(v) * sin(u) - 80 * cos(u)**5 * cos(v) * sin(u) +
    80 * cos(u)**7 * cos(v) * sin(u))),
    (2/15) * (3 + 5 * cos(u) * sin(u)) * sin(v)
)
plot3d_parametric_surface(*expr, (u, 0, pi), (v, 0, 2 * pi),
                          backend=PB, n=n)
```

```python
# https://mathematica.stackexchange.com/a/37715
expr = (
    cos(v) * (6 - (5/4 + sin(3 * u)) * sin(u - 3 * v)),
     (6 - (5/4 + sin(3 * u)) * sin(u - 3 * v)) * sin(v),
     -cos(u - 3 * v) * (5/4 + sin(3 * u))
)
plot3d_parametric_surface(*expr, (u, 0, 2 * pi), (v, 0, 2 * pi),
                          backend=KB, n=n)
```

```python

```


# What is Symbolic Computation?
Symbolic computation deals with the computation of mathematical objects symbolically. This means that the mathematical objects are represented exactly, not approximately, and mathematical expressions with unevaluated variables are left in symbolic form.

# SymPy Solver Demo
let's create a symbolic variable $z$, and define an arbitrary equation of $55.9728340445699z^2+32z−28.1086638217203=0$, and solve it symbolically.

```python
# Written by Jacob Chodoriwsky and Ghassem Tofighi
# SymPy is the python library for symbolic mathematics.
# https://docs.sympy.org/latest/index.html
import sympy as sp
import math
sp.init_printing(use_unicode=True)

# Define z as a symbolic variable.
z = sp.symbols('z')

# Get the k-value, substitute it into the function.
# Note that cos() takes angles in radians, so convert from degrees.
k = math.cos(math.radians(33))
k
```

```python
# Define our function.
f = (81*k**2 - 1)*z**2 + 32*z + (324*k**2 - 256)
f
```

```python
# Define the related equation.
eqn = sp.Eq(f,0)
eqn
```

```python
# Solve the equation
sp.solveset(eqn, z)
```

# How calculate RREF and solve system of linear equations in SymPy?
Assume the following system of linear equations. SymPy can reduce the following matrices to RREF:

$\left(\begin{array}{ccc|c}1 & 2 & -4 & 12 \\ 2 & 6 & 8 & 14 \\ 0 & 1 & 8 & -5\end{array}\right)$

\begin{alignedat}{3}
% R & L   &  R & L   &  R & L
  x & +{} &  2y & +{} & -4z & = 12 \\
  2x & +{} &  6y & +{} &  8z & = 14 \\
 & {} &  y & +{} &  8z & = -5 \\
\end{alignedat}

To calculate RREF we can use augmented matrix as follows:

```python
#Defining symbolic augmented matrix
A = sp.Matrix([[1,2,-4,12],[2,6,8,14],[0,1,8,-5]])
A

```

$\displaystyle \left[\begin{matrix}1 & 2 & -4 & 12\\2 & 6 & 8 & 14\\0 & 1 & 8 & -5\end{matrix}\right]$

```python
# print RREF
A.rref()
```

$\displaystyle \left( \left[\begin{matrix}1 & 0 & -20 & 22\\0 & 1 & 8 & -5\\0 & 0 & 0 & 0\end{matrix}\right], \  \left( 0, \  1\right)\right)$

# Solving system of linear equations in SymPy

RREF method does not provide final solutions for the following system of linear equations:

\begin{alignedat}{3}
% R & L   &  R & L   &  R & L
  x & +{} &  2y & +{} & -4z & = 12 \\
  2x & +{} &  6y & +{} &  8z & = 14 \\
 & {} &  y & +{} &  8z & = -5 \\
\end{alignedat}

To find solutions in Sympy you should use the *Sandard Matrix Representation*, $A\mathbf{x}=\mathbf{b}$:

\begin{alignedat}{3}
\left[\begin{array}{ccc}1 & 2 & -4 \\ 2 & 6 & 8 \\ 0 & 1 & 8\end{array}\right]\left[\begin{array}{l}x \\ y \\ z\end{array}\right]=\left[\begin{array}{c}12 \\ 14 \\ -5\end{array}\right]
\end{alignedat}

- $A$ is called the coefficient matrix
- $\mathbf{x}$ is the unknowns or solution vector
- $\mathbf{b}$ is the constant vector

Where

- $A=\left[\begin{array}{ccc}1 & 2 & -4 \\ 2 & 6 & 8 \\ 0 & 1 & 8\end{array}\right]$

- $\mathbf{x}=\left[\begin{array}{c}x \\ y \\ z\end{array}\right]$

- $\mathbf{b}=\left[\begin{array}{c}12 \\ 14 \\ -5\end{array}\right]$

To solve the system you can use `sp.linsolve` as follows:

```python
x, y, z = sp.symbols('x, y, z')
A = sp.Matrix([[1, 2, -4], [2, 6, 8], [0, 1, 8]])
b = sp.Matrix([12, 14, -5])
sp.linsolve((A, b), x, y, z)
```

The above answer is equivalent to what we find manually after solving using RREF method:
$$
\begin{aligned}
&z=t \in \mathbb{R}\\
&y=-5-8 t \\
&x=22+20 t
\end{aligned}
$$

# Calculating the Inverse Matrix
You can calculate the inverse of matrix using `inv()` method as follows:
## Note:
A square matrix is invertible if and only if the determinant is not equal to zero.

```python
A = sp.Matrix([[1, 2, -4], [2, 6, 8], [0, 1, 8]])
if(A.det()!=0):
  A_inv=A.inv()
else:
  A_inv=[]
  print ("Non Invertible Matrix: Matrix det == 0; not invertible.")
A_inv
```

```python
A = sp.Matrix([[2, 1, 0], [0, 4, -3], [1, -3, 2]])
if(A.det()!=0):
  A_inv=A.inv()
else:
  A_inv=[]
  print ("Non Invertible Matrix: Matrix det == 0; not invertible.")

A_inv
```

$\displaystyle \left[\begin{matrix}\frac{1}{5} & \frac{2}{5} & \frac{3}{5}\\\frac{3}{5} & - \frac{4}{5} & - \frac{6}{5}\\\frac{4}{5} & - \frac{7}{5} & - \frac{8}{5}\end{matrix}\right]$


# Gradient Boost Regression Tree

Gradient Boost Regression Trees (GBRT) are an ensemble learning technique that successivley trains new weak learners which are added to the ensemble to make up for errors in the previous ensemble.  The term "Gradient Boost" is derived from the structure of the algorithm to update the ensemble, which looks like gradient descent in the functional space.

Suppose we are given a task to predict an output variable $y$ from a set of training data $x$.  Let $F_{m}$ denote the predicted output from our ensemble predictor at step $m$ of the GBRT.  As $F_{m}$ is an imperfect predictor, the goal of GBRT is to add a new learner to the ensemble to try and reduce the prediction error associated with $F_{m}$.  The ensemble at step $m+1$ can be expressed as:
$$
F_{m+1} = F_{m} + \arg\min_{h}\Big[ \frac{1}{n}\sum_{i=1}^{n} L \big(y_{i},F_{m}(x_{i}) + h(x_{i})\big) \Big]
$$

where $L()$ is the loss function.  Instead of optimizing the rightmost term completely, the nonlinear loss function can be approximated by a first order Taylor Expansion (as L is assumed to be differentiable):
$$
\begin{equation}
L \big(y_{i},F_{m}(x_{i}) + h_{m}(x_{i})\big) \approx L \big(y_{i},F_{m}(x_{i}))\big) +  h(x_{i})\frac{dL(y_{i},F(x_{i}))}{dF(x_{i})}\Bigg|_{F_{m}(x_{i})}
\end{equation}
$$

Let
$$
\begin{equation}
g_{i} = \frac{dL(y_{i},F(x_{i}))}{dF(x_{i})}\Bigg|_{F_{m}(x_{i})}
\end{equation}
$$

Noting that the first term on the RHS of the Taylor Expansion doesn't affect the choice of $h$, we have:
$$
\begin{equation}
\arg\min_{h}\Big[ \frac{1}{n}\sum_{i=1}^{n} L \big(y_{i},F_{m}(x_{i}) + h(x_{i})\big) \Big] \approx \arg\min_{h} \Bigg[ \frac{1}{n}\sum_{i=1}^{n} h(x_{i})g_{i}\Bigg]
\end{equation}
$$

The quantity on the RHS will be minimized if $h$ is fit to predict $-\alpha g_{i}$ where $\alpha$ is positive and arbitrarily large.  In practice, $\alpha$ should be kept relatively small so that the first order Taylor Expansion is a good approximation.  If the loss function is the squared error, then it is straightforward to show that $g_{i}$ is the residual $r_{i} = y_{i} - F_{m}(x_{i})$.  Choosing $\alpha$ small implies only small improvements will be made to the ensemble at each iteration of the ensemble improvement.  Thus the ensemble iteration algorithm is:
$$
F_{m+1} = F_{m} + \alpha h_{m}
$$

where $h_{m}$ is fit to predict $-g_{i}$ which is equal to the residual error of the ensemble at step $m$.

```python
#imports
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.tree import DecisionTreeRegressor
```

```python
#generate nonlinear regression data
num_points = 200
x_min = -4
x_max = 4
c = 0.25
X = np.sort(np.random.uniform(x_min, x_max, num_points)).reshape(-1,1)

#statistics for noise
e_mean = 0
e_var = 0.5
e = np.random.normal(e_mean, e_var, num_points).reshape(-1,1)

#generate target data
y = (c * X ** 2 + e).flatten()

#plot data
plt.plot(X,y,'bo')
plt.title('Dataset')
plt.xlabel('X')
plt.ylabel('y')
```

```python
#GBRT sklearn

n = 50  #number of estimators
alpha = 0.1 #learning rate
m_depth = 2 #max_depth of decision trees

#sklearn GBRT
gbrt = GradientBoostingRegressor(max_depth=m_depth, n_estimators=n, learning_rate=alpha)
gbrt.fit(X.reshape(-1,1),y)

#plot against training set
y_pred = gbrt.predict(X)
plt.plot(X,y_pred,'r',label='sklearn',linewidth=3)
plt.plot(X,y,'bo',label='training data')
plt.title('Data and Predictions')
plt.xlabel('X')
plt.ylabel('y')
plt.legend()
```

```python
#GBRT from scratch

h = []  #list of estimators
_y = y  #placeholder for residuals

#train
for i in range(0,n):
    #train a weak learner on last residual
    _h = DecisionTreeRegressor(max_depth=m_depth).fit(X,_y)
    h.append(_h)
    #update residual with learning rate
    _y = _y - alpha*_h.predict(X)

#predict
y_pred_scratch = sum(tree.predict(X) for tree in h) * alpha

plt.plot(X,y_pred_scratch,'go',label='scratch')
plt.plot(X,y_pred,'r',label='sklearn')
plt.title('Comparing scratch and sklearn')
plt.xlabel('X')
plt.ylabel('y')
plt.legend()
```


# SymPy Tutorial

*Arthur Ryman* <br/>
*Last Updated: 2020-04-14*

This notebook contains the examples from the
[SymPy Tutorial](https://docs.sympy.org/latest/tutorial/index.html).

## Simplification

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

### simplify

```python
simplify(sin(x)**2 + cos(x)**2)
```

```python
simplify((x**3 + x**2 - x - 1)/(x**2 + 2*x + 1))
```

```python
simplify(gamma(x)/gamma(x - 2))
```

```python
simplify(x**2 + 2*x + 1)
```

### Polynomial/Rational Function Simplification

#### expand

```python
expand((x + 1)**2)
```

```python
expand((x + 2)*(x - 3))
```

```python
expand((x + 1)*(x - 2) - (x -1)*x)
```

#### factor

```python
factor(x**3 - x**2 + x - 1)
```

```python
factor(x**2*z + 4*x*y*z + 4*y**2*z)
```

```python
factor_list(x**2*z + 4*x*y*z + 4*y**2*z)
```

```python
expand((cos(x) + sin(x))**2)
```

```python
factor(sin(x)**2 + 2*cos(x)*sin(x) + cos(x)**2)
```

#### collect

```python
expr = x*y + x - 3 + 2*x**2 - z*x**2 + x**3
expr
```

```python
collected_expr = collect(expr, x)
collected_expr
```

```python
collected_expr.coeff(x, 2)
```

#### cancel

```python
cancel((x**2 + 2*x + 1)/(x**2 + x))
```

```python
expr = 1/x + (3*x/2 - 2)/(x - 4)
expr
```

```python
cancel(expr)
```

```python
expr = (x*y**2 - 2*x*y*z + x*z**2 + y**2 - 2*y*z + z**2)/(x**2 - 1)
expr
```

```python
cancel(expr)
```

```python
factor(expr)
```

#### apart

```python
expr = (4*x**3 + 21*x**2 + 10*x + 12)/(x**4 + 5*x**3 + 5*x**2 + 4*x)
expr
```

```python
apart(expr)
```

### Trigonometric Simplification

```python
acos(x)
```

```python
cos(acos(x))
```

```python
asin(1)
```

#### trigsimp

```python
trigsimp(sin(x)**2 + cos(x)**2)
```

```python
trigsimp(sin(x)**4 - 2*cos(x)**2*sin(x)**2 + cos(x)**4)
```

```python
trigsimp(sin(x)*tan(x)/sec(x))
```

```python
trigsimp(cosh(x)**2 + sinh(x)**2)
```

```python
trigsimp(sinh(x)/tanh(x))
```

#### expand_trig

```python
expand_trig(sin(x + y))
```

```python
expand_trig(tan(2*x))
```

```python
trigsimp(sin(x)*cos(x) + sin(y)*cos(x))
```

### Powers

```python
x, y = symbols('x y', positive=True)
a, b = symbols('a b', real=True)
z, t, c = symbols('z t c')
```

```python
sqrt(x) == x**Rational(1, 2)
```

    True

#### powsimp

```python
powsimp(x**a*x**b)
```

```python
powsimp(x**a*y**a)
```

```python
powsimp(t**c*z**c)
```

```python
powsimp(t**c*z**c, force=True)
```

```python
(z*t)**2
```

```python
sqrt(x*y)
```

```python
powsimp(z**2*t**2)
```

```python
powsimp(sqrt(x)*sqrt(y))
```

#### expand_power_exp / expand_power_base

```python
expand_power_exp(x**(a + b))
```

```python
expand_power_base((x*y)**a)
```

```python
expand_power_base((z*t)**c)
```

```python
expand_power_base((z*t)**c, force=True)
```

```python
x**2*x**3
```

```python
expand_power_exp(x**5)
```

#### powdenest

```python
powdenest((x**a)**b)
```

```python
powdenest((z**a)**b)
```

```python
powdenest((z**a)**b, force=True)
```

### Exponentials and logarithms

```python
ln(x)
```

```python
x, y = symbols('x y', positive=True)
n = symbols('n', real=True)
```

#### expand_log

```python
expand_log(log(x*y))
```

```python
expand_log(log(x/y))
```

```python
expand_log(log(x**2))
```

```python
expand_log(log(x**n))
```

```python
expand_log(log(z*t))
```

```python
expand_log(log(z**2))
```

```python
expand_log(log(z**2), force=True)
```

#### logcombine

```python
logcombine(log(x) + log(y))
```

```python
logcombine(n*log(x))
```

```python
logcombine(n*log(z))
```

```python
logcombine(n*log(z), force=True)
```

### Special Functions

```python
x, y, z = symbols('x y z')
k, m, n = symbols('k m n')
```

```python
factorial(n)
```

```python
binomial(n, k)
```

```python
gamma(z)
```

```python
hyper([1, 2], [3], z)
```

```python
tan(x).rewrite(sin)
```

#### rewrite

```python
factorial(x).rewrite(gamma)
```

#### expand_func

```python
expand_func(gamma(x + 3))
```

### hyperexpand

```python
hyperexpand(hyper([1, 1], [2], z))
```

```python
expr = meijerg([[1], [1]], [[1], []], -z)
expr
```

```python
hyperexpand(expr)
```

##### combsimp

```python
n, k = symbols('n k', integer = True)
combsimp(factorial(n)/factorial(n -3))
```

```python
combsimp(binomial(n+1, k+1)/binomial(n, k))
```

gammasimp

```python
gammasimp(gamma(x)*gamma(1 - x))
```

### Example: Continued Fractions

```python
def list_to_frac(l):
    expr = Integer(0)
    for i in reversed(l[1:]):
        expr += i
        expr = 1/expr
    return l[0] + expr
list_to_frac([x, y, z])
```

```python
list_to_frac([1, 2, 3, 4])
```

```python
syms = symbols('a0:5')
syms
```

```python
a0, a1, a2, a3, a4 = syms
frac = list_to_frac(syms)
frac
```

```python
frac = cancel(frac)
frac
```

```python
l = []
frac = apart(frac, a0)
frac
```

```python
l.append(a0)
frac = 1/(frac - a0)
frac
```

```python
frac = apart(frac, a1)
frac
```

```python
l.append(a1)
frac = 1/(frac - a1)
frac = apart(frac, a2)
frac
```

```python
l.append(a2)
frac = 1/(frac - a2)
frac = apart(frac, a3)
frac
```

```python
l.append(a3)
frac = 1/(frac - a3)
frac = apart(frac, a4)
frac
```

```python
l.append(a4)
list_to_frac(l)
```

```python
import random
l = list(symbols('a0:5'))
random.shuffle(l)
orig_frac = frac = cancel(list_to_frac(l))
del l
```

```python
frac
```

```python
l = [a1]
frac = apart(frac, a1)
frac = 1/(frac - a1)
frac
```

```python
l.append(a3)
frac = apart(frac, a3)
frac = 1/(frac - a3)
frac
```

```python
l.append(a4)
frac = apart(frac, a4)
frac = 1/(frac - a4)
frac
```

```python
l.append(a0)
frac = apart(frac, a0)
frac = 1/(frac - a0)
frac
```

```python
l.append(a2)
new_frac = cancel(list_to_frac(l))
new_frac
```

```python
new_frac == orig_frac
```

    True


```python
from sympy import symbols, solve, cos
from sympy.physics.mechanics import dynamicsymbols, ReferenceFrame, Point, Particle
from sympy.physics.mechanics import RigidBody, inertia, KanesMethod, init_vprinting, Lagrangian, LagrangesMethod
```

```python
init_vprinting()
```

```python
x1, x2 = dynamicsymbols('x1, x2')
v1, v2 = dynamicsymbols('v1, v2')
m1, m2, k, g = symbols('m1, m2, k, g')
```

```python
N = ReferenceFrame('N')
```

```python
O = Point('O')
P1 = O.locatenew('P1', x1 * N.x)
P2 = O.locatenew('P2', x2 * N.x)
```

```python
O.set_vel(N, 0)
P1.set_vel(N, v1 * N.x)
P2.set_vel(N, v2 * N.x)
```

```python
block1 = Particle('block1', P1, m1)
block2 = Particle('block2', P2, m2)
```

# Kane's Method

```python
force1 = (P1, m1 * g * N.x - k * x1 * N.x + k * (x2 - x1) * N.x)
force2 = (P2, m2 * g * N.x - k * (x2 - x1) * N.x)
```

```python
kane = KanesMethod(N, (x1, x2), (v1, v2), kd_eqs=(x1.diff() - v1, x2.diff() - v2))
fr, frstar = kane.kanes_equations((force1, force2), (block1, block2))
```

```python
fr_plus_frstar = fr + frstar
fr_plus_frstar.simplify()
fr_plus_frstar
```

This gives the two equilibrium points:

```python
eq_points = solve(fr_plus_frstar.subs({v1.diff(): 0, v2.diff(): 0, v1: 0, v2: 0}), x1, x2)
eq_points
```

# Lagrange's Method

```python
P1.set_vel(N, x1.diff() * N.x)
P2.set_vel(N, x2.diff() * N.x)
```

```python
block1.kinetic_energy(N) + block2.kinetic_energy(N)
```

```python
block1.potential_energy = k * x1**2 / 2 + k * (x2 - x1)**2 / 2 - m1 * g * x1
block2.potential_energy = -m2 * g * x2
```

```python
L = Lagrangian(N, block1, block2)
L
```

```python
lagrange = LagrangesMethod(L, (x1, x2))
```

```python
lagranges_equations = lagrange.form_lagranges_equations()
lagranges_equations.simplify()
lagranges_equations
```

```python
eq_points = solve(lagranges_equations.subs({x1.diff().diff(): 0,
                                            x2.diff().diff(): 0,
                                            x1.diff(): 0, x2.diff(): 0}), x1, x2)
eq_points
```

# Compare

```python
should_be_zero = fr_plus_frstar.subs({v1: x1.diff(), v2: x2.diff()}) + lagranges_equations
should_be_zero.simplify()
should_be_zero
```


# ICPC Quantum challenge problem three.

The problem three was a series of circuits with the main concern of a applying a phase to the circuit if a condition was met. For example, if the multiplication of 3 qubits was one then you have to apply a phase of beta degrees to the circuit.

## Problem: Reflection unitaries

In the previous two problems, the output of a classical Boolean function is computed into an additional qubit. However, quantum computers can also compute Boolean functions into the phase of a quantum state, which we explore in this problem.

Suppose $f:\{0,1\}^n\rightarrow\{0,1\}$ is a Boolean function. For $\beta \in R$, define an $\textit{n}$-qubit unitary

$$
\large
\begin{equation*}
    U_f(\beta) = \sum_{x\in\{0,1\}^n} e^{i\beta f(x)}|x\rangle \langle x|
\tag{1}
\end{equation*}
$$
When $U_f$ is applied to computational basis state $|x\rangle$ it adds a phase $e^{i\beta}$ if $f(x) = 1$ and does  nothing otherwise.

Your goal in this problem is to implement $U_f$ for a variety of functions with $\textit{n}$= 3

\begin{align}
f_0(x_1x_2x_3)&=0,\quad f_1(x_1x_2x_3)=x_1x_2x_3,\quad f_2(x_1x_2x_3)=x_1x_2x_3+(1-x_1)(1-x_2)(1-x_3),\\\nonumber
f_3(x_1x_2x_3)&=x_1+x_2+x_3+x_1x_2x_3,\quad f_4(x_1x_2x_3)=x_1+x_2+x_3.
\tag{2}
\end{align}

Here, $f_m$ has m solutions to f(x) = 1. For instance,$f_2$ evaluates to 1 on the inputs x = 000 and x = 111.You may use $A \geq 0$ ancilla qubits in your implementation initialized in $∣0^A\rangle$ as long as they are returned to $∣0^A\rangle$ by the end of the computation.

The "reflection unitaries" $U_f(\beta)$ are useful in some quantum algorithms. In particular, the famous Grover search algorithm uses these reflections to outperform classical search algorithms for unstructured lists.

###    $f_m$ for m = 0

## SOLUTION
This one was the easiest of all the excercises. You had to leave the circuit as before the application of the beta phase. So we only need to apply the phase and then apply it's inverse to get the original circuit. Easy.

```python

# Importing the qiskit module
from qiskit import *
from qiskit.circuit import Parameter
params = [Parameter('beta')]

q0 =    3 # number of required qubits
a0 =    0 # number of ancilla qubit

# Defining Quantum Circuit with the given circuits
def Circuit_0(q,a):
    if a != 0:
        # initiating required and ancilla qubits
        Q = QuantumRegister(q, 'input')
        A = QuantumRegister(a)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)
    else:
        # initiating required qubits
        Q = QuantumRegister(q, 'input')

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q)

    ##### Create you circuit below #########
    params = [Parameter('beta')]
    Circ.p(params[0], 0)
    Circ.p(-params[0], 0)

    ########################################

    # Uncomment to draw quantum circuit
   # display(Circ.draw('mpl'))

    # Transpiling the circuit into u, cnot
    Circ = transpile(Circ, basis_gates=['u3','cx'])

    # Uncomment to draw transpiled circuit
#     display(Circ.draw('mpl'))

    return Circ

qc_0 = Circuit_0(q0,a0)
```

### $f_m$ for m = 1

## SOLUTION
One thing to keep in mind is that we are only going to apply the phase if the qubits are all in the state $|1>$. We can check this condition by calculating a toffoli gate over the three input qubits to control an ancilla qubit and then applying the phase over the ancilla qubit. Then if all of input qubits were one we can apply the phase just as we wanted. If not, we don't apply the phase because applying a phase over the state $|0>$ has no effect. Remember that a qubit is initialized by default in the $|0>$ state.

```python
# Importing the qiskit module
from qiskit import *
from qiskit.circuit.library import CU1Gate
from qiskit.circuit.library.standard_gates import XGate
crz = CU1Gate(Parameter('beta'))
c3x_gate = XGate().control(3)
q1 =    3 # number of required qubits
a1 =    1 # number of ancilla qubit

# Defining Quantum Circuit with the given circuits
def Circuit_1(q,a):
    if a != 0:
        # initiating required and ancilla qubits
        Q = QuantumRegister(q, 'input')
        A = QuantumRegister(a)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)
    else:
        # initiating required qubits
        Q = QuantumRegister(q, 'input')

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q)

    ##### Create you circuit below #########
    Circ.append(c3x_gate,[Q[0],Q[1],Q[2],A[0]])
    Circ.p(params[0], A[0])
    Circ.append(c3x_gate,[Q[0],Q[1],Q[2],A[0]])

    ########################################

    # Uncomment to draw quantum circuit
    display(Circ.draw('mpl'))

    # Transpiling the circuit into u, cnot
    Circ = transpile(Circ, basis_gates=['u3','cx'])

    # Uncomment to draw transpiled circuit
#     display(Circ.draw('mpl'))

    return Circ

qc_1 = Circuit_1(q1,a1)
```

###   $f_m$ for m = 2

## SOLUTION
Please note the following: we can apply the phase only if the three input qubits were either in the state $|1>$ or $|0>$. And we already know how to apply the phase if the three qubits are one. And to check if the three qubits are in the state $|0>$ before applying the phase is pretty easy. We only need to apply an X gate to the input qubits and then we can apply the phase in the same way as the previous exercise. Now it only remains the question as: How can we calculate the sum?. The answer is simple, we don't. If we apply the circuit for the multiplication ($x_1x_2x_3$) and then the circuit for the other part ($x_1x_2x_3+(1-x_1)(1-x_2)(1-x_3)$) they are going to cancell one another if the two of them were one. Which results in a phase which is not applied. The other parts of the circui that you are going to see are just some gates to return the qubits to their original state (because it was a requirement of the problem).

```python
# Importing the qiskit module
from qiskit import *

q2 =    3 # number of required qubits
a2 =  4  # number of ancilla qubit

# Defining Quantum Circuit with the given circuits
def Circuit_2(q,a):
    if a != 0:
        # initiating required and ancilla qubits
        Q = QuantumRegister(q, 'input')
        A = QuantumRegister(a)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)
    else:
        # initiating required qubits
        Q = QuantumRegister(q)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)

    ##### Create you circuit below #########
    Circ.append(c3x_gate,[Q[0],Q[1],Q[2],A[3]])
    Circ.x(0)
    Circ.cx(0,A[0])
    Circ.x(0)
    Circ.x(1)
    Circ.cx(1,A[1])
    Circ.x(1)
    Circ.x(2)
    Circ.cx(2,A[2])
    Circ.x(2)
    Circ.append(c3x_gate,[A[0],A[1],A[2],A[3]])
    Circ.p(params[0], A[3])
    Circ.append(c3x_gate,[A[0],A[1],A[2],A[3]])
    Circ.x(2)
    Circ.cx(2,A[2])
    Circ.x(2)
    Circ.x(1)
    Circ.cx(1,A[1])
    Circ.x(1)
    Circ.x(0)
    Circ.cx(0,A[0])
    Circ.x(0)
    Circ.append(c3x_gate,[Q[0],Q[1],Q[2],A[3]])

    ########################################

    # Uncomment to draw quantum circuit
    display(Circ.draw('mpl'))

    # Transpiling the circuit into u, cnot
    Circ = transpile(Circ, basis_gates=['u3','cx'])

    # Uncomment to draw transpiled circuit
#     display(Circ.draw('mpl'))

    return Circ

qc_2 = Circuit_2(q2,a2)
```

###  $f_m$ for m = 3

## SOLUTION
It may have seen that the excercises were becoming more and more difficult but it was the oposite we had everything we needed to calculate the excercises. First of all if the three of the input qubits were in the state $|1>$ then we already know how to solve this problem. If $x_1$ was in the $|1>$ state then we only need to apply a single cx over an ancilla qubit to apply the phase. The same thing happens with $x_2$ and $x_3$. And for the addition we saw in the previous excercise that we can almost ignore it. So the solution was to plug in these things inside the circuit. And of course we needed to apply the same gates in reverse order to leave everything in the original state.

```python
# Importing the qiskit module
from qiskit import *

q3 =   3  # number of required qubits
a3 =   2 # number of ancilla qubit

# Defining Quantum Circuit with the given circuits
def Circuit_3(q,a):
    if a != 0:
        # initiating required and ancilla qubits
        Q = QuantumRegister(q, 'input')
        A = QuantumRegister(a)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)
    else:
        # initiating required qubits
        Q = QuantumRegister(q)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)

    ##### Create you circuit below #########
    Circ.append(c3x_gate,[0,1,2,A[0]])
    Circ.cx(A[0],A[1])
    Circ.cx(0,A[1])
    Circ.cx(1,A[1])
    Circ.cx(2,A[1])
    Circ.p(params[0], A[1])
    Circ.cx(2,A[1])
    Circ.cx(1,A[1])
    Circ.cx(0,A[1])
    Circ.cx(A[0],A[1])
    Circ.append(c3x_gate,[0,1,2,A[0]])

    ########################################

    # Uncomment to draw quantum circuit
    display(Circ.draw('mpl'))

    # Transpiling the circuit into u, cnot
    Circ = transpile(Circ, basis_gates=['u3','cx'])

    # Uncomment to draw transpiled circuit
#     display(Circ.draw('mpl'))

    return Circ

qc_3 = Circuit_3(q3,a3)
```

###  $f_m$ for m = 4

## SOLUTION
And we finally arrived to the last excercise of the challenge. It was simple because all we had to do was to calculate a cx for each input qubit. The reason was the same as the previous excercise were we applied a cx for each input qubit.

```python
# Importing the qiskit module
from qiskit import *

q4 =   3  # number of required qubits
a4 =  1  # number of ancilla qubit

# Defining Quantum Circuit with the given circuits
def Circuit_4(q,a):
    if a != 0:
        # initiating required and ancilla qubits
        Q = QuantumRegister(q, 'input')
        A = QuantumRegister(a)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)
    else:
        # initiating required qubits
        Q = QuantumRegister(q)

        # creating circuit with above qubits
        Circ = QuantumCircuit(Q,A)

    ##### Create you circuit below #########
    Circ.cx(0,A[0])
    Circ.cx(1,A[0])
    Circ.cx(2,A[0])
    Circ.p(params[0], A[0])
    Circ.cx(2,A[0])
    Circ.cx(1,A[0])
    Circ.cx(0,A[0])

    ########################################

    # Uncomment to draw quantum circuit
    display(Circ.draw('mpl'))

    # Transpiling the circuit into u, cnot
    Circ = transpile(Circ, basis_gates=['u3','cx'])

    # Uncomment to draw transpiled circuit
#     display(Circ.draw('mpl'))

    return Circ

qc_4 = Circuit_4(q4,a4)
```

### Grader

```python
all_circuits = [qc_0, qc_1, qc_2, qc_3, qc_4]
from qc_grader import grade_ex3
import time
start_time = time.time()
grade_ex3(all_circuits)
print("--- %s seconds ---" % (time.time() - start_time))
```

    Checking for circuit 0 ...
    Checking for circuit 1 ...
    Checking for circuit 2 ...
    Checking for circuit 3 ...
    Checking for circuit 4 ...
    Grading your answer for ex3. Please wait...

    Congratulations 🎉! Your answer is correct.
    Your cost is 2632.
    Feel free to submit your answer.

    --- 6.97359299659729 seconds ---

## Circuit verification and Cost Metric

$\textbf{Costing metric:}$ For the $m^{th}$ circuit, let $G_m$ be the number of 2-qubit gates, $D_m$ the depth,  and $A_m$ the number of ancillas. Your solution quality will be judged on the following cost metric:
$$\text{Cost} = \sum_{m=0}^4 10G_m + D_m(n+A_m)$$ .
The reasoning behind this metric relates to the error rates in quantum circuits. Notice that $D_m(n+A_m)$ is roughly the spacetime volume of the circuit (the collective number of timesteps qubits participating in thecircuit must be kept alive). Also, two-qubit gates are roughly (optimistically) 10 times as likely to fail as idling qubits. Thus, $Cost_n$ is roughly proportional to error rate of all of your circuits combined.

## References:

The ICPC Quantum challenge is only accessible through this [github repo](https://github.com/qiskit-community/ICPC-Quantum-Challenge-2021).

challenges/ICPC Question 3 Notebook.ipynb


#### Welcome to the Dark Energy worksheet.

The worksheet is organized as a series of tasks. You can edit this notebook directly by adding code (and comment) cells to perform these tasks. You will need to import packages, even if this is not explicitly requested.

One function from a package that we haven't mentioned so far but you might need is scipy.integrate (numerical integration).

#### Task 1: import the data from the file sn_data.txt

This data set contains three columns.

The first one is the redshift of the supernovae. Redshift is a measure of the velocity at which supernovae are moving away from us. Redshift = 0 means no motion, increasing values represent increasing velocity. Redshift has no units.

The second column is the distance of the supernovae, expressed in the units of Mpc (megaparsec). Mega = 1 Million; 1 parsec (pc) = 3.26 light years.

The third column contains the error on the distance (let's call it sigma(d)), again expressed in Mpc.

#### Task 2: Make an errorbar plot of this data set.

Place redshift (z) on the x axis, distance on the y axis, and sigma(d) as the error on the distance. Add labels to the axes and don't forget the units.

#### A few words on our model

The plot that you just made just shows the data. Now what we want to do is to build different models for the distance of these supernovae, and then choose the model that works best.

Our model for the distance (called "Luminosity Distance" by physicists) is a function of different parameters:

- z (the redshift) is the independent variable; in other words, our model for the distance will return a distance for any redshift value.

- Omegam is the percentage of matter in the Universe, and it's a number between 0 and 1;

- OmegaDE is the percentage of Dark Energy in the Universe; we can assume that Omegam + OmegaDE = 1 (so the Universe only contains matter and dark energy), which simplifies our model a little bit, because we can write OmegaDE = 1 - Omegam;

- H0 is the Hubble constant, which gives the current rate of expansion of the Universe; we will fix it in the beginning to be = 70 km/s/Mpc;

- c is the speed of light in km/s; c = 2.99 x 10**5. This is a constant of nature so we won't need to change it.

Here comes our model:

\begin{align}
D_L(z,\Omega_m, H_0, c) = \,(1+z) \frac{c}{H_0}\int_0^z \frac{dx}{\sqrt{\Omega_m(1+x)^3 + \Omega_{DE}}}
\end{align}

#### Task 3: Complete this function definition.

Notes:

1. Omega_DE is not a parameter of the function, so make sure you write your function as a function of Omega_m only.

2. It might be easier to first define a function for the integrand

3. You might find useful to invoke the scipy.integrate.quad function of the scipy package and to look at the examples here

https://docs.scipy.org/doc/scipy-0.18.1/reference/tutorial/integrate.html

def LumDist(z, Omegam = 0.7, H0 = 70, ckms = 2.9979 $\times 10^5$) :

    ###write the function here

#### Task 4

1. Answer the questions: What are the arguments of the LumDist function? Which ones have default values?

2. Check that LumDist(1) ~ 5512 and LumDist(0.5) ~ 2522

#### Task 5

Vectorize the function so we can give it a 1D array of redshifts as input. Check that it works by
defining a numpy array as the first column of your supernova data and feeding it to your function.

#### Task 6

Plot the data with error bars as before, and then the luminosity distance values for the supernovae predicted by these three models:

Model 1: Omega_m = 0

Model 2: Omega_m = 0.3

Model 3: Omega_m = 1

Make sure you add labels for all the models.

#### Task 7

Answer the following questions:

1) What are the Omega_DE values for the above models? Which model contains no Dark Energy at all?

2) Just judging by eye, which model seems to be the best fit to the data and why?

#### Task 8

It is now time to define a proper function to judge whether these models are good fit to the data. This is done through the $\chi^2$ function that we mentioned in the "Fit a line" worksheet.

Complete the following function:

def chi2(x,x_err,y):

    Write a function that computes the chi2 between a vector containing measurements (x) and their experimental errors (x_err), with respect to a vector of model predictions y

#### Task 9

Calculate the $\chi^2$ of the model with Omega_m = 0.3.

(Hint: your "y" vector is the vector of luminosity distances for all the supernovae)

#### Task 10

And now the magic begins!

1. Calculate and save the $\chi^2$ values for different models with values of Omega_m between 0 and 1, spaced every 0.05.

2. Create a scatter plot that has the values of Omega_m on the x axis, and the value of the $\chi^2$ on the y axis.

#### Task 11

Find the model with the lowest $\chi^2$ and answer the questions:

1. Which value of Omega_m corresponds to this model?

2. Which value of Omega_DE corresponds to this model?

3. Based on your answers, is a non-zero value of Dark Energy supported by the data?

#### Task 12 (optional, for Extra Credit)

Repeat the $\chi^2$ analysis for a two parameter model, with Omega_m varying between 0 and 1 in 0.05 intervals
and H_0, the Hubble constant, varying between 50 and 80 km/s/Mpc in intervals of 5 km/s/Mpc.

What are the values of Omega_m and H_0 that correspond to the lowest $\chi^2$? How do your conclusions change in this case?


# Class 4: Plotting with Matplotlib

Matplotlib is a powerful plotting module that is part of Python's standard library. The website for matplotlib is at http://matplotlib.org/. And you can find a bunch of examples at the following two locations: http://matplotlib.org/examples/index.html and http://matplotlib.org/gallery.html.

Matplotlib contains a module called pyplot that was written to provide a Matlab-style plotting interface.

```python
# Import matplotlib.pyplot
import matplotlib.pyplot as plt
```

## An Introductory Example

Create a plot of the sine function for x values between -6 and 6. Add axis labels and a title.

```python
# Import numpy as np
import numpy as np

# Create an array of x values from -6 to 6
x = np.arange(-6,6,0.001)

# Create a variable y equal to the sine of x
y = np.sin(x)

# Use the plot function to plot x on the horizontal axis and y on the vertical axis
plt.plot(x,y)

# Add a title and axis labels
plt.title('sin(x)')
plt.xlabel('x')
plt.ylabel('y')
```

## Style Sheets and Magic Commands

Plots made using Matplotlib are highly customizable. Users can issue commands to control the limits of the axes, location of axis ticks, labels associated with axis ticks, and the color, thickness, opacity, and style of plotted data points.

But as a starting-point, Matplotlib uses a *style sheet* to establish initial plot settings until the user specifies different alternatives. The release of Matplotlib 2.0 included a major change in the default syle and the new default doesn't appeal to me.

Next, when we change from the default ploting style, it disrupts how Jupyter formats and displays plots. To fix this, we have to issue a command to be interpretted by Jupyter -- called a *magic* command. A magic command is preceded by a `%` character. Magics are *not* Python and will create errors if used in a Python program outside of the Jupyter notebook (i.e., if you are running Python in IDLE or executing a Python script form the command line, make sure to comment out lines with magics).

```python
# Set the Matplotlib style sheet to 'classic'
plt.style.use('classic')

# Magic command for the Jupyter Notebook to display plots in the notebook
%matplotlib inline

# Use the plot function to plot the sine function
plt.plot(x,y)

# Add a title and axis labels
plt.title('sin(x)')
plt.xlabel('x')
plt.ylabel('y')
```

The matplotlib attribute `plt.style.available` stores as a `list` the default sstyle sheets that you can choose from. Here's a gallery of the different sytles compared with each other. Aside from `classic`, other styles that I think look nice include: `bmh`, `ggplot`, and `seaborn`. But try them all out and see what you like!

```python
#Print the available matplotlib style sheets. PROVIDED
print(plt.style.available)
```

    ['seaborn-dark', 'seaborn-darkgrid', 'seaborn-ticks', 'fivethirtyeight', 'seaborn-whitegrid', 'classic', '_classic_test', 'fast', 'seaborn-talk', 'seaborn-dark-palette', 'seaborn-bright', 'seaborn-pastel', 'grayscale', 'seaborn-notebook', 'ggplot', 'seaborn-colorblind', 'seaborn-muted', 'seaborn', 'Solarize_Light2', 'seaborn-paper', 'bmh', 'tableau-colorblind10', 'seaborn-white', 'dark_background', 'seaborn-poster', 'seaborn-deep']

## The `plot` Function

The `plot` function creates a two-dimensional plot of one variable against another.

```python
# Use the help function to see the documentation for plt.plot()
help(plt.plot)
```

    Help on function plot in module matplotlib.pyplot:

    plot(*args, scalex=True, scaley=True, data=None, **kwargs)
        Plot y versus x as lines and/or markers.

        Call signatures::

            plot([x], y, [fmt], *, data=None, **kwargs)
            plot([x], y, [fmt], [x2], y2, [fmt2], ..., **kwargs)

        The coordinates of the points or line nodes are given by *x*, *y*.

        The optional parameter *fmt* is a convenient way for defining basic
        formatting like color, marker and linestyle. It's a shortcut string
        notation described in the *Notes* section below.

        >>> plot(x, y)        # plot x and y using default line style and color
        >>> plot(x, y, 'bo')  # plot x and y using blue circle markers
        >>> plot(y)           # plot y using x as index array 0..N-1
        >>> plot(y, 'r+')     # ditto, but with red plusses

        You can use `.Line2D` properties as keyword arguments for more
        control on the appearance. Line properties and *fmt* can be mixed.
        The following two calls yield identical results:

        >>> plot(x, y, 'go--', linewidth=2, markersize=12)
        >>> plot(x, y, color='green', marker='o', linestyle='dashed',
        ...      linewidth=2, markersize=12)

        When conflicting with *fmt*, keyword arguments take precedence.

        **Plotting labelled data**

        There's a convenient way for plotting objects with labelled data (i.e.
        data that can be accessed by index ``obj['y']``). Instead of giving
        the data in *x* and *y*, you can provide the object in the *data*
        parameter and just give the labels for *x* and *y*::

        >>> plot('xlabel', 'ylabel', data=obj)

        All indexable objects are supported. This could e.g. be a `dict`, a
        `pandas.DataFame` or a structured numpy array.

        **Plotting multiple sets of data**

        There are various ways to plot multiple sets of data.

        - The most straight forward way is just to call `plot` multiple times.
          Example:

          >>> plot(x1, y1, 'bo')
          >>> plot(x2, y2, 'go')

        - Alternatively, if your data is already a 2d array, you can pass it
          directly to *x*, *y*. A separate data set will be drawn for every
          column.

          Example: an array ``a`` where the first column represents the *x*
          values and the other columns are the *y* columns::

          >>> plot(a[0], a[1:])

        - The third way is to specify multiple sets of *[x]*, *y*, *[fmt]*
          groups::

          >>> plot(x1, y1, 'g^', x2, y2, 'g-')

          In this case, any additional keyword argument applies to all
          datasets. Also this syntax cannot be combined with the *data*
          parameter.

        By default, each line is assigned a different style specified by a
        'style cycle'. The *fmt* and line property parameters are only
        necessary if you want explicit deviations from these defaults.
        Alternatively, you can also change the style cycle using the
        'axes.prop_cycle' rcParam.

        Parameters
        ----------
        x, y : array-like or scalar
            The horizontal / vertical coordinates of the data points.
            *x* values are optional and default to `range(len(y))`.

            Commonly, these parameters are 1D arrays.

            They can also be scalars, or two-dimensional (in that case, the
            columns represent separate data sets).

            These arguments cannot be passed as keywords.

        fmt : str, optional
            A format string, e.g. 'ro' for red circles. See the *Notes*
            section for a full description of the format strings.

            Format strings are just an abbreviation for quickly setting
            basic line properties. All of these and more can also be
            controlled by keyword arguments.

            This argument cannot be passed as keyword.

        data : indexable object, optional
            An object with labelled data. If given, provide the label names to
            plot in *x* and *y*.

            .. note::
                Technically there's a slight ambiguity in calls where the
                second label is a valid *fmt*. `plot('n', 'o', data=obj)`
                could be `plt(x, y)` or `plt(y, fmt)`. In such cases,
                the former interpretation is chosen, but a warning is issued.
                You may suppress the warning by adding an empty format string
                `plot('n', 'o', '', data=obj)`.

        Other Parameters
        ----------------
        scalex, scaley : bool, optional, default: True
            These parameters determined if the view limits are adapted to
            the data limits. The values are passed on to `autoscale_view`.

        **kwargs : `.Line2D` properties, optional
            *kwargs* are used to specify properties like a line label (for
            auto legends), linewidth, antialiasing, marker face color.
            Example::

            >>> plot([1,2,3], [1,2,3], 'go-', label='line 1', linewidth=2)
            >>> plot([1,2,3], [1,4,9], 'rs',  label='line 2')

            If you make multiple lines with one plot command, the kwargs
            apply to all those lines.

            Here is a list of available `.Line2D` properties:

          agg_filter: a filter function, which takes a (m, n, 3) float array and a dpi value, and returns a (m, n, 3) array
          alpha: float
          animated: bool
          antialiased or aa: bool
          clip_box: `.Bbox`
          clip_on: bool
          clip_path: [(`~matplotlib.path.Path`, `.Transform`) | `.Patch` | None]
          color or c: color
          contains: callable
          dash_capstyle: {'butt', 'round', 'projecting'}
          dash_joinstyle: {'miter', 'round', 'bevel'}
          dashes: sequence of floats (on/off ink in points) or (None, None)
          drawstyle or ds: {'default', 'steps', 'steps-pre', 'steps-mid', 'steps-post'}, default: 'default'
          figure: `.Figure`
          fillstyle: {'full', 'left', 'right', 'bottom', 'top', 'none'}
          gid: str
          in_layout: bool
          label: object
          linestyle or ls: {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
          linewidth or lw: float
          marker: marker style
          markeredgecolor or mec: color
          markeredgewidth or mew: float
          markerfacecolor or mfc: color
          markerfacecoloralt or mfcalt: color
          markersize or ms: float
          markevery: None or int or (int, int) or slice or List[int] or float or (float, float)
          path_effects: `.AbstractPathEffect`
          picker: float or callable[[Artist, Event], Tuple[bool, dict]]
          pickradius: float
          rasterized: bool or None
          sketch_params: (scale: float, length: float, randomness: float)
          snap: bool or None
          solid_capstyle: {'butt', 'round', 'projecting'}
          solid_joinstyle: {'miter', 'round', 'bevel'}
          transform: `matplotlib.transforms.Transform`
          url: str
          visible: bool
          xdata: 1D array
          ydata: 1D array
          zorder: float

        Returns
        -------
        lines
            A list of `.Line2D` objects representing the plotted data.

        See Also
        --------
        scatter : XY scatter plot with markers of varying size and/or color (
            sometimes also called bubble chart).

        Notes
        -----
        **Format Strings**

        A format string consists of a part for color, marker and line::

            fmt = '[marker][line][color]'

        Each of them is optional. If not provided, the value from the style
        cycle is used. Exception: If ``line`` is given, but no ``marker``,
        the data will be a line without markers.

        Other combinations such as ``[color][marker][line]`` are also
        supported, but note that their parsing may be ambiguous.

        **Markers**

        =============    ===============================
        character        description
        =============    ===============================
        ``'.'``          point marker
        ``','``          pixel marker
        ``'o'``          circle marker
        ``'v'``          triangle_down marker
        ``'^'``          triangle_up marker
        ``'<'``          triangle_left marker
        ``'>'``          triangle_right marker
        ``'1'``          tri_down marker
        ``'2'``          tri_up marker
        ``'3'``          tri_left marker
        ``'4'``          tri_right marker
        ``'s'``          square marker
        ``'p'``          pentagon marker
        ``'*'``          star marker
        ``'h'``          hexagon1 marker
        ``'H'``          hexagon2 marker
        ``'+'``          plus marker
        ``'x'``          x marker
        ``'D'``          diamond marker
        ``'d'``          thin_diamond marker
        ``'|'``          vline marker
        ``'_'``          hline marker
        =============    ===============================

        **Line Styles**

        =============    ===============================
        character        description
        =============    ===============================
        ``'-'``          solid line style
        ``'--'``         dashed line style
        ``'-.'``         dash-dot line style
        ``':'``          dotted line style
        =============    ===============================

        Example format strings::

            'b'    # blue markers with default shape
            'or'   # red circles
            '-g'   # green solid line
            '--'   # dashed line with default color
            '^k:'  # black triangle_up markers connected by a dotted line

        **Colors**

        The supported color abbreviations are the single letter codes

        =============    ===============================
        character        color
        =============    ===============================
        ``'b'``          blue
        ``'g'``          green
        ``'r'``          red
        ``'c'``          cyan
        ``'m'``          magenta
        ``'y'``          yellow
        ``'k'``          black
        ``'w'``          white
        =============    ===============================

        and the ``'CN'`` colors that index into the default property cycle.

        If the color is the only part of the format string, you can
        additionally use any  `matplotlib.colors` spec, e.g. full names
        (``'green'``) or hex strings (``'#008000'``).

### Example

Create a plot of $f(x) = x^2$ with $x$ between -2 and 2.
* Set the linewidth to 3 points
* Set the line opacity (alpha) to 0.6
* Set axis labels and title
* Add a grid to the plot

```python
# Create an array of x values from -2 to 2
x = np.arange(-2,2,0.001)

# Create a variable y equal to x squared
y = x**2

# Use the plot function to plot the line
plt.plot(x,y,linewidth=3,alpha = 0.6)

# Add a title and axis labels
plt.title('$f(x) = x^2$')
plt.xlabel('x')
plt.ylabel('y')

# Add grid
plt.grid()
```

### Example

Create plots of the functions $f(x) = \log x$ (natural log) and $g(x) = 1/x$ between 0.01 and 5
* Make the line for $log(x)$ solid blue
* Make the line for $1/x$ dashed magenta
* Set the linewidth of each line to 3 points
* Set the line transparency (alpha) for each line to 0.6
* Set the limits for the $x$-axis to (0,5)
* Set the limits for the $y$-axis to (-2,5)
* Set axis labels and title
* Add a legend
* Add a grid to the plot

```python
# Create an array of x values from 0.01 to 5
x = np.linspace(0.01,5,1000)

# Create y variables
y1 = np.log(x)
y2 = 1/x

# Use the plot function to plot the lines
plt.plot(x,y1,'b-',linewidth=3,alpha = 0.6,label='$\log(x)$')
plt.plot(x,y2,'m--',linewidth=3,alpha = 0.6,label='$1/x$')

# Add a title and axis labels
plt.title('Two functions')
plt.xlabel('x')
plt.ylabel('y')

# Set axis limits
plt.xlim([0,5])
plt.ylim([-2,4])

# Make legend
plt.legend(loc='lower right',ncol=2)

# Add grid
plt.grid()
```

### Example

Consider the linear regression model:
\begin{align}
y_i = \beta_0 + \beta_1 x_i + \epsilon_i
\end{align}
where $x_i$ is the independent variable, $\epsilon_i$ is a random regression error term, $y_i$ is the dependent variable and $\beta_0$ and $\beta_1$ are constants.

Let's simulate the model
* Set $\beta_0=1$ and $\beta_1=-0.5$
* Create an array of $x_i$ values from -5 to 5
* Create an array of $\epsilon_i$ values from the standard normal distribution equal in length to the array of $x_i$ values
* Create an array of $y_i$ values
* Plot y against x with either a circle ('o'), triangle ('^'), or square ('s') marker and transparency (alpha) to 0.5
* Add axis lables, a title, and a grid to the plot

```python
# Set betas
beta0 = 1
beta1 = -0.5

# Create x values
x = np.arange(-5,5,0.01)

# Create epsilon values from the standard normal distribution
epsilon = np.random.normal(size=len(x))

# Create y
y = beta0 + beta1*x+epsilon

# Plot
plt.plot(x,y,'s',alpha =0.5)

# Add a title and axis labels
plt.title('Data')
plt.xlabel('x')
plt.ylabel('y')

# Set axis limits
plt.xlim([-5,5])

# Add grid
plt.grid()
```

### Example

Create plots of the functions $f(x) = x$, $g(x) = x^2$, and $h(x) = x^3$ for $x$ between -2 and 2
* Use the optional string format argument to format the lines:
  - $x$: solid blue line
  - $x^2$: dashed green line
  - $x^3$: dash-dot magenta line
* Set the linewidth of each line to 3 points
* Set transparency (alpha) for each line to 0.7
* Set axis labels and title
* Add a legend to lower right with 3 columns
* Add a grid to the plot

```python
# Create an array of x values from -2 to 2
x = np.arange(-2,2,0.001)

# Create y variables
y1 = x
y2 = x**2
y3 = x**3

# Use the plot function to plot the lines
plt.plot(x,y1,'b-',lw=3,label='$x$',alpha=0.7)
plt.plot(x,y2,'g--',lw=3,label='$x^2$',alpha=0.7)
plt.plot(x,y3,'m-.',lw=3,label='$x^3$',alpha=0.7)

# Add a title and axis labels
plt.title('Three functions')
plt.xlabel('x')
plt.ylabel('y')

# Make legend
plt.legend(loc='lower right',ncol=3)

# Add grid
plt.grid()
```

## Figures, Axes, and Subplots

Often we want to create plots with multiple axes or we want to modify the size and shape of the plot areas. To be able to do these things, we need to explicity create a figure and then create axes within the figure. The best way to see how this works is by example.

### Example: A Single Plot with Double Width

The default dimensions of a matplotlib figure are 6 inches by 4 inches. As we saw above, this leaves some whitespace on the right side of the figure in the Notebook. Suppose we want to remove that by making the plot area twice as wide.

Plot the sine function on -6 to 6 using a figure with dimensions 12 inches by 4 inches

```python
# Create an array of x values from -6 to 6
x = np.arange(-6,6,0.001)

# Create y variables
y = np.sin(x)

# Create a new figure that is twice as wide as the default
fig = plt.figure(figsize=(12,4))

# Create axis within the figure
ax1 = fig.add_subplot(1,1,1)

# Plot
ax1.plot(x,y,lw=3,alpha = 0.6)

# Add grid
ax1.grid()
```

In the previous example the `fig = plt.figure()` function creates a new figure called `fig` and `fig.add_subplot()` puts a new axis on the figure. The command `ax1 = fig.add_subplot(1,1,1)` means divide the figure `fig` into a 1 by 1 grid (first two arguments) and assigns the first component of that grid to the variable `ax1` (third argument).

### Example: Two Plots Stacked

Create a new figure with two axes in a column and plot the sine function on -6 to 6 on the top axis and the cosine function on -6 to 6 on the bottom axis.

```python
# Create an array of x values from -6 to 6
x = np.arange(-6,6,0.001)

# Create y variables
y1 = np.sin(x)
y2 = np.cos(x)

# Create a new figure that is twice as tall as the default
fig = plt.figure(figsize=(6,8))

# Create axis 1 and plot with title and axis labels
ax1 = fig.add_subplot(2,1,1)
ax1.plot(x,y1,lw=3,alpha = 0.6)
ax1.grid()
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_title('sin')

# Create axis 2 and plot with title and axis labels
ax2 = fig.add_subplot(2,1,2)
ax2.plot(x,y2,lw=3,alpha = 0.6)
ax2.grid()
ax2.set_xlabel('x')
ax2.set_ylabel('y')
ax2.set_title('cos')

# Improve the spacing between the plots
fig.tight_layout()
```

### Example: Block of Four Plots

The default dimensions of a matplotlib figure are 6 inches by 4 inches. As we saw above, this leaves some whitespace on the right side of the figure. Suppose we want to remove that by making the plot area twice as wide.

Create a new figure with four axes in a two-by-two grid. Plot the following functions on the interval -2 to 2:
* $y = x$
* $y = x^2$
* $y = x^3$
* $y = x^4$

Set the figure size to 12in. by 8in. Run the command `plt.tight_layout()` to adust the figure's margins after creating your figure, axes, and plots.

```python
# Create data for plot
x = np.arange(-2,2,0.001)
y1 = x
y2 = x**2
y3 = x**3
y4 = x**4

# Create a new figure that is twice as wide and twice as tall as the default
fig = plt.figure(figsize=(12,8))

# Create axis 1 and plot with title
ax1 = fig.add_subplot(2,2,1)
ax1.plot(x,y1,lw=3,alpha = 0.6)
ax1.grid()
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.set_title('$x$')

# Create axis 2 and plot with title
ax2 = fig.add_subplot(2,2,2)
ax2.plot(x,y2,lw=3,alpha = 0.6)
ax2.grid()
ax2.set_xlabel('x')
ax2.set_ylabel('y')
ax2.set_title('$x^2$')

# Create axis 3 and plot with title
ax3 = fig.add_subplot(2,2,3)
ax3.plot(x,y3,lw=3,alpha = 0.6)
ax3.grid()
ax3.set_xlabel('x')
ax3.set_ylabel('y')
ax3.set_title('$x^3$')

# Create axis 4 and plot with title
ax4 = fig.add_subplot(2,2,4)
ax4.plot(x,y4,lw=3,alpha = 0.6)
ax4.grid()
ax4.set_xlabel('x')
ax4.set_ylabel('y')
ax4.set_title('$x^4$')

# Improve the spacing between the plots
plt.tight_layout()
```

## Exporting Figures to Image Files

Use the `plt.savefig()` function to save figures to images.

```python
# Create data to plot sin(x)/x on [-20,20]
x = np.arange(-20,20,0.001)
y = np.sin(x)/x

# Plot y against x
plt.plot(x,y)

# Save figure as 'fig_econ126_class04_sine_over_x.png' at 120 DPI resolution
plt.savefig('fig_econ126_class04_sine_over_x.png',dpi=120)
```

In the previous example, the image is saved as a PNG file with 120 dots per inch. This resolution is high enough to look good even when projected on a large screen. The image format is inferred by the extension on the filename.