Lab 2 Quantum Measurements

Prerequisite
- [Ch.1.4 Single Qubit Gates](https://qiskit.org/textbook/ch-states/single-qubit-gates.html)
- [Ch.2.2 Multiple Qubits and Entangled States](https://qiskit.org/textbook/ch-gates/multiple-qubits-entangled-states.html)
- [Mitigating Noise on Real Quantum Computers](https://www.youtube.com/watch?v=yuDxHJOKsVA&list=PLOFEBzvs-Vvp2xg9-POLJhQwtVktlYGbY&index=8)

Other relevant materials
- [Feynman Lectures Ch. III - 12](https://www.feynmanlectures.caltech.edu/III_12.html)
- [Quantum Operation](https://qiskit.org/documentation/tutorials/circuits/3_summary_of_quantum_operations.html)
- [Interactive Bloch Sphere](https://nonhermitian.org/kaleido/stubs/kaleidoscope.interactive.bloch_sphere.html#kaleidoscope.interactive.bloch_sphere)
- [Ch.5.2 Measurement Error Mitigation](https://qiskit.org/textbook/ch-quantum-hardware/measurement-error-mitigation.html)

```python
from qiskit import *
import numpy as np
from numpy import linalg as la
from qiskit.tools.monitor import job_monitor
import qiskit.tools.jupyter
```

Part 1: Measuring the state of a qubit

Goal

Determine the Bloch components of a qubit.

Fundamental to the operation of a quantum computer is the ability to compute the Bloch components of a qubit or qubits. These components correspond to the expectation values of the Pauli operators $X, Y, Z$, and are important quantities for applications such as quantum chemistry and optimization.  Unfortunately, it is impossible to simultaneously compute these values, thus requiring many executions of the same circuit.  In addition, measurements are restricted to the computational basis (Z-basis) so that each Pauli needs to be rotated to the standard basis to access the x and y components.  Here we verify the methods by considering the case of a random vector on the Bloch sphere.

📓 1. Express the expectation values of the Pauli operators for an arbitrary qubit state $|q\rangle$ in the computational basis.

The case for the expectation value of Pauli Z gate is given as an example.

Using the diagonal representation, also known as spectral form or orthonormal decomposition, of Pauli $Z$ gate and the relations among the Pauli gates (see [here](https://qiskit.org/textbook/ch-states/single-qubit-gates.html)), expectation values of $ X, Y, Z $ gates can be written as

$$
\begin{align}
\langle Z \rangle &=\langle q | Z | q\rangle =\langle q|0\rangle\langle 0|q\rangle - \langle q|1\rangle\langle 1|q\rangle
=|\langle 0 |q\rangle|^2 - |\langle 1 | q\rangle|^2\\\\
\langle X \rangle &= \\\\
\langle Y \rangle &=
\end{align}
\\
$$
, respectively.

Therefore, the expectation values of the Paulis for a qubit state $|q\rangle$ can be obtained by making a measurement in the standard basis after rotating the standard basis frame to lie along the corresponding axis. The probabilities of obtaining the two possible outcomes 0 and 1 are used to evaluate the desired expectation value as the above equations show.

2. Measure the Bloch sphere coordinates of a qubit using the qasm simulator and plot the vector on the Bloch sphere.

📓Step A. Create a qubit state using the circuit method, initialize with two random complex numbers as the parameter.

To learn how to use the function `initialize`, check [here](https://qiskit.org/documentation/tutorials/circuits/3_summary_of_quantum_operations.html#Arbitrary-initialization). (go to the `arbitrary initialization` section.)

```python
qc = QuantumCircuit(1)

#### your code goes here

```

📓 Step B. Build the circuits to measure the expectation values of $X, Y, Z$ gate based on your answers to the question 1.  Run the cell below to estimate the Bloch sphere coordinates of the qubit from step A using the qasm simulator.

The circuit for $Z$ gate measurement is given as an example.

```python
# z measurement of qubit 0
measure_z = QuantumCircuit(1,1)
measure_z.measure(0,0)

# x measurement of qubit 0
measure_x = QuantumCircuit(1,1)
# your code goes here

# y measurement of qubit 0
measure_y = QuantumCircuit(1,1)
# your code goes here

shots = 2**14 # number of samples used for statistics
sim = Aer.get_backend('qasm_simulator')
bloch_vector_measure = []
for measure_circuit in [measure_x, measure_y, measure_z]:

    # run the circuit with the selected measurement and get the number of samples that output each bit value
    counts = execute(qc+measure_circuit, sim, shots=shots).result().get_counts()

    # calculate the probabilities for each bit value
    probs = {}
    for output in ['0','1']:
        if output in counts:
            probs[output] = counts[output]/shots
        else:
            probs[output] = 0

    bloch_vector_measure.append( probs['0'] -  probs['1'] )

# normalizing the Bloch sphere vector
bloch_vector = bloch_vector_measure/la.norm(bloch_vector_measure)

print('The Bloch sphere coordinates are [{0:4.3f}, {1:4.3f}, {2:4.3f}]'
      .format(*bloch_vector))
```

Step C. Plot the vector on the Bloch sphere.

Note that the following cell for the interactive bloch_sphere would not run properly unless you work in [IQX](https://quantum-computing.ibm.com/login). You can either use `plot_bloch_vector` for the non-interactive version or install `kaleidoscope` by running

```
pip install kaleidoscope

```
in a terminal.  You also need to restart your kernel after the installation.  To learn more about how to use the interactive Bloch sphere, go [here](https://nonhermitian.org/kaleido/stubs/kaleidoscope.interactive.bloch_sphere.html#kaleidoscope.interactive.bloch_sphere).

```python
from kaleidoscope.interactive import bloch_sphere

bloch_sphere(bloch_vector, vectors_annotation=True)
```

```python
from qiskit.visualization import plot_bloch_vector

plot_bloch_vector( bloch_vector )
```

Part 2: Measuring Energy

Goal

Evaluate the energy levels of the hydrogen ground state using qasm simulator.

The energy of a quantum system can be estimated by measuring the expectation value of its Hamiltonian, which is a Hermitian operator, through the procedure we mastered in part 1.

The ground state of hydrogen is not defined as a single unique state but actually contains four different states due to the spins of the electron and proton. In part 2 of this lab, we evaluate the energy difference among these four states, which is from the `hyperfine splitting`, by computing the energy expectation value for the system of two spins with the Hamiltonian expressed in Pauli operators. For more information about `hyperfine structure`, see [here](https://www.feynmanlectures.caltech.edu/III_12.html)

Consider the system with two qubit interaction Hamiltonian $H = A(XX+YY+ZZ)$ where $A = 1.47e^{-6} eV$ and $X, Y, Z$ are Pauli gates. Then the energy expectation value of the system can be evaluated by combining the expectation value of each term in the Hamiltonian.
In this case, $E = \langle H\rangle = A( \langle XX\rangle + \langle YY\rangle + \langle ZZ\rangle )$.

📓 1. Express the expectation value of each term in the Hamiltonian for an arbitrary two qubit state   $|\psi \rangle$ in the computational basis.

The case for the term $\langle ZZ\rangle$ is given as an example.

$$
\begin{align}
\langle ZZ\rangle &=\langle \psi | ZZ | \psi\rangle =\langle \psi|(|0\rangle\langle 0| - |1\rangle\langle 1|)\otimes(|0\rangle\langle 0| - |1\rangle\langle 1|) |\psi\rangle
=|\langle 00|\psi\rangle|^2 - |\langle 01 | \psi\rangle|^2 - |\langle 10 | \psi\rangle|^2 + |\langle 11|\psi\rangle|^2\\\\
\langle XX\rangle &= \\\\
\langle YY\rangle &=
\end{align}
$$

2. Measure the expected energy of the system using the qasm simulator when two qubits are entangled. Regard the bell basis, four different entangled states.

📓Step A. Construct the circuits to prepare four different bell states.

Let's label each bell state as
$$
\begin{align}
Tri1 &= \frac{1}{\sqrt2} (|00\rangle + |11\rangle)\\
Tri2 &= \frac{1}{\sqrt2} (|00\rangle - |11\rangle)\\
Tri3 &= \frac{1}{\sqrt2} (|01\rangle + |10\rangle)\\
Sing &= \frac{1}{\sqrt2} (|10\rangle - |01\rangle)
\end{align}
$$

```python
# circuit for the state Tri1
Tri1 = QuantumCircuit(2)
# your code goes here

# circuit for the state Tri2
Tri2 = QuantumCircuit(2)
# your code goes here

# circuit for the state Tri3
Tri3 = QuantumCircuit(2)
# your code goes here

# circuit for the state Sing
Sing = QuantumCircuit(2)
# your code goes here

```

📓Step B. Create the circuits to measure the expectation value of each term in the Hamiltonian based on your answer to the question 1.

```python
# <ZZ>
measure_ZZ = QuantumCircuit(2)
measure_ZZ.measure_all()

# <XX>
measure_XX = QuantumCircuit(2)
# your code goes here

# <YY>
measure_YY = QuantumCircuit(2)
# your code goes here

```

Step C. Execute the circuits on qasm simulator by running the cell below and evaluate the energy expectation value for each state.

```python
shots = 2**14 # number of samples used for statistics

A = 1.47e-6 #unit of A is eV
E_sim = []
for state_init in [Tri1,Tri2,Tri3,Sing]:
    Energy_meas = []
    for measure_circuit in [measure_XX, measure_YY, measure_ZZ]:

        # run the circuit with the selected measurement and get the number of samples that output each bit value
        qc = state_init+measure_circuit
        counts = execute(qc, sim, shots=shots).result().get_counts()

        # calculate the probabilities for each computational basis
        probs = {}
        for output in ['00','01', '10', '11']:
            if output in counts:
                probs[output] = counts[output]/shots
            else:
                probs[output] = 0

        Energy_meas.append( probs['00'] - probs['01'] - probs['10'] + probs['11'] )

    E_sim.append(A * np.sum(np.array(Energy_meas)))
```

```python
# Run this cell to print out your results

print('Energy expectation value of the state Tri1 : {:.3e} eV'.format(E_sim[0]))
print('Energy expectation value of the state Tri2 : {:.3e} eV'.format(E_sim[1]))
print('Energy expectation value of the state Tri3 : {:.3e} eV'.format(E_sim[2]))
print('Energy expectation value of the state Sing : {:.3e} eV'.format(E_sim[3]))
```

Step D. Understanding the result.

If you found the energy expectation values successfully, you would have obtained exactly the same value, $A (= 1.47e^{-6} eV)$, for the triplet states, $|Tri1\rangle, |Tri2\rangle, |Tri3\rangle$ and one lower energy level, $-3A (= -4.41e^{-6} eV)$ for the singlet state $|Sing\rangle$.

What we have done here is measuring the energies of the four different spin states corresponding to the ground state of hydrogen and observed `hyperfine structure` in the energy levels caused by spin-spin coupling.  This tiny energy difference between the singlet and triplet states is the reason for the famous 21-cm wavelength radiation used to map the structure of the galaxy.

In the cell below, we verify the wavelength of the emission from the transition between the triplet states and singlet state.

```python
# reduced plank constant in (eV) and the speed of light(cgs units)
hbar, c = 4.1357e-15, 3e10

# energy difference between the triplets and singlet
E_del = abs(E_sim[0] - E_sim[3])

# frequency associated with the energy difference
f = E_del/hbar

# convert frequency to wavelength in (cm)
wavelength = c/f

print('The wavelength of the radiation from the transition\
 in the hyperfine structure is : {:.1f} cm'.format(wavelength))
```

Part 3: Execute the circuits on Quantum Computer

Goal

 Re-run the circuits on a IBM quantum system.  Perform measurement error mitigations on the result to improve the accuracy in the energy estimation.

Step A. Run the following cells to load your account and select the backend

```python
provider = IBMQ.load_account()
```

```python
backend = provider.get_backend('ibmq_athens')
```

Step B. Execute the circuits on the quantum system.

In Lab1 when we executed multiple circuits on a real quantum system, we submitted each circuit as a separate job which produces the multiple job ids. This time, we put all the circuits in a list and execute the list of the circuits as one job. In this way, all the circuit executions can happen at once, which would possibly decrease your wait time in the queue.

In addition, `transpile` is not used here as all the circuits that we run consist of one or two qubit gates.  We can still specify the initial_layout and optimization_level through `execute` function.  Without using `transpile`, the transpiled circuits are not accessible which is not a concern for this case.

📓 Check the backend configuration information and error map through the widget to determine your initial_layout.

```python
# run this cell to get the backend information through the widget
backend
```

```python
# assign your choice for the initial layout to the list variable `initial_layout`.
initial_layout =
```

Run the following cell to execute the circuits with the initial_layout on the backend.

```python
qc_all = [state_init+measure_circuit for state_init in [Tri1,Tri2,Tri3,Sing]
          for measure_circuit in [measure_XX, measure_YY, measure_ZZ] ]

shots = 8192
job = execute(qc_all, backend, initial_layout=initial_layout, optimization_level=3, shots=shots)
print(job.job_id())
job_monitor(job)
```

```python
# getting the results of your job
results = job.result()
```

```python
## To access the results of the completed job
#results = backend.retrieve_job('job_id').result()
```

Step C. Estimate the ground state energy levels from the results of the previous step by executing the cells below.

```python
def Energy(results, shots):
    """Compute the energy levels of the hydrogen ground state.

    Parameters:
        results (obj): results, results from executing the circuits for measuring a Hamiltonian.
        shots (int): shots, number of shots used for the circuit execution.

    Returns:
        Energy (list): energy values of the four different hydrogen ground states
    """
    E = []
    A = 1.47e-6

    for ind_state in range(4):
        Energy_meas = []
        for ind_comp in range(3):
            counts = results.get_counts(ind_state*3+ind_comp)

            # calculate the probabilities for each computational basis
            probs = {}
            for output in ['00','01', '10', '11']:
                if output in counts:
                    probs[output] = counts[output]/shots
                else:
                    probs[output] = 0

            Energy_meas.append( probs['00'] - probs['01'] - probs['10'] + probs['11'] )

        E.append(A * np.sum(np.array(Energy_meas)))

    return E
```

```python
E = Energy(results, shots)

print('Energy expectation value of the state Tri1 : {:.3e} eV'.format(E[0]))
print('Energy expectation value of the state Tri2 : {:.3e} eV'.format(E[1]))
print('Energy expectation value of the state Tri3 : {:.3e} eV'.format(E[2]))
print('Energy expectation value of the state Sing : {:.3e} eV'.format(E[3]))
```

Step D. Measurement error mitigation.

The results you obtained from running the circuits on the quantum system are not exact due to the noise from the various sources such as energy relaxation, dephasing, crosstalk between qubits, etc. In this step, we will alleviate the effects of the noise through the measurement error mitigation. Before we start, watch this [video](https://www.youtube.com/watch?v=yuDxHJOKsVA&list=PLOFEBzvs-Vvp2xg9-POLJhQwtVktlYGbY&index=8).

```python
from qiskit.ignis.mitigation.measurement import *
```

📓Construct the circuits to profile the measurement errors of all basis states using the function 'complete_meas_cal'.  Obtain the measurement filter object, 'meas_filter', which will be applied to the noisy results to mitigate readout (measurement) error.

For further helpful information to complete this task, check [here](https://qiskit.org/textbook/ch-quantum-hardware/measurement-error-mitigation.html).

```python
# your code to create the circuits, meas_calibs, goes here
meas_calibs, state_labels =

# execute meas_calibs on your choice of the backend
job = execute(meas_calibs, backend, shots = shots)
print(job.job_id())
job_monitor(job)
cal_results = job.result()
## To access the results of the completed job
#cal_results = backend.retrieve_job('job_id').result()

# your code to obtain the measurement filter object, 'meas_filter', goes here

```

```python
results_new = meas_filter.apply(results)
```

```python
E_new = Energy(results_new, shots)

print('Energy expectation value of the state Tri1 : {:.3e} eV'.format(E_new[0]))
print('Energy expectation value of the state Tri2 : {:.3e} eV'.format(E_new[1]))
print('Energy expectation value of the state Tri3 : {:.3e} eV'.format(E_new[2]))
print('Energy expectation value of the state Sing : {:.3e} eV'.format(E_new[3]))
```

Step E. Interpret the result.

📓 Compute the relative errors (or the fractional error) of the energy values for all four states with and without measurement error mitigation.

```python
# results for the energy estimation from the simulation,
# execution on a quantum system without error mitigation and
# with error mitigation in numpy array format
Energy_exact, Energy_exp_orig, Energy_exp_new = np.array(E_sim), np.array(E), np.array(E_new)
```

```python
# Calculate the relative errors of the energy values without error mitigation
# and assign to the numpy array variable `Err_rel_orig` of size 4
Err_rel_orig =
```

```python
# Calculate the relative errors of the energy values with error mitigation
# and assign to the numpy array variable `Err_rel_new` of size 4
Err_rel_new =
```

```python
np.set_printoptions(precision=3)

print('The relative errors of the energy values for four bell basis\
 without measurement error mitigation : {}'.format(Err_rel_orig))
```

```python
np.set_printoptions(precision=3)

print('The relative errors of the energy values for four bell basis\
 with measurement error mitigation : {}'.format(Err_rel_new))
```

📓 Compare the size of the errors before and after the measurement error mitigation and discuss about the effect of the readout error regarding the error map information of the backend that you selected.

**Your answer:**


# 诸闭壳层量子化学方法的密度矩阵

> 创建时间：2021-01-04；最后修改：2021-06-10

在这份简短笔记中，我们会回顾一些量子化学方法的密度矩阵，及其性质。大体的结论在下述表格中。

我们在这里只讨论闭壳层与实函数的情况。

| 方法 | RHF 轨道基函数 | 能量关系 | $P_p^q$ 对称性 | $\Gamma_{pr}^{qs}$ 对称性 | 1-RDM 与电子数 | $\Gamma_{pr}^{qs}$ 与 $P_p^q$ 的关系 | $\mathbf{P}$ 幂等性 | $P_i^a$ 为零 | $\Gamma_{ij}^{ab}$ 为零 | $F_p^q$ 对称性 | 1-RDM 偶极矩 |
| ------- |:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
| RHF     | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ | √ |
| Full-CI | √ | √ | √ | √ | √ | √ | × | × | × | √ | √ |
| MP2     | √ | √ | √ | √ | √ | × | × | √ | × | × | × |
| CCSD    | √ | √ | √ | √ | √ | √ | × | × | × | × | × |
| CISD    | √ | √ | √ | √ | √ | √ | × | × | × | × | × |
| CASCI   | × | √ | √ | √ | √ | √ | × | N/A | N/A | × | × |
| CASSCF  | × | √ | √ | √ | √ | √ | × | N/A | N/A | √ | √ |

之所以上面表格中 CASCI、CASSCF 方法不能说 $P_i^a$ (密度矩阵的占据-非占) 与 $\Gamma_{ij}^{ab}$ (2-RDM 的占据-非占)，是因为它们并是非基于 RHF 参考态的方法，不存在确切的占据与未占轨道。

## 预定义

```python
from pyscf import gto, scf, mp, cc, ci, mcscf, fci
import numpy as np
from functools import partial

np.einsum = partial(np.einsum, optimize=True)
np.set_printoptions(precision=5, linewidth=150, suppress=True)
```

这里讨论的密度矩阵并非是 Full-CI 的情形矩阵，而是会因方法各异而不同的。

这里采用相对比较严格的 Einstein Summation Convention，即被求和角标必须是一个在上，一个在下。

这份文档中使用下述上下标：

- $p, q, r, s, m, n$ 分子轨道

- $i, j$ 分子占据轨道，$a, b$ 分子未占轨道

- $\mu, \nu, \kappa, \lambda$ 原子轨道

分子轨道函数 $\phi_p (\boldsymbol{r})$ 与原子轨道函数 $\phi_\mu (\boldsymbol{r})$ 之间满足关系 ($C_p^\mu$ 称为原子轨道系数)

$$
\phi_p (\boldsymbol{r}) = C_p^\mu \phi_\mu (\boldsymbol{r})
$$

我们假定了研究体系必然是实函数，但我们暂且定义函数的共轭记号如下：(不使用 Einstein Summation)

$$
\phi^p (\boldsymbol{r}) = \phi_p^* (\boldsymbol{r})
$$

分子轨道之间是正交归一的，但原子轨道需要用重叠积分：

$$
\int \phi_p (\boldsymbol{r}) \phi^q (\boldsymbol{r}) \, \mathrm{d} \boldsymbol{r} = \delta_p^q, \; \int \phi_\mu (\boldsymbol{r}) \phi^\nu (\boldsymbol{r}) \, \mathrm{d} \boldsymbol{r} = S_\mu^\nu
$$

## Full-CI 密度矩阵的定义与性质

这里只作理论上的讨论。Full-CI 密度矩阵程序上的实现会在后面呈现。

密度矩阵与约化密度的定义有关。由于我们只讨论闭壳层情形，因此波函数可以安全地写成空间坐标的函数。

### 1-RDM 与基转换关系

我们回顾一阶约化密度 $\rho(\boldsymbol{r}; \boldsymbol{r}')$：

$$
\rho(\boldsymbol{r}; \boldsymbol{r}') = \idotsint \Psi^* (\boldsymbol{r}, \boldsymbol{r}_2, \boldsymbol{r}_3, \cdots, \boldsymbol{r}_{n_\mathrm{elec}}) \Psi (\boldsymbol{r}', \boldsymbol{r}_2, \boldsymbol{r}_3, \cdots, \boldsymbol{r}_{n_\mathrm{elec}}) \, \mathrm{d} \boldsymbol{r}_2 \, \mathrm{d} \boldsymbol{r}_3 \cdots \, \mathrm{d} \boldsymbol{r}_{n_\mathrm{elec}}
$$

但是现在只有有限的基函数展开一阶约化密度；如果这组基函数是 RHF 分子轨道 $\{ \phi_{p} (\boldsymbol{r}) \}$，那么定义下述分子轨道基一阶约化密度矩阵 $P_p^q$ (One-Order Reduced Density Matrix, 1-RDM)

$$
\rho(\boldsymbol{r}; \boldsymbol{r}') = P_p^q \phi^p (\boldsymbol{r}) \phi_q (\boldsymbol{r}')
$$

如果是原子轨道 $\{ \phi_\mu (\boldsymbol{r}) \}$，那么它称为原子轨道基 1-RDM $P_\mu^\nu$

$$
\rho(\boldsymbol{r}; \boldsymbol{r}') = P_\mu^\nu \phi^\mu (\boldsymbol{r}) \phi_\nu (\boldsymbol{r}')
$$

依据分子轨道与原子轨道间的关系，有

$$
\rho(\boldsymbol{r}; \boldsymbol{r}') = C_\mu^p P_p^q C_q^\nu \phi^\mu (\boldsymbol{r}) \phi_\nu (\boldsymbol{r}')
$$

因此，原子轨道基与分子轨道基的 1-RDM 间存在关系

$$
P_\mu^\nu = C_\mu^p P_p^q C_q^\nu
$$

### 1-RDM 迹

当 $\boldsymbol{r}, \boldsymbol{r}'$ 相同时，我们会将一阶约化密度简记为电子密度 $\rho(\boldsymbol{r}) = \rho(\boldsymbol{r}; \boldsymbol{r})$。

一阶约化密度具有积分为电子数的性质：

$$
\int \rho(\boldsymbol{r}) \, \mathrm{d} \boldsymbol{r} = n_\mathrm{elec}
$$

在分子轨道基的表示下，上式可以写为

$$
P_p^q \int \phi^p (\boldsymbol{r}) \phi_q (\boldsymbol{r}) \, \mathrm{d} \boldsymbol{r} = P_p^q \delta^p_q = \mathrm{tr} (\mathbf{P}) = n_\mathrm{nelec}
$$

### 1-RDM 对称性

首先我们可以证明下述等式：

$$
\begin{align}
&\quad\ \iint \phi_p (\boldsymbol{r}) \rho(\boldsymbol{r}; \boldsymbol{r}') \phi^q (\boldsymbol{r}') \, \mathrm{d} \boldsymbol{r} \, \mathrm{d} \boldsymbol{r}' \\
&= \iint \phi_p (\boldsymbol{r}) P_r^s \phi^r (\boldsymbol{r}) \phi_s (\boldsymbol{r}') \phi^q (\boldsymbol{r}') \, \mathrm{d} \boldsymbol{r} \, \mathrm{d} \boldsymbol{r}' \\
&= P_r^s \delta_p^r \delta_s^q = P_p^q
\end{align}
$$

对于上式，如果我们交换被积元变量 $\boldsymbol{r}, \boldsymbol{r'}$、并对表达式取共轭，得到 (不使用 Einstein Summation)

$$
\iint \phi_p (\boldsymbol{r}) \rho(\boldsymbol{r}; \boldsymbol{r}') \phi^q (\boldsymbol{r}') \, \mathrm{d} \boldsymbol{r} \, \mathrm{d} \boldsymbol{r}' = \iint \phi_q (\boldsymbol{r}) \rho^*(\boldsymbol{r}'; \boldsymbol{r}) \phi^p (\boldsymbol{r}') \, \mathrm{d} \boldsymbol{r} \, \mathrm{d} \boldsymbol{r}'
$$

如果我们再利用实数情形下，根据一阶约化密度的定义，有 $\rho(\boldsymbol{r}; \boldsymbol{r}') = \rho(\boldsymbol{r}'; \boldsymbol{r}) = \rho^*(\boldsymbol{r}'; \boldsymbol{r})$，那么可以立即得到 (不使用 Einstein Summation)

$$
P_p^q = \iint \phi_p (\boldsymbol{r}) \rho(\boldsymbol{r}; \boldsymbol{r}') \phi^q (\boldsymbol{r}') \, \mathrm{d} \boldsymbol{r} \, \mathrm{d} \boldsymbol{r}' = \iint \phi_q (\boldsymbol{r}) \rho(\boldsymbol{r}; \boldsymbol{r}') \phi^p (\boldsymbol{r}') \, \mathrm{d} \boldsymbol{r} \, \mathrm{d} \boldsymbol{r}' = P_q^p
$$

即 1-RDM 矩阵 $P_p^q$ 是对称矩阵。

### 2-RDM

与 1-RDM 相同地，依据二阶约化密度 $\gamma (\boldsymbol{r}_1, \boldsymbol{r}_2; \boldsymbol{r}'_1, \boldsymbol{r}'_2)$ 的定义：

$$
\gamma (\boldsymbol{r}_1, \boldsymbol{r}_2; \boldsymbol{r}'_1, \boldsymbol{r}'_2) = \idotsint \Psi^* (\boldsymbol{r}_1, \boldsymbol{r}_2, \boldsymbol{r}_3, \cdots, \boldsymbol{r}_{n_\mathrm{elec}}) \Psi (\boldsymbol{r}'_1, \boldsymbol{r}'_2, \boldsymbol{r}_3, \cdots, \boldsymbol{r}_{n_\mathrm{elec}}) \, \mathrm{d} \boldsymbol{r}_3 \cdots \, \mathrm{d} \boldsymbol{r}_{n_\mathrm{elec}}
$$

用分子轨道基作展开，可以定义分子轨道基的二阶约化密度矩阵 $\Gamma_{pr}^{qs}$ (Two-Order Reduced Density Matrix, 2-RDM)

$$
\gamma (\boldsymbol{r}_1, \boldsymbol{r}_2; \boldsymbol{r}'_1, \boldsymbol{r}'_2) = \Gamma_{pr}^{qs} \phi^p (\boldsymbol{r}_1) \phi_q (\boldsymbol{r}'_1) \phi^r (\boldsymbol{r}_2) \phi_s (\boldsymbol{r}'_2)
$$

原子与分子轨道基转换也与 1-RDM 类似：

$$
\Gamma_{\mu \kappa}^{\nu \lambda} = C_\mu^p C^\nu_q \Gamma_{pr}^{qs} C_\kappa^r C^\lambda_s
$$

### 2-RDM 与 1-RDM 的关系

出于全同粒子的性质，2-RDM 与 1-RDM 之间存在关系：

$$
\rho(\boldsymbol{r}_1; \boldsymbol{r}'_1) = \frac{1}{n_\mathrm{elec} - 1} \iint \gamma (\boldsymbol{r}_1, \boldsymbol{r}_2; \boldsymbol{r}'_1, \boldsymbol{r}_2) \, \mathrm{d} \boldsymbol{r}_2
$$

对上式展开并作一部分积分后，可以得到

$$
P_p^q \phi^p (\boldsymbol{r}_1) \phi_q (\boldsymbol{r}'_1) = \frac{1}{n_\mathrm{elec} - 1} \Gamma_{pr}^{qm} \phi^p (\boldsymbol{r}_1) \phi_q (\boldsymbol{r}'_1) \delta_m^r
$$

由于上式要在任意的 $\boldsymbol{r}_1, \boldsymbol{r}'_1$ 的取值下成立，因此可以认为

$$
P_p^q = \frac{1}{n_\mathrm{elec} - 1} \Gamma_{pr}^{qm} \delta_m^r
$$

注意上式要对等式右边作关于 $r, m$ 角标的求和。

### 2-RDM 对称性

分析 2-RDM 对称性相对比较麻烦。这里就略过讨论了。我们仅指出，实数闭壳层下的 2-RDM 应当具有二重对称性，而不具有更高的对称性：

$$
\Gamma_{pr}^{qs} = \Gamma_{rp}^{sq}
$$

### 密度矩阵与能量

这是最关键的一个性质。密度矩阵可以用来表示电子态的能量。

现在记原子轨道基组下的单电子算符积分为 $h_\mu^\nu$、双电子算符积分为 $g_{\mu \kappa}^{\nu \lambda}$，其中单电子算符包含动能、原子核-电子库伦势能、电场势能等贡献，双电子算符包含电子-电子库伦势能贡献。那么，体系单点能为

$$
E_\mathrm{tot} = E_\mathrm{elec} + E_\mathrm{nuc} = h_\mu^\nu P_\nu^\mu + \frac{1}{2} g_{\mu \kappa}^{\nu \lambda} \Gamma_{\nu \lambda}^{\mu \kappa} + E_\mathrm{nuc}
$$

### 1-RDM 与偶极矩

Full-CI 的 1-RDM 可以直接用以计算偶极矩；以 $z$ 轴方向施加的电场为例：

$$
d_z = - z_\mu^\nu P_\nu^\mu
$$

其中 $z_\mu^\nu = \langle \mu | z | \nu \rangle$。

### 广义 Fock 矩阵

广义 Fock 矩阵定义为

$$
F_p^q = h_p^r P_r^q + g_{pr}^{ms} \Gamma_{ms}^{qr}
$$

特别地，在 RHF 下，它是对角矩阵。而在 Full-CI 下，它会是对称矩阵。

## RHF 密度矩阵特有性质

### 幂等性

在 Conanical-HF 下，幂等性是几乎显然的：$P_p^q$ 一定是对角矩阵；如果 $p$ 所代表的轨道是占据轨道，那么一定填了因为填了两个电子而值为 2，否则为零。因此，Conanical-HF 下一定满足

$$
P_p^m P_m^q = 2 P_p^q
$$

一般程序都只会给出 Conanical-HF 的结果。但若讨论 Nonconanical-HF 时 $P_p^q$ 未必是对角矩阵，但上述结论应仍然成立。

### 非占-占据部分为零

Hartree-Fock 方法严格地将轨道分为占据与非占据。因此，Canonical 或 Nonconanical HF 方法都会保证 1-RDM 是块状对角化的；即在占据-非占 $P_i^a$，非占-占据 $P_a^i$，非占-非占 $P_a^b$ 均严格为零。对于 2-RDM 是类似的。

由于 Hartree-Fock 方法没有考虑非占轨道的贡献，因此任何 Post-HF 方法均一定程度上有激发态的贡献。一般来说，非占-非占的 $P_a^b$ 贡献总是存在的；但占据-非占或非占-占据的 $P_i^a$ 与 $P_a^i$ 则未必存在。

## 通用计算函数

下面的文档仅仅是验证开头的表格的代码。

### 分子定义：水分子

- `mol` 水分子实例；

- `nelec` $n_\mathrm{elec}$ 电子数；

- `nocc` $n_\mathrm{occ}$ 占据轨道数；

- `h` 原子轨道基 $h_\mu^\nu$，维度 $(\mu, \nu)$；

- `g` 原子轨道基 $g_{\mu \kappa}^{\nu \lambda}$，维度 $(\mu, \nu, \kappa, \lambda)$；

- `S` 原子轨道基 $S_\mu^\nu$，维度 $(\mu, \nu)$；

- `mf_rhf` RHF 实例。

```python
mol = gto.Mole()
mol.atom = """
O  0. 0. 0.
H  0. 0. 1.
H  0. 1. 0.
"""
mol.basis = "6-31G"
mol.verbose = 0
mol.build()
```

    <pyscf.gto.mole.Mole at 0x7feae8ff9580>

```python
nelec = mol.nelectron
nocc = mol.nelec[0]
nelec, nocc
```

    (10, 5)

```python
h = mol.intor("int1e_kin") + mol.intor("int1e_nuc")
g = mol.intor("int2e")
S = mol.intor("int1e_ovlp")
```

```python
mf_rhf = scf.RHF(mol).run()
```

### 验证能量表达式

验证

$$
E_\mathrm{tot} = h_\mu^\nu P_\nu^\mu + \frac{1}{2} g_{\mu \kappa}^{\nu \lambda} \Gamma_{\nu \lambda}^{\mu \kappa} + E_\mathrm{nuc} = h_p^q P_q^p + \frac{1}{2} g_{pr}^{qs} \Gamma_{qs}^{pr} + E_\mathrm{nuc}
$$

```python
eng_nuc = mol.energy_nuc()
```

```python
def verify_energy_relation(eng, eng_nuc, rdm1, rdm2, h_mo, g_mo):
    return np.allclose(np.einsum("pq, qp ->", h_mo, rdm1) + 0.5 * np.einsum("pqrs, qpsr ->", g_mo, rdm2) + eng_nuc, eng)
```

### 验证 1-RDM 对称性

验证 $P_p^q = P_q^p$。

```python
def verify_rdm1_symm(rdm1):
    # Output: 1-RDM symmetric property
    return np.allclose(rdm1, rdm1.T)
```

### 验证 2-RDM 对称性

验证 $\Gamma_{pr}^{qs} = \Gamma_{rp}^{sq}$。

```python
def verify_rdm2_symm(rdm2):
    return np.allclose(rdm2, np.einsum("pqrs -> rspq", rdm2))
```

### 验证 1-RDM 的迹

验证 $P_p^r \delta_r^p = n_\mathrm{elec}$。

```python
def verify_rdm1_tr(rdm1):
    return np.allclose(rdm1.trace(), nelec)
```

### 验证 1-RDM 与 2-RDM 的关系

验证 $P_p^q = (n_\mathrm{elec} - 1)^{-1} \Gamma_{pr}^{qm} \delta_m^r$。

```python
def verify_rdm12_relation(rdm1, rdm2):
    return np.allclose(rdm1, (nelec - 1)**-1 * rdm2.diagonal(axis1=-1, axis2=-2).sum(axis=-1))
```

### 验证 1-RDM 幂等性

验证 $P_p^m P_m^q = 2 P_p^q$。

```python
def verify_rdm1_idomp(rdm1):
    return np.allclose(rdm1 @ rdm1, 2 * rdm1)
```

### 验证 $P_i^a$ 为零

这里实际上同时验证 $P_a^i$ 是否为零。

```python
def verify_rdm1_ov(rdm1):
    mat1 = rdm1[nocc:, :nocc]
    mat2 = rdm1[:nocc, nocc:]
    return np.allclose(mat1, np.zeros_like(mat1)) and np.allclose(mat2, np.zeros_like(mat2))
```

### 验证 $\Gamma_{ij}^{ab}$ 为零

```python
def verify_rdm2_ovov(rdm2):
    mat = rdm2[:nocc, nocc:, :nocc, nocc:]
    return np.allclose(mat, np.zeros_like(mat))
```

### 验证广义 Fock 矩阵对称性

$$
F_p^q = h_p^r P_r^q + g_{pr}^{ms} \Gamma_{ms}^{qr}
$$

```python
def verify_gF_symm(rdm1, rdm2, h_mo, g_mo):
    gF = np.einsum("pr, rq -> pq", h_mo, rdm1) + np.einsum("pmrs, mqsr -> pq", g_mo, rdm2)
    return np.allclose(gF, gF.T, atol=1e-4)
```

### 偶极矩的验证

通过 1-RDM 计算的偶极矩为 (不考虑原子核影响)

$$
d_z = - z_\mu^\nu P_\nu^\mu
$$

但另一种偶极矩的计算方式是对 $h_\mu^\nu$ 作更改，求得该情形下的能量作数值差分得到。数值差分的间隙设定为 1e-4 单位电场强度。

```python
h_field = 1e-4

def get_hcore_p(mol_=mol):
    return mol.intor("int1e_kin") + mol.intor("int1e_nuc") - h_field * mol.intor("int1e_r")[2]
def get_hcore_m(mol_=mol):
    return mol.intor("int1e_kin") + mol.intor("int1e_nuc") + h_field * mol.intor("int1e_r")[2]

mf_rhf_p, mf_rhf_m = scf.RHF(mol), scf.RHF(mol)
mf_rhf_p.get_hcore = get_hcore_p
mf_rhf_m.get_hcore = get_hcore_m
mf_rhf_p.run(), mf_rhf_m.run()

charges = mol.atom_charges()
coords  = mol.atom_coords()
nucl_dip = np.einsum('i,ix->x', charges, coords)
```

```python
def verify_dip(method, rdm1, z_intg):
    mf_met_m, _, _, _ = method(mf_rhf_m)
    mf_met_p, _, _, _ = method(mf_rhf_p)
    dip_num = (mf_met_p.e_tot - mf_met_m.e_tot) / (2 * h_field) + nucl_dip[2]
    dip_rdm1 = - (rdm1 * z_intg).sum() + nucl_dip[2]
    return np.allclose(dip_num, dip_rdm1, atol=1e-4)
```

## 各种方法的验证

### 总验证程序

```python
def verify_all(method):
    # rdm1, rdm2 here are both in mo_basis
    mf_met, C, rdm1, rdm2 = method(mf_rhf)
    h_mo = C.T @ h @ C
    g_mo = np.einsum("up, vq, uvkl, kr, ls -> pqrs", C, C, g, C, C)
    z_intg = C.T @ mol.intor("int1e_r")[2] @ C
    print("===  Energy Relat  ===  ", verify_energy_relation(mf_met.e_tot, eng_nuc, rdm1, rdm2, h_mo, g_mo))
    print("===   1-RDM Symm   ===  ", verify_rdm1_symm(rdm1))
    print("===   2-RDM Symm   ===  ", verify_rdm2_symm(rdm2))
    print("===   1-RDM Trace  ===  ", verify_rdm1_tr(rdm1))
    print("===  12-RDM Relat  ===  ", verify_rdm12_relation(rdm1, rdm2))
    print("===   1-RDM Idomp  ===  ", verify_rdm1_idomp(rdm1))
    print("===   1-RDM ov     ===  ", verify_rdm1_ov(rdm1))
    print("===   2-RDM ovov   ===  ", verify_rdm2_ovov(rdm2))
    print("=== GenFock Symm   ===  ", verify_gF_symm(rdm1, rdm2, h_mo, g_mo))
    print("===   1-RDM Dipole ===  ", verify_dip(method, rdm1, z_intg))
```

### RHF

```python
def method_rhf(mf_rhf):
    mf_met = mf_rhf
    C = mf_rhf.mo_coeff
    Cinv = np.linalg.inv(C)
    # In AO basis
    rdm1 = mf_rhf.make_rdm1()
    rdm2 = np.einsum("uv, kl -> uvkl", rdm1, rdm1) - 0.5 * np.einsum("uv, kl -> ukvl", rdm1, rdm1)
    # Transform to MO basis
    rdm1 = np.einsum("pu, uv, qv -> pq", Cinv, rdm1, Cinv)
    rdm2 = np.einsum("pu, qv, uvkl, rk, sl -> pqrs", Cinv, Cinv, rdm2, Cinv, Cinv)
    return mf_met, C, rdm1, rdm2
```

```python
verify_all(method_rhf)
```

    ===  Energy Relat  ===   True
    ===   1-RDM Symm   ===   True
    ===   2-RDM Symm   ===   True
    ===   1-RDM Trace  ===   True
    ===  12-RDM Relat  ===   True
    ===   1-RDM Idomp  ===   True
    ===   1-RDM ov     ===   True
    ===   2-RDM ovov   ===   True
    === GenFock Symm   ===   True
    ===   1-RDM Dipole ===   True

### Full-CI

```python
def method_fci(mf_rhf):
    mf_met = fci.FCI(mf_rhf).run()
    C = mf_rhf.mo_coeff
    # In MO basis
    rdm1, rdm2 = mf_met.make_rdm12(mf_met.ci, mol.nao, mol.nelec)
    return mf_met, C, rdm1, rdm2
```

```python
verify_all(method_fci)
```

    ===  Energy Relat  ===   True
    ===   1-RDM Symm   ===   True
    ===   2-RDM Symm   ===   True
    ===   1-RDM Trace  ===   True
    ===  12-RDM Relat  ===   True
    ===   1-RDM Idomp  ===   False
    ===   1-RDM ov     ===   False
    ===   2-RDM ovov   ===   False
    === GenFock Symm   ===   True
    ===   1-RDM Dipole ===   True

### MP2

```python
def method_mp2(mf_rhf):
    mf_met = mp.MP2(mf_rhf).run()
    C = mf_rhf.mo_coeff
    # In MO basis
    rdm1, rdm2 = mf_met.make_rdm1(), mf_met.make_rdm2()
    return mf_met, C, rdm1, rdm2
```

```python
verify_all(method_mp2)
```

    ===  Energy Relat  ===   True
    ===   1-RDM Symm   ===   True
    ===   2-RDM Symm   ===   True
    ===   1-RDM Trace  ===   True
    ===  12-RDM Relat  ===   False
    ===   1-RDM Idomp  ===   False
    ===   1-RDM ov     ===   True
    ===   2-RDM ovov   ===   False
    === GenFock Symm   ===   False
    ===   1-RDM Dipole ===   False

### CCSD

```python
def method_ccsd(mf_rhf):
    mf_met = cc.CCSD(mf_rhf).run()
    C = mf_rhf.mo_coeff
    # In MO basis
    rdm1, rdm2 = mf_met.make_rdm1(), mf_met.make_rdm2()
    return mf_met, C, rdm1, rdm2
```

```python
verify_all(method_ccsd)
```

    ===  Energy Relat  ===   True
    ===   1-RDM Symm   ===   True
    ===   2-RDM Symm   ===   True
    ===   1-RDM Trace  ===   True
    ===  12-RDM Relat  ===   True
    ===   1-RDM Idomp  ===   False
    ===   1-RDM ov     ===   False
    ===   2-RDM ovov   ===   False
    === GenFock Symm   ===   False
    ===   1-RDM Dipole ===   False

### CISD

```python
def method_cisd(mf_rhf):
    mf_met = ci.CISD(mf_rhf).run()
    C = mf_rhf.mo_coeff
    # In MO basis
    rdm1, rdm2 = mf_met.make_rdm1(), mf_met.make_rdm2()
    return mf_met, C, rdm1, rdm2
```

```python
verify_all(method_cisd)
```

    ===  Energy Relat  ===   True
    ===   1-RDM Symm   ===   True
    ===   2-RDM Symm   ===   True
    ===   1-RDM Trace  ===   True
    ===  12-RDM Relat  ===   True
    ===   1-RDM Idomp  ===   False
    ===   1-RDM ov     ===   False
    ===   2-RDM ovov   ===   False
    === GenFock Symm   ===   False
    ===   1-RDM Dipole ===   False

### CASCI

```python
def method_casci(mf_rhf):
    mf_met = mcscf.CASCI(mf_rhf, ncas=4, nelecas=4).run()
    C = mf_met.mo_coeff
    Cinv = np.linalg.inv(C)
    # In AO basis
    rdm1, rdm2 = mcscf.addons.make_rdm12(mf_met)
    # Transform to MO basis
    rdm1 = np.einsum("pu, uv, qv -> pq", Cinv, rdm1, Cinv)
    rdm2 = np.einsum("pu, qv, uvkl, rk, sl -> pqrs", Cinv, Cinv, rdm2, Cinv, Cinv)
    return mf_met, C, rdm1, rdm2
```

```python
verify_all(method_casci)
```

    ===  Energy Relat  ===   True
    ===   1-RDM Symm   ===   True
    ===   2-RDM Symm   ===   True
    ===   1-RDM Trace  ===   True
    ===  12-RDM Relat  ===   True
    ===   1-RDM Idomp  ===   False
    ===   1-RDM ov     ===   False
    ===   2-RDM ovov   ===   False
    === GenFock Symm   ===   False
    ===   1-RDM Dipole ===   False

### CASSCF

```python
def method_casscf(mf_rhf):
    mf_met = mcscf.CASSCF(mf_rhf, ncas=4, nelecas=4).run()
    C = mf_met.mo_coeff
    Cinv = np.linalg.inv(C)
    # In AO basis
    rdm1, rdm2 = mcscf.addons.make_rdm12(mf_met)
    # Transform to MO basis
    rdm1 = np.einsum("pu, uv, qv -> pq", Cinv, rdm1, Cinv)
    rdm2 = np.einsum("pu, qv, uvkl, rk, sl -> pqrs", Cinv, Cinv, rdm2, Cinv, Cinv)
    return mf_met, C, rdm1, rdm2
```

```python
verify_all(method_casscf)
```

    ===  Energy Relat  ===   True
    ===   1-RDM Symm   ===   True
    ===   2-RDM Symm   ===   True
    ===   1-RDM Trace  ===   True
    ===  12-RDM Relat  ===   True
    ===   1-RDM Idomp  ===   False
    ===   1-RDM ov     ===   False
    ===   2-RDM ovov   ===   False
    === GenFock Symm   ===   True
    ===   1-RDM Dipole ===   True

## 补充

感谢 [hebrewsnabla](https://github.com/hebrewsnabla) 对 CASCI、CASSCF 密度矩阵的讨论。


# Solving inverse problems on GPUs with Pylops+CuPy - Introduction

Welcome to the tutorial of this session! In this tutorial we will consider a very basic problem: **1D deconvolution**

By the end of the tutorial you will have learned:
- the basics of PyLops and how to use built-in operators to solve inverse problems.
- how you can easily switch between CPU-based to GPU-based inversion;

## Create environment with PyLops+CuPy+CuSignal

```
!git clone https://github.com/PyLops/pylops_swungrendezvous2021.git
```

    fatal: destination path 'pylops_swungrendezvous2021' already exists and is not an empty directory.

```
!chmod u+x pylops_swungrendezvous2021/install.sh
!pylops_swungrendezvous2021/install.sh
```

    --2021-01-30 14:07:33--  https://repo.continuum.io/miniconda/Miniconda3-4.5.4-Linux-x86_64.sh
    Resolving repo.continuum.io (repo.continuum.io)... 104.18.200.79, 104.18.201.79, 2606:4700::6812:c84f, ...
    Connecting to repo.continuum.io (repo.continuum.io)|104.18.200.79|:443... connected.
    HTTP request sent, awaiting response... 301 Moved Permanently
    Location: https://repo.anaconda.com/miniconda/Miniconda3-4.5.4-Linux-x86_64.sh [following]
    --2021-01-30 14:07:33--  https://repo.anaconda.com/miniconda/Miniconda3-4.5.4-Linux-x86_64.sh
    Resolving repo.anaconda.com (repo.anaconda.com)... 104.16.131.3, 104.16.130.3, 2606:4700::6810:8303, ...
    Connecting to repo.anaconda.com (repo.anaconda.com)|104.16.131.3|:443... connected.
    HTTP request sent, awaiting response... 200 OK
    Length: 58468498 (56M) [application/x-sh]
    Saving to: ‘Miniconda3-4.5.4-Linux-x86_64.sh.1’

    Miniconda3-4.5.4-Li 100%[===================>]  55.76M   238MB/s    in 0.2s

    2021-01-30 14:07:34 (238 MB/s) - ‘Miniconda3-4.5.4-Linux-x86_64.sh.1’ saved [58468498/58468498]

    PREFIX=/usr/local
    installing: python-3.6.5-hc3d631a_2 ...
    Python 3.6.5 :: Anaconda, Inc.
    installing: ca-certificates-2018.03.07-0 ...
    installing: conda-env-2.6.0-h36134e3_1 ...
    installing: libgcc-ng-7.2.0-hdf63c60_3 ...
    installing: libstdcxx-ng-7.2.0-hdf63c60_3 ...
    installing: libffi-3.2.1-hd88cf55_4 ...
    installing: ncurses-6.1-hf484d3e_0 ...
    installing: openssl-1.0.2o-h20670df_0 ...
    installing: tk-8.6.7-hc745277_3 ...
    installing: xz-5.2.4-h14c3975_4 ...
    installing: yaml-0.1.7-had09818_2 ...
    installing: zlib-1.2.11-ha838bed_2 ...
    installing: libedit-3.1.20170329-h6b74fdf_2 ...
    installing: readline-7.0-ha6073c6_4 ...
    installing: sqlite-3.23.1-he433501_0 ...
    installing: asn1crypto-0.24.0-py36_0 ...
    installing: certifi-2018.4.16-py36_0 ...
    installing: chardet-3.0.4-py36h0f667ec_1 ...
    installing: idna-2.6-py36h82fb2a8_1 ...
    installing: pycosat-0.6.3-py36h0a5515d_0 ...
    installing: pycparser-2.18-py36hf9f622e_1 ...
    installing: pysocks-1.6.8-py36_0 ...
    installing: ruamel_yaml-0.15.37-py36h14c3975_2 ...
    installing: six-1.11.0-py36h372c433_1 ...
    installing: cffi-1.11.5-py36h9745a5d_0 ...
    installing: setuptools-39.2.0-py36_0 ...
    installing: cryptography-2.2.2-py36h14c3975_0 ...
    installing: wheel-0.31.1-py36_0 ...
    installing: pip-10.0.1-py36_0 ...
    installing: pyopenssl-18.0.0-py36_0 ...
    installing: urllib3-1.22-py36hbe7ace6_0 ...
    installing: requests-2.18.4-py36he2e5f8d_1 ...
    installing: conda-4.5.4-py36_0 ...
    unlinking: ca-certificates-2021.1.19-h06a4308_0
    unlinking: certifi-2020.12.5-py36h06a4308_0
    unlinking: cffi-1.14.4-py36h261ae71_0
    unlinking: chardet-4.0.0-py36h06a4308_1003
    unlinking: conda-4.9.2-py36h06a4308_0
    unlinking: cryptography-3.3.1-py36h3c74f83_0
    unlinking: idna-2.10-pyhd3eb1b0_0
    unlinking: libedit-3.1.20191231-h14c3975_1
    unlinking: libffi-3.3-he6710b0_2
    unlinking: libgcc-ng-9.1.0-hdf63c60_0
    unlinking: libstdcxx-ng-9.1.0-hdf63c60_0
    unlinking: ncurses-6.2-he6710b0_1
    unlinking: openssl-1.1.1i-h27cfd23_0
    unlinking: pip-20.3.3-py36h06a4308_0
    unlinking: pycosat-0.6.3-py36h27cfd23_0
    unlinking: pycparser-2.20-py_2
    unlinking: pyopenssl-20.0.1-pyhd3eb1b0_1
    unlinking: pysocks-1.7.1-py36h06a4308_0
    unlinking: python-3.6.12-hcff3b4d_2
    unlinking: readline-8.1-h27cfd23_0
    unlinking: requests-2.25.1-pyhd3eb1b0_0
    unlinking: ruamel_yaml-0.15.87-py36h7b6447c_1
    unlinking: setuptools-52.0.0-py36h06a4308_0
    unlinking: six-1.15.0-py36h06a4308_0
    unlinking: sqlite-3.33.0-h62c20be_0
    unlinking: tk-8.6.10-hbc83047_0
    unlinking: urllib3-1.26.3-pyhd3eb1b0_0
    unlinking: wheel-0.36.2-pyhd3eb1b0_0
    unlinking: xz-5.2.5-h7b6447c_0
    unlinking: yaml-0.2.5-h7b6447c_0
    unlinking: zlib-1.2.11-h7b6447c_3
    installation finished.
    Solving environment: - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - done

    ## Package Plan ##

      environment location: /usr/local

      added / updated specs:
        - conda
        - python=3.6

    The following packages will be UPDATED:

        ca-certificates: 2018.03.07-0            --> 2021.1.19-h06a4308_0
        certifi:         2018.4.16-py36_0        --> 2020.12.5-py36h06a4308_0
        cffi:            1.11.5-py36h9745a5d_0   --> 1.14.4-py36h261ae71_0
        conda:           4.5.4-py36_0            --> 4.9.2-py36h06a4308_0
        cryptography:    2.2.2-py36h14c3975_0    --> 3.3.1-py36h3c74f83_0
        libedit:         3.1.20170329-h6b74fdf_2 --> 3.1.20191231-h14c3975_1
        libffi:          3.2.1-hd88cf55_4        --> 3.3-he6710b0_2
        libgcc-ng:       7.2.0-hdf63c60_3        --> 9.1.0-hdf63c60_0
        libstdcxx-ng:    7.2.0-hdf63c60_3        --> 9.1.0-hdf63c60_0
        ncurses:         6.1-hf484d3e_0          --> 6.2-he6710b0_1
        openssl:         1.0.2o-h20670df_0       --> 1.1.1i-h27cfd23_0
        python:          3.6.5-hc3d631a_2        --> 3.6.12-hcff3b4d_2
        readline:        7.0-ha6073c6_4          --> 8.1-h27cfd23_0
        sqlite:          3.23.1-he433501_0       --> 3.33.0-h62c20be_0
        tk:              8.6.7-hc745277_3        --> 8.6.10-hbc83047_0
        xz:              5.2.4-h14c3975_4        --> 5.2.5-h7b6447c_0

    Preparing transaction: | / - done
    Verifying transaction: | / - \ | / - \ | / - \ | / done
    Executing transaction: \ | / - \ | / done
    Collecting package metadata (current_repodata.json): - \ | / - \ done
    Solving environment: / - \ | done

    ## Package Plan ##

      environment location: /usr/local

    The following packages will be downloaded:

        package                    |            build
        ---------------------------|-----------------
        chardet-4.0.0              |py36h06a4308_1003         199 KB
        pip-20.3.3                 |   py36h06a4308_0         1.8 MB
        pycosat-0.6.3              |   py36h27cfd23_0          82 KB
        pycparser-2.20             |             py_2          94 KB
        pyopenssl-20.0.1           |     pyhd3eb1b0_1          49 KB
        pysocks-1.7.1              |   py36h06a4308_0          31 KB
        requests-2.25.1            |     pyhd3eb1b0_0          52 KB
        ruamel_yaml-0.15.87        |   py36h7b6447c_1         244 KB
        setuptools-52.0.0          |   py36h06a4308_0         724 KB
        urllib3-1.26.3             |     pyhd3eb1b0_0         105 KB
        wheel-0.36.2               |     pyhd3eb1b0_0          33 KB
        yaml-0.2.5                 |       h7b6447c_0          75 KB
        zlib-1.2.11                |       h7b6447c_3         103 KB
        ------------------------------------------------------------
                                               Total:         3.5 MB

    The following packages will be REMOVED:

      asn1crypto-0.24.0-py36_0
      conda-env-2.6.0-h36134e3_1

    The following packages will be UPDATED:

      chardet                              3.0.4-py36h0f667ec_1 --> 4.0.0-py36h06a4308_1003
      idna               pkgs/main/linux-64::idna-2.6-py36h82f~ --> pkgs/main/noarch::idna-2.10-pyhd3eb1b0_0
      pip                                         10.0.1-py36_0 --> 20.3.3-py36h06a4308_0
      pycparser          pkgs/main/linux-64::pycparser-2.18-py~ --> pkgs/main/noarch::pycparser-2.20-py_2
      pyopenssl          pkgs/main/linux-64::pyopenssl-18.0.0-~ --> pkgs/main/noarch::pyopenssl-20.0.1-pyhd3eb1b0_1
      pysocks                                      1.6.8-py36_0 --> 1.7.1-py36h06a4308_0
      requests           pkgs/main/linux-64::requests-2.18.4-p~ --> pkgs/main/noarch::requests-2.25.1-pyhd3eb1b0_0
      ruamel_yaml                        0.15.37-py36h14c3975_2 --> 0.15.87-py36h7b6447c_1
      setuptools                                  39.2.0-py36_0 --> 52.0.0-py36h06a4308_0
      six                                 1.11.0-py36h372c433_1 --> 1.15.0-py36h06a4308_0
      urllib3            pkgs/main/linux-64::urllib3-1.22-py36~ --> pkgs/main/noarch::urllib3-1.26.3-pyhd3eb1b0_0
      wheel              pkgs/main/linux-64::wheel-0.31.1-py36~ --> pkgs/main/noarch::wheel-0.36.2-pyhd3eb1b0_0
      yaml                                     0.1.7-had09818_2 --> 0.2.5-h7b6447c_0
      zlib                                    1.2.11-ha838bed_2 --> 1.2.11-h7b6447c_3

    The following packages will be DOWNGRADED:

      pycosat                              0.6.3-py36h0a5515d_0 --> 0.6.3-py36h27cfd23_0

    Downloading and Extracting Packages
    pycparser-2.20       | 94 KB     | : 100% 1.0/1 [00:00<00:00,  9.23it/s]
    pysocks-1.7.1        | 31 KB     | : 100% 1.0/1 [00:00<00:00, 21.74it/s]
    wheel-0.36.2         | 33 KB     | : 100% 1.0/1 [00:00<00:00, 21.81it/s]
    setuptools-52.0.0    | 724 KB    | : 100% 1.0/1 [00:00<00:00, 10.19it/s]
    pip-20.3.3           | 1.8 MB    | : 100% 1.0/1 [00:00<00:00,  6.16it/s]
    chardet-4.0.0        | 199 KB    | : 100% 1.0/1 [00:00<00:00, 18.53it/s]
    urllib3-1.26.3       | 105 KB    | : 100% 1.0/1 [00:00<00:00, 19.89it/s]
    pycosat-0.6.3        | 82 KB     | : 100% 1.0/1 [00:00<00:00, 21.38it/s]
    pyopenssl-20.0.1     | 49 KB     | : 100% 1.0/1 [00:00<00:00, 25.32it/s]
    requests-2.25.1      | 52 KB     | : 100% 1.0/1 [00:00<00:00, 21.26it/s]
    zlib-1.2.11          | 103 KB    | : 100% 1.0/1 [00:00<00:00, 22.73it/s]
    ruamel_yaml-0.15.87  | 244 KB    | : 100% 1.0/1 [00:00<00:00, 17.55it/s]
    yaml-0.2.5           | 75 KB     | : 100% 1.0/1 [00:00<00:00, 17.32it/s]
    Preparing transaction: - done
    Verifying transaction: | / done
    Executing transaction: \ | / - \ | / - done
    Collecting package metadata (current_repodata.json): - \ | / - \ | / - \ | / - \ | / - \ | done
    Solving environment: - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ done

    ## Package Plan ##

      environment location: /usr/local

      added / updated specs:
        - cudatoolkit=10.1
        - cusignal

    The following packages will be downloaded:

        package                    |            build
        ---------------------------|-----------------
        cudatoolkit-10.1.243       |       h6bb024c_0       347.4 MB
        cusignal-0.17.0            |  py38_ge853242_0         888 KB  rapidsai
        icu-64.2                   |       he1b5a44_1        12.6 MB  conda-forge
        libopenblas-0.3.12         |pthreads_hb3c22a3_1         8.2 MB  conda-forge
        python_abi-3.6             |          1_cp36m           4 KB  conda-forge
        scipy-1.5.3                |   py36h976291a_0        18.6 MB  conda-forge
        ------------------------------------------------------------
                                               Total:       387.7 MB

    The following NEW packages will be INSTALLED:

      boost              conda-forge/linux-64::boost-1.72.0-py36h79e6602_1
      boost-cpp          conda-forge/linux-64::boost-cpp-1.72.0-h8e57a91_0
      bzip2              conda-forge/linux-64::bzip2-1.0.8-h516909a_3
      cudatoolkit        pkgs/main/linux-64::cudatoolkit-10.1.243-h6bb024c_0
      cudnn              conda-forge/linux-64::cudnn-7.6.5.32-hc0a50b0_1
      cupy               conda-forge/linux-64::cupy-8.3.0-py36h6953d74_2
      cusignal           rapidsai/noarch::cusignal-0.17.0-py38_ge853242_0
      fastrlock          conda-forge/linux-64::fastrlock-0.5-py36h831f99a_1
      icu                conda-forge/linux-64::icu-64.2-he1b5a44_1
      libblas            conda-forge/linux-64::libblas-3.9.0-7_openblas
      libcblas           conda-forge/linux-64::libcblas-3.9.0-7_openblas
      libgfortran-ng     conda-forge/linux-64::libgfortran-ng-7.5.0-h14aa051_18
      libgfortran4       conda-forge/linux-64::libgfortran4-7.5.0-h14aa051_18
      liblapack          conda-forge/linux-64::liblapack-3.9.0-7_openblas
      libllvm10          conda-forge/linux-64::libllvm10-10.0.1-he513fc3_3
      libopenblas        conda-forge/linux-64::libopenblas-0.3.12-pthreads_hb3c22a3_1
      llvmlite           pkgs/main/linux-64::llvmlite-0.34.0-py36h269e1b5_4
      nccl               conda-forge/linux-64::nccl-2.8.3.1-h8b44402_0
      numba              conda-forge/linux-64::numba-0.51.2-py36h7c3b610_0
      numpy              conda-forge/linux-64::numpy-1.18.1-py36h7314795_1
      python_abi         conda-forge/linux-64::python_abi-3.6-1_cp36m
      scipy              conda-forge/linux-64::scipy-1.5.3-py36h976291a_0

    The following packages will be UPDATED:

      certifi            pkgs/main::certifi-2020.12.5-py36h06a~ --> conda-forge::certifi-2020.12.5-py36h5fab9bb_1

    The following packages will be SUPERSEDED by a higher-priority channel:

      ca-certificates    pkgs/main::ca-certificates-2021.1.19-~ --> conda-forge::ca-certificates-2020.12.5-ha878542_0
      conda               pkgs/main::conda-4.9.2-py36h06a4308_0 --> conda-forge::conda-4.9.2-py36h5fab9bb_0

    Downloading and Extracting Packages
    libopenblas-0.3.12   | 8.2 MB    | : 100% 1.0/1 [00:01<00:00,  1.39s/it]
    python_abi-3.6       | 4 KB      | : 100% 1.0/1 [00:00<00:00, 31.52it/s]
    scipy-1.5.3          | 18.6 MB   | : 100% 1.0/1 [00:02<00:00,  2.83s/it]
    cudatoolkit-10.1.243 | 347.4 MB  | : 100% 1.0/1 [00:14<00:00, 14.51s/it]
    cusignal-0.17.0      | 888 KB    | : 100% 1.0/1 [00:01<00:00,  1.29s/it]
    icu-64.2             | 12.6 MB   | : 100% 1.0/1 [00:01<00:00,  1.74s/it]
    Preparing transaction: / - \ | / - \ | / - \ | / - done
    Verifying transaction: | / - \ | / - \ | / - \ | / - \ | / done
    Executing transaction: \ | / - \ | / - \ | / - \ | By downloading and using the cuDNN conda packages, you accept the terms and conditions of the NVIDIA cuDNN EULA -
      https://docs.nvidia.com/deeplearning/cudnn/sla/index.html

    / - \ | / - \ | / - \ done
    Collecting cupy-cuda101==8.1.0
      Downloading cupy_cuda101-8.1.0-cp36-cp36m-manylinux1_x86_64.whl (115.5 MB)
         |████████████████████████████████| 115.5 MB 34 kB/s
    Requirement already satisfied: numpy>=1.15 in /usr/local/lib/python3.6/site-packages (from cupy-cuda101==8.1.0) (1.18.1)
    Requirement already satisfied: fastrlock>=0.3 in /usr/local/lib/python3.6/site-packages (from cupy-cuda101==8.1.0) (0.5)
    Installing collected packages: cupy-cuda101
    Successfully installed cupy-cuda101-8.1.0
    Collecting pyFFTW
      Downloading pyFFTW-0.12.0-cp36-cp36m-manylinux1_x86_64.whl (2.6 MB)
         |████████████████████████████████| 2.6 MB 8.2 MB/s
    Requirement already satisfied: numpy<2.0,>=1.10 in /usr/local/lib/python3.6/site-packages (from pyFFTW) (1.18.1)
    Installing collected packages: pyFFTW
    Successfully installed pyFFTW-0.12.0
    Collecting package metadata (current_repodata.json): - \ | / - \ | / - \ | / - \ | / - done
    Solving environment: | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | / - \ | done

    ## Package Plan ##

      environment location: /usr/local

      added / updated specs:
        - pylops

    The following packages will be downloaded:

        package                    |            build
        ---------------------------|-----------------
        _libgcc_mutex-0.1          |      conda_forge           3 KB  conda-forge
        _openmp_mutex-4.5          |            1_gnu          22 KB  conda-forge
        fftw-3.3.8                 |nompi_hfc0cae8_1114         5.8 MB  conda-forge
        libgcc-ng-9.3.0            |      h2828fa1_18         7.8 MB  conda-forge
        libgomp-9.3.0              |      h2828fa1_18         376 KB  conda-forge
        libstdcxx-ng-9.3.0         |      h6de172a_18         4.0 MB  conda-forge
        openssl-1.1.1i             |       h7f98852_0         2.1 MB  conda-forge
        pyfftw-0.12.0              |   py36h3bbbe2f_2         2.1 MB  conda-forge
        pylops-1.12.0              |   py36h1d69622_0         206 KB  conda-forge
        pywavelets-1.1.1           |   py36h92226af_3         4.4 MB  conda-forge
        scikit-fmm-2021.1.21       |   py36h284efc9_0          55 KB  conda-forge
        ------------------------------------------------------------
                                               Total:        26.9 MB

    The following NEW packages will be INSTALLED:

      _openmp_mutex      conda-forge/linux-64::_openmp_mutex-4.5-1_gnu
      fftw               conda-forge/linux-64::fftw-3.3.8-nompi_hfc0cae8_1114
      libgomp            conda-forge/linux-64::libgomp-9.3.0-h2828fa1_18
      pyfftw             conda-forge/linux-64::pyfftw-0.12.0-py36h3bbbe2f_2
      pylops             conda-forge/linux-64::pylops-1.12.0-py36h1d69622_0
      pywavelets         conda-forge/linux-64::pywavelets-1.1.1-py36h92226af_3
      scikit-fmm         conda-forge/linux-64::scikit-fmm-2021.1.21-py36h284efc9_0

    The following packages will be UPDATED:

      libgcc-ng           pkgs/main::libgcc-ng-9.1.0-hdf63c60_0 --> conda-forge::libgcc-ng-9.3.0-h2828fa1_18
      libstdcxx-ng       pkgs/main::libstdcxx-ng-9.1.0-hdf63c6~ --> conda-forge::libstdcxx-ng-9.3.0-h6de172a_18

    The following packages will be SUPERSEDED by a higher-priority channel:

      _libgcc_mutex           pkgs/main::_libgcc_mutex-0.1-main --> conda-forge::_libgcc_mutex-0.1-conda_forge
      openssl              pkgs/main::openssl-1.1.1i-h27cfd23_0 --> conda-forge::openssl-1.1.1i-h7f98852_0

    Downloading and Extracting Packages
    pywavelets-1.1.1     | 4.4 MB    | : 100% 1.0/1 [00:00<00:00,  1.59it/s]
    fftw-3.3.8           | 5.8 MB    | : 100% 1.0/1 [00:00<00:00,  1.03it/s]
    openssl-1.1.1i       | 2.1 MB    | : 100% 1.0/1 [00:00<00:00,  3.20it/s]
    pyfftw-0.12.0        | 2.1 MB    | : 100% 1.0/1 [00:00<00:00,  1.09it/s]
    libgomp-9.3.0        | 376 KB    | : 100% 1.0/1 [00:00<00:00, 11.22it/s]
    scikit-fmm-2021.1.21 | 55 KB     | : 100% 1.0/1 [00:00<00:00,  6.01it/s]
    _libgcc_mutex-0.1    | 3 KB      | : 100% 1.0/1 [00:00<00:00, 28.46it/s]
    _openmp_mutex-4.5    | 22 KB     | : 100% 1.0/1 [00:00<00:00, 26.03it/s]
    libgcc-ng-9.3.0      | 7.8 MB    | : 100% 1.0/1 [00:01<00:00,  1.10s/it]
    libstdcxx-ng-9.3.0   | 4.0 MB    | : 100% 1.0/1 [00:00<00:00,  1.63it/s]
    pylops-1.12.0        | 206 KB    | : 100% 1.0/1 [00:00<00:00,  5.81it/s]
    Preparing transaction: - done
    Verifying transaction: | / - \ done
    Executing transaction: / done

```
import sys
_ = (sys.path.append("/usr/local/lib/python3.6/site-packages"))
```

## Example: 1D Deconvolution

The first problem we will solve with the aid of PyLops is a simple one-dimensional convolution/deconvolution problem.

We start by defining:

- a 1D input signal, $f(t)$, constructed as the superposition of three gaussian functions with different time shifts;
- a PyLops operator applying convolution ([Convolve1D](https://pylops.readthedocs.io/en/latest/api/generated/pylops.signalprocessing.Convolve1D.html)) with a given impulse response $h(t)$. For simplicity, we select a [Ricker](https://en.wikipedia.org/wiki/Mexican_hat_wavelet) wavelet to be convolved with the 1D input signal.

The resulting data, $g(t)$, is a 'blurred' shifted version of the input model mathematically computed by integration over time according to:

\begin{equation}
g(t) = \int_{-\infty}^{\infty} h(t - \tau) f(\tau) d\tau
\end{equation}

Next, we solve the inverse problem (i.e., **deconvolution**) where we seek for the model vector that would reproduce the observed data when convolved with a specific convolutional kernel.

Let's start now by importing all useful libraries

```
%matplotlib inline
import time
import os
import numpy as np
import cupy as cp
import scipy as sp
import matplotlib.pyplot as plt

from scipy.sparse import bsr_matrix
from pylops.utils import dottest
from pylops.utils.signalprocessing import convmtx
from pylops.utils.wavelets import ricker
from pylops.basicoperators import MatrixMult
from pylops.signalprocessing import Convolve1D
from pylops.optimization.sparsity import FISTA

plt.style.use('seaborn-dark')
print('cp.__version__:',  cp.__version__)
```

    cp.__version__: 8.1.0

We can now create our model vector and convolution operator and compute the data

```
# Input parameters
t_min, t_max, nt = -4.0, 4.0, 10001 # time axis
f_central = 15 # central frequency of wavelet
t_delay = 1 #
t_delay_wav = np.array([0.0, 0.6, 1.2])[:, np.newaxis] # times of spikes

# Time axis
t = np.linspace(t_min, t_max, nt)

# Model vector
sigma = 1 / (np.pi * f_central) ** 2
wav = np.exp(-((t - t_delay_wav) ** 2) / sigma)
model = wav[0] - 1.5*wav[1] + wav[2]

# Impulse response (Ricker wavelet)
impulse_response_full = ricker(t[nt//2:], np.sqrt(1 / (np.pi**2 * sigma)))[0]
impulse_response = impulse_response_full[np.argmax(impulse_response_full)-100:
                                         np.argmax(impulse_response_full)+101]

# Convolution operator with sparse matrix (discouraged!)
#R = convmtx(impulse_response, nt)
#Rop = MatrixMult(bsr_matrix(R[:, np.argmax(impulse_response):-np.argmax(impulse_response)]))

# Convolution operator
Rop = Convolve1D(nt, impulse_response,
                 offset=np.argmax(impulse_response),
                 method='fft')
dottest(Rop, nt, nt, verb=True)

# Data vector
data = Rop * model
```

    Dot test passed, v^T(Opu)=43.110587 - u^T(Op^Tv)=43.110587

We use here one of our sparse solvers [FISTA](https://pylops.readthedocs.io/en/latest/api/generated/pylops.optimization.sparsity.FISTA.html).

The choice of the solver is dependant on the problem we wish to solver; here we know that to recover also the very low and high frequencies of our input model which have been removed by the filter we need to provide further prior information to the inversion in the form of a sparsity constraint.

```
# Model reconstruction - Inverse problem
model_reconstructed = FISTA(Op=Rop,
                            data=data,
                            eps=1e-1,
                            niter=200,
                            show=True)[0]

# Plotting input vectors
fig, ax = plt.subplots(1, 4, figsize=(16, 3), facecolor='w', edgecolor='k')
ax[0].plot(t, impulse_response_full, 'r', lw=1.5)
ax[0].set_xlabel('Time - (sec)')
ax[0].set_ylabel('Amplitude')
ax[0].set_xlim([-0.2, 0.2])
ax[0].set_title("Operator")

ax[1].plot(t, model, 'k', lw=1.5)
ax[1].set_xlabel('Time - (sec)')
ax[1].set_xlim([-0.1, 2.5])
ax[1].set_title("Model")

ax[2].plot(t, data, 'b', lw=1.5)
ax[2].set_xlabel('Time - (sec)')
ax[2].set_xlim([-0.1, 2.5])
ax[2].set_title("Data")

ax[3].plot(t, model, 'k', lw=3, label='Target Model')
ax[3].plot(t, model_reconstructed, '--g', lw=2, label='Reconstruction')
ax[3].set_ylabel('Amplitude')
ax[3].set_xlabel('Time - (sec)')
ax[3].set_xlim([-0.1, 1.5])
ax[3].set_title("Model Reconstruction")
ax[3].legend()
ax[3].set_xlim([-0.1, 2.5])
fig.tight_layout();
```

And we are now ready to see how the above problem can also be solved on your GPU by leveraging CuPy.

All we need to do is:

- ensure the data is transferred to the GPU via [cp.asarray](https://docs.cupy.dev/en/stable/reference/generated/cupy.asarray.html)
- create a new Convolve1D operator where the impulse response is also moved to the GPU. By doing so, PyLops knows automatically that we want to solve our problem on the GPU!

```
# Convert to cupy
Rop_cp = Convolve1D(nt, cp.asarray(impulse_response),
                    offset=np.argmax(impulse_response),
                    method='fft')
data_cp = cp.asarray(data)
dottest(Rop, nt, nt, backend='cupy', verb=True)

# Model reconstruction - Inverse problem
model_reconstructed_cp = FISTA(Op=Rop_cp,
                               data=data_cp,
                               eps=1e-1,
                               niter=200,
                               show=True)[0]

# Plotting input vectors
fig, ax = plt.subplots(1, 4, figsize=(16, 3), facecolor='w', edgecolor='k')
ax[0].plot(t, impulse_response_full, 'r', lw=1.5)
ax[0].set_xlabel('Time - (sec)')
ax[0].set_ylabel('Amplitude')
ax[0].set_xlim([-0.2, 0.2])
ax[0].set_title("Operator")

ax[1].plot(t, model, 'k', lw=1.5)
ax[1].set_xlabel('Time - (sec)')
ax[1].set_xlim([-0.1, 2.5])
ax[1].set_title("Model")

ax[2].plot(t, data, 'b', lw=1.5)
ax[2].set_xlabel('Time - (sec)')
ax[2].set_xlim([-0.1, 2.5])
ax[2].set_title("Data")

ax[3].plot(t, model, 'k', lw=3, label='Target Model')
ax[3].plot(t, cp.asnumpy(model_reconstructed_cp), '--g', lw=2, label='Reconstruction')
ax[3].set_ylabel('Amplitude')
ax[3].set_xlabel('Time - (sec)')
ax[3].set_xlim([-0.1, 1.5])
ax[3].set_title("Model Reconstruction")
ax[3].legend()
ax[3].set_xlim([-0.1, 2.5])
fig.tight_layout();
```

Same results, but compare the timings :D

Let's finally try to do a more extensive comparision by considering model vectors of variable lenght

```
os.chdir('pylops_swungrendezvous2021')
from deconv import deconv

# Compare timings
nts = np.arange(100, 5100, 100) + 1

telapsed = np.vstack([np.array(deconv(nt)) for nt in nts])
plt.figure(figsize=(10, 2))
plt.plot(nts, telapsed[:, 0], 'k', lw=2, label='Numpy')
plt.plot(nts, telapsed[:, 1], 'r', lw=2, label='Cupy')
plt.xlabel('nt')
plt.ylabel('t[s]')
plt.title('Elapsed time for Deconvolution')
plt.legend()
```


Contenido bajo licencia Creative Commons BY 4.0 y código bajo licencia MIT. © Juan Gómez y Nicolás Guarín-Zapata 2020. Este material es parte del curso Modelación Computacional en el programa de Ingeniería Civil de la Universidad EAFIT.

# Introducción al Análisis Estructural

## Introducción

En este Notebook se introduce de manera intuitiva el concepto de rigidez mediante el problema simple de un resorte sometido a una carga constante. Posteriormente, la idea se extiende al caso de un sólido (o medio continuo). Luego, se sume que el el sólido está conformado por pedazos triangulares, cada uno de ellos definido por 3 "partículas", conectadas por "resortes". Esta idealización permite representar el sólido como un sistema de partículas conectadas por resortes. Una vez el problema se reduce a un sistema de masas-resortes se hace evidente que la solución de ambos sistemas usa el mismo tipo de algoritmo. En la parte final del Notebook se implementa un programa general para resolver sistemas representados en términos de relaciones fuerza-desplazamiento.

**Al completar este notebook usted debería estar en la capacidad de:**

* Entender el concepto generalizado de rigidez tanto en sistemas continuos como discretos.

* Usar razonamientos mecánicos para resolver problemas de equilibrio (estático) conformados por partículas conectadas por resortes.

* Reconocer la equivalencia entre el problema de un sólido sometido a cargas externas y el de un sistema de masas-resortes.

* Entender los pasos  para resolver problemas de sistemas masa-resorte o su equivalente.

### Metodología para usar el notebook

El presente notebook esta diseñado para ser usado bajo una metodología de aprendizaje activo y mediante un esquema de clase invertida. El NB está conformado por una parte teórica y una parte computacional. Para poder alcanzar de manera satisfactoria los objetivos de aprendizaje es necesario que antes de la clase presencial el estudiante lea con detalle los contenidos teóricos del NB.

Los bloques resaltados en amarillo corresponden a actividades para la clase, las cuales en algunos casos deben ser realizadas con el apoyo del profesor.

## Concepto intuitivo de rigidez

Considere los 2 modelos mostrados en la siguiente figura. El primero corresponde a un resorte de constante $k$ el cual tras ser sometido a la acción de una fuerza externa $F$ experimenta un cambio en su longitud igual a $\delta$. El mismo problema se ilustra en el modelo abstracto de un sólido representado por la "papa" y en el cual la línea continua describe la forma de la "papa" antes de aplicar la fuerza externa $F$, mientras que la línea punteada describe la forma de la "papa" tras aplicar la fuerza. Los apoyos esquemáticos (de triángulo y rodillo) que se muestran en la figura indican que la "papa" no se podrá desplazar como un cuerpo rígido.

En el resorte se satisface la relación

\begin{equation}
F = k \delta
\end{equation}

y en la cual el coeficiente de rigidez $k$ representa la fuerza necesaria para producir un desplazamiento unitario $\delta = 1.0$.

En el caso de la "papa", el problema es mas complejo ya que existen muchas direcciones posibles y puntos en los cuales aplicar las fuerzas y en los cuales medir los desplazamientos. ¿Cómo podemos cuantificar la rigidez para este caso, entonces?

Sea $\delta _i$ el desplazamiento en un punto cualquiera medido en una dirección $i$ y sea $F_j$ la fuerza aplicada en la dirección $j$ también en un punto arbitrario de la "papa". En este caso siguiendo la idea del resorte es posible escribir:

\begin{equation}
F_j = k \delta_i\, .
\end{equation}

Comparando esta relación con la correspondiente al problema del resorte se puede concluir que el coeficiente $k$ representa la fuerza necesaria a lo largo de la dirección $j$ para producir un desplazamiento unitario en la dirección $i.$

Nótese que si se cambia la dirección en la que se aplica la fuerza y la dirección en la que se mide el desplazamiento es esperable que el coeficiente de rigidez también cambie. Por lo tanto un modelo mas general sería:

\begin{equation}
F_j = k_{ji} \delta_i\, .
\end{equation}

y en el cual los subíndices $ij$ dan cuenta de que el coeficiente de rigidez $k_{ij}$ representa la fuerza necesaria a lo largo de la dirección $j$ para producir un desplazamiento unitario en la dirección $i.$

### Como describir el sólido?

El modelo propuesto para el caso del sólido es físicamente correcto pero operativamente existen infinitas direcciones posibles $i$ y $j$ y surge entonces la pregunta: **¿cómo cuantificar la rigidez para el caso de la "papa"?**

Consideremos algunas posibles soluciones:

1.  Solución discreta: Aproximar el sólido mediante un sistema finito de partículas conectadas por resortes.

2. Solución del continuo: Estudiar el comportamiento de un elemento diferencial generando un sistema de ecuaciones diferenciales (en las variables $u_i$, $\epsilon_{ij}$ y $\sigma_{ij}$).

3. Solución numérica: Combinar **1** y **2** para hacer una aproximación discreta del continuo (métodos por elementos finitos). La comparación entre las soluciones **1** y **2** se describe en la siguiente tabla:

Discutir con sus compañeros de grupo las posibles ventajas y desventajas de las soluciones **1** a la **3**.

| Planteamiento del continuo | Planteamiento discreto |
|:--------------------------:|:----------------------:|
| Número infinito de elementos diferenciales. | Número finito de partículas conectadas por resortes. |
| Desplazamientos, deformaciones y tensiones. | Desplazamientos y fuerzas. |

### Elementos finitos (aproximando el continuo).

La figura muestra nuevamente el problema esquemático de la "papa". Esta ha sido dividida en un numero **finito** de triángulos conectados en las esquinas. Cada triángulo esta definido por 3 puntos correspondientes a sus vértices. Denominemos un triangulo típico como $\Omega_e$. Asuma que cada vértice del triángulo típico $\Omega_e$ se comporta como una partícula y que esta puede experimentar desplazamientos (y fuerzas) en la dirección horizontal y vertical. Asuma además que para el triángulo típico las 3 partículas que lo definen están conectadas por resortes.

¿Cuántos coeficientes del tipo $k_{ij}$ existen para cada triángulo?

### Relación fuerza-desplazamiento para todo el sistema

Se tiene entonces que para un triangulo típico $\Omega_e$ es posible relacionar las 6 fuerzas con los 6 desplazamientos mediante una ecuación general como:

\begin{equation}
F_j = k_ {ji} \delta_i
\end{equation}

y en la cual el término $k_{ji}$ representa 36 coeficientes de rigidez. Si se considera que el índice $j$ puede tomar 6 valores correspondientes a 6 fuerzas y que el índice $i$ puede tomar 6 valores correspondientes a 6 desplazamientos entonces se puede reconocer que se tiene un sistema de 6 ecuaciones el cual podemos escribir matricialmente como:

\begin{equation}
\{ F \}=[ K ] \{ u \}\, .
\end{equation}

De manera similar, es posible anticipar que es posible escribir una relación que considere todos los puntos del dominio. Por ejemplo, si se tienen $N$ puntos en total habrán $2N$ fuerzas y $2N$ desplazamientos. Escribamos la relación entre fuerzas y desplazamientos para todo el sólido (representado por $N$ puntos) como:

\begin{equation}
F_j^G = k_ {ji}^G \delta_i\, .
\end{equation}

En esta relación hemos usado el superíndice $G$ para enfatizar que las fuerzas, desplazamientos y coeficientes de rigidez corresponden a toda el sólido y no a un elemento típico. En forma matricial se tiene:

\begin{equation}
\{ F^G \} = [ K^G ] \{ U^G \}\, .
\end{equation}

¿Cuántos coeficientes del tipo $k_{ij}$ existen para todo el sólido si este esta representado por $N$ puntos?

El superínidce $G$ también indica que las ecuaciones son válidas para todo el sistema global representado por todos los triángulos en los que ha sido dividido el sólido.

* Identifique el argumento de tipo físico que permite formar el sistema global de ecuaciones:

\begin{equation}
F_j ^ G = k_{ji} ^ G \delta_i\, .
\end{equation}

* Si el sólido se divide en $M$ triángulos unidos a través de un total de $N$ vértices, ¿cuántos desplazamientos del tipo $\delta_i$ hay en el sistema?

* Si el sólido se divide en $M$ triángulos unidos a través de un total de $N$ vértices, ¿cuántas fuerzas del tipo $F_j ^ G$ hay en el sistema?

De acuerdo con la discusión anterior, tanto el equilibrio de un triángulo típico $\Omega_e$ como el de un resorte pueden formularse mediante una relación fuerza-desplazamiento de la forma:

\begin{equation}
\{ F \} = [ K ] \{ u \}\, .
\end{equation}

En el caso del resorte es sencillo determinar la matriz de rigidez $[K]$.

En el caso del sólido el problema es un poco más elaborado y por el momento nos conformaremos con suponer que esta matriz se encuentra disponible. En cualquier caso, sea que se trata de un sistema de partículas conectadas por resortes, o un sólido representado por triángulos, el problema se resuelve ensamblando la matriz de rigidez global $[K^G]$ y resolviendo el sistema de ecuaciones:

\begin{equation}
\{ F^G \} = [ K^G ] \{ U^G \}\, .
\end{equation}

En la siguiente sección se discute con más detalle el problema usando un sistema de partículas unidas por resortes y se implementa su solución en el computador.

### Sistema particula-resorte

La siguiente actividad permite entender desde un punto de vista físico el proceso de solución del problema de varias partículas interactuando a través de un sistema de resortes como el que se muestra en la figura.

El sistema de varias partículas se encuentra conectado por resortes con coeficiente de rigidez $k$. El sistema se encuentra sometido a diferentes fuerzas externas (representadas en múltiplos de $P$). Por efectos de visualización las partículas se representan como carritos rectangulares.

Se requiere:

1. Plantear las ecuaciones de equilibrio para una partícula típica mostrada en la figura. Asuma que la partícula esta conectada a un resorte $i$ por la izquierda y a un resorte $i+1$ por la derecha de manera que el diagrama de cuerpo libre para la partícula es como el que se muestra en la figura.

2. Plantear las relaciones fuerza-desplazamiento para el resorte típico mostrado en la figura. En esta los subíndices 1 y 2 hacen referencia a las fuerzas y desplazamientos de los puntos 1 y 2 que definen el resorte.

3. Enumerar las partículas y resortes del sistema. (Asumir el empotramiento como una partícula).

4. Escribir las ecuaciones de equilibrio para cada una de las 4 partículas.

5. Usando las relaciones fuerza-desplazamiento de los diferentes resortes re-escribir las ecuaciones del numeral **4**.

## Algoritmo de solución

En la solución del problema anterior se usaron planteamientos mecánicos para formular las ecuaciones de equlibrio del sistema de partículas. Consideremos ahora su solución de una manera sistemática de tal forma que el problema se pueda codificar en un programa general de análisis estructural.

### Equilbrio de los resortes

Considere el siguiente sistema de 3 partículas, las cuales están conectadas por resortes $i$ e $i+1$ con coeficientes de rigidez $K^i$ y $K^{i+1}$. De manera similar, las partículas tienen asignados los nombres $j-1$, $j$ y $j+1$ y sus respectivos desplazamientos se denotan por $u_{j-1}$, $u_{j}$ y $u_{j+1}$.

Escribamos las ecuaciones de equilibrio para los resortes $i$ e $i+1$ en términos de los desplazamientos $u_{j - 1}$, $u_j$ y $u_{j + 1}$ como:

$$\begin{Bmatrix} f_1^i\\ f_2^i \end{Bmatrix}
= \begin{bmatrix}
  k_{11}^i & k_{12}^i\\
  k_{21}^i & k_{22}^i
\end{bmatrix}
\begin{Bmatrix} u_{j - 1}\\ u_j \end{Bmatrix}\, ,$$

y

$$\begin{Bmatrix} f_1^{i + 1}\\ f_2^{i + 1}\end{Bmatrix}
= \begin{bmatrix}
  k_{11}^{i + 1} & k_{12}^{i + 1}\\
  k_{21}^{i + 1} & k_{22}^{i + 1} \end{bmatrix}
\begin{Bmatrix} u_j\\ u_{j + 1} \end{Bmatrix}\, .$$

Note que hemos usado notación fila-columna en los índices para los coeficientes de rigidez para simplificar su implementación en el computador.

### Equilibrio de una partícula

Asumiendo que la partícula $j$ se encuentra sometida a una carga externa $P$, se tiene, de su diagrama de cuerpo libre, que:

$$
k_{21}^i u_{j - 1} + (k_{22}^i + k_{11}^{i + 1}) u_j + k_{12}^{i + 1} u_{j + 1} = P_j.
$$

Procediendo de manera similar para las partículas ${j-1}$ y ${j+1}$ y considerando la contribución de los resortes $K^i$ y $K^{i+1}$ obtenemos el siguiente bloque del sistema completo de ecuaciones:

$$\begin{bmatrix}
  k_{11}^i &k_{12}^i &0\\
  k_{21}^i &k_{22}^i + k_{11}^{i + 1} &k_{12}^{i + 1}\\
  0 &k_{21}^{i + 1} &k_{22}^{i + 1}
\end{bmatrix}\, . $$

Al considerar el sistema completo de partículas y resortes se obtiene un sistema de ecuaciones lineales de la forma general:

$$
[K_G] \{U_G\}  = \{F_G\}\, .
$$

En este sistema cada ecuación representa el equilibrio de una partícula.

### Ensamblaje

La construcción de las matrices globales que describen el equilibrio de cada masa (o partícula) en el sistema puede programarse en el computador mediante un algoritmo que considere el aporte (en términos de fuerzas) de cada elemento (o resorte).

En Mecánica Computacional, a este proceso se le denomina como **Ensamblaje** de las ecuaciones globales. La operación de ensamblaje puede realizarse después de identificar la conexión entre los (desplazamientos o) grados de libertad globales y los (desplazamientos o) grados de libertad locales mediante una matriz que almacena en cada fila los identificadores de los grados de libertad globales asociados a cada elemento.

Por ejemplo, en el sistema de 3-partículas los resortes $i$ y $i+1$ tienen como desplazamientos de sus extremos los denotados como $u_{j-1}$ y $u_j$ y $u_j$ y $u_{j+1}$. Dichos índices contienen toda la información necesaria para realizar el ensamblaje. La matriz que almacena los indices globales para todos los elementos en el modelo es la matriz `DME` dada por:

$$ DME = \begin{bmatrix}
  j - 1 &j\\
  j &j + 1\\
\end{bmatrix}\, .$$

Nótese que en esta matriz los datos de la primera fila mostrada son $j - 1$ y $j$, los cuales corresponden a los identificadores de los grados de libertad asociados con el resorte $i$. De la misma manera los datos de la segunda fila mostrada son $j$ y $j+1$ los cuales corresponden a los identificadores de los grados de libertad asociados con el resorte $i+1$.

Una vez se tiene disponible la matriz `DME`, el proceso de ensamblaje se realiza como se describe a continuación:

\begin{align}
&K_{j - 1,j - 1} \leftarrow K_{j - 1,j - 1} + k_{11}^i\\
&K_{j - 1,j} \leftarrow K_{j - 1,j} + k_{12}^i\\
&K_{j,j - 1} \leftarrow K_{j,j - 1} + k_{21}^i\\
&K_{j,j} \leftarrow K_{j,j} + k_{22}^i
\end{align}

y

\begin{align}
&K_{j,j} \leftarrow K_{j,j} + k_{11}^{i + 1}\\
&K_{j,j + 1} \leftarrow K_{j,j + 1} + k_{12}^{i + 1}\\
&K_{j + 1,j} \leftarrow K_{j + 1,j} + k_{21}^{i + 1}\\
&K_{j + 1,j + 1} \leftarrow K_{j + 1,j + 1} + k_{22}^{i + 1}
\end{align}

Note la conexión entre los índices locales, correspondientes a $1$ y $2$, y las posiciones en la matriz global, correspondientes a $j-1$, $j$ y $j+1$.

## Programa simple de análisis estructural

De las secciones anteriores se concluye que el problema de equilibrio se resuelve tras ensamblar la contribución de cada resorte (o triángulo) en una matriz de rigidez global. En esta sección discutimos su solución en el computador. Para definir el modelo usaremos archivos de texto en los cuales indicaremos al programa las partículas que lo conforman, los resortes del sistema y su conexión con las partículas, la localización de las cargas externas y las propiedades de los resortes.

Inicialmente se describen las rutinas o funciones que conforman el programa y en la parte final estas son llamadas desde el programa principal.

El programa principal ejecuta los siguientes pasos:

* Lee el modelo.

* Calcula la matriz `DME`.

* Ensambla el sistema global de ecuaciones.

* Resuelve el sistema para determinar los desplazamientos globales $UG$.

(Los archivos de texto con los datos de entrada para las partículas, elementos, coeficientes de rigidez y cargas de este problema están almacenados en la carpeta `files` de este repositorio).

```python
import numpy as np
```

 La rutina `readin()` lee los archivos de entrada que se encuentran en la carpeta `files` del respositorio y que deben ser almacenados localmente bajo esta misma estructura.

```python
def readin():
    nodes = np.loadtxt('files/sprnodes.txt', ndmin=2)
    mats = np.loadtxt('files/sprmater.txt', ndmin=2)
    elements = np.loadtxt('files/spreles.txt' , ndmin=2, dtype=np.int)
    loads = np.loadtxt('files/sprloads.txt', ndmin=2)
    return nodes, mats, elements, loads
```

En el archivo de noos, en el cual se almacena la información de cada partícula, hay un código con valor $-1$ o $0$ indicando si la partícula se encuentra restringida (como en el empotramiento) o libre. Con esa información la rutina `eqcounter()` asigna identificadores de grados de libertad o números de ecuaciones a cada una de las partículas declaradas como libres.

Agregue un comentario a cada línea relevante del código y use los comentarios para escribir el pseudocódigo de la función.

```python
def eqcounter(nodes):
    nn = nodes.shape[0]
    IBC = np.zeros((nn, 1), dtype=np.integer)
    neq = 0
    for cont in range(nn):
        IBC[cont] = int(nodes[cont, 2])
        if IBC[cont] == 0:
            IBC[cont] = neq
            neq = neq + 1
    return neq, IBC
```

El numero de ecuación asignado a cada partícula es usado ahora para formar la matriz `DME`. Cada fila de esta matriz contiene los identificadores de los desplazamientos de los extremos del resorte.

Agregue un comentario a cada línea relevante del código y use los comentarios para escribir el pseudocódigo de la función.

```python
def DME(nodes, elements):
    nels = elements.shape[0]
    DME_mat = np.zeros((nels, 2), dtype=np.integer)
    neq, IBC = eqcounter(nodes)
    ndof = 2
    nnodes = 2
    for ele in range(nels):
        for node in range(nnodes):
            pos = elements[ele, node + 3]
            DME_mat[ele, node] = IBC[pos]
    return DME_mat, IBC, neq
```

Usando la matriz `DME` es posible ensamblar la matriz global de coeficientes de rigidez en términos de ecuaciones como:

$$
K_{j - 1,j - 1} \leftarrow K_{j - 1,j - 1} + k_{11}^i\, .\\
$$

```python
def assembly(elements, mats, nodes, neq, DME_mat):
    IELCON = np.zeros([2], dtype=np.integer)
    KG = np.zeros((neq, neq))
    nels = elements.shape[0]
    nnodes = 2
    ndof = 2
    for el in range(nels):
        elcoor = np.zeros((nnodes))
        im = np.int(elements[el, 2])
        par = mats[im]
        for j in range(nnodes):
            IELCON[j] = elements[el, j+3]
            elcoor[j] = nodes[IELCON[j], 1]
        kloc = uelspring(par[0])
        dme = DME_mat[el, :ndof]
        for row in range(ndof):
            glob_row = dme[row]
            if glob_row != -1:
                for col in range(ndof):
                    glob_col = dme[col]
                    if glob_col != -1:
                        KG[glob_row, glob_col] = KG[glob_row, glob_col] +\
                                                 kloc[row, col]

    return KG
```

La función `uelspring` calcula la matriz de rigidez de un resorte típico, mientras que la función `loadassem` ensambla el vector de cargas externas aplicadas sobre las partículas.

Agregue un comentario a cada línea relevante de los códigos de las siguientes función y úselos para escribir los pseudocódigos correspondientes.

```python
def uelspring(kcof):
    """
    Esta rutina calcula la matriz de rigidez para
    un elemento tipo resorte conformado por 2 nodos.

    Parámetros
    ----------
    kcof : float
      Coeficiente de rigidez del resorte (>0).

    Retorna
    -------
    kloc : ndarray
      Matriz de coeficientes de rigidez local para
      el elemento tipo resorte (2, 2).

    """
    kloc = np.array([
        [kcof, -kcof],
        [-kcof, kcof]])
    return kloc
```

```python
def loadasem(loads, IBC, neq, nl):
    """
    Ensambla el vector de cargas o de valores conocidos
    en el sistema global de ecuaciones.

    Parámetros
    ----------
    loads : ndarray
      Arreglo con las cargas impuestas a las partículas.
    IBC : ndarray (int)
      Arreglo que almacena el identificador de grado de libertad
      a cada partícula.
    neq : int
      Numero de ecuaciones en el sistema.
    nl : int
      Numero de cargas en el sistema.

    Retorna
    -------
    rhs_glob : ndarray
      Arreglo con las cargas impuestas a las partículas,
      rhs se refiere a lado derecho (del inglés right-hand-side).

    """
    rhs_glob = np.zeros((neq))
    for cont in range(nl):
        il = int(loads[cont, 0])
        ilx = IBC[il]
        if ilx != -1:
            rhs_glob[ilx] = loads[cont, 1]
    return rhs_glob
```

### Programa principal

```python
nodes, mats, elements, loads = readin()
DME, IBC, neq = DME(nodes, elements)
KG = assembly(elements, mats, nodes, neq, DME)
RHSG = loadasem(loads, IBC, neq, 3)
UG = np.linalg.solve(KG, RHSG)
print(UG)
```

    [0.002  0.0025 0.0045]

### Actividades para resolver con el computador

* Usando los desplazamientos encontrados en la solución del sistema determine las fuerzas resultantes en cada resorte e indique que elementos están a compresión y que elementos están a tensión.

* Usando las fuerzas encontradas en el paso anterior verifique el equilibrio del sistema.

* Libere el punto correspondiente al empotramiento y calcule nuevamente la solución. Discuta sus resultados.

* Resuelva nuevamente el sistema haciendo la rigidez del resorte mas a la derecha igual a $5k$. Discuta como varían las fuerzas internas en comparación con las del sistema original.

### Glosario de términos

**Coeficiente de rigidez:** Fuerza experimentada por un resorte empotrado en su extremo y sometido en su extremo opuesto a un desplazamiento unitario.

**Matriz de rigidez:** Matriz cuyos elementos son coeficientes de rigidez relacionando las fuerzas y los desplazamientos de un sistema de partículas.

**Sistema discreto:** Sistema mecánico cuyas ecuaciones de equilibrio son representables de manera directa mediante relaciones fuerza-desplazamiento por medio de una matriz de rigidez.

**Sistema continuo:** Sistema mecánico cuyas ecuaciones de equilibrio corresponden a ecuaciones diferenciales parciales que se satisfacen sobre el dominio del sistema.

**Ensamblaje:** Operación mediante la cual se considera el aporte de un sistema de resortes a la rigidez de todo el sistema.

## Formato del notebook

La siguiente celda cambia el formato del Notebook.

```python
from IPython.core.display import HTML
def css_styling():
    styles = open('./nb_style.css', 'r').read()
    return HTML(styles)
css_styling()
```

<link href='http://fonts.googleapis.com/css?family=Fenix' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Source+Code+Pro:300,400' rel='stylesheet' type='text/css'>