# Simulating Molecules using VQE

In this tutorial, we introduce the Variational Quantum Eigensolver (VQE), motivate its use, explain the necessary theory, and demonstrate its implementation in finding the ground state energy of molecules.

## Contents
1. [Introduction](#introduction)
2. [The Variational Method of Quantum Mechanics](#varmethod)
    1. [Mathematical Background](#backgroundmath)
    2. [Bounding the Ground State](#groundstate)
3. [The Variational Quantum Eigensolver](#vqe)
    1. [Variational Forms](#varforms)
    2. [Simple Variational Forms](#simplevarform)
    3. [Parameter Optimization](#optimization)
    4. [Example with a Single Qubit Variational Form](#example)
    5. [Structure of Common Variational Forms](#commonvarforms)
4. [VQE Implementation in Qiskit](#implementation)
    1. [Running VQE on a Statevector Simulator](#implementationstatevec)
    2. [Running VQE on a Noisy Simulator](#implementationnoisy)
5. [Problems](#problems)
6. [References](#references)

## Introduction
In many applications it is important to find the minimum eigenvalue of a matrix. For example, in chemistry, the minimum eigenvalue of a Hermitian matrix characterizing the molecule is the ground state energy of that system. In the future, the quantum phase estimation algorithm may be used to find the minimum eigenvalue. However, its implementation on useful problems requires circuit depths exceeding the limits of hardware available in the NISQ era. Thus, in 2014, Peruzzo *et al.* proposed VQE to estimate the ground state energy of a molecule using much shallower circuits [1].

Formally stated, given a Hermitian matrix $H$ with an unknown minimum eigenvalue $\lambda_{min}$, associated with the eigenstate $|\psi_{min}\rangle$, VQE provides an estimate $\lambda_{\theta}$ bounding $\lambda_{min}$:

\begin{align*}
    \lambda_{min} \le \lambda_{\theta} \equiv \langle \psi(\theta) |H|\psi(\theta) \rangle
\end{align*}

where $|\psi(\theta)\rangle$ is the eigenstate associated with $\lambda_{\theta}$. By applying a parameterized circuit, represented by $U(\theta)$, to some arbitrary starting state $|\psi\rangle$, the algorithm obtains an estimate $U(\theta)|\psi\rangle \equiv |\psi(\theta)\rangle$ on $|\psi_{min}\rangle$. The estimate is iteratively optimized by a classical controller changing the parameter $\theta$ minimizing the expectation value of $\langle \psi(\theta) |H|\psi(\theta) \rangle$.

## The Variational Method of Quantum Mechanics
### Mathematical Background

VQE is an application of the variational method of quantum mechanics. To better understand the variational method, some preliminary mathematical background is provided. An eigenvector, $|\psi_i\rangle$, of a matrix $A$ is invariant under transformation by $A$ up to a scalar multiplicative constant (the eigenvalue $\lambda_i$). That is,

\begin{align*}
    A |\psi_i\rangle = \lambda_i |\psi_i\rangle
\end{align*}

Furthermore, a matrix $H$ is Hermitian when it is equal to its own conjugate transpose.

\begin{align*}
    H = H^{\dagger}
\end{align*}

The spectral theorem states that the eigenvalues of a Hermitian matrix must be real. Thus, any eigenvalue of $H$ has the property that $\lambda_i = \lambda_i^*$. As any measurable quantity must be real, Hermitian matrices are suitable for describing the Hamiltonians of quantum systems. Moreover, $H$ may be expressed as

\begin{align*}
    H = \sum_{i = 1}^{N} \lambda_i |\psi_i\rangle \langle \psi_i |
\end{align*}

where each $\lambda_i$ is the eigenvalue corresponding to the eigenvector $|\psi_i\rangle$. Furthermore, the expectation value of the observable $H$ on an arbitrary quantum state $|\psi\rangle$ is given by

\begin{align}
    \langle H \rangle_{\psi} &\equiv \langle \psi | H | \psi \rangle
\end{align}

Substituting $H$ with its representation as a weighted sum of its eigenvectors,

\begin{align}
    \langle H \rangle_{\psi} = \langle \psi | H | \psi \rangle &= \langle \psi | \left(\sum_{i = 1}^{N} \lambda_i |\psi_i\rangle \langle \psi_i |\right) |\psi\rangle\\
    &= \sum_{i = 1}^{N} \lambda_i \langle \psi | \psi_i\rangle \langle \psi_i | \psi\rangle \\
    &= \sum_{i = 1}^{N} \lambda_i | \langle \psi_i | \psi\rangle |^2
\end{align}

The last equation demonstrates that the expectation value of an observable on any state can be expressed as a linear combination using the eigenvalues associated with $H$ as the weights. Moreover, each of the weights in the linear combination is greater than or equal to 0, as $| \langle \psi_i | \psi\rangle |^2 \ge 0$ and so it is clear that

\begin{align}
    \lambda_{min} \le \langle H \rangle_{\psi} = \langle \psi | H | \psi \rangle = \sum_{i = 1}^{N} \lambda_i | \langle \psi_i | \psi\rangle |^2
\end{align}

The above equation is known as the **variational method** (in some texts it is also known as the variational principle) [2]. It is important to note that this implies that the expectation value of any wave function will always be at least the minimum eigenvalue associated with $H$. Moreover, the expectation value of the eigenstate $|\psi_{min}\rangle$ is given by $\langle \psi_{min}|H|\psi_{min}\rangle = \langle \psi_{min}|\lambda_{min}|\psi_{min}\rangle = \lambda_{min}$. Thus, as expected, $\langle H \rangle_{\psi_{min}}=\lambda_{min}$.

### Bounding the Ground State
When the Hamiltonian of a system is described by the Hermitian matrix $H$ the ground state energy of that system, $E_{gs}$, is the smallest eigenvalue associated with $H$. By arbitrarily selecting a wave function $|\psi \rangle$ (called an *ansatz*) as an initial guess approximating $|\psi_{min}\rangle$, calculating its expectation value, $\langle H \rangle_{\psi}$, and iteratively updating the wave function, arbitrarily tight bounds on the ground state energy of a Hamiltonian may be obtained.

## The Variational Quantum Eigensolver
### Variational Forms
A systematic approach to varying the ansatz is required to implement the variational method on a quantum computer. VQE does so through the use of a parameterized circuit with a fixed form. Such a circuit is often called a *variational form*, and its action may be represented by the linear transformation $U(\theta)$. A variational form is applied to a starting state $|\psi\rangle$ (such as the vacuum state $|0\rangle$, or the Hartree Fock state) and generates an output state $U(\theta)|\psi\rangle\equiv |\psi(\theta)\rangle$. Iterative optimization over $|\psi(\theta)\rangle$ aims to yield an expectation value $\langle \psi(\theta)|H|\psi(\theta)\rangle \approx E_{gs} \equiv \lambda_{min}$. Ideally, $|\psi(\theta)\rangle$ will be close to $|\psi_{min}\rangle$ (where 'closeness' is characterized by either state fidelity, or Manhattan distance) although in practice, useful bounds on $E_{gs}$ can be obtained even if this is not the case.

Moreover, a fixed variational form with a polynomial number of parameters can only generate transformations to a polynomially sized subspace of all the states in an exponentially sized Hilbert space. Consequently, various variational forms exist. Some, such as Ry and RyRz are heuristically designed, without consideration of the target domain. Others, such as UCCSD, utilize domain specific knowledge to generate close approximations based on the problem's structure. The structure of common variational forms is discussed in greater depth later in this document.

### Simple Variational Forms
When constructing a variational form we must balance two opposing goals. Ideally, our $n$ qubit variational form would be able to generate any possible state $|\psi\rangle$ where $|\psi\rangle \in \mathbb{C}^N$ and $N=2^n$. However, we would like the variational form to use as few parameters as possible. Here, we aim to give intuition for the construction of variational forms satisfying our first goal, while disregarding the second goal for the sake of simplicity.

Consider the case where $n=1$. The U3 gate takes three parameters, $\theta, \phi$ and $\lambda$, and represents the following transformation:

$$
\begin{align}
    U3(\theta, \phi, \lambda) = \begin{pmatrix}\cos(\frac{\theta}{2}) & -e^{i\lambda}\sin(\frac{\theta}{2}) \\ e^{i\phi}\sin(\frac{\theta}{2}) & e^{i\lambda + i\phi}\cos(\frac{\theta}{2}) \end{pmatrix}
\end{align}
$$

Up to a global phase, any possible single qubit transformation may be implemented by appropriately setting these parameters. Consequently, for the single qubit case, a variational form capable of generating any possible state is given by the circuit:

Moreover, this universal 'variational form' only has 3 parameters and thus can be efficiently optimized. It is worth emphasizing that the ability to generate an arbitrary state ensures that during the optimization process, the variational form does not limit the set of attainable states over which the expectation value of $H$ can be taken. Ideally, this ensures that the minimum expectation value is limited only by the capabilities of the classical optimizer.

A less trivial universal variational form may be derived for the 2 qubit case, where two body interactions, and thus entanglement, must be considered to achieve universality. Based on the work presented by *Shende et al.* [3] the following is an example of a universal parameterized 2 qubit circuit:

Allow the transformation performed by the above circuit to be represented by $U(\theta)$. When optimized variationally, the expectation value of $H$ is minimized when $U(\theta)|\psi\rangle \equiv |\psi(\theta)\rangle \approx |\psi_{min}\rangle$. By formulation, $U(\theta)$ may produce a transformation to any possible state, and so this variational form may obtain an arbitrarily tight bound on two qubit ground state energies, only limited by the capabilities of the classical optimizer.

### Parameter Optimization
Once an efficiently parameterized variational form has been selected, in accordance with the variational method, its parameters must be optimized to minimize the expectation value of the target Hamiltonian. The parameter optimization process has various challenges. For example, quantum hardware has various types of noise and so objective function evaluation (energy calculation) may not necessarily reflect the true objective function. Additionally, some optimizers perform a number of objective function evaluations dependent on cardinality of the parameter set. An appropriate optimizer should be selected by considering the requirements of an application.

A popular optimization strategy is gradient descent where each parameter is updated in the direction yielding the largest local change in energy. Consequently, the number of evaluations performed depends on the number of optimization parameters present. This allows the algorithm to quickly find a local optimum in the search space. However, this optimization strategy often gets stuck at poor local optima, and is relatively expensive in terms of the number of circuit evaluations performed. While an intuitive optimization strategy, it is not recommended for use in VQE.

An appropriate optimizer for optimizing a noisy objective function is the *Simultaneous Perturbation Stochastic Approximation* optimizer (SPSA). SPSA approximates the gradient of the objective function with only two measurements. It does so by concurrently perturbing all of the parameters in a random fashion, in contrast to gradient descent where each parameter is perturbed independently. When utilizing VQE in either a noisy simulator or on real hardware, SPSA is recommended as the classical optimizer.

When noise is not present in the cost function evaluation (such as when using VQE with a statevector simulator), a wide variety of classical optimizers may be useful. Two such optimizers supported by Qiskit Aqua are the *Sequential Least Squares Programming* optimizer (SLSQP) and the *Constrained Optimization by Linear Approximation* optimizer (COBYLA). It is worth noting that COBYLA only performs one objective function evaluation per optimization iteration (and thus the number of evaluations is independent of the parameter set's cardinality). Therefore, if the objective function is noise-free and minimizing the number of performed evaluations is desirable, it is recommended to try COBYLA.

### Example with a Single Qubit Variational Form

We will now use the simple single qubit variational form to solve a problem similar to ground state energy estimation. Specifically, we are given a random probability vector $\vec{x}$ and wish to determine a possible parameterization for our single qubit variational form such that it outputs a probability distribution that is close to $\vec{x}$ (where closeness is defined in terms of the Manhattan distance between the two probability vectors).

We first create the random probability vector in python:

```python
import numpy as np
np.random.seed(999999)
target_distr = np.random.rand(2)
# We now convert the random vector into a valid probability vector
target_distr /= sum(target_distr)
```

We subsequently create a function that takes the parameters of our single U3 variational form as arguments and returns the corresponding quantum circuit:

```python
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
def get_var_form(params):
    qr = QuantumRegister(1, name="q")
    cr = ClassicalRegister(1, name='c')
    qc = QuantumCircuit(qr, cr)
    qc.u3(params[0], params[1], params[2], qr[0])
    qc.measure(qr, cr[0])
    return qc
```

Now we specify the objective function which takes as input a list of the variational form's parameters, and returns the cost associated with those parameters:

```python
from qiskit import Aer, transpile, assemble
backend = Aer.get_backend("qasm_simulator")
NUM_SHOTS = 10000

def get_probability_distribution(counts):
    output_distr = [v / NUM_SHOTS for v in counts.values()]
    if len(output_distr) == 1:
        output_distr.append(1 - output_distr[0])
    return output_distr

def objective_function(params):
    # Obtain a quantum circuit instance from the paramters
    qc = get_var_form(params)
    # Execute the quantum circuit to obtain the probability distribution associated with the current parameters
    t_qc = transpile(qc, backend)
    qobj = assemble(t_qc, shots=NUM_SHOTS)
    result = backend.run(qobj).result()
    # Obtain the counts for each measured state, and convert those counts into a probability vector
    output_distr = get_probability_distribution(result.get_counts(qc))
    # Calculate the cost as the distance between the output distribution and the target distribution
    cost = sum([np.abs(output_distr[i] - target_distr[i]) for i in range(2)])
    return cost
```

Finally, we create an instance of the COBYLA optimizer, and run the algorithm. Note that the output varies from run to run. Moreover, while close, the obtained distribution might not be exactly the same as the target distribution, however, increasing the number of shots taken will increase the accuracy of the output.

```python
from qiskit.aqua.components.optimizers import COBYLA

# Initialize the COBYLA optimizer
optimizer = COBYLA(maxiter=500, tol=0.0001)

# Create the initial parameters (noting that our single qubit variational form has 3 parameters)
params = np.random.rand(3)
ret = optimizer.optimize(num_vars=3, objective_function=objective_function, initial_point=params)

# Obtain the output distribution using the final parameters
qc = get_var_form(ret[0])
t_qc = transpile(qc, backend)
qobj = assemble(t_qc, shots=NUM_SHOTS)
counts = backend.run(qobj).result().get_counts(qc)
output_distr = get_probability_distribution(counts)

print("Target Distribution:", target_distr)
print("Obtained Distribution:", output_distr)
print("Output Error (Manhattan Distance):", ret[1])
print("Parameters Found:", ret[0])

```

    C:\Users\lao8n\AppData\Local\Temp\ipykernel_16152\2737664276.py:6: DeprecationWarning: The QuantumCircuit.u3 method is deprecated as of 0.16.0. It will be removed no earlier than 3 months after the release date. You should use QuantumCircuit.u instead, which acts identically. Alternatively, you can decompose u3 in terms of QuantumCircuit.p and QuantumCircuit.sx: u3(ϴ,φ,λ) = p(φ+π) sx p(ϴ+π) sx p(λ) (2 pulses on hardware).
      qc.u3(params[0], params[1], params[2], qr[0])
    C:\Users\lao8n\AppData\Local\Temp\ipykernel_16152\2737664276.py:6: DeprecationWarning: The QuantumCircuit.u3 method is deprecated as of 0.16.0. It will be removed no earlier than 3 months after the release date. You should use QuantumCircuit.u instead, which acts identically. Alternatively, you can decompose u3 in terms of QuantumCircuit.p and QuantumCircuit.sx: u3(ϴ,φ,λ) = p(φ+π) sx p(ϴ+π) sx p(λ) (2 pulses on hardware).
      qc.u3(params[0], params[1], params[2], qr[0])

    Target Distribution: [0.51357006 0.48642994]
    Obtained Distribution: [0.5303, 0.4697]
    Output Error (Manhattan Distance): 0.008859881261160885
    Parameters Found: [1.52931144 0.69338981 0.61526659]

### Structure of Common Variational Forms
As already discussed, it is not possible for a polynomially parameterized variational form to generate a transformation to any state. Variational forms can be grouped into two categories, depending on how they deal with this limitation. The first category of variational forms use domain or application specific knowledge to limit the set of possible output states. The second approach uses a heuristic circuit without prior domain or application specific knowledge.

The first category of variational forms exploit characteristics of the problem domain to restrict the set of transformations that may be required. For example, when calculating the ground state energy of a molecule, the number of particles in the system is known *a priori*. Therefore, if a starting state with the correct number of particles is used, by limiting the variational form to only producing particle preserving transformations, the number of parameters required to span the new transformation subspace can be greatly reduced. Indeed, by utilizing similar information from Coupled-Cluster theory, the variational form UCCSD can obtain very accurate results for molecular ground state energy estimation when starting from the Hartree Fock state. Another example illustrating the exploitation of domain-specific knowledge follows from considering the set of circuits realizable on real quantum hardware. Extant quantum computers, such as those based on super conducting qubits, have limited qubit connectivity. That is, it is not possible to implement 2-qubit gates on arbitrary qubit pairs (without inserting swap gates). Thus, variational forms have been constructed for specific quantum computer architectures where the circuits are specifically tuned to maximally exploit the natively available connectivity and gates of a given quantum device. Such a variational form was used in 2017 to successfully implement VQE for the estimation of the ground state energies of molecules as large as BeH$_2$ on an IBM quantum computer [4].

In the second approach, gates are layered such that good approximations on a wide range of states may be obtained. Qiskit Aqua supports three such variational forms: RyRz, Ry and SwapRz (we will only discuss the first two). All of these variational forms accept multiple user-specified configurations. Three essential configurations are the number of qubits in the system, the depth setting, and the entanglement setting. A single layer of a variational form specifies a certain pattern of single qubit rotations and CX gates. The depth setting says how many times the variational form should repeat this pattern. By increasing the depth setting, at the cost of increasing the number of parameters that must be optimized, the set of states the variational form can generate increases. Finally, the entanglement setting selects the configuration, and implicitly the number, of CX gates. For example, when the entanglement setting is linear, CX gates are applied to adjacent qubit pairs in order (and thus $n-1$ CX gates are added per layer). When the entanglement setting is full, a CX gate is applied to each qubit pair in each layer. The circuits for RyRz corresponding to `entanglement="full"` and `entanglement="linear"` can be seen by executing the following code snippet:

```python
from qiskit.circuit.library import EfficientSU2
entanglements = ["linear", "full"]
for entanglement in entanglements:
    form = EfficientSU2(num_qubits=4, entanglement=entanglement)
    if entanglement == "linear":
        print("=============Linear Entanglement:=============")
    else:
        print("=============Full Entanglement:=============")
    # We initialize all parameters to 0 for this demonstration
    display(form.draw(fold=100))
    print()
```

    =============Linear Entanglement:=============

     »
q_0: »
     »
q_1: »
     »
q_2: »
     »
q_3: »
     »
«     ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
«q_0: ┤0                                                                                ├
«     │                                                                                │
«q_1: ┤1                                                                                ├
«     │  EfficientSU2(θ[0],θ[1],θ[2],θ[3],θ[4],θ[5],θ[6],θ[7],θ[8],θ[9],θ[10],θ[11],θ[12],θ[13],θ[14],θ[15],θ[16],θ[17],θ[18],θ[19],θ[20],θ[21],θ[22],θ[23],θ[24],θ[25],θ[26],θ[27],θ[28],θ[29],θ[30],θ[31]) │
«q_2: ┤2                                                                                ├
«     │                                                                                │
«q_3: ┤3                                                                                ├
«     └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

    =============Full Entanglement:=============

     »
q_0: »
     »
q_1: »
     »
q_2: »
     »
q_3: »
     »
«     ┌──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
«q_0: ┤0                                                                                ├
«     │                                                                                │
«q_1: ┤1                                                                                ├
«     │  EfficientSU2(θ[0],θ[1],θ[2],θ[3],θ[4],θ[5],θ[6],θ[7],θ[8],θ[9],θ[10],θ[11],θ[12],θ[13],θ[14],θ[15],θ[16],θ[17],θ[18],θ[19],θ[20],θ[21],θ[22],θ[23],θ[24],θ[25],θ[26],θ[27],θ[28],θ[29],θ[30],θ[31]) │
«q_2: ┤2                                                                                ├
«     │                                                                                │
«q_3: ┤3                                                                                ├
«     └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘

Assume the depth setting is set to $d$. Then, RyRz has $2n\times (d+1)$ parameters and Ry (not shown) has $n\times (d+1)$ parameters.

## VQE Implementation in Qiskit
This section illustrates an implementation of VQE using the programmatic approach. Qiskit Aqua also enables a declarative implementation, however, it reveals less information about the underlying algorithm. This code, specifically the preparation of qubit operators, is based on the code found in the Qiskit Tutorials repository (and as of July 2019, may be found at: https://github.com/Qiskit/qiskit-tutorials ).

The following libraries must first be imported.

```python
from qiskit.aqua.algorithms import VQE, NumPyEigensolver
import matplotlib.pyplot as plt
import numpy as np
from qiskit.chemistry.components.variational_forms import UCCSD
from qiskit.chemistry.components.initial_states import HartreeFock
from qiskit.circuit.library import EfficientSU2
from qiskit.aqua.components.optimizers import COBYLA, SPSA, SLSQP
from qiskit.aqua.operators import Z2Symmetries
from qiskit import IBMQ, BasicAer, Aer
from qiskit.chemistry.drivers import PySCFDriver, UnitsType
from qiskit.chemistry import FermionicOperator
from qiskit.aqua import QuantumInstance
from qiskit.ignis.mitigation.measurement import CompleteMeasFitter
from qiskit.providers.aer.noise import NoiseModel
```

### Running VQE on a Statevector Simulator
We demonstrate the calculation of the ground state energy for LiH at various interatomic distances. A driver for the molecule must be created at each such distance. Note that in this experiment, to reduce the number of qubits used, we freeze the core and remove two unoccupied orbitals. First, we define a function that takes an interatomic distance and returns the appropriate qubit operator, $H$, as well as some other information about the operator.

```python
def get_qubit_op(dist):
    driver = PySCFDriver(atom="Li .0 .0 .0; H .0 .0 " + str(dist), unit=UnitsType.ANGSTROM,
                         charge=0, spin=0, basis='sto3g')
    molecule = driver.run()
    freeze_list = [0]
    remove_list = [-3, -2]
    repulsion_energy = molecule.nuclear_repulsion_energy
    num_particles = molecule.num_alpha + molecule.num_beta
    num_spin_orbitals = molecule.num_orbitals * 2
    remove_list = [x % molecule.num_orbitals for x in remove_list]
    freeze_list = [x % molecule.num_orbitals for x in freeze_list]
    remove_list = [x - len(freeze_list) for x in remove_list]
    remove_list += [x + molecule.num_orbitals - len(freeze_list)  for x in remove_list]
    freeze_list += [x + molecule.num_orbitals for x in freeze_list]
    ferOp = FermionicOperator(h1=molecule.one_body_integrals, h2=molecule.two_body_integrals)
    ferOp, energy_shift = ferOp.fermion_mode_freezing(freeze_list)
    num_spin_orbitals -= len(freeze_list)
    num_particles -= len(freeze_list)
    ferOp = ferOp.fermion_mode_elimination(remove_list)
    num_spin_orbitals -= len(remove_list)
    qubitOp = ferOp.mapping(map_type='parity', threshold=0.00000001)
    qubitOp = Z2Symmetries.two_qubit_reduction(qubitOp, num_particles)
    shift = energy_shift + repulsion_energy
    return qubitOp, num_particles, num_spin_orbitals, shift
```

First, the exact ground state energy is calculated using the qubit operator and a classical exact eigensolver. Subsequently, the initial state $|\psi\rangle$ is created, which the VQE instance uses to produce the final ansatz $\min_{\theta}(|\psi(\theta)\rangle)$. The exact result and the VQE result at each interatomic distance is stored. Observe that the result given by `vqe.run(backend)['energy'] + shift` is equivalent the quantity $\min_{\theta}\left(\langle \psi(\theta)|H|\psi(\theta)\rangle\right)$, where the minimum is not necessarily the global minimum.

When initializing the VQE instance with `VQE(qubitOp, var_form, optimizer, 'matrix')` the expectation value of $H$ on $|\psi(\theta)\rangle$ is directly calculated through matrix multiplication. However, when using an actual quantum device, or a true simulator such as the `qasm_simulator` with `VQE(qubitOp, var_form, optimizer, 'paulis')` the calculation of the expectation value is more complicated. A Hamiltonian may be represented as a sum of a Pauli strings, with each Pauli term acting on a qubit as specified by the mapping being used. Each Pauli string has a corresponding circuit appended to the circuit corresponding to $|\psi(\theta)\rangle$. Subsequently, each of these circuits is executed, and all of the results are used to determine the expectation value of $H$ on $|\psi(\theta)\rangle$. In the following example, we initialize the VQE instance with `matrix` mode, and so the expectation value is directly calculated through matrix multiplication.

Note that the following code snippet may take a few minutes to run to completion.

```python
#native windows is not supported
backend = BasicAer.get_backend("statevector_simulator")
distances = np.arange(0.5, 4.0, 0.1)
exact_energies = []
vqe_energies = []
optimizer = SLSQP(maxiter=5)
for dist in distances:
    qubitOp, num_particles, num_spin_orbitals, shift = get_qubit_op(dist)
    result = NumPyEigensolver(qubitOp).run()
    exact_energies.append(np.real(result.eigenvalues) + shift)
    initial_state = HartreeFock(
        num_spin_orbitals,
        num_particles,
        qubit_mapping='parity'
    )
    var_form = UCCSD(
        num_orbitals=num_spin_orbitals,
        num_particles=num_particles,
        initial_state=initial_state,
        qubit_mapping='parity'
    )
    vqe = VQE(qubitOp, var_form, optimizer)
    vqe_result = np.real(vqe.run(backend)['eigenvalue'] + shift)
    vqe_energies.append(vqe_result)
    print("Interatomic Distance:", np.round(dist, 2), "VQE Result:", vqe_result, "Exact Energy:", exact_energies[-1])

print("All energies have been calculated")
```

```python
plt.plot(distances, exact_energies, label="Exact Energy")
plt.plot(distances, vqe_energies, label="VQE Energy")
plt.xlabel('Atomic distance (Angstrom)')
plt.ylabel('Energy')
plt.legend()
plt.show()
```

Note that the VQE results are very close to the exact results, and so the exact energy curve is hidden by the VQE curve.

### Running VQE on a Noisy Simulator

Here, we calculate the ground state energy for H$_2$ using a noisy simulator and error mitigation.

First, we prepare the qubit operator representing the molecule's Hamiltonian:

```python
driver = PySCFDriver(atom='H .0 .0 -0.3625; H .0 .0 0.3625', unit=UnitsType.ANGSTROM, charge=0, spin=0, basis='sto3g')
molecule = driver.run()
num_particles = molecule.num_alpha + molecule.num_beta
qubitOp = FermionicOperator(h1=molecule.one_body_integrals, h2=molecule.two_body_integrals).mapping(map_type='parity')
qubitOp = Z2Symmetries.two_qubit_reduction(qubitOp, num_particles)
```

Now, we load a device coupling map and noise model from the IBMQ provider and create a quantum instance, enabling error mitigation:

```python
IBMQ.load_account()
provider = IBMQ.get_provider(hub='ibm-q')
backend = Aer.get_backend("qasm_simulator")
device = provider.get_backend("ibmq_lima")
coupling_map = device.configuration().coupling_map
noise_model = NoiseModel.from_backend(device.properties())
quantum_instance = QuantumInstance(backend=backend,
                                   shots=8192,
                                   noise_model=noise_model,
                                   coupling_map=coupling_map,
                                   measurement_error_mitigation_cls=CompleteMeasFitter,
                                   cals_matrix_refresh_period=30)
```

    /usr/local/anaconda3/lib/python3.7/site-packages/qiskit/providers/ibmq/ibmqfactory.py:192: UserWarning: Timestamps in IBMQ backend properties, jobs, and job results are all now in local time instead of UTC.
      warnings.warn('Timestamps in IBMQ backend properties, jobs, and job results '

Finally, we must configure the optimizer, the variational form, and the VQE instance. As the effects of noise increase as the number of two qubit gates circuit depth increase, we use a heuristic variational form (RYRZ) rather than UCCSD as RYRZ has a much shallower circuit than UCCSD and uses substantially fewer two qubit gates.

The following code may take a few minutes to run to completion.

```python
exact_solution = NumPyEigensolver(qubitOp).run()
print("Exact Result:", np.real(exact_solution.eigenvalues) + molecule.nuclear_repulsion_energy)
optimizer = SPSA(maxiter=100)
var_form = EfficientSU2(qubitOp.num_qubits, entanglement="linear")
vqe = VQE(qubitOp, var_form, optimizer=optimizer)
ret = vqe.run(quantum_instance)
vqe_result = np.real(ret['eigenvalue']+ molecule.nuclear_repulsion_energy)
print("VQE Result:", vqe_result)
```

    Exact Result: [-1.13722138]
    VQE Result: -1.0949628247613228

When noise mitigation is enabled, even though the result does not fall within chemical accuracy (defined as being within 0.0016 Hartree of the exact result), it is fairly close to the exact solution.

## Problems
1. You are given a Hamiltonian $H$ with the promise that its ground state is close to a maximally entangled $n$ qubit state. Explain which variational form (or forms) is likely to efficiently and accurately learn the ground state energy of $H$. You may also answer by creating your own variational form, and explaining why it is appropriate for use with this Hamiltonian.
2. Calculate the number of circuit evaluations performed per optimization iteration, when using the COBYLA optimizer, the `qasm_simulator` with 1000 shots, and a Hamiltonian with 60 Pauli strings.
3. Use VQE to estimate the ground state energy of BeH$_2$ with an interatomic distance of $1.3$Å. You may re-use the function `get_qubit_op(dist)` by replacing `atom="Li .0 .0 .0; H .0 .0 " + str(dist)` with `atom="Be .0 .0 .0; H .0 .0 -" + str(dist) + "; H .0 .0 " + str(dist)` and invoking the function with `get_qubit_op(1.3)`. Note that removing the unoccupied orbitals does not preserve chemical precision for this molecule. However, to get the number of qubits  required down to 6 (and thereby allowing efficient simulation on most laptops), the loss of precision is acceptable. While beyond the scope of this exercise, the interested reader may use qubit tapering operations to reduce the number of required qubits to 7, without losing any chemical precision.

## References
1. Peruzzo, Alberto, et al. "A variational eigenvalue solver on a photonic quantum processor." *Nature communications* 5 (2014): 4213.
2. Griffiths, David J., and Darrell F. Schroeter. Introduction to quantum mechanics. *Cambridge University Press*, 2018.
3. Shende, Vivek V., Igor L. Markov, and Stephen S. Bullock. "Minimal universal two-qubit cnot-based circuits." arXiv preprint quant-ph/0308033 (2003).
4. Kandala, Abhinav, et al. "Hardware-efficient variational quantum eigensolver for small molecules and quantum magnets." Nature 549.7671 (2017): 242.

```python
import qiskit
qiskit.__qiskit_version__
```

    {'qiskit-terra': '0.16.0',
     'qiskit-aer': '0.7.0',
     'qiskit-ignis': '0.5.0',
     'qiskit-ibmq-provider': '0.11.0',
     'qiskit-aqua': '0.8.0',
     'qiskit': '0.23.0'}


[Link to book](http://www.math.chalmers.se/Math/Grundutb/GU/MMA310/A15/GaloisBookAug2015Student.pdf)

# Chapter 1
This part seems to me to be tedious and not very informative. I choose to skip it at this point.

# Chapter 2

## Exercise 2.1

### Section a

The subset $\mathbb{Z}$ is not a field because it does not include inverses to most terms.

### Section b

The subset $\{0,1\}$ is not a field because it is not losed $1+1 \notin \{0,1\}$

### Section c

The subset $\{0\}$ is not a field because it does not contain a term neutral with respect to multiplication.

### Section d

This is a field

### Section e

This is not a field, because of closure violation $\sqrt[3]{2} \cdot \sqrt[3]{2} = 2^{2/3}$ is not a member.

### Section f

This is not a field, because of closure violation $\sqrt[4]{2} \cdot \sqrt[4]{2} = \sqrt{2}$ is not a member.

### Section g

This is not a field, becuase of the absence of inverses

### Section h

This is not a field, because it is not closed with respect to addition

## Exercise 2.2

Every subfield of $\mathbb{C}$ has to contain 1, and so has to contain $\mathbb{Z}$, and to close this set it must contain $\mathbb{Q}$.

## Exercise 2.3

$\{0,1\}$ under binary addition and multiplication has characteristic 2.

## Exercise 2.4

You start out with unity, and keep adding it to itself. If it's a finite field, at some point, elements are going to repeat. Let $n_1< n_2 \in \mathbb{N}$ such that $n_1 \cdot 1 = n_2 \cdot 1 \Rightarrow \left(n_2 - n_1\right) \cdot 1 = 0$, hence $n_2 - n_1$ is the characteristic of a finite field.

## Exercise 2.5

### Section a

$K \left(\alpha\right)$ is closed under addition (trivial) and multiplication. Let $a,b,c,d \in K$

$\left(a + b \alpha\right) \left(c+d\alpha \right) = a c + b d \alpha^2 + \left(b c + a d\right) \alpha \in K \left(\alpha\right)$

### Section b

If $c^2 = a\cdot b$, then $\sqrt{b} = c/\sqrt{a}$ and so $K \left(\sqrt{a}\right) = K \left(\sqrt{b}\right)$.

If $K \left(\sqrt{a}\right) = K \left(\sqrt{b}\right)$, then $\exists d,e \in K$ such that $1 = d\sqrt{a} + e \sqrt{b}$. Squaring yields $1 = d^2 a + e^2 b + 2 d e \sqrt{a b}$. Therefore $\sqrt{a b} \in K$.

## Exercise 2.6

### Section a

$\mathbb{Q} \left(\sqrt{2}\right) = \{a + b \sqrt{2} : a,b \in \mathbb{Q} \}$

### Section b

$\mathbb{Q} \left( i \right) = \{a + i b : a, b \in \mathbb{q} \}$

### Section c

$\mathbb{Q} \left(\sqrt{2}, i \right) = \{a + i b + \sqrt{2} c + i \sqrt{2} d : a, b, c, d \in \mathbb{q} \}$

### Section d

$\mathbb{Q} \left( \sqrt{2}, \sqrt{3}\right) = \{a+b \sqrt{2} + c \sqrt{3} + d \sqrt{6} : a,b,c,d \in \mathbb{Q} \}$

## Exercise 2.7

### Section a

$i \sqrt{5} = \sqrt{5} \cdot i$

### Section b

$\sqrt{6} = \sqrt{2} \cdot \sqrt{3}$

### Section c

$\sqrt{5} - \sqrt{7} \in \mathbb{Q} \left(\sqrt{5} + \sqrt{7}\right)$

### Section d

$\sqrt{a} - \sqrt{b} \in \mathbb{Q} \left(\sqrt{a} + \sqrt{b}\right)$

## Exercise 2.8

### Section a

$1+4\sqrt{2} \in \mathbb{Q} \left(\sqrt{2}\right)$

### Section b

Same as 2.6 c

### Section c

Same as 2.7 a

### Section d

$\{a + b i + c \sqrt{i} + d i \sqrt{i} : a,b,c,d \in \mathbb{Q}$

## Exercise 2.9

### Section a

The determinant of a non - zero matrix is $a^2 + b^2 = a^2\left(x^2+1 \right)$ where $x = \frac{b}{a} \in K$. If the equation $X^2 + 1 = 0$ has a solution in $K$, then there are non zero elements which do not have an inverse, and so $L$ is not a field.

### Section b

$diag \left(a, a\right)$ where $a \in K$ is isomorphic to $K$.

### Section c

$GF(3^2)$?

## Exercise 2.10

### Section a

The determinant can be written as $a \left(a-b\right) + b^2 = a^2 \left(1-x+x^2 \right)$, so $L$ is a field iff the equation $X^2 - X + 1$ must not have roots in $K$.

### Section b

Let $K = \{1,0\}$ with binary addition and multiplication. The elements of $L$ are

$A = \begin{bmatrix}
1 & 0\\
 0 & 1
\end{bmatrix}$

$B = \begin{bmatrix}
1 & 1\\
 1 & 0
\end{bmatrix}$

$C = \begin{bmatrix}
0 & 1\\
 1 & 1
\end{bmatrix}$

$D = \begin{bmatrix}
0 & 0\\
 0 & 0
\end{bmatrix}$

and the multiplication table is

$\begin{matrix}
\cdot & A & B & C & D \\
A & A & B & C & D\\
B & B & C & A & D\\
C & C & A & B & D\\
D & D & D & D & D
\end{matrix}$

## Exercise 2.11

2

## Exercise 2.12

### Section a

$\left(a +b\right)^p = \sum_{n=0}^{p} {p \choose n} a^n b^{p-n} = a^p + b^p$

### Section b

Follows immediately from the previous section

### Section c

$K^p \in K$, closure under multiplication is trivial, and closure under addition follows from the previous sections.

## Exercise 2.13

Both sections seems to me to follow immediately from the defintion of a compositum

## Exercise 2.14

Suppose the polynomial has at least one root $r$. Let us define a new variable $y = x-r$. Substituting into the polynomial yields

$f \left(r+y \right) = y^p+r^p -y-r+a = y^p-y = y \left(y^{p-1}-1 \right)$

So $y = 1 \Rightarrow x = r+1$ is also a solution. In this way, we can use a single solution to generate a series of $p$ unique solution. From the fundamental theorem of algebra, we know that there can be no more solutions to the equations.

# Section 3

## Exercise 3.1

### Section a

Any nontrivial decomposition of the polynomials of degree 2 or 3 includes a polynomial of degree 1, which has a root in $K$.

### Section b

It is trivial to see that there are no zeros. The challenge is to find the decomposition

$x^4 + 4 = \left(x^2 + 2 x +2 \right) \left(x^2 - 2 x + 2 \right)$

## Exercise 3.2

There are $5 \cdot 4 = 20$ binary polynomails with two coefficents. However, if neither of the terms is 1, then is would always be possible to factor out $x$, and so such polynomials will be reducible. Therefore, we only have to worry about 5 polynomials

$x+1$ is irreducible

$x^2 + 1 = \left(x+1 \right)^2$

$x^3 + 1 = \left(x+1 \right) \left(x^2 + x +1 \right)$

$x^4 + 1 = \left(x^2+1\right)^2$

$x^5 + 1 = \left(x +1 \right) \left(x^4+x^3+x^2+x+1 \right)$

## Exercise 3.3

$\sum_{i=0}^{n} a_i \left(\frac{p}{q} \right)^i = 0 \Rightarrow \sum_{i=0}^{n} a_i q^{n-i} p^i = a_0 q^n + \sum_{i=1}^{n-1} a_i q^{n-1} p^{i} + a_n p^n= 0$

Hence, $p|a_0$ and $q|a_n$

## Exercise 3.4

### Section a

$x^4 + 64 = \left(x^2 + 4 x + 8\right) \left(x^2 -4 x + 8 \right)$

### Section b

$x^4 +1 = \left(x^2 + \sqrt{2} x + 1\right) \left(x^2 - \sqrt{2} x +1\right)$

### Section c

$x^7 + 1 = \left(x + 1\right) \left(x^6+x^5+x^4+x^3+x^2+x +1\right)$

### Section d

Brute force!

```python
def ex3p4sd():
    import itertools
    import sympy
    sympy.init_printing()
    x = sympy.Symbol('x')
    polynomials = [(x**2+itm[0]*x+itm[1])*(x**2+itm[2]*x+itm[3])
                   for itm in itertools.product(range(5),repeat=4)]
    polynomials = [itm for itm in polynomials
                   if itm.expand().subs(x,0)%5==2 and
                   itm.diff(x).subs(x,0)%5==0 and
                   itm.diff(x,2).subs(x,0)/2%5==0 and
                   itm.diff(x,3).subs(x,0)/6%5==0]
    return polynomials
ex3p4sd()
```

So I guess it's irreducible

### Section e

Irreducible

### Section f

$x^6 + 27 = \left(x^2-3 \right) \left(x^4 +3 x^2- 9 \right)$

### Section g

$x^3 + 2 = \left(x + 2\right) \left(x^2+x+1 \right)$

### Section h

There are no roots in $\mathbb{F}_3$, so the only possible decomposition is into two second order polynomials

```python
def ex3p6sd():
    import itertools
    import sympy
    sympy.init_printing()
    x = sympy.Symbol('x')
    polynomials = [(x**2+itm[0]*x+itm[1])*(x**2+itm[2]*x+itm[3])
                   for itm in itertools.product(range(5),repeat=4)]
    polynomials = [itm for itm in polynomials
                   if itm.expand().subs(x,0)%3==2 and
                   itm.diff(x).subs(x,0)%3==1 and
                   itm.diff(x,2).subs(x,0)/2%3==0 and
                   itm.diff(x,3).subs(x,0)/6%3==0]
    return polynomials
ex3p6sd()
```

So I guess it's irreducible

## Exercise 3.5

### Section a

Suppose $f\left(z \right) = 0$, then, by applying a complex conjugate to this entire equation, we get $f \left( \bar{z}\right) = 0$ (since the coefficients are real) so the conjugate is also a root.

### Section b

Every real root of the poynomial translates to a monomial factor, and from the previous section, every pair of conjugate complex roots translate to a quadratic factor.

## Exercise 3.6

None of the polynomials have roots in $\mathbb{Z}$, so the only possible decomposition is into two quadratics.

### Section a

There are two options for a decomposion

$\left(x^2 + a x \pm 1\right) \left(x^2 + b x \pm 1\right) = x^4 + \left(a+b\right)x^3+\left(a b \pm 2\right) \pm \left(a + b\right)x + 1$

In which case $b = -a$ and $a^2 = \pm2$, and no integer can satisfy this condition.

### Section b

Using the same decomposion as in the previous section, the conditions on the coefficients are

$a + b = 0 \Rightarrow b = -a$

$a^2 = 8$ or $a^2 = 12$

Again, no integer can satisfy either equation.

### Section c

$\left(x^2 +a x + b\right) \left(x^2 +c x +d\right) = x^4 + \left(a + c\right) x^3 + \left(b+d+a c \right) x^2 + \left(b c+a d \right) x + bd$

Conditions on the coefficients

$a + c = -4 \Rightarrow c = -4-a$

$b d = 8 \Rightarrow b = \pm 2, d = \pm 4$ (without loss of generality)

$\pm 6 - a \left(a+4\right) = 12$

```python
def ex3p6sc():
    import sympy
    x = sympy.Symbol('x')
    return [sympy.solve(x**2+4*x+6,x),
            sympy.solve(x**2+4*x+18,x)]
ex3p6sc()
```

No integer solution exists

## Exercise 3.7

### Section a

Let us suppose that such a polynomial is reducible. Let $g \left(X\right) = \sum_{i=0}^{k} g_i X^i$ and $h \left(X\right) = \sum_{i=0}^l h_i X^i$ be the factors of $f$. Therefore, $k+l = n$, $a_n = g_k h_l$ and $a_0 = h_0 g_0$. Hence, either $p|h_0$ or $p|g_0$ (but not both). Without loss of generality, we assume $p|h_0$. The first order coefficient is $h_0 g_1 + h_1 g_0$, and so $p|h_1$. Continuing this way, we see that all coefficients of $h$ must divide $p$, but this contradicts $p \nmid a_0$

### Section b

$2|2$ but $2^2 \nmid 2$. According to the Eisenstein's lemma, the polynomial is irreducible.

### Section c

Transforming $x \rightarrow y + 1$, we get $f \left(y\right) = \sum_{i=1}^{p} {p \choose i} y^{i-1}$. This polynomial satisfies the conditions Eisenstein's lemma for the prime $p$.

## Exercise 3.8

### Section a

Let $f \left(x\right) = \sum_{i=0}^n a_n x^n$ be a polynomial in $\mathbb{Z} \left[x\right]$ and $g \left(x \right) = \sum_{i=0}^n \left(a_n \bmod p \right) x^n$ be a polynomial in $\mathbb{F}_p \left[x\right]$. Suppose $g$ is irreducible in $\mathbb{F}_p \left[x\right]$, but $f$ is reducible in $\mathbb{Z} \left[x\right]$. Let $f_1 \cdot f_2 = f$ be a decomposition in $\mathbb{Z} \left[x\right]$, then their reduction with respect to modulo $p$ will yield a decomposition in $\mathbb{F}_p \left[x\right]$.

### Section b

Let us assume, by contradiction, that the polynomial is reducible in $\mathbb{Q} \left[x\right]$. In that case, there exist two factors such that $f = g \cdot h$. We can reduce these factors by modulo $p$ and $q$, and produce two factors in both $\mathbb{F}_p \left[x\right]$ and $\mathbb{F}_q \left[x\right]$ which would have the same dimensions.

### Section c

#### Subsection 1

```python
def ex3p8sc1():
    import sympy
    x = sympy.Symbol('x')
    temp = sympy.poly(x**4+x+1)
    return sympy.factor(temp, modulus=2)
ex3p8sc1()
```

#### Subsection 2

```python
def ex3p8sc2():
    import sympy
    x = sympy.Symbol('x')
    temp = sympy.poly(x**5+x**2+1)
    return sympy.factor(temp, modulus=2)
ex3p8sc2()
```

#### Subsection 3

```python
def ex3p8sc3():
    import sympy
    x = sympy.Symbol('x')
    temp = sympy.poly(x**4+3*x+4)
    return sympy.factor(temp, modulus=5)
ex3p8sc3()
```

#### Subsection 4

```python
def ex3p8sc4():
    import sympy
    x = sympy.Symbol('x')
    temp = sympy.poly(x**5+3*x+1)
    return [sympy.factor(temp, modulus=2),
            sympy.factor(temp, modulus=3)]
ex3p8sc4()
```

#### Subsection 5

```python
def ex3p8sc5():
    import sympy
    x = sympy.Symbol('x')
    temp = sympy.poly(x**6+5*x**2+x+1)
    return [sympy.factor(temp, modulus=2),
            sympy.factor(temp, modulus=5)]
ex3p8sc5()
```

#### Subsection 6

```python
def ex3p8sc6():
    import sympy
    x = sympy.Symbol('x')
    temp = sympy.poly(x**7+5*x**4+x**2+1)
    return [sympy.factor(temp, modulus=2),
            sympy.factor(temp, modulus=5)]
ex3p8sc6()
```

## Exercise 3.9

### Section a

```python
def exercise_3p9a():
    import sympy
    a = sympy.Symbol('a')
    b = sympy.Symbol('b')
    ap = sympy.Symbol("a'")
    bp = sympy.Symbol("b'")
    p = sympy.Symbol('p')
    q = sympy.Symbol('q')
    r = sympy.Symbol('r')
    x = sympy.Symbol('x')
    T = sympy.Symbol('T')
    pol_1 = x**4+p*x**2+q*x+r
    pol_2 = (x**2+a*x+b)*(x**2+ap*x+bp)
    eqns = [(pol_1-pol_2).diff(x,n).subs(x,0).simplify()
            for n in range(4)]
    eqns = [eqn.subs(sympy.solve(eqns[-1],ap,dict=True)[0])
           for eqn in eqns]
    eqns = [eqn.subs(sympy.solve(eqns[-2],bp,dict=True)[0])
           for eqn in eqns]
    eqns = [eqn.subs(sympy.solve(eqns[-3],b,dict=True)[0])
           for eqn in eqns]
    res = sympy.fraction(-4*eqns[0].simplify())[0].subs(a,sympy.sqrt(T)).expand().simplify()
    return res.collect('T')
exercise_3p9a()
```

### Section b

Everything follows immediately from section a, except for the condition with $\delta$ in the case $q=0$. The equation we get for $b$ is

$b^2 - p b + r = 0$

which has a solution in $K$ if the determinant $\delta = p^2 - 4 r$ is a square.

## Exercise 3.10

The idea here is the same as Euclid's proof that there are infinitely many prime numbers. Suppose there were a finite amount of irreducible polynomials, then I could just generate a new one by multiplying all of them and adding one.

## Exercise 3.11

Let consider a non trivial polynomial product $f = g\cdot h$ such that $f$ has integer coefficients and $g,h$ have rational coefficents. We can have all integer coefficients by multiplying by the greatest common denominator. In that case, we will have the product of two primitive polynomials on one side of the equality, and a non primitive polynomial on the other side. This would violate Gauss' lemma, unless the greatest common denominator is 1, and the original coefficients of all polynomials are integers.

## Exercise 4.1

a-c are algebraic, d-f are not

## Exercise 4.2

### Section a

Let $\alpha$ be a root of the polynomial $f$. From the simple extension theorem, $\left[ K \left(\alpha\right):K\right] = \deg f$. Let $M$ be a field that contains all roots of $f$, according to the tower law $\deg f | \left[M:K\right]$, and consequently $\deg f | \left[M:L\right] \left[L:K\right]$. If $\deg f$ and $\left[L:K\right]$ are co - prime, then $\deg f | \left[M:L\right]$, and so $f$ must be irreducible over $L$.

### Section b

Immediate from section a

## Exercise 4.3

### Section a

$x^6-2 x^3-2$

```python
def ex4p3sa():
    import sympy
    sympy.init_printing()
    x = sympy.Symbol('x')
    poly = x**6-2*x**3-2
    return [poly,poly.subs(x,(sympy.sqrt(3)+1)**sympy.Rational(1,3)).simplify()]
ex4p3sa()
```

### Section b

```python
def ex4p3sb():
    import sympy
    sympy.init_printing()
    x = sympy.Symbol('x')
    res = sympy.Eq(x,sympy.sqrt(2)+2**sympy.Rational(1,3))
    res = sympy.Eq(res.lhs-sympy.sqrt(2),res.rhs-sympy.sqrt(2))
    res = sympy.Eq(res.lhs**3,res.rhs**3)
    res = sympy.expand(res)
    res = sympy.Eq(res.lhs-2+sympy.sqrt(2)*(3*x**2+2),res.rhs-2+sympy.sqrt(2)*(3*x**2+2))
    res = res.simplify()
    res = sympy.Eq(res.lhs**2, res.rhs**2)
    return (res.lhs - res.rhs).expand()
ex4p3sb()
```

### Section c

$\frac{x^5 - 1}{x-1} = x^4 + x^3 + x^2 +x + 1$

### Section d

$x^2 - 2$

### Section e

$x^3 - 2$

### Section f

$\frac{x^p - 1}{x-1} = \sum_{n=0}^{p-1} x^n$

```python

```


```python
# deep learning related tools
import sympy
import numpy as np
import tensorflow as tf

# quantum ML tools
import tensorflow_quantum as tfq
import cirq
import collections

# visualization tools (inline matploit only notebook needed)
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

```python
## Prepare classicial data..

# prepare mnist data
def load_mnist(prepro=True):
    (x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
    if prepro:
        # Rescale the images from [0,255] to the [0.0,1.0] range.
        x_train, x_test = x_train[..., np.newaxis]/255.0, x_test[..., np.newaxis]/255.0

    return (x_train, y_train), (x_test, y_test)

# only keep label 3, 6 imgs ; transform into binary label ( True : 3  / False : 6 )
def filter_36(x, y):
    keep = (y == 3) | (y == 6)
    x, y = x[keep], y[keep]
    y = y == 3
    return x, y

#get dummy meta information
def show_meta_info(tra_data, tst_data, tra_lab, only_vis=False):
    if only_vis:
        print("Number of training examples:", len(tra_data))
        print("Number of test examples:", len(tst_data))
        return
    plt.figure()
    plt.title( str(tra_lab[0]) )
    plt.imshow(tra_data[0, :, :, 0])
    plt.colorbar()

# Downsampling image may allow the duplicate image exists
def remove_contradicting(xs, ys):
    mapping = collections.defaultdict(set)
    orig_x = {}
    # Determine the set of labels for each unique image:
    for x,y in zip(xs,ys):
       orig_x[tuple(x.flatten())] = x
       mapping[tuple(x.flatten())].add(y)

    new_x = []
    new_y = []
    # use set-dict to store label & dict
    for flatten_x in mapping:
      x = orig_x[flatten_x]
      labels = mapping[flatten_x]
      if len(labels) == 1:
          new_x.append(x)
          new_y.append(next(iter(labels)))
      else:
          # Throw out images that match more than one label.
          pass

    num_uniq_3 = sum(1 for value in mapping.values() if len(value) == 1 and True in value)
    num_uniq_6 = sum(1 for value in mapping.values() if len(value) == 1 and False in value)
    num_uniq_both = sum(1 for value in mapping.values() if len(value) == 2)

    print("Number of unique images:", len(mapping.values()))
    print("Number of unique 3s: ", num_uniq_3)
    print("Number of unique 6s: ", num_uniq_6)
    print("Number of unique contradicting labels (both 3 and 6): ", num_uniq_both)
    print()
    print("Initial number of images: ", len(xs))
    print("Remaining non-contradicting unique images: ", len(new_x))

    return np.array(new_x), np.array(new_y)
```

```python
## Quantum data transformation ..

def convert_to_circuit(image):
    """Encode truncated classical image into quantum datapoint."""
    qbit_shape = image.shape[:-1]   # eliminate batch size channel
    values = np.ndarray.flatten(image)
    qubits = cirq.GridQubit.rect(*qbit_shape)
    circuit = cirq.Circuit()
    for i, value in enumerate(values):
        if value:
            circuit.append(cirq.X(qubits[i]))
    return circuit

class CircuitLayerBuilder():
    def __init__(self, data_qubits, readout):
        self.data_qubits = data_qubits
        self.readout = readout

    def add_layer(self, circuit, gate, prefix):
        for i, qubit in enumerate(self.data_qubits):
            symbol = sympy.Symbol(prefix + '-' + str(i))
            circuit.append(gate(qubit, self.readout)**symbol)
```

## **Transform classical data into quantum data (quantum circuit)**

```python
## Prepare classical data
(x_train, y_train), (x_test, y_test) = load_mnist(prepro=True)
#show_meta_info(x_train, x_test, y_train)

## Reduce the origianl task into binary-classification
x_train, y_train = filter_36(x_train, y_train)
x_test, y_test = filter_36(x_test, y_test)
#print("\n\n After preprocessing : \n\n")
#show_meta_info(x_train, x_test, y_train)

## Down-sampling the data itself for fitting the qbit-limitation (about 20 bits)
dwn_im = lambda im, dwn_shap : tf.image.resize(im, dwn_shap).numpy()
x_tra_dwn = dwn_im(x_train, (4, 4))
x_tst_dwn = dwn_im(x_test, (4, 4))   ## 4 x 4 = 16 < 20 bit hardware limitation..
x_tra, y_tra = remove_contradicting(x_tra_dwn, y_train) # dwn_im may let the img become similar
#show_meta_info(x_tra, x_tst_dwn, y_tra)

## Encode the data as quantum circuits
THRESHOLD = 0.5
x_tra_bin = np.array(x_tra > THRESHOLD, dtype=np.float32)
x_tst_bin = np.array(x_tst_dwn > THRESHOLD, dtype=np.float32)
#_ = remove_contradicting(x_train_bin, y_tra)  # num of data may not enough..
show_meta_info(x_tra_bin, x_tst_bin, y_tra)

## package binary image into quantum circuit
x_tra_circ = [ convert_to_circuit(bin_im) for bin_im in x_tra_bin ]
x_tst_circ = [ convert_to_circuit(bin_im) for bin_im in x_tst_bin ]
SVGCircuit(x_tra_circ[0])
```

## **Build Qauntum Classifier**

```python
## Convert circuit into tf.tensor
x_train_tfcirc = tfq.convert_to_tensor(x_train_circ)
x_test_tfcirc = tfq.convert_to_tensor(x_test_circ)

demo_builder = CircuitLayerBuilder(data_qubits = cirq.GridQubit.rect(4,1),
                                   readout=cirq.GridQubit(-1,-1))

circuit = cirq.Circuit()
demo_builder.add_layer(circuit, gate = cirq.XX, prefix='xx')
SVGCircuit(circuit)
```

```python
def create_quantum_model():
    """Create a QNN model circuit and readout operation to go along with it."""
    data_qubits = cirq.GridQubit.rect(4, 4)  # a 4x4 grid.
    readout = cirq.GridQubit(-1, -1)         # a single qubit at [-1,-1]
    circuit = cirq.Circuit()

    # Prepare the readout qubit.
    circuit.append(cirq.X(readout))
    circuit.append(cirq.H(readout))

    builder = CircuitLayerBuilder(
        data_qubits = data_qubits,
        readout=readout)

    # Then add layers (experiment by adding more).
    builder.add_layer(circuit, cirq.XX, "xx1")
    builder.add_layer(circuit, cirq.ZZ, "zz1")

    # Finally, prepare the readout qubit.
    circuit.append(cirq.H(readout))

    return circuit, cirq.Z(readout)

model_circuit, model_readout = create_quantum_model()
```

```python
# Build the Keras model.
model = tf.keras.Sequential([
    # The input is the data-circuit, encoded as a tf.string
    tf.keras.layers.Input(shape=(), dtype=tf.string),
    # The PQC layer returns the expected value of the readout gate, range [-1,1].
    tfq.layers.PQC(model_circuit, model_readout),
])
```

Next, describe the training procedure to the model, using the `compile` method.

Since the the expected readout is in the range `[-1,1]`, optimizing the hinge loss is a somewhat natural fit.

Note: Another valid approach would be to shift the output range to `[0,1]`, and treat it as the probability the model assigns to class `3`. This could be used with a standard a `tf.losses.BinaryCrossentropy` loss.

To use the hinge loss here you need to make two small adjustments. First convert the labels, `y_train_nocon`, from boolean to `[-1,1]`, as expected by the hinge loss.

```python
y_tra_hinge = 2.0*y_tra-1.0
y_tst_hinge = 2.0*y_tst-1.0

def hinge_accuracy(y_true, y_pred):
    y_true = tf.squeeze(y_true) > 0.0
    y_pred = tf.squeeze(y_pred) > 0.0
    result = tf.cast(y_true == y_pred, tf.float32)

    return tf.reduce_mean(result)

model.compile(
    loss=tf.keras.losses.Hinge(),
    optimizer=tf.keras.optimizers.Adam(),
    metrics=[hinge_accuracy])

print(model.summary())
```

    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    pqc (PQC)                    (None, 1)                 32
    =================================================================
    Total params: 32
    Trainable params: 32
    Non-trainable params: 0
    _________________________________________________________________
    None

```python
EPOCHS = 1
BATCH_SIZE = 32

NUM_EXAMPLES = len(x_tra_tfcirc)
x_train_tfcirc_sub = x_train_tfcirc[:NUM_EXAMPLES]
y_train_hinge_sub = y_train_hinge[:NUM_EXAMPLES]
```

```python
qnn_history = model.fit(
      x_train_tfcirc_sub, y_train_hinge_sub,
      batch_size=32,
      epochs=EPOCHS,
      verbose=1,
      validation_data=(x_test_tfcirc, y_test_hinge))

qnn_results = model.evaluate(x_test_tfcirc, y_test)
```

```python

```


```python
# 그래프, 수학 기능 추가
# Add graph and math features
import pylab as py
import numpy as np
import numpy.linalg as nl
# 기호 연산 기능 추가
# Add symbolic operation capability
import sympy as sy

```

```python
sy.init_printing()

```

# 룽게-쿠타법 (RK4)
Runge-Kutta Method (RK4)

전진 오일러법과 수정 오일러법은 $t=t_0$, $t=t_1$, ... 에서의 기울기만 사용하였다.
Forward Euler Method and Modified Euler Method used slopes at $t=t_0$, $t=t_1$, ... only.

룽게-쿠타 법은 1900년대에 독일 수학자 칼 룽게와 마틴 쿠타가 공동 개발했던 상미분 방정식의 근사 해법의 모음이다.
Runge-Kutta methods are a group of numerical methods to solve ordinary differential equations that two German mathematicians Carl Runge and Martin Kutta developed in 1900s.

실은 전진 오일러법이나 수정 오일러법도 룽게-쿠타법에 포함된다.
In fact, Forward Euler method or Modified Euler method are included in the Runge-Kutta method.

룽게-쿠타법 가운데 **RK4**가 가장 널리 사용된다.
Among the Runge-Kutta methods, **RK4** is used the most frequently.

**RK4** 는 $t=t_1$ 에서의 해를 구하기 위해 $t=t_0$, $t=\frac{1}{2}\left(t_0+t_1\right)$, $t=t_1$ 에서의 기울기를 사용한다.

To find a solution at $t=t_1$, **RK4** uses slopes at $t=t_0$, $t=\frac{1}{2}\left(t_0+t_1\right)$, and $t=t_1$.

$$
\begin{cases}
    \frac{d}{dt}\mathbf{x}=f(\mathbf{x}, t) \\
    \mathbf{x}(t_0)=\mathbf{x}_0
\end{cases}
$$

위 미분방정식의 경우, 절차는 다음과 같다.

For the differential equation above, the procedure is as follows.

1. $t=t_0$ 에서의 기울기 $s_1=f(\mathbf{x}_0, t_0)$을 구한다.

At $t=t_0$, find the slope $s_1=f(\mathbf{x}_0, t_0)$.
1. $t=t_\frac{1}{2}$ 에서의 기울기 $s_2=f(\mathbf{x}_0 + s_1 t_{\frac{1}{2}}, t_{\frac{1}{2}})$을 구한다.

At $t=t_\frac{1}{2}$, find the slope $s_2=f(\mathbf{x}_0 + s_1 t_{\frac{1}{2}}, t_{\frac{1}{2}})$.
1. 다시 한번, $t=t_\frac{1}{2}$ 에서의 기울기 $s_3=f(\mathbf{x}_0 + s_2 t_{\frac{1}{2}}, t_{\frac{1}{2}})$을 구한다.

Once again, at $t=t_\frac{1}{2}$, find the slope $s_3=f(\mathbf{x}_0 + s_2 t_{\frac{1}{2}}, t_{\frac{1}{2}})$.
1. $t=t_1$ 에서의 기울기 $s_4=f(\mathbf{x}_0 + s_3 t_1, t_1)$을 구한다.

At $t=t_1$, find the slope $s_4=f(\mathbf{x}_0 + s_3 t_1, t_1)$.
1. $t_0 \le t \le t_1$ 구간을 대표하는 기울기 $s=\frac{\Delta t}{6} \left( s_1 + 2s_2 + 2s_3 + s_4 \right)$을 구한다.

 Find the slope representing interval $t_0 \le t \le t_1$, $s=\frac{\Delta t}{6} \left( s_1 + 2s_2 + 2s_3 + s_4 \right)$.
1. $t=t_1$ 에서 $\mathbf{x}(t_1) = \mathbf{x}_0 + s \left(t_1 - t_0 \right)$ 을 구한다.

At $t=t_1$ 에서, find $\mathbf{x}(t_1) = \mathbf{x}_0 + s \left(t_1 - t_0 \right)$.

python 으로 써 보자.
Let's write in python.

```python
def rk4_step(f, x0, t0, t1):
    """
    One time step of Runge-Kutta method

    f: dx_dt function
    x0 : initial condition
    t0 : this step time
    t1 : next step time
    """
    delta_t = (t1 - t0)
    delta_t_half = delta_t * 0.5
    t_half = t0 + delta_t_half

    # Step 1
    s1 = f(x0, t0)

    # Step 2
    s2 = f(x0 + s1 * delta_t_half, t_half)

    # Step 3
    s3 = f(x0 + s2 * delta_t_half, t_half)

    # Step 4
    s4 = f(x0 + s3 * delta_t, t1)

    # Step 5
    s = (1.0 / 6.0) * (s1 + (s2 + s3) * 2 + s4)

    # Step 6
    x1 = x0 + s * delta_t

    return x1

```

```python
def rk4(f, t_array, x_0):
    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        x_i_plus_1 = rk4_step(f, x_i, t_i, t_array[k+1])

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1)

        x_i = x_i_plus_1

    return time_list, result_list

```

다시 1계 선형 상미분 방정식의 예를 살펴 보자.
Let's reconsider the 1st order linear ODE.

$$
\left\{
    \begin{align}
        a_0 \frac{d}{dt}x(t)+a_1 x(t)&=0 \\
        x(0)&=x_0 \\
    \end{align}
\right.
$$

룽게-쿠타법 결과를 엄밀해, 오일러법, 훈법과 비교해보자.
Let's compare the result from Runge-Kutta method with the exact solution, Euler method, and Heun's method.

```python
a_0, a_1 = 2.0, 1.0

def dx_dt(x, t):
    return - a_1 * x / a_0

```

```python
def exact(t):
    return x_0 * py.exp((-a_1 / a_0) * t)

```

```python
def euler(f, t_array, x_0):
    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        delta_t = t_array[k+1] - t_array[k]

        # slope
        s_i = f(x_i, t_i)

        # x[i + 1]
        x_i_plus_1 = x_i + s_i * delta_t

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1)

        x_i = x_i_plus_1

    return time_list, result_list

```

```python
def heun(f, t_array, x_0):
    time_list = [t_array[0]]
    result_list = [x_0]

    x_i = x_0

    for k, t_i in enumerate(t_array[:-1]):
        # time step
        delta_t = t_array[k+1] - t_array[k]

        # slope at i
        s_i = f(x_i, t_i)

        # x[i + 1] by Forward Euler
        x_i_plus_1 = x_i + s_i * delta_t

        # slope at i + 1
        s_i_plus_1 = f(x_i_plus_1, t_array[k+1])

        # average of slope
        s_average = (s_i + s_i_plus_1) * 0.5

        # x[i + 1] by Modified Euler
        x_i_plus_1_m = x_i + s_average * delta_t

        time_list.append(t_array[k+1])
        result_list.append(x_i_plus_1_m)

        x_i = x_i_plus_1_m

    return time_list, result_list

```

```python
# Time step interval
delta_t = 1.0

# Time array
t_sec_array = np.arange(0, 6 + delta_t*0.5, delta_t)

# Initial state
x_0 = 4.5

# *** Forward Euler ***
t_euler, x_euler = euler(dx_dt, t_sec_array, x_0)
py.plot(t_euler, x_euler, '.-', label='Euler')

# *** Heun ***
t_heun, x_heun = heun(dx_dt, t_sec_array, x_0)
py.plot(t_heun, x_heun, '.-', label='Heun')

# *** RK4 ***
t_rk4, x_rk4 = rk4(dx_dt, t_sec_array, x_0)
py.plot(t_rk4, x_rk4, '.-', label='RK4')

# *** Exact Solution
t_exact_array = np.linspace(0, 6)
x_exact_array = exact(t_exact_array)
py.plot(t_exact_array, x_exact_array, label='exact')

py.xlabel('t(sec)')
py.ylabel('x(m)')

py.legend(loc=0)
py.grid(True)

```

룽게-쿠타법의 해가 엄밀해에 더 가까움을 알 수 있다.

We can see that Runge-Kutta method is closer to the exact solution.

## 도전 과제
Try This

다음 미분방정식의 엄밀해를 구하시오:

Find exact solution of the following differential equation:

$$
\begin{align}
10 \frac{d}{dt}x(t) + 50 x(t) &= 0 \\
x(0) &= 2
\end{align}
$$

위 미분방정식의 수치해를 전진 오일러법으로 구하시오.

Find numerical solution of the above differential equation using Forward Euler Method.

위 미분방정식의 수치해를 훈의 방법으로 구하고 엄밀해, 전진 오일러법과 비교하시오.

Find numerical solution of the above differential equation using Heun's method and compare with exact solution and Forward Euler Method.

위 미분방정식의 수치해를 RK4법으로 구하고 엄밀해, 전진 오일러법, 훈의 방법과 비교하시오.

Find numerical solution of the above differential equation using RK$ and compare with exact solution, Forward Euler Method, and Heun's Method.

## Final Bell
마지막 종

```python
# stackoverfow.com/a/24634221
import os
os.system("printf '\a'");

```

```python

```