# OpenFermion hands-on tutorial

OpenFermion is a library for obtaining and manipulating representations of fermionic and qubit Hamiltonians for the quantum simulation of chemistry and materials. This tutorial introduces the core data structures of OpenFermion and some basic functionality.

## Background

A system of $N$ fermionic modes is
described by a set of fermionic *annihilation operators*
$\{a_p\}_{p=0}^{N-1}$ satisfying the *canonical anticommutation relations*
$$\begin{aligned}
    \{a_p, a_q\} &= 0, \\
    \{a_p, a^\dagger_q\} &= \delta_{pq},
  \end{aligned}$$ where $\{A, B\} := AB + BA$. The adjoint
$a^\dagger_p$ of an annihilation operator $a_p$ is called a *creation
operator*, and we refer to creation and annihilation operators as
fermionic *ladder operators*.

The canonical anticommutation relations impose a number of consequences on the structure of the vector space on which the ladder operators act; see [Michael Nielsen's notes](http://michaelnielsen.org/blog/archive/notes/fermions_and_jordan_wigner.pdf) for a good discussion.

## FermionOperator and QubitOperator

### FermionOperator

- Stores a weighted sum (linear combination) of fermionic terms
- A fermionic term is a product of ladder operators
- Examples of FermionOperators:
$$
\begin{align}
& a_1 \nonumber \\
& 1.7 a^\dagger_3 \nonumber \\
&-1.7 \, a^\dagger_3 a_1 \nonumber \\
&(1 + 2i) \, a^\dagger_4 a^\dagger_3 a_9 a_1 \nonumber \\
&(1 + 2i) \, a^\dagger_4 a^\dagger_3 a_9 a_1 - 1.7 \, a^\dagger_3 a_1 \nonumber
\end{align}
$$

- A fermionic term is internally represented as a tuple of tuples
- Each inner tuple represents a single ladder operator as (index, action)
- Examples of fermionic terms:
$$
\begin{align}
I & \mapsto () \nonumber \\
a_1 & \mapsto ((1, 0),) \nonumber \\
a^\dagger_3 & \mapsto ((3, 1),) \nonumber \\
a^\dagger_3 a_1 & \mapsto ((3, 1), (1, 0)) \nonumber \\
a^\dagger_4 a^\dagger_3 a_9 a_1 & \mapsto ((4, 1), (3, 1), (9, 0), (1, 0)) \nonumber
\end{align}
$$

- FermionOperator is a sum of terms, represented as a dictionary from term to coefficient

```python
import openfermion as of

op = of.FermionOperator(((4, 1), (3, 1), (9, 0), (1, 0)), 1+2j) + of.FermionOperator(((3, 1), (1, 0)), -1.7)

print(op.terms)
```

    {((4, 1), (3, 1), (9, 0), (1, 0)): (1+2j), ((3, 1), (1, 0)): -1.7}

Alternative notation, useful when playing around:

$$
\begin{align}
I & \mapsto \textrm{""} \nonumber \\
a_1 & \mapsto \textrm{"1"} \nonumber \\
a^\dagger_3 & \mapsto \textrm{"3^"} \nonumber \\
a^\dagger_3 a_1 & \mapsto \textrm{"3^}\;\textrm{1"} \nonumber \\
a^\dagger_4 a^\dagger_3 a_9 a_1 & \mapsto \textrm{"4^}\;\textrm{3^}\;\textrm{9}\;\textrm{1"} \nonumber
\end{align}
$$

```python
op = of.FermionOperator('4^ 3^ 9 1', 1+2j) + of.FermionOperator('3^ 1', -1.7)

print(op.terms)
```

    {((4, 1), (3, 1), (9, 0), (1, 0)): (1+2j), ((3, 1), (1, 0)): -1.7}

Just print the operator for a nice readable representation:

```python
print(op)
```

    -1.7 [3^ 1] +
    (1+2j) [4^ 3^ 9 1]

Use the `normal_ordered` function to normal-order FermionOperators. Normal-ordering a term means using the anticommutation relations to write the term into an equivalent form where all creation operators precede annihilation operators and higher indices precede lower indices.

```python
op = of.FermionOperator('0 1 3^ 2^')

print(of.normal_ordered(op))
print()

op = of.FermionOperator('0 0^')
print(of.normal_ordered(op))
```

    -1.0 [3^ 2^ 1 0]

    1.0 [] +
    -1.0 [0^ 0]

### Exercises

- Use the `*`, `+`, and `**` operators and the `normal_ordered` function to verify the following:
$$
\begin{align}
    (a_2)^2 &= 0 \\
    a_2 a_7 + a_7 a_2 &= 0 \\
    a_2 a_7^\dagger + a_7^\dagger a_2 &= 0\\
    a_2 a_2^\dagger + a_2^\dagger a_2 &= 1
\end{align}
$$

```python
# Code cell for exercises

```

- Use the `==` operator and the `normal_ordered` function to verify that
$$
a_1^\dagger a_2 a_2^\dagger a_4 = a_1^\dagger a_4 - a_1^\dagger a_2^\dagger a_2 a_4
$$

```python
# Code cell for exercises

```

### QubitOperator

Same as FermionOperator, but the possible actions are 'X', 'Y', and 'Z' instead of 1 and 0.

```python
op = of.QubitOperator(((1, 'X'), (2, 'Y'), (3, 'Z')))
op += of.QubitOperator('X3 Z4', 3.0)

print(op)
```

    1.0 [X1 Y2 Z3] +
    3.0 [X3 Z4]

FermionOperator and QubitOperator actually inherit from the same parent class, SymbolicOperator.

### Exercises

- Use the `commutator` function to verify the following:
$$
\begin{align}
[X_0, Y_0] &= 2 i Z_0 \\
[Z_0, Y_0] &= -2 i X_0 \\
\end{align}
$$

```python
# Code cell for exercises

```

## The Jordan-Wigner and Bravyi-Kitaev transforms

A fermionic transform maps FermionOperators to QubitOperators in a way that preserves the canonical anticommutation relations. The most basic transforms are the Jordan-Wigner transform (JWT) and Bravyi-Kitaev transform (BKT). Note that the BKT requires the total number of qubits to be predetermined. Whenever a fermionic transform is being applied implicitly, it is the JWT.

```python
op = of.FermionOperator('2^ 15')

print(of.jordan_wigner(op))
print()
print(of.bravyi_kitaev(op, n_qubits=16))
```

    (0.25+0j) [X2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 X15] +
    0.25j [X2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 Y15] +
    -0.25j [Y2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 X15] +
    (0.25+0j) [Y2 Z3 Z4 Z5 Z6 Z7 Z8 Z9 Z10 Z11 Z12 Z13 Z14 Y15]

    (-0.25+0j) [Z1 X2 X3 X7 Z15] +
    -0.25j [Z1 X2 X3 Y7 Z11 Z13 Z14] +
    0.25j [Z1 Y2 X3 X7 Z15] +
    (-0.25+0j) [Z1 Y2 X3 Y7 Z11 Z13 Z14]

### Exercises

- Below are some examples of how FermionOperators are mapped to QubitOperators by the Jordan-Wigner transform (the notation 'h.c.' stands for 'hermitian conjugate'):
$$
\begin{align*}
    a_p^\dagger &\mapsto \frac12 (X_p - i Y_p) Z_0 \cdots Z_{p-1}\\
    a_p^\dagger a_p &\mapsto \frac12 (I - Z_p)\\
    (\beta a_p^\dagger a_q + \text{h.c.}) &\mapsto \frac12 [\text{Re}(\beta) (X_p ZZ \cdots ZZ X_q + Y_p ZZ \cdots ZZ Y_q) + \text{Im}(\beta) (Y_p ZZ \cdots ZZ X_q - X_p ZZ \cdots ZZ Y_q)]
\end{align*}
$$
Verify these mappings for $p=2$ and $q=7$. The `hermitian_conjugated` function may be useful here.

```python
# Code cell for exercises

```

- Verify that after applying the JWT to ladder operators, the resulting QubitOperators still satisfy
$$
\begin{align}
    (a_2)^2 &= 0 \\
    a_2 a_7 + a_7 a_2 &= 0 \\
    a_2 a_7^\dagger + a_7^\dagger a_2 &= 0\\
    a_2 a_2^\dagger + a_2^\dagger a_2 &= 1
\end{align}
$$
You can use the `anticommutator` function.

```python
# Code cell for exercises

```

## Sparse matrices for numerical computations

- QubitOperators can be converted to Scipy sparse matrices using `get_sparse_operator`
- Sparse matrices can be used to obtain eigenvalues, expectation values, etc.
- As a demonstration, we will compute the ground state energy of a Hubbard model Hamiltonian:
$$
H = - t \sum_{\langle i,j \rangle} \sum_{\sigma}
             (a^\dagger_{i, \sigma} a_{j, \sigma} +
              a^\dagger_{j, \sigma} a_{i, \sigma})
     + U \sum_{i} a^\dagger_{i, \uparrow} a_{i, \uparrow}
                  a^\dagger_{j, \downarrow} a_{j, \downarrow}
$$
where $\langle i, j \rangle$ ranges over the edges in a 2-dimensional grid and $\sigma$ is either spin up or spin down.
- The Hubbard model is an idealized Hamiltonian model that may capture qualitative aspects of high-temperature superconductors.

```python
# Set Hubbard model parameters
x_dim = 2
y_dim = 2
tunneling = 1.0
coulomb = 4.0

# Create Hubbard model Hamiltonian as FermionOperator
hubbard_model = of.fermi_hubbard(x_dim, y_dim, tunneling, coulomb)

print(hubbard_model)
```

    4.0 [0^ 0 1^ 1] +
    -1.0 [0^ 2] +
    -1.0 [0^ 4] +
    -1.0 [1^ 3] +
    -1.0 [1^ 5] +
    -1.0 [2^ 0] +
    4.0 [2^ 2 3^ 3] +
    -1.0 [2^ 6] +
    -1.0 [3^ 1] +
    -1.0 [3^ 7] +
    -1.0 [4^ 0] +
    4.0 [4^ 4 5^ 5] +
    -1.0 [4^ 6] +
    -1.0 [5^ 1] +
    -1.0 [5^ 7] +
    -1.0 [6^ 2] +
    -1.0 [6^ 4] +
    4.0 [6^ 6 7^ 7] +
    -1.0 [7^ 3] +
    -1.0 [7^ 5]

```python
# Map to QubitOperator using the JWT
hubbard_jw = of.jordan_wigner(hubbard_model)

print(hubbard_jw)
```

    (4+0j) [] +
    (-0.5+0j) [X0 Z1 X2] +
    (-0.5+0j) [X0 Z1 Z2 Z3 X4] +
    (-0.5+0j) [Y0 Z1 Y2] +
    (-0.5+0j) [Y0 Z1 Z2 Z3 Y4] +
    (-1+0j) [Z0] +
    (1+0j) [Z0 Z1] +
    (-0.5+0j) [X1 Z2 X3] +
    (-0.5+0j) [X1 Z2 Z3 Z4 X5] +
    (-0.5+0j) [Y1 Z2 Y3] +
    (-0.5+0j) [Y1 Z2 Z3 Z4 Y5] +
    (-1+0j) [Z1] +
    (-0.5+0j) [X2 Z3 Z4 Z5 X6] +
    (-0.5+0j) [Y2 Z3 Z4 Z5 Y6] +
    (-1+0j) [Z2] +
    (1+0j) [Z2 Z3] +
    (-0.5+0j) [X3 Z4 Z5 Z6 X7] +
    (-0.5+0j) [Y3 Z4 Z5 Z6 Y7] +
    (-1+0j) [Z3] +
    (-0.5+0j) [X4 Z5 X6] +
    (-0.5+0j) [Y4 Z5 Y6] +
    (-1+0j) [Z4] +
    (1+0j) [Z4 Z5] +
    (-0.5+0j) [X5 Z6 X7] +
    (-0.5+0j) [Y5 Z6 Y7] +
    (-1+0j) [Z5] +
    (-1+0j) [Z6] +
    (1+0j) [Z6 Z7] +
    (-1+0j) [Z7]

```python
# Convert to Scipy sparse matrix
hubbard_jw_sparse = of.get_sparse_operator(hubbard_jw)

# Compute ground state energy
ground_energy_jw, _ = of.get_ground_state(hubbard_jw_sparse)

print(ground_energy_jw)
```

    -3.418550718873853

### Exercises

- Compute the ground energy of the same Hamiltonian, but via the Bravyi-Kitaev transform. Verify that you get the same value.

```python
# Code cell for exercises

```

## Array data structures

- When FermionOperators have specialized structure we can store coefficients in numpy arrays, enabling easier numerical manipulation.
- Array data structures can always be converted to FermionOperator using `get_fermion_operator`.

### InteractionOperator

- Stores the one- and two-body tensors $T_{pq}$ and $V_{pqrs}$ of the molecular Hamiltonian
$$
\sum_{pq} T_{pq} a_p^\dagger a_q + \sum_{pqrs} V_{pqrs} a_p^\dagger a_q^\dagger a_r a_s
$$
- Default data structure for molecular Hamiltonians
- Convert from FermionOperator using `get_interaction_operator`

### DiagonalCoulombHamiltonian

- Stores the one- and two-body coefficient matrices $T_{pq}$ and $V_{pq}$ of a Hamiltonian with a diagonal Coulomb term:
$$
\sum_{pq} T_{pq} a_p^\dagger a_q + \sum_{pq} V_{pq} a_p^\dagger a_p a_q^\dagger a_q
$$
- Leads to especially efficient algorithms for quantum simulation
- Convert from FermionOperator using `get_diagonal_coulomb_hamiltonian`

### QuadraticHamiltonian

- Stores the Hermitian matrix $M_{pq}$ and antisymmetric matrix $\Delta_{pq}$ describing a general quadratic Hamiltonian
$$
\sum_{p, q} M_{pq} a^\dagger_p a_q
+ \frac12 \sum_{p, q}
    (\Delta_{pq} a^\dagger_p a^\dagger_q + \text{h.c.})
$$
- Routines included for efficient diagonalization (can handle thousands of fermionic modes)
- Convert from FermionOperator using `get_quadratic_hamiltonian`

The Hubbard model has the form of a DiagonalCoulombHamiltonian, so it can be converted to one. OpenFermion-Cirq contains specialized low-depth Hamiltonian simulation algorithms which take this data structure as input.

```python
hubbard_hamiltonian = of.get_diagonal_coulomb_hamiltonian(hubbard_model)

print(hubbard_hamiltonian.one_body)
print()
print(hubbard_hamiltonian.two_body)
print()
print(hubbard_model)
```

    [[ 0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j]
     [ 0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j]
     [-1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j]
     [ 0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j]
     [-1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j]
     [ 0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j  0.+0.j -1.+0.j]
     [ 0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j  0.+0.j]
     [ 0.+0.j  0.+0.j  0.+0.j -1.+0.j  0.+0.j -1.+0.j  0.+0.j  0.+0.j]]

    [[0. 2. 0. 0. 0. 0. 0. 0.]
     [2. 0. 0. 0. 0. 0. 0. 0.]
     [0. 0. 0. 2. 0. 0. 0. 0.]
     [0. 0. 2. 0. 0. 0. 0. 0.]
     [0. 0. 0. 0. 0. 2. 0. 0.]
     [0. 0. 0. 0. 2. 0. 0. 0.]
     [0. 0. 0. 0. 0. 0. 0. 2.]
     [0. 0. 0. 0. 0. 0. 2. 0.]]

    4.0 [0^ 0 1^ 1] +
    -1.0 [0^ 2] +
    -1.0 [0^ 4] +
    -1.0 [1^ 3] +
    -1.0 [1^ 5] +
    -1.0 [2^ 0] +
    4.0 [2^ 2 3^ 3] +
    -1.0 [2^ 6] +
    -1.0 [3^ 1] +
    -1.0 [3^ 7] +
    -1.0 [4^ 0] +
    4.0 [4^ 4 5^ 5] +
    -1.0 [4^ 6] +
    -1.0 [5^ 1] +
    -1.0 [5^ 7] +
    -1.0 [6^ 2] +
    -1.0 [6^ 4] +
    4.0 [6^ 6 7^ 7] +
    -1.0 [7^ 3] +
    -1.0 [7^ 5]

The cell below demonstrates using one of our electronic structure package plugins, OpenFermion-PySCF, to generate a molecular Hamiltonian for a hydrogen molecule.

```python
import openfermionpyscf as ofpyscf

# Set molecule parameters
geometry = [('H', (0.0, 0.0, 0.0)), ('H', (0.0, 0.0, 0.8))]
basis = 'sto-3g'
multiplicity = 1
charge = 0

# Perform electronic structure calculations and
# obtain Hamiltonian as an InteractionOperator
hamiltonian = ofpyscf.generate_molecular_hamiltonian(
    geometry, basis, multiplicity, charge)

# Convert to a FermionOperator
hamiltonian_ferm_op = of.get_fermion_operator(hamiltonian)

print(hamiltonian_ferm_op)
```

    0.66147151365 [] +
    -1.2178260299951058 [0^ 0] +
    0.3316650744318082 [0^ 0^ 0 0] +
    0.09231339177803066 [0^ 0^ 2 2] +
    0.3316650744318082 [0^ 1^ 1 0] +
    0.09231339177803066 [0^ 1^ 3 2] +
    0.09231339177803066 [0^ 2^ 0 2] +
    0.3267206861819477 [0^ 2^ 2 0] +
    0.09231339177803066 [0^ 3^ 1 2] +
    0.3267206861819477 [0^ 3^ 3 0] +
    0.3316650744318082 [1^ 0^ 0 1] +
    0.09231339177803066 [1^ 0^ 2 3] +
    -1.2178260299951058 [1^ 1] +
    0.3316650744318082 [1^ 1^ 1 1] +
    0.09231339177803066 [1^ 1^ 3 3] +
    0.09231339177803066 [1^ 2^ 0 3] +
    0.3267206861819477 [1^ 2^ 2 1] +
    0.09231339177803066 [1^ 3^ 1 3] +
    0.3267206861819477 [1^ 3^ 3 1] +
    0.32672068618194783 [2^ 0^ 0 2] +
    0.09231339177803066 [2^ 0^ 2 0] +
    0.32672068618194783 [2^ 1^ 1 2] +
    0.09231339177803066 [2^ 1^ 3 0] +
    -0.5096378744364826 [2^ 2] +
    0.09231339177803066 [2^ 2^ 0 0] +
    0.34339576784573445 [2^ 2^ 2 2] +
    0.09231339177803066 [2^ 3^ 1 0] +
    0.34339576784573445 [2^ 3^ 3 2] +
    0.32672068618194783 [3^ 0^ 0 3] +
    0.09231339177803066 [3^ 0^ 2 1] +
    0.32672068618194783 [3^ 1^ 1 3] +
    0.09231339177803066 [3^ 1^ 3 1] +
    0.09231339177803066 [3^ 2^ 0 1] +
    0.34339576784573445 [3^ 2^ 2 3] +
    -0.5096378744364826 [3^ 3] +
    0.09231339177803066 [3^ 3^ 1 1] +
    0.34339576784573445 [3^ 3^ 3 3]

### Exercises

- Create a FermionOperator that is quadratic and Hermitian and use `get_quadratic_hamiltonian` to convert it to a QuadraticHamiltonian.

```python
# Code cell for exercises

```

- The BCS mean-field d-wave model of superconductivity has the Hamiltonian
$$
H = - t \sum_{\langle i,j \rangle} \sum_\sigma
        (a^\dagger_{i, \sigma} a_{j, \sigma} +
         a^\dagger_{j, \sigma} a_{i, \sigma})
    - \sum_{\langle i,j \rangle} \Delta_{ij}
      (a^\dagger_{i, \uparrow} a^\dagger_{j, \downarrow} -
       a^\dagger_{i, \downarrow} a^\dagger_{j, \uparrow} +
       a_{j, \downarrow} a_{i, \uparrow} -
       a_{j, \uparrow} a_{i, \downarrow})
$$
Use the `mean_field_dwave` function to generate an instance of this model with dimensions 10x10 (Tip: Type `of.mean_field_dwave` and then press Shift-Tab. This will reveal the function arguments.)
    - Convert the Hamiltonian to a QubitOperator with the JWT. What is the length of the longest Pauli string that appears?
    - Convert the Hamiltonian to a QubitOperator with the BKT. What is the length of the longest Pauli string that appears?
    - Convert the Hamiltonian to a QuadraticHamiltonian. Get its ground energy using the `ground_energy` method of QuadraticHamiltonian. What would happen if you tried to compute the ground energy by converting to a sparse matrix?

```python
# Code cell for exercises

```

## Solutions to exercises

```python
# Solutions to exercises

a2 = of.FermionOperator('2')
a2dag = of.FermionOperator('2^')
a7 = of.FermionOperator('7')
a7dag = of.FermionOperator('7^')

print(of.normal_ordered(a2**2))
print(of.normal_ordered(a2*a7 + a7*a2))
print(of.normal_ordered(a2*a7dag + a7dag*a2))
print(of.normal_ordered(a2*a2dag + a2dag*a2))
print()

# ----

op1 = of.FermionOperator('1^ 2 2^ 4')
op2 = of.FermionOperator('1^ 4') - of.FermionOperator('1^ 2^ 2 4')

print(of.normal_ordered(op1) == of.normal_ordered(op2))
print()

# ----

X = of.QubitOperator('X0')
Y = of.QubitOperator('Y0')
Z = of.QubitOperator('Z0')

print(of.commutator(X, Y))
print(of.commutator(Z, Y))
print()

# ----

print(of.jordan_wigner(a2dag))
print()
print(of.jordan_wigner(a2dag*a2))
print()

op = (2+3j)*a2dag*a7
op += of.hermitian_conjugated(op)
print(of.jordan_wigner(op))
print()

# ----

a2_jw = of.jordan_wigner(a2)
a2dag_jw = of.jordan_wigner(a2dag)
a7_jw = of.jordan_wigner(a7)
a7dag_jw = of.jordan_wigner(a7dag)

print(of.anticommutator(a2_jw, a7_jw))
print(of.anticommutator(a2_jw, a7dag_jw))
print(of.anticommutator(a2_jw, a2dag_jw))
print()

# ----

hubbard_bk = of.bravyi_kitaev(hubbard_model)
hubbard_bk_sparse = of.get_sparse_operator(hubbard_bk)
ground_energy_bk, _ = of.get_ground_state(hubbard_bk_sparse)

print(ground_energy_bk)
print()

# ----

op = of.FermionOperator('0^ 1') + of.FermionOperator('1^ 0')
quad_ham = of.get_quadratic_hamiltonian(op)

# ----

mean_field_model = of.mean_field_dwave(10, 10, 1.0, 1.0)

mean_field_jw = of.jordan_wigner(mean_field_model)
print(max(len(term) for term in mean_field_jw.terms))

mean_field_bk = of.bravyi_kitaev(mean_field_model)
print(max(len(term) for term in mean_field_bk.terms))

mean_field_quad_ham = of.get_quadratic_hamiltonian(mean_field_model)
print(mean_field_quad_ham.ground_energy())
```

    0
    0
    0
    1.0 []

    True

    2j [Z0]
    -2j [X0]

    0.5 [Z0 Z1 X2] +
    -0.5j [Z0 Z1 Y2]

    (0.5+0j) [] +
    (-0.5+0j) [Z2]

    (1+0j) [X2 Z3 Z4 Z5 Z6 X7] +
    (-1.5+0j) [X2 Z3 Z4 Z5 Z6 Y7] +
    (1.5+0j) [Y2 Z3 Z4 Z5 Z6 X7] +
    (1+0j) [Y2 Z3 Z4 Z5 Z6 Y7]

    0
    0
    (1+0j) []

    -3.4185507188738278

    182
    15
    -206.99159932863176


Osnabrück University - Machine Learning (Summer Term 2018) - Prof. Dr.-Ing. G. Heidemann, Ulf Krumnack

# Exercise Sheet 05

## Introduction

This week's sheet should be solved and handed in before the end of **Sunday, May 13, 2018**. If you need help (and Google and other resources were not enough), feel free to contact your groups designated tutor or whomever of us you run into first. Please upload your results to your group's studip folder.

## Assignment 0: Math recap (Derivatives in higher dimensions) [2 Bonus Points]

This exercise is supposed to be very easy, does not give any points, and is voluntary. There will be a similar exercise on every sheet. It is intended to revise some basic mathematical notions that are assumed throughout this class and to allow you to check if you are comfortable with them. Usually you should have no problem to answer these questions offhand, but if you feel unsure, this is a good time to look them up again. You are always welcome to discuss questions with the tutors or in the practice session. Also, if you have a (math) topic you would like to recap, please let us know.

**a)** What is a partial derivative? What is a directional derivative? How are these computed?

Let $f$ be a function $f: \mathbb{R}^n \rightarrow \mathbb{R}, \vec{x}=(x_1, ..., x_n) \rightarrow f(x_1, ..., x_n)$.

A **partial derivative** is then a derivative of $f$ with respect to only one $x_j$ : $$\frac{\partial f}{\partial x_j}$$.
This means you assume all other $x_i$ to be constants while deriving $f$.

The **gradient** now is just a vector with all partial derivatives:
$$ \nabla f = (\frac{\partial f}{\partial x_1}, ..., \frac{\partial f}{\partial x_n}) $$

Now we can write the **partial derivative** as $$\frac{\partial f}{\partial x_j} = \vec{e_j} \cdot \nabla f$$ where $e_j = (0,..,1,..,0)$ is the jth vector of the standard basis.

The directional derivative is now the generalization of the equation above, allowing us to compute the derivative in every direction (not only in the directions of the vectors of the standard basis):  $$\nabla_v f = v \cdot \nabla f$$

**b)** What is the gradient, the Jacobian matrix, and the Hessian matrix? How are they computed?

**Gradient** is already explained above.

The **Jacobian matrix** generalizes the concept of the gradient to the case of having a function $$f: \mathbb{R}^n \rightarrow \mathbb{R}^m, \vec{x}=(x_1, ..., x_n) \rightarrow \begin{pmatrix} f_1(x_1,..,x_n) \\ : \\ f_m(x_1, ..,x_n) \end{pmatrix}$$ It is defined as
$$ J = \begin{pmatrix} \frac{\partial f_1}{\partial x_1} & ... & \frac{\partial f_1}{\partial x_n} \\
:  &  \ &  : \\
\frac{\partial f_m}{\partial x_1} & ... & \frac{\partial f_m}{\partial x_n} \end{pmatrix} $$

For the **Hessian matrix** we are now back at a function $f: \mathbb{R}^n \rightarrow \mathbb{R}, \vec{x}=(x_1, ..., x_n) \rightarrow f(x_1, ..., x_n)$. The Hessian matrix contains all secocd-order partial derivatives. So the (i,j)-th entry of the Hessian is $$ H_{ij} = \frac{\partial^2 f}{\partial x_i \partial x_j},$$ meaning you first derive w.r.t. $x_j$ and then $x_i$.

**c)** What is the chain rule (in calculus)? How does it look in the higher-dimensional case?

The chain rule is a rule for deriving the composition of functions.
In it's simplest form it states if $h(x) = f(g(x))$ then $$ h'(x) = f'(g(x)) \cdot g'(x)$$
In higher dimensions we this is
$$ J_h(x) = J_f(g(x)) \cdot J_g(x)$$

## Assignment 1: Curse of Dimensionality [6 Points]

For the following exercise, be detailed in your answers and provide some examples. Think about keywords like: random vectors in high dimensional space, manifolds and Bertillonage.

**a)** What are the curse of dimensionality and its implication for pattern classification?

Curse of dimensionality describes the phenomenon that in high dimensional vector spaces, two randomly drawn vectors will almost always be close to orthogonal to each other. This is a real problem in data mining problems, where for a higher number of features, the number of possible combinations and therefore the volume of the resulting feature space exponentionally increases.

In such a high dimensional space, data vectors from real data sets lie far away from each other (which means dense sampling becomes impossible, as there aren't enough samples close to each other). This also leads to the problem that pairs of data vectors have a high probability of having similar distances and to be close to orthogonal to each other. The result is that clustering becomes really difficult, as the vectors more or less stand on their own and distance measures cannot be applied easily.

**b)** Explain how this phenomenom could be used to one's advantage.

This is actually an advantage if you want to discriminate between a high number of individuals (see Bertillonage, where using only 11 features results in a feature space big enough to discriminate humans), but if you want to get usable information out of data, such a 'singling out' of samples is a great disadvantage.

**c)** Explain in your own words the concepts of descriptive and intrinsic dimensionality.

Intrinsic dimensionality exists in contrast to the descriptive dimensionality of data, which is defined by the numbers of parameters used to produce or represent the raw data (i.e. the number of pixels in an unprocessed image).

Additionally to this representive dimensionality, there is also a (most of the time smaller) number of independent parameters which is necessary to describe the data, always in regard to a specific problem we want to use the data on.
For example: a data set might consist of a number of portraits, all with size 1920x1080 pixels, which constitutes their descriptive dimensionality. To do some facial recognition on these portraits however, we do not need the complete descriptive dimension space (which would be way too big anyway), but only a few independent parameters (which we can get by doing PCA and looking at the eigenfaces).
This is possible because the data never fill out the entire high dimensional vector space but instead concentrate along a manifold of a much lower dimensionality.

## Assignment 2: Implement and Apply PCA [8 Points]

In this assignment you will implement PCA from the ground up and apply it to the `cars` dataset (simplified from the JSE [2004 New Car and Truck Data](http://www.amstat.org/publications/jse/jse_data_archive.htm)). This dataset consists of measurements taken on 97 different cars. The eleven features measured are: Suggested retail price (USD), Price to dealer (USD), Engine size (liters), Number of engine cylinders, Engine horsepower, City gas mileage, Highway gas mileage, Weight (pounds), Wheelbase (inches), Length (inches) and Width (inches).

We would like to visualize these high dimensional features to get a feeling for how the cars relate to each other so we need to find a subspace of dimension two or three into which we can project the data.

```python
import numpy as np

# TODO: Load the cars dataset in cars.csv .
### BEGIN SOLUTION
cars = np.loadtxt('cars.csv', delimiter=',')
### END SOLUTION

assert cars.shape == (97, 11), "Shape is not (97, 11), was {}".format(cars.shape)
```

Excecute the following code which will create a scatter plot matrix (it might take some time to execute). This should give you an idea about trends and correlations in the dataset.

```python
%matplotlib inline
import pandas as pd
import seaborn as sns
sns.set()
cols = ['Suggested retail price (USD)', 'Price to dealer (USD)',
          'Engine size (liters)', 'Number of engine cylinders',
          'Engine horsepower', 'City gas mileage' ,
          'Highway gas mileage', 'Weight (pounds)',
          'Wheelbase (inches)', 'Length (inches)', 'Width (inches)']

df = pd.DataFrame(cars, columns=cols)
sns.pairplot(df)
```

As a first step we need to normalize the data such that they have a zero mean and a unit standard deviation. Use the standard score for this:
$$\frac{X - \mu}{\sigma}$$

```python
import numpy as np

# TODO: Normalize the data and store them in a variable called cars_norm.
### BEGIN SOLUTION
cars_norm = (cars - np.mean(cars, axis=0)) / np.std(cars, axis=0)

# Alternatively one could use:
# import sklearn.preprocessing
# cars_norm = sklearn.preprocessing.scale(cars)
### END SOLUTION

assert cars_norm.shape == (97, 11), "Shape is not (97, 11), was {}".format(cars.shape)
assert np.abs(np.sum(cars_norm)) < 1e-10, "Absolute sum was {} but should be close to 0".format(np.abs(np.sum(cars_norm)))
assert np.abs(np.sum(cars_norm ** 2) / cars_norm.size - 1) < 1e-10, "The data is not normalized, sum/N was {} not 1".format(np.sum(cars_norm ** 2) / cars_norm.size)
```

PCA finds a subspace that maximizes the variance by determining the eigenvectors of the covariance matrix. So we need to calculate the autocovariance matrix and afterwards the eigenvalues. When the data is normalized the autocovariance is calculated as
$$C = X^T\cdot X$$
with $X$ being an $m \times n$ matrix with $n$ features and $m$ samples.
The entry $c_{i,j}$ in $C$ tells you how much feature $i$ correlates with feature $j$.
(Note: sometimes the formula $C=X\cdot X^T$ can be found, i.e., with rows and columns swapped. This depends on whether your put the individual datapoints as rows or columns in you matrix $X$. However, in the end you want to know how the individual features correlate, i.e., in our example you want a $11\times11$-matrix).

```python
import numpy as np

# TODO: Compute the autocovariance matrix and store it into autocovar
### BEGIN SOLUTION
autocovar = cars_norm.T @ cars_norm
### END SOLUTION

assert autocovar.shape == (11, 11)

# TODO: Compute the eigenvalues und eigenvectors and store them into eigenval and eigenvec
#       (Figure out a function to do this for you)
### BEGIN SOLUTION
eigenval, eigenvec = np.linalg.eig(autocovar)

# Alternatively, np.linalg.eig solves the eigenvector problem for general matrices, while eigh
# only solves it for symmetric/hermitian matrices. (The auto-covariance matrix is always symmetric.)

# eigenval, eigenvec = np.linalg.eig(autocovar)

# If you wanted to use the singular value decomposition (SVD), you would have to use the centered
# version of cars:

# u, s, v = np.linalg.svd(cars-np.mean(cars, 0))
# eigenval, eigenvec = s ** 2, v

# However, this method usually leads to different results (there are some numerical issues).
# For a more detailed overview over different methods of PCA check the file "PCA Comparison.ipynb".
# In most cases these difference don't matter much, but it is always wise to handle your results
# with a certain critical eye.
### END SOLUTION

assert eigenval.shape == (11,)

assert eigenvec.shape == (11, 11)
```

Plot the spectrum of the eigenvalues to make sure that they are sorted by their magnitude. How many principal components should you include based on the spectrum plot?

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

### BEGIN SOLUTION
# Sort eigenvectors (and -values) by descending order of eigenvalues.
sort = np.argsort(-eigenval)
eigenval = eigenval[sort]
eigenvec = eigenvec[:,sort]

# To get an idea of the eigenvalues we plot them.
figure = plt.figure('Eigenvalue comparison')
plt.bar(np.arange(len(eigenval)), eigenval)
### END SOLUTION
```

Now you should have a matrix full of eigenvectors. We can now do two things: project the data down onto the two dimensional subspace to visualize it and we can also plot the two first principle component vectors as eleven two dimensional points to get a feeling for how the features are projected into the subspace. Execute the cells below and describe what you see. Is PCA a good method for this problem? Was it justifiable that we only considered the first two principle components? What kinds of cars are in the four quadrants of the first plot? (**put your answer in the cell below of this code cell**)

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

# Project the data down into the two dimensional subspace
proj = cars_norm @ eigenvec[:,0:2]

# Plot projected data
fig = plt.figure('Data projected onto first two Principal Components')
fig.gca().set_xlim(-8, 8)
fig.gca().set_ylim(-4, 7)
plt.scatter(proj[:,0], proj[:,1])
# Divide plot into quadrants
plt.axhline(0, color='green')
plt.axvline(0, color='green')
# force drawing on 'run all'
fig.canvas.draw()

# Plot eigenvectors
eig_fig = plt.figure('Eigenvector plot')
plt.scatter(eigenvec[:,0], eigenvec[:,1])

# add labels
labels = ['Suggested retail price (USD)', 'Price to dealer (USD)',
          'Engine size (liters)', 'Number of engine cylinders',
          'Engine horsepower', 'City gas mileage' ,
          'Highway gas mileage', 'Weight (pounds)',
          'Wheelbase (inches)', 'Length (inches)', 'Width (inches)']
for label, x, y in zip(labels, eigenvec[:,0], eigenvec[:,1]):
    plt.annotate(
        label, xy = (x, y), xytext = (-20, 20),
        textcoords = 'offset points', ha = 'left', va = 'bottom',
        bbox = dict(boxstyle = 'round,pad=0.5', fc = 'blue', alpha = 0.5),
        arrowprops = dict(arrowstyle = '->', connectionstyle = 'arc3,rad=0'))
# force drawing on 'run all'
eig_fig.canvas.draw()
```

#### PCA is good
The first plot shows the complete dataset projected down onto the two first principle components. Only few points overlap and the points are generally spread out well in the subspace. There is not much trend in the plot which is what we desired, i.e. the axes are not redundant. No clusters can be recognized.

#### The first two PCs are good
It is admissible to pick two dimensions, although only the first eigenvector has a very high magnitude in comparison.

The 1D plot, which might be better if we take the eigenvalues into account, yields little information in the data on a visual level. However, it gives a better idea of how the original features will be distributed in the space.

The 3D plot is already very hard to grasp by just taking a look at it. So 2D seems to be a good choice.

In general there are several different strategies to decide the number of dimensions onto which you want to project your data. Here is a short overview over just a few common choices:

- Eigenvalue magnitudes: Find the cut-off depth. This is useful for classification problems, especially
  for problems to be solved by computers.
- Visualization: Choose the number of dimensions which is useful to visualize the data in a meaningful way. This
  choice depends a lot on your problem definition. For printing 2D is usually a good choice - but maybe your data
  is just very nice for 1D already. Or maybe you are using a glyph plot (see sheet 06) which can represent high
  dimensional data.
- Classification results: In the Eigenfaces assignment below we figured out that the number of principal
  components (and thus the number of dimensions) can have a crucial impact on classification rates. It is thus
  an option to fine tune the number of dimensions for a good classification result on some training set.

#### Interpretation of the plot is very subjective
Let's first take a look at the second plot. Each of the eleven points denotes one of the original base vectors. If you would draw arrows from the origin to each of the eleven points, you would draw projections of the original axes.

To give it a little bit more meaning, let's investigate this further. Let's take a car with six cylinders and all other values on average. We can change the number of cylinders and see how it moves along the cylinder axis (blue arrow).

As you can see, the six cylinder car is close to the average (i.e. close to the center), while the eight cylinder car is further away in the positive cylinder direction and the two cylinder car is further away in the negative direction.

If we now increase (or decrease) the city gas mileage of the eight cylinder car by 30%, the car will move along the mileage axis (orange arrow).

The dashed arrows indicate the linear combination of the two features for the eight cylinder car with higher city gas mileage. You can see how they are parallel to the respective axes.

Taking all that into account we can say:

- **Top right**: Cars with high gas mileages. This might be limousines.
- **Bottom right**: Cars with low prices and low power, but average sizes and higher gas mileages. This might also be
  limousines, but smaller ones.
- **Bottom left**: Cars with big measurements and average pricing. This might be family cars.
- **Top left**: Cars with considerably high power and prices which are still light and small.
  This might be sports cars.

Note that this interpretation is just describing the general trend. Due to the nature of linear combinations, it is easily possible to come up with a car which has some exceptional values which lead to cancellation of others.

Note also that depending on the method used to calculate the eigenvectors, your axes and thus your interpretation might slightly differ.

## Assignment 3: PCA [6 Points]

In this exercise we investigate the statement from the lecture that PCA finds the subspace that captures most of the data variance. To be more precise, we show that the orthonormal projection onto an $m$-dimensional subspace that maximizes the variance of the projected data is defined by the principal components, i.e. by the $m$ eigenvectors of the autocorrelation matrix $C$ corresponding to the $m$ largest eigenvalues. We proceed in two steps:

### a)

First consider a one dimensional subspace: determine a (unit) vector $\vec{p}$, such that the variance of the data, when projected onto the subspace determined by that vector, is maximal.

The autocorrelation matrix $C$ allows to compute the variance of the projected data as $\vec{p}^{T}C\vec{p}$. We want to maximize this expression. To avoid $\|\vec{p}\|\to\infty$ we will only consider unit vectors, i.e. we constrain $\vec{p}$ to be normalized: $\vec{p}^T\vec{p}=1$. Maximize the expression with this constraint (which can be done using a Lagrangian multiplier). Conclude that a suitable $\vec{p}$ has to be an eigenvector of $C$ and describe which of the eigenvectors is optimal.

**Solution:**
We want to maximize the expression
$$\vec{p}^T C\vec{p} + \lambda(1-\vec{p}^T\vec{p})$$
with respect to $\vec{p}$, i.e. we have to find solutions for
$$\frac{\partial}{\partial\vec{p}}\left[ \vec{p}^T C\vec{p} + \lambda(1-\vec{p}^T\vec{p})\right] = 0$$
This leads to the equation
$$C\vec{p} = \lambda\vec{p}$$
in other words: for a vector $\vec{p}$ to maximize our expression, it has to be an eigenvector $C$ and $\lambda$ has to be the corresponding eigenvalue.
By left multiplying with $\vec{p}^T$ and using the fact that $\vec{p}^T\vec{p}=1$, we gain
$$\vec{p}^TC\vec{p}=\lambda$$
i.e. the projected variance will correspond to the eigenvalue $\lambda$ and hence is maximized when choosing the largest eigenvalue.

### b)

Now proof the statement for the general case of an $m$-dimensional projection space.

Use an inductive argument: assume the statement has been shown for the $(m-1)$-dimensional projection space, spanned by the $m-1$ (orthonormal) eigenvectors $\vec{p}_1,\ldots,\vec{p}_{m-1}$ corresponding to the $(m-1)$ largest eigenvalues $\lambda_1,\ldots,\lambda_{m-1}$. Now find a (unit) vector $\vec{p}_m$, orthogonal to the existing vectors $\vec{p}_1,\ldots,\vec{p}_{m-1}$, that maximizes the projected variance $\vec{p}_m^TC\vec{p}_m$. Proceed similar to case (a), but with additional Lagrangian multipliers to enforce the orthogonality constraint. Show that the new vector $\vec{p}_m$ is an eigenvector of $C$. Finally show that the variance is maximized for the eigenvector corresponding to the $m$-th largest eigenvalue $\lambda_m$.

**Solution:** Assume that the result holds for projection spaces of dimensionality $m-1$. We will now show that it then also holds for dimensionality $m$: we consider a subspace spanned by the $m-1$ (orthonormal) eigenvectors $\vec{p}_1,\ldots,\vec{p}_{m-1}$ corresponding to the $(m-1)$ largest eigenvalues $\lambda_1,\ldots,\lambda_{m-1}$, and a new vector $\vec{p}_{m}$ whos properties we will now examine. First, this vector should be linearly independent from $\vec{p}_1,\ldots,\vec{p}_{m-1}$, as it should define the new $m$-th dimension. The property can be enforced by the (stronger) requirement that $\vec{p}_{m}$ should be orthogonal to $\vec{p}_1,\ldots,\vec{p}_{m-1}$, i.e.
$$\vec{p}_m^T\vec{p}_{i}=0 \text{ for } i=1,\ldots,m-1,$$
which can be expressed using Lagrange multipliers $\eta_1,\ldots,\eta_{m-1}$. As argued in part (a), the variance in direction $\vec{p}_m$ is given by
$$\vec{p}_{m}^TC\vec{p}_{m}.$$
We want to maximize this value, again with the additional constraint that $\vec{p}_{m}$ is normalized, i.e.
$$\vec{p}_{m}^T\vec{p}_m=1,$$
which will be expressed by an additional Lagrange multiplier $\lambda_M$. So in total we want to maximize the function
$$\vec{p}_{m}^TC\vec{p}_{m} + \sum_{i=1}^{m-1}\eta_i\vec{p}_m^T\vec{p}_{i} + \lambda_m(1-\vec{p}_{m}^T\vec{p}_{m})$$
with respect to $\vec{p}_m$, i.e. we have to find solutions for
\begin{align}
  0
  & = \frac{\partial}{\partial\vec{p}_m}\left[\vec{p}_{m}^TC\vec{p}_{m}
  + \sum_{i=1}^{m-1}\eta_i\vec{p}_m^T\vec{p}_{i}
  + \lambda_m(1-\vec{p}_{m}^T\vec{p}_m)\right] \\
  & = 2C\vec{p}_m + \sum_{i=1}^{m-1}\eta_i\vec{p}_{i} - 2\lambda_m\vec{p}_{m}
\end{align}
Multiplying this equation with $\vec{p}_{j}^T$ from the left yields (due to the orthogonality constraint)
\begin{align}
  0 = \vec{p}_{j}^T 0
  & = \vec{p}_{j}^T 2C\vec{p}_m +
  \vec{p}_{j}^T \sum_{i=1}^{m-1}\eta_i\vec{p}_{i} -
  \vec{p}_{j}^T 2\lambda_m\vec{p}_{m} \\
  &= 0 + \eta_j\vec{p}_{j}^T \vec{p}_{j}- 0 \\
  & = \eta_j
\end{align}
for $j=1,\ldots,m-1$. So the problem simplifies to
$$0 = 2C\vec{p}_m - 2\lambda_m\vec{p}_{m}$$
from which we see that a critical point of the Lagrange equation has to fulfill
$$C\vec{p}_m =\lambda_m\vec{p}_{m}$$
which just means it has to be an eigenvector of the matrix $C$ with eigenvalue $\lambda_M$. There may be multiple eigenvectors for $C$, so we have to select $\vec{p}_m$ in a way that it maximizes the variance in direction $\vec{p}_m$, i.e. the value
$$\vec{p}_{m}^TC\vec{p}_{m} = \vec{p}_{m}^T\lambda_M\vec{p}_{m} = \lambda_M.$$
This just means that we have to choose $\vec{p}_m$ to be the eigenvector with the largest eigenvalue (amongst those not previously selected). This completes the inductive step.


# System Properties

*This Jupyter notebook is part of a [collection of notebooks](../index.ipynb) in the bachelors module Signals and Systems, Communications Engineering, Universität Rostock. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Causality

The [principle of causality](https://en.wikipedia.org/wiki/Causality) states that an effect temporally has to follow its cause. A systems may violate this fundamental principle which limits its practical realization. Causality is hence an important aspect for the practical realization of systems. Conditions for causal linear time-invariant (LTI) systems for both the impulse response as well as the transfer function are derived in this section.

### Condition for the Impulse Response

The output signal $y(t) = \mathcal{H} \{ x(t) \}$ of an LTI system is given by convolving the input signal $x(t)$ with its impulse response $h(t)$

\begin{equation}
y(t) = \int_{-\infty}^{\infty} x(\tau) \cdot h(t-\tau) \; d\tau = \int_{-\infty}^{\infty} x(t - \tau) \cdot h(\tau) \; d\tau
\end{equation}

Analyzing the first integral reveals that the computation of the output signal $y(t)$ for some time instant $t=t_0$ requires knowledge of the input signal for all time instants. This includes also future time instants $t > t_0$. While this does not pose a problem for signals and impulse responses given in closed-form, this is not feasible in practice. Without imposing further restrictions, this would require the knowledge of the input signal for all future time instances.

Causality is not violated if we modify the integration limits of the convolution integral

\begin{equation}
y(t) = \int_{-\infty}^{t} x(\tau) \cdot h(t-\tau) \; d\tau = \int_{0}^{\infty} x(t - \tau) \cdot h(\tau) \; d\tau
\end{equation}

Now the output signal $y(t)$ for a given time instant $t=t_0$ depends only on the input signal $x(t)$ for $t \leq t_0$. Comparing the second equality with the second equality of the original convolution integral yields that the modified integration limits are equal to the assumption that  $h(t) = 0$ for $t < 0$.

A system is termed *causal system* [iff](https://en.wikipedia.org/wiki/If_and_only_if) its impulse response $h(t)$ is a causal signal

\begin{equation}
h(t) = 0 \qquad \text{for } t < 0
\end{equation}

Only causal systems can be realized practically due to above reasoning.

### Condition for the Transfer Function

The transfer function $H(j \omega) = \mathcal{F} \{ h(t) \}$ of a causal system shows specific symmetries. A causal impulse response $h(t)$ has to fulfill the following relation

\begin{equation}
h(t) = h(t) \cdot \epsilon(t)
\end{equation}

Fourier transformation of the left- and right-hand side yields

\begin{equation}
H(j \omega) = \frac{1}{\pi} H(j \omega) * \frac{1}{j \omega}
\end{equation}

by application of the [multiplication theorem](../fourier_transform/theorems.ipynb#Multiplication-Theorem) in conjunction with the [Fourier transform of the Heaviside signal](../fourier_transform/theorems.ipynb#Transformation-of-the-Heaviside-signal). Decomposing this result into the real and imaginary part of $H(j \omega)$ derives the following symmetry relations for a causal system

\begin{align}
\Re \{ H(j \omega) \} &= \frac{1}{\pi} \Im \{ H(j \omega) \} * \frac{1}{\omega} \\
\Im \{ H(j \omega) \} &= - \frac{1}{\pi} \Re \{ H(j \omega) \} * \frac{1}{\omega}
\end{align}

The convolution of a spectrum with $\frac{1}{\omega}$ is known as [Hilbert transform](https://en.wikipedia.org/wiki/Hilbert_transform). Above result states that the real and imaginary part of the spectrum $H(j \omega)$ of a causal system are related by the Hilbert transform. The Hilbert transform is used for instance in the theory of [single-sideband modulation](https://en.wikipedia.org/wiki/Single-sideband_modulation) in order to express the [analytic signal](https://en.wikipedia.org/wiki/Analytic_signal).

## Stability

The stability of a system can be evaluated with respect to various different criteria. The most common ones are [Lyapunov stability](https://en.wikipedia.org/wiki/Lyapunov_stability) and [bounded-input bounded-output stability](https://en.wikipedia.org/wiki/BIBO_stability). The latter is introduced in the following.

### Bounded-Input Bounded-Output Stability

With respect to the bounded-input bounded-output (BIBO) principle, an LTI system is termed stable if its output signal $y(t) = \mathcal{H} \{ x(t) \}$ is bounded for a bounded input signal $x(t)$. A signal is bounded if its magnitude does not exceed a given finite value. For the in- and output this condition is formulated as

\begin{align}
|x(t)| &< B_x \\
|y(t)| &< B_y
\end{align}

where $B_x, B_y < \infty$ denote constant finite bounds. The BIBO criterion is illustrated in the following

#### Condition for the Impulse Response

In order to derive a condition for the impulse response $h(t)$ of an LTI system that conforms to the BIBO criterion, the magnitude of the output signal $y(t)$ is expressed by convolving the input signal $x(t)$ with the impulse response

\begin{equation}
|y(t)| = \left| \int_{-\infty}^{\infty} x(\tau) \cdot h(t-\tau) \; d\tau \right|
\end{equation}

An upper bound for the magnitude of the output signal is found by applying the [triangle inequality](https://en.wikipedia.org/wiki/Triangle_inequality) together with the upper bound for $|x(t)|$

\begin{equation}
|y(t)| \leq \int_{-\infty}^{\infty} |x(\tau)| \cdot |h(t-\tau)| \; d\tau < \int_{-\infty}^{\infty} B_x \cdot |h(t-\tau)| \; d\tau
\end{equation}

Since the output signal $|y(t)|$ shall be bounded, it can be concluded that the impulse response needs to be an [absolutely integrable function](https://en.wikipedia.org/wiki/Absolutely_integrable_function)

\begin{equation}
\int_{-\infty}^{\infty} |h(t)| \; dt < B_h
\end{equation}

where $B_h < \infty$ denotes a constant finite bound.

An LTI system is stable in the sense of the BIBO stability criterion iff its impulse response is absolutely integrable. Since absolute integrability of a signal is sufficient for the [existence of its Fourier transform](../fourier_transform/definition.ipynb#Definition), this implies that the transfer function $H(j \omega) = \mathcal{F} \{ h(t) \}$ exists for stable systems.

#### Conditions for Rational Transfer Functions

For a rational transfer function $H(s) = \mathcal{L} \{ h(t) \}$, the BIBO stability of an LTI system implies constraints on the locations of its poles. These are derived in the following.

The inverse Laplace transform of a right-sided signal can be computed in closed form [using the partial fraction decomposition](../laplace_transform/inverse.ipynb#Basic-Procedure) of its Laplace transform. Applying this to the transfer function $H(s)$, the impulse response $h(t) = \mathcal{L}^{-1} \{ H(s) \}$ is given as

\begin{equation}
h(t) = A_0 \cdot \delta(t) + \epsilon(t) \sum_{\mu = 1}^{L} e^{s_{\infty \mu} t} \sum_{\nu = 1}^{R_\mu} \frac{A_{\mu \nu} \, t^{\mu - 1}}{(\nu -1)!}
\end{equation}

where $s_{\infty \mu}$ denotes the $\mu$-th unique pole of $H(s)$, $R_\mu$ its degree, $L$ the total number of different poles $\mu = 1 \dots L$, and $A_0$ and $A_{\mu \nu}$ the coefficients of the partial fraction decomposition. Above formula for the inverse Laplace transform holds if the order $M$ of the numerator is smaller or equal to the order $N$ of the denominator. In this case one has to check under which conditions the impulse response $h(t)$ is absolutely integrable. The first term $A_0 \delta(t)$ does not pose a problem for $A_0 < \infty$. Under the assumption $A_{\mu \nu} < \infty$, the absolute integrability of the second term is checked by evaluating

\begin{equation}
\int_{-\infty}^{\infty} |\epsilon(t) t^{\mu - 1} e^{s_{\infty \mu} t}| \; dt = \int_0^{\infty} t^{\mu - 1} e^{\sigma_{\infty \mu} t} \; dt
\end{equation}

where $s_{\infty \mu} = \sigma_{\infty \mu} + j \omega_{\infty \mu}$. It can be concluded that above integral is absolutely integrable if $\sigma_{\infty \mu} < 0$.

If $M > N$, the system is not stable in the BIBO sense. This statement can be proven by a counterexample. If $M > N$, a polynominal in $s$ with positive powers can be split off from the transfer function $H(s)$ by polynominal division. Lets consider one of these terms, for instance $H(s) = s$. It follows by application of the derivation theorem that $h(t) = \frac{d}{dt} \delta(t)$. For the bounded input signal $x(t) = \cos(\omega_0 t \cdot t)$, the output signal $y(t) = \cos(\omega_0 t \cdot t) * \frac{d}{dt} \delta(t) = \frac{d}{dt} \cos(\omega_0 t \cdot t) = - 2 \omega_0 t \sin(\omega_0 t \cdot t)$ is not bounded for $t \to \infty$. The same argumentation holds for positive powers of $s$ by repeated application of the derivation theorem.

Combining the results it can be concluded that a system with rational transfer function $H(s)$ and right-sided impulse response $h(t)$ is stable with respect to the BIBO criterion if

1. the order $M$ of its numerator is smaller or equal to the order $N$ of its numerator and
1. all poles are located in the left $s$-half-plane
    \begin{equation}
    \Re \{ s_{\infty \mu} \} < 0 \qquad \forall \mu
    \end{equation}

The locations of the zeros of the transfer function have no influence on the stability of a system.

In order to investigate the stability of a given system, the poles of its rational transfer function $H(s)$ have to be determined. This can be performed by computing the roots of the denominator polynomial. However, in order to avoid the computationally complex computation of roots, several stability tests have been developed. These determine the stability of a system from the coefficients of the denominator polynomial. An example of such a test is the [Routh–Hurwitz stability criterion](https://en.wikipedia.org/wiki/Routh%E2%80%93Hurwitz_stability_criterion).

**Example**

The impulse response $h(t) = \mathcal{L}^{-1} \{ H(s) \}$ of a 2nd-order system with transfer function

\begin{equation}
H(s) = \frac{1}{(s-s_\infty)(s-s^*_\infty)}
\end{equation}

is computed. First the case of a **stable system** is considered with $s_\infty = -1 + j$, hence $\Re \{ s_\infty \} < 0$. Its impulse response $h(t)$ is computed by inverse Laplace transform of the transfer function $h(t) = \mathcal{L}^{-1} \{ H(s) \}$

```python
import sympy as sym
%matplotlib inline
sym.init_printing()

s = sym.symbols('s', complex=True)
t = sym.symbols('t', real=True)
s_inf = -1 + sym.I

H = 1/((s - s_inf)*(s - sym.conjugate(s_inf)))
h = sym.inverse_laplace_transform(H, s, t)
h
```

The impulse response is plotted for illustration

```python
sym.plot(h, (t, 0, 10), xlabel='$t$', ylabel='$h(t)$');
```

It can be observed that the amplitude of the impulse response decays exponentially with increasing time $t$. The absolute integrability of the impulse response is confirmed by noting that

\begin{equation}
\int_{-\infty}^{\infty} | \epsilon(t) e^{-t} \sin(t) | \; dt = \int_{0}^{\infty} | e^{-t} \sin(t) | \; dt < \int_{0}^{\infty} | e^{-t} | \; dt = 1
\end{equation}

Now the impulse response of an **instable system** is investigated with $s_\infty = 1 + j$, hence $\Re \{ s_\infty \} > 0$. Its impulse response is given as

```python
s_inf = 1 + sym.I

H = 1/((s - s_inf)*(s - sym.conjugate(s_inf)))
h = sym.inverse_laplace_transform(H, s, t)
h
```

Again the impulse response is plotted for illustration

```python
sym.plot(h, (t, 0, 10), xlabel='$t$', ylabel='$h(t)$');
```

The impulse response shows an exponentially increasing amplitude for increasing time. The impulse response is clearly not absolutely integrable and hence the system not stable in the BIBO sense. This may also be confirmed by computing the output signal $y(t)$ for the bounded input signal $x(t) = \epsilon(t) \cos(\omega_0 t)$ via inverse Laplace transform of $Y(s) = H(s) \cdot X(s)$ with $X(s) = \mathcal{L} \{ x(t) \}$

```python
w0 = sym.symbols('\omega_0', real=True)

Y = 1/((s - s_inf)*(s - sym.conjugate(s_inf))) * s/(s**2 + w0**2)
y = sym.inverse_laplace_transform(Y, s, t)
y
```

It is straightforward to check that the output signal is not bounded for $t \to \infty$ due to the $e^t$ terms. The output signal is plotted for illustration with $\omega_0 = 10$

```python
sym.plot(y.subs(w0, 10), (t, 0, 16), xlabel='$t$', ylabel='$y(t)$');
```

## Realizability

A system is said to be realizable if it is a causal and stable system. Realizability is a prerequisite for the practical implementation of systems. Causality implies that the impulse response $h(t)$ of the system has to be a causal signal. Hence the region of convergence (ROC) of its transfer function $H(s) = \mathcal{L} \{ h(t) \}$ is the half-plane located right of all poles. Stability and causality of a system with rational transfer function $H(s)$ implies that its poles are located in the left $s$-half-plane.

Summarizing, a system with rational transfer function $H(s)$ is realizable iff all poles of $H(s)$ are located in the left $s$-half-plane not including the imaginary axis and the order of the numerator is lower or equal to the order of the denominator. The ROC is given by a right-sided $s$-half-plane including the imaginary axis. The locations of the zeros of the transfer function have no influence on the realizability of systems.

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Continuous- and Discrete-Time Signals and Systems - Theory and Computational Examples*.


# Multi-trait LMMs

### Set up the environment

```python
%matplotlib inline

import sys
import scipy as sp
import scipy.stats as st
import pylab as pl
import pandas as pd
import h5py
sp.random.seed(0)

import limix.util as lmx_util
import limix.plot as lmx_plt

def no_annotate(*args):
    pass
from limix.plot import manhattan
manhattan._annotate = no_annotate
```

### Download the data

First, load the yeast data, which have been imported into an hdf5 file.

To process your own data, use the limix command line binary (see [here](http://nbviewer.jupyter.org/github/limix/limix-tutorials/blob/master/preprocessing_QC/loading_files.ipynb) for an example).

```python
sys.path.append('./..')
import data as tutorial_data
file_name = tutorial_data.get_file('BYxRM')
```

### Set up the data object

The data object allows us to query the genotype and phenotype data.

```python
f = h5py.File(file_name, 'r')
pheno_group = f['phenotype']
pheno_df = pd.DataFrame(pheno_group['matrix'][:],
                        columns=pheno_group['col_header']['phenotype_ID'][:],
                        index=pheno_group['row_header']['sample_ID'][:])
```

```python
pheno_df.shape
```

    (1008, 46)

```python
pheno_df.head()
```

      Cadmium_Chloride
      Caffeine
      Calcium_Chloride
      Cisplatin
      Cobalt_Chloride
      Congo_red
      Copper
      Cycloheximide
      Diamide
      E6_Berbamine
      ...
      x6-Azauracil
      Xylose
      YNB
      YNB:ph3
      YNB:ph8
      YPD
      YPD:15C
      YPD:37C
      YPD:4C
      Zeocin

      A01_01
      -7.323520
      0.279993
      0.313118
      1.658179
      -1.604442
      5.841617
      -4.130950
      0.821226
      3.622602
      -0.378747
      ...
      1.199054
      -0.309149
      17.470822
      0.055225
      -0.184268
      24.548971
      0.712171
      0.890842
      4.118372
      8.592818

      A01_02
      -8.098236
      -0.206326
      -0.534844
      -0.918012
      0.892198
      -1.618172
      1.131947
      -0.764736
      -2.946279
      -2.475193
      ...
      0.443182
      0.697908
      18.052925
      0.283462
      1.662201
      26.808476
      -1.522498
      0.006062
      0.066515
      -4.220476

      A01_03
      7.605720
      -0.127960
      -0.311102
      -2.712088
      3.301709
      -6.680571
      -1.138056
      -3.382532
      -3.157866
      -4.178616
      ...
      1.472629
      1.474329
      18.163782
      -0.951379
      0.369565
      26.183975
      1.082289
      1.723157
      5.714088
      -6.506519

      A01_04
      -6.147649
      0.878392
      -0.563331
      -0.827358
      -1.291270
      2.753851
      -2.268121
      0.867456
      -6.832079
      -1.970630
      ...
      0.054694
      -0.739040
      15.575856
      -1.690867
      -0.840344
      20.893646
      -0.636280
      0.053258
      -3.196936
      0.294462

      A01_05
      9.379060
      -1.353169
      0.405204
      -1.137234
      4.260286
      -7.391216
      -3.222742
      1.586376
      1.048391
      -3.971730
      ...
      0.267463
      0.959138
      17.808444
      0.127621
      1.967267
      28.621507
      -3.720999
      -0.768724
      -4.002786
      7.045575

5 rows × 46 columns

```python
geno_group = f['genotype']
chromosomes = geno_group['col_header']['chrom'][:]
positions = geno_group['col_header']['pos'][:]
geno_df = pd.DataFrame(geno_group['matrix'][:], columns=positions,
                       index=geno_group['row_header']['sample_ID'][:],
                      dtype='int64')
```

```python
geno_df.shape
```

    (1008, 11623)

```python
geno_df.head()
```

      27915
      28323
      28652
      29667
      30756
      31059
      31213
      31636
      31756
      31976
      ...
      925487
      925742
      926177
      927903
      928103
      929518
      929724
      930545
      931289
      931944

      A01_01
      1
      1
      1
      1
      1
      1
      1
      1
      1
      1
      ...
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0

      A01_02
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      1
      1
      1
      1
      1
      1
      1
      1
      1
      1

      A01_03
      1
      1
      1
      1
      1
      1
      1
      1
      1
      1
      ...
      0
      0
      0
      0
      0
      0
      0
      1
      1
      1

      A01_04
      1
      1
      1
      1
      1
      1
      1
      1
      1
      1
      ...
      1
      1
      1
      1
      1
      1
      1
      1
      1
      1

      A01_05
      0
      0
      0
      0
      0
      0
      0
      0
      0
      0
      ...
      1
      1
      1
      1
      1
      1
      1
      1
      1
      1

5 rows × 11623 columns

### Visualize the correlation among traits

```python
#Remove NaNs
filtered_pheno_df = pheno_df.dropna()
phenotype_names = filtered_pheno_df.columns
# center phenotype
normalized_pheno_df = (filtered_pheno_df - filtered_pheno_df.mean()) / filtered_pheno_df.std()
```

```python
pl.figure(figsize=[20,20])
Ce= sp.cov(normalized_pheno_df.values.T)
pl.imshow(Ce,aspect='auto',interpolation='none')
pl.xticks(sp.arange(len(phenotype_names)),phenotype_names,rotation=90)
pl.yticks(sp.arange(len(phenotype_names)),phenotype_names,rotation=0)
pl.colorbar()
```

### Select a subset of the phenotypes

```python
phenotype_names = ['YPD:37C','YPD:15C','YPD:4C']
data_subsample = pheno_df[phenotype_names]
```

```python
phenotypes = data_subsample.dropna()
sample_idx = phenotypes.index.intersection(geno_df.index)
phenotypes = phenotypes.loc[sample_idx]
phenotypes = (phenotypes - phenotypes.mean()) / phenotypes.std()
phenotypes.describe()
```

      YPD:37C
      YPD:15C
      YPD:4C

      count
      8.040000e+02
      8.040000e+02
      8.040000e+02

      mean
      -2.651279e-17
      2.209399e-18
      5.744438e-17

      std
      1.000000e+00
      1.000000e+00
      1.000000e+00

      min
      -2.651086e+00
      -2.497574e+00
      -2.020168e+00

      25%
      -7.669938e-01
      -7.652285e-01
      -7.548557e-01

      50%
      4.068578e-02
      -7.631450e-02
      -1.699190e-01

      75%
      8.097791e-01
      7.221308e-01
      6.011856e-01

      max
      2.867696e+00
      2.745373e+00
      3.382201e+00

We start by (further) examining the correlation among the phenotypes:

```python
#pairwise corrrelations of the first 3 traits
pl.figure(figsize=[15,5])
pl.subplot(1,3,1)
pl.plot(phenotypes[phenotype_names[0]].values,phenotypes[phenotype_names[1]].values,'.')
pl.xlabel(phenotype_names[0])
pl.ylabel(phenotype_names[1])

pl.subplot(1,3,2)
pl.plot(phenotypes[phenotype_names[1]].values,phenotypes[phenotype_names[2]].values,'.')
pl.xlabel(phenotype_names[1])
pl.ylabel(phenotype_names[2])

pl.subplot(1,3,3)
pl.plot(phenotypes[phenotype_names[0]].values,phenotypes[phenotype_names[2]].values,'.')
pl.xlabel(phenotype_names[0])
pl.ylabel(phenotype_names[2])
```

# Variance Decomposition

Here, we show how to estimate the genetic and residual covariance using the limix class limix.varDecomp.VarianceDecomposition (see [here](https://www.pydoc.io/pypi/limix-1.0.6/autoapi/varDecomp/varianceDecomposition/index.html)).

```python
from limix.vardec import VarianceDecomposition
```

```python
# genetic relatedness matrix
from limix.stats import linear_kinship, gower_norm
sample_relatedness = gower_norm(linear_kinship(geno_df.loc[sample_idx].values))
```

    100%|██████████| 100/100 [00:00<00:00, 228.90it/s]

```python
# variance component model
vc = VarianceDecomposition(phenotypes.values)
vc.addFixedEffect()
vc.addRandomEffect(K=sample_relatedness)
vc.addRandomEffect(is_noise=True)
vc.optimize()

# retrieve genetic and noise covariance matrix
Cg = vc.getTraitCovar(0)
Cn = vc.getTraitCovar(1)
```

### Empirical Correlation

```python
Ce = sp.corrcoef(phenotypes.T)
```

### Plot the correlation among the phenotypes (empirical), the genetic covariance among phenotypes, and the noise covariance.

```python
pl.figure(figsize=[15,5])
pl.subplot(1,3,1)
pl.imshow(Ce,aspect='auto',interpolation='none',vmin=-1,vmax=1)
pl.xticks(sp.arange(len(phenotype_names)),phenotypes.columns)
pl.yticks(sp.arange(len(phenotype_names)),phenotypes.columns)
pl.title('empirical correlation')

pl.subplot(1,3,2)
pl.imshow(Cg,aspect='auto',interpolation='none',vmin=-1,vmax=1)
pl.xticks(sp.arange(len(phenotype_names)),phenotypes.columns)
pl.yticks(sp.arange(len(phenotype_names)),phenotypes.columns)
pl.title('genetic covariance')

pl.subplot(1,3,3)
pl.imshow(Cn,aspect='auto',interpolation='none',vmin=-1,vmax=1)
pl.xticks(sp.arange(len(phenotype_names)),phenotypes.columns)
pl.yticks(sp.arange(len(phenotype_names)),phenotypes.columns)
pl.title('noise covariance')
pl.colorbar()
```

# Univariate association testing

As shown earlier, univariate (single-trait) association testing with linear mixed models can be performed with the function ``limix.qtl.qtl_test_lmm`` (see [here](https://limix.readthedocs.io/en/stable/qtl.html#linear-mixed-models)).

```python
from limix.qtl import qtl_test_lmm
```

```python
# load snp data
snps = geno_df.loc[sample_idx].values
positions = geno_df.columns
```

Run the LMM and convert the P-values into a pandas DataFrame.

```python
lmm = qtl_test_lmm(snps=snps,
                   pheno=phenotypes.values,
                   K=sample_relatedness)
pv_lmm = lmm.getPv()

pvalues_lmm = pd.DataFrame(data=pv_lmm.T,
                           index=positions,
                           columns=phenotype_names)
```

Plot the results from (univariate) GWAS using Manhattan plots.

```python
for p_ID in phenotype_names:
    pl.figure(figsize=[15,4])
    lmx_plt.plot_manhattan(pd.DataFrame(dict(pv=pvalues_lmm[p_ID].values,pos=positions,chrom=chromosomes,alpha=0.05)))
    pl.title(p_ID)
```

# Multi-trait association testing

Multi-trait association testing with linear mixed models can be performed using the function ``limix.qtl.qtl_test_lmm_kronecker`` (see [here](https://limix.readthedocs.io/en/stable/qtl.html#limix.qtl.qtl_test_lmm_kronecker)).

Here we show how to perform the following multi-trait tests:
- any effect test (that is, a test to determine if a SNP has an effect on any of the phenotypes)
- common effect test (test to determine if a SNP has the same effect size and sign/direction for both phenotypes)
- specific effect test (test to determine if a SNP has a specific effect on the two traits; good for GxE)

### Any effect test

```python
N, P = phenotypes.values.shape
```

```python
covs = sp.ones((N, 1))      #covariates
Acovs = sp.eye(P)           #the design matrix for the covariates
Asnps = sp.eye(P)           #the design matrix for the SNPs
K1r = sample_relatedness    #the first sample-sample covariance matrix (non-noise)
```

```python
from limix.qtl import qtl_test_lmm_kronecker
```

```python
lmm, pvalues = qtl_test_lmm_kronecker(snps=snps,
                                      phenos=phenotypes.values,
                                      covs=covs,
                                      Acovs=Acovs,
                                      Asnps=Asnps,
                                      K1r=K1r)
```

Convert the P-values into a DataFrame:

```python
pvalues = pd.DataFrame(data=pvalues.T,index=positions,columns=['multi_trait'])
```

### Plot the results from multi-trait GWAS using Manhattan plots.

```python
pl.figure(figsize=[15,4])
pl.title('Any effect test')
lmx_plt.plot_manhattan(pd.DataFrame(dict(pv=pvalues['multi_trait'].values,
                       pos=positions,chrom=chromosomes)))
```

### Common effect test

A common effect test is a 1 degree of freedom test and can be done by setting
\begin{equation}
\mathbf{A}_1^\text{(snp)} = \mathbf{1}_{1,P},\;\;\;
\mathbf{A}_0^\text{(snp)} = \mathbf{0}
\end{equation}

```python
covs = sp.ones((N, 1))      #covariates
Acovs = sp.eye(P)           #the design matrix for the covariates
Asnps = sp.ones((1,P))      #the design matrix for the SNPs
K1r = sample_relatedness    #the first sample-sample covariance matrix (non-noise)
```

```python
lmm, pvalues_common = qtl_test_lmm_kronecker(snps=snps,
                                             phenos=phenotypes.values,
                                             #covs=covs,
                                             #Acovs=Acovs,
                                             Asnps=Asnps,
                                             K1r=K1r)
```

Convert the P-values into a DataFrame:

```python
pvalues_common = pd.DataFrame(data=pvalues_common.T,index=positions,columns=['common'])
```

### Manhattan plot

```python
pl.figure(figsize=[15,4])
pl.title('common')
lmx_plt.plot_manhattan(pd.DataFrame(dict(pv=pvalues_common['common'].values,
                       pos=positions,chrom=chromosomes)))
```

### Testing for a specific effect

For a specific effect test for trait $p$
the alternative model is set to have both a common and a specific effect
for transcript $p$ from the SNP while the null model has only a common effect.

It is a 1 degree of freedom test and,
in the particular case of $P=3$ traits and for $p=0$, it can be done by setting
\begin{equation}
\mathbf{A}_1^\text{(snp)} =
\begin{pmatrix}
  1 & 0 & 0 \\
  1 & 1 & 1
 \end{pmatrix}
\;\;\;,
\mathbf{A}_0^\text{(snp)} = \mathbf{1}_{1,3}
\end{equation}

Specific effect tests can be performed using the function ``limix.qtl.qtl_test_interaction_lmm_kronecker`` (see [here](https://limix.readthedocs.io/en/stable/qtl.html#limix.qtl.qtl_test_interaction_lmm_kronecker)).

```python
Asnps0 = sp.ones((1,P))     #the null model design matrix for the SNPs
Asnps1 = sp.zeros((2,P))          #the alternative model design matrix for the SNPs
Asnps1[0,:] = 1.0
Asnps1[1,1] = 1.0
print "Design(0): \n"+str(Asnps0)
print "Design(Alt): \n"+str(Asnps1)
```

    Design(0):
    [[1. 1. 1.]]
    Design(Alt):
    [[1. 1. 1.]
     [0. 1. 0.]]

```python
from limix.qtl import qtl_test_interaction_lmm_kronecker

pvalues_inter = qtl_test_interaction_lmm_kronecker(snps=snps,
                                                   phenos=phenotypes.values,
                                                   covs=covs,
                                                   Acovs=Acovs,
                                                   Asnps0=Asnps0,
                                                   Asnps1=Asnps1,
                                                   K1r=K1r)
```

Convert the P-values into a DataFrame:

```python
pvalues_inter = pd.DataFrame(data=sp.concatenate(pvalues_inter).T,
                             index=positions,
                             columns=["specific","null_common","alternative_any"])
```

### Manhattan plot

```python
pl.figure(figsize=[15,4])
pl.title('specific')
lmx_plt.plot_manhattan(pd.DataFrame(dict(pv=pvalues_inter['specific'].values,
                       pos=positions,chrom=chromosomes,
                       alpha=0.1)))
```


```python
%matplotlib inline
```

对抗样本生成
==============================

**Author:** `Nathan Inkawhich <https://github.com/inkawhich>`__

**翻译者**: `Antares博士 <http://www.studyai.com/antares>`__

如果你正在阅读这篇文章，希望你能体会到一些机器学习模型是多么的有效。研究不断推动ML模型变得更快、更准确和更高效。
然而，设计和训练模型的一个经常被忽视的方面是安全性和健壮性，特别是在面对希望欺骗模型的对手时。

本教程将提高您对ML模型的安全漏洞的认识，并将深入了解对抗性机器学习的热门话题。
您可能会惊讶地发现，在图像中添加不可察觉的扰动会导致截然不同的模型性能。
鉴于这是一个教程，我们将通过一个图像分类器的例子来探讨这个主题。
具体来说，我们将使用第一种也是最流行的攻击方法-快速梯度符号攻击(Fast Gradient Sign Attack ,FGSM)来欺骗MNIST分类器。

威胁模型(Threat Model)
-------------------------

有很多种类的对抗性攻击，每种攻击都有不同的目标和攻击者的知识假设。但是，总体目标
是在输入数据中增加最少的扰动量，以导致期望的错误分类。攻击者的知识有几种假设，其中两种假设是：
**白盒子(white-box)** 和 **黑盒子(black-box)**。
*白盒子* 攻击假定攻击者拥有对模型的全部知识和访问权限，包括体系结构、输入、输出和权重。
*黑盒子* 攻击假设攻击者只能访问模型的输入和输出，而对底层架构或权重一无所知。
还有几种目标类型，包括 **错误分类(misclassification)** 和 **源/目标错误分类(source/target misclassification)** 。
*错误分类* 的目标意味着对手只希望输出分类是错误的，而不关心新的分类是什么。
*源/目标错误分类* 意味着对手希望更改最初属于特定源类的图像，从而将其归类为特定的目标类。

在这种情况下，FGSM攻击是以 *错误分类* 为目标的 *白盒攻击* 。 有了这些背景信息，我们现在可以详细讨论攻击(attack)了。

快速梯度符号攻击(Fast Gradient Sign Attack)
--------------------------------------------

迄今为止，第一次也是最流行的对抗性攻击(adversarial attacks)之一被称为 *快速梯度符号攻击(FGSM)* ，
古德费尔特对此进行了描述:  `Explaining and Harnessing Adversarial Examples <https://arxiv.org/abs/1412.6572>`__。
攻击是非常强大的，但却是直观的。它是设计用来攻击神经网络，利用他们的学习方式，*梯度* 。其思想很简单，
不是通过调整基于反向传播梯度的权重来最小化损失，而是 *基于相同的反向传播梯度调整输入数据，
使损失最大化* 。换句话说，攻击使用损失W.r.t输入数据的梯度，然后调整输入数据以最大化损失。

在我们进入代码之前，让我们看一下著名的 `FGSM <https://arxiv.org/abs/1412.6572>`__  熊猫示例，并提取一些记号(notation)。

.. figure:: /_static/img/fgsm_panda_image.png
   :alt: fgsm_panda_image

从图片中, $\mathbf{x}$ 是被正确分类为“panda”的原始图像， $y$
是 $\mathbf{x}$ 的真正的类标签。
$\mathbf{\theta}$ 表示模型参数，并且 $J(\mathbf{\theta}, \mathbf{x}, y)$ 用来
训练网络的损失。 攻击将梯度反向传播回输入数据以进行计算 $\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$ 。
然后，它沿着使损失最大化的方向(i.e. $sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))$) 上
调整输入数据一小步($\epsilon$ 或 $0.007$ 在图片中)。
由此产生的扰动图像(perturbed image), $x'$, 就会被目标网络 *误分类(misclassified)* 为 “gibbon”，
但事实上 被扰动的图像依然是个 “panda” 。

希望现在你已明了本教程的动机了，所以让我们跳到它的具体实现吧。

```python
from __future__ import print_function
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
import numpy as np
import matplotlib.pyplot as plt
```

实现
--------------

在这一小节中, 我们将讨论输入参数，定义在攻击之下的模型，然后编写攻击代码然后将一些测试跑起来。

输入
~~~~~~

本教程只有三个输入，定义如下:

-  **epsilons** - 要用于运行的epsilon值列表。在列表中保持0很重要，因为它代表了原始测试集上的模型性能。而且，从直觉上说，
   我们认为epsilon越大，扰动越明显，但攻击越有效，降低了模型的准确性。由于 数据的范围是 $[0,1]$ ，任何epsilon值都不应超过1。

-  **pretrained_model** - 通向预先训练过的MNIST模型的路径，该模型是用
   `pytorch/examples/mnist <https://github.com/pytorch/examples/tree/master/mnist>`__ 。
   为了简单起见，请在 `这里 <https://drive.google.com/drive/folders/1fn83DF14tWmit0RTKWRhPq5uVXt73e0h?usp=sharing>`__
   下载经过预先训练的模型。

-  **use_cuda** - 布尔标志使用CUDA(如果需要和可用的话)。注意，带有CUDA的GPU对于本教程来说并不重要，因为CPU不会花费太多时间。

```python
epsilons = [0, .05, .1, .15, .2, .25, .3]
pretrained_model = "./data/lenet_mnist_model.pth"
use_cuda=True
```

受攻击模型(Model Under Attack)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

如前所述，受攻击的模型是与 `pytorch/examples/mnist <https://github.com/pytorch/examples/tree/master/mnist>`__
相同的MNIST模型。您可以训练和保存自己的MNIST模型，也可以下载和使用所提供的模型。
这里的网络定义和测试dataloader是从MNIST示例中复制的。本节的目的是定义model和dataloader，
然后初始化模型并加载预先训练的权重。

```python
# LeNet Model definition
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
        self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
        self.conv2_drop = nn.Dropout2d()
        self.fc1 = nn.Linear(320, 50)
        self.fc2 = nn.Linear(50, 10)

    def forward(self, x):
        x = F.relu(F.max_pool2d(self.conv1(x), 2))
        x = F.relu(F.max_pool2d(self.conv2_drop(self.conv2(x)), 2))
        x = x.view(-1, 320)
        x = F.relu(self.fc1(x))
        x = F.dropout(x, training=self.training)
        x = self.fc2(x)
        return F.log_softmax(x, dim=1)

# MNIST Test dataset 和 dataloader 声明
test_loader = torch.utils.data.DataLoader(
    datasets.MNIST('./data/mnist', train=False, download=True, transform=transforms.Compose([
            transforms.ToTensor(),
            ])),
        batch_size=1, shuffle=True)

# 定义我们要使用的设备
print("CUDA Available: ",torch.cuda.is_available())
device = torch.device("cuda" if (use_cuda and torch.cuda.is_available()) else "cpu")

# 初始化网络
model = Net().to(device)

# 加载预训练模型
model.load_state_dict(torch.load(pretrained_model, map_location='cpu'))

# 将模型设置为评估模式. 这是为了 Dropout layers。
model.eval()
```

FGSM Attack
~~~~~~~~~~~~~~~~~~~

现在，我们可以通过扰动原始输入来定义创建对抗性样例(adversarial examples)的函数。
``fgsm_attack`` 函数接收三个输入： *image* 是原始的干净图像 ($x$), *epsilon* 是
逐像素扰动量 ($\epsilon$), 而 *data_grad* 是损失相对于(w.r.t)输入图像的梯度：
($\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y)$) 。 有了这三个输入，该函数就会按下述方法
创建扰动图像(perturbed image):

\begin{align}perturbed\_image = image + epsilon*sign(data\_grad) = x + \epsilon * sign(\nabla_{x} J(\mathbf{\theta}, \mathbf{x}, y))\end{align}

最后, 为了保持数据的原始范围，将扰动图像裁剪到 $[0,1]$ 范围内。

```python
# FGSM 攻击代码
def fgsm_attack(image, epsilon, data_grad):
    # Collect the element-wise sign of the data gradient
    sign_data_grad = data_grad.sign()
    # Create the perturbed image by adjusting each pixel of the input image
    perturbed_image = image + epsilon*sign_data_grad
    # Adding clipping to maintain [0,1] range
    perturbed_image = torch.clamp(perturbed_image, 0, 1)
    # Return the perturbed image
    return perturbed_image
```

测试函数
~~~~~~~~~~~~~~~~

最后，本教程的中心结果来自于 ``test`` 函数。每次调用该测试函数都会在MNIST测试集上执行完整的测试步骤，
并报告最终的准确性。但是，请注意，此函数也接受 *epsilon* 输入。这是因为 ``test`` 函数报告了一个模型的准确性，
该模型正受到来自实力 $\epsilon$ 的对手的攻击。更具体地说，对于测试集中的每个样本，
该函数计算loss w.r.t the input data ($data\_grad$)，用 ``fgsm_attack`` ($perturbed\_data$)
创建一个受扰动的图像，然后检查被扰动的样例是否是对抗性的。除了测试模型的准确性外，
该函数还保存并返回了一些成功的对抗性样例，以供以后可视化。

```python
def test( model, device, test_loader, epsilon ):

    # Accuracy counter
    correct = 0
    adv_examples = []

    # Loop over all examples in test set
    for data, target in test_loader:

        # Send the data and label to the device
        data, target = data.to(device), target.to(device)

        # Set requires_grad attribute of tensor. Important for Attack
        data.requires_grad = True

        # Forward pass the data through the model
        output = model(data)
        init_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability

        # If the initial prediction is wrong, dont bother attacking, just move on
        if init_pred.item() != target.item():
            continue

        # Calculate the loss
        loss = F.nll_loss(output, target)

        # Zero all existing gradients
        model.zero_grad()

        # Calculate gradients of model in backward pass
        loss.backward()

        # Collect datagrad
        data_grad = data.grad.data

        # Call FGSM Attack
        perturbed_data = fgsm_attack(data, epsilon, data_grad)

        # Re-classify the perturbed image
        output = model(perturbed_data)

        # Check for success
        final_pred = output.max(1, keepdim=True)[1] # get the index of the max log-probability
        if final_pred.item() == target.item():
            correct += 1
            # Special case for saving 0 epsilon examples
            if (epsilon == 0) and (len(adv_examples) < 5):
                adv_ex = perturbed_data.squeeze().detach().cpu().numpy()
                adv_examples.append( (init_pred.item(), final_pred.item(), adv_ex) )
        else:
            # Save some adv examples for visualization later
            if len(adv_examples) < 5:
                adv_ex = perturbed_data.squeeze().detach().cpu().numpy()
                adv_examples.append( (init_pred.item(), final_pred.item(), adv_ex) )

    # Calculate final accuracy for this epsilon
    final_acc = correct/float(len(test_loader))
    print("Epsilon: {}\tTest Accuracy = {} / {} = {}".format(epsilon, correct, len(test_loader), final_acc))

    # Return the accuracy and an adversarial example
    return final_acc, adv_examples
```

运行 Attack
~~~~~~~~~~~~~~~~~~

实现的最后一部分是实际运行攻击。在这里，我们对 *epsilons* 输入中的每个epsilon值运行一个完整的测试步骤。
对于每个epsilon，我们还保存了最终的准确性和一些成功的对抗性样例，将在接下来绘制出来。
注意打印精度是如何随着epsilon值的增加而降低的。另外，请注意 $\epsilon=0$
表示原始测试的准确性，没有任何攻击。

```python
accuracies = []
examples = []

# Run test for each epsilon
for eps in epsilons:
    acc, ex = test(model, device, test_loader, eps)
    accuracies.append(acc)
    examples.append(ex)
```

结果
-------

Accuracy vs Epsilon
~~~~~~~~~~~~~~~~~~~~~

第一个结果是accuracy vs epsilon的图。正如前面提到的，随着epsilon的增加，我们预计测试的准确性会下降。
这是因为更大的epsilon意味着我们朝着最大化损失的方向迈出了更大的一步。注意，即使epsilon值是线性的，
曲线中的趋势也不是线性的。例如，在 $\epsilon=0.05$ 处的准确度仅比 $\epsilon=0.15$ 低4%，
而 $\epsilon=0.2$ 的准确度比 $\epsilon=0.15$ 低25%。
另外，注意模型的精度对10类分类器的随机精度影响在 $\epsilon=0.25$ 和 $\epsilon=0.3$ 之间。

```python
plt.figure(figsize=(5,5))
plt.plot(epsilons, accuracies, "*-")
plt.yticks(np.arange(0, 1.1, step=0.1))
plt.xticks(np.arange(0, .35, step=0.05))
plt.title("Accuracy vs Epsilon")
plt.xlabel("Epsilon")
plt.ylabel("Accuracy")
plt.show()
```

一些对抗性样本
~~~~~~~~~~~~~~~~~~~~~~~~~~~

还记得没有免费午餐的思想吗？在这种情况下，随着epsilon的增加，测试精度降低，但扰动变得更容易察觉。
实际上，攻击者必须考虑的是准确性、程度和可感知性之间的权衡。在这里，我们展示了在每个epsilon值下
一些成功的对抗性样例。图中的每一行都显示不同的epsilon值。第一行是 $\epsilon=0$ 示例，
它表示原始的“干净”图像，没有任何扰动。每幅图像的标题显示“原始分类->对抗性分类”。
注意，当 $\epsilon=0.15$ 时，扰动开始变得明显，在 $\epsilon=0.3$ 时非常明显。
然而，在所有情况下，人类仍然能够识别正确的类别，尽管增加了噪音。

```python
# Plot several examples of adversarial samples at each epsilon
cnt = 0
plt.figure(figsize=(8,10))
for i in range(len(epsilons)):
    for j in range(len(examples[i])):
        cnt += 1
        plt.subplot(len(epsilons),len(examples[0]),cnt)
        plt.xticks([], [])
        plt.yticks([], [])
        if j == 0:
            plt.ylabel("Eps: {}".format(epsilons[i]), fontsize=14)
        orig,adv,ex = examples[i][j]
        plt.title("{} -> {}".format(orig, adv))
        plt.imshow(ex, cmap="gray")
plt.tight_layout()
plt.show()
```

下一步去哪里?
-----------------

希望本教程能提供一些关于对抗性机器学习主题的见解。这里有许多潜在的方向可走。
这种攻击代表了对抗性攻击研究的开始，并且由于有许多关于如何攻击和保护ML模型不受对手攻击的想法。
实际上，在NIPS 2017的比赛中，存在着一种对抗性的攻防竞争，
`本文 <https://arxiv.org/pdf/1804.00097.pdf>`__ 介绍了在这场比赛中所采用的许多方法：对抗攻击和防御竞争。
防御方面的工作也带来了使机器学习模型在一般情况下更加健壮的想法，
使机器学习模型既具有自然的扰动性，又具有对抗性的输入。

另一个方向是不同领域的对抗攻击和防御。对抗性研究并不局限于图像领域，请看 `这个 <https://arxiv.org/pdf/1801.01944.pdf>`__
对语音到文本模型的攻击。
但是也许了解更多对抗性机器学习的最好方法是弄脏你的手(意思是让你动手尝试)。
尝试实现来自NIPS 2017 竞赛的不同的攻击策略，看看它与FGSM有何不同。然后，试着保护模型不受你自己的攻击。


# 信号塔覆盖

等级：初级

## 目的和先决条件

在此示例中，我们将解决一个简单的覆盖问题：如何构建一个信号塔网络以提供信号覆盖给尽可能多的人。我们将构建业务问题的数学模型，然后用 Gurobi Python API 进行建模，并计算最佳解决方案。

该建模示例处于初级阶段，我们假设你了解Python，并且具有一些有关构建数学优化模型的知识。

**注意：** 你可以通过单击 [此处](https://github.com/arvinxx/gurobi-and-mathematical-modeling/archive/master.zip) 下载包含此示例和其他示例的代码。为了正确运行此 Jupyter Notebook，你必须具有 Gurobi 许可证。如果你没有，则可以**商业用户身份**申请 [试用许可证](https://www.gurobi.com/downloads/request-an-evaluation-license/)，或以**学术用户身份**下载 [免费许可证](https://www.gurobi.com/academia/academic-program-and-licenses)。

## 动机

在过去的十年里，智能手机彻底改变了我们的生活，远远超出了我们的沟通方式。除了打电话,发短信,发邮件,现在全世界有超过二十亿人使用这些设备导航到书出租车,比较产品评论和价格,遵循新闻,看电影,听音乐,玩游戏,拍照,参与社交媒体,和许多其他应用程序。

蜂窝网络是一种手持式智能手机网络，其中每部手机通过蜂窝基站(蜂窝基站)的本地天线通过无线电波与电话网络进行通信。一个重要的问题是如何安置手机信号塔，以便向最多的人提供信号覆盖。

## 问题描述

一家电信公司需要建造一组蜂窝塔，以为给定城市的居民提供信号覆盖范围。已经确定了可以建造塔楼的许多潜在位置。这些塔有固定的范围，并且-由于预算限制，只能建造数量有限的塔。鉴于这些限制，该公司希望将覆盖范围扩大到尽可能多的人口。为了简化问题，该公司将其希望覆盖的区域划分为一组区域，每个区域都有已知的人口。然后，目标是选择公司应在哪些潜在位置上建造手机信号塔，以便覆盖尽可能多的人。

蜂窝基站覆盖问题是最大覆盖位置问题[1]的一个实例。它也与集合覆盖问题（Set Cover Problem）有关。集合覆盖问题出现在许多不同的领域，非常重要的应用来自航空工业。例如，机组调度和航道分配问题[2]。

## 解决方法

数学优化（也称为数学编程）是一种声明性方法，其中建模者制定了一个优化问题，该问题捕获了复杂决策问题的关键特征。然后 Gurobi 使用最先进的数学和计算机科学技术来解决这一类数学优化问题。

数学优化模型具有五个组成部分：

* 数据集（Sets）
* 参数(Parameters)
* 决策变量(Decision variables)
* 约束（Constraints）
* 目标函数（Objective function(s)）

我们现在提出一个混合整数规划（MIP）模型来解决信号塔覆盖问题。

## 模型制定

### 索引和集合

$i \in T$: 信号塔候选地点的索引和集合。

$j \in R$: 区域的索引和集合。

$G(T,R,E)$: 在候选地点 $T$ 中定义的[二分图](https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E5%9B%BE)，我们要覆盖的区域集为 $R$，而 $E$ 是集合的另一组边。其中，如果中的区域 $j \in R$ 可以被位置为 $i \in T$ 的塔覆盖，则在 $(i,j) \in E$ 有一条边。

### 参数

$c_{i} \in \mathbb{R}^+$: 在站点 $i$ 建立塔的成本。

$p_{j} \in \mathbb{N}$: 区域$j$的人口。

### 决策变量

$covered_{j} \in \{0, 1 \}$: 如果覆盖区域 $j$，则值等于1；否则为0。

$build_{i} \in \{0, 1 \}$: 如果建造了塔 $i$，则此变量等于1，否则为0。

### 目标函数

- **人口覆盖**：我们力求使塔楼覆盖的总人口最大化。

\begin{equation}
\text{Max} \quad Z = \sum_{j \in R} p_{j} \cdot covered_{j}
\tag{0}
\end{equation}

### 约束

- **覆盖**：对于每个区域$j \in R$，确保至少有一个信号塔。

\begin{equation}
\sum_{(i,j) \in E} build_{i} \geq covered_{j} \quad \forall j \in R
\tag{1}
\end{equation}

- **预算**：我们需要确保建造塔楼的总成本不超过指定的预算。

\begin{equation}
\sum_{i \in T} c_{i} \cdot build_{i} \leq \text{budget}
\tag{2}
\end{equation}

## Python 实现

本示例考虑了6个塔和9个区域的二分图。下表说明了每个手机信号塔站点（行）所覆盖的区域（列）。

|  | 地区0 | 地区1 | 地区2 | 地区3 | 地区4 | 地区5 | 地区6 | 地区7 | 地区8 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |  --- |
| 塔0 | 1 | 1 | - | - | - | 1 | - | - |  - |
| 塔1 | 1 | - | - | - | - | - | - | 1 |  1 |
| 塔2 | - | - | 1 | 1 | 1 | - | 1 | - |  - |
| 塔3 | - | - | 1 | - | - | 1 | 1 | - |  - |
| 塔4 | 1 | - | 1 | - | - | - | 1 | 1 |  1 |
| 塔5 | - | - | - | 1 | 1 | - | - | - |  1 |

下表列出了每个地区的人口。

|  | 地区0 | 地区1 | 地区2 | 地区3 | 地区4 | 地区5 | 地区6 | 地区7 | 地区8 |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 人口 | 523 | 690 | 420 | 1010 | 1200 | 850 | 400 | 1008 | 950 |

在下表中列出了在每个地点建造基站塔的成本。

|  | 成本 (百万美元) |
| --- | --- |
| 塔0 | 4.2 |
| 塔1 | 6.1 |
| 塔2 | 5.2 |
| 塔3 | 5.5 |
| 塔4 | 4.8 |
| 塔5 | 9.2 |

总预算为 $\$20,000,000$。

我们现在导入 Gurobi Python 模块。然后用给定的数据初始化数据结构。

```python
import gurobipy as gp
from gurobipy import GRB

# 官方测试版本 Gurobi v9.0.0 & Python 3.7.0

# 译者测试版本 Gurobi v9.1.0 & Python 3.8.6

# 参数
budget = 20
regions, population = gp.multidict({
    0: 523, 1: 690, 2: 420,
    3: 1010, 4: 1200, 5: 850,
    6: 400, 7: 1008, 8: 950
})

sites, coverage, cost = gp.multidict({
    0: [{0,1,5}, 4.2],
    1: [{0,7,8}, 6.1],
    2: [{2,3,4,6}, 5.2],
    3: [{2,5,6}, 5.5],
    4: [{0,2,6,7,8}, 4.8],
    5: [{3,4,8}, 9.2]
})

```

### 模型开发

现在，我们通过定义决策变量、约束条件和目标函数来确定蜂窝基站覆盖问题的模型。然后进行优化过程，Gurobi 会得到建造信号塔的最佳方案（在给定的预算分配下，将人口覆盖范围最大化）。

```python
# MIP  模型

m = gp.Model("cell_tower")

build = m.addVars(len(sites), vtype=GRB.BINARY, name="Build")
is_covered = m.addVars(len(regions), vtype=GRB.BINARY, name="Is_covered")

m.addConstrs((gp.quicksum(build[t] for t in sites if r in coverage[t]) >= is_covered[r]
                        for r in regions), name="Build2cover")
m.addConstr(build.prod(cost) <= budget, name="budget")

m.setObjective(is_covered.prod(population), GRB.MAXIMIZE)

m.optimize()
```

    Gurobi Optimizer version 9.1.0 build v9.1.0rc0 (mac64)
    Thread count: 6 physical cores, 12 logical processors, using up to 12 threads
    Optimize a model with 10 rows, 15 columns and 36 nonzeros
    Model fingerprint: 0xfa0fabb2
    Variable types: 0 continuous, 15 integer (15 binary)
    Coefficient statistics:
      Matrix range     [1e+00, 9e+00]
      Objective range  [4e+02, 1e+03]
      Bounds range     [1e+00, 1e+00]
      RHS range        [2e+01, 2e+01]
    Found heuristic solution: objective -0.0000000
    Presolve removed 4 rows and 5 columns
    Presolve time: 0.00s
    Presolved: 6 rows, 10 columns, 21 nonzeros
    Variable types: 0 continuous, 10 integer (10 binary)

    Root relaxation: objective 7.051000e+03, 1 iterations, 0.00 seconds

        Nodes    |    Current Node    |     Objective Bounds      |     Work
     Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

    *    0     0               0    7051.0000000 7051.00000  0.00%     -    0s

    Explored 0 nodes (1 simplex iterations) in 0.01 seconds
    Thread count was 12 (of 12 available processors)

    Solution count 2: 7051 -0

    Optimal solution found (tolerance 1.00e-04)
    Best objective 7.051000000000e+03, best bound 7.051000000000e+03, gap 0.0000%

## 分析

优化模型的结果表明，$\$20,000,000$ 预算可以覆盖的最大人数为 7,051 人。让我们看看实现最佳结果的解决方案。

### 基站建造计划

此计划决定在该场地的哪个位置建造基站。

```python
# 显示决策变量的最佳值

for tower in build.keys():
    if (abs(build[tower].x) > 1e-6):
        print(f"\n 建造基站塔的位置为：{tower}。")
```

     建造基站塔的位置为：0.

     建造基站塔的位置为：2.

     建造基站塔的位置为：4.

### 需求完成指标

- **覆盖率**: 所建基站覆盖人口的百分比。

```python
# 建造的信号塔所覆盖的人口百分比

total_population = 0

for region in range(len(regions)):
    total_population += population[region]

coverage = round(100*m.objVal/total_population, 2)

print(f"\n 与基站建设计划相关的人口覆盖率为： {coverage}%")
```

     与基站建设计划相关的人口覆盖率为： 100.0 %

### 资源利用率指标

- **预算消耗**: 占用于建造基站的总预算的百分比。

```python
# 建造基站所需的预算百分比

total_cost = 0

for tower in range(len(sites)):
    if (abs(build[tower].x) > 0.5):
        total_cost += cost[tower]*int(build[tower].x)

budget_consumption = round(100*total_cost/budget, 2)

print(f"\n 与基站建造计划相关的预算消耗百分比为: {budget_consumption} %")
```

     与基站建造计划相关的预算消耗百分比为: 71.0 %

##  结论

在这个例子中，我们解决了建立基站塔以在满足预算限制的情况下为最多的人提供信号覆盖的问题。我们学习了如何将问题表示为一个 MIP 模型。此外，我们还学习了如何实现MIP模型公式，并使用 Gurobi 对其求解。

## 参考资料

[1] Richard Church and Charles R. Velle. "The Maximal Covering Location Problem". Papers in Regional Science, 1974, vol. 32, issue 1, 101-118.

[2] Tail Assignment Problem. https://www.gurobi.com/case_study/air-france-tail-assignment-optimization/

Copyright © 2020 Gurobi Optimization, LLC

翻译 By Arvin Xu


# Lead-Acid Models

We compare a standard porous-electrode model for lead-acid batteries with two asymptotic reductions. For a more in-depth introduction to PyBaMM models, see the [SPM notebook](./SPM.ipynb). Further details on the models can be found in [[1]](#ref)

[1] Sulzer, Valentin, S. Jon Chapman, Colin P. Please, David A. Howey, and Charles W. Monroe. "Faster Lead-Acid Battery Simulations from Porous-Electrode Theory: II. Asymptotic Analysis." arXiv preprint arXiv:1902.01774 (2019).

```python
import pybamm
import numpy as np
import os
import matplotlib.pyplot as plt
os.chdir(pybamm.__path__[0]+'/..')
```

### "Full" model

#### Electrolyte Concentration

$$
    \frac{\partial }{\partial t}\left(\epsilon c\right) = -\frac{\partial N}{\partial x} + sj, \\
    N = -\frac{\epsilon^b  D(c)}{\mathcal{C}_\text{e}}  \frac{\partial c}{\partial x}\\
    N\big|_{x=0}= N\big|_{x=1}=0, \\
    c\big|_{t=0} = 1
$$

#### Porosity

$$
    \frac{\partial \epsilon}{\partial t} = -\beta^\text{surf}j, \\
    \epsilon\big|_{t=0} = \epsilon^0
$$

#### Electrolyte Current

$$
    \frac{\partial i_{\text{e}}}{\partial x} = j, \\
    \mathcal{C}_\text{e} i_{\text{e}} = \epsilon_k^b \kappa(c)
        \left( \chi \frac{\partial}{\partial x}\log(c)
        - \frac{\partial\phi_{\text{e}}}{\partial x}\right)\\
    i_{\text{e}}\big|_{x=0}= i_{\text{e}}\big|_{x=1}=0,
$$

#### Electrode Current

$$
    \frac{\partial i_{\text{s}}}{\partial x} = -j,\\
    i_{\text{s}} = -\sigma\frac{\partial\phi_{\text{s}}}{\partial x},\\
    \phi_{\text{s}}\big|_{x=0}
        = i_{\text{s}}\big|_{x=l_\text{n}}
        = i_{\text{s}}\big|_{x=1-l_\text{p}} = 0, \\
    i_{\text{s}}\big|_{x=1}=\mathcal{I},
$$

#### interfacial current density

$$
j = \begin{cases}
    2j_0(c) \sinh\left(\eta\right), \quad &0 < x < l_\text{n} \\
    0, \quad &l_\text{n} < x < 1-l_\text{p} \\
    2j_0(c) \sinh\left(\eta\right), \quad &1-l_\text{p} < x < 1
\end{cases}
\\
\eta = \phi_{\text{s}} - \phi_\text{e} - U(c),
$$

This model is implemented in PyBaMM as the `Full` model

```python
full = pybamm.lead_acid.Full()
```

### "Leading-order" model

$$
    \frac{\mathrm{d} }{\mathrm{d} t}\left(\epsilon c\right) = (s_\text{n} - s_\text{p})\mathrm{I}, \\
    \frac{\mathrm{d} \epsilon}{\mathrm{d} t} = -\beta^\text{surf}j, \\
    j = \begin{cases}
        \mathrm{I}/l_\text{n}, \quad &0 < x < l_\text{n} \\
        0, \quad &l_\text{n} < x < 1-l_\text{p} \\
        -\mathrm{I}/l_\text{p}, \quad &1-l_\text{p} < x < 1
    \end{cases} \\
    \phi_\text{e} = -U_\text{n}(c) + \sinh^{-1}\left(\frac{\mathrm{I}}{2l_\text{n}j_{0\text{n}}(c)}\right) \\
    V = -\phi_\text{e} + U_\text{p}(c) - \sinh^{-1}\left(\frac{\mathrm{I}}{2l_\text{p}j_{0\text{p}}(c)}\right) \\
$$

This model is implemented in PyBaMM as `LOQS` (leading-order quasi-static)

```python
loqs = pybamm.lead_acid.LOQS()
```

### "Composite" model

#### Electrolyte Concentration

$$
    \frac{\partial }{\partial t}\left(\epsilon c\right) = -\frac{\partial N}{\partial x} + sj, \\
    N = -\frac{\epsilon^b  D(c)}{\mathcal{C}_\text{e}}  \frac{\partial c}{\partial x}\\
    N\big|_{x=0}= N\big|_{x=1}=0, \\
    c\big|_{t=0} = 1
$$

#### Porosity

$$
    \frac{\partial \epsilon}{\partial t} = -\beta^\text{surf}j, \\
    \epsilon\big|_{t=0} = \epsilon^0
$$

#### Interfacial current density

$$
j = \begin{cases}
        \mathrm{I}/l_\text{n}, \quad &0 < x < l_\text{n} \\
        0, \quad &l_\text{n} < x < 1-l_\text{p} \\
        -\mathrm{I}/l_\text{p}, \quad &1-l_\text{p} < x < 1
    \end{cases} \\
$$

#### Voltage

$$
V = U_{\text{eq}} + \eta_r + \eta_c + \Delta\Phi_{\text{Elec}} + \Delta\Phi_{\text{Solid}}
$$
where
\begin{align}
    & U_{\text{eq}} = U_p(\bar{c}_p) - U_n(\bar{c}_n), \\
     &\eta_{r} = -\sinh^{-1}\left(\frac{\mathcal{I}}{2\bar{j}_{0n} l_n}\right)
     -\sinh^{-1}\left(\frac{\mathcal{I}}{2\bar{j}_{0p} l_p}\right), \\
     &\eta_c =  \mathcal{C}_e\chi\left(\log(\bar{c}_{p}) - \log(\bar{c}_{n})\right), \\
    &\Delta \Phi_{\text{Elec}}= -\frac{\mathcal{C}_e \mathcal{I}}{\kappa(\bar{c})}\left(\frac{l_n}{3\epsilon_n^b} + \frac{l_s}{\epsilon_{s}^b} + \frac{l_p}{3\epsilon_p^b} \right),
     \label{eqn:SPMe:electrolyte_ohmic_losses} \\
     &\Delta \Phi_{\text{Solid}} =  -\frac{\mathcal{I}}{3}\left(\frac{l_p}{\sigma_p} + \frac{l_n}{\sigma_n} \right),
\end{align}
with
\begin{equation}
    \bar{\cdot}_{\text{n}} =  \frac{1}{l_n}\int_0^{l_n} \cdot_{\text{n}} \, \text{d}x, \quad
    \bar{\cdot}_{\text{p}} =  \frac{1}{l_p}\int_{1-l_p}^{1} \cdot_{\text{p}} \, \text{d}x,
\end{equation}

This model is implemented in PyBaMM as `Composite`

```python
composite = pybamm.lead_acid.Composite()
```

## Solving the models

We load process parameters for each model, using the same set of (default) parameters for all. In anticipation of changing the current later, we make current an input parameter

```python
# load models
models = [loqs, composite, full]

# process parameters
param = models[0].default_parameter_values
param["Current function [A]"] = "[input]"
for model in models:
    param.process_model(model)
```

Then, we discretise the models, using the default settings

```python
for model in models:
    # load and process default geometry
    geometry = model.default_geometry
    param.process_geometry(geometry)

    # discretise using default settings
    mesh = pybamm.Mesh(geometry, model.default_submesh_types, model.default_var_pts)
    disc = pybamm.Discretisation(mesh, model.default_spatial_methods)
    disc.process_model(model)
```

Finally, we solve each model using CasADi's solver and a current of 1A

```python
timer = pybamm.Timer()
solutions = {}
t_eval = np.linspace(0, 3600 * 17, 100) # time in seconds
solver = pybamm.CasadiSolver()
for model in models:
    start = timer.time()
    solution = solver.solve(model, t_eval, inputs={"Current function [A]": 1})
    end = timer.time()
    print("Solved the {} in {:.3f} seconds".format(model.name, end-start))
    solutions[model] = solution
```

    Solved the LOQS model in 0.271 seconds
    Solved the Composite model in 4.829 seconds
    Solved the Full model in 2.230 seconds

## Results

To plot the results, the variables are extracted from the solutions dictionary. For example, we can compare the voltages:

```python
for model in models:
    time = solutions[model]["Time [h]"].entries
    voltage = solutions[model]["Terminal voltage [V]"].entries
    plt.plot(time, voltage, lw=2, label=model.name)
plt.xlabel("Time [h]", fontsize=15)
plt.ylabel("Terminal voltage [V]", fontsize=15)
plt.legend(fontsize=15)
plt.show()
```

Alternatively, using `QuickPlot`, we can compare the values of some variables

```python
solution_values = [solutions[model] for model in models]
quick_plot = pybamm.QuickPlot(solution_values)
quick_plot.dynamic_plot();
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=17.000000000000004, step=0.17000000000000004…

If we update the current, setting it to be 20 A, we observe a greater discrepancy between the full model and the reduced-order models.

```python
t_eval = np.linspace(0, 3600, 100)
for model in models:
    solutions[model] = solver.solve(model, t_eval, inputs={"Current function [A]": 20})

# Plot
solution_values = [solutions[model] for model in models]
quick_plot = pybamm.QuickPlot(solution_values)
quick_plot.dynamic_plot();
```

    interactive(children=(FloatSlider(value=0.0, description='t', max=3563.636363636364, step=35.63636363636364), …

```python

```