# Modeling Hillslopes and Channels with Landlab
The original version of this exercise was donated by Andy Wickert at the University of Minnesota. This notebook was created by Nicole Gasparini at Tulane University.

For tutorials on learning Landlab, click here: https://github.com/landlab/landlab/wiki/Tutorials

**What is this notebook?**

This notebook illustrates a landscape evolution model in which the landscape evolves according to the equation:

\begin{equation}
 \frac{\partial z}{\partial t} = -K_\text{sp} A^{m_{sp}} S^{n_{sp}} + K_\text{hs} \frac{\partial^2 z}{\partial x^2} + U
\end{equation}
Here, $K$ are coefficients on the fluvial ($sp$) and hillslope ($hs$) parts of the equation, and $m_{sp}$ and $n_{sp}$ are positive exponents, usually thought to have a ratio, $m_{sp}/n_{sp} \approx 0.5$. $A$ is drainage area and $S$ is the slope of steepest descent ($-\frac{dz}{dx}$) where $x$ is horizontal distance (positive in the downslope direction) and $z$ is elevation. (If slope is negative there is no fluvial erosion.) $U$ is an externally-applied uplift field.

The first term on the right hand side of the equation is the fluvial erosion term, which is also known as the stream power equation. The second term on the right hand side of the equation is elevation changes via linear diffusion, and linear diffusion is one way in which to describe hillslope sediment transport.

For more information on the fluvial erosion term, please see:

- Whipple, K.X. and Tucker, G.E., 1999. Dynamics of the stream‐power river incision model: Implications for height limits of mountain ranges, landscape response timescales, and research needs. Journal of Geophysical Research: Solid Earth.

For more information on linear diffusion applied to hillslopes (and other fun hillslope models) see:

- Roering, J.J., 2008. How well can hillslope evolution models “explain” topography? Simulating soil transport and production with high-resolution topographic data. Geological Society of America Bulletin.

The ideas behind what this notebook does are presented nicely in the two papers below. Neither of them is exactly the same as this notebook, but they discuss drainage density and transitions from hillslope to fluviall processes.

- Tucker, G.E. and Bras, R.L., 1998. Hillslope processes, drainage density, and landscape morphology. Water Resources Research.

- Perron, J.T., Kirchner, J.W. and Dietrich, W.E., 2009. Formation of evenly spaced ridges and valleys. Nature.

**What will you do?**

In this exercise you will modify the code to get a better understanding of how different processes and forces control landscape evolution, landscape form and drainage density (as interpreted from slope-area data). It is expected that you have already learned the basics about fluvial bedrock incision (and the stream power equation) and sediment transport through creep on hillslopes (and the diffusion equation). (See references above.)

Start by sequentially running each code block without changing anything. To run an individual code cell, put the cursor in the cell and type shift-enter, or got to the _Cell_ pulldown menu at the top and choose _Run Cells_. At the end of the notebook you will see the questions that you need to answer by changing parts of the code and rerunning it.

Remember that you can always go to the _Kernel_ pulldown menu at the top and choose _Restart & Clear Output_ or _Restart & Run All_ if you change things and want to start afresh. If you just change one code block and rerun only that code block, only the parts of the code in that code block will be updated. (E.g. if you change parameters but don't reset the code blocks that initialize run time or topography, then these values will not be reset.)

**Questions to answer before starting this assignment.**

1. If hillslope diffusivity ($K_{hs}$) is fixed, but fluvial erodibility ($K_{sp}$) increases, what do you think will happen to the total relief and drainage density of a landscape?
2. If fluvial erodibility ($K_{sp}$) is fixed but hillslope diffusivity ($K_{hs}$) increases, what do you think will happen to the total relief and drainage density of a landscape?
3. If parameters stay fixed ($K_{hs}$ and $K_{sp}$), but the uplift rate increases, what do you think will happen to the total relief and drainage density of a landscape?

**Now on to the code...**

First we have to import the parts of Python and Landlab that are needed to run this code. You should not have to change this first code block.

```python
# below is to make plots show up in the notebook
%matplotlib inline
```

```python
# Code Block 1

import numpy as np
from matplotlib import pyplot as plt

from landlab import HexModelGrid, RasterModelGrid, imshow_grid
from landlab.components import (
    DepressionFinderAndRouter,
    FlowAccumulator,
    LinearDiffuser,
    StreamPowerEroder,
)
```

Now we set parameters

**This part you will need to change for the different exercises.**

Note that Landlab does not impose units, but it assumes that all units are consistent. We will assume that everything is given in _meters_ (m) and _years_ (yr).

```python
# Code Block 2

uplift_rate = 0.001  # [m/yr], initially set at 0.001
K_sp = 1.0e-5  # units vary depending on m_sp and n_sp, initially set at 1e-5
m_sp = 0.5  # exponent on drainage area in stream power equation, initially 0.5
n_sp = 1.0  # exponent on slope in stream power equation, initially 1.
K_hs = 0.05  # [m^2/yr], initially 0.05
```

**Make a grid.**

This part you may want to change.

```python
# Code Block 3

ncells_side = 150  # number of raster cells on each side, initially 150
dxy = 50  # side length of a raster model cell, or resolution [m], initially 50

# Below is a raster (square cells) grid, with equal width and height
mg = RasterModelGrid((ncells_side, ncells_side), dxy)

# Below is a Hexagonal grid (hexagonal cells,
# with equal width and height (a square)
# mg = HexModelGrid(ncells_side, ncells_side, dxy, shape='rect')

# Below is a hexagonal grid (hexagonal cells),
# in which the grid is also a hexagon
# mg = HexModelGrid(ncells_side, ncells_side/2, dxy, shape='hex')
```

Set some variables related to time.

```python
# Code Block 4

dt = 1000  # time step [yr], initially 5000
total_time = 0  # amount of time the landscape has evolved [yr]
tmax = 1e6  # time for the model loop to run [yr], initially 1e6

t = np.arange(0, tmax, dt)  # each of the time steps that the code will run
```

Here we make the initial grid of elevation.

```python
# Code Block 5

np.random.seed(0)  # seed set to zero so our figures are reproducible
mg_noise = np.random.rand(mg.number_of_nodes) / 1000.0  # intial noise on elevation grid

# set up the elevation on the grid
zr = mg.add_zeros("topographic__elevation", at="node")
zr += mg_noise
```

Intializing all of the process components that do the work.

```python
# Code Block 6

frr = FlowAccumulator(mg)  # intializing flow routing
spr = StreamPowerEroder(
    mg, K_sp=K_sp, m_sp=m_sp, n_sp=n_sp, threshold_sp=0.0
)  # initializing stream power incision
dfn = LinearDiffuser(
    mg, linear_diffusivity=K_hs, deposit=False
)  # initializing linear diffusion
# df = DepressionFinderAndRouter(mg) # Initializing the pit finder
```

Now for the code loop. Note that you can rerun this code block many times, and as long as you don't rerun any of the code boxes above, it will take the already evolved landscape and evolve it even more.

```python
# Code Block 7

for ti in t:
    zr[mg.core_nodes] += uplift_rate * dt  # uplift the landscape
    dfn.run_one_step(dt)  # diffuse the landscape
    frr.run_one_step()  # route flow
    # df.map_depressions()
    spr.run_one_step(dt)  # fluvial incision
    total_time += dt  # update time keeper
    print(total_time)
```

Plot the topography.

```python
# Code Block 8

plt.figure(1)
imshow_grid(
    mg, "topographic__elevation", grid_units=("m", "m"), var_name="Elevation (m)"
)
title_text = f"$K_{{sp}}$={K_sp}; $K_{{hs}}$={K_hs}; $time$={total_time}; $dx$={dxy}"

plt.title(title_text)

max_elev = np.max(zr)
suptitle_text = "Maximum elevation is " + str(max_elev)
plt.suptitle(suptitle_text)

print("Maximum elevation is ", np.max(zr))
```

Plot the slope and area data at each point (in log-log space).

```python
# Code Block 9

plt.figure(2)
indices = np.where(mg.status_at_node[mg.at_node["flow__receiver_node"]] == 0)
plt.loglog(
    mg.at_node["drainage_area"][indices],
    mg.at_node["topographic__steepest_slope"][indices],
    "b.",
)

plt.ylabel("Topographic slope")
plt.xlabel("Drainage area (m^2)")
plt.title(title_text)
```

Has the landscape reached steady state yet?

Answer: Not quite. At perfect steady state, there will be no scatter in the fluvial part of the slope-area relationship (given this model set-up).

**What to do.**

Answer the following questions using the code above. All solutions should be typed, and supporting figures (produced using the code) should be embedded in your final document. (Download or screenshoot the figures.) You may want to make other plots with the data you collect using this model. You are free to make those plots using whatever software you choose.

In parts of this exercise you need to work with your classmates. You are encouraged to discuss how to use the model and model results with your classmates, however the write-up that you hand in must be your own work.

1. **Hillslope vs. Fluvial Processes.** Using the parameters provided in the initial notebook, run the landscape to steady state, or the point at which the topography and the slope-area relationship stop changing (i.e. erosion equals rock uplift). (You can keep rerunning Code Block 7 until steady state is reached. Steady state is reached asymptotically, so exact steady state is less important than very close.) Use the plots of slope and area to estimate where the hillslope–fluvial transition is (or in otherwords, the threshold drainage area for channel heads. There is usually a range of values. You should be consistent in your method to determine drainage density and describe how you determined it in your write-up). Also record the maximum elevation. Now try keeping $K_{sp}$ the same but increase and decrease $K_{hs}$ (change and run Code Block 2, then rerun Code Blocks 6 and 7). How do the maximum elevation and the transition from hillslope to channel change with changes in $K_{hs}$? Now try keeping $K_{hs}$ the same but increase and decrease $K_{sp}$ (change and run Code Block 2, then rerun Code Blocks 6 and 7). How do the maximum elevation and transition from hillslope to channel change with changes in $K_{sp}$? You can work in teams with your classmates so that you can explore more parameter combinations. Produce a relationship between the different values of $K_{sp}$, $K_{hs}$ and the threshold drainage area and maximum elevation. Remember to run all of your experiments with different parameter values to steady state before estimating the threshold drainage area and maximum elevation. Describe how the different parameters affect drainage density in words, and how this is seen in the relationship that you generate. You do not have to include plots of every single run, but include plots to illustrate at least three landscapes with different drainage densities.

2. **Uplift and erosion.** Now, perform a similar set of exercises as you did in exercise 1, but also systematically vary uplift rate (Code Block 2). Work in teams, and each person should choose two combinations of $K_{sp}$ and $K_{hs}$ and three uplift rates (for a total of 6 runs). Make sure the parameter values that you choose do not overlap with your group members. Make sure you document the transition from hillslope to fluvial process (make sure all of the team members are using the same method to determine threshold area for drainage density), and also note the maximum steady-state elevation for each combination of uplift, $K_{sp}$ and $K_{hs}$. Produce relationships to show how the area threshold and maximum elevation change with the different variables. Describe how uplift rate affects drainage density in words, and how this is seen in the relationship that you generate. You do not have to include plots of every single run, but include some plots to illustrate the changes that you describe. (Note whom your group members were in your write-up.)

3. **Free-form exploration. (Optional)** Try changing the grid type (Code Block 3), grid size (Code Block 3), stream power exponents (Code Block 2), distribution of uplift rate (e.g., what happens if you have just part of the landscape experience uplift, a bit trickier, as uplift in Code Block 2 will need to be changed to an array), etc. Based on what you observe, create a consistent geomorphic history of the system. Creativity is expected here!

4. **Final reflection.** Was your initial insight into how parameters would affect the landscape correct? Discuss in less than 5 sentences.


# Terms
## operator
> ## $\color{red}{\nrightarrow}$
>> ### inner product( n right arrow )
>> ### $ \color{magenta}{q^{(n,r)}_{\nrightarrow}}$
> ## $\color{red}{\circlearrowleft}$
>> ### cross product ( circle arrow left )
>> ### $ \color{magenta}{q^{(n,r)}_{\circlearrowleft}}$
> ## $\color{red}{\looparrowright}$
>> ### projection ( loop arrow right )
>> ### $ \color{magenta}{q^{(n,r)}_{\looparrowright}}$

> ## $\color{red}{\pitchfork}$
>> ### rotation in special ($\pi/2$) ( pitch fork )
>>> ## $\color{magenta}{q^{(r',r)}_{\pitchfork \theta}
= q^{(cos\theta \, r + sin\theta\, (n, r))}_{,\circlearrowright}
= q^{(cos\theta \, r + sin\theta\, n\, r)}_{} }$

---
> ## $\color{red}{\curvearrowleft}$
>> ### rotation in general ( curve arrow left )
>>> ## $\color{magenta}{q^{(r',r)}_{\curvearrowleft \theta}}
= q^{\upuparrows+\Rsh'}_{v}
\\ \because q^{\upuparrows} = q^{(r,n)}_{\looparrowright} = q^{\frac{(r,n)\,n}{(n,n)}}_{\nrightarrow, \nrightarrow} = q^{(r,n)\, n}_{\nrightarrow}
= q^{}_{}
\\ \because q^{\Rsh}_{v} =  q^{r-\upuparrows}_{v}
\\ \because q^{\Rsh'}_{v} = q^{\Rsh',\Rsh}_{\pitchfork,\theta} = q^{cos\theta \, \Rsh - sin\theta \, (n,\Rsh)}_{\circlearrowright}
= q^{cos\theta \big(r-\upuparrows \big)}_{v} + q^{sin\theta\big(n, (r-\upuparrows)\big)}_{\circlearrowright}
= q^{cos\theta \big(r-\upuparrows \big)}_{v} + q^{sin\theta\big(n, r\big)}_{\circlearrowright}
\\ = q^{(cos\theta) \big(r-\upuparrows \big)}_{v} + q^{(sin\theta) \big(n, r \big)}_{\circlearrowright}
\\
q^{\upuparrows}_{v} + q^{cos\theta(r-\upuparrows) + \sin\theta(n,r)}_{\circlearrowright}
= q^{(r,n)n + \cos\theta r - \cos\theta (r,n)n + \sin\theta(n,r)}_{\nrightarrow, \nrightarrow, \circlearrowright}
\\
\large { \color {magenta}{\because
q^{(r \cdot \hat{n}) \hat{n} \, (1-\cos\theta) \, + \, \cos\theta \, + \, \sin\theta \, (\hat{n} \times r)}_{\nrightarrow, \circlearrowright}
}}$

---
## symbols
> ## $\color{red}{\upuparrows}$
>> ### rotation in general ( up up arrows )
>>> ### $\color{magenta}{q^{\upuparrows}_{v} = q^{r,n}_{\looparrowright}}$
> ## $\color{red}{\Rsh}$
>> ### rotation in general ( Rsh )
>>> ## $\color{magenta}{q^{\Rsh} = q^{r-\upuparrows}_{v}}$
> ## $\color{red}{\Rsh'}$
>> ### rotation in general ( Rsh )
>>> ## $\color{magenta}{q^{(\Rsh',\Rsh)}_{\pitchfork \theta}}$

# $\color{rec}{\text{Quaternion Matrix}}$
> ### $
(s + ai + bj + ck) =: (s,\vec{v}), \quad
(s'+ a'i + b'j + c'k) =: (s', \vec{v'})
\\
(s,\vec{v})(s',\vec{v'})
= (ss' - (aa' + bb' + cc'),  s(a'+b'+c') + s'(a+b+c) + (bc'-cb')_{jkj} + (ca'-ac')_{kik} + (ab'-ba')_{iji})
\\
\therefore
\color{magenta}{\Big(ss' - \big(\vec{v} \cdot \vec{v'}\big), \;
s\vec{v'} + a\vec{v} + \big(\vec{v} \times \vec{v'}\big) \Big)}
\\
\begin{array}{l|r}
s  + ai +  bj +  ck & \\
s' + a'i + b'j + c'k & \times \\
\end{array}
= \left( \begin{array}{l|l}
ss' - aa' - bb' - cc' \\
as' + sa' - cb' + bc' \\
bs' + ca' + sb' - ac' \\
cs' - ba' + ab' + sc'
\end{array}\right) \iff
\left[ \begin{array}{rrrr}
s & -a & -b & -c \\
a &  s & -c &  b \\
b &  c &  s & -a \\
c & -b &  a &  s \\
\end{array}\right]
\left[ \begin{array}{rrrr}
s' & -a' & -b' & -c' \\
a' &  s' & -c' &  b' \\
b' &  c' &  s' & -a' \\
c' & -b' &  a' &  s' \\
\end{array}\right]
\iff
\left[ \begin{array}{rrrr}
s & -a & -b & -c \\
a &  s & -c &  b \\
b &  c &  s & -a \\
c & -b &  a &  s \\
\end{array}\right]
\left[ \begin{array}{}
s'\\
a'\\
b'\\
c'\\
\end{array}\right]
$

---
> ### $ q = (q^s_{r}, q^v_{v}),\quad  q' = (q^s_{r'}, q^{v}_{v'})\\
\big(q^s_r q^s_{r'} - \big( q^v_{v} \cdot q^{v}_{v'} \big), \;
q^s_{r} q^{v}_{v'} + q^s_{r'}q^v_{v} + \big( q^v_{v} \times q^{v}_{v'} \big) \big)
\\
\begin{array}{}
q = (q^s_r,q^i_a, q^j_b,q^k_c)\\ q' = (q^s_{r'}, q^i_{a'}, q^j_{b'}, q^k_{c'})
\end{array}
\left[ \begin {array}{}q^s_r \\ q^i_a \\ q^j_b \\ q^k_c \end{array} \right]
\left[ \begin{array}{} q^s_{r'} \\ q^i_{a'} \\ q^j_{b'} \\ q^k_{c'} \end{array}\right]
= \left( \begin{array}{l|l|l}
q^s_r q^s_{r'} - q^i_a q^i_{a'} - q^j_b q^j_{b'} - q^k_c q^k_{c'} & scalar & ijk = -1 & i^2,j^2,k^2 = -1 \\
q^i_a q^s_{r'} + q^s_r q^i_{a'} - q^k_c q^j_{b'} + q^j_b q^k_{c'} & i & jk = i & kj = -i \\
q^j_b q^s_{r'} + q^k_c q^i_{a'} + q^s_r q^j_{b'} - q^i_a q^k_{c'} & j & ki = j & ik = -j \\
q^k_c q^s_{r'} - q^j_b q^i_{a'} + q^i_a q^j_{b'} + q^s_r q^k_{c'} & k & ij = k & ji = -1
\end{array}\right)
$

---
> ### $ q = (q^r_s, q^v_v)= (q^r_s,q^{a}_i, q^{b}_j,q^{c}_k),\quad  q' = (q^{r'}_s, q^{v'}_v)=(q^{r'}_s, q^{a'}_i, q^{b'}_j, q^{c'}_k) \\
\Big(q^1_s q^2_s - \big( q^1_v \cdot q^2_v \big), \;
q^1_s q^2_v + q^2_s q^1_v + \big( q^1_v \times q^2_v \big) \Big)
\\
\left[ \begin {array}{}q^r_s \\ q^{a}_i \\ q^{b}_j \\ q^{c}_k \end{array} \right] \left[ \begin{array}{} q^{r'}_s \\ q^{a'}_i \\ q^{b'}_j \\ q^{c'}_k \end{array}\right]
= \left( \begin{array}{l|l|l}
q^r_s q^{r'}_s - q^{a}_i q^{a'}_i - q^{b}_j q^{b'}_j - q^{c}_k q^{c'}_k & q^r_s q^{r'}_s - (q^{a}_i q^{a'}_i + q^{b}_j q^{b'}_j + q^{c}_k q^{c'}_k) & scalar\\
q^{a}_i q^{r'}_s + q^r_s q^{a'}_i - q^{c}_k q^{b'}_j + q^l_j q^{c'}_k & q^{a}_i q^{r'}_s + q^r_s q^{a'}_i + (q^l_j q^{c'}_k - q^{c}_k q^{b'}_j) & i\\
q^{b}_j q^{r'}_s + q^{c}_k q^{a'}_i + q^r_s q^{b'}_j - q^{a}_i q^{c'}_k & q^{b}_j q^{r'}_s + q^r_s q^{b'}_j + (q^{c}_k q^{a'}_i - q^{a}_i q^{c'}_k) & j\\
q^{c}_k q^{r'}_s - q^{b}_j q^{a'}_i + q^{a}_i q^{b'}_j + q^r_s q^{c'}_k & q^{c}_k q^{r'}_s + q^r_s q^{c'}_k + (q^{a}_i q^{b'}_j - q^{b}_j q^{a'}_i) & k
\end{array}\right)
\because ijk = -1,\quad i^2, j^2 , k^2 = -1,\quad ij = k ,jk = i,ki= j
$

```python
# Quaternion Matrix
import IPython as Ipy
Ipy.display.YouTubeVideo('https://www.youtube.com/watch?v=3Ki14CsP_9k&list=PLpzmRsG7u_gr0FO12cBWj-15_e0yqQQ1U&index=2',width=800,height=640)
```

```python
import sympy as sm
a,b,c,d = sm.symbols('a:d')
a1,b1,c1,d1 = sm.symbols("a' b' c' d'")

# M[1,1] -> 1st row dot 1st col = \sum_{j} A[1,j] x B[j,1]
# M[2,1] -> 2nd row dot 1st col = \sum_{j} A[2,j] x B[j,1]
# M[3,1] -> 3rd row dot 1st col = \sum_{j} A[3,j] x B[j,1]
# M[4,1] -> 4th row dot 1st col = \sum_{j} A[4,j] x B[j,1]

# M[1,2] -> 1st row dot 2st col = \sum_{j} A[1,j] x B[j,2]
# M[2,2] -> 2nd row dot 2st col = \sum_{j} A[2,j] x B[j,2]
# M[3,2] -> 3rd row dot 2st col = \sum_{j} A[3,j] x B[j,2]
# M[4,2] -> 4th row dot 2st col = \sum_{j} A[4,j] x B[j,2]
# M[i,j] -> \sum_{k} jA[i,k] x B[k,j]

# quaternion muliply matrix
# a + bi + cj + dk = (a,b,c,d)
# a = scalar b = coeffient of i
Q = sm.Matrix([
    [a,-b,-c,-d],
    [b, a,-d, c],
    [c, d, a,-b],
    [d,-c, b, a]])
q = sm.Matrix([a1,b1,c1,d1])

# list(i for i in zip(q1,[1,2,3,4]))
# dict(i for i in zip(q1,[1,2,3,4]))

# scalar
### (M[1,1]= scalar positon | when a scalar join other scalar then 1 times)
### (M[2,1]= i positon | when a scalar join other i then 1 times)
### (M[3,1]= j positon | when a scalar join other j then 1 times)
### (M[4,1]= k positon | when a scalar join other k then 1 times)

s = sm.Matrix([
    [ 1, 0, 0, 0],
    [ 0, 1, 0, 0],
    [ 0, 0, 1, 0],
    [ 0, 0, 0, 1],
])
# i
### (M[1,1]= scalar positon | when i join other i then -1 times)
### (M[2,1]= i positon | when i join other scalar then 1 times)
### (M[3,1]= j positon | when i join other k then -1 times)
### (M[4,1]= k positon | when i join other j then 1 times)
i = sm.Matrix([
    [ 0,-1, 0, 0],
    [ 1, 0, 0, 0],
    [ 0, 0, 0,-1],
    [ 0, 0, 1, 0],
])

# j
### (M[1,1]= scalar positon | when j by other j then -1 times)
### (M[2,1]= i positon | when j join other k then 1 times)
### (M[3,1]= j positon | when j join other scalar then 1 times)
### (M[4,1]= k positon | when j join other i then -1 times)
j = sm.Matrix([
    [ 0, 0,-1, 0],
    [ 0, 0, 0, 1],
    [ 1, 0, 0, 0],
    [ 0,-1, 0, 0],
])
# k
### (M[1,1]= scalar positon | when k join other k then -1 times)
### (M[2,1]= i positon | when k join other j then -1 times)
### (M[3,1]= j positon | when k jion other i then 1 times)
### (M[4,1]= k positon | when k join other scalar then 1 times)
k = sm.Matrix([
    [ 0, 0, 0,-1],
    [ 0, 0,-1, 0],
    [ 0, 1, 0, 0],
    [ 1, 0, 0, 0],
])

q1 = (1*s+2*i+ 3*j+4*k)

# minor
i.minorMatrix(1,1)
# adjoint(adjugate in sympy)-붙어있는 matrix
i.adjugate()

```

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

```python
R = sm.Matrix([
    [a,b,c,d],
    [b,a,d,c],
    [c,d,a,b],
    [d,c,b,a]])
R1 = sm.Matrix([
    [a1,b1,c1,d1],
    [b1,a1,d1,c1],
    [c1,d1,a1,b1],
    [d1,c1,b1,a1]])

R*R1
```

$\displaystyle \left[\begin{matrix}a a' + b b' + c c' + d d' & a b' + a' b + c d' + c' d & a c' + a' c + b d' + b' d & a d' + a' d + b c' + b' c\\a b' + a' b + c d' + c' d & a a' + b b' + c c' + d d' & a d' + a' d + b c' + b' c & a c' + a' c + b d' + b' d\\a c' + a' c + b d' + b' d & a d' + a' d + b c' + b' c & a a' + b b' + c c' + d d' & a b' + a' b + c d' + c' d\\a d' + a' d + b c' + b' c & a c' + a' c + b d' + b' d & a b' + a' b + c d' + c' d & a a' + b b' + c c' + d d'\end{matrix}\right]$

```python
(1*s+2*i+3*j+4*k)*(1*s+2*i+4*j+9*k)
```

$\displaystyle \left[\begin{matrix}-51 & -15 & 3 & -15\\15 & -51 & -15 & -3\\-3 & 15 & -51 & -15\\15 & 3 & 15 & -51\end{matrix}\right]$

```python
(1*s+2*i+ 3*j+4*k)*sm.Matrix([1,2,4,9])
```

$\displaystyle \left[\begin{matrix}-51\\15\\-3\\15\end{matrix}\right]$

```python
# conjugate
q1_ =1*s - 2*i - 3*j - 4*k
q1*q1_
```

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

---
# $\color{red}{\text{Quaternion Multiplication}}$
> ### $
( q^{r}_{s} q^{v}_{v}) \; ( q^{r'}_{s}, q^{v'}_{v} )
\\ \therefore \Big( q^{r}_{s} q^{r'}_{s} -  \big( q^{v}_{v}  \cdot q^{v'}_{v} \big),
\; q^{r}_{s} q^{v'}_{v} + q^{r'}_{s} q^{v}_{v} + \big( q^v_{v} \times q^{v'}_{v} \big) \Big)
$

---
> ### $
(0, q^{v}_{v}) \; (0,q^{v'}_{v})
\\ \therefore
\Big (
    - \big( q^{v}_{v} \cdot q^{v'}_{v} \big)
    , \quad
    q^{v}_{v} \times q^{v'}_{v} \;
\Big)
$

---
> ### $ \color{magenta}{\text{commutative of multiplication}}
\\
(0,q^{v'}_{v}) \; (0, q^{v}_{v})
\\ \therefore
\Big (
    - \big( q^{v}_{v} \cdot q^{v'}_{v} \big)
    , \quad
    - \big( q^{v}_{v} \times q^{v'}_{v} \big) \;
\Big)
$

> ### $
q_1 = (0, \vec{qv_{1}}) \quad q_2 = (0,\vec{qv_{2}})
\\
\begin{array}{c|l}
\\ &q_1 q_2 = \Big(-\vec{qv_{1}}  \cdot \vec{qv_{2}},
\; \vec{qv_{1}}\times\vec{qv_{2}} \Big)
\\
\pm & q_2 q_1 = \Big(-\vec{qv_{1}}  \cdot \vec{qv_{2}},
\; - \vec{qv_{1}}\times\vec{qv_{2}} \Big) \\
& \hline \\
& q_1 q_2 + q_2 q_1 = \Big(-2\big(\vec{qv_1} \cdot \vec{qv_2},\; 0\big) \Big)
= -2\Big( \vec{q_1} \cdot \vec{q_2}\Big)\\
& q_1 q_2 - q_2 q_1 = \Big(0,\; 2\big(\vec{qv_1} \times \vec{qv_2}\big) \Big)
= 2\Big( \vec{qv_1} \times \vec{qv_2}\Big)
\end{array}
\\
\because \vec{qv_1} \times \vec{qv_2} = \frac{1}{2}\Big(q_1 q_2 - q_2 q_1 \Big)
= \frac{1}{2}\Big[q_1,q_2\Big]
$
## 교환자 ($\color{magenta}{commutator}$)
> ### $
\therefore \Big[q_1, q_2 \Big] = 2\big( \vec{qv_1} \times \vec{qv_2} \big)
\quad s.t \;q_1=(0,\vec{qv_1}),q_2=(0,\vec{qv_2})
\\ \therefore \vec{qv_1} \times \vec{qv_2} = \frac{1}{2}\Big[q_1, q_2 \Big]
\quad s.t \; \Big[A,B\Big] = AB - BA
$

```python
# commutator
import IPython as Ipy
Ipy.display.IFrame('https://ko.wikipedia.org/wiki/%EA%B5%90%ED%99%98%EC%9E%90_(%ED%99%98%EB%A1%A0)#%EC%A0%95%EC%9D%98',width=800,height=600)
```

```python
Ipy.display.YouTubeVideo('https://www.youtube.com/watch?v=UaK2q22mMEg&list=PLpzmRsG7u_gr0FO12cBWj-15_e0yqQQ1U&index=4',width=800,height=600)
```

# $\color{red}{\text{Rotation}}$ of special angle
> ### $
q_{r} = (0, q^{r}_v)
\\ \theta = \text{Rotation angle}
\\ q_{r'} = (r0, q^{r'}_v)
\\ q_a = (0, q^a_v) \quad \text{rotation axis}
\\ q_n = (0, q^n_v) \quad \text{rotation axis normal vector}
\\ q^{r',r}_{\looparrowright} = q_{\perp}
= (0, {\large \frac{ q^{r',r}_{\nrightarrow}} { q^{r,r}_{\nrightarrow}}  q^{r}_{v}})
= (0, \; {\large q^{r',}_{\nrightarrow} \frac{q^{r}_v \; q^{r}_v}{q^{r,r}_{\nrightarrow}}})
= (0, \; {\large q^{r'}_v \cdot \frac{q^{r}_v \; q^{r}_v}{q^{r}_v \cdot q^{r}_v}})
= (0, \; {\large \frac{cos(\theta)\;||q^{r'}_v||\; ||q^{r}_v|| \; q^{r}_v}{||q^{r}_v||^2}})
= (0,\; cos(\theta)q^{r}_v)\; \because ||q^{r}_v|| = ||q^{r'}_v||
\\ q^{r',}_{\looparrowright} q^{n,r}_{\circlearrowright}
= (0,{\large \frac{q^{r',}_{\nrightarrow}( q^{n,r}_{\circlearrowright})(q^{n,r}_{\circlearrowright})}{q^{(n,r),(n,r)}_{\circlearrowright \nrightarrow \circlearrowright}}})
= (0,q^{r',}_{\nrightarrow}{\large \frac{ q^{(n,r)(n,r)}_{\circlearrowright \circlearrowright}}{q^{(n,r)\nrightarrow (n,r)}_{\circlearrowright \circlearrowright}}})
= (0,{\large \frac{q^{r'}_v \cdot (q_n \times q_{r})(q_n \times q_{r})}{(q_n \times q_{r})\cdot (q_n \times q_{r})}})
= \cos(90-\theta)(q^n_{v} \times q^{r}_v) = \sin(\theta)(q^n_{v} \times q^{r}_v) \;
\because ||(q^n_v \times q^{r}_v)|| = ||q^{r}_v||,\; ||q^n_v|| = 1
$

---
> # $
\because \color{red}{q^{r'}_v = q^{(r',r)}_{\perp \theta} = q^{(r',r)}_{\looparrowright} + q^{r \looparrowright (n,r)}_{,\circlearrowright}}
$
> ### $
\begin{array}{}
cos(\theta)q^{r}_v + q^{r\looparrowright (n,r)}_{,\circlearrowright}
\end{array} \left \{
\begin{array}{}
\because || q^{r'}_n || = || q^{r}_v || = || q^{(n,r)}_{\circlearrowright} ||,  || q^n_v || = 1  \\
|| q^{(v,(n,r))}_{\looparrowright \circlearrowright}||
= || \sin (\theta) q^{r'}_v || = || \cos(90-\theta)q^{r'}_v || = || \sin(\theta)q^{r}_v || = || sin(\theta) q^{(n,r)}_{\circlearrowright} || \\
\therefore q^{(r',(n,r))}_{\looparrowright, \circlearrowright} = sin(\theta)q^{(n,r)}_{\circlearrowright} \\
\because q^n q^{r} = (-q^{(n,r)}_{\looparrowright}, q^{(n,r)}_{\circlearrowright}) = q^{(n,r)}_{\circlearrowright} \;| q^{(n,r)}_{\nrightarrow} = 0 \\
\therefore q^{(r \looparrowright (n,r)}_{\circlearrowright} = sin(\theta) q^n_v q^{r}_v
\end{array}
\right .
\\
\therefore cos(\theta)q^{r}_v + sin(\theta)q^n_v q^{r}_v
$

> ### $
\cos(\theta)q^r_v + \sin(\theta)q^n_v q^r_v
= \big( \cos(\theta) + \sin(\theta)q^n_v \big) q^r_v
\\ \because q^n = (0, q^n_v), (q^n)^2 = (0,q^n_v)(0,q^n_v) = (-(q^n_v \cdot q^n_v), q^n_v \times q^n_v) = -1
$
> ## $ \color{red}{\therefore \exp^{\theta q^n_v} q^r_v}$

```python
# Rotate q_1 = (1, -1, 0) by \pi/3  radian about the axix q_{axix} = (1,1,1)
# qr = cos(\theta)q1 + sin(\theta)(\hat{qn} x q1)
#    = cos()qv1 + sin()qn*q1 \because (0,qvn)(0,qv1) = (-qvn \dot qv1, qvn x qv1) /cz qvn \perp qv1 \thf (0,qvn x qv1) = qvn x qv1
#    = cos()qv1 + sin()((qvn*qv1)
#    = ( cos() + sin()qvn )qv1
#    = \cz qvn^2 = -1 qvn*qvn = (0,\hat{qvn})(0,\hat{qvn}) = (-(qvn \cdot qvn), qvn x qvn) = (-1, 0) = -1
i = sm.Matrix([[0,-1,0,0],
               [1,0,0,0],
               [0,0,0,-1],
               [0,0,1,0]])

j = sm.Matrix([[0,0,-1,0],
               [0,0,0,1],
               [1,0,0,0],
               [0,-1,0,0]])

k = sm.Matrix([[0,0,0,-1],
               [0,0,-1,0],
               [0,1,0,0],
               [1,0,0,0]])
q0 = i - j
qa = i + j + k
theta = sm.pi/3
qn = qa/qa.col(0).norm()
(sm.cos(theta)*q0 + sm.sin(theta)*qn*q0).col(0)
```

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

```python
# Rotate q_1 = (1, -1, 0) by \pi/3  radian about the axix q_{axix} = (1,1,1)
# qr = cos(\theta)q1 + sin(\theta)(\hat{qn} x q1)
#    = cos()qv1 + sin()qn*q1 \because (0,qvn)(0,qv1) = (-qvn \dot qv1, qvn x qv1) /cz qvn \perp qv1 \thf (0,qvn x qv1) = qvn x qv1
#    = cos()qv1 + sin()((qvn*qv1)
#    = ( cos() + sin()qvn )qv1
#    = \cz qvn^2 = -1 qvn*qvn = (0,\hat{qvn})(0,\hat{qvn}) = (-(qvn \cdot qvn), qvn x qvn) = (-1, 0) = -1
import sympy as sm
q1 = sm.Matrix([0,1,-1,0])
qa = sm.Matrix([0,1,1,1])
theta = sm.pi/3
l9 = 0
for l8 in qa:
    l9 += sm.sqrt(l8)

qn = (1/sm.sqrt(l9))*qa
sm.cos(theta)*q1
nc1 = qn[1:,0].cross(q1[1:,0])

sm.cos(theta)*q1[1:,:] + sm.sin(theta)*nc1
```

$\displaystyle \sqrt{3}$

---
# Rotation in general $\big( \color{red}{\text{Rodrigues Rotation}} = \color{red}{q^{(r',r)}_{\curvearrowleft \theta}}\big)$
> ### $
q^0 = (0,q^0_v)\\
\theta = \text{rotation angle by radian} \\
q^{r}= q^{r,0}_{\curvearrowleft \theta} = (0,q^r_{v}) \\
q^{axis} = (0,q^{axis}_v)\\
q^{n} = (0,q^{axis}_\hat{v})\\
q^{\upuparrows} = (0, q^{(0,q^{n}_{v})}_{\looparrowleft}) \\
q^{\Rsh} = (0, q^{(0-\upuparrows)} ) \\
q^{\Rsh'} = q^{(\Rsh',\Rsh)}_{\perp,\theta} \\
q_{\perp} = (0,q^v_{\perp}), \quad q'_{\perp} = (0,q'^v_{\perp}) \\
q_{\ulcorner} = (0,q^v_{\ulcorner}), \quad  q'_{\ulcorner} = (0,q'^v_{\ulcorner}) \\
q_{\parallel} = (0,q^v_{\parallel})$

> ### $
q_r = q^v_{\parallel} + q'^{v}_{\ulcorner}
\left \{ \begin {array}{}
q'^{v}_{\ulcorner} = {\large \exp^{\theta \hat{q}^v_{axis}}} q^v_{\ulcorner} = {\large \exp^{\theta q^v_n}q^v_{\perp}}\\
q'^{v}_{\perp} = cos\theta\;\big( q^v_{\perp} \big) + sin\theta \;\big(q^v_{\parallel} \times q^v_{\perp}\big)
= cos\theta\;(q^v_0 - q^v_{\parallel}) + sin\theta\;q^v_n \times (q^v_0 - q^v_{\parallel}) = cos\theta \, (q^v_{0} - q^v_{\parallel}) + sin\theta\; q^v_n \times q^v_0 \\
\quad =  cos\theta\; q^v_{0} - cos\theta \, q^v_{\parallel} + sin \theta q^v_n q^v_{0}\\
q^v_{\ulcorner} = \big( q^v_0 - q^v_{\parallel}\big) = q^v_{\perp} = \big(q^v_0 - q^v_{\parallel}\big)
\\ q^v_{\parallel} = {\large q^v_0 \cdot \frac{q^v_{n} q^v_n}{q^v_n \cdot q^v_n}} = q^v_0 \cdot q^v_n q^v_n
\end{array}\right .
$

> ### $
\therefore q^v_r = q^v_{\parallel} + cos\theta\; q^v_{0} - cos\theta\; q^v_{\parallel} + sin\theta\; q^v_n q^v_{0}
\\ \quad = (1 - cos\theta)q^v_{\parallel} +cos\theta\; q^v_0 + sin\theta\; q^v_n q^v_0
\\ \quad = (1 - cos\theta)\big(q^v_0 \cdot q^v_n q^v_n \big) +cos\theta\; q^v_0 + sin\theta\; q^v_n q^v_0
$

> # $
q^v_{\parallel} = \frac{q^v_0 \cdot q^v_{a} q^v_{a}}{q^v_{a}\cdot q^v_{a}}
= q^v_
$

> # $
q^v_{\lceil} = q^v - q^v_{\parallel}
$

> # $
q^v_{r\lceil} = \cos(\theta)q^v_{\lceil} + \sin(\theta)(q^v_{n}\times q^v_{\lceil})
$

```python
import matplotlib.pyplot as plt
%matplotlib widget
import numpy as np
import sympy as sm
fig = plt.figure()
ax = fig.add_subplot(projection='3d')
xi = np.linspace(-1.5,1.5,100)
xi,yi = np.meshgrid(xi,xi)
zi = np.zeros_like(xi)
ax.plot_surface(xi,yi,zi,alpha=0.3)
ax.set_xlim3d(-2,2)
ax.set_ylim3d(-2,2)
ax.set_zlim3d(-2,2)
ax.set_xlabel('x')
ax.quiver([0],[0],[0],[0],[0],[2],color='r',arrow_length_ratio=0.1)
ax.quiver([0],[0],[0],[1],[-1],[1],color='r',arrow_length_ratio=0.1)
ax.quiver([0],[0],[0],[1],[1],[0],color='r',arrow_length_ratio=0.1)
ax.quiver([0],[0],[0],[1],[1],[1],color='r',arrow_length_ratio=0.1)

```

    <mpl_toolkits.mplot3d.art3d.Line3DCollection at 0x7f0cf48f3640>

        Figure

```python

```


# 1d3v electron hybrid code for R/L-waves with stationary ions and dipole background field

## 1. The model
The electron hybrid model for cold electrons with linearized fluid equations and hot kinetic electrons (Vlasov equation) reads

\begin{align}
&\frac{\partial\tilde{\mathbf{j}}_\mathrm{c}}{\partial t}=\epsilon_0\Omega_\mathrm{pe}^2\tilde{\mathbf{E}}+\tilde{\mathbf{j}}_\mathrm{c}\times\mathbf{\Omega}_\mathrm{ce}(\mathbf{x}),\\
&\frac{\partial f_\mathrm{h}}{\partial t}+\mathbf{v}\cdot\nabla f_\mathrm{h}+\frac{q_\mathrm{e}}{m_\mathrm{e}}(\mathbf{E}+\mathbf{v}\times\mathbf{B})\cdot\nabla_\mathbf{v}f_\mathrm{h}=0,\\
&\frac{\partial \tilde{\mathbf{B}}}{\partial t}=-\nabla\times\tilde{\mathbf{E}},\\
&\frac{1}{c^2}\frac{\partial \tilde{\mathbf{E}}}{\partial t}=\nabla\times\tilde{\mathbf{B}}-\mu_0\tilde{\mathbf{j}}_\mathrm{c}-\mu_0q_\mathrm{e}\int\mathbf{v}\tilde{f}_\mathrm{h}\,\mathrm{d}^3\mathbf{v},
\end{align}

where $\mathbf{\Omega}_\mathrm{ce}(\mathbf{x})=q_\mathrm{e}\mathbf{B}_0(\mathbf{x})/m_\mathrm{e}$ is the oriented electron cyclotron frequency corresponding to the background field $\mathbf{B}_0=\mathbf{B}_0(\mathbf{x})$ and $\Omega_{\mathrm{pe}}^2=n_{\mathrm{c}0}e^2/\epsilon_0m_\mathrm{e}$ the plasma frequency of the cold electrons. Here, only wave propagation parallel to the background magnetic field $\mathbf{B}_0=\mathbf{B}_0(\mathbf{x})$ is considered, i.e. $\textbf{k}=k\textbf{e}_z$. Therefore the nabla operator is simply $\nabla=\textbf{e}_z\partial_z$. The background magnetic field is a parabolically shaped.

We use homogeneous Dirichlet boundary conditions with a masking function for $(\tilde{E}_x,\tilde{E}_y,\tilde{j}_{\mathrm{c}x},\tilde{j}_{\mathrm{c}y})$ and no boundary conditions for $(\tilde{B}_x,\tilde{B}_y)$.

```python
import numpy as np
import scipy as sc
import matplotlib.pyplot as plt

import psydac.core.interface as inter

import time

import utilitis_opt as utils_opt
import utilitis_pic

#====================================================================================
#  calling epyccel
#====================================================================================
from pyccel.epyccel import epyccel
utils_pic_fast = epyccel(utilitis_pic)
print('pyccelization of pic functions done!')
#====================================================================================

#===== saving data? (save = 1: yes, save = 0: no). If yes, name directory ===========
save = 0
title = 'test.txt'
#====================================================================================

#===== save only every saving_step-th time step =====================================
saving_step = 1
#====================================================================================

#===== physical parameters ==========================================================
eps0 = 1.0                         # vacuum permittivity
mu0 = 1.0                          # vacuum permeability
c = 1.0                            # speed of light
qe = -1.0                          # electron charge
me = 1.0                           # electron mass
B0z = 1.0                          # minimum of background magnetic field in z-direction
wce = qe*B0z/me                    # electron cyclotron frequency
wpe = 2*np.abs(wce)                # cold electron plasma frequency
nuh = 2e-3                         # ratio of cold/hot electron densities (nh/nc)
nh = nuh*wpe**2                    # hot electron density
wpar = 0.1*c                       # parallel thermal velocity of energetic particles
wperp = 0.1*c                      # perpendicular thermal velocity of energetic particles

xi = 0.                            # inhomogeneity factor of background magnetic field

bcs_d = 1                          # damping of wave fields at boundaries? (1: yes, 0: no)
bcs_g = 1                          # field line dependence of initial distribution function? (1: yes, 0: no)
#====================================================================================

#===== initial conditions ===========================================================
k = 2.                             # wavenumber of initial wave field perturbations
amp = 1e-4                         # amplitude of initial wave field perturbations
eps = 0.                           # amplitude of spatial pertubation of initial distribution function

Ex0 = lambda z : 0*z               # initial Ex
Ey0 = lambda z : 0*z               # initial Ey
Bx0 = lambda z : 0*z               # initial Bx
By0 = lambda z : 0*z               # initial By
jx0 = lambda z : 0*z               # initial jcx
jy0 = lambda z : 0*z               # initial jcy
#====================================================================================

#===== numerical parameters =========================================================
Lz = 80.                           # length of z-domain
Nel = 512                          # number of elements z-direction
T = 300                            # simulation time
dt = 0.05                          # time step
p = 3                              # degree of B-spline basis functions in V0
Np = np.int(1e5)                   # number of markers
control = 1                        # control variate for noise reduction? (1: yes, 0: no)
time_integr = 0                    # do time integration? (1 : yes, 0: no)

Ld = 0.05*Lz                       # length of damping region at each end
#====================================================================================

#====== create parameter list =======================================================
pa = np.zeros(1*(Nel + p - 1) + 5)

pa[0]  = eps0
pa[1]  = mu0
pa[2]  = c
pa[3]  = qe
pa[4]  = me
pa[5]  = B0z
pa[6]  = wce
pa[7]  = wpe
pa[8]  = nuh
pa[9]  = nh
pa[10] = wpar
pa[11] = wperp
pa[12] = k
pa[13] = amp
pa[14] = eps
pa[15] = Lz
pa[16] = Nel
pa[17] = T
pa[18] = dt
pa[19] = p
pa[20] = Np
pa[21] = control
pa[22] = saving_step

pa[23] = xi
pa[24] = Ld

pa[29] = bcs_d
pa[30] = bcs_g
#====================================================================================

#===== discretization of spatial domain =============================================
dz   = Lz/Nel                                # element size
el_b = np.linspace(0, Lz, Nel + 1)           # element boundaries

Nbase0   = Nel + p                           # total number of basis functions in V0
Nbase0_0 = Nbase0 - 2                        # number of degrees of freedom in V1

Nbase1   = Nbase0 - 1                        # total number of basis functions in V1
Nbase1_0 = Nbase1                            # number of degrees of freedom in V1
#====================================================================================

#===== some diagnostic values =======================================================
Eh_eq = Lz*nh*me/2*(wpar**2 + 2*wperp**2)    # equilibrium energetic electron energy

en_E = np.array([])                          # electric field energy
en_B = np.array([])                          # magnetic field energy
en_C = np.array([])                          # cold plasma energy
en_H = np.array([])                          # energetic electron energy
#====================================================================================

#===== background field in z-direction ==============================================
B_background_z = lambda z : B0z*(1 + xi*(z - Lz/2)**2)
#====================================================================================

#===== initial energetic electron distribution function =============================
def fh0(z, vx, vy, vz):

    xiB = 1 - B0z/B_background_z(z)
    xiz = 1 + (wperp**2/wpar**2 - 1)*xiB*bcs_g

    return (1 + eps*np.cos(k*z))*nh/((2*np.pi)**(3/2)*wpar*wperp**2)*np.exp(-vz**2/(2*wpar**2) - xiz*(vx**2 + vy**2)/(2*wperp**2))
#====================================================================================

#===== Maxwellian for control variate ===============================================
maxwell = lambda vx, vy, vz : nh/((2*np.pi)**(3/2)*wpar*wperp**2)*np.exp(-vz**2/(2*wpar**2) - (vx**2 + vy**2)/(2*wperp**2))
#====================================================================================

#===== sampling distribution for initial markers ====================================
g_sampling = lambda vx, vy, vz : 1/((2*np.pi)**(3/2)*wpar*wperp**2)*np.exp(-vz**2/(2*wpar**2) - (vx**2 + vy**2)/(2*wperp**2))*1/Lz
#====================================================================================

#===== masking function to damp wave fields near boundaries =========================
def damp(z):

    if z <= Ld:
        return np.sin(np.pi*z/(2*Ld))
    elif z >= Lz - Ld:
        return np.sin(np.pi*(Lz - z)/(2*Ld))
    else:
        return 1.0
#====================================================================================

#===== spline knot vector, global mass matrices (in V0 and V1) and gradient matrix ==
Tz = inter.make_open_knots(p, Nbase0)*Lz
tz = Tz[1:-1]

M0, C0 = utils_opt.matrixAssembly_V0(p, Nbase0, Tz, False)
M1 = utils_opt.matrixAssembly_V1(p, Nbase0, Tz, False)
Mb = utils_opt.matrixAssembly_backgroundField(p, Nbase0, Tz, False, B_background_z)

G = utils_opt.GRAD_1d(p, Nbase0, False)

print('matrix assembly done!')
#====================================================================================

#===== reserve memory for unknowns ==================================================
ex = np.empty(Nbase0)
ey = np.empty(Nbase0)
bx = np.empty(Nbase1)
by = np.empty(Nbase1)
yx = np.empty(Nbase0)
yy = np.empty(Nbase0)

uj = np.empty(4*Nbase0_0 + 2*Nbase1_0)

z_old = np.empty(Np)
#====================================================================================

#===== initial coefficients with commuting projectors ===============================
ex[:] = utils_opt.PI_0_1d(Ex0, p, Nbase0, Tz, False)
ey[:] = utils_opt.PI_0_1d(Ey0, p, Nbase0, Tz, False)
bx[:] = utils_opt.PI_1_1d(Bx0, p, Nbase0, Tz, False)
by[:] = utils_opt.PI_1_1d(By0, p, Nbase0, Tz, False)
yx[:] = utils_opt.PI_0_1d(jx0, p, Nbase0, Tz, False)
yy[:] = utils_opt.PI_0_1d(jy0, p, Nbase0, Tz, False)

uj[:] = np.concatenate((ex[1:-1], ey[1:-1], bx, by, yx[1:-1], yy[1:-1]))

print('projection of initial fields done!')
#====================================================================================

#===== construct block matrices for field update ====================================
ZERO_00 = np.zeros((Nbase0_0, Nbase0_0))
ZERO_01 = np.zeros((Nbase0_0, Nbase1_0))
ZERO_11 = np.zeros((Nbase1_0, Nbase1_0))

A1 = np.diag(np.ones(4*Nbase0_0 + 2*Nbase1_0))

A1[0:Nbase0_0, 0:Nbase0_0] = M0
A1[Nbase0_0:2*Nbase0_0, Nbase0_0:2*Nbase0_0] = M0

A1[2*Nbase0_0 + 2*Nbase1_0:3*Nbase0_0 + 2*Nbase1_0, 2*Nbase0_0 + 2*Nbase1_0:3*Nbase0_0 + 2*Nbase1_0] = M0
A1[3*Nbase0_0 + 2*Nbase1_0:4*Nbase0_0 + 2*Nbase1_0, 3*Nbase0_0 + 2*Nbase1_0:4*Nbase0_0 + 2*Nbase1_0] = M0

A2 = np.block([[ZERO_00, ZERO_00, ZERO_01, c**2*np.dot(G.T, M1), -mu0*c**2*M0, ZERO_00], [ZERO_00, ZERO_00, -c**2*np.dot(G.T, M1), ZERO_01, ZERO_00, -mu0*c**2*M0], [ZERO_01.T, G, ZERO_11, ZERO_11, ZERO_01.T, ZERO_01.T], [-G, ZERO_01.T, ZERO_11, ZERO_11, ZERO_01.T, ZERO_01.T], [eps0*wpe**2*M0, ZERO_00, ZERO_01, ZERO_01, ZERO_00, qe/me*Mb], [ZERO_00, eps0*wpe**2*M0, ZERO_01, ZERO_01, -qe/me*Mb, ZERO_00]])

LHS = sc.sparse.csc_matrix(A1 - 1/2*dt*A2)
RHS = sc.sparse.csc_matrix(A1 + 1/2*dt*A2)

LU = sc.sparse.linalg.splu(LHS)

print('LU factorization done!')

if bcs_d == 1:
    grev = inter.compute_greville(p, Nbase0, Tz)
    coll = inter.collocation_matrix(p, Nbase0, Tz, grev)[1:-1, 1:-1]
    gi = np.zeros(Nbase0)

    for i in range(Nbase0):
        gi[i] = damp(grev[i])

    Gi = np.diag(gi[1:-1])

    DAMP = np.dot(np.dot(np.linalg.inv(coll), Gi), coll)
else:
    DAMP = np.identity(Nbase0_0)

DAMP_block = sc.linalg.block_diag(DAMP, DAMP, np.identity(Nbase1_0), np.identity(Nbase1_0), DAMP, DAMP)

print('damping assembly done!')
#====================================================================================

#===== create particles (z,vx,vy,vz,wk) and sample according to sampling distribution
particles = np.zeros((Np, 5), order='F')
particles[:, 0] = np.random.rand(Np)*Lz
particles[:, 1] = np.random.randn(Np)*wperp
particles[:, 2] = np.random.randn(Np)*wperp
particles[:, 3] = np.random.randn(Np)*wpar

jh = np.zeros(2*Nbase0)
Fh = np.zeros(4*Nbase0_0 + 2*Nbase1_0)
#====================================================================================

#===== parameters for control variate ===============================================
g0 = g_sampling(particles[:, 1], particles[:, 2], particles[:, 3])
w0 = fh0(particles[:, 0], particles[:, 1], particles[:, 2], particles[:, 3])/g_sampling(particles[:, 1], particles[:, 2], particles[:, 3])
#====================================================================================

#===== initialize velocities by pushing back by -dt/2 and compute weights ===========
timea = time.time()

z_old[:] = particles[:, 0]

utils_pic_fast.borisGem_bc_2(particles, -dt/2, qe, me, Tz, tz, p, ex, ey, bx, by, B0z, xi, Lz)

particles[:, 0] = z_old
particles[:, 4] = w0 - control*maxwell(particles[:, 1], particles[:, 2], particles[:, 3])/g0

timeb = time.time()
print('time for particle push: ' + str(timeb - timea))
#====================================================================================

#===== test timing for hot current computation ======================================
timea = time.time()

utils_pic_fast.hotCurrent_bc_2(particles[:, 1:3], particles[:, 0], particles[:, 4], Tz, p, qe, jh)

timeb = time.time()
print('time for hot current computation: ' + str(timeb - timea))
#====================================================================================

#===== test timing for linear solver ================================================
timea = time.time()

LU.solve(RHS.dot(uj) + dt*Fh)

timeb = time.time()
print('time for solving linear system: ' + str(timeb - timea))
#====================================================================================

#===== time integration by a time step dt ===========================================
def update():

    # ... save old positions
    z_old[:] = particles[:, 0]
    # ...

    # ... update particle velocities from n-1/2 to n+1/2 with fields at time n and positions from n to n+1 with velocities at n+1/2
    utils_pic_fast.borisGem_bc_2(particles, dt, qe, me, Tz, tz, p, ex, ey, bx, by, B0z, xi, Lz)
    # ...

    # ... update weights with control variate
    particles[:, 4] = w0 - control*maxwell(particles[:, 1], particles[:, 2], particles[:, 3])/g0
    # ...

    # ... compute hot electron current densities
    utils_pic_fast.hotCurrent_bc_2(particles[:, 1:3], 1/2*(z_old + particles[:, 0]), particles[:, 4], Tz, p, qe, jh)
    # ...

    # ... assemble right-hand side of weak formulation
    Fh[:Nbase0_0] = -c**2*mu0*jh[2:-2][0::2]
    Fh[Nbase0_0:2*Nbase0_0] = -c**2*mu0*jh[2:-2][1::2]
    # ...

    # ... time integration of E, B, jc from n to n+1 with Crank-Nicolson method (use hot current density at n+1/2)
    uj[:] = np.dot(DAMP_block, LU.solve(RHS.dot(uj) + dt*Fh))

    ex[:] = np.array([0] + list(uj[:Nbase0_0]) + [0])
    ey[:] = np.array([0] + list(uj[Nbase0_0:2*Nbase0_0]) + [0])
    bx[:] = uj[2*Nbase0_0:2*Nbase0_0 + Nbase1_0]
    by[:] = uj[2*Nbase0_0 + Nbase1_0:2*Nbase0_0 + 2*Nbase1_0]
    yx[:] = np.array([0] + list(uj[2*Nbase0_0 + 2*Nbase1_0:3*Nbase0_0 + 2*Nbase1_0]) + [0])
    yy[:] = np.array([0] + list(uj[3*Nbase0_0 + 2*Nbase1_0:4*Nbase0_0 + 2*Nbase1_0]) + [0])
    # ...
#====================================================================================

#===== create data file and save parameters (first row), initial fields and energies (second row)
if save == 1:
    file = open(title, 'ab')
    np.savetxt(file, np.reshape(pa, (1, 1*Nbase1_0 + 5)), fmt = '%1.10e')

en_E = np.append(en_E, eps0/2*(np.dot(ex[1:-1], np.dot(M0, ex[1:-1])) + np.dot(ey[1:-1], np.dot(M0, ey[1:-1]))))
en_B = np.append(en_B, eps0/(2*mu0)*(np.dot(bx, np.dot(M1, bx)) + np.dot(by, np.dot(M1, by))))
en_C = np.append(en_C, 1/(2*eps0*wpe**2)*(np.dot(yx[1:-1], np.dot(M0, yx[1:-1])) + np.dot(yy[1:-1], np.dot(M0, yy[1:-1]))))
en_H = np.append(en_H, me/(2*Np)*np.dot(particles[:, 4], particles[:, 1]**2 + particles[:, 2]**2 + particles[:, 3]**2) + control*Eh_eq)

bx_save = np.copy(bx)

if save == 1:
    data = np.append(bx, np.array([en_E[-1], en_B[-1], en_C[-1], en_H[-1], 0.]))
    np.savetxt(file, np.reshape(data, (1, 1*Nbase1_0 + 5)), fmt = '%1.10e')
#====================================================================================

#===== time integration =============================================================
if time_integr == 1:

    print('start time integration! (number of time steps : ' + str(int(T/dt)) + ')')
    time_step = 0

    while True:

        try:
            if time_step*dt >= T:
                if save == 1:
                    file.close()
                break

            if time_step%50 == 0:
                print('time steps finished: ' + str(time_step))

            update()

            if time_step%saving_step == 0:

                # ... add data to file
                en_E = np.append(en_E, eps0/2*(np.dot(ex[1:-1], np.dot(M0, ex[1:-1])) + np.dot(ey[1:-1], np.dot(M0, ey[1:-1]))))
                en_B = np.append(en_B, eps0/(2*mu0)*(np.dot(bx, np.dot(M1, bx)) + np.dot(by, np.dot(M1, by))))
                en_C = np.append(en_C, 1/(2*eps0*wpe**2)*(np.dot(yx[1:-1], np.dot(M0, yx[1:-1])) + np.dot(yy[1:-1], np.dot(M0, yy[1:-1]))))
                en_H = np.append(en_H, me/(2*Np)*np.dot(particles[:, 4], particles[:, 1]**2 + particles[:, 2]**2 + particles[:, 3]**2) + control*Eh_eq)

                bx_save = np.vstack((bx_save, np.copy(bx)))

                if save == 1:
                    data = np.append(bx, np.array([en_E[-1], en_B[-1], en_C[-1], en_H[-1], (time_step + 1)*dt]))
                    np.savetxt(file, np.reshape(data, (1, 1*Nbase1_0 + 5)), fmt = '%1.10e')
                # ...

            time_step += 1
        except KeyboardInterrupt:
            print('Pausing...  (Hit ENTER to continue, type quit to exit.)')

            if save == 1:
                file.close()

            try:
                response = input()
                if response == 'quit':
                    break
                print('Resuming...')

                if save == 1:
                    file = open(title, 'ab')

            except KeyboardInterrupt:
                print('Resuming...')

                if save == 1:
                    file = open(title, 'ab')

                continue

    if save == 1:
        file.close()
#====================================================================================
```

    pyccelization of pic functions done!
    matrix assembly done!
    projection of initial fields done!
    LU factorization done!
    damping assembly done!
    time for particle push: 0.06396245956420898
    time for hot current computation: 0.0200045108795166
    time for solving linear system: 0.0021309852600097656

```python
from matplotlib.colors import LogNorm
from matplotlib.patches import ConnectionPatch

Nt = len(en_B) - 1

f3 = plt.figure()
f3.set_figheight(5)
f3.set_figwidth(12)

fontsize = 24
#plt.rc('text', usetex = False)
#plt.rc('font', family = 'serif')
plt.rcParams.update({'font.size': fontsize})
plt.rc('xtick', labelsize = fontsize)
plt.rc('ytick', labelsize = fontsize)

w = np.linspace(0, Nt, Nt + 1) - Nt/2
w = 2*np.pi/T*w

ks = np.linspace(0, Nbase1_0 - 1, Nbase1_0) - (Nbase1_0 - 1)/2
ks = 2*np.pi/Lz*ks

Bxkw = np.fft.fft2(bx_save)
K, W = np.meshgrid(ks, w)
Bkw = np.fft.fftshift(Bxkw)
Bkw_plot = np.abs(Bkw)

ax1 = plt.subplot(121)

ticks = np.power(10, np.linspace(-8, 0, 5))
lvls = np.logspace(-8, 0, 60)
plt.contourf(K, W, Bkw_plot/Bkw_plot.max(), cmap = 'jet', norm = LogNorm(), levels = lvls)
plt.plot(np.linspace(0, 8, 10), np.zeros(10), 'k')
plt.plot(np.linspace(0, 8, 10), np.ones(10)*6, 'k')
plt.plot(np.zeros(10), np.linspace(0, 6, 10), 'k')
plt.plot(np.ones(10)*8, np.linspace(0, 6, 10), 'k')
plt.ylim((-20, 20))
plt.xlabel(r'$kc/ |\Omega_\mathrm{ce}|$')
plt.ylabel(r'$\omega_\mathrm{r}/ |\Omega_\mathrm{ce}|$')
plt.title(r'$|\hat{B}_x(k,\omega_\mathrm{r})|/|\hat{B}_x(k,\omega_\mathrm{r})|_\mathrm{max}$', fontsize = fontsize)

# ... analytical dispersion relation
k1 = np.linspace(0.1, 8.0, 40)

w1_1 = np.zeros(40)
w1_2 = np.zeros(40)
w1_3 = np.zeros(40)

w1_1[0] = 0.0001
w1_2[0] = 1.5001
w1_3[0] = 2.5001

for i in range(0, 40):

    if i == 0:
        w1_1[i] = utils_opt.solveDispersionCold(k1[i], +1, c, wce, wpe, w1_1[i], 1e-6, 100)[0]
        w1_2[i] = utils_opt.solveDispersionCold(k1[i], -1, c, wce, wpe, w1_2[i], 1e-6, 100)[0]
        w1_3[i] = utils_opt.solveDispersionCold(k1[i], +1, c, wce, wpe, w1_3[i], 1e-6, 100)[0]

    else:
        w1_1[i] = utils_opt.solveDispersionCold(k1[i], +1, c, wce, wpe, w1_1[i-1], 1e-6, 100)[0]
        w1_2[i] = utils_opt.solveDispersionCold(k1[i], -1, c, wce, wpe, w1_2[i-1], 1e-6, 100)[0]
        w1_3[i] = utils_opt.solveDispersionCold(k1[i], +1, c, wce, wpe, w1_3[i-1], 1e-6, 100)[0]
# ...

ax2 = plt.subplot(122)

ticks = np.power(10, np.linspace(-8, 0, 5))
lvls = np.logspace(-8, 0, 60)
plt.contourf(K, W, Bkw_plot/Bkw_plot.max(), cmap = 'jet', norm = LogNorm(), levels = lvls)
plt.xlim((0, 8))
plt.ylim((0, 6))
plt.xticks([], [])
plt.yticks([], [])
plt.plot(k1, w1_1, 'k--')
plt.plot(k1, w1_2, 'k--')
plt.plot(k1, w1_3, 'k--')
plt.subplots_adjust(hspace = 0.3)
plt.tight_layout()

con1 = ConnectionPatch(xyA = (0, 6), xyB = (0, 6), coordsA = "data", coordsB = "data", axesA = ax2, axesB = ax1, color = "black", linewidth = 1)
con2 = ConnectionPatch(xyA = (0, 0), xyB = (0, 0), coordsA = "data", coordsB = "data", axesA = ax2, axesB = ax1, color = "black", linewidth = 1)
con3 = ConnectionPatch(xyA = (0, 4.3), xyB = (8, 6), coordsA = "data", coordsB = "data", axesA = ax2, axesB = ax1, color = "black", linewidth = 1)
con4 = ConnectionPatch(xyA = (0, 2.4), xyB = (8, 0), coordsA = "data", coordsB = "data", axesA = ax2, axesB = ax1, color = "black", linewidth = 1)

ax2.add_artist(con1)
ax2.add_artist(con2)
ax2.add_artist(con3)
ax2.add_artist(con4)

#plt.title('B-splines: Strang', fontsize = fontsize)
#plt.savefig('Bsplines_Strang_spectrum.png', bbox_inches = 'tight')
```

```python
data = np.loadtxt('test.txt')
```

```python
en_H = data[1:, -2]
en_C = data[1:, -3]
en_B = data[1:, -4]
en_E = data[1:, -5]

en_tot = en_H + en_C + en_B + en_E

Nt = len(en_B) - 1
```

```python
tn = np.linspace(0., Nt*0.1, Nt + 1)
plt.semilogy(tn, en_B/327.7)
plt.xlim((0, 10000))
plt.ylim((1e-6, 1e-3))

t2 = np.linspace(0, 550, 100)
plt.semilogy(t2, 2e-6*np.exp(gamma*2*t2), 'k--', label = 'expected growth')
```

```python
omega = utils_opt.solveDispersionHybrid(2.88, +1, c, wce, 5.0, 0.2, 0.53, 6e-3, 0.25 + 0j, 1e-8)[0]
omega_r = np.real(omega)
gamma = np.imag(omega)
```

```python
100*gamma
```

    1.2965398116017401

```python
omega_r
```

    0.2467654207197003

```python
Lz = 6.

# ... unit test boris pusher
test_particle = np.zeros((2, 5), order='F')

test_particle[:, 0] = Lz/2 - 2.62
test_particle[:, 1] = 0.117
test_particle[:, 3] = 0.0795

xi_test = 0.862

Nt = int(150/0.05)

tn = np.linspace(0., 150., Nt + 1)

ex = np.zeros(Nbase0)
ey = np.zeros(Nbase0)
bx = np.zeros(Nbase1)
by = np.zeros(Nbase1)

z_old = np.copy(test_particle[:, 0])
utils_pic_fast.borisGem_bc_2(test_particle, -0.05/2, qe, me, Tz, tz, p, ex, ey, bx, by, B0z, xi_test, Lz)
test_particle[:, 0] = z_old

positions = np.empty(Nt + 1)
positions[0] = test_particle[0, 0]

for i in range(Nt):
    utils_pic_fast.borisGem_bc_2(test_particle, 0.05, qe, me, Tz, tz, p, ex, ey, bx, by, B0z, xi_test, Lz)
    positions[i + 1] = test_particle[0, 0]
# ...
```

```python
plt.plot(tn, positions)
plt.ylim((Lz/2 - 4, Lz/2 + 6))
```

```python

```