# Defining the DICOM orientation

The Idea of this is to emulate the information gathered from  [nibabel](http://nipy.org/nibabel/dicom/dicom_orientation.html)
and [spm_dicom_orient.py](http://nipy.org/nibabel/_downloads/spm_dicom_orient.py) into one handy jupyter notebook with the explanations along with the sympy output.

This page describes the DICOM orientaion. The sections will be as follows:

1. Defining the **DICOM** orientation
* DICOM patient coordinate system
* DICOM pixel data
* Pixel spacing
* DICOM voxel to patient coordinate system mapping
* (i, j), columns, rows in DICOM
* DICOM affines again
* DICOM affine formula
* Getting a 3D affine from a DICOM slice or list of slices
* DICOM affine Definitions
* Derivations
* 3D affine formulae
* Working out the Z coordinates for a set of slices

# DICOM patient coordinate system
First we define the standard DICOM patient-based coordinate system. This is what DICOM means by $x$, $y$ and $z$ axes in its orientation specification. From section C.7.6.2.1.1 of the From section C.7.6.2.1.1 of the [DICOM object definitions](ftp://medical.nema.org/medical/dicom/2011/11_03pu3.pdf) (2009):

 > If Anatomical Orientation Type (0010,2210) is absent or has a value of BIPED, the x-axis is increasing to the left hand side of the patient. The y-axis is increasing to the posterior side of the patient. The z-axis is increasing toward the head of the patient.

(we’ll ignore the quadupeds for now).

Three major cuts we have (or planes):

Transverse (AKA Axial) divides head from feet

Sagitall Cut - right between the eyes

Coronal Cut - the Filet

In a way it’s funny to call this the ‘patient-based’ coordinate system. ‘Doctor-based coordinate system’ is a better name.

DICOM defines a term: "Reference Coordinates System" or RCS.  The RCS is a very intuitive coordinate system of the patient body: $X$ direction is from Right to Left. So if the patient is standing in front of you with the arm raised to the sides, then $X$ direction is from the right hand to the left hand.

$Y$ direction is from front to back or medical-wise from Anterior to Posterior so if the patient is standing in front of you so you see him/her from his/her left side, then $Y$ goes from your left to your right (confusing? look at the picture).

$Z$ direction goes from Feet to Head. At least this is simple to explain.

Now that we know the directions, there are letters assigned to the ends of each direction:

* [R] - Right - The direction in which X decreases.
* [L] - Left - The direction in which X increases.
* [A] - Anterior - The direction in which Y decreases.
* [P] - Posterior - The direction in which Y increases.
* [F] - Feet - The direction in which Z decreases.
* [H] - Head - The direction in which Z increases.

#  DICOM pixel data
C.7.6.3.1.4 - Pixel Data

> Pixel Data (7FE0,0010) for this image. The order of pixels sent for each image plane is left to right, top to bottom, i.e., the upper left pixel (labeled 1,1) is sent first followed by the remainder of row 1, followed by the first pixel of row 2 (labeled 2,1) then the remainder of row 2 and so on.

The resulting pixel array then has size (‘Rows’, ‘Columns’), with row-major storage (rows first, then columns). We’ll call this the DICOM pixel array. Thinking about it in the image plane (not with respect to the RCS), $X$ is vertical (rows) and increasing downward and $Y$ is horizontal (columns) increasing left to right.

# Pixel spacing
Section 10.7.1.3: Pixel Spacing

> The first value is the row spacing in mm, that is the spacing between the centers of adjacent rows, or vertical spacing. The second value is the column spacing in $mm$, that is the spacing between the centers of adjacent columns, or horizontal spacing.

# DICOM voxel to patient coordinate system mapping
See:

* http://www.dclunie.com/medical-image-faq/html/part2.html
* http://fixunix.com/dicom/50449-image-position-patient-image-orientation-patient.html
See [wikipedia direction cosine](https://en.wikipedia.org/wiki/Direction_cosine) for a definition of direction cosines.

From section C.7.6.2.1.1 of the [DICOM object definitions](http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf) (2009):

> The Image Position (0020,0032) specifies the $x$, $y$, and $z$ coordinates of the upper left hand corner of the image; it is the center of the first voxel transmitted. Image Orientation (0020,0037) specifies the direction cosines of the first row and the first column with respect to the patient. These Attributes shall be provide as a pair. Row value for the $x$, $y$, and $z$ axes respectively followed by the Column value for the $x$, $y$, and $z$ axes respectively.

From Section C.7.6.1.1.1 we see that the ‘positive row axis’ is left to right, and is the direction of the rows, given by the direction of last pixel in the first row from the first pixel in that row. Similarly the ‘positive column axis’ is top to bottom and is the direction of the columns, given by the direction of the last pixel in the first column from the first pixel in that column.

Let’s rephrase: the first three values of ‘Image Orientation Patient’ are the direction cosine for the ‘positive row axis’. That is, they express the direction change in ($x$, $y$, $z$), in the DICOM patient coordinate system (DPCS), as you move along the row. That is, as you move from one column to the next. That is, as the column array index changes. Similarly, the second triplet of values of ‘Image Orientation Patient’ ( **img_ornt_pat[3:]** in Python), are the direction cosine for the ‘positive column axis’, and express the direction you move, in the DPCS, as you move from row to row, and therefore as the row index changes.

Further down section C.7.6.2.1.1 (RCS below is the reference coordinate system - see [DICOM object definitions](http://dicom.nema.org/medical/dicom/current/output/pdf/part03.pdf) section 3.17.1):

> The Image Plane Attributes, in conjunction with the Pixel Spacing Attribute, describe the position and orientation of the image slices relative to the patient-based coordinate system. In each image frame the Image Position (Patient) (0020,0032) specifies the origin of the image with respect to the patient-based coordinate system. RCS and the Image Orientation (Patient) (0020,0037) attribute values specify the orientation of the image frame rows and columns. The mapping of pixel location ($i$, $j$) to the RCS is calculated as follows:

$$\begin{bmatrix}
  P_x  \\ P_y \\ P_z \\ 1
  \end{bmatrix}
= \begin{bmatrix}
  X_x \Delta_i & Y_x \Delta_j & 0 & S_x \\
  X_y \Delta_i & Y_y \Delta_j & 0 & S_y \\
  X_z \Delta_i & Y_z \Delta_j & 0 & S_z \\
  0 & 0 & 0  & 1 \\
  \end{bmatrix}
  \begin{bmatrix}
  i  \\ j \\ 0 \\ 1
  \end{bmatrix}
  =   M \begin{bmatrix}
  i  \\ j \\ 0 \\ 1
  \end{bmatrix}
$$

Where:

* $P_{xyz}$ : The coordinates of the voxel (i,j) in the frame’s image plane in units of mm.
* $S_{xyz}$ : The three values of the Image Position (Patient) (0020,0032) attributes. It is the location in mm from the origin of the RCS.
* $X_{xyz}$ : The values from the row (X) direction cosine of the Image Orientation (Patient) (0020,0037) attribute.
* $Y_{xyz}$ : The values from the column (Y) direction cosine of the Image Orientation (Patient) (0020,0037) attribute.
* $i$ : Column index to the image plane. The first column is index zero.
* $\Delta_i$ : Column pixel resolution of the Pixel Spacing (0028,0030) attribute in units of mm.
* $j$ : Row index to the image plane. The first row index is zero.
* $\Delta_j$ : Row pixel resolution of the Pixel Spacing (0028,0030) attribute in units of mm.

# $(i, j)$, columns, rows in DICOM
We stop to ask ourselves, what does DICOM mean by voxel $(i, j)$?

Isn’t that obvious? Oh dear, no it isn’t. See the [DICOM voxel to patient coordinate system mapping](#DICOM voxel to patient coordinate system mapping) formula above. In particular, you’ll see that thinking in terms of the traditional $X$-$Y$ graph:

* i : Column index to the image plane. The first column is index zero. think $X$ direction
* j : Row index to the image plane. The first row index is zero. think $Y$ direction

That is, if we have the [DICOM pixel data](#DICOM pixel data) as defined above, and we call that **pixel_array** , then voxel ($i$, $j$) in the notation above is given by **pixel_array**[$j$, $i$]. Essentially we need to switch $i$ and $j$

What does this mean? It means that, if we want to apply the formula above to array indices in **pixel_array**, we first have to apply a column / row flip to the indices. Say $M_{pixelArr}$ is the affine to go from array indices in **pixel_array** to $mm$ in the DPCS. Then, given $M$ above:
$$ M_{pixelArr} = M \begin{bmatrix}
  0 & 1 & 0 & 0 \\
  1 & 0 & 0 & 0 \\
  0 & 0 & 1 & 0 \\
  0 & 0 & 0  & 1 \\
  \end{bmatrix}
$$

# DICOM affines again
The [(i, j), columns, rows in DICOM](# \(i, j\), columns, rows in DICOM) is rather confusing, so we’re going to rephrase the affine mapping; we’ll use $r$ for the row index (instead of $j$ above), and $c$ for the column index (instead of $i$).

Next we define a flipped version of ‘ImageOrientationPatient’, $F$, that has flipped columns. Thus if the vector of 6 values in ‘ImageOrientationPatient’ are $(i_1 ... i_6)$, then:
$$ F = \begin{bmatrix}
  i_4 & i_1 \\
  i_5 & i_2 \\
  i_6 & i_3 \\
  \end{bmatrix}
  =
  \begin{bmatrix}
  F_{11} & F_{12} \\
  F_{21} & F_{22} \\
  F_{31} & F_{32} \\
  \end{bmatrix}
$$

Now the first column of $F$ contains what the DICOM docs call the ‘column ($Y$) direction cosine’, and second column contains the ‘row ($X)$ direction cosine’. We prefer to think of these as (respectively) the row index direction cosine and the column index direction cosine.

Now we can rephrase the DICOM affine mapping with:

$$\begin{bmatrix}
  P_x  \\ P_y \\ P_z \\ 1
  \end{bmatrix}
= \begin{bmatrix}
  F_{11} \Delta r & F_{12} \Delta c & 0 & S_x \\
  F_{21} \Delta r & F_{22} \Delta c & 0 & S_y \\
  F_{31} \Delta r & F_{32} \Delta c & 0 & S_z \\
  0 & 0 & 0  & 1 \\
  \end{bmatrix}
  \begin{bmatrix}
  r  \\ c \\ 0 \\ 1
  \end{bmatrix}
  =   A \begin{bmatrix}
  r  \\ c \\ 0 \\ 1
  \end{bmatrix}
$$

Where:

* $P_{xyz}$ : The coordinates of the voxel ($c$, $r$) in the frame’s image plane in units of mm.
* $S_{xyz}$ : The three values of the Image Position (Patient) (0020,0032) attributes. It is the location in mm from the origin of the RCS.
* $F_{:,1}$ : The values from the column (Y) direction cosine of the Image Orientation (Patient) (0020,0037) attribute - see above.
* $F_{:,2}$ : The values from the row (X) direction cosine of the Image Orientation (Patient) (0020,0037) attribute - see above.
* $c$ : Column index to the image plane. The first column is index zero.
* $\Delta c$ : Column pixel resolution of the Pixel Spacing (0028,0030) attribute in units of mm.
* $r$ : Row index to the image plane. The first row index is zero.
* $\Delta r$ : Row pixel resolution of the Pixel Spacing (0028,0030) attribute in units of mm.

For later convenience we also define values useful for 3D volumes:

* $s$ : slice index to the slice plane. The first slice index is zero.
* $\Delta s$ : spacing in mm between slices.

# Getting a 3D affine from a DICOM slice or list of slices
Let us say, we have a single DICOM file, or a list of DICOM files that we believe to be a set of slices from the same volume. We’ll call the first the _single slice_ case, and the second, _multi slice_.

In the _multi slice_ case, we can assume that the ‘ImageOrientationPatient’ field is the same for all the slices.

We want to get the affine transformation matrix A that maps from voxel coordinates in the DICOM file(s), to $mm$ in the [DICOM patient coordinate system]().

By voxel coordinates, we mean coordinates of form ($r$,$c$,$s$) - the row, column and slice indices - as for the [DICOM affine formula]().

In the single slice case, the voxel coordinates are just the indices into the pixel array, with the third (slice) coordinate always being 0.

In the _multi-slice_ case, we have arranged the slices in ascending or descending order, where slice numbers range from $0$ to $N−1$ - where $N$ is the number of slices - and the slice coordinate is a number on this scale.

We know, from [DICOM affine formula](), that the first, second and fourth columns in $A$ are given directly by the (flipped) ‘ImageOrientationPatient’, ‘PixelSpacing’ and ‘ImagePositionPatient’ field of the first (or only) slice.

Our job then is to fill the first three rows of the third column of $A$. Let’s call this the vector $\textbf{k}$ with values $k_1$, $k_2$, $k_3$.

# DICOM affine Definitions
See also the definitions in [DICOM affine formula](). In addition

* $T_1$ is the 3 element vector of the ‘ImagePositionPatient’ field of the first header in the list of headers for this volume.
* $T_N$ is the ‘ImagePositionPatient’ vector for the last header in the list for this volume, if there is more than one header in the volume.
* vector $\textbf{n}=(n_1,n_2,n_3)$ is the result of taking the cross product of the two columns of $F$ from [DICOM affine formula]().

# Derivations
For the single slice case we just fill $\textbf{k}$ with $\textbf{n} \cdot \Delta s $ - on the basis that the $Z$ dimension should be right-handed orthogonal to the $X$ and $Y$ directions.

For the multi-slice case, we can fill in $k$ by using the information from $T^N$, because $T^N$ is the translation needed to take the first voxel in the last ( $slice index = N−1$ ) slice to $mm$ space. So:

$$ \left(T^N_1\right) = A
\begin{bmatrix}
  0  \\ 0 \\ -1 + N \\ 1
  \end{bmatrix}
$$

From this it follows that:
$$\left\{k_1 : \frac{T^1_1 - T^N_1}{1-N},\: k_2 : \frac{T^1_2 - T^N_2}{1-N}
,\: k_3 : \frac{T^1_3 - T^N_3}{1-N}\right\}$$

and therefore:
# 3D affine formulae

$$A_{multi}
= \begin{bmatrix}
  F_{11} \Delta r & F_{12} \Delta c & \frac{T^1_1 - T^N_1}{1-N} & T^1_1 \\
  F_{21} \Delta r & F_{22} \Delta c & \frac{T^1_2 - T^N_2}{1-N} & T^1_2 \\
  F_{31} \Delta r & F_{32} \Delta c & \frac{T^1_3 - T^N_3}{1-N} & T^1_3 \\
  0 & 0 & 0  & 1 \\
  \end{bmatrix}
$$

$$A_{single}
= \begin{bmatrix}
  F_{11} \Delta r & F{12} \Delta c & \Delta s n_1 & T^1_1 \\
  F_{21} \Delta r & F_{22} \Delta c & \Delta s n_2 & T^1_2 \\
  F_{31} \Delta r & F_{32} \Delta c & \Delta s n_3 & T^1_3 \\
  0 & 0 & 0  & 1 \\
  \end{bmatrix}
$$

See derivations/spm_dicom_orient.py for the derivations and some explanations

# Working out the Z coordinates for a set of slices
We may have the problem (see e.g. Sorting files into volumes) of trying to sort a set of slices into anatomical order. For this we want to use the orientation information to tell us where the slices are in space, and therefore, what order they should have.

To do this sorting, we need something that is proportional, plus a constant, to the voxel coordinate for the slice (the value for the slice index).

Our DICOM might have the ‘SliceLocation’ field (0020,1041). ‘SliceLocation’ seems to be proportianal to slice location, at least for some GE and Philips DICOMs I was looking at. But, there is a more reliable way (that doesn’t depend on this field), and uses only the very standard ‘ImageOrientationPatient’ and ‘ImagePositionPatient’ fields.

Consider the case where we have a set of slices, of unknown order, from the same volume.

Now let us say we have one of these slices - slice $i$. We have the affine for this slice from the calculations above, for a single slice ($A_{single}$).

Now let’s say we have another slice $j$ from the same volume. It will have the same affine, except that the ‘ImagePositionPatient’ field will change to reflect the different position of this slice in space. Let us say that there a translation of $d$ slices between $i$ and $j$. If $A_i$ ($A$ for slice $i$) is $A_{single}$ then $A_j$ for $j$ is given by:

$$ A_j = A_{single} \begin{bmatrix}
  1 & 0 & 0 & 0 \\
  0 & 1 & 0 & 0 \\
  0 & 0 & 1 & d \\
  0 & 0 & 0  & 1 \\
  \end{bmatrix}
$$

and ‘ImagePositionPatient’ for $j$ is:
$$ T^j = \begin{bmatrix}
  T^1_1 + \Delta s d n_1 \\ T^1_2 + \Delta s d n_2 \\ T^1_3 + \Delta s d n_3
  \end{bmatrix}
$$

Remember that the third column of $A$ gives the vector resulting from a unit change in the slice voxel coordinate. So, the ‘ImagePositionPatient’ of slice - say slice $j$ - can be thought of the addition of two vectors $T^j = \textbf{a + b}$, where $\textbf{a}$ is the position of the first voxel in some slice (here slice 1, therefore $\textbf{a} = T^1$) and $\textbf{b}$ is $d$ times the third column of $A$. Obviously $d$ can be negative or positive. This leads to various ways of recovering something that is proportional to $d$ plus a constant. The algorithm suggested in this [ITK post on ordering slices](http://www.itk.org/pipermail/insight-users/2003-September/004762.html) - and the one used by SPM - is to take the inner product of $T^j$ with the unit vector component of third column of $A_j$ - in the descriptions here, this is the vector $\textbf{n}$:

$$ T^j\cdot \textbf{c} = \left( T^1_1 n_1 + T^1_2 n_2 + T^1_3 n_3 + \Delta s d n^2_1
\Delta s d n^2_2 + \Delta s d n^2_3 \right) $$
This is the distance of ‘ImagePositionPatient’ along the slice direction cosine.

The unknown $T^1$ terms pool into a constant, and the operation has the neat feature that, because the $n^2_{123}$ terms, by definition, sum to 1, the whole can be expressed as $\gamma + \Delta s d $ - i.e. it is equal to the slice voxel size ( $\Delta s $) multiplied by $d$, plus a constant.

Again, see derivations/spm_dicom_orient.py for the derivations.

```python
""" Symbolic versions of the DICOM orientation mathemeatics.

Notes on the SPM orientation machinery.

There are symbolic versions of the code in ``spm_dicom_convert``,
``write_volume`` subfunction, around line 509 in the version I have
(SPM8, late 2009 vintage).
"""
import numpy as np
import sympy as sym
from sympy.abc import x
from IPython.display import display, Math, Latex
import IPython.display as disp
from __future__ import division
#from sympy.interactive import printing

"""after running this, all math output
will be formatted nicely for the duration
of the notebook session
"""
sym.init_printing() #use_latex='mathjax') #use_latex=True)
#sym.init_session(quiet=True)
#%load_ext sympyprinting
#%pylab inline

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

```python
# The code below is general (independent of SPMs code)
def numbered_matrix(nrows, ncols, symbol_prefix):
    return sym.Matrix(nrows, ncols, lambda i, j: sym.Symbol(
            symbol_prefix + '_{%d%d}' % (i+1, j+1)))

def numbered_vector(nrows, symbol_prefix):
    return sym.Matrix(nrows, 1, lambda i, j: sym.Symbol(
            symbol_prefix + '_{%d}' % (i+1)))

# Dump out the formulae here to latex for the RST docs
def my_latex(expr):
    S = sym.latex(expr)
    return S[1:-1]

def spm_full_matrix(x2, y2, to_inv, inv_lhs):
    rhs = to_inv[:,:]
    rhs[:,1] = x2
    lhs = inv_lhs[:,:]
    lhs[:,1] = y2
    return lhs * rhs.inv()

def disp_eqn(expr):
    disp.display_latex(expr)
```

```python
# premultiplication matrix to go from 0 based to 1 based indexing
one_based = sym.eye(4)
one_based[:3,3] = (1,1,1)

disp_eqn(one_based)
# premult for swapping row and column indices
row_col_swap = sym.eye(4)
row_col_swap[:,0] = sym.eye(4)[:,1]
row_col_swap[:,1] = sym.eye(4)[:,0]
disp_eqn(row_col_swap)
```

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

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

```python
# various worming matrices
orient_pat = numbered_matrix(3, 2, 'F')
disp_eqn(orient_pat)
```

$$\left[\begin{matrix}F_{{11}} & F_{{12}}\\F_{{21}} & F_{{22}}\\F_{{31}} & F_{{32}}\end{matrix}\right]$$

```python
orient_cross = numbered_vector(3, 'n')
disp_eqn(orient_cross)
```

$$\left[\begin{matrix}n_{{1}}\\n_{{2}}\\n_{{3}}\end{matrix}\right]$$

```python
missing_r_col = numbered_vector(3, 'k')
disp_eqn(missing_r_col)
```

$$\left[\begin{matrix}k_{{1}}\\k_{{2}}\\k_{{3}}\end{matrix}\right]$$

```python
pos_pat_0 = numbered_vector(3, 'T^1')
disp_eqn(pos_pat_0)
```

$$\left[\begin{matrix}T^{1}_{{1}}\\T^{1}_{{2}}\\T^{1}_{{3}}\end{matrix}\right]$$

```python
pos_pat_N = numbered_vector(3, 'T^N')

pixel_spacing = sym.symbols(('\Delta{r}', '\Delta{c}'))
NZ = sym.Symbol('N')
slice_thickness = sym.Symbol('\Delta{s}')
disp_eqn(pos_pat_N)
disp_eqn(pixel_spacing)
disp_eqn(NZ)
disp_eqn(slice_thickness)
```

$$\left[\begin{matrix}T^{N}_{{1}}\\T^{N}_{{2}}\\T^{N}_{{3}}\end{matrix}\right]$$

$$\left ( \Delta{r}, \quad \Delta{c}\right )$$

$$N$$

$$\Delta{s}$$

```python
R3 = orient_pat * np.diag(pixel_spacing)
#disp_eqn(R3)
R = sym.zeros(4,2)
R[:3,:] = R3
#disp_eqn(R3)
disp_eqn(R)
```

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c}\\0 & 0\end{matrix}\right]$$

```python
# The following is specific to the SPM algorithm.
x1 = sym.ones(4,1)
y1 = sym.ones(4,1)
y1[:3,:] = pos_pat_0
disp_eqn(x1)
disp_eqn(y1)
```

$$\left[\begin{matrix}1\\1\\1\\1\end{matrix}\right]$$

$$\left[\begin{matrix}T^{1}_{{1}}\\T^{1}_{{2}}\\T^{1}_{{3}}\\1\end{matrix}\right]$$

```python
to_inv = sym.zeros(4,4)
to_inv[:,0] = x1
to_inv[:,1] = sym.symbols('a, b, c, d')
to_inv[0,2] = 1
to_inv[1,3] = 1
disp_eqn(to_inv)
```

$$\left[\begin{matrix}1 & a & 1 & 0\\1 & b & 0 & 1\\1 & c & 0 & 0\\1 & d & 0 & 0\end{matrix}\right]$$

```python
inv_lhs = sym.zeros(4,4)
inv_lhs[:,0] = y1
inv_lhs[:,1] = sym.symbols('e, f, g, h')
inv_lhs[:,2:] = R
disp_eqn(inv_lhs)
```

$$\left[\begin{matrix}T^{1}_{{1}} & e & F_{{11}} \Delta{r} & F_{{12}} \Delta{c}\\T^{1}_{{2}} & f & F_{{21}} \Delta{r} & F_{{22}} \Delta{c}\\T^{1}_{{3}} & g & F_{{31}} \Delta{r} & F_{{32}} \Delta{c}\\1 & h & 0 & 0\end{matrix}\right]$$

```python
# single slice case
orient = sym.zeros(3,3)
orient[:3,:2] = orient_pat
orient[:,2] = orient_cross
disp_eqn(orient)
```

$$\left[\begin{matrix}F_{{11}} & F_{{12}} & n_{{1}}\\F_{{21}} & F_{{22}} & n_{{2}}\\F_{{31}} & F_{{32}} & n_{{3}}\end{matrix}\right]$$

```python
x2_ss = sym.Matrix((0,0,1,0))
y2_ss = sym.zeros(4,1)
y2_ss[:3,:] = orient * sym.Matrix((0,0,slice_thickness))
A_ss = spm_full_matrix(x2_ss, y2_ss, to_inv, inv_lhs)
disp_eqn(x2_ss)
disp_eqn(y2_ss)
disp_eqn(A_ss)
```

$$\left[\begin{matrix}0\\0\\1\\0\end{matrix}\right]$$

$$\left[\begin{matrix}\Delta{s} n_{{1}}\\\Delta{s} n_{{2}}\\\Delta{s} n_{{3}}\\0\end{matrix}\right]$$

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \Delta{s} n_{{1}} & - F_{{11}} \Delta{r} - F_{{12}} \Delta{c} + T^{1}_{{1}} - \Delta{s} n_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \Delta{s} n_{{2}} & - F_{{21}} \Delta{r} - F_{{22}} \Delta{c} + T^{1}_{{2}} - \Delta{s} n_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \Delta{s} n_{{3}} & - F_{{31}} \Delta{r} - F_{{32}} \Delta{c} + T^{1}_{{3}} - \Delta{s} n_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right]$$

```python
# many slice case
x2_ms = sym.Matrix((1,1,NZ,1))
y2_ms = sym.ones(4,1)
y2_ms[:3,:] = pos_pat_N
A_ms = spm_full_matrix(x2_ms, y2_ms, to_inv, inv_lhs)
disp_eqn(A_ms)
A_ms.simplify()
disp_eqn(A_ms)
# End of SPM algorithm
```

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & - \frac{T^{1}_{{1}}}{N - 1} + \frac{T^{N}_{{1}}}{N - 1} & - F_{{11}} \Delta{r} - F_{{12}} \Delta{c} + T^{1}_{{1}} \left(1 + \frac{1}{N - 1}\right) - \frac{T^{N}_{{1}}}{N - 1}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & - \frac{T^{1}_{{2}}}{N - 1} + \frac{T^{N}_{{2}}}{N - 1} & - F_{{21}} \Delta{r} - F_{{22}} \Delta{c} + T^{1}_{{2}} \left(1 + \frac{1}{N - 1}\right) - \frac{T^{N}_{{2}}}{N - 1}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & - \frac{T^{1}_{{3}}}{N - 1} + \frac{T^{N}_{{3}}}{N - 1} & - F_{{31}} \Delta{r} - F_{{32}} \Delta{c} + T^{1}_{{3}} \left(1 + \frac{1}{N - 1}\right) - \frac{T^{N}_{{3}}}{N - 1}\\0 & 0 & 0 & 1\end{matrix}\right]$$

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \frac{- T^{1}_{{1}} + T^{N}_{{1}}}{N - 1} & \frac{1}{N - 1} \left(N T^{1}_{{1}} - T^{N}_{{1}} - \left(N - 1\right) \left(F_{{11}} \Delta{r} + F_{{12}} \Delta{c}\right)\right)\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \frac{- T^{1}_{{2}} + T^{N}_{{2}}}{N - 1} & \frac{1}{N - 1} \left(N T^{1}_{{2}} - T^{N}_{{2}} - \left(N - 1\right) \left(F_{{21}} \Delta{r} + F_{{22}} \Delta{c}\right)\right)\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \frac{- T^{1}_{{3}} + T^{N}_{{3}}}{N - 1} & \frac{1}{N - 1} \left(N T^{1}_{{3}} - T^{N}_{{3}} - \left(N - 1\right) \left(F_{{31}} \Delta{r} + F_{{32}} \Delta{c}\right)\right)\\0 & 0 & 0 & 1\end{matrix}\right]$$

```python
# Rather simpler derivation from DICOM affine formulae - see
# dicom_orientation.rst

# single slice case
single_aff = sym.eye(4)
rot = orient
disp_eqn(rot)
disp_eqn(pixel_spacing)
disp_eqn(slice_thickness)
rot_scale = rot * sym.diag(pixel_spacing[0], pixel_spacing[1],
                             [slice_thickness])
disp_eqn(rot_scale)
single_aff[:3,:3] = rot_scale
single_aff[:3,3] = pos_pat_0
disp_eqn(single_aff)
```

$$\left[\begin{matrix}F_{{11}} & F_{{12}} & n_{{1}}\\F_{{21}} & F_{{22}} & n_{{2}}\\F_{{31}} & F_{{32}} & n_{{3}}\end{matrix}\right]$$

$$\left ( \Delta{r}, \quad \Delta{c}\right )$$

$$\Delta{s}$$

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \Delta{s} n_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \Delta{s} n_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \Delta{s} n_{{3}}\end{matrix}\right]$$

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \Delta{s} n_{{1}} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \Delta{s} n_{{2}} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \Delta{s} n_{{3}} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right]$$

```python
# For multi-slice case, we have the start and the end slice position
# patient.  This gives us the third column of the affine, because,
# ``pat_pos_N = aff * [[0,0,ZN-1,1]].T
multi_aff = sym.eye(4)
multi_aff[:3,:2] = R3
trans_z_N = sym.Matrix((0,0, NZ-1, 1))
multi_aff[:3, 2] = missing_r_col
multi_aff[:3, 3] = pos_pat_0
est_pos_pat_N = multi_aff * trans_z_N
eqns = tuple(est_pos_pat_N[:3,0] - pos_pat_N)
solved =  sym.solve(eqns, tuple(missing_r_col))
multi_aff_solved = multi_aff[:,:]
multi_aff_solved[:3,2] = multi_aff_solved[:3,2].subs(solved)
disp_eqn(multi_aff_solved)
```

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \frac{- T^{1}_{{1}} + T^{N}_{{1}}}{N - 1} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \frac{- T^{1}_{{2}} + T^{N}_{{2}}}{N - 1} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \frac{- T^{1}_{{3}} + T^{N}_{{3}}}{N - 1} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right]$$

```python
#print(sym.latex(multi_aff_solved))
```

    \left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \frac{- T^{1}_{{1}} + T^{N}_{{1}}}{N - 1} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \frac{- T^{1}_{{2}} + T^{N}_{{2}}}{N - 1} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \frac{- T^{1}_{{3}} + T^{N}_{{3}}}{N - 1} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right]

showing that the preivous equation is equivalent to the one shown earlier
$$
\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \frac{T^{1}_{{1}} - T^{N}_{{1}}}{1 - N} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \frac{T^{1}_{{2}} - T^{N}_{{2}}}{1 - N} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \frac{ T^{1}_{{3}} - T^{N}_{{3}}}{1 - N} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right]$$

```python
# Check that SPM gave us the same result
A_ms_0based = A_ms * one_based
A_ms_0based.simplify()
A_ss_0based = A_ss * one_based
A_ss_0based.simplify()
disp_eqn(A_ms_0based)
disp_eqn(A_ss_0based)
assert single_aff == A_ss_0based
assert multi_aff_solved == A_ms_0based
```

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \frac{- T^{1}_{{1}} + T^{N}_{{1}}}{N - 1} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \frac{- T^{1}_{{2}} + T^{N}_{{2}}}{N - 1} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \frac{- T^{1}_{{3}} + T^{N}_{{3}}}{N - 1} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right]$$

$$\left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \Delta{s} n_{{1}} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \Delta{s} n_{{2}} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \Delta{s} n_{{3}} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right]$$

```python
# Now, trying to work out Z from slice affines
A_i = single_aff
nz_trans = sym.eye(4)
NZT = sym.Symbol('d')
nz_trans[2,3] = NZT
A_j = A_i * nz_trans
IPP_i = A_i[:3,3]
IPP_j = A_j[:3,3]

disp_eqn(IPP_i)
disp_eqn(IPP_j)
```

$$\left[\begin{matrix}T^{1}_{{1}}\\T^{1}_{{2}}\\T^{1}_{{3}}\end{matrix}\right]$$

$$\left[\begin{matrix}T^{1}_{{1}} + \Delta{s} d n_{{1}}\\T^{1}_{{2}} + \Delta{s} d n_{{2}}\\T^{1}_{{3}} + \Delta{s} d n_{{3}}\end{matrix}\right]$$

```python
# SPM does it with the inner product of the vectors
spm_z = IPP_j.T * orient_cross
spm_z.simplify()
disp_eqn(spm_z)
```

$$\left[\begin{matrix}n_{{1}} \left(T^{1}_{{1}} + \Delta{s} d n_{{1}}\right) + n_{{2}} \left(T^{1}_{{2}} + \Delta{s} d n_{{2}}\right) + n_{{3}} \left(T^{1}_{{3}} + \Delta{s} d n_{{3}}\right)\end{matrix}\right]$$

```python
# We can also do it with a sum and division, but then we'd get undefined
# behavior when orient_cross sums to zero.
ipp_sum_div = sum(IPP_j) / sum(orient_cross)
ipp_sum_div = sym.simplify(ipp_sum_div)
disp_eqn(ipp_sum_div)
```

$$\frac{1}{n_{{1}} + n_{{2}} + n_{{3}}} \left(T^{1}_{{1}} + T^{1}_{{2}} + T^{1}_{{3}} + \Delta{s} d n_{{1}} + \Delta{s} d n_{{2}} + \Delta{s} d n_{{3}}\right)$$

```python
str_latex = ('Latex stuff\n' +
             '    R = ' + my_latex(to_inv) + '\n\n' +
             '   L = ' + my_latex(inv_lhs) + '\n\n' +
             '   0B = ' + my_latex(one_based) + '\n\n' +
             '   ' + my_latex(solved)  + '\n\n' +
             '   A_{multi} = ' + my_latex(multi_aff_solved)  + '\n\n' +
             '   A_{single} = ' + my_latex(single_aff)  + '\n\n' +
             r'   \left(\begin{smallmatrix}T^N\\1\end{smallmatrix}\right) = A ' +
             my_latex(trans_z_N)   + '\n\n' +
             '   A_j = A_{single} ' + my_latex(nz_trans)  + '\n\n' +
             '   T^j = ' + my_latex(IPP_j)  + '\n\n' +
             '  T^j \cdot \mathbf{c} = ' + my_latex(spm_z)  + '\n')
```

```python
print(str_latex)
```

    Latex stuff
        R = left[\begin{matrix}1 & a & 1 & 0\\1 & b & 0 & 1\\1 & c & 0 & 0\\1 & d & 0 & 0\end{matrix}\right

       L = left[\begin{matrix}T^{1}_{{1}} & e & F_{{11}} \Delta{r} & F_{{12}} \Delta{c}\\T^{1}_{{2}} & f & F_{{21}} \Delta{r} & F_{{22}} \Delta{c}\\T^{1}_{{3}} & g & F_{{31}} \Delta{r} & F_{{32}} \Delta{c}\\1 & h & 0 & 0\end{matrix}\right

       0B = left[\begin{matrix}1 & 0 & 0 & 1\\0 & 1 & 0 & 1\\0 & 0 & 1 & 1\\0 & 0 & 0 & 1\end{matrix}\right

       left \{ k_{{1}} : \frac{- T^{1}_{{1}} + T^{N}_{{1}}}{N - 1}, \quad k_{{2}} : \frac{- T^{1}_{{2}} + T^{N}_{{2}}}{N - 1}, \quad k_{{3}} : \frac{- T^{1}_{{3}} + T^{N}_{{3}}}{N - 1}\right \

       A_{multi} = left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \frac{- T^{1}_{{1}} + T^{N}_{{1}}}{N - 1} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \frac{- T^{1}_{{2}} + T^{N}_{{2}}}{N - 1} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \frac{- T^{1}_{{3}} + T^{N}_{{3}}}{N - 1} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right

       A_{single} = left[\begin{matrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \Delta{s} n_{{1}} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \Delta{s} n_{{2}} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \Delta{s} n_{{3}} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{matrix}\right

       \left(\begin{smallmatrix}T^N\\1\end{smallmatrix}\right) = A left[\begin{matrix}0\\0\\N - 1\\1\end{matrix}\right

       A_j = A_{single} left[\begin{matrix}1 & 0 & 0 & 0\\0 & 1 & 0 & 0\\0 & 0 & 1 & d\\0 & 0 & 0 & 1\end{matrix}\right

       T^j = left[\begin{matrix}T^{1}_{{1}} + \Delta{s} d n_{{1}}\\T^{1}_{{2}} + \Delta{s} d n_{{2}}\\T^{1}_{{3}} + \Delta{s} d n_{{3}}\end{matrix}\right

      T^j \cdot \mathbf{c} = left[\begin{matrix}n_{{1}} \left(T^{1}_{{1}} + \Delta{s} d n_{{1}}\right) + n_{{2}} \left(T^{1}_{{2}} + \Delta{s} d n_{{2}}\right) + n_{{3}} \left(T^{1}_{{3}} + \Delta{s} d n_{{3}}\right)\end{matrix}\right

```python

```


#  École des Ponts ParisTech
##  SPH pour l'hydraulique
###  Projet SPH GMM 2021
####  Parois mobiles
 Auteurs : Ruben Persicot & Yohan Lanier

 Encadrants : Rémi Carmigniani et Damien Violeau

## 1. Ecoulement de couette plan.

### 1.1 Etude analytique

$\textbf{Schéma du problème étudié :}$

$\textbf{Hypothèses :}$
 - (H1) La paroi supérieure est mobile, la paroi inférieure est immobile.
 - (H2) L'écoulement est incompressible.
 - (H3) L'écoulement est permanent.
 - (H4) Le gradient de pression est nul selon x. Autrement dit, la paroi supérieure est le seul moteur de l'écoulement

On cherche $\underline u$ de la forme $\underline u(x,y)= u(x,y)\underline{e_x}$.

$\textbf{Conditions aux limites :}$
  - en $y=+\frac{e}{2}, \underline{u}= U\underline{e_x}$  d'après (H1)
  - en $y=-\frac{e}{2}, \underline{u}= 0$                d'après (H1)

$\textbf{Résolution : }$
D'après (H2) on montre que $\underline u= u(y)\underline{e_x}$

On résout l'équation de Navier-Stockes pour l'écoulement considéré :

$\begin{array}{rcl}
\rho(\frac{\partial \underline{u}}{\partial t}+ \underline{u}\cdot\underline{\underline{grad}}(\underline{u}))&=&-\underline{grad}(p)+\rho \underline{g} + \mu \underline{\Delta} \underline{u} \\
0&=&-\underline{grad}(p)+\rho \underline{g} + \mu \underline{\Delta} \underline{u} \ \  \textrm{d'après (H2) et (H3)}
\end{array}$

On projette cette équation sur les axes $x$ et $y$ :
$ \left\{
    \begin{array}{ll}
        \mu\frac{\partial^2 u}{\partial x^2} = \frac{\partial p}{\partial x} = 0 \ \ \textrm{d'après (H4)}\\
        -\rho g = \frac{\partial p}{\partial y}\\
    \end{array}
\right.
$

Ainsi on montre que le profil de vitesse est linéaire selon $y$ :
$\begin{equation}
        u(y)= U(\frac{y}{e}+\frac{1}{2})
    \end{equation}$

Par ailleurs, le profil de pression est aussi linéaire selon $y$ :
$\begin{equation}
        p(y)= p_0 - \rho g y
    \end{equation}$

### 1.2 Etude numérique

#### Import des fichiers et des modules utiles pour programmer un code SPH.

```python
%load_ext autoreload
%autoreload 2
import numpy as np
from sys import exit
import os.path
from os import path
import csv
import time
import matplotlib
import matplotlib.pyplot as plt
#matplotlib.rcParams['text.usetex'] = True
from src.spyh import *
from src.sphvar import *
from src.plotParticles import *
from src.state import *
from src.contrib import *
from src.analytical_solutions import *
import numba
```

    The autoreload extension is already loaded. To reload it, use:
      %reload_ext autoreload

#### Définition des grandeurs du problèmes et du fluide

```python
#COUETTE PARAMETERS
e = 0.5 #Half width (m)
U = 1 # Upper bound velocity (m/s)
#FLUID PARAMETERS
Re = 1 # Reynolds number
rhoF = 1000
mu = rhoF*U*e/Re #Dynamic Vicosity (Pa.s)
grav = np.array([0.0,0.0])  #Gravity (m.s-2)
#grav= np.array([0.0,-9.81])
c0 = 10*U  #(m/s)
gamma = 7
B = rhoF*c0**2/gamma
#DENSITY & SHEPARD THRESHOLDS :
shepardMin = 10**(-6)
rhoMin = 0.5*rhoF
rhoMax = 1.5*rhoF
```

#### Définition des propriétés des particules

```python
#PARTICLE & SPACES PARAMETERS :
N = 4
M = 8
dr = 2*e/(M*N) #Particle radius
h = smthfc*dr
m=dr*dr*rhoF
lspace = 2*h
#COMPUTATION DOMAIN :
xOrigin = 0
yOrigin = -e-nBound*dr
xSize = 10*lspace
ySize = 2*e+2*nBound*dr
xMax = xOrigin+xSize
yMax = yOrigin+ySize
```

#### Création du domaine de calcul

Pour notre projet nous avons crée un nouveau type de particules dont le FLAG est MOBILEBOUND. Celles-ci permettent de modéliser le mouvement d'une paroi mobile dont on impose le mouvement.

Ce nouveau type de particule est défini dans [src/sphvar.py](src/sphvar.py)

Les murs seront composés d'une couche de *nBound =4* particules *fictives*. Ce nombre est suffisant pour éviter la pénétration du mur.

```python
#INIT PART:
part = init_particles()
part = addBox(part,[0,-e,xSize,2*e],FLUID,dr,rhoF)                  #Fluid domain
part = addBox(part,[0,-e-nBound*dr,xSize,nBound*dr],BOUND,dr,rhoF)  #Bound domain
part = addBox(part,[0,e,xSize,nBound*dr],MOBILEBOUND,dr,rhoF)       #Moving bound domain
#PERIODICITY VECTOR
vecPer = np.array([xSize,0])
posSpace,neibSpace,partSpace,listNeibSpace = init_spaces(xOrigin,yOrigin,xSize,ySize,lspace,dr,vecPer)
part,partSpace = sortPart(part,posSpace,partSpace,xOrigin,yOrigin,xSize,ySize,lspace,dr)
listNeibSpace= getListNeib(partSpace,neibSpace,listNeibSpace)
```

#### Affichage du domaine

```python
%matplotlib notebook
Umax= 1
tabUx = part[:,VEL[0]]
domain = [xOrigin,xMax,yOrigin,yMax,0,Umax]
plotPropertiesWithBound(part,tabUx,r'$U_x$',domain,dr,1)
```

    <IPython.core.display.Javascript object>

#### Affichage des espaces

```python
plotSpaces(posSpace,'k',lspace,1)
neibSpace[2][neibSpace[2]>-1]
spacesOutline(posSpace[neibSpace[2][neibSpace[2]>-1]],'r',lspace,1)
```

#### Initialisation de la vitesse de la paroi supérieure

```python
part[:,VEL] = initMobileBoundVelocity((part[:,INFO]==MOBILEBOUND), part[:,VEL], U)
```

#### Interpolation de la vitesse et de la masse volumique au bord

```python
part[:,RHO],part[:,VEL] =interpolateBoundaryPeriodicX((part[:,INFO]==BOUND),\
                                     part[:,SPID],\
                                     part[:,POS],\
                                     part[:,VEL],\
                                     part[:,RHO],\
                                     listNeibSpace,aW,h,m,B,rhoF,gamma,grav,vecPer[0],shepardMin)
```

#### Gestion des enregistrements

```python
current_directory = os.getcwd()
case_directory = os.path.join(current_directory, r'Results/Couette_'+time.strftime("%Y%m%d_%H%M%S"))
os.mkdir(case_directory)
data_directory = os.path.join(case_directory,r'Data')
figures_directory = os.path.join(case_directory,r'Figures')
os.mkdir(data_directory)
os.mkdir(figures_directory)
```

#### Gestion du pas de temps et des fréquences de sorties des figures

```python
# Here we specify the output frequencies
dt_figure = 0.025*e**2*rhoF/mu
t_print = 0
t_end = 3
# Time management
t=0
it=0
im_count=0
```

#### Programmation de la solution analytique

```python
#Analytical solution for the velocity with renormalization
ytab = np.linspace(-1,1,100)
Utab = (U/2)*(ytab/(2*e) + 1)
```

### 1.3 Simulation SPH

```python
%matplotlib notebook
while t<t_end:
    #STEP1 : CFL Computation
    dt = CFLConditions(part[:,VEL],h,c0,grav,rhoF,mu)
    #STEP2 : Interpolation of the density and the velocy near bounds
    part[:,RHO],part[:,VEL] =interpolateBoundaryPeriodicX((part[:,INFO]==BOUND),\
                                     part[:,SPID],\
                                     part[:,POS],\
                                     part[:,VEL],\
                                     part[:,RHO],\
                                     listNeibSpace,\
                                 aW,h,m,B,rhoF,gamma,grav,vecPer[0],shepardMin)
    #STEP3 : Interpolation of the density and the velocy near mobile bounds
    part[:,RHO], part[:, VEL] = interpolateMobileBoundaryPeriodicX((part[:,INFO]==MOBILEBOUND),\
                                     part[:,SPID],\
                                     part[:,POS],\
                                     part[:,VEL],\
                                     np.array([U, 0.0]),\
                                     part[:,RHO],\
                                     listNeibSpace,\
                                 aW,h,m,B,rhoF,gamma,grav,vecPer[0],shepardMin)

    #STEP4 : Computation of density and forces
    part[:,FORCES],part[:,DRHODT] = computeForcesMorrisPeriodicX((part[:,INFO]==FLUID),\
                                                 part[:,SPID],\
                                                 part[:,POS],\
                                                 part[:,VEL],\
                                                 part[:,RHO],\
                                                 listNeibSpace,\
                                                 aW,h,m,B,rhoF,gamma,grav,mu,vecPer[0])
    #STEP5 : Time integration for fluid particle
    part[:,POS],part[:,VEL],part[:,RHO] = integrationStepPeriodicX((part[:,INFO]==FLUID),\
                                                       part[:,POS],\
                                                       part[:,VEL],\
                                                       part[:,RHO],\
                                                       part[:,FORCES],\
                                                       part[:,DRHODT],\
                                                       dt,vecPer[0])
    #STEP6 : Time integration for mobile bound particle
    part[:,POS], part[:,RHO] = integrationStepPeriodicX_Moving_Bound((part[:,INFO]==MOBILEBOUND),\
                                                       part[:,POS],\
                                                       part[:,VEL],\
                                                       part[:,RHO],\
                                                       part[:,FORCES],\
                                                       part[:,DRHODT],\
                                                       dt,vecPer[0])

    #STEP7 : correct particle density if too low
    part[:,RHO] = checkDensity(part[:,RHO],rhoMin,rhoMax)

    #STEP8 : Neighbours update
    part,partSpace = sortPart(part,posSpace,partSpace,xOrigin,yOrigin,xSize,ySize,lspace,dr)
    listNeibSpace= getListNeib(partSpace,neibSpace,listNeibSpace)
    t +=dt
    it +=1
    if t>=t_print:
        fig = plt.figure(1)
        plt.clf()
        plt.title(r'$t\nu/e^2 = %2.2f$'%(t*mu/rhoF/e**2))
        velMagn = (part[:,VEL[0]]*part[:,VEL[0]]+part[:,VEL[1]]*part[:,VEL[1]])**.5
        domain = [xOrigin,xMax,yOrigin,yMax,0,1]
        plotPropertiesWithBound(part,velMagn,r'$u/U_0$',domain,dr,1)
        figname = os.path.join(figures_directory,r'vel_%06d.png'%im_count)
        fig.savefig(figname,bbox_inches='tight')
        fig.canvas.draw()
        plt.pause(0.01)

        #FIGURE DISPLAY
        fig2 = plt.figure(2)
        plt.clf()
        plt.plot(part[:,POS[1]]/e,part[:,VEL[0]]/U,'bo',label=r'$U_x$ SPH')
        plt.plot(part[:,POS[1]]/e,part[:,VEL[1]]/U,'ro',label=r'$U_y$ SPH')
        plt.plot(ytab,Utab,'--k',label=r'Asymptotic')
        #uan = analyticalCouetteFlow(ytab,U,e,timetab*rhoF/mu*e**2/tau)
        #plt.plot(ytab,uan,'--r',label='Analytical')
        plt.xlabel('$z/e$',fontsize=18)
        plt.ylabel('$u/U_0$',fontsize=18)
        plt.xlim(-1-nBound*dr/e,1+nBound*dr/e)
        plt.ylim(-0.2,1.2)
        plt.legend(loc='upper left')
        plt.tight_layout()
        ax = plt.gca()
        ax.tick_params(axis = 'both', which = 'major', labelsize = 18)
        ax.xaxis.set_major_locator(plt.MaxNLocator(5))
        ax.yaxis.set_major_locator(plt.MaxNLocator(5))
        plt.tight_layout()
        plt.show(block=False)
        plt.draw()
        figname = os.path.join(figures_directory,r'UV_%06d.png'%im_count)
        fig2.savefig(figname,bbox_inches='tight')
        fig2.canvas.draw()
        plt.pause(0.01)
        im_count = im_count+1
        t_print +=dt_figure

```

### 1.4 Analyse des résultats


# Coaddition and Subtraction

Version 0.1

By Yusra AlSayyad (Princeton University)

This notebook provides some problems for ground-based coaddition and subtraction.

We add and subtract images for different purposes: addition to get the benefits of longer exposures and subtraction to reveal what has changed.  However, in order to perform either operation, the following  image characteristics need to be normalized:
* Astrometric calibration (i.e. WCS)
* Photometric calibration (i.e. Zeropoint)
* Background level
* Point Spread Function (PSF) (optional for coaddition)

For these problems, we'll assume that we have images that have already been normalized for WCS, zeropoint, and background variations.

### Problem 1) Expected depth of a coadd

In order of magnitude calculations, you may hear people throw around the statement that the depth of a coadd increases by a factor of $\sqrt{N}$  for N single-epoch images. Where does this come from? Under what conditions is this a good approximation?

We are going to use our same star + noise 1-D simulation from  `IntroductionToBasicStellarPhotometry.ipynb` and `FindingSources.ipynb`:

```python
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MultipleLocator
from scipy.stats import norm
```

```python
def pixel_plot(pix, counts, fig=None, ax=None):
    '''Make a pixelated 1D plot'''
    if fig is None and ax is None:
        fig, ax = plt.subplots()

    ax.step(pix, counts,
            where='post')

    ax.set_xlabel('pixel number')
    ax.set_ylabel('relative counts')
    ax.xaxis.set_minor_locator(MultipleLocator(1))
    ax.xaxis.set_major_locator(MultipleLocator(5))
    fig.tight_layout()
    return fig, ax

# It is sufficient to copy and paste from
# your introductionToBasicStellarPhotometry noteboook

def phi(x, mu, fwhm):
    """Evalute the 1d PSF N(mu, sigma^2) along x

    Parameters
    ----------
    x : array-like of shape (n_pixels,)
        detector pixel number
    mu : float
        mean position of the 1D star
    fwhm : float
        Full-width half-maximum of the stellar profile on the detector

    Returns
    -------
    flux : array-like of shape (n_pixels,)
        Flux in each pixel of the input array
    """
    sigma = fwhm/2.3548
    flux = 1/np.sqrt(2*np.pi*sigma**2)*np.exp(-(x - mu)**2/(2*sigma**2))
    return flux

# Define your image simulation function to
# It is sufficient to copy and paste from
# your introductionToBasicStellarPhotometry noteboook
# Note that the background S should now be supplied as
# an array of length (x) or a constant.

def simulate(x, mu, fwhm, S, F):
    """simulate a noisy stellar signal

    Parameters
    ----------
    x : array-like
        detector pixel number
    mu : float
        mean position of the 1D star
    fwhm : float
        Full-width half-maximum of the stellar profile on the detector
    S : float or array-like of len(x)
        Sky background for each pixel
    F : float
        Total stellar flux

    Returns
    -------
    noisy_counts : array-like (same shape as x)
        the (noisy) number of counts in each pixel
    """
    source = F * phi(x+np.mean(np.diff(x))/2, mu, fwhm)
    sky_plus_source = S * np.ones_like(x) + source
    noisy_counts = np.random.normal(sky_plus_source, np.sqrt(sky_plus_source))
    return noisy_counts
```

### Problem 1.1) Make a simple mean coadd

Simulate N observations of a star, and coadd them by taking the mean of the N observations. (We can only do this because they are already astrometrically and photometrically aligned and have the same background value.)

```python
MU = 35
S = 100
F = 100
FWHM = 5

x = np.arange(100)

# simulate a single observation of the star and plot:
y = simulate(x, MU, FWHM, S, F)
pixel_plot(x, y)
```

```python
# Write a simulateN function that returns an array of size (N, x)
# representing N realizations of your simulated star
# This will stand in as a stack of multiple observations of one star

def simulateN(x, mu, fwhm, S, F, N):
    """simulate a noisy stellar signal

    Parameters
    ----------
    x : array-like
        detector pixel number
    mu : float
        mean position of the 1D star
    fwhm : float
        Full-width half-maximum of the stellar profile on the detector
    S : float or array-like of len(x)
        Sky background for each pixel
    F : float
        Total stellar flux
    N: int
        Number of images to simulate

    Returns
    -------
    noisy_counts : array-like of shape (N, x)
        the (noisy) number of counts in each pixel
    """
    noisy_counts = np.empty(shape=(N, x.shape[0]))
    for i in range(N):
        noisy_counts[i] = simulate(x, mu, fwhm, S, F)
    return noisy_counts
```

```python
# simulate N=50 images with the same star
x = np.arange(100)
stack = simulateN(x, MU, FWHM, S, F, 50)
# where stack is an array of size (50, 100) representing a pile of 50 images with 100 pixels

# coadd by taking the mean and plot the result
coadd = np.mean(stack, axis=0)
pixel_plot(x, coadd)

# Try a few different N to see how it affects the S/N of your result

```

```python
fig, ax = plt.subplots()
# Plot the coadds of N=[1, 10, and 100] on the same plot:
for n in [1, 10, 100]:
    stack = simulateN(x, MU, FWHM, S, F, n)
    pixel_plot(x, np.mean(stack, axis=0), fig=fig, ax=ax)

```

### Problem 1.2)  SNR vs N

Now compute the observed SNR of the simulated star on each coadd and compare to the expected SNR in the idealized case. The often repeated mnemonic for SNR inscrease as a function of number of images, $N$, is that noise decreses like $\sqrt{N}$. This is of course idealized case where the noise in each observation is identical.

Using your `simulateN` function, simulate a series of mean coadds with increasing N.

* First, plot the empirical noise/uncertainty/stdev as a function of N. and overplot the expected uncertainty given the input sky level. You can use an area you know isn't touched by the star.
* Next, plot the empirical SNR of the star (measured flux/fluxErr) as a function of N. Overplot the expected SNR. You can assume you know the sky level.

Your expected scaling with N should roughly track your empirical estimate.

```python
# complete

# hint. One way to start this
# std = []
# flux = []
# Ns = np.arange(1, 1000, 5)
# for N in Ns:
    # y = simulateN(...)
    # complete

# plt.plot(Ns, ...,  label="coadd")
# plt.plot(Ns, ..., label="expected")
# plt.xlabel('N')
# plt.ylabel('pixel noise')
# plt.legend()
```

```python
# complete

# plt.plot(Ns, ...,  label="coadd")
# plt.plot(Ns, ..., label="expected")
# plt.xlabel('N')
# plt.ylabel('PSF Flux SNR')
# plt.legend()
```

```python

```

## Problem 2) PSFs and Image weights in coadds

Problem (1) pretends that the input images are identical in quality, however this is never the case in practice. In practice, adding another image does not necessarily increase the SNR. For example, imagine you have two exposures, but in one the dome light was accidentally left on. A coadd with these two images weighted equally will have a worse SNR than the first image alone. Therefore the images should be aggregated with a weighted mean, so that images of poor quality don't degrade the quality of the coadd. What weights to we pick?

Weights can be chosen to either minimize the variance on the coadd or maximize the SNR of point sources on the coadd.

#### Some background:

Assuming that all noise sources are independent, the SNR of the measurement of flux from a star is:
\begin{equation}
SNR \propto {{N_{\rm photons}}\over{ \sigma_{\rm sky}} \sqrt{A} },
\end{equation}
where $N_{\rm photons}$ is the number of photons detected from the star,
$A$ is the area in pixels covered by the star.
The per-pixel sky noise  $\sigma_{sky}$ includes all sources of noise: dark current, read noise and sky-background, and it coded in the variance plane of the image. For the epoch $i$,  $\sigma^2_{i, {\rm sky}}$ is the average of the variance plane.

The $N_{\rm photons}$ is proportional to transparency $T$, and the area that the stellar photons cover is determined by the seeing: $A \propto {\rm FWHM}^2$.

Therefore, a **coadd optimized for point-source detection** would, to weight each image by the SNR$^2$, use the following as weights which prefers good-seeing epochs taken when the sky is transparent and dark.
\begin{align}
w_i & = {\rm SNR}^2 \propto {{T_i^2}\over{{\rm FWHM_i}^2 \sigma_i^2}}.
\end{align}

The usual inverse-variance weighting which produces the **minimum-variance co-add**, is given by,
\begin{align}
w_i & =T_i^2/\sigma_i^2
\end{align}

In practice, the factor of $T_i^2$ is incorporated into the variance when flux-scaling the single-epoch images to a common zeropoint. This step multiplies the image by a scale-factor, which increases the variance of the image by the square of the scale factor. The scale factor is inversely proportional to the transparency, so that
$\sigma_{scaled}= \sigma/T$.

For this problem assume the images are all on the same zeropoint (like problem 1) i.e. T=1.

### Problem 2.1  Weighting images in Variable Sky

Now simulate 50 observations of stars with Sky `S` ranging from 100 to 1000. Remember to subtract this background off before stacking this time! Plot the plain (unweighted) mean coadd vs. the minimum variance coadd. Weights should add up to 1. What's the empirical noise estimate of the coadd?

```python
# complete
```

### Problem 2.2  Weighting images in Variable Seeing

Simulate 50 observations with FWHM's ranging from 2-10 pixels. Keep the flux amplitude `F`, and sky noise `S` both fixed.
Generate two coadds, (1) with the weights that minimize variance and (2) with the weights that maximize point source SNR. Weights should add up to 1. Plot both coadds.

```python
# complete
```

### Problem 2.3 Image variance vs per pixel variance (Challenge Problem)

Why do we use per image variances instead of per pixel variances? Let's see! Start tracking the per-pixel variance when you simulate the star. Make a coadd of 200 observations with FWHM's ranging from 2 to 20 pixels.  Make a coadd weighted by the per-pixel inverse variance. How does the profile of the star look in this coadd compared to an unweighted coadd and compared to the coadd with the $w_i  = \frac{1}{{\rm FWHM_i}^2 \sigma_i^2}$? (You may have to plot the difference to see the change).

```python
# complete
```

## Problem 3) Dipoles in Image Subtraction

In the lesson, we said that just because you see a dipole in a difference image, does not mean that the astrometric registration is terrible. For this problem, we'll forgo the pixelated simulated star from the previous problems and operate with Gaussian profiles.

```python
# Create two Gaussian 1-D profiles with

ASTROM_OFFSET = 0.1  # units of e.g. pixels
FLUX_SCALE = 1. # units of e.g nanojansky
PSF = 1  # rms pixels

x = np.linspace(-5, 5)
y1 = FLUX_SCALE * norm.pdf(x, ASTROM_OFFSET, PSF)
y2 = FLUX_SCALE * norm.pdf(x, -ASTROM_OFFSET, PSF)
plt.plot(x, y1, label='profile 1')
plt.plot(x, y2, label='profile 2')
plt.xlabel('x (pixel)')
plt.ylabel('y (flux)')
plt.legend()

```

### Problem 3.1) Plot the difference of these two profiles:

```python
plt.plot(x, y2 - y1)
plt.xlabel('x (pixel)')
plt.ylabel('y (flux)')
```

### Problem 3.2)  What if we have amazing astrometric registration
and shrink the astrometric offset by a factor of a thousand. Is there a star sufficiently bright to produce the same dipole? What is its `FLUX SCALE`?

```python
ASTROM_OFFSET = 0.0001
PSF = 1.
FLUX_SCALE2 = 500

# complete
y1_amazing = FLUX_SCALE2 * norm.pdf(x, ASTROM_OFFSET, PSF)
y2_amazing = FLUX_SCALE2 * norm.pdf(x, -ASTROM_OFFSET, PSF)

# Plot both dipoles (for the offset=0.1 and the offset=0.0001 in the same figure.
plt.plot(x, y2 - y1, label='orig')
plt.plot(x, y2_amazing - y1_amazing, label='amaz')
plt.xlabel('x (pixel)')
plt.ylabel('y (flux)')
plt.legend()
# Same or different subplots up to you.
```

### Problem 3.3) Distance between peaks.

Does the distance between the dipole's positive and negative peaks depend on the astrometric offset? If not, what *does* it depend on?  You can answer this by vizualizing the dipoles vs offsets. But for a **challenge, measure the distance between peaks** and plot them as a function of astrometric offset or another factor.

```python
def dist(x, delta_y):
    return x[np.argmin(y)] - x[np.argmax(y)]
```

```python
dist(x, y2 - y1)
```

    1.6326530612244898

```python
fluxes = np.arange(1., 1001, 50)
dists = np.zeros_like(fluxes)

for i, f in enumerate(fluxes):
    yy1 = f * norm.pdf(x, ASTROM_OFFSET, PSF)
    yy2 = f * norm.pdf(x, -ASTROM_OFFSET, PSF)
    dists[i] = dist(x, yy2 - yy1)
```

```python
plt.plot(fluxes, dists)
plt.xlabel('Flux')
plt.ylabel('Distance')
```

```python
offsets = 10.0 ** np.array([-4, -3, -2, -1, 0, 1])
```

```python
dists2 = np.zeros_like(offsets)

for i, ao in enumerate(offsets):
    yy1 = FLUX_SCALE * norm.pdf(x, ao, PSF)
    yy2 = FLUX_SCALE * norm.pdf(x, -ao, PSF)
    dists2[i] = dist(x, yy2 - yy1)
```

```python
plt.plot(offsets, dists2)
plt.xlabel('Flux')
plt.ylabel('Distance')
```

### Problem  3.4)
In the problem setup we assumed that the astrometric error was < the PSF width. Is this assumption likely to hold in an image you'd get from a wide-field imager like Rubin's?


# Computation of the Deformation
# of Ferromagnetic Material Taking Into Account
# Magnetic Forces and Magnetostriction

To compute the deformation of ferromagnetic material, differents force distributions can be taken into account such as total magnetic force, anisotropy and/or rotational excitation induced force, or magnetoelastic / magnetostrictive force.

To compute this latter, it is proposed to use a formulation of this force tensor on isotropic material based on the following scientific article: [L. Vandevelde, J. Gyselinck, M. A. C. De Wulf and J. A. A. Melkebeek, "Finite-element computation of the deformation of ferromagnetic material taking into account magnetic forces and magnetostriction," in IEEE Transactions on Magnetics, vol. 40, no. 2, pp. 565-568, March 2004, doi: 10.1109/TMAG.2004.824540.](https://ieeexplore.ieee.org/abstract/document/1284477)

This tutorial explains how to use the ForceTensor class in Pyleecan to get the deformation of the material, and presents a validation case.

The notebook related to this tutorial is available on [GitHub](https://github.com/Eomys/pyleecan/tree/master/Tutorials/tuto_MagnetostrictiveForces.ipynb).

# Computation on Pyleecan

## Import Pyleecan modules and machine definition

```python
%matplotlib notebook

from os.path import join
import pathlib
from numpy import zeros, exp, pi, real, meshgrid, mean
from numpy.testing import assert_array_almost_equal
from SciDataTool import DataTime, VectorField, Data1D
import csv
import numpy as np
import matplotlib.pyplot as plt

from pyleecan.Classes.ForceTensor import ForceTensor
from pyleecan.Classes.Simu1 import Simu1
from pyleecan.Classes.MagFEMM import MagFEMM
from pyleecan.Classes.InputCurrent import InputCurrent
from pyleecan.Classes.OPdq import OPdq
from pyleecan.Classes.SolutionVector import SolutionVector

from pyleecan.Functions.load import load
from pyleecan.definitions import DATA_DIR

```

## Run the simulation
The electromagnetic field is solved in a 2D magnetostatic simulation using FEMM software. The material used is a Benchmark stator on which references deformation values for magnetostrictive forces are available. The "tensor" dictionnary line 28. will be useful to declare other types of force density one might want to compute.

```python
# Load machine
Benchmark = load(join(DATA_DIR, "Machine", "Benchmark.json"))
# Prepare simulation
simu = Simu1(name="Benchmark_Tensor", machine=Benchmark)

simu.input = InputCurrent(Ir=None, Na_tot=2 ** 6, Nt_tot=1)
simu.input.OP = OPdq(Id_ref=0, Iq_ref=0, N0=1200)

simu.elec = None

simu.mag = MagFEMM(
    type_BH_stator=1,  # 0 for saturated
    type_BH_rotor=1,
    is_periodicity_a=False,
    is_periodicity_t=False,
    is_get_meshsolution=True,
    is_sliding_band=False,
    # nb_worker=cpu_count(),
    Kmesh_fineness=1,
)

# Our ForceTensor class
simu.force = ForceTensor(
    is_periodicity_a=False,
    is_periodicity_t=False,
    tensor={
        "magnetostriction": True,
    },
)

# Run simulation
out = simu.run()

```

    [16:58:00] Starting running simulation Benchmark_Tensor (machine=Benchmark)
    [16:58:00] Starting Magnetic module
    [16:58:08] Computing Airgap Flux in FEMM
    [16:58:30] Starting Force module
    [16:58:45] End of simulation Benchmark_Tensor

## Display the deformation
It should open on a new window pop-up

```python
out.force.meshsolution.plot_glyph(
    label="F",
    is_point_arrow=True,
    # save_path="add a save path if you want",
)
```

    WARNING:root:VTK requires 3D points, but 2D points given. Appending 0 third component.

    <pyvistaqt.plotting.BackgroundPlotter at 0x13f83805a60>

# Validation case and code details

## Import the reference results
These are available thanks to Pr. Lieven Vandevelde from the Gand University, Belgium.

```python
#CSV import
current_workdir = str(pathlib.Path().absolute())
ref_path = current_workdir.replace("Tutorials","Tests\Data\Benchmark_model_stator_ms.csv")
with open(ref_path, "r") as file:
    reader = csv.reader(file, skipinitialspace=True)
    l1 = next(reader)
    l2 = next(reader)
    l3 = next(reader)
    nb_node = int(l2[1])
    dim = int(l2[2])
    Nt_tot = int(l2[3])
    f2 = np.zeros((nb_node, dim))
    node_number_list = []
    indices_nodes = []
    for row in reader:
        f2[int(row[0])][0] = 1000*float(row[3])
        f2[int(row[0])][1] = 1000*float(row[4])
        node_number_list.append(int(row[10]))
        indices_nodes.append(int(row[11]))

# Little trick to reshape f2 so that it can be compared to f, since Indices_Points2 isn't taken into account yet
_,_,connectivity = np.intersect1d(indices_nodes,node_number_list,return_indices=True)
f2 = f2[connectivity,:]

f2 = f2.reshape((Nt_tot,nb_node,dim))

components2 = {}
Indices_Point2 = Data1D(name="indice", values=np.array(node_number_list), is_components=True)

Time = out.force.meshsolution.solution[0].field.get_axes()[0]

fx2_data = DataTime(
    name="Nodal force 2 (x)",
    unit="N",
    symbol="Fx2",
    axes=[Time, Indices_Point2],
    values=f2[..., 0],
)
components2["comp_x"] = fx2_data

fy2_data = DataTime(
    name="Nodal force 2 (y)",
    unit="N",
    symbol="Fy2",
    axes=[Time, Indices_Point2],
    values=f2[..., 1],
)
components2["comp_y"] = fy2_data

vec_force2 = VectorField(name="Nodal forces 2", symbol="F2", components=components2)
solforce2 = SolutionVector(field=vec_force2, type_cell="node", label="F2")
out.force.meshsolution.solution.append(solforce2)

```

## Analyse & compare

One can firstly plot the reference deformations

```python
out.force.meshsolution.plot_glyph(
    label="F2",
    is_point_arrow=True,
    # save_path="add a save path if you want",
)
```

    WARNING:root:VTK requires 3D points, but 2D points given. Appending 0 third component.

    <pyvistaqt.plotting.BackgroundPlotter at 0x13fd9885820>

To pursue further comparisons we will filter the relevant values given the following criteria :
$$f_{node} \geq \frac{f_{max}}{1000} $$ One can see that the **mean relative error** along both axis is approximately **1.5%** which validates the algorithm.

Relative error is defined as follow :
\begin{equation}
error_x = \frac{|f_x - f_{ref,x}|}{|f_{ref,x}|}
\end{equation}

```python
%matplotlib inline
computed_forces_x = out.force.meshsolution.solution[0].field.components["comp_x"].values
reference_forces_x = f2[..., 0]

computed_forces_y = out.force.meshsolution.solution[0].field.components["comp_y"].values
reference_forces_y = f2[..., 1]

# Filter on 1e-3 * max
relevant_mask_x = reference_forces_x > 1e-3*np.max(reference_forces_x)
relevant_mask_y = reference_forces_y > 1e-3*np.max(reference_forces_y)

relevant_computed_forces_x = computed_forces_x[relevant_mask_x]
relevant_reference_forces_x = reference_forces_x[relevant_mask_x]
relevant_computed_forces_y = computed_forces_y[relevant_mask_y]
relevant_reference_forces_y = reference_forces_y[relevant_mask_y]

plt.figure()
mean_error_x = np.mean(np.abs(relevant_computed_forces_x-relevant_reference_forces_x)/np.abs(relevant_reference_forces_x))
plt.plot(np.arange(len(relevant_computed_forces_x)),np.abs(relevant_computed_forces_x-relevant_reference_forces_x)/np.abs(relevant_reference_forces_x),'o',label='Mean error={}'.format(np.round(mean_error_x,3)))
plt.xlabel('Relevant nodes')
plt.ylabel('Relative error')
plt.legend(loc='best')
plt.title('Relative errors along axis X')
plt.show()

plt.figure()
mean_error_y = np.mean(np.abs(relevant_computed_forces_y-relevant_reference_forces_y)/np.abs(relevant_reference_forces_y))
plt.plot(np.arange(len(relevant_computed_forces_y)),np.abs(relevant_computed_forces_y-relevant_reference_forces_y)/np.abs(relevant_reference_forces_y),'o',label='Mean error={}'.format(np.round(mean_error_y,3)))
plt.xlabel('Relevant nodes')
plt.ylabel('Relative error')
plt.legend(loc='best')
plt.title('Relative errors along axis Y')
plt.show()

```

It could be interesting to display the forces on **relevant nodes** that have a significatively huge error, i.e. : $$ error > 0.2 $$
It is done on the next plot based on errors along axis X, with $$ f - f_{ref} $$ being displayed on these nodes.
One can see that it happends mostly on nodes at the interface between stator and air.

```python
big_diff_mask_x = np.logical_and(np.abs(computed_forces_x-reference_forces_x)/np.abs(reference_forces_x) > 0.2, np.abs(computed_forces_x-reference_forces_x)/np.abs(reference_forces_x) < 1.5)
big_diff_mask_y = np.logical_and(np.abs(computed_forces_y-reference_forces_y)/np.abs(reference_forces_y) > 0.2, np.abs(computed_forces_y-reference_forces_y)/np.abs(reference_forces_y) < 1.5)

big_diff_x = np.where(big_diff_mask_x,computed_forces_x-reference_forces_x,np.zeros(computed_forces_x.shape))
big_diff_y_x = np.where(big_diff_mask_x,computed_forces_y-reference_forces_y,np.zeros(computed_forces_y.shape))

components_diff_x = {}
fx_diff_data = DataTime(
    name="Nodal force diff x (x)",
    unit="N",
    symbol="Fxdx",
    axes=[Time, Indices_Point2],
    values=big_diff_x,
)
components_diff_x["comp_x"] = fx_diff_data

fy_diff_data = DataTime(
    name="Nodal force diff x (y)",
    unit="N",
    symbol="Fydx",
    axes=[Time, Indices_Point2],
    values=big_diff_y_x,
)
components_diff_x["comp_y"] = fx_diff_data

vec_force_dx = VectorField(name="Nodal forces dx", symbol="Fdx", components=components_diff_x)
solforce_dx = SolutionVector(field=vec_force_dx, type_cell="node", label="Fdx")
out.force.meshsolution.solution.append(solforce_dx)

out.force.meshsolution.plot_glyph(
    label="Fdx",
    is_point_arrow=True,
    # is_show_fig=True,
    # save_path=join(save_path,"magneto_plot_glyph2.png"),
)
```

    WARNING:root:VTK requires 3D points, but 2D points given. Appending 0 third component.

    <pyvistaqt.plotting.BackgroundPlotter at 0x13fd969ab80>

## Code details

The gist of the physics can be found in the method element_loop, which is a loop on the element of the mesh that is called in comp_force_nodal. There, the magnetostrictive tensor is computed :

```python
# if(self.tensor['magnetostriction']):
#     tme = self.comp_magnetostrictive_tensor(mue, Me, Nt_tot, polynomial_coeffs)
#     total_tensor += tme
```

comp_magnetostrictive_tensor uses the following defintion from the already introduced article :
$$ \alpha^c = -\alpha_1 * \mu_0 * MM - \alpha_2*\mu_0*M^2*I $$
where $\alpha_i$ depend on the material and have default values according to experimental results in the publication.

This tensor is bounded to an element since we currently work with first-order element, meaning physical values such as B,H,M are element-wise constants.

It is then multiplied by the outward normal of the current vertice and the length of the latter, which gives a force that should be added to the opposite node of the current vertice (in triangular element), but since the sum of nodal forces on a given element is null, one can also add $-\frac{1}{2}$ of this force on each vertice's nodes.

```python
# edge_force = np.tensordot(
#     total_tensor, normal_to_edge, [[1], [0]]
# )  # [[1],[0]] means sum product over rows for normal (which is vertical) and over rows for tme

# # Total edge force contribution, to be added to the 2 nodes that made the edge
# fe = -Ve0 * edge_force
# f[node_indice, :, :] = f[node_indice, :, :] + fe / 2
# f[next_node_indice, :, :] = f[next_node_indice, :, :] + fe / 2
```

```python

```


```python
# Allow us to load `open_cp` without installing
import sys, os.path
sys.path.insert(0, os.path.abspath(".."))
```

# A grid based model SEPP model

In (1), an explicitly grid-based self-exciting point process (SEPP) model was considered.  The paper (1) is mostly concerned with two field-trials of the SEPP or epidemic-type aftershock sequence (ETAS) type prediction models.  There is no real discussion as to why a grid based model was chosen, compared to the earlier continuous space model using kernel density estimation.

### References:

1. Mohler et al. "Randomized Controlled Field Trials of Predictive Policing". Journal of the American Statistical Association (2015) DOI:10.1080/01621459.2015.1077710
2. Lewis, Mohler, "A Nonparametric EM algorithm for Multiscale Hawkes Processes", preprint (2011) see http://math.scu.edu/~gmohler/EM_paper.pdf

# The model

The area of interest is divided into a grid; 150m squares were used in (1).  To quote from (1):

> The size of the grid cells on which μ is defined can be estimated by
maximum likelihood and in general the optimum size of the grid
cell will decrease with increasing data. However, for a fixed area
flagged for patrol, a greater number of small hotspots are more
difficult to patrol than a small number of large hotspots. The
150 × 150 m hotspots were chosen in this study to be the size
of a city block in Foothill and were then held constant across all
of the experimental regions.

The model looks _at each grid cell individually_ and uses what we might recognise as a Hawkes process (in the original form studied by Hawkes, not the more general form).  In grid cell $n$, the conditional intensity $\lambda_n(t)$ is given by

\begin{equation} \lambda_n(t) = \mu_n + \sum_{t^i_n < t} \theta \omega e^{-\omega(t-t^i_n)}
\tag{eq 1}
\end{equation}

where here:

- $\mu_n$ is the background rate in cell $n$.
- $\omega$ and $\theta$ control the "near-repeat" behaviour.  Notice that these do not depend upon $n$.
- As always, we sum over events $t^i_n$ which have occurred before the time of interest $t$, and _which have occurred in the same grid cell_.

It seems surprising that inter-cell interactions are not considered at all in this model.

# Model fitting

I do not quite understand the paper (1) here.  The following is what I think _should_ have been written.

We make initial estimates for $\mu_n$ (perhaps setting each $\mu_n$ to be the same) and $\omega, \theta$.  Then we alternate the following two steps until convergence:

### Expectation step:

Compute the upper triangular "probability matrix":

$$ p^{n}_{ji} \propto \theta\omega e^{-\omega(t_n^i - t_n^j)}, \qquad p^n_{ii} \propto \mu_n $$

where $p^{n}_{ji}$ is the probability, in cell $n$, then event $j$ triggered event $i$.  We set $p^n_{ji} = 0$ if $j > i$ and then normalise so that each column of the matrix sums to $1$ (which is why in the above formula I only give what $p^n_{ji}$ is proportional to).

### Maximisation step:

We then compute the new parameters:

$$ \omega = \frac{\sum_n \sum_{i<j} p^n_{ij}}{\sum_n \sum_{i<j} p^n_{ij}(t_n^j - t_n^i)}, \qquad
\theta = \frac{\sum_n \sum_{i<j} p^n_{ij}}{\sum_n \sum_j 1}, \qquad \mu_n = \frac{\sum_i p^n_{ii}}{T} $$
where $T$ is the time window of data under consideration.

(Note here I have given an estimate for $\mu_n$, whereas (1) seems to sum over $n$ and give an estimate for $\mu$, the total rate across the study area, but does not specify a suitable estimate for $\mu_n$.)

# Simulation

Simulating a process satisfying (eq 1) can be accomplished in the same way as simulating a more general Hawkes-type process.  However, in the simple form we have here, we can find faster simulation methods.

1. We simulate the "branching process" form.  This can be done on a cell by cell basis.
2. So firstly we simulate a homogeneous Poisson process with intensity $\mu_n$.
3. Then each point $i$ gives rise to an independent inhomogeneous Poisson process with intensity $\theta\omega e^{-\omega(t-t_i)}$ where event $i$ occurred at time $t_i$.

To simulate the process in 3 we make use of the following result.  If $\lambda^*$ is the conditional intensity of a process, then the _compensator_ is
$$ \Lambda(t) = \int_0^t \lambda^*(s) \ ds. $$
Then we have the following useful result:

> If $(t_1,\cdots,t_k)$ is a realisation of time $[0,T]$ of a point process with conditional intensity $\lambda^*$ then $\{\Lambda(t_1),\cdots,\Lambda(t_k)\}$ is unit rate Poisson process.

The converse to this statement is also true.  So, if $\Lambda$ is invertible, then we may simulate a unit rate Poisson process $\{s_1,\cdots,s_k\}$ and then $t_i  = \Lambda^{-1}(s_i)$ will be a process with conditional intensity $\lambda^*$.

In our case, $\lambda^*(t) = \theta\omega e^{-\omega t}$ (translating so that the "trigger" event occurs at $t=0$) and so
$$ \Lambda(t) = \theta \int_0^t \omega e^{-\omega s} \ ds = \theta \Big[ -e^{-\omega s} \Big]_0^t
= \theta\big( 1- e^{-\omega t} \big). $$
This has inverse $\Lambda^{-1}(s) = \omega^{-1} \log(\theta / (\theta - s) )$ defined for $0\leq s < \theta$. So

- Simulate a unit rate Poisson process $\{s_1,\cdots,s_k\}$ on $[0,\theta)$ and then set $t_i = \Lambda^{-1}(s_i)$ to get the "after-shock" events.
- This allows us to think about the parameters $\theta$ and $\omega$.  $\theta$ controls the rate of the process-- we expect an average of $\theta$ events.  $\omega$ controls the scale of the resulting process.

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

## Simulate some test data

We lay down a grid, set a random background rate in each cell, simulate the process, and then add some noise to generate x,y coordinates in each grid cell.  This is obviously a bit artifical, as in real data, it is likely that the background rate in a cell will be related to the background rate in neighbouring cells.

```python
import open_cp.sources.sepp as source_sepp

rates = np.random.random(size=(10,10))
simulation = source_sepp.GridHawkesProcess(rates, 0.5, 10)
points = simulation.sample_to_randomised_grid(0, 365, grid_size=50)
time_unit = source_sepp.make_time_unit(datetime.timedelta(days=1))
timed_points = source_sepp.scale_to_real_time(points, datetime.datetime(2017,1,1),
    time_unit)
```

```python
fig, ax = plt.subplots(ncols=2, figsize=(16,7))

ax[0].scatter(timed_points.xcoords, timed_points.ycoords, alpha=0.1)
ax[0].set_title("Space location of simulated data")
ax[0].set(xlim=[0,500], ylim=[0,500])

times = timed_points.times_datetime()
ax[1].scatter(times, timed_points.xcoords, alpha=0.1)
ax[1].set_xlim([datetime.datetime(2017,1,1), datetime.datetime(2018,1,1)])
ax[1].set_title("Date against x location")
fig.autofmt_xdate()
None
```

# Train the model

Remember that we rescaled time.
- We sample initially with an implied unit of "days", and then convert to minutes; one day = 1440 minutes
- The aftershock rate was initially 10, so a mean of 0.1 days which becomes 144 minutes
- So the rate we look for is 1/144 = 0.00694...

```python
import open_cp.seppexp as sepp
```

```python
region = open_cp.RectangularRegion(xmin=0, xmax=500, ymin=0, ymax=500)
trainer = sepp.SEPPTrainer(region, grid_size=50)
trainer.data = timed_points
predictor = trainer.train(iterations=50)
print("Predicted omega={}, theta={}".format(predictor.omega, predictor.theta))
```

    Predicted omega=0.0069985113729184294, theta=0.49508368828061144

```python
fig, ax = plt.subplots(ncols=1, figsize=(8,6))
ax.plot([0,1], [0,1], linewidth=1, color="r")
ax.scatter(rates.ravel(), predictor.mu.ravel()*1440)
ax.set(xlim=[0,1], ylim=[0,np.max(predictor.mu)*1440*1.1])
ax.set(xlabel="Actual background rate", ylabel="Predicted background rate")
ax.set_title("Predicted background rates")
None
```

# Make predictions

Once trained, we have an estimate of the background rate in each grid cell, and the global parameters $\theta, \omega$ describing the triggering / aftershock behaviour.  These can then be applied to any data fitting the same grid pattern (though of course the data should be compatible!)

```python
predictor.data = timed_points
dates = [datetime.datetime(2018,1,1), datetime.datetime(2018,1,2)]
predictions = [predictor.predict(date) for date in dates]
background_prediction = predictor.background_prediction()
```

```python
fig, ax = plt.subplots(ncols=3, figsize=(16,6))
for a in ax:
    a.set(xlim=[region.xmin, region.xmax], ylim=[region.ymin, region.ymax])

for pred, date, a in zip(predictions, dates, ax):
    m = a.pcolormesh(*pred.mesh_data(), pred.intensity_matrix, cmap="Blues")
    a.set_title("Prediction for {}".format(date))
    fig.colorbar(m, ax=a)

m2 = ax[2].pcolormesh(*background_prediction.mesh_data(), background_prediction.intensity_matrix, cmap="Blues")
ax[2].set_title("Background rate")
fig.colorbar(m2, ax=ax[2])
None
```

As with the other SEPP model, we see that "aftershocks" dominate the prediction, but as we predict further into the future, we converge on the background rate.

A more realistic "prediction" method would be to simulate future events using the model, and then compute the intensity based on this simulation, perhaps finally taking an average over many simulations.

```python

```

examples/Self-exciting point processes 2.ipynb


```python
from IPython.core.display import HTML, Image
css_file = 'style.css'
HTML(open(css_file, 'r').read())
```

<link href='http://fonts.googleapis.com/css?family=Alegreya+Sans:100,300,400,500,700,800,900,100italic,300italic,400italic,500italic,700italic,800italic,900italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Arvo:400,700,400italic' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=PT+Mono' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Shadows+Into+Light' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Philosopher:400,700,400italic,700italic' rel='stylesheet' type='text/css'>

<style>

@font-face {
    font-family: "Computer Modern";
    src: url('http://mirrors.ctan.org/fonts/cm-unicode/fonts/otf/cmunss.otf');
}

/* Formatting for header cells */
.text_cell_render h1 {
    font-family: 'Philosopher', sans-serif;
    font-weight: 400;
    font-size: 2.2em;
    line-height: 100%;
    color: rgb(0, 80, 120);
    margin-bottom: 0.1em;
    margin-top: 0.1em;
    display: block;
}
.text_cell_render h2 {
    font-family: 'Philosopher', serif;
    font-weight: 400;
    font-size: 1.9em;
    line-height: 100%;
    color: rgb(245,179,64);
    margin-bottom: 0.1em;
    margin-top: 0.1em;
    display: block;
}

.text_cell_render h3 {
    font-family: 'Philosopher', serif;
    margin-top:12px;
    margin-bottom: 3px;
    font-style: italic;
    color: rgb(94,127,192);
}

.text_cell_render h4 {
    font-family: 'Philosopher', serif;
}

.text_cell_render h5 {
    font-family: 'Alegreya Sans', sans-serif;
    font-weight: 300;
    font-size: 16pt;
    color: grey;
    font-style: italic;
    margin-bottom: .1em;
    margin-top: 0.1em;
    display: block;
}

.text_cell_render h6 {
    font-family: 'PT Mono', sans-serif;
    font-weight: 300;
    font-size: 10pt;
    color: grey;
    margin-bottom: 1px;
    margin-top: 1px;
}

.CodeMirror{
        font-family: "PT Mono";
        font-size: 100%;
}

</style>

## Library import

```python
from sympy import init_printing, Matrix, symbols, eye, Rational
init_printing()
```

# LU decomposition of a matrix A

In this notebook, we will decompose the matrix $A$ into and upper and lower triangular matrix, such that multiplying these will return $A$.  this is shown in (1) where $L$ is the lower triangular matrix and $U$ is the upper triangular matrix.

$$ A = LU \tag{1}$$

## Turning a matrix of coefficients into _U_pper triangular form

Consider the following matrix of coefficients shown in (2).

$$ \begin{bmatrix} 1 & -2 & 1 \\ 3 & 2 & -2 \\ 6 & -1 & -1 \end{bmatrix}  \tag{2}$$

We need to convert this into upper triangular form.  A generic $ 3 \times 3$ upper triangular matrix is shown in (3).

$$ \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ 0 & a_{22} & a_{23} \\ 0 & 0 & a_{3,3} \end{bmatrix} \tag{3} $$

Note that all entries _below_ the main diagonal are $0$.  This is an _upper triangular matrix_.

To get our matrix in (2) into upper triangular form, successive elementary row operation follow, which remember, is nothing other than matrix multiplication of the elementary matrices.  An elementary matrix is an identity matrix on which one elementary row operation was performed.  below, we create the $3 \times 3$ matrix in (2) and save it as `sympy` matrix object named `A`.

```python
A = Matrix([[1, -2, 1], [3, 2, -2], [6, -1, -1]])
A
```

The `eye()` function returns an identity matrix.

```python
eye(3)  # Identity matrix of size 3 x 3
```

We have to get a $-3$ in the first _pivot_ (the $1$ in row $1$, column $1$) to get rid of the $3$ in row $2$, column $1$ (we call the resulting elementary matrix `E21`, referring to the row $2$, column $1$).  Then we add the new row $1$ to row $2$.  Row $1$ of the identity matrix is then $\left( -3,0,0 \right) $ (but we leave it as $\left( 1,0,0 \right)$ in `E21`).  Adding this to row $2$ leaves $ \left( -3,1,0 \right) $.

To see why this is so, if we multiply row $1$ by $-3$ we have $ \left( -3, 0, 0 \right) $.  Adding this to row $2$, which is $ \left( 0,1,0 \right) $, results in $ \left( -3,1,0 \right) $ and hence `E21` being what we see below.

```python
E21 = Matrix([[1, 0, 0], [-3, 1, 0], [0, 0, 1]])
E21
```

Now we left-multiply $A$ by the elementary matrix `E21`.  In matrix notation, we would write $E_{21} A$.

```python
E21 * A # The resulting matrix after multiplication by E21
```

Good, we now have a leading $0$ in row $2$.  We follow the same steps to get rid of the leading $6$ in row $3$, column $1$.  Multiplying row $1$ (of the identity matrix) by $-6$ and adding this new row to row $3$ yields elementary matrix `E31`.

```python
E31 = Matrix([[1, 0, 0], [0, 1, 0], [-6, 0, 1]])
E31
```

Now for the left-multiplication of $E_{21} A$ by `E31` so that we have $E_{31} E_{21} A$.

```python
E31 * E21 * A # This got rid of the leading 6 in row 3
```

Now the $11$ in row $3$, column $2$ is the _pivot_ and we also need to get rid of the $8$ in row $2$, column $2$.  Unfortunately, we have an $8$ and an $11$ to deal with.  We will have to do two elementary row operations.  First $-11$ times row $2$ of the identity matrix $ \therefore \left( 0,-11,0 \right) $,  added to $8$ times row $3$ $ \therefore \left( 0,0,8 \right) $) such that we have $ \left( 0,-11,8 \right) $.  Below it is save as `E32`.

```python
E32 = Matrix([[1, 0 , 0], [0, 1, 0], [0, -11, 8]])
E32
```

```python
U = E32 * E31 * E21 * A
U # We call is U for upper triangular
```

The matrix is now in upper triangular form, achived by the elementary matrixes shown in (4).

$$ { E }_{ 32 } { E }_{ 31 } { E }_{ 21 } A=U \tag{4} $$

## Calculating the _L_ower triangular from

Note, to reverse the process above, we would have to do the multiplication shown in (5).

$$ { \left( { E }_{ 21 } \right)  }^{ -1 }{ \left( { E }_{ 31 } \right)  }^{ -1 }{ \left( { E }_{ 32 } \right)  }^{ -1 }\left( { E }_{ 32 } \right) \left( { E }_{ 31 } \right) \left( { E }_{ 21 } \right) A=A \tag{5} $$

The inverse of a matrix can be calculated using the `sympy` method `.inv()`.

We can check this with a Boolean logic, using the `==` symbol, which checks if the left- and right-hand sides re equal.

```python
E21.inv() * E31.inv() * E32.inv() * E32 * E31 * E21 * A == A # The Boolean double equal signs asks: Is the
# left-hand side equal to the right-hand side?
```

    True

We will be back with the identity matrix just multiplying the inverse elementary matrices and the elementary matrices.

```python
E21.inv() * E31.inv() * E32.inv() * E32 * E31 * E21
```

We left-multiply both sides of (4) by these inverse elementary matrices as shown in (6).

$$ { \left( { E }_{ 21 } \right)  }^{ -1 }{ \left( { E }_{ 31 } \right)  }^{ -1 }{ \left( { E }_{ 32 } \right)  }^{ -1 }\left( { E }_{ 32 } \right) \left( { E }_{ 31 } \right) \left( { E }_{ 21 } \right) A={ \left( { E }_{ 21 } \right)  }^{ -1 }{ \left( { E }_{ 31 } \right)  }^{ -1 }{ \left( { E }_{ 32 } \right)  }^{ -1 }U \tag{6} $$

The multiplication of these inverse elementary matrices results in a _l_ower triangular matrix, $L$, such that we have (1).

```python
L = E21.inv() * E31.inv() * E32.inv()
L
```

Let's check if this is so.

```python
A == L * U # Checking this with a Boolean question
```

    True

```python
A, L * U # They are identical
```

## Doing this in one go using sympy

The `sympy` library provides the `.LUdecomposition()` method for rectangular matrices.  It returns three values.

```python
L, U, _ = A.LUdecomposition()
```

```python
L
```

```python
U # Note the difference from the U above
```

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

Note the subtle difference between this $U$ and the one calculated above.  It simply has row $3$ divided by $8$.  It makes no difference as shown below.

```python
L * U # Back to A
```

### What's special about L?

Our methods only works when no row interchange happens.  It also actually only works when doing the conventional subtracting the scalar multiplication of a row from another row, leaving the positive scalar.  (This is opposed to the negatives I often use in my head, allowing me to add the two rows).

Note the $3$ (in row $2$, column $1$) and the $6$ (in row $3$, column $1$).  They are the row multiplications we have to do for `E21` and `E31`.  The
$\frac{11}{8}$ is what we did for `E32` (we just did it in two steps so as not to use fractions).

## Row exchanges

Sometimes, we have to allow row exchanges, i.e. if the pivot contains a $0$.

As an example, from a $ 3 \times 3 $ identity matrix we could have the following.

```python
eye(3)
```

Exchanging rows $1$ and $2$.

```python
Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]])
```

```python
A, Matrix([[0, 1, 0], [1, 0, 0], [0, 0, 1]]) * A # Showing row exchange
```

By the way, how many permutations of row exchanges are there?  This answer is $n!$, where $n$ is the number of rows.

## Example problems

### Example problem 01

1. Perform LU decomposition of:
$$  \begin{bmatrix} 1 & 0 & 1 \\ a & a & a \\ b & b & a \end{bmatrix} $$

2. For which values of $a$ and $b$ does $L$ and $U$ exist?

#### Solution

```python
a, b = symbols('a b')
```

```python
A = Matrix([[1, 0, 1], [a, a, a], [b, b, a]])
A
```

```python
L,U, _ = A.LUdecomposition()
```

```python
L, U
```

Checking.

```python
L * U == A
```

    True

For existence it is clear that $ a \ne 0 $.  Not only for the division by $0$, but because we will have a row of $0$'s and a $0$ in the pivot position, row $2$, column $2$.  Furthermore, $a \ne b$, for the same reasons.

## Hints and tips

```python
E21, E21.inv()
```

To take the inverse of an elementary matrix, simply change the sign of the off-diagonal elements and multiply each element by 1 over the determinant (more about the determinant later).  The determinant is easy to do for these $ n=3 $ square matrices, since the top row is $ \left( 1,0,0 \right) $.

```python
E32, E32.inv()
```

$$\begin{pmatrix}\left[\begin{matrix}1 & 0 & 0\\0 & 1 & 0\\0 & -11 & 8\end{matrix}\right], & \left[\begin{matrix}1 & 0 & 0\\0 & 1 & 0\\0 & \frac{11}{8} & \frac{1}{8}\end{matrix}\right]\end{pmatrix}$$

By keeping track of the elementary matrices it is easy to get $L$ and $U$.  It is also easy to get the inverses of $L$ and $U$.  This means it is easy to calculate the values of a column vector $ \underline{x} $ when we have $ A \underline{x} = \underline{b} $ as shown in (7).

$$ \begin{align} Ax &= LUx=b\\ Ux &= { L }^{ -1 }b\\ x &= { U }^{ -1 }{ L }^{ -1 }b \end{align} \tag{7} $$

```python

```

Mathematics/Linear Algebra/0.0 MIT-18.06 - Jupyter/Lecture_4_LU_decomposition_of_A.ipynb