# NANO106 - Symmetry Computations on $mmm (D_{2h})$ Point Group
*by Shyue Ping Ong*

This notebook demonstrates the computation of orbits in the mmm point group. It is part of course material for UCSD's NANO106 - Crystallography of Materials. Unlike the $m\overline{3}m (O_h)$ version, this duplicates relevant code from the symmetry package to explicitly demonstrate the priniciples of generating point group symmetry operations.

# Preliminaries

Let's start by importing the numpy, sympy and other packages we need.

```python
import numpy as np
import itertools
from sympy import symbols
```

We will now define a useful function for checking existence of np.arrays in a list of arrays. It is not the most efficient implementation, but would suffice for our purposes.

```python
def in_array_list(array_list, a):
    for i in array_list:
        if np.all(np.equal(a, i)):
            return True
    return False
```

# Generating the Symmetry Operations

Next, we specify the generators for mmm point group, which are the three mirror planes. Note that the existence of the three two-fold rotation axes is implied by the existence of the three mirror planes.

```python
generators = []
for i in xrange(3):
    g = np.eye(3).astype(np.int)
    g[i, i] = -1
    generators.append(g)
```

We will now generate all the group symmetry operation matrices from the generators.

```python
symm_ops = []
symm_ops.extend(generators)
new_ops = generators

while len(new_ops) > 0:
    gen_ops = []
    for g1, g2 in itertools.product(new_ops, symm_ops):
        #Note that we cast the op to int to improve presentation of the results.
        #This is fine in crystallographic reference frame.
        op = np.dot(g1, g2)
        if not in_array_list(symm_ops, op):
            gen_ops.append(op)
            symm_ops.append(op)
        op = np.dot(g2, g1)
        if not in_array_list(symm_ops, op):
            gen_ops.append(op)
            symm_ops.append(op)
    new_ops = gen_ops
print "The order of the group is %d. The group matrices are:" % len(symm_ops)
for op in symm_ops:
    print op
```

    The order of the group is 8. The group matrices are:
    [[-1  0  0]
     [ 0  1  0]
     [ 0  0  1]]
    [[ 1  0  0]
     [ 0 -1  0]
     [ 0  0  1]]
    [[ 1  0  0]
     [ 0  1  0]
     [ 0  0 -1]]
    [[1 0 0]
     [0 1 0]
     [0 0 1]]
    [[-1  0  0]
     [ 0 -1  0]
     [ 0  0  1]]
    [[-1  0  0]
     [ 0  1  0]
     [ 0  0 -1]]
    [[ 1  0  0]
     [ 0 -1  0]
     [ 0  0 -1]]
    [[-1  0  0]
     [ 0 -1  0]
     [ 0  0 -1]]

# Computing Orbits

Using sympy, we specify the symbolic symbols x, y, z to represent position coordinates. We also define a function to generate the orbit given a set of symmetry operations and a point p.

```python
x, y, z = symbols("x y z")

def get_orbit(symm_ops, p):
    """Given a set of symmops and a point p, this function returns the orbit"""
    orbit = []
    for o in symm_ops:
        pp = np.dot(o, p)
        if not in_array_list(orbit, pp):
            orbit.append(pp)
    return orbit
```

## Orbit for General Position

```python
p = np.array([x, y, z])
print "For the general position %s, the orbit is " % str(p)
for o in get_orbit(symm_ops, p):
    print o,
```

    For the general position [x y z], the orbit is
    [-x y z] [x -y z] [x y -z] [x y z] [-x -y z] [-x y -z] [x -y -z] [-x -y -z]

## Orbit for Special Position on two-fold rotation axes

```python
p = np.array([0, 0, z])
orb = get_orbit(symm_ops, p)
print "For the special position %s on the two-fold axis, the orbit comprise %d points:" % (str(p), len(orb))
for o in orb:
    print o,
```

    For the special position [0 0 z] on the two-fold axis, the orbit comprise 2 points:
    [0 0 z] [0 0 -z]

The orbit is similar for the other two-fold axes on the a and b axes are similar.

## Orbit for Special Position on mirror planes

Positions on the mirror on the a-b plane have coordinates (x, y, 0).

```python
p = np.array([x, y, 0])
orb = get_orbit(symm_ops, p)
print "For the special position %s on the two-fold axis, the orbit comprise %d points:" % (str(p), len(orb))
for o in orb:
    print o,
```

    For the special position [x y 0] on the two-fold axis, the orbit comprise 4 points:
    [-x y 0] [x -y 0] [x y 0] [-x -y 0]

The orbit is similar for the other two mirror planes on the a-c and b-c planes are similar.

lectures/lecture_4_point_group_symmetry/Symmetry Computations on mmm (D_2h) Point Group.ipynb


# [Learn Quantum Computing with Python and Q#](https://www.manning.com/books/learn-quantum-computing-with-python-and-q-sharp?a_aid=learn-qc-granade&a_bid=ee23f338)<br>Chapter 9 Exercise Solutions
----
> Copyright (c) Sarah Kaiser and Chris Granade.
> Code sample from the book "Learn Quantum Computing with Python and Q#" by
> Sarah Kaiser and Chris Granade, published by Manning Publications Co.
> Book ISBN 9781617296130.
> Code licensed under the MIT License.

### Preamble

```python
import numpy as np
import qutip as qt
import matplotlib.pyplot as plt
import qsharp
%matplotlib inline
```

### Exercise 9.1

**In Chapter 4, you used Python type annotations to represent the concept of a _strategy_ in the CHSH game.
User-defined types in Q# can be used in a similar fashion.
Give it a go by defining a new UDT for CHSH strategies and then use your new UDT to wrap the constant strategy from Chapter 4.**

*HINT*: Your and Eve's parts of the strategy can each be represented as operations that take a `Result` and output a `Result`.
That is, as operations of type `Result => Result`.

```python
strategy = qsharp.compile("""
    newtype Strategy = (
        PlayAlice: (Result => Result),
        PlayBob: (Result => Result)
    );
""")
strategy
```

----
### Exercise 9.2

**You can find the model for Lancelot's results if you use Born's rule!
We have put the definition from Chapter 2 below, see if you can plot the resulting value as a function of Lancelot's scale using Python.
Does your plot look like a trigonometric function?**

\begin{align}
    \Pr(\text{measurement} | \text{state}) = |\left\langle \text{measurement} \mid \text{state} \right\rangle|^2
\end{align}

*HINT*: For Lancelot's measurements, the $\left\langle \text{measurement} \right|$ part of Born's rule is given by $\left\langle 1 \right|$.
Immediately before measuring, his qubit will be in the state $H R_1(\theta * \textrm{scale}) H \left|0\right\rangle$.
You can simulate the `R1` operation in QuTiP by using the matrix form in the Q# reference at https://docs.microsoft.com/qsharp/api/qsharp/microsoft.quantum.intrinsic.r1.

For the purposes of illustration, let's choose $\theta = 0.456$ radians.

```python
theta = 0.456
```

Next, as the hint gives us, we'll need to define a matrix that we can use to simulate the `R1` operation:

```python
def r1_matrix(angle: float) -> qt.Qobj:
    return qt.Qobj([
        [1, 0],
        [0, np.exp(1j * angle)]
    ])
```

```python
r1_matrix(theta)
```

We can use this to find Lancelot's state after applying each hidden rotation:

```python
def lancelot_final_state(theta: float, scale: float) -> qt.Qobj:
    initial_state = qt.basis(2, 0)
    # Simulate the H Q# operation.
    state = qt.qip.operations.hadamard_transform() * initial_state
    # Simulate the R1 operation.
    state = r1_matrix(theta * scale) * state
    # Simulate undoing the H operation with another call to H.
    state = qt.qip.operations.hadamard_transform() * state

    return state
```

```python
lancelot_final_state(theta, 1.2)
```

We now have everything we need to predict the probability of a "1" outcome:

```python
def lancelot_pr1(theta: float, scale: float) -> float:
    ket1 = qt.basis(2, 1)

    # Apply Born's rule.
    return np.abs((ket1.dag() * lancelot_final_state(theta, scale))[0, 0]) ** 2
```

```python
lancelot_pr1(theta, 1.2)
```

Plotting for a variety of different scales, we see the expected sinusoidal shape:

```python
scales = np.linspace(0, 20, 201)
pr1s = [lancelot_pr1(theta, scale) for scale in scales]
plt.plot(scales, pr1s)
```

----
### Exercise 9.3

**Try writing Q# programs that use `AssertQubit` and `DumpMachine` to verify that:**

- $\left|+\right\rangle$ and $\left|-\right\rangle$ are both eigenstates of the `X` operation.
- $\left|0\right\rangle$ and $\left|1\right\rangle$ are both eigenstates of the `Rz` operation, regardless of what angle you choose to rotate by.

For even more practice, try figuring out what the eigenstates of the `Y` and `CNOT` operations and writing a Q# program to verify your guesses!

*HINT*: You can find the vector form of the eigenstates of a unitary operation using QuTiP.
For instance, the eigenstates of the `Y` operation are given by `qt.sigmay().eigenstates()`.
From there, you can use what you learned about rotations in Chapters 4 and 5 to figure out which Q# operations prepare those states.

Don't forget you can always test if a particular state is an eigenstate of an operation by just writing a quick test in Q#!

Let's start by verifying that $\left|+\right\rangle$ and $\left|-\right\rangle$ are both eigenstates of the `X` operation.

```python
verify_x_eigenstates = qsharp.compile("""
    open Microsoft.Quantum.Diagnostics;

    operation VerifyXEigenstates() : Unit {
        use q = Qubit();
        // Prepare |+⟩.
        H(q);

        // Check that the X operation does nothing.
        X(q);
        Message("Checking that |+⟩ is an eigenstate of the X operation.");
        DumpMachine();

        // Reset so that we're ready for the next check.
        Reset(q);

        // Next, do the same with |−⟩.
        X(q);
        H(q);

        X(q);
        Message("");
        Message("Checking that |−⟩ is an eigenstate of the X operation.");
        DumpMachine();

        Reset(q);
    }
""")
```

```python
verify_x_eigenstates.simulate()
```

Notice that in both cases, we got back the same state (up to a global phase), confirming the first part of the exercise.
Doing the same for `Rz`, we add an input for the rotation angle:

```python
verify_rz_eigenstates = qsharp.compile("""
    open Microsoft.Quantum.Diagnostics;

    operation VerifyRzEigenstates(angle : Double) : Unit {
        use q = Qubit();
        // Prepare |0⟩ by doing nothing.

        // Check that the Rz operation does nothing.
        Rz(angle, q);
        Message("Checking that |0⟩ is an eigenstate of the Rz operation.");
        DumpMachine();

        // Reset so that we're ready for the next check.
        Reset(q);

        // Next, do the same with |1⟩.
        X(q);

        Rz(angle, q);
        Message("");
        Message("Checking that |1⟩ is an eigenstate of the Rz operation.");
        DumpMachine();

        Reset(q);
    }
""")
```

```python
verify_rz_eigenstates.simulate(angle=0.123)
```

```python
verify_rz_eigenstates.simulate(angle=4.567)
```

Using the hint, we can find what eigenstates we should try for the `Y` and `CNOT` operations:

```python
qt.sigmay().eigenstates()
```

```python
qt.qip.operations.cnot().eigenstates()
```

That is, $(|0\rangle + i |1\rangle) / \sqrt{2}$ and $(|0\rangle - i |1\rangle) / \sqrt{2}$ are eigenstates of the `Y` operation, while $|00\rangle$, $|01\rangle$, $|1+\rangle$ and $|1-\rangle$ are eigenstates of the `CNOT` operation.

----
### Exercise 9.4

**Verify that $\left|0\right\rangle\left\langle 0\right| \otimes \mathbb{1} + \left|1\right\rangle\left\langle{1}\right| \otimes X$ is the same as:**

\begin{align}
    U_{\mathrm{CNOT}} = \left(\begin{matrix}
        \mathbb{1} & 0 \\
        0 & X
    \end{matrix}\right).
\end{align}

*HINT*: You can verify this by hand, by using NumPy's `np.kron` function, or QuTiP's `qt.tensor` function.
If you need a refresher, check out how you simulated teleportation in Chapter 5, or check out the derivation of the Deutsch–Jozsa algorithm in Chapter 7.

```python
ket0 = qt.basis(2, 0)
ket1 = qt.basis(2, 1)
```

```python
projector_0 = ket0 * ket0.dag()
projector_0
```

```python
projector_1 = ket1 * ket1.dag()
projector_1
```

```python
qt.tensor(projector_0, qt.qeye(2)) + qt.tensor(projector_1, qt.sigmax())
```

----
### Exercise 9.5

**Either by hand or using QuTiP, verify that state dumped by running the Q# snippet below is the same as $\left|-1\right\rangle = \left|-\right\rangle \otimes \left|1\right\rangle$.**

```Q#
use (control, target) = (Qubit(), Qubit());
H(control);
X(target);

CZ(control, target);
DumpRegister((), [control, target]);

Reset(control);
Reset(target);
```

*NOTE*: If you seem to get the right answer other than that the order of the qubits are swapped, note that `DumpMachine` uses a _little-endian_ representation to order states.
In little-endian, |2⟩ is short-hand for |01⟩, not |10⟩.
If this seems confusing, blame the x86 processor architecture…

Let's first run the above snippet to see what output is generated.

```python
run_exercise_95 = qsharp.compile("""
    open Microsoft.Quantum.Diagnostics;

    operation RunExercise95() : Unit {
        use (control, target) = (Qubit(), Qubit());
        H(control);
        X(target);

        CZ(control, target);
        DumpRegister((), [control, target]);

        Reset(control);
        Reset(target);
    }
""")
```

```python
run_exercise_95.simulate()
```

Next, let's compute what $\left|-1\right\rangle = \left|-\right\rangle \otimes \left|1\right\rangle$ in vector notation by using QuTiP.

```python
ket_minus = qt.Qobj([
    [1],
    [-1]
]) / np.sqrt(2)
ket1 = qt.basis(2, 1)
```

```python
qt.tensor(ket_minus, ket1)
```

As the note suggests, these two outputs appear different at first, but the resolution is that Q# uses little-endian notation, such that "|2⟩" means the |01⟩ amplitude, which QuTiP prints as the second row.
We can make this more clear by manually telling IQ# to print out as bitstrings instead of little-endian notation.

**WARNING:** Calling the `%config` magic from Python is not officially supported, and may break in future versions of Q#.

```python
qsharp.client._execute('%config dump.basisStateLabelingConvention = "Bitstring"')
```

```python
run_exercise_95.simulate()
```

----
### Epilogue

_The following cell logs what version of the components this was last tested with._

```python
qsharp.component_versions()
```

    {'iqsharp': LooseVersion ('0.17.2105143879'),
     'Jupyter Core': LooseVersion ('1.5.0.0'),
     '.NET Runtime': LooseVersion ('.NETCoreApp,Version=v3.1'),
     'qsharp': LooseVersion ('0.17.2105.143879')}


```python
%pylab inline

from mpl_toolkits.mplot3d import Axes3D
import sympy as sym
```

    Populating the interactive namespace from numpy and matplotlib

Solving

$$
-\Delta u = f
$$

Example 1 (peak 2d):

$$
u(x,y) = x(x-1)y(y-1)e^{-100\big( (x-0.5)^2+(y-0.117)^2\big)}
$$

Example 2 (for L-shaped domain, no-rhs, only boundary conditions, in polar coordinates):

$$
u(r,\theta) = r^{2/3} \sin\left(\frac{2\theta+5\pi}{3}\right)
$$

etc.

Play around with your own examples!

```python
from sympy import atan2, sin, cos

def u(x,y):
    return x*(x-1)*y*(y-1)*exp(-100*(x-0.5)**2-100*(y-0.117)**2)

def u1(x,y):
    r = (x*x+y*y)**(1/2)
    theta = atan2(x,y)
    return r**(2/3)*sin(theta*2/3)

def u2(x,y,z):
    return x*(x-1)*y*(y-1)*z*(z-1)*exp(-100*(x-0.5)**2-100*(y-0.117)**2-100*(z-0.331)**2)

def u3(x,y,z):
    return (x*x + y*y + z*z)**(1/4);

def u4(x,y):
    return sin(2*sym.pi*y)*sin(2*sym.pi*x)

def u5(x,y):
    return x*(x-1)*y*(y-1)*(10*exp(-100*(x-0.5)**2-100*(y-0.1)**2)+.1*exp(-100*(x-0.1)**2-100*(y-0.5)**2))

all_2d_us = [u, u1, u4, u5]
all_3d_us = [u2, u3]
```

```python
for a in all_2d_us:
    print(sym.latex(a))
```

    <function u at 0x7fb701623f28>
    <function u1 at 0x7fb701623ea0>
    <function u4 at 0x7fb701623b70>
    <function u5 at 0x7fb701623ae8>

```python
def prm(symu, f):
    print(
        "Exact solution expression     = "+ str(symu).replace('**', '^')+"\n" +
        "Forcing term expression       = "+ str(f).replace('**', '^') + "\n" +
        "Boundary condition expression = "+ str(symu).replace('**', '^')+ "\n" +
        "Problem constants             = "+ "pi:"+str(pi)
    )

def generate_prm(symu):
    f = -symu.diff(x,2)-symu.diff(y,2)
    f = sym.simplify(f)
    prm(symu, f)
```

```python
from sympy import exp
x,y,z = sym.var('x,y,z')

generate_prm(u(x,y))
```

    Exact solution expression     = x*y*(x - 1)*(y - 1)*exp(-100*(x - 0.5)^2 - 100*(y - 0.117)^2)
    Forcing term expression       = (x*(x - 1)*(-y*(y - 1)*(200*y - 23.4)^2 + 200*y*(y - 1) + 2*y*(200*y - 23.4) + 2*(y - 1)*(200*y - 23.4) - 2) + y*(y - 1)*(-x*(x - 1)*(200*x - 100.0)^2 + 200*x*(x - 1) + 2*x*(200*x - 100.0) + 2*(x - 1)*(200*x - 100.0) - 2))*exp(-100*(x - 0.5)^2 - 100*(y - 0.117)^2)
    Boundary condition expression = x*y*(x - 1)*(y - 1)*exp(-100*(x - 0.5)^2 - 100*(y - 0.117)^2)
    Problem constants             = pi:3.141592653589793

```python
generate_prm(u1(x,y))
```

    Exact solution expression     = (x^2 + y^2)^0.333333333333333*sin(2*atan2(x, y)/3)
    Forcing term expression       = 0
    Boundary condition expression = (x^2 + y^2)^0.333333333333333*sin(2*atan2(x, y)/3)
    Problem constants             = pi:3.141592653589793

```python
generate_prm(u2(x,y,z))
```

    Exact solution expression     = x*y*z*(x - 1)*(y - 1)*(z - 1)*exp(-100*(x - 0.5)^2 - 100*(y - 0.117)^2 - 100*(z - 0.331)^2)
    Forcing term expression       = z*(z - 1)*(x*(x - 1)*(-y*(y - 1)*(200*y - 23.4)^2 + 200*y*(y - 1) + 2*y*(200*y - 23.4) + 2*(y - 1)*(200*y - 23.4) - 2) + y*(y - 1)*(-x*(x - 1)*(200*x - 100.0)^2 + 200*x*(x - 1) + 2*x*(200*x - 100.0) + 2*(x - 1)*(200*x - 100.0) - 2))*exp(-100*(x - 0.5)^2 - 100*(y - 0.117)^2 - 100*(z - 0.331)^2)
    Boundary condition expression = x*y*z*(x - 1)*(y - 1)*(z - 1)*exp(-100*(x - 0.5)^2 - 100*(y - 0.117)^2 - 100*(z - 0.331)^2)
    Problem constants             = pi:3.141592653589793

```python
generate_prm(u3(x,y,z))
```

    Exact solution expression     = (x^2 + y^2 + z^2)^0.25
    Forcing term expression       = (0.75*(x^2 + y^2)*(x^2 + y^2 + z^2)^0.75 - 1.0*(x^2 + y^2 + z^2)^1.75)*(x^2 + y^2 + z^2)^(-2.5)
    Boundary condition expression = (x^2 + y^2 + z^2)^0.25
    Problem constants             = pi:3.141592653589793

```python
generate_prm(u4(x,y))
```

    Exact solution expression     = sin(2*pi*x)*sin(2*pi*y)
    Forcing term expression       = 8*pi^2*sin(2*pi*x)*sin(2*pi*y)
    Boundary condition expression = sin(2*pi*x)*sin(2*pi*y)
    Problem constants             = pi:3.141592653589793

```python
generate_prm(u5(x,y))
```

    Exact solution expression     = x*y*(x - 1)*(y - 1)*(10*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) + 0.1*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2))
    Forcing term expression       = -x*(x - 1)*(y*(y - 1)*(0.1*(200*y - 100.0)^2*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2) + 10*(200*y - 20.0)^2*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) - 2000*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) - 20.0*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2)) - 2*y*((20.0*y - 10.0)*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2) + (2000*y - 200.0)*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2)) - 2*(y - 1)*((20.0*y - 10.0)*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2) + (2000*y - 200.0)*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2)) + 20*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) + 0.2*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2)) - y*(y - 1)*(x*(x - 1)*(10*(200*x - 100.0)^2*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) + 0.1*(200*x - 20.0)^2*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2) - 2000*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) - 20.0*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2)) - 2*x*((20.0*x - 2.0)*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2) + (2000*x - 1000.0)*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2)) - 2*(x - 1)*((20.0*x - 2.0)*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2) + (2000*x - 1000.0)*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2)) + 20*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) + 0.2*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2))
    Boundary condition expression = x*y*(x - 1)*(y - 1)*(10*exp(-100*(x - 0.5)^2 - 100*(y - 0.1)^2) + 0.1*exp(-100*(x - 0.1)^2 - 100*(y - 0.5)^2))
    Problem constants             = pi:3.141592653589793

```python

```


Dimensional Analysis

## Find the relevant dimensionless parameters for a physics problem from a given set of physical variables in accordance with the Buckingham $\Pi$ theorem

```python
import numpy as np
import pandas as pd
import sympy as sp
from sympy import Matrix, init_printing

import itertools
```

#### The file ['physical-variables.tsv'](physical-variables.tsv) contains a table of physical variables and their asscociated dimensions (base quantity exponents)

```python
# Load physical variable dimensions table into pandas data frame
variables_df = pd.read_csv('physical-variables.tsv', delimiter='\t', index_col='Symbol')
variables_df.head()
```

      Quantity
      L
      M
      T
      I
      Theta
      N
      J
      SI unit
      in SI base units
      Category
      Comment

      Symbol

      l
      Length
      1
      0
      0
      0
      0
      0
      0
      m
      m
      Base quantity
      lowercase L

      m
      Mass
      0
      1
      0
      0
      0
      0
      0
      kg
      kg
      Base quantity
      -

      t
      Time
      0
      0
      1
      0
      0
      0
      0
      s
      s
      Base quantity
      -

      I
      Electric current
      0
      0
      0
      1
      0
      0
      0
      A
      A
      Base quantity
      -

      T
      Temperature
      0
      0
      0
      0
      1
      0
      0
      K
      K
      Base quantity
      -

#### Select the physical variables that are relevant to the physics problem! Choose variables (symbols) from the [physical-variables table](physical-variables.tsv)!

```python
# Enter the symbols separated by a space in a single string!

variables = 'F_D u eta rho D'              # Flow around sphere (drag force)

#--------------------------------MORE EXAMPLES---------------------------------------
#variables = 'F_D eta u rho D'             # Flow around sphere (drag force)
#variables = 'v g rho t D eta m'           # velocity of a sphere sinking in a fluid
#variables = 'h u c_p rho eta k_th D'      # forced convection heat transfer
#variables = 'a_s p rho'                   # speed of sound
#variables = 'M_lambda k_B h_P c lambda T' # spectral exitance of black body
#variables = 'x F rho E_Y g L'             # large elastic deformations
#variables = 'F J E_Y L'                   # buckling load of a beam
#variables = 'f L E_Y rho  D'              # natural frequency of an elastic rod
#variables = 'M omega eta rho R'           # torque of rotating disc in fluid
#variables = 'P omega u L rho'             # power of wind turbine
#variables = 'E p_0 R t rho_0'             # strong explosion
#-----------------------------------------------------------------------------------

# Split the string at the spaces to get a list of variables
variables_list  = variables.split()

# Display the selected variables and associated SI units
variables_table = variables_df.loc[variables_list,['Quantity',
                                                   'SI unit',
                                                   'in SI base units',
                                                   'Category',
                                                   'Comment']]
print('Selected physical variables: \n')
display(variables_table)

# Display the base quantity exponents for the selected variables
dimensions_table = variables_df.loc[variables_list,'L':'J'].transpose()
print('Dimensions: \n')
display(dimensions_table)
```

    Selected physical variables:

      Quantity
      SI unit
      in SI base units
      Category
      Comment

      Symbol

      F_D
      Drag / Resistance force
      N
      kg m / s^2
      Mechanics
      -

      u
      Flow velocity
      m / s
      m / s
      Kinematics
      -

      eta
      Dynamic viscosity
      Pa s
      kg / (m s)
      Mechanics
      -

      rho
      Density
      kg / m^3
      kg / m^3
      Mechanics
      -

      D
      Diameter
      m
      m
      Geometry
      -

    Dimensions:

      Symbol
      F_D
      u
      eta
      rho
      D

      L
      1
      1
      -1
      -3
      1

      M
      1
      0
      1
      1
      0

      T
      -2
      -1
      -1
      0
      0

      I
      0
      0
      0
      0
      0

      Theta
      0
      0
      0
      0
      0

      N
      0
      0
      0
      0
      0

      J
      0
      0
      0
      0
      0

#### Base quantities (L, M, T, ... , J) with all zeros can be omitted

```python
# Drop all rows with all zeros
dimensions_table = dimensions_table[(dimensions_table.T != 0).any()]
display(dimensions_table)
table_index = list(dimensions_table.index)
```

      Symbol
      F_D
      u
      eta
      rho
      D

      L
      1
      1
      -1
      -3
      1

      M
      1
      0
      1
      1
      0

      T
      -2
      -1
      -1
      0
      0

#### The values of the dimensions table yield the dimensional matrix $A$

$
A=
\begin{pmatrix}
        a_{11}    & a_{12}  & ...     & a_{1n}\\
        a_{21}    & a_{22}  & ...     & a_{2n} \\
        \vdots    & \vdots  &         & \vdots   \\
        a_{m1}    & a_{m2}  & ...     & a_{mn}\\
\end{pmatrix}
$

```python
# Get dimensional matrix (values of dimensions_table)
A_mat = Matrix(dimensions_table.values)
print('A = ')
display(A_mat)

# Get the shape of the dimensional matrix
n_rows, n_cols = A_mat.shape
print(f'number of rows:    m = {n_rows}')
print(f'number of columns: n = {n_cols}')
```

    A =

$\displaystyle \left[\begin{matrix}1 & 1 & -1 & -3 & 1\\1 & 0 & 1 & 1 & 0\\-2 & -1 & -1 & 0 & 0\end{matrix}\right]$

    number of rows:    m = 3
    number of columns: n = 5

#### The number of the expected dimensionless parameters can be obtained from the difference between the number of physical variables and the rank  of the dimensional matrix

#### $d=n-r$

$d$ : number of dimensionless parameters
$n$ : number of physical variables
$r$ : rank of dimensional matrix

```python
n_variables = n_cols
print(f'number of physical variables:       n = {n_variables}')

rank_A = A_mat.rank()
print(f'rank of dimensional matrix:         r = {rank_A}')

d = n_variables-rank_A
print(f'number of dimensionless parameters: d = {d}')

# Proceed only if there are solutions
if d > 0:
    proceed = True
else:
    proceed = False
    print('with the selected variables no dimensionless parameters can be found!')
```

    number of physical variables:       n = 5
    rank of dimensional matrix:         r = 3
    number of dimensionless parameters: d = 2

#### For this procedure to work, the $r$ rightmost column vectors of the dimensional matrix must be linearly independent. If this isn't the case, the variables of the input-string must be manually rearranged!

```python
if proceed:
    # Get submatrix C_temp containing the r rightmost vectors
    C_temp = Matrix(dimensions_table[variables_list[-rank_A:]].values)

    # Check if the rank of submatrix C_temp differs from rank_A
    if C_temp.rank() != rank_A:
        print(f'column-vectors of last {rank_A} variables are not linearly independent!')
        print(f'please rearrange the variables in the input-string!')
        print(f'possible combinations for the last {rank_A} variables are:')

        # Get all possible combinations of a number of rank_A variables
        combinations = itertools.combinations(variables_list, rank_A)

        # Check for each of the combinations if the rank of submatrix C_temp is equal to rank_A
        for combination in combinations:
            C_temp = Matrix(dimensions_table[list(combination)].values)
            if C_temp.rank() == rank_A:
                print('... ' + ' '.join(list(combination)))

        proceed = False
```

#### If the number of columns of the dimensional matrix is lower or equal to the number of its rows ($n<= m$), or if the rank of the dimensional matrix is lower than the number of rows ($r<m$), then one or more rows must be deleted before we can proceed. However, the rank of the dimensional matrix must not change due to deletion of the rows!

```python
if proceed:
    if (n_cols <= n_rows) or (rank_A < n_rows):
        while (n_cols <= n_rows) or (rank_A < n_rows):
            print(' ')
            print('please delete a row!')

            # Calculate for each row the rank of the remaining matrix, if this row is deleted
            ranks = []
            for i in range(n_rows):
                A_reduced = A_mat.copy()
                A_reduced.row_del(i)
                r_reduced = A_reduced.rank()
                ranks.append(r_reduced)

            # Create data frame with the ranks of the reduced matrices
            ranks_df = pd.DataFrame({'rank of remaining matrix, if this row is deleted':ranks,
                                     'this row may be deleted':(np.array(ranks)==rank_A),
                                     'row index':list(range(n_rows))},
                                     index=table_index)

            # Display dimensions table and ranks of reduced matrices in single data frame
            combined_dfs = dimensions_table.join(ranks_df)
            display(combined_dfs)

            # Get user input with choice of row to be deleted
            row_to_del = np.NaN
            deletable_rows = list(ranks_df.loc[ranks_df['this row may be deleted'],'row index'])
            while row_to_del not in deletable_rows:
                print(f"please select suitable row index! Choose from {deletable_rows}")
                while True:
                    try:
                        row_to_del = int(input(f'Which row to delete? ... '))
                        break
                    except ValueError:
                        print("Oops!  That was no valid number.  Try again...")

            # Delete row from matrix
            A_mat.row_del(row_to_del)
            n_rows, n_cols = A_mat.shape
            rank_A = A_mat.rank()
            print(rank_A)

            # Delete element from table index
            table_index.pop(row_to_del)

            # Delete row from dimension table
            dimensions_table.drop(dimensions_table.index[row_to_del], inplace=True)

        # Check if rightmost quadratic submatrix C_temp is invertible (i.e. not singular)
        C_temp = A_mat[:,-n_rows:]
        if C_temp.det() != 0:
            print('\n\nNew dimensions table')
            display(dimensions_table)
            print(f'number of rows: m = {n_rows}')
            print(f'number of columns: n = {n_cols}')

        else:
            print(f'the rightmost quadratic submatrix is not invertible!')
            print(f'please rearrange the variables in the input-string!')

            proceed = False
```

#### Now that we have guaranteed that we have more columns than rows in the dimensional matrix ($n>m$), and that the matrix has full rank ($r=m$), we can start to solve following linear system of equations:

$
\begin{pmatrix}
        a_{11}    & a_{12}  & ...     & a_{1n}\\
        a_{21}    & a_{22}  & ...     & a_{2n} \\
        \vdots    & \vdots  &         & \vdots   \\
        a_{m1}    & a_{m2}  & ...     & a_{mn}\\
\end{pmatrix}
\cdot
\begin{pmatrix}
        k_{1}\\
        k_{2} \\
        \vdots  \\
        k_{n}\\
\end{pmatrix}
= 0
\hspace{1cm} \rightarrow \hspace{1cm} \textbf{A} \cdot \vec{k} = 0
$

where the $k_1, ..., k_n$ are the unknown exponentes we are looking for. Since we have more unknown variables ($n$) than equations ($m$), the linear system of equations is underdetermined. A homogeneous (with all constant terms equal to zero) underdetermined linear system always has non-trivial solutions. There are an infinity of such solutions, which form a vector space, whose dimension is the difference between the number of unknowns (in our case $n$) and the rank of the matrix of the system (in our case $r=m$). So our solutions vector space has the dimension $d=n-r$, and we can thus determine $d$ independent dimensionless parameters.

In order to find one of the possible solutions we can freely choose values for the first $d$ unknown variables ($k_1, ..., k_d$) and then solve the equation system for the remaining $r$ unknown variables ($k_{d+1}, ..., k_n$).

We therefore partition matrix $\textbf{A}$ into submatrix $\textbf{B}$ of size $(m \times d)$ and submatrix $\textbf{C}$ of size $(m \times (n-d))$. Since ($m=r$) and ($n-d=r$), $\textbf{C}$ is a quadratic matrix of size ($r \times r$). The $n$-elements vector $\vec{k}$ is split into the vector $\vec{x}$ with $d$ elements and vector $\vec{y}$ with $r$ elements. $\vec{x}$ contains the variables for which we want to set the values and $\vec{y}$ contains the variables of which we want to find the values.

$
\begin{align}
\textbf{A} \cdot \vec{k} = 0 \\
\textbf{B} \cdot \vec{x} + \textbf{C} \cdot \vec{y} = 0
\end{align}
$

$
\begin{pmatrix}
        a_{11}    & a_{12}  & ...   & a_{1d}\\
        a_{21}    & a_{22}  & ...   & a_{2d} \\
         \vdots   & \vdots  &         & \vdots   \\
        a_{m1}    & a_{m2}  & ...   & a_{md}\\
\end{pmatrix}
\cdot
\begin{pmatrix}
        k_{1}\\
        k_{2} \\
        \vdots  \\
        k_{d}\\
\end{pmatrix}
+
\begin{pmatrix}
        a_{1(d+1)}    & a_{1(d+2)}  & ...   & a_{1n}\\
        a_{2(d+1)}    & a_{2(d+2)}  & ...   & a_{2n} \\
        \vdots        & \vdots      &       & \vdots   \\
        a_{m(d+1)}    & a_{m(d+2)}  & ...   & a_{mn}\\
\end{pmatrix}
\cdot
\begin{pmatrix}
        k_{d+1}\\
        k_{d+2} \\
        \vdots  \\
        k_{n}\\
\end{pmatrix}
= 0\\
$

We then subtract the first term to get it on the right side.

$
\textbf{C} \cdot \vec{y} = - \textbf{B} \cdot \vec{x}
$

$
\begin{pmatrix}
        a_{1(d+1)}    & a_{1(d+2)}  & ...   & a_{1n}\\
        a_{2(d+1)}    & a_{2(d+2)}  & ...   & a_{2n} \\
        \vdots        & \vdots      &       & \vdots   \\
        a_{m(d+1)}    & a_{m(d+2)}  & ...   & a_{mn}\\
\end{pmatrix}
\cdot
\begin{pmatrix}
        k_{d+1}\\
        k_{d+2} \\
        \vdots  \\
        k_{n}\\
\end{pmatrix}
=
- \begin{pmatrix}
        a_{11}    & a_{12}  & ...   & a_{1d}\\
        a_{21}    & a_{22}  & ...   & a_{2d} \\
         \vdots   & \vdots  &         & \vdots   \\
        a_{m1}    & a_{m2}  & ...   & a_{md}\\
\end{pmatrix}
\cdot
\begin{pmatrix}
        k_{1}\\
        k_{2} \\
        \vdots  \\
        k_{d}\\
\end{pmatrix}\\
$

We then multiply on both sided of the equation from the left with the inverse of matrix $\textbf{C}$. On the left hand side the matrix multiplication of $\textbf{C}^{-1}$ with $\textbf{C}$ yields the identity matrix $\textbf{I}$ of size ($d \times d$) and the matrix multiplication $\textbf{I} \cdot \vec{y}$ yields of course the vector $\vec{y}$ itself.

$
\begin{align}
\textbf{C}^{-1} \cdot \textbf{C} \cdot \vec{y} &= - \textbf{C}^{-1} \cdot \textbf{B} \cdot \vec{x}\\
\textbf{I} \cdot \vec{y} &= - \textbf{C}^{-1} \cdot \textbf{B} \cdot \vec{x}
\end{align}
$

$
\begin{pmatrix}
        1      & 0      & ...   & 0\\
        0      & 1      & ...   & 0 \\
        \vdots & \vdots &       & \vdots   \\
        0      & 0      & ...   & 1\\
\end{pmatrix}
\cdot
\begin{pmatrix}
        k_{d+1}\\
        k_{d+2} \\
        \vdots  \\
        k_{n}\\
\end{pmatrix}
=
-
\begin{pmatrix}
        a_{1(d+1)}    & a_{1(d+2)}  & ...   & a_{1n}\\
        a_{2(d+1)}    & a_{2(d+2)}  & ...   & a_{2n} \\
        \vdots        & \vdots      &       & \vdots \\
        a_{m(d+1)}    & a_{m(d+2)}  & ...   & a_{mn}\\
\end{pmatrix}^{-1}
\cdot
\begin{pmatrix}
        a_{11}    & a_{12}  & ...   & a_{1d}\\
        a_{21}    & a_{22}  & ...   & a_{2d} \\
         \vdots   & \vdots  &         & \vdots   \\
        a_{m1}    & a_{m2}  & ...   & a_{md}\\
\end{pmatrix}
\cdot
\begin{pmatrix}
        k_{1}\\
        k_{2} \\
        \vdots  \\
        k_{d}\\
\end{pmatrix}\\
$

On the right hand side the term $- \textbf{C}^{-1} \cdot \textbf{B}$ yields the matrix $\textbf{D}$ of size $(m \times d)$.

$
\vec{y} = \textbf{D} \cdot \vec{x}
$

$
\begin{pmatrix}
        k_{d+1}\\
        k_{d+2} \\
        \vdots  \\
        k_{n}\\
\end{pmatrix}
=
\textbf{D}
\cdot
\begin{pmatrix}
        k_{1}\\
        k_{2} \\
        \vdots  \\
        k_{d}\\
\end{pmatrix}\\
$

We can now freely choose the values for $\vec{x}$ and then determine the values of $\vec{y}$ to obtain all values needed to build the dimensionless parameters:

$\Pi = p_1^{k_{1}} \cdot p_2^{k_{2}} \cdot p_3^{k_{3}} \cdot \; ... \; \cdot p_n^{k_{n}}$

Since our solutions form a vector space with the dimenision of $d$, we can specify a different $\vec{x}$-vector $d$-times, but have to be sure, that these vectors are linearly independent. The easiest choice for the $\vec{x}$-vectors that meet this criteria are of course the unit vectors.

So, for the first dimensionless parameter we get:

$
\begin{pmatrix}
        k_{(1)d+1}\\
        k_{(1)d+2} \\
        \vdots  \\
        k_{(1)n}\\
\end{pmatrix}
=
\textbf{D}
\cdot
\begin{pmatrix}
        1\\
        0 \\
        \vdots  \\
        0\\
\end{pmatrix}
\hspace{1cm} \rightarrow \hspace{1cm}
\Pi_1 = p_1^1 \cdot p_2^0 \cdot p_3^0 \cdot \; ... \cdot p_d^0 \cdot p_{d+1}^{k_{(1)d+1}} \cdot \; ... \; \cdot p_n^{k_{(1)n}}
$

for the second dimensionless parameter we get:

$
\begin{pmatrix}
        k_{(2)d+1}\\
        k_{(2)d+2} \\
        \vdots  \\
        k_{(2)n}\\
\end{pmatrix}
=
\textbf{D}
\cdot
\begin{pmatrix}
        0\\
        1 \\
        \vdots  \\
        0\\
\end{pmatrix}
\hspace{1cm} \rightarrow \hspace{1cm}
\Pi_2 = p_1^0 \cdot p_2^1 \cdot p_3^0 \cdot \; ... \cdot p_d^0 \cdot p_{d+1}^{k_{(2)d+1}} \cdot \; ... \; \cdot p_n^{k_{(2)n}}
$

and so on...

However, we can simplify the procedure and obtain all values in one step. To do this, instead of $\vec{x}$ we enter all $d$ unit vectors at once (i.e., the identity matrix $\textbf{I}$ ) and obtain the solution matrix $\textbf{Y}$

$
\begin{align}
\vec{y}    &= \textbf{D} \cdot \vec{x}\\
\textbf{Y} &=\textbf{D} \cdot \textbf{I}\\
\textbf{Y} &=\textbf{D}\\
\end{align}
$

which shows that the matrix $\textbf{D}$ already contains all the values we are looking for!

To get a matrix $\textbf{K}$ with all the $k$-exponents we simply have to concatenate the identity matrix $\textbf{I}$ of size ($d \times d$) with the transpose of matrix $\textbf{D}$

$
\textbf{K} = \left[ I D^T \right]
$

$
\textbf{K} =
\begin{pmatrix}
        1       & 0      & ... & 0      & k_{(1)d+1} & k_{(1)d+2}  & ...   & k_{(1)n}\\
        0       & 1      & ... & 0      & k_{(2)d+1} & k_{(2)d+2}  & ...   & k_{(2)n} \\
        \vdots  & \vdots &     & \vdots & \vdots     & \vdots      &       & \vdots   \\
        0       & 0      & ... & 1      & k_{(d)d+1} & k_{(d)d+2}  & ...   & k_{(d)n}\\
\end{pmatrix}
$

where the i-th row of matrix $\textbf{K}$ contains the exponents of the i-th dimensionless parameter:

$
\Pi_i = p_1^{k_{(i)1}} \cdot p_2^{k_{(i)2}} \cdot p_3^{k_{(i)3}} \cdot \; ... \cdot p_n^{k_{(i)n}} \hspace{1cm} (i = 1 ...d)
$

```python
if proceed:
    print('A = ')
    display(A_mat)
    print('\n')

    # Partition matrix A into submatrix B...
    B_mat = A_mat[:,0:d]
    print('B = ')
    display(B_mat)
    print('\n')

    # ...and submatrix C
    C_mat = A_mat[:,d:]
    print('C = ')
    display(C_mat)
    print('\n')

    # Invert matrix C
    C_inv = C_mat.inv()
    print('C_inv = ')
    display(C_inv)
    print('\n')

    # Calculate matrix D
    D_mat = - C_inv*B_mat
    print('D = ')
    display(D_mat)
    print('\n')

    # Concatenate identity matrix with transpose of matrix D
    K_mat = sp.eye(d).row_join(D_mat.T)
    print('K = ')
    display(K_mat)
    print('\n')

    # Create list of dimensionless parameter names
    Pi_list = []
    for j in range(d):
        Pi_list.append('Pi_' + str(j+1))

    # Create data frame from K_mat values
    exponents_df = pd.DataFrame(np.array(K_mat), index=Pi_list, columns=variables_list)
    display(exponents_df)
```

    A =

$\displaystyle \left[\begin{matrix}1 & 1 & -1 & -3 & 1\\1 & 0 & 1 & 1 & 0\\-2 & -1 & -1 & 0 & 0\end{matrix}\right]$

    B =

$\displaystyle \left[\begin{matrix}1 & 1\\1 & 0\\-2 & -1\end{matrix}\right]$

    C =

$\displaystyle \left[\begin{matrix}-1 & -3 & 1\\1 & 1 & 0\\-1 & 0 & 0\end{matrix}\right]$

    C_inv =

$\displaystyle \left[\begin{matrix}0 & 0 & -1\\0 & 1 & 1\\1 & 3 & 2\end{matrix}\right]$

    D =

$\displaystyle \left[\begin{matrix}-2 & -1\\1 & 1\\0 & 1\end{matrix}\right]$

    K =

$\displaystyle \left[\begin{matrix}1 & 0 & -2 & 1 & 0\\0 & 1 & -1 & 1 & 1\end{matrix}\right]$

      F_D
      u
      eta
      rho
      D

      Pi_1
      1
      0
      -2
      1
      0

      Pi_2
      0
      1
      -1
      1
      1

#### Now that we have our solution, we want to display the obtained dimensionless parameters nicely using Sympy.

```python
if proceed:
    # Define Sympy symbols
    var_symb = sp.symbols(variables)
    Pi_symb = sp.symbols(Pi_list)

    # Create list with Sympy equations
    expressions = []
    for i in range(d):
        product = 1
        for j in range(n_cols):
            product *= var_symb[j]**K_mat[i,j]
        expression = sp.Eq(Pi_symb[i],product)
        expressions.append(expression)
        display(expression)
        print('')

    display(variables_table)
```

$\displaystyle \Pi_{1} = \frac{F_{D} \rho}{\eta^{2}}$

$\displaystyle \Pi_{2} = \frac{D \rho u}{\eta}$

      Quantity
      SI unit
      in SI base units
      Category
      Comment

      Symbol

      F_D
      Drag / Resistance force
      N
      kg m / s^2
      Mechanics
      -

      u
      Flow velocity
      m / s
      m / s
      Kinematics
      -

      eta
      Dynamic viscosity
      Pa s
      kg / (m s)
      Mechanics
      -

      rho
      Density
      kg / m^3
      kg / m^3
      Mechanics
      -

      D
      Diameter
      m
      m
      Geometry
      -


# Example for using the Python/SymPy tool chain with the signalflowgrapher
Hanspeter Schmid, 14.10.2020

## Draw a signal-flow graph
signalflowgrapher file `sfg.json`

Screen shot:

## Nice vector graphics for documentation / Jupyter notebooks
If you want it to look nicer, save as TikZ, translate with pdflatex to a pdf, and if you need it, make an svg from it with pdf2svg:

## Evaluate Mason in signalflowgrapher and paste code to Jupyter notebook

```python
import sympy as sp
Delta = sp.symbols('Delta')
L1,L2 = sp.symbols('L1,L2')
T_num = sp.symbols('T_num')
T_den = sp.symbols('T_den')
T_io = sp.symbols('T_io')
q_p,a_0,omega_p,a_1,a_2,s = sp.symbols('q_p,a_0,omega_p,a_1,a_2,s')

loops = [(L1, -omega_p**2/s**2), (L2, -omega_p/(q_p*s))]
determinant = [(Delta, -L1 - L2 + 1)]
denominator = [(T_den, Delta)]

P1,D1,P2,D2 = sp.symbols('P1,D1,P2,D2')
paths = [(P1, (a_1 + a_2*s)/s), (D1, 1), (P2, a_0/s**2), (D2, 1)]
numerator = [(T_num, D1*P1 + D2*P2)]

transfer_function = [(T_io, T_num/T_den)]
T=T_io.subs(transfer_function).subs(numerator).subs(denominator).subs(determinant).subs(paths).subs(loops).simplify()
display(T)
```

$\displaystyle \frac{q_{p} \left(a_{0} + s \left(a_{1} + a_{2} s\right)\right)}{\omega_{p}^{2} q_{p} + \omega_{p} s + q_{p} s^{2}}$

## Nicer expression with hanspitools
pip install hanspitools

```python
import hanspitools as ht
```

```python
ht.mani.numden(lambda p: ht.mani.nicepoly(p,s),T)
```

$\displaystyle \frac{a_{0} q_{p} + a_{1} q_{p} s + a_{2} q_{p} s^{2}}{\omega_{p}^{2} q_{p} + \omega_{p} s + q_{p} s^{2}}$

## Insert numbers and plot bode diagram

```python
import numpy as np
```

```python
target = [(a_0,1),(a_1,0),(a_2,0),(omega_p,1),(q_p,5)]
wn = 10.0**np.arange(-2,2,0.01)
```

```python
T_a = ht.freq.amplitude(T,s,wn,target)
T_p = ht.freq.phase(T,s,wn,target)
```

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

```python
fig, ax1 = plt.subplots()

color = 'tab:blue'
ax1.set_xlabel('$\omega$ in rad/sec')
ax1.set_ylabel('$|T|$', color=color)
ax1.loglog(wn, T_a, color=color)
ax1.tick_params(axis='y', labelcolor=color)

ax2 = ax1.twinx()  # instantiate a second axes that shares the same x-axis

color = 'tab:red'
ax2.set_ylabel('$\phi$ in degrees', color=color)  # we already handled the x-label with ax1
ax2.plot(wn, T_p, color=color)
ax2.tick_params(axis='y', labelcolor=color)

fig.tight_layout()  # otherwise the right y-label is slightly clipped
plt.show()
```

    <IPython.core.display.Javascript object>


# **Public finance - modelling intensive margin labor supply**

```python
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
import time
from scipy import linalg
from scipy import optimize
import sympy as sm
import seaborn as sns
from IPython import display

from modelproject import (budget_cons, budget_cons_comp, utility_function, labor_supply_fig_func)
sm.init_printing(use_unicode=True) # for pretty printing
%load_ext autoreload
%autoreload 2
```

```python
np.seterr(all='print')
```

    {'divide': 'warn', 'over': 'warn', 'under': 'ignore', 'invalid': 'warn'}

# Describe the model

In this notebook we want to examine the intensive labor supply i.e. hours worked given employment and how the labor supply changes for a given tax change.

The setup for the model is a follow:
The householdse maxmize utility which is a function of consumption and leisure. The time endowment is normalized to 1, hence leisure can be expressed as $1-h$ where $h$ is the relative time worked. The householde maxmize their utility subjected to the budget constraint.

\\[ \max u(x,1-h) \text{ s.t. } x=(1-t_{w})wh-t_{0}+a \\]

 - $u(\cdot)$ is the utility funciton
 - $x$ is consumption
 - $1-h$ is leisure
 - $w$ is the wage
 - $t_{w}$ is a tax on labor income
 - $t_{0}$ is lump sum tax
 - $a$ is non-labor income

Is the notebook we will define utility function as a quasi-linear utility function:

\\[ u(x,1-h)=x+\frac{1}{1+\frac{1}{\varepsilon}}(1-h)^{1+\frac{1}{\varepsilon}} \\]

where $\varepsilon$ is the elasticity of substitution

# Define the model in Python

Specify the symbols

```python
x = sm.symbols('x')             # consumption
h = sm.symbols('h')             # working share
epsilon = sm.symbols('epsilon') # elasticity
tw = sm.symbols('t_w')          # tax on labor income
w = sm.symbols('w')             # wage
t0 = sm.symbols('t_0')          # lump sump tax
a = sm.symbols('a')             # non-labor income
```

Defining the utility function:

```python
utility=x+(1/(1+1/epsilon))*(1-h)**(1+1/epsilon)
utility
```

Defining the budget constraint

```python
budget_con = sm.Eq(x,(1-tw)*w*h-t0+a)
budget_con
```

# Solve the model

**Find optimimum - Algorithm**
1. Insert the budget constraint in the utility function
2. Take the derivative wrt. $h$
3. Solve the first order conditions

**Step 1**

```python
# Find X
x_from_con=sm.solve(budget_con,x)
# Insert the budget constraint in the utility function
utility_sub=utility.subs(x,x_from_con[0])
utility_sub
```

**Step 2**

```python
#Find first derivative wrt. h
foc=sm.diff(utility_sub,h)
#simplify the expression
foc_sim=sm.simplify(foc)
foc_sim
```

**Step 3**

```python
#Find the optimal h as a function of the model parameters
sol = sm.solve(foc_sim,h)
sol
```

# Use solutions

```python
# Turn solution into Python function
_sol_func=sm.lambdify((tw,w,epsilon),sol[0])
def sol_func(tw=0.2,w=10,epsilon=-0.9,a=2,t0=1):
    opt_h=_sol_func(tw,w,epsilon)
    opt_x=(1-tw)*w*opt_h-t0+a
    opt_u=opt_x+1/(1+1/epsilon)*(1-opt_h)**(1+1/epsilon)
    return opt_h, opt_x, opt_u

# find optimum for hours worked, concsumption level and the implied utility
optimal_h,optimal_x,optimal_u=sol_func()

# find optimum for hours worked, consumption level and the implied utility when the tax increases
optimal_h_tax,optimal_x_tax,optimal_u_tax=sol_func(tw=0.5)

sol_func()

```

```python
#Find optimum for hours worked, consumption level under compensation

# As the utility function is quasi-linear there are no income effect
# This imply that the optimal level hours worked under compensation is
# the same without compensation
optimal_h_com=optimal_h_tax

#the optimal level of comsumption under compensation can be found as:
optimal_x_com=optimal_u-1/(1+1/(-0.9))*(1-optimal_h_com)**(1+1/(-0.9))

```

```python
# tax income before tax increase
tax_before=optimal_h*10*0.2

# tax income before tax increase
tax_after=optimal_h_tax*10*0.5

print(tax_before,tax_after)
```

    1.6922138966637712 3.825381056911981

# Plotting results

```python
#make different grids to evaluate the two functions with
grid = np.linspace(0,1,500)
grid_b = np.linspace(optimal_h-0.2,optimal_h+0.2,500)
grid_b_tax = np.linspace(optimal_h_tax-0.2,optimal_h_tax+0.2,500)

#before tax increase
budget_line=budget_cons(grid)
u_func_opt=utility_function(grid_b,optimal_u)

#after tax increase
budget_line_tax=budget_cons(grid,tw=0.5)
budget_line_tax_com=budget_cons_comp(grid,optimal_u,optimal_u_tax)
u_func_opt_tax=utility_function(grid_b_tax,optimal_u_tax)

fig = plt.figure(figsize=(8,8))

ax = fig.add_subplot(1,1,1)

ax.plot(grid,budget_line,lw=2,linestyle='-',color='black',label='Budget constraint',zorder=-1)
ax.plot(grid_b,u_func_opt,lw=2,linestyle='-',color='red',label='utility_function function',zorder=-1)
ax.plot(grid,budget_line_tax,lw=2,linestyle='-',color='black',zorder=-1)
ax.plot(grid,budget_line_tax_com,lw=2,linestyle='--',color='black',label='Compensated budget constraint',zorder=-1)
ax.plot(grid_b_tax,u_func_opt_tax,lw=2,linestyle='-',color='red',zorder=-1)

plt.scatter(optimal_h, optimal_x, s=100, zorder=1,marker="o",color='blue')
plt.annotate('$A$',xy=(optimal_h, optimal_x),xytext=(-7,10),textcoords='offset points',fontsize=20)

plt.scatter(optimal_h_com, optimal_x_com, s=100,zorder=1, marker="o",color='blue')
plt.annotate('$B$',xy=(optimal_h_com, optimal_x_com),xytext=(-7,10),textcoords='offset points',fontsize=20,)

plt.scatter(optimal_h_tax, optimal_x_tax, s=100, zorder=1,marker="o",color='blue')
plt.annotate('$C$',xy=(optimal_h_tax, optimal_x_tax),xytext=(-7,10),textcoords='offset points',fontsize=20,)

plt.text(0.1, 7.5,'$A$ to $B$: Substition effect \n$B$ to $C$: Income effect \n$A$ to $C$: Total effect', fontsize=20)

ax.set_ylim([0,10]);
ax.legend(loc='lower right',fontsize=15)

ax.set_xlabel('$h$',fontsize=20)
ax.set_ylabel('$x$',fontsize=20)
ax.set_title('Optimum',fontsize=30)
plt.show()
```

# Optimize the utility numerically

We use the utility-function expressed only as a function $h$

```python
#make the utility function into a python function
def _utility_func(h):
    return (1-0.2)*10*h-1+2+(1/(1+1/(-0.9)))*(1-h)**(1+1/(-0.9))
def utility_func(x):
    #add a -1 to convert the maximization problem into a minimization problem
    return -1*_utility_func(x[0])

```

```python
# evaluate the utility function
u_grid=_utility_func(grid)

#plot the utility function
fig = plt.figure()
ax = fig.add_subplot(1,1,1)

ax.plot(grid,u_grid,lw=1,label='Budget constraint')

ax.set_ylim([-10,0]);
ax.legend(loc='lower right')

ax.set_xlabel('$h$');
ax.set_ylabel('$Utility$');
ax.set_title('Optimum');
```

```python
x0 = [0.2]
result = optimize.minimize(utility_func,x0,
                           method='BFGS',
                           options={'disp':True}) # display the results

optimal_h_num=result.x
optimal_u_num=(-1)*result.fun
print(f'Optimal hours worked: {optimal_h_num[0]}')

```

    Warning: Desired error not necessarily achieved due to precision loss.
             Current function value: nan
             Iterations: 1
             Function evaluations: 336
             Gradient evaluations: 112
    Optimal hours worked: 1034.44

# Introduction of af top tax

In the previous example we asumed a linear tax system which implies that marginal tax rate is the same across income levels. In the following we will see what happens when we introduce a top tax.

With a top tax the marginal tax rate increases from $t_{1}$ to $t_{2}>t_{1}$ when the income exceed a certain threshold $K$.

The budget constraint with progressives taxes can be written as:

\\[ x=(1-t_{1})\cdot min(wh,K)+(1-t_{2})\cdot max(h\cdot w-K,0)-t_{0}+a \\]

To find the optimal choice of $h$ we can use the solution from the previous model in a three step algorithm.

The algorithm to find the consumpers choice of $h$ is:
1. Calculate the optimal choice of $h$ under $t_{1}$ and check if this is a possible solution, i.e the income must be lower than $K$
2. Calculate the optimal choice of $h$ under $t_{2}$ and check if this is a possible solution, i.e the income must be lower than $K$
3. If neither of the two previous choices of $h$ are possible, the consumer will choose $h=\frac{K}{w}$

```python
def sol_func_top_tax(f,ta=0.2,tb=0.5,w=10,epsilon=-0.9,a=2,t0=1,K=5):
    #Tax below
    opt_h_below=f(tw=ta,epsilon=epsilon)

     #Tax above
    opt_h_above=f(tw=tb,epsilon=epsilon)

    #Tax kink
    opt_h_kink=K/w

    if opt_h_below[0]*w<K:
        opt_h=opt_h_below[0]

    elif opt_h_above[0]*w>K:
        opt_h=opt_h_above[0]

    else:
        opt_h=opt_h_kink

    opt_x=(1-ta)*min(w*opt_h,K)+(1-tb)*max(w*opt_h-K,0)-t0+a
    opt_u=opt_x+1/(1+1/epsilon)*(1-opt_h)**(1+1/epsilon)

    return opt_h, opt_x, opt_u

optimal_h_top,optimal_x_top,optimal_u_top=sol_func_top_tax(sol_func)
sol_func_top_tax(sol_func)

```

```python
def budget_cons_top_tax(h,ta=0.2,tb=0.5,w=10,a=2,t0=1,K=5):
    return (1-ta)*min(w*h,K)+(1-tb)*max(w*h-K,0)-t0+a

#before tax increase
budget_line_top=[]
for i in grid:
    budget_line_top.append(budget_cons_top_tax(i))

grid_b_top = np.linspace(optimal_h_top-0.2,optimal_h_top+0.2,500)
u_func_top=utility_function(grid_b_top,optimal_u_top,epsilon=-0.9)

```

```python
fig = plt.figure(figsize=(8,8))

ax = fig.add_subplot(1,1,1)

ax.plot(grid,budget_line,lw=2,linestyle='-',color='black',label='Budget constraint',zorder=-1)
ax.plot(grid_b,u_func_opt,lw=2,linestyle='-',color='red',label='Utility function',zorder=-1)

ax.plot(grid,budget_line_top,lw=2,linestyle='-',color='black',zorder=-1)
ax.plot(grid_b_top,u_func_top,lw=2,linestyle='-',color='red',zorder=-1)

plt.scatter(optimal_h, optimal_x, s=100, zorder=1,marker="o",color='blue')
plt.annotate('$A$',xy=(optimal_h, optimal_x),xytext=(-7,10),textcoords='offset points',fontsize=20)

plt.scatter(optimal_h_top, optimal_x_top, s=100, zorder=1,marker="o",color='blue')
plt.annotate('$B$',xy=(optimal_h_top, optimal_x_top),xytext=(-7,10),textcoords='offset points',fontsize=20,)

plt.text(0.1, 7.5,'$A$: Optimum without top tax \n$B$: Optimum with top tax', fontsize=20)

ax.set_ylim([0,10]);
ax.legend(loc='lower right',fontsize=15)

ax.set_xlabel('$h$',fontsize=20)
ax.set_ylabel('$x$',fontsize=20)
ax.set_title('Optimum',fontsize=30)
plt.show()
```

# What happens when we change different parameters in the model

## The distribution of $h$ over different values of $\epsilon$

```python
#Make a list over different values of epsilon
epsilon_array = np.arange(-1,0,0.001)
epsilon_list = [round(epsilon,3) for epsilon in epsilon_array]
#remove -1 from the list as it is not a posssile value
epsilon_list.remove(-1)
#Create empty list
hours=[]
cons=[]
utility=[]
#Loop through the list and calculate h for each value of epsilon
for i in epsilon_list:
    _hours,_cons,_utility=sol_func_top_tax(sol_func,epsilon=i)
    hours.append(_hours)
hours
#plot the distribution over h
sns.distplot(hours, bins=20,kde=False);
```

From the distribution above we can see that people bunch around the threshold for the top tax

## Plot labor supply curve for different values of epsilon

```python
#Please run this twice (at least) to get the full experience!
color = ['red', 'black']

epsilon_array = np.arange(-1,0,0.05)
epsilon_list = [round(epsilon,2) for epsilon in epsilon_array]
epsilon_list.remove(-1)
for epsilon in epsilon_list:
    time.sleep(0.2)
    display.display(labor_supply_fig_func(epsilon, 0.2, 0.5, sol_func));
    display.clear_output(wait=True);

```


# Solow Model with finite resources and climate externalities

Imports and set magics:

```python
import numpy as np
from scipy import optimize
import sympy as sm
import matplotlib.pyplot as plt

# autoreload modules when code is run
%load_ext autoreload
%autoreload 2
#XD FOR SMART UPLOAD
# local modules
import modelproject
```

# Model description

We consider a Solow-model that incorporates fossil fuels and climate externalities

 The model structure is

1 $$Y_t=D_tK^\alpha _t(A_tL_t)^{\beta}E^{\epsilon}_t,\text{   }\alpha, \beta, \varepsilon \geq0, \alpha+\beta+\varepsilon=1$$

2 $$ K_{t+1}=sY_t+(1-\delta) K_t, \text{  } 0<\delta<1 $$

3 $$L_{t+1}=(1+n)L_t, \text{   } n\geq 0$$

4 $$A_{t+1}=(1+g)A_t, \text{   } g\geq 0$$

5 $$ R_{t+1}=R_t-E_t$$

6 $$ E_t=s_ER_t, \text{   } 0<s_E<\delta $$

7 $$ D_t=\left(\frac{R_t}{R_0}\right)^{\phi},\text{   }\phi\geq 0$$

* $K_t$ is capital
* $L_t$ is labor (growing with a constant rate of $n$)
* $E_t$ is fossil fuels (where the amount used in each period is a fraction of the remaining reserve)
* $R_t$ is the remaining reserve of fossile fuels, $R_0$ is the initial reserve
* $A_t$ is technology (growing with a constant rate of $g$)
* $D_t$ is the damage function that models the climate externalities
* $Y_t$ is output of the economy

Using the above equations, the following transition curve can be found

$$z_{t+1} =\left(\frac{1}{1-s_E}\right)^{\varepsilon+\phi}\left(\frac{1}{(1+n)(1+g)}\right)^\beta (s+(1-\delta)z_t)^{1-\alpha})z_t^\alpha$$

where $z_t$ is the capital/output ratio $z_t=\frac{K}{Y}$

## Specification of the damage function

In our model specification, the damage function directly affects the production Y. This means that the direct interperetation of the damage function is that expending fossil fuels, R, destroys output. Other ways to model this could be that the damage function is applied to Capital or maybe add landmass to the model and have the damage function diminish the landmass. These alternative specifications can give the damage function a better interperetation. In the example of reducing landmass, the interperetation would be that expenditure of fossile fuels causes landmass to be reduced via global warming which melts ice that causes the sea level to rise. However, where we apply the damage function is irrelevant, as they are mathematically equivalent if you scale $\phi$

Say for example, the damage function affected Capital instead of directly diminishing output, our model would look like:

$$Y_t=(K_tD_t)^\alpha(A_tL_t)^{\beta}E^{\epsilon}_t,\text{   }\alpha, \beta, \varepsilon \geq0, \alpha+\beta+\varepsilon=1$$

$\leftrightarrow$

$$Y_t= D_t^\alpha K_t^\alpha(A_tL_t)^{\beta}E^{\epsilon}_t,\text{   }\alpha, \beta, \varepsilon \geq0, \alpha+\beta+\varepsilon=1$$

Which means, that if we scale $\phi$ by $\frac{1}{\alpha}$, these would be completly equivalent. Which is why we don't think it's a problem to have $D_t$ directly affect $Y_t$, despite the implication that usage of fossile fuels directly destroys output. The damage function can essentially be seen as a part of the TFP. As for the actual form of the damage function, we "stole" it from an old macroeconomics exam.

## Solving the model - steady state

How to solve

* 1 Find the transition equation, which describes the evolution of the economy given the initial values. In this case the transition equation has already been found (equation 8).

* 2 Specify the parameters of the model

* 3 Find the steady state, that is, the point to which the economy converges. In this case steady state is described in terms of the capital-output ratio, since it does not converge in capital per work (or output per worker).

## Parameters:
* $\varepsilon$ = 0.1
* $\beta$ = 0.6
* $\alpha$ = 0.3
* $\delta$ =0.05
* n = 0.01
* g = 0.02
* s =0.20
* se = 0.005

All parameters are given reasonable values based on those given in chapter 7 in Introducing Advanced Macroeconomics by Sørensen and Jacobsen.

Convergence requires that
$$z_{t+1}=z_t \Leftrightarrow z_{t+1}-z_t=0$$

```python
# We define a function that uses the above condition to find steady state

def solve_model_with_liquiddinosaurs(phi, epsilon=0.1 , beta=0.6, alpha=0.3, delta=0.05, n=0.01, g=0.02, s=0.2, se=0.005,display=False):

    """Solves the Solowmodel with fossile fuels and climate externalities for given parameters"""

    # We rewrite the transition equation so that we can use scipy to maximize it numerically
    obj_zss = lambda z: z- (1/(1-se))**(epsilon+phi)*((1/((1+n)*(1+g)))**beta)*((s+(1-delta)*z)**(1-alpha))*z**alpha

    if type(phi) != list: # Commands executed if the augument for phi passed to the function is not a list
        ss_result = optimize.root_scalar(obj_zss,bracket=[0.1,100],method="brentq")
        # We call a numerical optimizer from scipy using the "brentq" method, where the bracket defines an interval which we expect contains the steady state value, defined in such a way that we aviod the trivial steady state (z* = 0)
        if display == True: # If True a f string will be printed otherwise the function returns the result
            print(f' The steady state value is: z* = {ss_result.root:.3f}')
        else:
            return(ss_result.root)

    else: # Commands executed if the augument for phi passed to the function is a list
        phi_list=[] # List that saves each phi
        ss_list=[] # List that stores the steady state value for each phi
        for phi in phi: # Numerical optimization as above
            ss_list.append(optimize.root_scalar(obj_zss,bracket=[0.1,100],method="brentq").root)
            phi_list.append(phi)
        if display == True: # If True multiple f strings will be printed, otherwise the function returns the steady state values in a list
            for i,value in zip(phi_list,ss_list):
                print(f' The steady state value for \u03C6 = {i} is: z* = {value:.3f}')
        else:
            return(ss_list)
```

```python
# Now we solve the model using the function above.
# We first consider the case where phi is equal to zero - that is, the Solow model with fossil fuels but without climate externalities as a baseline.
solve_model_with_liquiddinosaurs(0,display=True)
```

    The steady state value is: z* = 2.663

```python
# Now we introduce climate changes in the model by allowing phi to take various values
phis=[0,0.2,0.4,0.6,0.8,1,1.2,1.4,1.6,1.8,2] # List of different values that phi can take

# We then plot the relationship between phi and steady state
fig = plt.figure()
ax = fig.add_subplot(1,1,1)
cs = plt.scatter(phis,solve_model_with_liquiddinosaurs(phis))
ax.set_xlabel('$\phi$')
ax.set_ylabel('$z^*$').set_rotation(0)
ax.set_title("Steady state");

```

In the above plot it can be seen that $z^*$ is increasing in $\phi$. This is a due to the fact, that for a given capital stock less output will be produced when $\phi$ increases due to a larger climate externality effect

## Simulation

We have defined the model equations in the modelproject.py module, now we import them.

```python
from modelproject import Y_t, D_t, E_t, A_t1, K_t1, L_t1, R_t1
```

Then we create a function to simulate the model

```python
def simulate(K0,A0,L0,R0,periods,a=0.3,b=0.6,e=0.1,g=0.02,n=0.01,s=0.2,phi=1,sE=0.005,d=0.05):
    """Simulates the Solowmodel for given starting values of capital, labour, technology and fossil fuels aswell as given model parameters"""

# First we generate lists to store the output for each endogenus variabel and add/calculate the period 0 values:

    listA = [A0]
    listK = [K0]
    listL = [L0]
    listR = [R0]
    listD = [1]   # The damage function will always be 1 in period 0, since R_t=R_0 -> (R_t/R_0)^phi=1
    listE = [E_t(sE = sE, R = R0)]
    listY = [Y_t(listD[-1], K0, A0, L0, listE[-1], a = a, b = b, e = e)]
    listt = [0] # We always start in period t = 0, so the first element in t is 0

    # Now we simulate for the amount of periods specified:
    i = 1 # Where i indicates the period, so we start by simulating periode t = 0+1 =1
    # The following calculations are done in order of causality, so we can call the last element in the needed variable list, therefore Y has to be done last, so the last element in K, A, L and so on has already been updated for the current period.
    while i <= periods:
        listA.append(A_t1(listA[-1],g = g))
        listK.append(K_t1(Y = listY[-1], K = listK[-1], s = s, d = d))
        listL.append(L_t1(listL[-1], n = n))
        listR.append(R_t1(listR[-1], E = listE[-1]))
        listE.append(E_t(R = listR[-1], sE = sE))
        listD.append(D_t(R = listR[-1], R_0 = R0, phi = phi))
        listY.append(Y_t(D = listD[-1],K = listK[-1],A = listA[-1],L = listL[-1],E = listE[-1], a = a, b = b, e = e))
        listt.append(i)
        i+=1

# We return the lists for Y, L, K, t, D and R as they are the variables we are interested in analyzing.
    return listY, listL, listK, listt, listD, listR
```

```python
# Now we define a list of phi values we want to analyze.
# We chose 0, 0.5, 1 and 2 instead of 0, 0.5, 1 and 1.5, because it makes a plot later on symmetrical.
philist = [0, 0.5, 1, 2]
```

```python
# Then we create some lists containing 4 lists of the Y, K etc. values for each phi value in philist. The [0] and [2] are because the simulate-function returns 6 lists with Y being index 0 and K beign index 2 etc.
# We use 1 as all the starting values because the values themselves are less interesting than their evolution over time.
smalltlist = simulate(1,1,1,1,200)[3]
smalllist = simulate(1,1,1,1,200)[1]
smallklist = np.array([simulate(1,1,1,1, 200, phi = phi)[2] for phi in philist])
smallylist = np.array([simulate(1,1,1,1, 200, phi = phi)[0] for phi in philist])

# We then calculate y and k, by dividing Y and K by L.
smalllowercaseylist = np.divide(smallylist,smalllist)
smalllowercaseklist = np.divide(smallklist,smalllist)
# Then we calculate z = K/Y = k/y
smallzlist = np.divide(smalllowercaseklist,smalllowercaseylist)
```

```python
# We then create a figure showing 2 plots, one of z and one of y both against time.
lilyfig = plt.figure(figsize = [15, 8])
ox1 = lilyfig.add_subplot(2,1,1)

plt.plot(smalltlist,smalllowercaseylist[0], label = "$\phi$ = 0")
plt.plot(smalltlist,smalllowercaseylist[1], label = "$\phi$ = 0.5")
plt.plot(smalltlist,smalllowercaseylist[2], label = "$\phi$ = 1")
plt.plot(smalltlist,smalllowercaseylist[3], label = "$\phi$ = 2")
plt.legend()

h = plt.ylabel("$y_t$")
h.set_rotation(0)
plt.xlabel("t")
ox1.set_title("Income per worker")

ox2 = lilyfig.add_subplot(2,1,2)
plt.plot(smalltlist,smallzlist[0], label = "$\phi$ = 0")
plt.plot(smalltlist,smallzlist[1], label = "$\phi$ = 0.5")
plt.plot(smalltlist,smallzlist[2], label = "$\phi$ = 1")
plt.plot(smalltlist,smallzlist[3], label = "$\phi$ = 2")
plt.legend()

# We hide the axis labels and tick of the upper plot, as they share the same axis.
ox1.axes.get_xaxis().set_visible(False)

h = plt.ylabel("$z_t$")
h.set_rotation(0)
plt.xlabel("t")
ox2.set_title("Capital/Income ratio")
plt.close(lilyfig)
```

The following figure displays how both the income per worker and capital/income ratio develops over time given different values of $\phi$. $\phi$ is parameter that determines the amount of damage oilconsumption has on production and if the damage is mostly from the first or last amount of oil used. If $\phi$ = 0 then we'll have a standard Solow-model with a finite resource. We see that any $\phi$ > 0 makes income per worker fall to a lower level and that the captil/income ratio in SS is higher.

```python
# The figure won't show till you run this cell. This way there's less space between the analyzing text and the figure itself
lilyfig
```

## What is a realistic value of $\phi$?

We now simulate the damage function and the remaining amount of fossil fuels over 2000 periods. We use this many periods because the amount of fossil fuels, R, declines rather slowly.

```python
dlist = np.array([simulate(1,1,1,1, 2000, phi = phi)[4] for phi in philist])
rlist = simulate(1,1,1,1,2000)[5]
ylist = np.array([simulate(1,1,1,1, 2000, phi = phi)[0] for phi in philist])
llist= np.array([simulate(1,1,1,1, 2000, phi = phi)[1] for phi in philist])
lowercaseylist = np.divide(ylist,llist)
```

```python
# Then we plot the damage function against the remaining amount of fossil fuels. The figure doesn't show because we closed the figure display, this is to give it more space. It won't show till you run the next code cell.
# Create the figure object
damagefig = plt.figure(figsize = [8, 8])
ax = fig.add_subplot(1,1,1)
# Then we add the dataseries
plt.plot(rlist, dlist[0], label = "$\phi$ = 0")
plt.plot(rlist, dlist[1], label = "$\phi$ = 0.5")
plt.plot(rlist, dlist[2], label = "$\phi$ = 1")
plt.plot(rlist, dlist[3], label = "$\phi$ = 2")
plt.legend()
h = plt.ylabel("$D_t$")
h.set_rotation(0)
plt.xlabel("$R_t$")
# We define the lower limit of the y-axis
plt.ylim(bottom = 0)
plt.close(damagefig)
```

The figure plots the damage function against the remaining amount of fossile fuels, for different values of $\phi$. Unsurprisingly, when $\phi$ = 0, the damage function remains at 1 the entire time, because $\phi$ = 0 $\rightarrow$ $D_t$ = 1 $\forall$ $R_t$.

When $\phi$ = 1 the damage function becomes linear and since we chose $R_0$ = 1 the slope is exactly -1, although the axis is inverted. In any case, the implication of having $\phi$ = 1 is that climate change linearly damages production (the economy). This isn't exactly in line with reality, an argument for this is, that a global increase in temperature of 0.1 degrees celcius will barely have any effect on the world. Whereas a global increase in temperature of 1 degrees will impact economies in many ways, positive and negative, see https://link.springer.com/article/10.1023/A:1014500930521 for some estimates of a 1 C degree increase. Although these estimates are positive when using a simple sum and negative when using global average values.

Either way we expect the damage of climate changes to grow exponentially. Therefore we reject $\phi$ > 1 because it gives a concave damage function meaning small amounts of fossile fuel usage lead to great losses in output. From the red curve in the figure below you can see that with $\phi$ = 2, the damagefunction falls very quickly, but then becomse flatter.

Having $\phi$ < 1 means that the damages are convex, so that small expenditures of fossile fuels only lead to minor losses due to climate changes. But as more fossile fuels are used, the damage grows for each unit of fuel used. This is more in line with our expectations and therefore we think 0 < $\phi$ < 1 suits the model best.

```python
# Now we display the figure. Now there's also some space for the analysis text above :).
damagefig
```

# Growth Rates

We now consider the growth rates towards steady state and the growth rates in steady state. This is done by:

* 1 Solving the model numerically as we did in the beginning, but only using the values for $\phi$ used in the simulation above

* 2 Find the period in which the economy has converged to steady state. We define that period as the period, in which the difference between $z_t$ and z∗ is below 0.5%

* 3 Then for each $\phi$ we calculate the average growth rate from period t=0 to that period in which the economy has converged, and the growth rate in steady state. Since the model converges towards steady state asymptotically, the steady state growth rate is calculated as the average growth rate from the first steady state period to period 2000.

```python
SS_list_new = solve_model_with_liquiddinosaurs(philist) # list that stores the steady state values for the same phis used in the simulation above

Period_list=[] # List to store periods in which the economy has converged for various phi

# Now for each phi we loop through the smallzlist to find steady state
for i in range(0,4,1): # Loop that goes through each Steady state
    for j in range(0,200,1): # Loop over periods
        if (SS_list_new[i]-smallzlist[i][j])/SS_list_new[i]<0.005: # Checks for convergence
            period = j # Updates j if the model has converged to steady state
            Period_list.append(period) # Saves j
            break
    print(f'For \u03C6 = {philist[i]} the economy is in steady from period t = {Period_list[i]}') # Print of each phi with associated convergence period
```

    For φ = 0 the economy is in steady from period t = 82
    For φ = 0.5 the economy is in steady from period t = 88
    For φ = 1 the economy is in steady from period t = 94
    For φ = 2 the economy is in steady from period t = 108

It can thus be seen that as $\phi$ increases convergence to steady state takes longer. This result might be due to the fact, that when $\phi$ is large a greater proportion of output is used to compensate for climate damage. This means that in each period less capital is accumualted and thus the marginal product of capital will diminish at a lower pace, which results in a longer convergence time.

```python
# Growth rates towards Steady State

for i in range(0,4,1): # Looping over the various steady states
    growth_factor = smalllowercaseylist[i][Period_list[i]]/smalllowercaseylist[0] # Calculate the growth factor over the entire period
    avg_growth = (growth_factor**(1/Period_list[i])-1)*100 # Using the growth factor to calcualte the average growth
    print(f'The average growth rate towards steady state for \u03c6 = {philist[i]} is: {avg_growth[i]:.3f} % per period' ) # Printing
```

    The average growth rate towards steady state for φ = 0 is: 1.731 % per period
    The average growth rate towards steady state for φ = 0.5 is: 1.345 % per period
    The average growth rate towards steady state for φ = 1 is: 0.965 % per period
    The average growth rate towards steady state for φ = 2 is: 0.241 % per period

We see that a higher $\phi$ implies lower growth rates towards steady state, which is not a suprise given the figure that shows the evolution of the capital/income ratio, and the fact that it takes longer for the economy to converge when $\phi$ increases

```python
# Growth in steady state

for i in range(0,4,1): # Looping over the various steady states
    growth_factor = lowercaseylist[i][-1]/(lowercaseylist[i][Period_list[i]]) # Calculating the growth factor from first steady state period to period 2000
    avg_growth = (growth_factor**(1/(2000-Period_list[i]))-1)*100 # Using the growth factor to calculate the average growth from the first steady state period to period 2000
    print(f'the approximate steady state growth rate for \u03c6 = {philist[i]} is: {avg_growth:.3f} % per period') # Printing

```

    the approximate steady state growth rate for φ = 0 is: 1.495 % per period
    the approximate steady state growth rate for φ = 0.5 is: 1.132 % per period
    the approximate steady state growth rate for φ = 1 is: 0.771 % per period
    the approximate steady state growth rate for φ = 2 is: 0.052 % per period

From the above calculation it can be seen, that the classical insight from the Solow model still holds in this model. Namely that an economy outside steady state will grow at a higher rate, than a structurally identical economy in steady state.

Furthermore it can be seen, that in steady state all of the economies attain long run growth rates that are below 2% per period - even though exogenous technological growth is assumed to be 2% per period. This is due to the fact, that even with $\phi$=0 the introduction of finite ressources (here oil) will imply a long run growth drag, due to resources becomming more and more scarce. Increasing $\phi$ will only increase this growth drag, as more output will be destroyed in each period.

Though one also sees, that even in the long run, income per worker will stil show positive growth rates, which is due to the fact, that the Cobb-Douglas production function assummes that the climate and the scarce resources can be substituted by technologically augumented labour.

## The welfare loss of climate externalities

The last question we would like to anwser is how much production is lost when including climate externalities in the model, compared to the "standard" Solow model with finite ressources.

To do this we start by simulating the model, with 3 values of $\phi$, 0, 0.4 and 0.8. $\phi$ = 0 means there are no externalities, while we use 0.4 and 0.8 as lower and upper boundaries on a interval in which $\phi$ might be.

```python
listofphi = [0, 0.2, 0.9]
listofy = np.array([simulate(1,1,1,1, 100, phi = phi)[0] for phi in listofphi])
listoft = smalltlist = simulate(1,1,1,1,100)[3]
periodstocheck = range(0,101)

# Lists to store output
lossofylow = []
lossofyhigh = []

# We make a loop that calculates the percentage output/per worker loss relative to a Solow model with phi = 0
for i in periodstocheck:
    for j in range(1,3):
        if j == 1:
            lossofylow.append((listofy[j][i]/listofy[0][i]-1)*100)
        else:
            lossofyhigh.append((listofy[j][i]/listofy[0][i]-1)*100)
```

```python
elplot = plt.figure(figsize = [7, 7])
ax = elplot.add_subplot(1,1,1)
plt.plot(listoft, lossofyhigh, label = "Upper bound")
plt.plot(listoft, lossofylow, label = "Lower bound")

plt.legend()
k = plt.ylabel("%loss")
k.set_rotation(0)
plt.xlabel("$t$");
```

The above figure shows, that with $\phi$ $\in$ $[0.2, 0.9]$ the loss in income for a given period, when including climate externalities is somewhere between 10% and 40% after 100 periods. 100 Periods being the approximate time of reaching steady state (not exactly, but approximately).

## Conclusion

From the analysis we can conclude, that extending the Solow model with climate externalities in the form of a damage function that defines climate damage as the ratio of current fossil fuels reserves to initial reserves, alters non of the main conclusion of the Solow model. That is, the model still predicts that the growth rate towards steady state is higher than the steady state growth rate, and that growth will be positive in the long run. Though the model does predict that finite resources and climate damage will imply serious long run growth drags, which can lead to high welfare losses compared to scenarios where climate damage is mitigated, and that convergence takes longer - which can be an improvement since the standard Solow model tends to converge too fast

```python

```


# Non Linear Regression Analysis

Estimated time needed: **20** minutes

## Objectives

After completing this lab you will be able to:

-   Differentiate between Linear and non-linear regression
-   Use Non-linear regression model in Python

If the data shows a curvy trend, then linear regression will not produce very accurate results when compared to a non-linear regression because, as the name implies, linear regression presumes that the data is linear.
Let's learn about non linear regressions and apply an example on python. In this notebook, we fit a non-linear model to the datapoints corrensponding to China's GDP from 1960 to 2014.

Importing required libraries

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

Though Linear regression is very good to solve many problems, it cannot be used for all datasets. First recall how linear regression, could model a dataset. It models a linear relation between a dependent variable y and independent variable x. It had a simple equation, of degree 1, for example y = $2x$ + 3.

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph
y = 2*(x) + 3
y_noise = 2 * np.random.normal(size=x.size)
ydata = y + y_noise
#plt.figure(figsize=(8,6))
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Independent Variable')
plt.show()
```

Non-linear regressions are a relationship between independent variables $x$ and a dependent variable $y$ which result in a non-linear function modeled data. Essentially any relationship that is not linear can be termed as non-linear, and is usually represented by the polynomial of $k$ degrees (maximum power of $x$).

$$ \ y = a x^3 + b x^2 + c x + d \ $$

Non-linear functions can have elements like exponentials, logarithms, fractions, and others. For example: $$ y = \log(x)$$

Or even, more complicated such as :
$$ y = \log(a x^3 + b x^2 + c x + d)$$

Let's take a look at a cubic function's graph.

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph
y = 1*(x**3) + 1*(x**2) + 1*x + 3
y_noise = 20 * np.random.normal(size=x.size)
ydata = y + y_noise
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Independent Variable')
plt.show()
```

As you can see, this function has $x^3$ and $x^2$ as independent variables. Also, the graphic of this function is not a straight line over the 2D plane. So this is a non-linear function.

Some other types of non-linear functions are:

### Quadratic

$$ Y = X^2 $$

```python
x = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph

y = np.power(x,2)
y_noise = 2 * np.random.normal(size=x.size)
ydata = y + y_noise
plt.plot(x, ydata,  'bo')
plt.plot(x,y, 'r')
plt.ylabel('Dependent Variable')
plt.xlabel('Independent Variable')
plt.show()
```

### Exponential

An exponential function with base c is defined by $$ Y = a + b c^X$$ where b ≠0, c > 0 , c ≠1, and x is any real number. The base, c, is constant and the exponent, x, is a variable.

```python
X = np.arange(-5.0, 5.0, 0.1)

##You can adjust the slope and intercept to verify the changes in the graph

Y= np.exp(X)

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Independent Variable')
plt.show()
```

### Logarithmic

The response $y$ is a results of applying logarithmic map from input $x$'s to output variable $y$. It is one of the simplest form of **log()**: i.e. $$ y = \log(x)$$

Please consider that instead of $x$, we can use $X$, which can be polynomial representation of the $x$'s. In general form it would be written as
\begin{equation}
y = \log(X)
\end{equation}

```python
X = np.arange(-5.0, 5.0, 0.1)

Y = np.log(X)

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Independent Variable')
plt.show()
```

### Sigmoidal/Logistic

$$ Y = a + \frac{b}{1+ c^{(X-d)}}$$

```python
X = np.arange(-5.0, 5.0, 0.1)

Y = 1-4/(1+np.power(3, X-2))

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Independent Variable')
plt.show()
```

# Non-Linear Regression example

For an example, we're going to try and fit a non-linear model to the datapoints corresponding to China's GDP from 1960 to 2014. We download a dataset with two columns, the first, a year between 1960 and 2014, the second, China's corresponding annual gross domestic income in US dollars for that year.

```python
import numpy as np
import pandas as pd

#downloading dataset
!wget -nv -O china_gdp.csv https://cf-courses-data.s3.us.cloud-object-storage.appdomain.cloud/IBMDeveloperSkillsNetwork-ML0101EN-SkillsNetwork/labs/Module%202/data/china_gdp.csv

df = pd.read_csv("china_gdp.csv")
df.head(10)
```

    2020-12-25 04:02:07 URL:https://cf-courses-data.s3.us.cloud-object-storage.appdomain.cloud/IBMDeveloperSkillsNetwork-ML0101EN-SkillsNetwork/labs/Module%202/data/china_gdp.csv [1218/1218] -> "china_gdp.csv" [1]

      Year
      Value

      0
      1960
      5.918412e+10

      1
      1961
      4.955705e+10

      2
      1962
      4.668518e+10

      3
      1963
      5.009730e+10

      4
      1964
      5.906225e+10

      5
      1965
      6.970915e+10

      6
      1966
      7.587943e+10

      7
      1967
      7.205703e+10

      8
      1968
      6.999350e+10

      9
      1969
      7.871882e+10

**Did you know?** When it comes to Machine Learning, you will likely be working with large datasets. As a business, where can you host your data? IBM is offering a unique opportunity for businesses, with 10 Tb of IBM Cloud Object Storage: [Sign up now for free](http://cocl.us/ML0101EN-IBM-Offer-CC)

### Plotting the Dataset

This is what the datapoints look like. It kind of looks like an either logistic or exponential function. The growth starts off slow, then from 2005 on forward, the growth is very significant. And finally, it decelerate slightly in the 2010s.

```python
plt.figure(figsize=(8,5))
x_data, y_data = (df["Year"].values, df["Value"].values)
plt.plot(x_data, y_data, 'ro')
plt.ylabel('GDP')
plt.xlabel('Year')
plt.show()
```

### Choosing a model

From an initial look at the plot, we determine that the logistic function could be a good approximation,
since it has the property of starting with a slow growth, increasing growth in the middle, and then decreasing again at the end; as illustrated below:

```python
X = np.arange(-5.0, 5.0, 0.1)
Y = 1.0 / (1.0 + np.exp(-X))

plt.plot(X,Y)
plt.ylabel('Dependent Variable')
plt.xlabel('Independent Variable')
plt.show()
```

The formula for the logistic function is the following:

$$ \hat{Y} = \frac1{1+e^{\beta_1(X-\beta_2)}}$$

$\beta_1$: Controls the curve's steepness,

$\beta_2$: Slides the curve on the x-axis.

### Building The Model

Now, let's build our regression model and initialize its parameters.

```python
def sigmoid(x, Beta_1, Beta_2):
     y = 1 / (1 + np.exp(-Beta_1*(x-Beta_2)))
     return y
```

Lets look at a sample sigmoid line that might fit with the data:

```python
beta_1 = 0.10
beta_2 = 1990.0

#logistic function
Y_pred = sigmoid(x_data, beta_1 , beta_2)

#plot initial prediction against datapoints
plt.plot(x_data, Y_pred*15000000000000.)
plt.plot(x_data, y_data, 'ro')
```

Our task here is to find the best parameters for our model. Lets first normalize our x and y:

```python
# Lets normalize our data
xdata =x_data/max(x_data)
ydata =y_data/max(y_data)
```

#### How we find the best parameters for our fit line?

we can use **curve_fit** which uses non-linear least squares to fit our sigmoid function, to data. Optimal values for the parameters so that the sum of the squared residuals of sigmoid(xdata, *popt) - ydata is minimized.

popt are our optimized parameters.

```python
from scipy.optimize import curve_fit
popt, pcov = curve_fit(sigmoid, xdata, ydata)
#print the final parameters
print(" beta_1 = %f, beta_2 = %f" % (popt[0], popt[1]))
```

     beta_1 = 690.447527, beta_2 = 0.997207

Now we plot our resulting regression model.

```python
x = np.linspace(1960, 2015, 55)
x = x/max(x)
plt.figure(figsize=(8,5))
y = sigmoid(x, *popt)
plt.plot(xdata, ydata, 'ro', label='data')
plt.plot(x,y, linewidth=3.0, label='fit')
plt.legend(loc='best')
plt.ylabel('GDP')
plt.xlabel('Year')
plt.show()
```

## Practice

Can you calculate what is the accuracy of our model?

```python
# write your code here

msk = np.random.rand(len(df)) < 0.8
train_x = xdata[msk]
test_x = xdata[~msk]
train_y = ydata[msk]
test_y = ydata[~msk]
```

```python
train_x
```

    array([0.97318769, 0.97368421, 0.97418073, 0.97467726, 0.97517378,
           0.97567031, 0.97616683, 0.97666336, 0.97715988, 0.97765641,
           0.97815293, 0.97864945, 0.9796425 , 0.98013903, 0.98113208,
           0.9816286 , 0.98212512, 0.98311817, 0.9836147 , 0.98411122,
           0.98460775, 0.98510427, 0.98560079, 0.98659384, 0.98758689,
           0.98857994, 0.98907646, 0.98957299, 0.99006951, 0.99056604,
           0.99106256, 0.99205561, 0.99255214, 0.99304866, 0.99404171,
           0.99453823, 0.99503476, 0.99602781, 0.99652433, 0.99751738,
           0.9980139 , 0.99851043, 0.99950348, 1.        ])

Click here for the solution

```python
# split data into train/test
msk = np.random.rand(len(df)) < 0.8
train_x = xdata[msk]
test_x = xdata[~msk]
train_y = ydata[msk]
test_y = ydata[~msk]

# build the model using train set
popt, pcov = curve_fit(sigmoid, train_x, train_y)

# predict using test set
y_hat = sigmoid(test_x, *popt)

# evaluation
print("Mean absolute error: %.2f" % np.mean(np.absolute(y_hat - test_y)))
print("Residual sum of squares (MSE): %.2f" % np.mean((y_hat - test_y) ** 2))
from sklearn.metrics import r2_score
print("R2-score: %.2f" % r2_score(y_hat , test_y) )

```

```python
test_x
```

    array([0.97914598, 0.98063555, 0.98262165, 0.98609732, 0.98709037,
           0.98808342, 0.99155909, 0.99354518, 0.99553128, 0.99702085,
           0.99900695])

Want to learn more?

IBM SPSS Modeler is a comprehensive analytics platform that has many machine learning algorithms. It has been designed to bring predictive intelligence to decisions made by individuals, by groups, by systems – by your enterprise as a whole. A free trial is available through this course, available here: SPSS Modeler

Also, you can use Watson Studio to run these notebooks faster with bigger datasets. Watson Studio is IBM's leading cloud solution for data scientists, built by data scientists. With Jupyter notebooks, RStudio, Apache Spark and popular libraries pre-packaged in the cloud, Watson Studio enables data scientists to collaborate on their projects without having to install anything. Join the fast-growing community of Watson Studio users today with a free account at Watson Studio

### Thank you for completing this lab!

## Author

Saeed Aghabozorgi

### Other Contributors

Joseph Santarcangelo

## Change Log

| Date (YYYY-MM-DD) | Version | Changed By | Change Description                 |
| ----------------- | ------- | ---------- | ---------------------------------- |
| 2020-11-03        | 2.1     | Lakshmi    | Made changes in URL                |
| 2020-08-27        | 2.0     | Lavanya    | Moved lab to course repo in GitLab |
|                   |         |            |                                    |
|                   |         |            |                                    |

##
 © IBM Corporation 2020. All rights reserved.

Data Science Certification/Machine Learning with Python/ML0101EN-Reg-NoneLinearRegression-py-v1.ipynb