# The Euler equations of gas dynamics

In this notebook, we discuss the equations and the structure of the exact solution to the Riemann problem.  In [Euler_approximate](Euler_approximate.ipynb) and [FV_compare](FV_compare.ipynb), we will investigate approximate Riemann solvers.

## Fluid dynamics

In this chapter we study the system of hyperbolic PDEs that governs the motions of a compressible gas in the absence of viscosity.  These consist of conservation laws for *mass, momentum*, and *energy*.  Together, they are referred to as the *compressible Euler equations*, or simply the Euler equations.  Our discussion here is fairly brief; for much more detail see (LeVeque, 2002) or (Toro, 2013).

### Mass conservation

We will use $\rho(x,t)$ to denote the fluid density and $u(x,t)$ for its velocity.  Then the equation for conservation of mass is just the familiar *continuity equation*:

$$\rho_t + (\rho u)_x = 0.$$

### Momentum conservation

We discussed the conservation of momentum in a fluid already in [Acoustics](Acoustics.ipynb).  For convenience, we review the ideas here.  The momentum density is given by the product of mass density and velocity, $\rho u$.  The momentum flux has two components.  First, the momentum is transported in the same way that the density is; this flux is given by the momentum density times the velocity: $\rho u^2$.

To understand the second term in the momentum flux, we must realize that a fluid is made up of many tiny molecules.  The density and velocity we are modeling are average values over some small region of space.  The individual molecules in that region are not all moving with exactly velocity $u$; that's just their average.  Each molecule also has some additional random velocity component.  These random velocities are what accounts for the *pressure* of the fluid, which we'll denote by $p$.  These velocity components also lead to a net flux of momentum.  Thus the momentum conservation equation is

$$(\rho u)_t + (\rho u^2 + p)_x = 0.$$

This is very similar to the conservation of momentum equation in the shallow water equations, as discussed in [Shallow_water](Shallow_water.ipynb), in which case $hu$ is the momentum density and $\frac 1 2 gh^2$ is the hydrostatic pressure.  For gas dynamics, a different expression must be used to compute the pressure $p$ from the conserved quantities.  This relation is called the *equation of state* of the gas, as discussed further below.

### Energy conservation

The energy has two components: internal energy density $\rho e$ and kinetic energy density $\rho u^2/2$:

$$E = \rho e + \frac{1}{2}\rho u^2.$$

Like the momentum flux, the energy flux involves both bulk transport ($Eu$) and transport due to pressure ($pu$):

$$E_t + (u(E+p)) = 0.$$

### Equation of state

You may have noticed that we have 4 unknowns (density, momentum, energy, and pressure) but only 3 conservation laws.  We need one more relation to close the system.  That relation, known as the equation of state, expresses how the pressure is related to the other quantities.  We'll focus on the case of a polytropic ideal gas, for which

$$p = \rho e (\gamma-1).$$

Here $\gamma$ is the ratio of specific heats, which for air is approximately 1.4.

## Hyperbolic structure of the 1D Euler equations

We can write the three conservation laws as a single system $q_t + f(q)_x = 0$ by defining
\begin{align}
q & = \begin{pmatrix} \rho \\ \rho u \\ E\end{pmatrix}, &
f(q) & = \begin{pmatrix} \rho u \\ \rho u^2 + p \\ u(E+p)\end{pmatrix}.
\label{euler_conserved}
\end{align}
These are the one-dimensional Euler system. As usual, one can define the $3 \times 3$ Jacobian matrix by differentiating this flux function with respect to the three components of $q$.

In our discussion of the structure of these equations, it is convenient to work with the primitive variables $(\rho, u, p)$ rather than the conserved variables.  The quasilinear form is particularly simple in the primitive variables:

\begin{align} \label{euler_primitive}
\begin{bmatrix} \rho \\ u \\ p \end{bmatrix}_t +
\begin{bmatrix} u & \rho & 0 \\ 0 & u & 1/\rho \\ 0 & \gamma \rho & u \end{bmatrix} \begin{bmatrix} \rho \\ u \\ p \end{bmatrix}_x & = 0.
\end{align}

### Characteristic velocities
The eigenvalues of the flux Jacobian $f'(q)$ for the 1D Euler equations are:

\begin{align}
\lambda_1 & = u-c & \lambda_2 & = u & \lambda_3 & = u+c
\end{align}

Here $c$ is the sound speed:

$$ c = \sqrt{\frac{\gamma p}{\rho}}.$$

These are also the eigenvalues of the coefficient matrix appearing in (\ref{euler_primitive}), and show that acoustic waves propagate at speeds $\pm c$ relative to the fluid velocity $u$. There is also a characteristic speed $\lambda_2 =u$ corresponding to the transport of entropy at the fluid velocity, as discussed further below.

The eigenvectors of the coefficient matrix appearing in (\ref{euler_primitive}) are:

\begin{align}\label{euler_evecs}
r_1 & = \begin{bmatrix} -\rho/c \\ 1 \\ - \rho c \end{bmatrix} &
r_2 & = \begin{bmatrix} 1 \\ 0 \\ 0 \end{bmatrix} &
r_3 & = \begin{bmatrix}  \rho/c \\ 1 \\ \rho c \end{bmatrix}.
\end{align}

These vectors show the relation between jumps in the primitive variables across waves in each family.  The eigenvectors of the flux Jacobian $f'(q)$ arising from the conservative form (\ref{euler_conserved})  would be different, and would give the relation between jumps in the conserved variables across each wave.

Notice that the second characteristic speed, $\lambda_2$, depends only on $u$ and that $u$ does not change as we move in the direction of $r_2$.  In other words, the 2-characteristic velocity is constant on 2-integral curves.  This is similar to the wave that carries changes in the tracer that we considered in [Shallow_tracer](Shallow_tracer.ipynb).  We say this characteristic field is *linearly degenerate*; it admits neither shocks nor rarefactions.  In a simple 2-wave, all characteristics are parallel.  A jump in this family carries a change only in the density, and is referred to as a *contact discontinuity*.

Mathematically, the $p$th field is linearly degenerate if
\begin{align}\label{lindegen}
\nabla \lambda_p(q) \cdot r_p(q) = 0,
\end{align}
since in this case the eigenvalue $\lambda_p(q)$ does not vary in the direction of the eigenvector $r_p(q)$, and hence is constant along integral curves of this family. (Recall that $r_p(q)$ is the tangent vector at each point on the integral curve.)

The other two fields have characteristic velocities that *do* vary along the corresponding integral curves. Moreover they vary in a monotonic manner as we move along an integral curve, always increasing as we move in one direction, decreasing in the other.  Mathematically, this means that
\begin{align}\label{gennonlin}
\nabla \lambda_p(q) \cdot r_p(q) \ne 0
\end{align}
as $q$ varies, so that the directional derivative of $\lambda_p(q)$ cannot change sign as we move along the curve.  This is analogous to the flux for a scalar problem being convex, and means that the 1-wave and the 3-wave in any Riemann solution to the Euler equations will be a single shock or rarefaction wave, not the sort of compound waves we observed in [Nonconvex_scalar](Nonconvex_scalar.ipynb) in the nonconvex scalar case.  Any characteristic field satisfying (\ref{gennonlin}) is said to be *genuinely nonlinear*.

### Entropy

Another important quantity in gas dynamics is the *specific entropy*:

$$ s = c_v \log(p/\rho^\gamma) + C,$$

where $c_v$ and $C$ are constants. From the expression (\ref{euler_evecs}) for the eigenvector $r_2$, we see that the pressure and velocity are constant across a 2-wave.
A simple 2-wave is also called an *entropy wave* because a variation in density while the pressure remains constant requires a variation in the entropy of the gas as well.  On the other hand a simple acoustic wave (a continuously varying pure 1-wave or 3-wave) has constant entropy throughout the wave; the specific entropy is a Riemann invariant for these families.

A shock wave (either a 1-wave or 3-wave) satisfies the Rankine-Hugoniot conditions and exhibits a jump in entropy.  To be physically correct, the entropy of the gas must *increase* as gas molecules pass through the shock, leading to the *entropy condition* for selecting shock waves.  We have already seen this term used in the context of scalar nonlinear equations shallow water flow, even though the entropy condition in those cases did not involve the physical entropy.

### Riemann invariants

Since the Euler equations have three components, we expect each integral curve (a 1D set in 3D space) to be defined by two Riemann invariants.  These are:

\begin{align}
1 & : s, u+\frac{2c}{\gamma-1} \\
2 & : u, p \\
3 & : s, u-\frac{2c}{\gamma-1}.
\end{align}

### Integral curves

The level sets of these Riemann invariants are two-dimensional surfaces; the intersection of two appropriate level sets defines an integral curve.

The 2-integral curves, of course, are simply lines of constant pressure and velocity (with varying density).  Since the field is linearly degenerate, these coincide with the Hugoniot loci.
We can determine the form of the 1- and 3-integral curves using the Riemann invariants above.  For a curve passing through $(\rho_0,u_0,p_0)$, we find

\begin{align}
    \rho(p) &= (p/p_0)^{1/\gamma} \rho_0,\\
    u(p) & = u_0 \pm \frac{2c_0}{\gamma-1}\left(1-(p/p_0)^{(\gamma-1)/(2\gamma)}\right).
\end{align}
Here the plus sign is for 1-waves and the minus sign is for 3-waves.

Below we plot the projection of some integral curves on the $p-u$ plane.

```python
%matplotlib inline
```

```python
%config InlineBackend.figure_format = 'svg'
from exact_solvers import euler
from exact_solvers import euler_demos
from ipywidgets import widgets
from ipywidgets import interact
State = euler.Primitive_State
gamma = 1.4
```

If you wish to examine the Python code for this chapter, see:

- [exact_solvers/euler.py](exact_solvers/euler.py) ...
   [on github,](https://github.com/clawpack/riemann_book/blob/FA16/exact_solvers/euler.py)
- [exact_solvers/euler_demos.py](exact_solvers/euler_demos.py) ...
   [on github.](https://github.com/clawpack/riemann_book/blob/FA16/exact_solvers/euler_demos.py)

```python
interact(euler.plot_integral_curves,
         gamma=widgets.FloatSlider(min=1.1,max=3,value=1.4),
         rho_0=widgets.FloatSlider(min=0.1,max=3.,value=1.,
                                   description=r'$\rho_0$'));
```

    interactive(children=(Checkbox(value=True, description='plot_1'), Checkbox(value=False, description='plot_3'),…

## Rankine-Hugoniot jump conditions

The Hugoniot loci for 1- and 3-shocks are
\begin{align}
    \rho(p) &= \left(\frac{1 + \beta p/p_0}{p/p_\ell + \beta} \right),\\
    u(p) & = u_0 \pm \frac{2c_0}{\sqrt{2\gamma(\gamma-1)}}
        \left(\frac{1-p/p_0}{\sqrt{1+\beta p/p_0}}\right), \\
\end{align}
where $\beta = (\gamma+1)/(\gamma-1)$.
Here the plus sign is for 1-shocks and the minus sign is for 3-shocks.

Below we plot the projection of some Hugoniot loci on the $p-u$ plane.

```python
interact(euler.plot_hugoniot_loci,
         gamma=widgets.FloatSlider(min=1.1,max=3,value=1.4),
         rho_0=widgets.FloatSlider(min=0.1,max=3.,value=1.,
                                   description=r'$\rho_0$'));
```

    interactive(children=(Checkbox(value=True, description='plot_1'), Checkbox(value=False, description='plot_3'),…

### Entropy condition

As mentioned above, a shock wave is physically relevant only if the entropy of the gas increases as the gas particles move through the shock.  A discontinuity satisfying the Rankine-Hugoniot jump conditions that violates this entropy condition (an "entropy-violating shock") is not physically correct and should be replaced by a rarefaction wave in the Riemann solution.

This physical entropy condition is equivalent to the mathematical condition that for a 1-shock to be physically relevant, the 1-characteristics must impinge on the shock (the Lax entropy condition). If the entropy condition is violated, the 1-characteristics would spread out, allowing the insertion of an expansion fan (rarefaction wave).

## Exact solution of the Riemann problem

The general Riemann solution is found following the steps listed below.  This is essentially the same procedure used to determine the correct solution to the Riemann problem for the shallow water equations in [Shallow_water](Shallow_water.ipynb), where more details are given.

The Euler equations are a system of three equations and the general Riemann solution consists of three waves, so we must determine two intermediate states rather than the one intermediate state in the shallow water equations.  However, it is nearly as simple because of the fact that we know the pressure and velocity are constant across the 2-wave, and so there is a single intermediate pressure $p_m$ and velocity $u_m$ in both intermediate states, and it is only the density that takes different values $\rho_{m1}$ and $\rho_{m2}$.  Moreover any jump in density is allowed across the 2-wave, and we have expressions given above for how $u(p)$ varies along any integral curve or Hugoniot locus, expressions that do not explicitly involve $\rho$.  So we can determine the intermediate $p_m$ by finding the intersection point of two relevant curves, in step 3 of this general algorithm:

1. Define a piecewise function giving the middle state velocity $u_m$ that can be connected to the left state by an entropy-satisfying shock or rarefaction, as a function of the middle-state pressure $p_m$.
2. Define a piecewise function giving the middle state velocity $u_m$ that can be connected to the right state by an entropy-satisfying shock or rarefaction, as a function of the middle-state pressure $p_m$.
3. Use a nonlinear rootfinder to find the intersection of the two functions defined above.
4. Use the Riemann invariants to find the intermediate state densities and the solution structure inside any rarefaction waves.

Step 4 above requires finding the structure of rarefaction waves.  This can be done using the the fact that the Riemann invariants are constant through the rarefaction wave.  See Chapter 14 of (LeVeque, 2002) for more details.

## Examples of Riemann solutions

Here we present some representative examples of Riemann problems and solutions.  The examples chosen are closely related to the examples used in [Shallow_water](Shallow_water.ipynb) and you might want to refer back to that notebook and compare the results.

### Problem 1: Sod shock tube

First we consider the classic shock tube problem. The initial condition consists of high density and pressure on the left, low density and pressure on the right and zero velocity on both sides. The solution is composed of a shock propagating to the right (3-shock), while a left-going rarefaction forms (1-rarefaction). In between these two waves, there is a jump in the density, which is the contact discontinuity (2-wave) in the linearly degenerate characteristic field.

Note that this set of initial conditions is analogous to the "dam break" problem for shallow water quations, and the resulting structure of the solution is very similar to that obtained when those equations are solved with the addition of a scalar tracer.  However, in the Euler equations the entropy jump across a 2-wave does affect the fluid dynamics on either side, so this is not a passive tracer and solving the Riemann problem is slightly more complex.

```python
left_state  = State(Density = 3.,
                    Velocity = 0.,
                    Pressure = 3.)
right_state = State(Density = 1.,
                    Velocity = 0.,
                    Pressure = 1.)

euler.riemann_solution(left_state,right_state)
```

    interactive(children=(FloatSlider(value=0.5, description='t', max=0.9), Dropdown(description='Show characteris…

Here is a plot of the solution in the phase plane, showing the integral curve connecting the left and middle states, and the Hugoniot locus connecting the middle and right states.

```python
euler.phase_plane_plot(left_state, right_state)
```

### Problem 2: Symmetric expansion

Next we consider the case of equal densities and pressures, and equal and opposite velocities, with the initial states moving away from each other.  The result is two rarefaction waves (the contact has zero strength).

```python
left_state  = State(Density = 1.,
                    Velocity = -3.,
                    Pressure = 1.)
right_state = State(Density = 1.,
                    Velocity = 3.,
                    Pressure = 1.)

euler.riemann_solution(left_state,right_state);
```

    interactive(children=(FloatSlider(value=0.5, description='t', max=0.9), Dropdown(description='Show characteris…

```python
euler.phase_plane_plot(left_state, right_state)
```

### Problem 3: Colliding flows

Next, consider the case in which the left and right states are moving toward each other.  This leads to a pair of shocks, with a high-density, high-pressure state in between.

```python
left_state  = State(Density = 1.,
                    Velocity = 3.,
                    Pressure = 1.)
right_state = State(Density = 1.,
                    Velocity = -3.,
                    Pressure = 1.)

euler.riemann_solution(left_state,right_state)
```

    interactive(children=(FloatSlider(value=0.5, description='t', max=0.9), Dropdown(description='Show characteris…

```python
euler.phase_plane_plot(left_state, right_state)
```

## Plot particle trajectories

In the next plot of the Riemann solution in the $x$-$t$ plane, we also plot the trajectories of a set of particles initially distributed along the $x$ axis at $t=0$, with the spacing inversely proportional to the density.  The evolution of the distance between particles gives an indication of how the density changes.

```python
left_state  = State(Density = 3.,
                    Velocity = 0.,
                    Pressure = 3.)
right_state = State(Density = 1.,
                    Velocity = 0.,
                    Pressure = 1.)

euler.plot_riemann_trajectories(left_state, right_state)
```

Since the distance between particles in the above plot is inversely proportional to density, we see that the density around a particle increases as it goes through the shock wave but decreases through the rarefaction wave, and that in general there is a jump in density across the contact discontinuity, which lies along the particle trajectory emanating from $x=0$ at $t=0$.

## Riemann solution with a colored tracer

Next we plot the Riemann solution with the density plot also showing an advected color to help visualize the flow better. The fluid initially to the left of $x=0$ is colored red and that initially to the right of $x=0$ is colored blue, with stripes of different shades of these colors to help visualize the motion of the fluid.

Let's plot the Sod shock tube data with this colored tracer:

```python
def plot_with_stripes_t_slider(t):
    euler_demos.plot_with_stripes(rho_l=3.,u_l=0.,p_l=3.,
                                  rho_r=1.,u_r=0.,p_r=1.,
                                  gamma=gamma,t=t)

interact(plot_with_stripes_t_slider,
         t=widgets.FloatSlider(min=0.,max=1.,step=0.1,value=0.5));
```

    interactive(children=(FloatSlider(value=0.5, description='t', max=1.0), Output()), _dom_classes=('widget-inter…

Note the following in the figure above:

 - The edges of each stripe are being advected with the fluid velocity, so you can visualize how the fluid is moving.
 - The width of each stripe initially is inversely proportional to the density of the fluid, so that the total mass of gas within each stripe is the same.
 - The total mass within each stripe remains constant as the flow evolves, and the width of each stripe remains inversely proportional to the local density.
 - The interface between the red and blue gas moves with the contact discontinuity.  The velocity and pressure are constant but the density can vary across this wave.

## Interactive Riemann solver

The initial configuration specified below gives a rather different looking solution than when using initial conditions of Sod, but with the same mathematical structure. *In the live notebook, you can easily adjust the initial data and immediately see the resulting solution.*

```python
euler_demos.euler_demo1(rho_l=2.,u_l=0.,p_l=2.5,
                        rho_r=3.,u_r=0.,p_r=5., gamma=gamma)
```

    interactive(children=(FloatSlider(value=2.0, description='$\\rho_l$', max=10.0, min=1.0), FloatSlider(value=0.…

    VBox(children=(HBox(children=(FloatSlider(value=2.0, description='$\\rho_l$', max=10.0, min=1.0), FloatSlider(…

    Output()

## Riemann problems with vacuum
A vacuum state (with zero pressure and density) in the Euler equations is similar to a dry state (with depth $h=0$) in the shallow water equations.  It can arise in the solution of the Riemann problem in two ways:

1. An initial left or right vacuum state: in this case the Riemann solution consists of a single rarefaction, connecting the non-vacuum state to vacuum.
2. A problem where the left and right states are not vacuum but middle states are vacuum.  Since this means the middle pressure is smaller than that to the left or right, this can occur only if the 1- and 3-waves are both rarefactions.  These rarefactions are precisely those required to connect the left and right states to the middle vacuum state.

### Initial vacuum state
Next we start with the density and pressure set to 0 in the left state. The velocity plot looks a bit strange, but note that the velocity is undefined in vacuum.  The solution structure consists of a rarefaction wave, similar to what is observed in the analogous case of a dam break problem with dry land on one side (depth $=0$), as discussed in [Shallow_water](Shallow_water.ipynb).

```python
left_state  = State(Density =0.,
                    Velocity = 0.,
                    Pressure = 0.)
right_state = State(Density = 1.,
                    Velocity = -3.,
                    Pressure = 1.)

euler.riemann_solution(left_state,right_state)
```

    interactive(children=(FloatSlider(value=0.5, description='t', max=0.9), Dropdown(description='Show characteris…

```python
euler.phase_plane_plot(left_state, right_state)
```

The phase plane plot may look odd, but recall that in the vacuum state velocity is undefined, and since $p_\ell = p_m = 0$, the left and middle states are actually the same.

### Middle vacuum state

Finally, we consider an example where there is sufficiently strong outflow ($u_\ell<0$ and $u_r>0$) that a vacuum state forms, analogous to the dry state that appears in the similar example in [Shallow_water](Shallow_water.ipynb).

```python
left_state  = State(Density =1.,
                    Velocity = -10.,
                    Pressure = 1.)
right_state = State(Density = 1.,
                    Velocity = 10.,
                    Pressure = 1.)

euler.riemann_solution(left_state,right_state)
```

    interactive(children=(FloatSlider(value=0.5, description='t', max=0.9), Dropdown(description='Show characteris…

```python
euler.phase_plane_plot(left_state, right_state)
```

Again the phase plane plot may look odd, but since the velocity is undefined in the vacuum state the middle state with $p_m = 0$ actually connects to both integral curves, which correspond to the two outgoing rarefaction waves.

```python

```


# **CS224W - Colab 3**

In Colab 2 we constructed GNN models by using PyTorch Geometric's built in GCN layer, `GCNConv`. In this Colab we will go a step deeper and implement the **GraphSAGE** ([Hamilton et al. (2017)](https://arxiv.org/abs/1706.02216)) layer directly. Then we will run our models on the CORA dataset, which is a standard citation network benchmark dataset.

**Note**: Make sure to **sequentially run all the cells in each section** so that the intermediate variables / packages will carry over to the next cell

Have fun and good luck on Colab 3 :)

# Device
We recommend using a GPU for this Colab.

Please click `Runtime` and then `Change runtime type`. Then set the `hardware accelerator` to **GPU**.

## Installation

```python
# Install torch geometric
import os
if 'IS_GRADESCOPE_ENV' not in os.environ:
  !pip install torch-scatter -f https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
  !pip install torch-sparse -f https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
  !pip install torch-geometric
  !pip install -q git+https://github.com/snap-stanford/deepsnap.git
```

    Looking in links: https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
    Requirement already satisfied: torch-scatter in /home/andreas/.local/lib/python3.8/site-packages (2.0.9)
    Looking in links: https://pytorch-geometric.com/whl/torch-1.9.0+cu111.html
    Requirement already satisfied: torch-sparse in /home/andreas/.local/lib/python3.8/site-packages (0.6.12)
    Requirement already satisfied: scipy in /home/andreas/.local/lib/python3.8/site-packages (from torch-sparse) (1.7.2)
    Requirement already satisfied: numpy<1.23.0,>=1.16.5 in /home/andreas/.local/lib/python3.8/site-packages (from scipy->torch-sparse) (1.21.4)
    Requirement already satisfied: torch-geometric in /home/andreas/.local/lib/python3.8/site-packages (2.0.2)
    Requirement already satisfied: PyYAML in /usr/lib/python3/dist-packages (from torch-geometric) (5.3.1)
    Requirement already satisfied: googledrivedownloader in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (0.4)
    Requirement already satisfied: tqdm in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (4.62.3)
    Requirement already satisfied: scipy in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (1.7.2)
    Requirement already satisfied: requests in /usr/lib/python3/dist-packages (from torch-geometric) (2.22.0)
    Requirement already satisfied: pandas in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (1.3.4)
    Requirement already satisfied: numpy in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (1.21.4)
    Requirement already satisfied: rdflib in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (6.0.2)
    Requirement already satisfied: yacs in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (0.1.8)
    Requirement already satisfied: jinja2 in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (3.0.3)
    Requirement already satisfied: scikit-learn in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (1.0.1)
    Requirement already satisfied: networkx in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (2.6.3)
    Requirement already satisfied: pyparsing in /home/andreas/.local/lib/python3.8/site-packages (from torch-geometric) (3.0.6)
    Requirement already satisfied: python-dateutil>=2.7.3 in /usr/lib/python3/dist-packages (from pandas->torch-geometric) (2.7.3)
    Requirement already satisfied: pytz>=2017.3 in /usr/lib/python3/dist-packages (from pandas->torch-geometric) (2019.3)
    Requirement already satisfied: isodate in /home/andreas/.local/lib/python3.8/site-packages (from rdflib->torch-geometric) (0.6.0)
    Requirement already satisfied: setuptools in /usr/lib/python3/dist-packages (from rdflib->torch-geometric) (45.2.0)
    Requirement already satisfied: MarkupSafe>=2.0 in /home/andreas/.local/lib/python3.8/site-packages (from jinja2->torch-geometric) (2.0.1)
    Requirement already satisfied: threadpoolctl>=2.0.0 in /home/andreas/.local/lib/python3.8/site-packages (from scikit-learn->torch-geometric) (3.0.0)
    Requirement already satisfied: joblib>=0.11 in /home/andreas/.local/lib/python3.8/site-packages (from scikit-learn->torch-geometric) (1.1.0)
    Requirement already satisfied: six in /usr/lib/python3/dist-packages (from isodate->rdflib->torch-geometric) (1.14.0)

```python
import torch_geometric
torch_geometric.__version__
```

    '2.0.2'

# 1) GNN Layers

## Implementing Layer Modules

In Colab 2, we implemented a GCN model for node and graph classification tasks. However, for that notebook we took advantage of PyG's built in GCN module. For Colab 3, we provide a build upon a general Graph Neural Network Stack, into which we will be able to plugin our own module implementations: GraphSAGE and GAT.

We will then use our layer implemenations to complete node classification on the CORA dataset, a standard citation network benchmark. In this dataset, nodes correspond to documents and edges correspond to undirected citations. Each node or document in the graph is assigned a class label and features based on the documents binarized bag-of-words representation. Specifically, the Cora graph has 2708 nodes, 5429 edges, 7 prediction classes, and 1433 features per node.

## GNN Stack Module

Below is the implementation of a general GNN stack, where we can plugin any GNN layer, such as **GraphSage**, **GAT**, etc. This module is provided for you. Your implementations of the **GraphSage** and **GAT** (Colab 4) layers will function as components in the GNNStack Module.

```python
import torch
import torch_scatter
import torch.nn as nn
import torch.nn.functional as F

import torch_geometric.nn as pyg_nn
import torch_geometric.utils as pyg_utils

from torch import Tensor
from typing import Union, Tuple, Optional
from torch_geometric.typing import (OptPairTensor, Adj, Size, NoneType,
                                    OptTensor)

from torch.nn import Parameter, Linear
from torch_sparse import SparseTensor, set_diag
from torch_geometric.nn.conv import MessagePassing
from torch_geometric.utils import remove_self_loops, add_self_loops, softmax

class GNNStack(torch.nn.Module):
    def __init__(self, input_dim, hidden_dim, output_dim, args, emb=False):
        super(GNNStack, self).__init__()
        conv_model = self.build_conv_model(args.model_type)
        self.convs = nn.ModuleList()
        self.convs.append(conv_model(input_dim, hidden_dim))
        assert (args.num_layers >= 1), 'Number of layers is not >=1'
        for l in range(args.num_layers-1):
            self.convs.append(conv_model(args.heads * hidden_dim, hidden_dim))

        # post-message-passing
        self.post_mp = nn.Sequential(
            nn.Linear(args.heads * hidden_dim, hidden_dim), nn.Dropout(args.dropout),
            nn.Linear(hidden_dim, output_dim))

        self.dropout = args.dropout
        self.num_layers = args.num_layers

        self.emb = emb

    def build_conv_model(self, model_type):

        if model_type == 'GraphSage':
            return GraphSage

        elif model_type == 'GAT':
            # When applying GAT with num heads > 1, you need to modify the
            # input and output dimension of the conv layers (self.convs),
            # to ensure that the input dim of the next layer is num heads
            # multiplied by the output dim of the previous layer.
            # HINT: In case you want to play with multiheads, you need to change the for-loop that builds up self.convs to be
            # self.convs.append(conv_model(hidden_dim * num_heads, hidden_dim)),
            # and also the first nn.Linear(hidden_dim * num_heads, hidden_dim) in post-message-passing.
            return GAT

    def forward(self, data):
        x, edge_index, batch = data.x, data.edge_index, data.batch

        for i in range(self.num_layers):
            x = self.convs[i](x, edge_index)
            x = F.relu(x)
            x = F.dropout(x, p=self.dropout,training=self.training)

        x = self.post_mp(x)

        if self.emb == True:
            return x

        return F.log_softmax(x, dim=1)

    def loss(self, pred, label):
        return F.nll_loss(pred, label)
```

## Creating Our Own Message Passing Layer

Now let's start implementing our own message passing layers! Working through this part will help us become acutely familiar with the behind the scenes work of implementing Pytorch Message Passing Layers, allowing us to build our own GNN models. To do so, we will work with and implement 3 critcal functions needed to define a PyG Message Passing Layer: `forward`, `message`, and `aggregate`.

Before diving head first into the coding details, let us quickly review the key components of the message passing process. To do so, we will focus on a single round of messsage passing with respect to a single central node $x$. Before message passing, $x$ is associated with a feature vector $x^{l-1}$, and the goal of message passing is to update this feature vector as $x^l$. To do so, we implement the following steps: 1) each neighboring node $v$ passes its current message $v^{l-1}$ across the edge $(x, v)$ - 2) for the node $x$, we aggregate all of the messages of the neighboring nodes (for example through a sum or mean) - and 3) we transform the aggregated information by for example applying linear and non-linear transformations. Altogether, the message passing process is applied such that every node $u$ in our graph updates its embedding by acting as the central node $x$ in step 1-3 described above.

Now, we extending this process to that of a single message passing layer, the job of a message passing layer is to update the current feature representation or embedding of each node in a graph by propagating and transforming information within the graph. Overall, the general paradigm of a message passing layers is: 1) pre-processing -> 2) **message passing** / propagation -> 3) post-processing.

The `forward` fuction that we will implement for our message passing layer captures this execution logic. Namely, the `forward` function handles the pre and post-processing of node features / embeddings, as well as initiates message passing by calling the `propagate` function.

The `propagate` function encapsulates the message passing process! It does so by calling three important functions: 1) `message`, 2) `aggregate`, and 3) `update`. Our implementation will vary slightly from this, as we will not explicitly implement `update`, but instead place the logic for updating node embeddings after message passing and within the `forward` function. To be more specific, after information is propagated (message passing), we can further transform the node embeddings outputed by `propagate`. Therefore, the output of `forward` is exactly the node embeddings after one GNN layer.

Lastly, before starting to implement our own layer, let us dig a bit deeper into each of the functions described above:

1.

```
def propagate(edge_index, x=(x_i, x_j), extra=(extra_i, extra_j), size=size):
```
Calling `propagate` initiates the message passing process. Looking at the function parameters, we highlight a couple of key parameters.

  - `edge_index` is passed to the forward function and captures the edge structure of the graph.
  - `x=(x_i, x_j)` represents the node features that will be used in message passing. In order to explain why we pass the tuple `(x_i, x_j)`, we first look at how our edges are represented. For every edge $(i, j) \in \mathcal{E}$, we can differentiate $i$ as the source or central node ($x_{central}$) and j as the neighboring node ($x_{neighbor}$).

    Taking the example of message passing above, for a central node $u$ we will aggregate and transform all of the messages associated with the nodes $v$ s.t. $(u, v) \in \mathcal{E}$ (i.e. $v \in \mathcal{N}_{u}$). Thus we see, the subscripts `_i` and `_j` allow us to specifcally differenciate features associated with central nodes (i.e. nodes  recieving message information) and neighboring nodes (i.e. nodes passing messages).

    This is definitely a somewhat confusing concept; however, one key thing to remember / wrap your head around is that depending on the perspective, a node $x$ acts as a central node or a neighboring node. In fact, in undirected graphs we store both edge directions (i.e. $(i, j)$ and $(j, i)$). From the central node perspective, `x_i`, x is collecting neighboring information to update its embedding. From a neighboring node perspective, `x_j`, x is passing its message information along the edge connecting it to a different central node.

  - `extra=(extra_i, extra_j)` represents additional information that we can associate with each node beyond its current feature embedding. In fact, we can include as many additional parameters of the form `param=(param_i, param_j)` as we would like. Again, we highlight that indexing with `_i` and `_j` allows us to differentiate central and neighboring nodes.

  The output of the `propagate` function is a matrix of node embeddings after the message passing process and has shape $[N, d]$.

2.
```
def message(x_j, ...):
```
The `message` function is called by propagate and constructs the messages from
neighboring nodes $j$ to central nodes $i$ for each edge $(i, j)$ in *edge_index*. This function can take any argument that was initially passed to `propagate`. Furthermore, we can again differentiate central nodes and neighboring nodes by appending `_i` or `_j` to the variable name, .e.g. `x_i` and `x_j`. Looking more specifically at the variables, we have:

  - `x_j` represents a matrix of feature embeddings for all neighboring nodes passing their messages along their respective edge (i.e. all nodes $j$ for edges $(i, j) \in \mathcal{E}$). Thus, its shape is $[|\mathcal{E}|, d]$!
  - In implementing GAT we will see how to access additional variables passed to propagate

  Critically, we see that the output of the `message` function is a matrix of neighboring node embeddings ready to be aggregated, having shape $[|\mathcal{E}|, d]$.

3.
```
def aggregate(self, inputs, index, dim_size = None):
```
Lastly, the `aggregate` function is used to aggregate the messages from neighboring nodes. Looking at the parameters we highlight:

  - `inputs` represents a matrix of the messages passed from neighboring nodes (i.e. the output of the `message` function).
  - `index` has the same shape as `inputs` and tells us the central node that corresponding to each of the rows / messages $j$ in the `inputs` matrix. Thus, `index` tells us which rows / messages to aggregate for each central node.

  The output of `aggregate` is of shape $[N, d]$.

For additional resources refer to the PyG documentation for implementing custom message passing layers: https://pytorch-geometric.readthedocs.io/en/latest/notes/create_gnn.html

## GraphSage Implementation

For our first GNN layer, we will implement the well known GraphSage ([Hamilton et al. (2017)](https://arxiv.org/abs/1706.02216)) layer!

For a given *central* node $v$ with current embedding $h_v^{l-1}$, the message passing update rule to tranform $h_v^{l-1} \rightarrow h_v^l$ is as follows:

\begin{equation}
h_v^{(l)} = W_l\cdot h_v^{(l-1)} + W_r \cdot AGG(\{h_u^{(l-1)}, \forall u \in N(v) \})
\end{equation}

where $W_1$ and $W_2$ are learanble weight matrices and the nodes $u$ are *neighboring* nodes. Additionally, we use mean aggregation for simplicity:

\begin{equation}
AGG(\{h_u^{(l-1)}, \forall u \in N(v) \}) = \frac{1}{|N(v)|} \sum_{u\in N(v)} h_u^{(l-1)}
\end{equation}

One thing to note is that we're adding a **skip connection** to our GraphSage implementation through the term $W_l\cdot h_v^{(l-1)}$.

Before implementing this update rule, we encourage you to think about how different parts of the formulas above correspond with the functions outlined earlier: 1) `forward`, 2) `message`, and 3) `aggregate`. As a hint, we are given what the aggregation function is (i.e. mean aggregation)! Now the question remains, what are the messages passed by each neighbor nodes and when do we call the `propagate` function?

Note: in this case the message function or messages are actually quite simple. Additionally, remember that the `propagate` function encapsulates the operations of / the outputs of the combined `message` and `aggregate` functions.

Lastly, $\ell$-2 normalization of the node embeddings is applied after each iteration.

For the following questions, DON'T refer to any existing implementations online.

```python
class GraphSage(MessagePassing):

    def __init__(self, in_channels, out_channels, normalize = True,
                 bias = False, **kwargs):
        super(GraphSage, self).__init__(**kwargs)

        self.in_channels = in_channels
        self.out_channels = out_channels
        self.normalize = normalize
        self.lin_l = None
        self.lin_r = None

        ############################################################################
        # TODO: Your code here!
        # Define the layers needed for the message and update functions below.
        # self.lin_l is the linear transformation that you apply to embedding
        #            for central node.
        # self.lin_r is the linear transformation that you apply to aggregated
        #            message from neighbors.
        # Don't forget the bias!
        # Our implementation is ~2 lines, but don't worry if you deviate from this.
        self.lin_l = nn.Linear(self.in_channels, self.out_channels, bias = bias)
        self.lin_r = nn.Linear(self.in_channels, self.out_channels, bias = bias) # not done...

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

        self.reset_parameters()

    def reset_parameters(self):
        self.lin_l.reset_parameters()
        self.lin_r.reset_parameters()

    def forward(self, x, edge_index, size = None):
        """"""

        out = None

        ############################################################################
        # TODO: Your code here!
        # Implement message passing, as well as any post-processing (our update rule).
        # 1. Call the propagate function to conduct the message passing.
        #    1.1 See the description of propagate above or the following link for more information:
        #        https://pytorch-geometric.readthedocs.io/en/latest/notes/create_gnn.html
        #    1.2 We will only use the representation for neighbor nodes (x_j), so by default
        #        we pass the same representation for central and neighbor nodes as x=(x, x).
        # 2. Update our node embedding with skip connection from the previous layer.
        # 3. If normalize is set, do L-2 normalization (defined in
        #    torch.nn.functional)
        #
        # Our implementation is ~5 lines, but don't worry if you deviate from this.

        ############################################################################
        x_j = torch.zeros([edge_index[1].size()[0],x.size()[1]])
        for i in range(edge_index[1].size()[0]): # find better way
            x_j[i] = x[edge_index[1][i]]
        agg = self.aggregate(x_j, edge_index[0],x.size()[0])
        out = self.lin_l(x) + self.lin_r(agg)

        if (self.normalize == 1):
            out=F.normalize(out,p=2)
        return out #Combined message parssing for all nodes

    def message(self, x_j):
        #print("test")
        out = None

        ############################################################################
        # TODO: Your code here!
        # Implement your message function here.
        # Hint: Look at the formulation of the mean aggregation function, focusing on
        # what message each neighboring node passes.
        #
        # Our implementation is ~1 lines, but don't worry if you deviate from this.
        ############################################################################

        return out

    def aggregate(self, inputs, index, dim_size = None):

        #out = None

        # The axis along which to index number of nodes.
        node_dim = self.node_dim

        ############################################################################
        # TODO: Your code here!
        # Implement your aggregate function here.
        # See here as how to use torch_scatter.scatter:
        # https://pytorch-scatter.readthedocs.io/en/latest/functions/scatter.html#torch_scatter.scatter
        #
        # Our implementation is ~1 lines, but don't worry if you deviate from this.
        out = torch_scatter.scatter(inputs, index, node_dim, dim_size=dim_size, reduce="mean")

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

        return out

```

## Building Optimizers

This function has been implemented for you. **For grading purposes please use the default Adam optimizer**, but feel free to play with other types of optimizers on your own.

```python
import torch.optim as optim

def build_optimizer(args, params):
    weight_decay = args.weight_decay
    filter_fn = filter(lambda p : p.requires_grad, params)
    if args.opt == 'adam':
        optimizer = optim.Adam(filter_fn, lr=args.lr, weight_decay=weight_decay)
    elif args.opt == 'sgd':
        optimizer = optim.SGD(filter_fn, lr=args.lr, momentum=0.95, weight_decay=weight_decay)
    elif args.opt == 'rmsprop':
        optimizer = optim.RMSprop(filter_fn, lr=args.lr, weight_decay=weight_decay)
    elif args.opt == 'adagrad':
        optimizer = optim.Adagrad(filter_fn, lr=args.lr, weight_decay=weight_decay)
    if args.opt_scheduler == 'none':
        return None, optimizer
    elif args.opt_scheduler == 'step':
        scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=args.opt_decay_step, gamma=args.opt_decay_rate)
    elif args.opt_scheduler == 'cos':
        scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=args.opt_restart)
    return scheduler, optimizer
```

## Training and Testing

Here we provide you with the functions to train and test. **Please do not modify this part for grading purposes.**

```python
import time

import networkx as nx
import numpy as np
import torch
import torch.optim as optim
from tqdm import trange
import pandas as pd
import copy

from torch_geometric.datasets import TUDataset
from torch_geometric.datasets import Planetoid
from torch_geometric.data import DataLoader

import torch_geometric.nn as pyg_nn

import matplotlib.pyplot as plt

def train(dataset, args):

    print("Node task. test set size:", np.sum(dataset[0]['test_mask'].numpy()))

    test_loader = loader = DataLoader(dataset, batch_size=args.batch_size, shuffle=False)

    # build model
    model = GNNStack(dataset.num_node_features, args.hidden_dim, dataset.num_classes,
                            args)
    scheduler, opt = build_optimizer(args, model.parameters())

    # train
    losses = []
    test_accs = []
    best_acc = 0
    best_model = None
    for epoch in trange(args.epochs, desc="Training", unit="Epochs"):
        total_loss = 0
        model.train()
        for batch in loader:
            opt.zero_grad()
            pred = model(batch) # runs here?
            label = batch.y
            pred = pred[batch.train_mask]
            label = label[batch.train_mask]
            loss = model.loss(pred, label)
            loss.backward()
            opt.step()
            total_loss += loss.item() * batch.num_graphs
        total_loss /= len(loader.dataset)
        losses.append(total_loss)

        if epoch % 10 == 0:
          test_acc = test(test_loader, model)
          test_accs.append(test_acc)
          if test_acc > best_acc:
            best_acc = test_acc
            best_model = copy.deepcopy(model)
        else:
          test_accs.append(test_accs[-1])

    return test_accs, losses, best_model, best_acc, test_loader

def test(loader, test_model, is_validation=False, save_model_preds=False, model_type=None):
    test_model.eval()

    correct = 0
    # Note that Cora is only one graph!
    for data in loader:
        with torch.no_grad():
            # max(dim=1) returns values, indices tuple; only need indices
            pred = test_model(data).max(dim=1)[1]
            label = data.y

        mask = data.val_mask if is_validation else data.test_mask
        # node classification: only evaluate on nodes in test set
        pred = pred[mask]
        label = label[mask]

        if save_model_preds:
          print ("Saving Model Predictions for Model Type", model_type)

          data = {}
          data['pred'] = pred.view(-1).cpu().detach().numpy()
          data['label'] = label.view(-1).cpu().detach().numpy()

          df = pd.DataFrame(data=data)
          # Save locally as csv
          df.to_csv('CORA-Node-' + model_type + '.csv', sep=',', index=False)

        correct += pred.eq(label).sum().item()

    total = 0
    for data in loader.dataset:
        total += torch.sum(data.val_mask if is_validation else data.test_mask).item()

    return correct / total

class objectview(object):
    def __init__(self, d):
        self.__dict__ = d

```

## Let's Start the Training!

We will be working on the CORA dataset on node-level classification.

This part is implemented for you. **For grading purposes, please do not modify the default parameters.** However, feel free to play with different configurations just for fun!

**Submit your best accuracy and loss on Gradescope.**

```python
if 'IS_GRADESCOPE_ENV' not in os.environ:
    for args in [
        {'model_type': 'GraphSage', 'dataset': 'cora', 'num_layers': 2, 'heads': 1, 'batch_size': 32, 'hidden_dim': 32, 'dropout': 0.5, 'epochs': 500, 'opt': 'adam', 'opt_scheduler': 'none', 'opt_restart': 0, 'weight_decay': 5e-3, 'lr': 0.01},
    ]:
        args = objectview(args)

        for model in ['GraphSage']:
            args.model_type = model

            # Match the dimension.
            if model == 'GAT':
              args.heads = 2
            else:
              args.heads = 1
            if args.dataset == 'cora':
                dataset = Planetoid(root='/tmp/cora', name='Cora')
            else:
                raise NotImplementedError("Unknown dataset")

            test_accs, losses, best_model, best_acc, test_loader = train(dataset, args)

            print("Maximum test set accuracy: {0}".format(max(test_accs)))
            print("Minimum loss: {0}".format(min(losses)))

            # Run test for our best model to save the predictions!
            test(test_loader, best_model, is_validation=False, save_model_preds=True, model_type=model)

            plt.title(dataset.name)
            plt.plot(losses, label="training loss" + " - " + args.model_type)
            plt.plot(test_accs, label="test accuracy" + " - " + args.model_type)
        plt.legend()
        plt.show()

```

## Question 1.1: What is the maximum accuracy obtained on the test set for GraphSage? (10 points)

Running the cell above will show the results of your best model and save your best model's predictions to a file named *CORA-Node-GraphSage.csv*.

As we have seen before you can view this file by clicking on the *Folder* icon on the left side pannel. When you sumbit your assignment, you will have to download this file and attatch it to your submission.


```python
import numpy as np
import numba
import matplotlib.pyplot as plt
import sympy as sym
plt.style.use('presentation.mplstyle')
#%matplotlib notebook

def d2np(d):

    names = []
    numbers = ()
    dtypes = []
    for item in d:
        names += item
        if type(d[item]) == float:
            numbers += (d[item],)
            dtypes += [(item,float)]
        if type(d[item]) == int:
            numbers += (d[item],)
            dtypes += [(item,int)]
        if type(d[item]) == np.ndarray:
            numbers += (d[item],)
            dtypes += [(item,np.float64,d[item].shape)]
    return np.array([numbers],dtype=dtypes)

```

### Fortescue

```python
alpha = np.exp(2.0/3*np.pi*1j)
A_0a =  np.array([[1, 1, 1],
                  [1, alpha**2, alpha],
                  [1, alpha, alpha**2]])

A_a0 = 1/3* np.array([[1, 1, 1],
                      [1, alpha, alpha**2],
                      [1, alpha**2, alpha]])
```

### Voltage source

```python
theta = np.deg2rad(20.0)
V_zero =  0.0*np.exp(1j*0.0)
V_neg  = 20.0*np.exp(1j*0.0)
V_pos  =400.0/np.sqrt(3)*np.exp(1j*theta)

V_zpn = np.array([[V_zero],[V_pos],[V_neg]])

V_abc = A_0a @ V_zpn
```

### Control inputs

```python
L = 500e-6
R = 0.01
omega = 2.0*np.pi*50.0
w = omega
v_dc = 800.0

V_012 = A_a0 @ V_abc
v_z = V_012[0,0]
v_p = V_012[1,0]
v_n = V_012[2,0]
```

### PLL

```python
theta_pll = np.angle(v_p)
```

### Park

```python
v_dq_z = v_z
v_dq_p = v_p*np.exp(-1j*theta_pll)*np.sqrt(2)
v_dq_n = v_n*np.exp( 1j*theta_pll)*np.sqrt(2)

v_d_z = v_dq_z.real # ??
v_q_z = v_dq_z.imag # ??
v_d_p = v_dq_p.imag
v_q_p = v_dq_p.real
v_d_n = v_dq_n.imag
v_q_n = v_dq_n.real
```

### References

```python
p_ref = 0.6e6
q_ref = 0.2e6

pq_ref = np.array([p_ref,q_ref,0,0]).reshape(4,1)
i2p=3/2*np.array([[ v_d_p, v_q_p, v_d_n, v_q_n],  # i_d_p
                  [-v_q_p, v_d_p,-v_q_n, v_d_n],  # i_q_p
                  [-v_q_n, v_d_n, v_q_p,-v_d_p],  # i_d_n
                  [ v_d_n, v_q_n, v_d_p, v_q_p]]) # i_q_n

p2i=np.linalg.inv(i2p)

i_dq_pn = p2i@pq_ref

i_d_p_ref = 100.0
i_q_p_ref = 0.0
i_d_n_ref = 0.0
i_q_n_ref = 0.0

i_d_p_ref = i_dq_pn[0,0]
i_q_p_ref = i_dq_pn[1,0]
i_d_n_ref = i_dq_pn[2,0]
i_q_n_ref = i_dq_pn[3,0]

mode = 'p_cte'

if mode == 'p_pos_i_n_0':
    i_d_p_ref = -(0.666666666666667*p_ref*v_d_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + 0.666666666666667*q_ref*v_q_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_p_ref = 0.666666666666667*(-p_ref*v_q_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + q_ref*v_d_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_d_n_ref = 0
    i_q_n_ref = 0

if mode == 'q_cte':
    i_d_p_ref = 0.666666666666667*(p_ref*v_d_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) + q_ref*v_q_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_p_ref = 0.666666666666667*(p_ref*v_q_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) - q_ref*v_d_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_d_n_ref = 0.666666666666667*(p_ref*v_d_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) - q_ref*v_q_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_n_ref = 0.666666666666667*(p_ref*v_q_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) + q_ref*v_d_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)

if mode == 'pq_cte': # Lipo
    i_d_p_ref = 0.666666666666667*(-p_ref*v_d_p + q_ref*v_q_p)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_q_p_ref = -(0.666666666666667*p_ref*v_q_p + 0.666666666666667*q_ref*v_d_p)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_d_n_ref = 0.666666666666667*(p_ref*v_d_n + q_ref*v_q_n)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_q_n_ref = 0.666666666666667*(p_ref*v_q_n - q_ref*v_d_n)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)

if mode == 'p_cte':
    i_d_p_ref = -(0.666666666666667*p_ref*v_d_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + 0.666666666666667*q_ref*v_q_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_p_ref = 0.666666666666667*(-p_ref*v_q_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + q_ref*v_d_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_d_n_ref = 0.666666666666667*(p_ref*v_d_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) - q_ref*v_q_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_n_ref = 0.666666666666667*(p_ref*v_q_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + q_ref*v_d_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)

if mode == 'z_mode':
    I_p_ref = np.conj((p_ref+1j*q_ref)/v_p)/3/np.sqrt(3)
    Z_p = v_p/I_p_ref
    I_n_ref = np.conj((p_ref+1j*q_ref)/v_n)/3/np.sqrt(3)
    Z_n = v_n/I_n_ref
    i_d_p_ref = ((v_q_p + 1j*v_d_p)/Z_p).imag
    i_q_p_ref = ((v_q_p + 1j*v_d_p)/Z_p).real
    i_d_n_ref = ((v_q_n + 1j*v_d_n)/Z_n).imag
    i_q_n_ref = ((v_q_n + 1j*v_d_n)/Z_n).real

```

### Control

```python
#L*did = e_d - R*i_d - w*L*i_q - v_d
#L*diq = e_q - R*i_q + w*L*i_d - v_q

eta_d_p = 2.0/v_dc*(R*i_d_p_ref + L*w*i_q_p_ref + v_d_p)
eta_q_p = 2.0/v_dc*(R*i_q_p_ref - L*w*i_d_p_ref + v_q_p)
eta_d_n = 2.0/v_dc*(R*i_d_n_ref + L*w*i_q_n_ref + v_d_n)
eta_q_n = 2.0/v_dc*(R*i_q_n_ref - L*w*i_d_n_ref + v_q_n)

eta_dq_p = eta_q_p + 1j*eta_d_p
e_dq_p = v_dc/2.0*eta_dq_p       # phase-neutral peak value

eta_dq_n = eta_q_n + 1j*eta_d_n
e_dq_n = v_dc/2.0*eta_dq_n       # phase-neutral peak value
```

### Modulation

```python
e_p = e_dq_p *np.exp( 1j*theta_pll)/np.sqrt(2) # phase-neutral RMS value
e_n = e_dq_n *np.exp(-1j*theta_pll)/np.sqrt(2) # phase-neutral RMS value
e_z = 0.0
#e_n = 0.0

e_012 = np.array([e_z,e_p,e_n]).reshape(3,1)
e_abc = A_0a @ e_012
```

### Plant

```python
Z_1 = R +1j *L*omega
Z_2 = Z_1
Z_0 = Z_1

Z_012 = np.diag([Z_0,Z_1,Z_2])

Z_abc = A_0a @ Z_012 @ A_a0

Y_abc = np.linalg.inv(Z_abc)
I_abc = Y_abc @ (e_abc-V_abc)

I_abc
```

    array([[ 842.37826331  +4.37596529j],
           [-374.42050632-862.59440972j],
           [-467.95775699+858.21844443j]])

```python
V_abc.T @ np.conj(I_abc)
```

    array([[ 600000.+200000.j]])

```python
I_012 = A_a0 @ I_abc
i_dq_z_out = I_012[0] ## ???
i_dq_p_out = I_012[1]*np.exp(-1j*theta_pll)*np.sqrt(2)
i_dq_n_out = I_012[2]*np.exp( 1j*theta_pll)*np.sqrt(2)

i_d_p = i_dq_p_out.imag
i_q_p = i_dq_p_out.real
i_d_n = i_dq_n_out.imag
i_q_n = i_dq_n_out.real

print(i_d_p_ref,i_d_p)
print(i_q_p_ref,i_q_p)
print(i_d_n_ref,i_d_n)
print(i_q_n_ref,i_q_n)

```

    -405.209221304 [-405.2092213]
    1233.99987042 [ 1233.99987042]
    -69.5266782161 [-69.52667822]
    -88.4204018619 [-88.42040186]

## Fisix

```python
p_cte_ref = 1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p
p_cos_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
p_sin_ref = 1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
q_cte_ref = -1.5*i_d_n*v_q_n - 1.5*i_d_p*v_q_p + 1.5*i_q_n*v_d_n + 1.5*i_q_p*v_d_p
q_cos_ref = -1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
q_sin_ref = 1.5*i_d_n*v_d_p - 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p - 1.5*i_q_p*v_q_n

lhs = ['p_cte_ref','p_cos_ref','p_sin_ref','q_cte_ref','q_cos_ref','q_sin_ref']
rhs = [p_cte_ref,p_cos_ref,p_sin_ref,q_cte_ref,q_cos_ref,q_sin_ref]
for lh,rh in zip(lhs,rhs):
    print('{:s}_ref = {:s}'.format(str(lh) ,str(sym.simplify(rh))))
```

    p_cte_ref_ref = 600000.000000000
    p_cos_ref_ref = 2.69210431724787e-10
    p_sin_ref_ref = 6.18456397205591e-11
    q_cte_ref_ref = 200000.000000000
    q_cos_ref_ref = 68121.9540560623
    q_sin_ref_ref = -86633.9469654325

### From phasor to time

```python
t = np.linspace(0.0,0.04-0.04/1000,1000)

v_a = (np.exp(1j*w*t)*V_abc[0]).real*np.sqrt(2)
v_b = (np.exp(1j*w*t)*V_abc[1]).real*np.sqrt(2)
v_c = (np.exp(1j*w*t)*V_abc[2]).real*np.sqrt(2)
i_a = (np.exp(1j*w*t)*I_abc[0]).real*np.sqrt(2)
i_b = (np.exp(1j*w*t)*I_abc[1]).real*np.sqrt(2)
i_c = (np.exp(1j*w*t)*I_abc[2]).real*np.sqrt(2)
v_a_p = (np.exp(1j*(w*t-np.pi/2))*V_abc[0]).real*np.sqrt(2)
v_b_p = (np.exp(1j*(w*t-np.pi/2))*V_abc[1]).real*np.sqrt(2)
v_c_p = (np.exp(1j*(w*t-np.pi/2))*V_abc[2]).real*np.sqrt(2)
#i_a = i_a_p + i_a_n
#i_b = i_c_p + i_c_n
#i_c = i_b_p + i_b_n

p = v_a*i_a + v_b*i_b + v_c*i_c
q = (i_a*(v_b-v_c) + i_b*(v_c-v_a) + i_c*(v_a-v_b))/np.sqrt(3)
q_lipo = v_a_p*i_a + v_b_p*i_b + v_c_p*i_c
#q = (i_a*(v_c-v_b) + i_b*(v_a-v_c) + i_c*(v_b-v_a))/np.sqrt(3)
```

```python
I_abc
```

    array([[ 842.37826331  +4.37596529j],
           [-374.42050632-862.59440972j],
           [-467.95775699+858.21844443j]])

```python
I_zpn = A_a0 @ I_abc
V_zpn = A_a0 @ V_abc

I_p = I_zpn[1]
I_n = I_zpn[2]
V_p = V_zpn[1]
V_n = V_zpn[2]

w = 2.0*np.pi*50.0

i_alpha_p = (np.exp( 1j*w*t)*I_p).imag*np.sqrt(2)
i_beta_p  = (np.exp( 1j*w*t)*I_p).real*np.sqrt(2)
i_alpha_n = (np.exp(-1j*w*t)*I_n).imag*np.sqrt(2)
i_beta_n  = (np.exp(-1j*w*t)*I_n).real*np.sqrt(2)

v_alpha_p = (np.exp( 1j*w*t)*V_p).imag*np.sqrt(2)
v_beta_p  = (np.exp( 1j*w*t)*V_p).real*np.sqrt(2)
v_alpha_n = (np.exp(-1j*w*t)*V_n).imag*np.sqrt(2)
v_beta_n  = (np.exp(-1j*w*t)*V_n).real*np.sqrt(2)

v_alpha_p_lipo = (-1j*np.exp( 1j*w*t)*V_p).imag*np.sqrt(2)
v_beta_p_lipo  = (-1j*np.exp( 1j*w*t)*V_p).real*np.sqrt(2)
v_alpha_n_lipo = (1j*np.exp(-1j*w*t)*V_n).imag*np.sqrt(2)
v_beta_n_lipo  = (1j*np.exp(-1j*w*t)*V_n).real*np.sqrt(2)

i_alpha = i_alpha_p + i_alpha_n
i_beta  = i_beta_p + i_beta_n
v_alpha = v_alpha_p + v_alpha_n
v_beta  = v_beta_p + v_beta_n
v_alpha_lipo = v_alpha_p_lipo + v_alpha_n_lipo
v_beta_lipo  = v_beta_p_lipo + v_beta_n_lipo
#Clark = 2/3*[[1/np.sqrt(2),1/np.sqrt(2),1/np.sqrt(2)],
#             [1,-0.5,-0.5]
#             [0,-np.sqrt(3)/2,np.sqrt(3)/2]]

#i_oab = np.array([0.0,i_alpha,i_beta])
#v_oab = np.array([0.0,v_alpha,v_beta])
inv_Clark=np.linalg.inv(Clark)
def oab2abc(alpha,beta):
    N_t = len(alpha)
    abc = np.zeros((3,N_t))
    for it in range():
        abc[:,it] = Clark
#for
#v_abc = np.lianlg.solve(Clark,v_oab)

p = 3/2*(i_alpha*v_alpha + i_beta*v_beta)
q = 3/2*(v_alpha*i_beta - v_beta*i_alpha)
q_lipo = 3/2*(i_alpha*v_alpha_lipo + i_beta*v_beta_lipo)
```

```python
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 4), sharex = True)

axes[0].plot(t, v_alpha)
axes[0].plot(t, v_beta)

axes[1].plot(t, i_alpha)
axes[1].plot(t, i_beta)

axes[2].plot(t, p/1000)
axes[2].plot(t, q/1000)
axes[2].plot(t, q_lipo/1000)

print('p = ',np.average(p))
print('q = ',np.average(q))
print('q_lipo = ',np.average(q_lipo))

print('i_alpha_max = ',np.max(abs(i_alpha)))
print('i_beta_max = ',np.max(abs(i_beta)))
```

    <IPython.core.display.Javascript object>

    p =  600000.0
    q =  197022.332506
    q_lipo =  200000.0
    i_alpha_max =  1405.03781184
    i_beta_max =  1193.74749139

Lipo

Figure 1

p =  500000.0
q =  1400000.0
q_lipo =  200000.0
i_alpha_max =  8080.75866864
i_beta_max =  1538.33671853

```python

```

    0.5714285714285714

### Reference following check

```python
I_012 = A_a0 @ I_abc
i_dq_z_out = I_012[0]*np.exp(-1j*theta_pll)*np.sqrt(2)
i_dq_p_out = I_012[1]*np.exp(-1j*theta_pll)*np.sqrt(2)
i_dq_n_out = I_012[2]*np.exp(-1j*theta_pll)*np.sqrt(2)

i_d_p = i_dq_p_out.imag
i_q_p = i_dq_p_out.real
i_d_n = i_dq_n_out.imag
i_q_n = i_dq_n_out.real

print(i_d_p_ref,i_dq_p_out.real)
print(i_q_p_ref,i_dq_p_out.imag)
print(i_d_n_ref,i_dq_n_out.real)
print(i_q_n_ref,i_dq_n_out.imag)

p_cte_ref = 1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p
p_cos_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
p_sin_ref =-1.5*i_d_n*v_q_p + 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p - 1.5*i_q_p*v_d_n
q_cte_ref = 1.5*i_d_n*v_q_n + 1.5*i_d_p*v_q_p - 1.5*i_q_n*v_d_n - 1.5*i_q_p*v_d_p
q_cos_ref = 1.5*i_d_n*v_q_p + 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p - 1.5*i_q_p*v_d_n
q_sin_ref = 1.5*i_d_n*v_d_p - 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p - 1.5*i_q_p*v_q_n

# Lipo
p_cte_ref = 1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p
p_cos_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
p_sin_ref = -1.5*i_d_n*v_q_p + 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p - 1.5*i_q_p*v_d_n
q_cte_ref = -1.5*i_d_n*v_q_n + 1.5*i_d_p*v_q_p + 1.5*i_q_n*v_d_n - 1.5*i_q_p*v_d_p
q_cos_ref = 1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
q_sin_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n

lhs = ['p_cte_ref','p_cos_ref','p_sin_ref','q_cte_ref','q_cos_ref','q_sin_ref']
rhs = [p_cte_ref,p_cos_ref,p_sin_ref,q_cte_ref,q_cos_ref,q_sin_ref]
for lh,rh in zip(lhs,rhs):
    print('{:s}_ref = {:s}'.format(str(lh) ,str(sym.simplify(rh))))

```

    -1632.99316186 [ 4082.48290464]
    4082.48290464 [-1632.99316186]
    -2538.14986202 [-3806.00464724]
    -2838.62559665 [-119.70223554]
    p_cte_ref_ref = 465260.769509609
    p_cos_ref_ref = -473917.012361638
    p_sin_ref_ref = -1184792.53090409
    q_cte_ref_ref = -1304554.74865842
    q_cos_ref_ref = 1184792.53090409
    q_sin_ref_ref = -473917.012361638

### Positive sequence calculation

```python
Z = R +1j *L*omega
I_pos = (e_p - v_p)/Z
I_pos
```

    (757.18068090442762-4.5051966725939852e-13j)

```python
S  =V_abc.T @ np.conj(I_abc)
S
```

    array([[ 500000. +2.04844920e-08j]])

```python
I_012 = A_a0 @ I_abc
I_012*np.sqrt(2)
```

    array([[ -6.02915504e-14 -8.03887339e-14j],
           [  1.07081519e+03 -4.82834833e-11j],
           [ -2.31838289e+02 +2.77743076e-11j]])

```python
import sympy as sym
```

```python
v_d_p,v_q_p,v_d_n,v_q_n = sym.symbols('v_d_p,v_q_p,v_d_n,v_q_n')

i2p = sym.Matrix([[ v_d_p, v_q_p, v_d_n, v_q_n],
                  [-v_q_p, v_d_p,-v_q_n, v_d_n],
                  [-v_q_n, v_d_n, v_q_p,-v_d_p],
                  [ v_d_n, v_q_n, v_d_p, v_q_p]])

p2i = sym.simplify(i2p.inv())
```

```python
sym.simplify(p2i)
```

    Matrix([
    [-v_d_p/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2), -v_q_p/(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2), (-v_d_n**2*v_q_n + 2*v_d_n*v_d_p*v_q_p - v_d_p**2*v_q_n - v_q_n**3 + v_q_n*v_q_p**2)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4),  (v_d_n**3 + v_d_n*v_d_p**2 + v_d_n*v_q_n**2 - v_d_n*v_q_p**2 + 2*v_d_p*v_q_n*v_q_p)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)],
    [-v_q_p/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2),  v_d_p/(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2),  (v_d_n**3 - v_d_n*v_d_p**2 + v_d_n*v_q_n**2 + v_d_n*v_q_p**2 - 2*v_d_p*v_q_n*v_q_p)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4),  (v_d_n**2*v_q_n + 2*v_d_n*v_d_p*v_q_p - v_d_p**2*v_q_n + v_q_n**3 + v_q_n*v_q_p**2)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)],
    [ v_d_n/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2), -v_q_n/(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2), (-v_d_n**2*v_q_p + 2*v_d_n*v_d_p*v_q_n - v_d_p**2*v_q_p + v_q_n**2*v_q_p - v_q_p**3)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4), (-v_d_n**2*v_d_p - 2*v_d_n*v_q_n*v_q_p - v_d_p**3 + v_d_p*v_q_n**2 - v_d_p*v_q_p**2)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)],
    [ v_q_n/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2),  v_d_n/(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2), (-v_d_n**2*v_d_p - 2*v_d_n*v_q_n*v_q_p + v_d_p**3 + v_d_p*v_q_n**2 + v_d_p*v_q_p**2)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4),  (v_d_n**2*v_q_p - 2*v_d_n*v_d_p*v_q_n - v_d_p**2*v_q_p - v_q_n**2*v_q_p - v_q_p**3)/(v_d_n**4 + 2*v_d_n**2*v_q_n**2 - v_d_p**4 - 2*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)]])

```python
theta = np.deg2rad(0.0)
phi = np.deg2rad(90.0)
V_zero =  0.0*np.exp(1j*0.0)
V_neg  =100.0*np.exp(1j*0.0)
V_pos  =231.0*np.exp(1j*theta)

V_012 = np.array([[V_zero],[V_pos],[V_neg]])

V_abc = A_0a @ V_012

I_zero =   0.0*np.exp(1j*0.0)
I_neg  =   0.0*np.exp(1j*(theta+phi))
I_pos  = 10.0*np.exp(1j*(theta-phi))

s_012 = 500e3
sin_012 = 0.0
cos_012 = 0.0
I_pos = (V_neg*sin_012 - V_pos*s_012)/(3*(V_neg**2 - V_pos**2))
I_neg = (V_neg*s_012 - V_pos*sin_012)/(3*(V_neg**2 - V_pos**2))

#I_pos = (-V_neg*sin_012 + V_pos*s_012)/(3*(V_neg**2 + V_pos**2))
#I_neg = ( V_neg*s_012 + V_pos*sin_012)/(3*(V_neg**2 + V_pos**2))
#I = 1j
#I_pos = 0.333333333333333*(V_neg*sin_012 - V_pos*s_012*(1.0 + I))/(V_neg**2*(1.0 - I) - V_pos**2*(1.0 + I))
#I_neg = 0.333333333333333*(V_neg*s_012*(1.0 - I) - V_pos*sin_012)/(V_neg**2*(1.0 - I) - V_pos**2*(1.0 + I))

#I_pos = 0.333333333333333*(V_neg*sin_012 - V_pos*s_012*(1.0 - I))/(V_neg**2*(1.0 + I) - V_pos**2*(1.0 - I))
#I_neg = 0.333333333333333*(V_neg*s_012*(1.0 + I) - V_pos*sin_012)/(V_neg**2*(1.0 + I) - V_pos**2*(1.0 - I))

#I_pos = 0.333333333333333*(I*V_neg*cos_012 + V_pos*s_012)/(V_neg**2 + V_pos**2)
#I_neg = 0.333333333333333*(V_neg*s_012 - I*V_pos*cos_012)/(V_neg**2 + V_pos**2)

#I_pos= (0.166666666666667 - 0.166666666666667*I)*(V_neg*(cos_012 + sin_012) - V_pos*s_012*(1.0 + I))/(V_neg**2 - V_pos**2)
#I_neg= (0.166666666666667 - 0.166666666666667*I)*(V_neg*s_012*(1.0 + I) - V_pos*(cos_012 + sin_012))/(V_neg**2 - V_pos**2)

#I_neg = (cos_012 + sin_012)/(6*V_pos)
#I_pos = (-V_neg*(cos_012 + sin_012) + 2*V_pos*s_012)/(6*V_pos**2)
I_pos = np.conj(s_012/(3*V_pos))
I_neg = -V_neg*I_pos/(V_pos)

I_012 = np.array([[I_zero],[I_pos],[I_neg]])

I_abc = A_0a @ I_012

```

```python
v_abc = (np.exp(1j*2.0*np.pi*50.0*t)*V_abc).real*np.sqrt(2)
i_abc = (np.exp(1j*2.0*np.pi*50.0*t)*I_abc).real*np.sqrt(2)

p = np.sum(v_abc * i_abc, axis=0)
q = -((v_abc[1]- v_abc[2]) * i_abc[0] + (v_abc[2]- v_abc[0]) * i_abc[1]  + (v_abc[0]- v_abc[1]) * i_abc[2] )/np.sqrt(3)
```

```python
fig, axes = plt.subplots(nrows=3, ncols=1, figsize=(8, 6), sharex = True)

axes[0].plot(t, v_abc[0,:])
axes[0].plot(t, v_abc[1,:])
axes[0].plot(t, v_abc[2,:])

axes[1].plot(t, i_abc[0,:])
axes[1].plot(t, i_abc[1,:])
axes[1].plot(t, i_abc[2,:])

axes[2].plot(t, p/1000)
axes[2].plot(t, q/1000)
```

    <IPython.core.display.Javascript object>

    [<matplotlib.lines.Line2D at 0x7f22cd879b70>]

```python
3*V_pos*I_pos
```

    (500000+0j)

```python
3*V_neg*I_neg
```

    (-93701.392402691112+0j)

```python
s_012 = 3*V_pos*I_pos + 3*V_neg*I_neg
```

```python
s_012
```

    (406298.60759730887+0j)

```python
sin_012 = 3*V_pos*I_neg + 3*V_neg*I_pos
cos_012 = 3*V_pos*I_neg - 3*V_neg*I_pos
```

```python
print(sin_012,cos_012)
```

    (-2.91038304567e-11+0j) (-432900.4329+0j)

```python
s_012,sin_012,cos_012,V_pos,I_pos,V_neg,I_neg = sym.symbols('s_012,sin_012,cos_012,V_pos,I_pos,V_neg,I_neg ')

sin_012_ = 3*V_pos*I_neg + 3*V_neg*I_pos
cos_012_ = 3*V_pos*I_neg - 3*V_neg*I_pos

eq1 = -s_012 + 3*V_pos*I_pos + 3*V_neg*I_neg
eq2 = sin_012-sin_012_ - cos_012+cos_012_
sym.solve([eq1,eq2],[I_pos,I_neg])
```

    {I_neg: (2*V_neg*s_012 + V_pos*(cos_012 - sin_012))/(6*V_neg**2),
     I_pos: -(cos_012 - sin_012)/(6*V_neg)}

```python

```

```python
I_pos
```

## Control Fisix

```python
from sympy.functions import re,im

v_d_p,v_q_p,v_d_n,v_q_n,i_d_p,i_q_p,i_d_n,i_q_n,wt = sym.symbols('v_d_p,v_q_p,v_d_n,v_q_n,i_d_p,i_q_p,i_d_n,i_q_n,wt',real=True)
p_ref,q_ref = sym.symbols('p_ref,q_ref',real=True)

exp_p = sym.cos( wt)+1j*sym.sin( wt)
exp_n = sym.cos(-wt)+1j*sym.sin(-wt)

v_dq_p = v_q_p + 1j*v_d_p
v_dq_n = v_q_n + 1j*v_d_n
i_dq_p = i_q_p + 1j*i_d_p
i_dq_n = i_q_n + 1j*i_d_n

s = 3/2*(v_dq_p*exp_p + v_dq_n*exp_n)*sym.conjugate(i_dq_p*exp_p + i_dq_n*exp_n)
s = sym.simplify(sym.factor(sym.expand(s)))

```

```python
p = sym.collect(re(s),[sym.cos(2*wt),sym.sin(2*wt)])
q = sym.collect(im(s),[sym.cos(2*wt),sym.sin(2*wt)])
```

```python
p_cos = p.diff(sym.cos(2*wt))
p_sin = p.diff(sym.sin(2*wt))
p_cte = sym.simplify(p - p_cos*sym.cos(2*wt) - p_sin*sym.sin(2*wt))
q_cos = q.diff(sym.cos(2*wt))
q_sin = q.diff(sym.sin(2*wt))
q_cte = sym.simplify(q - q_cos*sym.cos(2*wt) - q_sin*sym.sin(2*wt))
```

```python
lhs = ['p_cte','p_cos','p_sin','q_cte','q_cos','q_sin']
rhs = [p_cte,p_cos,p_sin,q_cte,q_cos,q_sin]
for lh,rh in zip(lhs,rhs):
    print('{:s}_ref = {:s}'.format(str(lh) ,str(sym.simplify(rh))))
```

    p_cte_ref = 1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p
    p_cos_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
    p_sin_ref = 1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
    q_cte_ref = -1.5*i_d_n*v_q_n - 1.5*i_d_p*v_q_p + 1.5*i_q_n*v_d_n + 1.5*i_q_p*v_d_p
    q_cos_ref = -1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
    q_sin_ref = 1.5*i_d_n*v_d_p - 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p - 1.5*i_q_p*v_q_n

### References for p constant

```python
sol = sym.solve([p_cte-p_ref,q_cte-q_ref,p_cos,p_sin],[i_d_p,i_q_p,i_d_n,i_q_n])
for item in [i_d_p,i_q_p,i_d_n,i_q_n]:
    print('{:s}_ref = {:s}'.format(str(item) ,str(sym.simplify(sol[item]))))
```

    i_d_p_ref = -(0.666666666666667*p_ref*v_d_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + 0.666666666666667*q_ref*v_q_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_p_ref = 0.666666666666667*(-p_ref*v_q_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + q_ref*v_d_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_d_n_ref = 0.666666666666667*(p_ref*v_d_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) - q_ref*v_q_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_n_ref = 0.666666666666667*(p_ref*v_q_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2) + q_ref*v_d_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)

### References for q constant

```python
sol = sym.solve([p_cte-p_ref,q_cte-q_ref,q_cos,q_sin],[i_d_p,i_q_p,i_d_n,i_q_n])
for item in [i_d_p,i_q_p,i_d_n,i_q_n]:
    print('{:s}_ref = {:s}'.format(str(item) ,str(sym.simplify(sol[item]))))
```

    i_d_p_ref = 0.666666666666667*(p_ref*v_d_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) + q_ref*v_q_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_p_ref = 0.666666666666667*(p_ref*v_q_p*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) - q_ref*v_d_p*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_d_n_ref = 0.666666666666667*(p_ref*v_d_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) - q_ref*v_q_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)
    i_q_n_ref = 0.666666666666667*(p_ref*v_q_n*(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2) + q_ref*v_d_n*(v_d_n**2 + v_d_p**2 + v_q_n**2 + v_q_p**2))/(v_d_n**4 + 2.0*v_d_n**2*v_q_n**2 - v_d_p**4 - 2.0*v_d_p**2*v_q_p**2 + v_q_n**4 - v_q_p**4)

### References for p and q constant

```python
sol = sym.solve([p_cte-p_ref,q_cte-q_ref,p_cos,q_sin],[i_d_p,i_q_p,i_d_n,i_q_n])
for item in [i_d_p,i_q_p,i_d_n,i_q_n]:
    print('{:s}_ref = {:s}'.format(str(item) ,str(sym.simplify(sol[item]))))
```

```python

```

```python
sym.simplify(p_cos-q_sin)
```

```python
sym.simplify(p_sin-q_cos)
```

### Lipo

```python
import sympy as sym
from sympy.functions import re,im

v_d_p,v_q_p,v_d_n,v_q_n,i_d_p,i_q_p,i_d_n,i_q_n,wt = sym.symbols('v_d_p,v_q_p,v_d_n,v_q_n,i_d_p,i_q_p,i_d_n,i_q_n,wt',real=True)
p_ref,q_ref = sym.symbols('p_ref,q_ref',real=True)

exp_p = sym.cos( wt)+1j*sym.sin( wt)
exp_n = sym.cos(-wt)+1j*sym.sin(-wt)

v_dq_p = v_d_p + 1j*v_q_p
v_dq_n = v_d_n + 1j*v_q_n
i_dq_p = i_d_p + 1j*i_q_p
i_dq_n = i_d_n + 1j*i_q_n

s = 3/2*(exp_p*v_dq_p + exp_n*v_dq_n)*sym.conjugate(exp_p*i_dq_p + exp_n*i_dq_n)
s = sym.simplify(sym.factor(sym.expand(s)))

t = 3/2*(-1j*exp_p*v_dq_p + 1j*exp_n*v_dq_n)*sym.conjugate(exp_p*i_dq_p + exp_n*i_dq_n)
t = sym.simplify(sym.factor(sym.expand(t)))

p = sym.collect(re(s),[sym.cos(2*wt),sym.sin(2*wt)])
q = sym.collect(re(t),[sym.cos(2*wt),sym.sin(2*wt)])

p_cos = p.diff(sym.cos(2*wt))
p_sin = p.diff(sym.sin(2*wt))
p_cte = sym.simplify(p - p_cos*sym.cos(2*wt) - p_sin*sym.sin(2*wt))
q_cos = q.diff(sym.cos(2*wt))
q_sin = q.diff(sym.sin(2*wt))
q_cte = sym.simplify(q - q_cos*sym.cos(2*wt) - q_sin*sym.sin(2*wt))

lhs = ['p_cte','p_cos','p_sin','q_cte','q_cos','q_sin']
rhs = [p_cte,p_cos,p_sin,q_cte,q_cos,q_sin]
for lh,rh in zip(lhs,rhs):
    print('{:s}_ref = {:s}'.format(str(lh) ,str(sym.simplify(rh))))
```

    p_cte_ref = 1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p
    p_cos_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
    p_sin_ref = -1.5*i_d_n*v_q_p + 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p - 1.5*i_q_p*v_d_n
    q_cte_ref = -1.5*i_d_n*v_q_n + 1.5*i_d_p*v_q_p + 1.5*i_q_n*v_d_n - 1.5*i_q_p*v_d_p
    q_cos_ref = 1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
    q_sin_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n

```python
p
```

    1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p + (1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n)*cos(2*wt) + (-1.5*i_d_n*v_q_p + 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p - 1.5*i_q_p*v_d_n)*sin(2*wt)

```python
sol = sym.solve([p_cte-p_ref,q_cte-q_ref,p_cos,p_sin],[i_d_p,i_q_p,i_d_n,i_q_n])
for item in [i_d_p,i_q_p,i_d_n,i_q_n]:
    print('{:s}_ref = {:s}'.format(str(item) ,str(sym.simplify(sol[item]))))
```

    i_d_p_ref = -(0.666666666666667*p_ref*v_d_p + 0.666666666666667*q_ref*v_q_p)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_q_p_ref = 0.666666666666667*(-p_ref*v_q_p + q_ref*v_d_p)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_d_n_ref = 0.666666666666667*(p_ref*v_d_n - q_ref*v_q_n)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_q_n_ref = 0.666666666666667*(p_ref*v_q_n + q_ref*v_d_n)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)

```python
q
```

    -1.5*i_d_n*v_q_n + 1.5*i_d_p*v_q_p + 1.5*i_q_n*v_d_n - 1.5*i_q_p*v_d_p + (1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n)*sin(2*wt) + (1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n)*cos(2*wt)

```python
p_cte_ref = 1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p
p_cos_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
p_sin_ref = -1.5*i_d_n*v_q_p + 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p - 1.5*i_q_p*v_d_n
q_cte_ref = -1.5*i_d_n*v_q_n + 1.5*i_d_p*v_q_p + 1.5*i_q_n*v_d_n - 1.5*i_q_p*v_d_p
q_cos_ref = 1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
q_sin_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
```

```python
## Lipo con dq según fisix
```

```python
import sympy as sym
from sympy.functions import re,im

v_d_p,v_q_p,v_d_n,v_q_n,i_d_p,i_q_p,i_d_n,i_q_n,wt = sym.symbols('v_d_p,v_q_p,v_d_n,v_q_n,i_d_p,i_q_p,i_d_n,i_q_n,wt',real=True)
p_ref,q_ref = sym.symbols('p_ref,q_ref',real=True)

exp_p = sym.cos( wt)+1j*sym.sin( wt)
exp_n = sym.cos(-wt)+1j*sym.sin(-wt)

v_dq_p = v_q_p + 1j*v_d_p
v_dq_n = v_q_n + 1j*v_d_n
i_dq_p = i_q_p + 1j*i_d_p
i_dq_n = i_q_n + 1j*i_d_n

s = 3/2*(exp_p*v_dq_p + exp_n*v_dq_n)*sym.conjugate(exp_p*i_dq_p + exp_n*i_dq_n)
s = sym.simplify(sym.factor(sym.expand(s)))

t = 3/2*(-1j*exp_p*v_dq_p + 1j*exp_n*v_dq_n)*sym.conjugate(exp_p*i_dq_p + exp_n*i_dq_n)
t = sym.simplify(sym.factor(sym.expand(t)))

p = sym.collect(re(s),[sym.cos(2*wt),sym.sin(2*wt)])
q = sym.collect(re(t),[sym.cos(2*wt),sym.sin(2*wt)])

p_cos = p.diff(sym.cos(2*wt))
p_sin = p.diff(sym.sin(2*wt))
p_cte = sym.simplify(p - p_cos*sym.cos(2*wt) - p_sin*sym.sin(2*wt))
q_cos = q.diff(sym.cos(2*wt))
q_sin = q.diff(sym.sin(2*wt))
q_cte = sym.simplify(q - q_cos*sym.cos(2*wt) - q_sin*sym.sin(2*wt))

lhs = ['p_cte','p_cos','p_sin','q_cte','q_cos','q_sin']
rhs = [p_cte,p_cos,p_sin,q_cte,q_cos,q_sin]
for lh,rh in zip(lhs,rhs):
    print('{:s}_ref = {:s}'.format(str(lh) ,str(sym.simplify(rh))))
```

    p_cte_ref = 1.5*i_d_n*v_d_n + 1.5*i_d_p*v_d_p + 1.5*i_q_n*v_q_n + 1.5*i_q_p*v_q_p
    p_cos_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n
    p_sin_ref = 1.5*i_d_n*v_q_p - 1.5*i_d_p*v_q_n - 1.5*i_q_n*v_d_p + 1.5*i_q_p*v_d_n
    q_cte_ref = 1.5*i_d_n*v_q_n - 1.5*i_d_p*v_q_p - 1.5*i_q_n*v_d_n + 1.5*i_q_p*v_d_p
    q_cos_ref = -1.5*i_d_n*v_q_p + 1.5*i_d_p*v_q_n + 1.5*i_q_n*v_d_p - 1.5*i_q_p*v_d_n
    q_sin_ref = 1.5*i_d_n*v_d_p + 1.5*i_d_p*v_d_n + 1.5*i_q_n*v_q_p + 1.5*i_q_p*v_q_n

```python
sol = sym.solve([p_cte-p_ref,q_cte-q_ref,p_cos,p_sin],[i_d_p,i_q_p,i_d_n,i_q_n])
for item in [i_d_p,i_q_p,i_d_n,i_q_n]:
    print('{:s}_ref = {:s}'.format(str(item) ,str(sym.simplify(sol[item]))))
```

    i_d_p_ref = 0.666666666666667*(-p_ref*v_d_p + q_ref*v_q_p)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_q_p_ref = -(0.666666666666667*p_ref*v_q_p + 0.666666666666667*q_ref*v_d_p)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_d_n_ref = 0.666666666666667*(p_ref*v_d_n + q_ref*v_q_n)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)
    i_q_n_ref = 0.666666666666667*(p_ref*v_q_n - q_ref*v_d_n)/(v_d_n**2 - v_d_p**2 + v_q_n**2 - v_q_p**2)

```python

```

```python

Clark = sym.Matrix([[1.0/sym.sqrt(2.0),1.0/sym.sqrt(2.0),1.0/sym.sqrt(2.0)],[1.0,-1.0/2.0,-1.0/2.0],[0,-sym.sqrt(3.0)/2.0,sym.sqrt(3.0)/2.0]])
```

```python
import numpy as np
Clark = 2/3*np.array([[1/np.sqrt(2),  1/np.sqrt(2),1/np.sqrt(2)],
                      [            1,         -0.5,        -0.5],
                      [            0,-np.sqrt(3)/2,np.sqrt(3)/2]])
inv_Clark = np.linalg.inv(Clark)
```

```python
pasar al tiempo con seq. pos y neg
```

```python
inv_Clark
```

    array([[  7.07106781e-01,   1.00000000e+00,   2.40370336e-17],
           [  7.07106781e-01,  -5.00000000e-01,  -8.66025404e-01],
           [  7.07106781e-01,  -5.00000000e-01,   8.66025404e-01]])

```python

```


# Variational Quantum Eigensolver (VQE)
In this section, the variational quantum eigensolver (VQE) is run on the simulator using Qulacs to find the ground state of the quantum chemical Hamiltonian obtained using OpenFermion/PySCF.

This notebook is translated from https://dojo.qulacs.org/ja/latest/notebooks/6.2_qulacs_VQE.html

Necessary packages:
* qulacs
* openfermion
* openfermion-pyscf
* pyscf
* scipy
* numpy

## Install and import necessary packages

```python
## Please execute if various libraries are not installed
## When use Google Colaboratory, please ignore 'You must restart the runtime in order to use newly installed versions'.
## Crash when restarting runtime.
!pip install qulacs pyscf openfermion openfermionpyscf
```

```python
import qulacs
from openfermion.transforms import get_fermion_operator, jordan_wigner
from openfermion.transforms import get_sparse_operator
from openfermion.hamiltonians import MolecularData
from openfermionpyscf import run_pyscf
from scipy.optimize import minimize
from pyscf import fci
import numpy as np
import matplotlib.pyplot as plt
```

## Create Hamiltonian
Create Hamiltonian by PySCF in the same procedure as described in the previous section.

```python
basis = "sto-3g"
multiplicity = 1
charge = 0
distance  = 0.977
geometry = [["H", [0,0,0]],["H", [0,0,distance]]]
description  = "tmp"
molecule = MolecularData(geometry, basis, multiplicity, charge, description)
molecule = run_pyscf(molecule,run_scf=1,run_fci=1)
n_qubit = molecule.n_qubits
n_electron = molecule.n_electrons
fermionic_hamiltonian = get_fermion_operator(molecule.get_molecular_hamiltonian())
jw_hamiltonian = jordan_wigner(fermionic_hamiltonian)
```

## Convert Hamiltonian to qulacs Hamiltonian
In Qulacs, observables like Hamiltonians are handled by the `Observable` class. There is a function `create_observable_from_openfermion_text` that converts OpenFermion Hamiltonian to Qulacs `Observable`.

```python
from qulacs import Observable
from qulacs.observable import create_observable_from_openfermion_text
qulacs_hamiltonian = create_observable_from_openfermion_text(str(jw_hamiltonian))
```

## Construct ansatz
Construct a quantum circuit on Qulacs. Here, the quantum circuit is modeled after the experiments with superconducting qubits (A. Kandala et. al. , “Hardware-efficient variational quantum eigensolver for small molecules and quantum magnets“, Nature **549**, 242–246).

```python
from qulacs import QuantumState, QuantumCircuit
from qulacs.gate import CZ, RY, RZ, merge

depth = n_qubit
```

```python
def he_ansatz_circuit(n_qubit, depth, theta_list):
    """he_ansatz_circuit
    Returns hardware efficient ansatz circuit.

    Args:
        n_qubit (:class:`int`):
            the number of qubit used (equivalent to the number of fermionic modes)
        depth (:class:`int`):
            depth of the circuit.
        theta_list (:class:`numpy.ndarray`):
            rotation angles.
    Returns:
        :class:`qulacs.QuantumCircuit`
    """
    circuit = QuantumCircuit(n_qubit)
    for d in range(depth):
        for i in range(n_qubit):
            circuit.add_gate(merge(RY(i, theta_list[2*i+2*n_qubit*d]), RZ(i, theta_list[2*i+1+2*n_qubit*d])))
        for i in range(n_qubit//2):
            circuit.add_gate(CZ(2*i, 2*i+1))
        for i in range(n_qubit//2-1):
            circuit.add_gate(CZ(2*i+1, 2*i+2))
    for i in range(n_qubit):
        circuit.add_gate(merge(RY(i, theta_list[2*i+2*n_qubit*depth]), RZ(i, theta_list[2*i+1+2*n_qubit*depth])))

    return circuit
```

## Define VQE cost function
As explained in Section 5-1, VQE obtains an approximate ground state by minimizing the expected value

\begin{equation}
\left<H(\theta)\right>=\left<\psi(\theta)|H|\psi(\theta)\right>
\end{equation}

of Hamiltonian for state $\left|\psi(\theta)\right>=U(\theta)\left|0\right>$ output from the quantum circuit $U(\theta)$ with parameters. The following defines a function that returns the expectation value of this Hamiltonian.

```python
def cost(theta_list):
    state = QuantumState(n_qubit) #Prepare |00000>
    circuit = he_ansatz_circuit(n_qubit, depth, theta_list) #Construct quantum circuit
    circuit.update_quantum_state(state) #Operate quantum circuit on state
    return qulacs_hamiltonian.get_expectation_value(state) #Calculate expectation value of Hamiltonian
```

## Run VQE
Now everthing is prepared, run VQE. For optimization, the BFGS method implemented in scipy is applied, and initial parameters are randomly selected. This process should end in tens of seconds.

```python
cost_history = []
init_theta_list = np.random.random(2*n_qubit*(depth+1))*1e-1
cost_history.append(cost(init_theta_list))
method = "BFGS"
options = {"disp": True, "maxiter": 50, "gtol": 1e-6}
opt = minimize(cost, init_theta_list,
               method=method,
               callback=lambda x: cost_history.append(cost(x)))
```

The results can be plotted to see that they converge to the correct solution.

```python
plt.rcParams["font.size"] = 18
plt.plot(cost_history, color="red", label="VQE")
plt.plot(range(len(cost_history)), [molecule.fci_energy]*len(cost_history), linestyle="dashed", color="black", label="Exact Solution")
plt.xlabel("Iteration")
plt.ylabel("Energy expectation value")
plt.legend()
plt.show()
```

If you are interested, you can calculate the ground state by varying the `distance` between hydrogen atoms to find the interatomic distance at which the hydrogen molecule is most stable. (It should be about 0.74 angstroms, depending on the performance of the ansatz.)


# Parameter Estimation in Markov Networks

> In the last session we saw how to estimate the parameters of a Bayesian Network. In particular, we saw that the maximum likelihood estimator can be calculated in closed form, simply as the frequentist interpretation of probability.
>
> In this session we will see how to estimate the parameters of a Markov Network. We will see that tha partition function will make it difficult to obtain a closed form solution of the optimal parameters, and then, we will have to use numerical methods to find them.
>
> Additionally, we will examine the MAP estimation of the parameters and its relation with including regularization terms in the optimization.

> **Objetives:**
> - To study the maximum likelihood parameter estimation problem for Markov Networks.
> - To study the maximum a posteriori parameter estimation problem for Markov Networks.

> **References:**
> - Probabilistic Graphical Models: Principles and Techniques, By Daphne Koller and Nir Friedman. Ch. 20.
> - Mastering Probabilistic Graphical Models Using Python, By Ankur Ankan and Abinash Panda. Ch. 6.
> - Probabilistic Graphical Models Specialization, offered through Coursera. Prof. Daphne Koller.

 Imagen recuperada de: https://upload.wikimedia.org/wikipedia/commons/d/d5/Hey_Machine_Learning_Logo.png.

___

## 1. Maximum likelihood parameter estimation in log-linear models

### 1.1. Log-likelihood function: lack of separability

A key property of the (log-)likelihood function for BNs was that it can be decomposed as the (sum) product of local likelihoods. Then, to maximize the whole function, one could maximize each local function separately.

We will see that this principle does not hold for MNs because of the partition function $Z$.

**Example:**

Consider the pairwise MN

```python
from IPython.display import Image
```

```python
Image("figures/pairwiseMN.png")
```

Assume, for instance, that we know:

|  $A$  |  $B$  |  $\phi_1$  |
| ----- | ----- | ---------- |
| $a^0$ | $b^0$ |  $1$       |
| $a^0$ | $b^1$ |  $1$       |
| $a^1$ | $b^0$ |  $1$       |
| $a^1$ | $b^1$ | $\theta_1$ |

|  $B$  |  $C$  |  $\phi_1$  |
| ----- | ----- | ---------- |
| $b^0$ | $c^0$ |  $1$       |
| $b^0$ | $c^1$ | $\theta_2$ |
| $b^1$ | $c^0$ |  $1$       |
| $b^1$ | $c^1$ |  $1$       |

We want to estimate $\bar{\theta} = [\theta_1, \theta_2]$ with the IID data

$$\mathcal{D}=\{(a[1], b[1], c[1]), \dots, (a[M], b[M], c[M])\}.$$

We know that

$$P_{\Phi}(A,B,C) =% \frac{1}{Z} \phi_1(A,B) \phi_2(B,C),$$

with $Z = \sum_{A,B,C} \phi_1(A,B) \phi_2(B,C)$.

Thus, the log-likelihood function is ( See in the whiteboard):

$$l(\bar{\theta}: \mathcal{D}) =% \sum_{d=1}^{M} \left(\log \phi_1(a[d], b[d]) + \log \phi_2(b[d], c[d]) - \log Z(\bar{\theta})\right),$$

with $Z(\bar{\theta}) = 4 + 2\theta_1 + 2 \theta_2.$

Assuming that $M(a^1, b^1)$ and $M(b^0, c^1)$ are the number of times that the joint assignments $a^1, b^1$ and $b^0, c^1$ appear in $\mathcal{D}$, respectively, we have:

$$l(\bar{\theta}: \mathcal{D}) =% M(a^1, b^1) \log \theta_1 + M(b^0, c^1) \log \theta_2 - M \log(4 + 2\theta_1 + 2 \theta_2).$$

The partition function $Z(\bar{\theta}) =% 4 + 2\theta_1 + 2 \theta_2$ **couples the parameters**:
- It is not possible to decompose the likelihood.
- We cannot obtain a closed form estimation for the parameters.

### 1.2. Log-likelihood for log-linear models

Recall that the log-linear models are a general representation. Given a set of features $\mathcal{F}=\{f_i(\bar{D}_i)\}_{i=1}^{k}$, where $f_i(\bar{D}_i)$ is a feature function defined over the variables $\bar{D}_i$, we have that the joint distribution for the log-linear model is:

$$P(X_1,\dots,X_n:\bar{\theta}) = \frac{1}{Z(\bar{\theta})} \exp\left\{ \sum_{i=1}^{k}\theta_i f_i (\bar{D}_i)\right\}.$$

The log-likelihood function is:

\begin{align}
l(\bar{\theta}:\mathcal{D}) & = \sum_{d=1}^{M}\left(\sum_{i=1}^{k}\theta_i f_i (\bar{x}[d]) - \log Z(\bar{\theta})\right) \\
                            & = \sum_{i=1}^{k}\theta_i \sum_{d=1}^{M} f_i (\bar{x}[d]) - M\log Z(\bar{\theta}),
\end{align}

where

$$\log Z(\bar{\theta}) = \log \left(\sum_{\bar{X}}\exp\left\{ \sum_{i=1}^{k}\theta_i f_i (\bar{D}_i)\right\}\right)$$

Before continuing, let's prove the following results (in the whiteboard):

$$\frac{\partial}{\partial \theta_i} \log Z(\bar{\theta})= E_{\theta}[f_i]$$

$$\frac{\partial^2}{\partial \theta_i \partial \theta_j} \log Z(\bar{\theta})= \mathrm{cov}_{\theta}[f_i,f_j]$$

Hence the Hessian (second derivatives' matrix) of log-partition function $\log Z(\bar{\theta})$ is:

$$\frac{\partial^2}{\partial \theta_i \partial \theta_j} \log Z(\bar{\theta})= \mathrm{cov}_{\theta}[f_i,f_j]$$

the covariance matrix of the features - **semi-positive definite**. In this sense the log-partition is:

1. Concave

2. Convex

3. None of the above

Then, $-M \log Z(\bar{\theta})$ is __.

On the other hand, the function $\sum_{i=1}^{k}\theta_i \sum_{d=1}^{M} f_i (\bar{x}[d])$ is linear.

Thus, the log-likelihood function is concave:

- No local maxima.
- Good theoretical guarantees

#### Maximum likelihood estimation

Given the above, we can divide the log-likelihood by the number of samples $M$ and the resulting function would still be concave:

\begin{align}
\frac{1}{M} l(\bar{\theta}:\mathcal{D}) & = \sum_{i=1}^{k}\theta_i \frac{1}{M}\sum_{d=1}^{M} f_i (\bar{x}[d]) - \log Z(\bar{\theta}) \\
                                        & = \sum_{i=1}^{k}\theta_i E_{\mathcal{D}}[f_i] - \log Z(\bar{\theta})
\end{align}

where $E_{\mathcal{D}}[f_i] = \frac{1}{M}\sum_{d=1}^{M} f_i (\bar{x}[d])$ is the empirical expectation of the feature $f_i$ in the data $\mathcal{D}$.

Thus, the gradient of the log-likelihood by the number of samples $M$ is:

$$\frac{\partial}{\partial \theta_i} \frac{1}{M} l(\bar{\theta}:\mathcal{D}) = E_{\mathcal{D}}[f_i] - E_{\theta}[f_i]$$

> *Theorem*. Given a set of features $\mathcal{F}$, $\hat{\theta}$ is the MLE if and only if
>
> $$E_{\mathcal{D}}[f_i] = E_{\hat{\theta}}[f_i]$$
>
> for all $i$.

Then, **how do we compute the MLE parameters?**

We can use numerical methods. In particular, the first order gradient ascent will do the job

$$\frac{\partial}{\partial \theta_i} \frac{1}{M} l(\bar{\theta}:\mathcal{D}) = E_{\mathcal{D}}[f_i] - E_{\theta}[f_i].$$

For the gradient, we need the expectations of the features:

- In data.
- Relative to current model: in this step we need to perform inference at each gradient step.

Unfortunately, `pgmpy` MarkovModel object do not have method fit. We'll do it ourselves to illustrate the above.

**Example:** See in the whiteboard the log-linear model of A-B.

```python
# Import numpy and pandas

```

```python
# Import scipy.optimize.fmin_cg

```

```python
# Generate some random data for A - B

```

```python
# Wrap data in a dataframe

```

```python
# Obtain empirical expectation of features

```

```python
# Objective function

```

```python
# Gradient

```

```python
# Solution

```

```python

```

```python

```

## 2. MAP estimation for MNs

As for BNs, MLE for MNs is very susceptible to overfitting of the parameters.

### Gaussian parameter prior

Often, the zero-mean univariate Gaussian (assuming independence of parameters) is used:

$$P(\bar{\theta}) = \prod_{i=1}^{k} \frac{1}{\sqrt{2\pi}\sigma} \exp\left\{-\frac{\theta_i^2}{2\sigma^2}\right\}$$

- $\sigma^2$ can be interpreted as the confidence that we have for the parameters not being close to zero.

### Laplacian parameter prior

Another commonly used prior is the Laplace distribution:

$$P(\bar{\theta}) = \prod_{i=1}^{k} \frac{1}{2\beta} \exp\left\{-\frac{|\theta_i|}{\beta}\right\}$$

- $\beta$ can be interpreted as the confidence that we have for the parameters not being close to zero.

### MAP estimation and regularization

What happens when we maximize the a posteriori distribution?

\begin{align}
\arg \max_{\theta} P(\mathcal{D}, \bar{\theta}) & = \arg \max_{\theta} P(\mathcal{D}| \bar{\theta}) P(\bar{\theta}) \\
                                                & = \arg \max_{\theta} \left(l(\bar{\theta}:\mathcal{D}) + \log P(\bar{\theta})\right)
\end{align}

- If $P$ is Gaussian: $-log P(\bar{\theta}) \equiv L_2$ (dense)
- If $P$ is Laplacian: $-log P(\bar{\theta}) \equiv L_1$ (sparse)

```python
# MAP objective function

```

```python
# MAP objective function

```

```python
# Solution

```

```python

```

Created with Jupyter by Esteban Jiménez Rodríguez.