# 02 - Reverse Time Migration

This notebook is the second in a series of tutorial highlighting various aspects of seismic inversion based on Devito operators. In this second example we aim to highlight the core ideas behind seismic inversion, where we create an image of the subsurface from field recorded data. This tutorial follows on the modelling tutorial and will reuse the modelling operator and velocity model.

## Imaging requirement

Seismic imaging relies on two known parameters:

- **Field data** - or also called **recorded data**. This is a shot record corresponding to the true velocity model. In practice this data is acquired as described in the first tutorial. In order to simplify this tutorial we will generate synthetic field data by modelling it with the **true velocity model**.

- **Background velocity model**. This is a velocity model that has been obtained by processing and inverting the field data. We will look at this methods in the following tutorial as it relies on the method we are describing here. This velocity model is usually a **smooth version** of the true velocity model.

## Imaging computational setup

In this tutorial, we will introduce the back-propagation operator. This operator simulates the adjoint wave-equation, that is a wave-equation solved in a reversed time order. This time reversal led to the naming of the method we present here, called Reverse Time Migration. The notion of adjoint in exploration geophysics is fundamental as most of the wave-equation based imaging and inversion methods rely on adjoint based optimization methods.

## Notes on the operators

As we already describe the creation of a forward modelling operator, we will use a thin wrapper function instead. This wrapper is provided by a utility class called `AcousticWaveSolver`, which provides all the necessary operators for seismic modeling, imaging and inversion. The `AcousticWaveSolver` provides a more concise API for common wave propagation operators and caches the the Devito `Operator` objects to avoid unnecessary recompilation. However, any newly introduced operators will be fully described and only used from the wrapper in the next tutorials.

As before we initialize printing and import some utilities. We also raise the Devito log level to avoid excessive logging for repeated operator invocations.

```python
import numpy as np
%matplotlib inline

from devito import configuration
configuration['log-level'] = 'WARNING'
```

## Computational considerations

Seismic inversion algorithms are generally very computationally demanding and require a large amount of memory to store the forward wavefield. In order to keep this tutorial as light-weight as possible we are using a very simple
velocity model that requires low temporal and special resolution. For a more realistic model, a second set of preset parameters for a reduced version of the 2D Marmousi data set [1] is provided below in comments. This can be run to create some more realistic subsurface images. However, this second present is more computationally demanding and requires a slightly more powerful workstation.

```python
# Configure model presets
from examples.seismic import demo_model

# Enable model presets here:
preset = 'twolayer-isotropic'  # A simple but cheap model (recommended)
# preset = 'marmousi2d'  # A larger more realistic model

# Standard preset with a simple two-layer model
if preset == 'twolayer-isotropic':
    def create_model(grid=None):
        return demo_model('twolayer-isotropic', origin=(0., 0.), shape=(101, 101),
                          spacing=(10., 10.), nbpml=20, grid=grid, ratio=2)
    filter_sigma = (1, 1)
    nshots = 21
    nreceivers = 101
    t0 = 0.
    tn = 1000.  # Simulation last 1 second (1000 ms)
    f0 = 0.010  # Source peak frequency is 10Hz (0.010 kHz)

# A more computationally demanding preset based on the 2D Marmousi model
if preset == 'marmousi2d-isotropic':
    def create_model(grid=None):
        return demo_model('marmousi2d-isotropic', data_path='../../../../opesci-data/',
                          grid=grid)
    filter_sigma = (6, 6)
    nshots = 301  # Need good covergae in shots, one every two grid points
    nreceivers = 601  # One recevier every grid point
    t0 = 0.
    tn = 3500.  # Simulation last 3.5 second (3500 ms)
    f0 = 0.025  # Source peak frequency is 25Hz (0.025 kHz)
```

# True and smooth velocity models

First, we create the model data for the "true" model from a given demonstration preset. This model represents the subsurface topology for the purposes of this example and we will later use it to generate our synthetic data readings. We also generate a second model and apply a smoothing filter to it, which represents our initial model for the imaging algorithm. The perturbation between these two models can be thought of as the image we are trying to recover.

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_velocity, plot_perturbation
from scipy import ndimage

# Create true model from a preset
model = create_model()

# Create initial model and smooth the boundaries
model0 = create_model(grid=model.grid)
model0.vp = ndimage.gaussian_filter(model0.vp, sigma=filter_sigma, order=0)

# Plot the true and initial model and the perturbation between them
plot_velocity(model)
plot_velocity(model0)
plot_perturbation(model0, model)
```

## Acquisition geometry

Next we define the positioning and the wave signal of our source and the location of our receivers. To generate the wavelet for our source we require the discretized values of time that we are going to use to model a single "shot",,
which again depends on the grid spacing used in our model. For consistency this initial setup will look exactly as in the previous modelling tutorial, although we will vary the position of our source later on during the actual imaging algorithm.

```python
#NBVAL_IGNORE_OUTPUT
# Define acquisition geometry: source
from examples.seismic import AcquisitionGeometry

# First, position source centrally in all dimensions, then set depth
src_coordinates = np.empty((1, 2))
src_coordinates[0, :] = np.array(model.domain_size) * .5
src_coordinates[0, -1] = 20.  # Depth is 20m

# Define acquisition geometry: receivers

# Initialize receivers for synthetic and imaging data
rec_coordinates = np.empty((nreceivers, 2))
rec_coordinates[:, 0] = np.linspace(0, model.domain_size[0], num=nreceivers)
rec_coordinates[:, 1] = 30.

# Geometry

geometry = AcquisitionGeometry(model, rec_coordinates, src_coordinates, t0, tn, f0=.010, src_type='Ricker')
# We can plot the time signature to see the wavelet
geometry.src.show()
```

# True and smooth data

We can now generate the shot record (receiver readings) corresponding to our true and initial models. The difference between these two records will be the basis of the imaging procedure.

For this purpose we will use the same forward modelling operator that was introduced in the previous tutorial, provided by the `WaveSolver` utility class. This object instantiates a set of pre-defined operators according to an initial definition of the acquisition geometry, consisting of source and receiver symbols. The solver objects caches the individual operators and provides a slightly more high-level API that allows us to invoke the modelling modelling operators from the initial tutorial in a single line. In the following cells we use this to generate shot data by only specifying the respective model symbol `m` to use, and the solver will create and return a new `Receiver` object the represents the readings at the previously defined receiver coordinates.

```python
# Compute synthetic data with forward operator
from examples.seismic.acoustic import AcousticWaveSolver

solver = AcousticWaveSolver(model, geometry, space_order=4)
true_d , _, _ = solver.forward(m=model.m)
```

```python
# Compute initial data with forward operator
smooth_d, _, _ = solver.forward(m=model0.m)
```

```python
#NBVAL_IGNORE_OUTPUT
# Plot shot record for true and smooth velocity model and the difference
from examples.seismic import plot_shotrecord

plot_shotrecord(true_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data, model, t0, tn)
plot_shotrecord(smooth_d.data - true_d.data, model, t0, tn)
```

# Imaging with back-propagation

As we explained in the introduction of this tutorial, this method is based on back-propagation.

## Adjoint wave equation

If we go back to the modelling part, we can rewrite the simulation as a linear system solve:

\begin{equation}
\mathbf{A}(\mathbf{m}) \mathbf{u} = \mathbf{q}
\end{equation}

where $\mathbf{m}$ is the discretized square slowness, $\mathbf{q}$ is the discretized source and $\mathbf{A}(\mathbf{m})$ is the discretized wave-equation. The discretized wave-equation matricial representation is a lower triangular matrix that can be solve with forward substitution. The pointwise writing or the forward substitution leads to the time-stepping stencil.

On a small problem one could form the matrix explicitly and transpose it to obtain the adjoint discrete wave-equation:

\begin{equation}
\mathbf{A}(\mathbf{m})^T \mathbf{v} = \delta \mathbf{d}
\end{equation}

where $\mathbf{v}$ is the discrete **adjoint wavefield** and  $\delta \mathbf{d}$ is the data residual defined as the difference between the field/observed data and the synthetic data $\mathbf{d}_s = \mathbf{P}_r \mathbf{u}$. In our case we derive the discrete adjoint wave-equation from the discrete forward wave-equation to get its stencil.

## Imaging

Wave-equation based imaging relies on one simple concept:

- If the background velocity model is cinematically correct, the forward wavefield $\mathbf{u}$ and the adjoint wavefield $\mathbf{v}$ meet at the reflectors position at zero time offset.

The sum over time of the zero time-offset correlation of these two fields then creates an image of the subsurface. Mathematically this leads to the simple imaging condition:

\begin{equation}
  \text{Image} = \sum_{t=1}^{n_t} \mathbf{u}[t] \mathbf{v}[t]
\end{equation}

In the following tutorials we will describe a more advanced imaging condition that produces shaper and more accurate results.

## Operator

We will now define the imaging operator that computes the adjoint wavefield $\mathbf{v}$ and correlates it with the forward wavefield $\mathbf{u}$. This operator essentially consist of three components:
* Stencil update of the adjoint wavefield `v`
* Injection of the data residual at the adjoint source (forward receiver) location
* Correlation of `u` and `v` to compute the image contribution at each timestep

```python
# Define gradient operator for imaging
from devito import TimeFunction, Operator, Eq, solve
from examples.seismic import PointSource

def ImagingOperator(model, image):
    # Define the wavefield with the size of the model and the time dimension
    v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4)

    u = TimeFunction(name='u', grid=model.grid, time_order=2, space_order=4,
                     save=geometry.nt)

    # Define the wave equation, but with a negated damping term
    eqn = model.m * v.dt2 - v.laplace - model.damp * v.dt

    # Use `solve` to rearrange the equation into a stencil expression
    stencil = Eq(v.backward, solve(eqn, v.backward))

    # Define residual injection at the location of the forward receivers
    dt = model.critical_dt
    residual = PointSource(name='residual', grid=model.grid,
                           time_range=geometry.time_axis,
                           coordinates=geometry.rec_positions)
    res_term = residual.inject(field=v, expr=residual * dt**2 / model.m)

    # Correlate u and v for the current time step and add it to the image
    image_update = Eq(image, image - u * v)

    return Operator([stencil] + res_term + [image_update],
                    subs=model.spacing_map)
```

## Implementation of the imaging loop

As we just explained, the forward wave-equation is solved forward in time while the adjoint wave-equation is solved in a reversed time order. Therefore, the correlation of these two fields over time requires to store one of the two fields. The computational procedure for imaging follows:

- Simulate the forward wave-equation with the background velocity model to get the synthetic data and save the full wavefield $\mathbf{u}$
- Compute the data residual
- Back-propagate the data residual and compute on the fly the image contribution at each time step.

This procedure is applied to multiple source positions (shots) and summed to obtain the full image of the subsurface. We can first visualize the varying locations of the sources that we will use.

```python
#NBVAL_IGNORE_OUTPUT

# Prepare the varying source locations
source_locations = np.empty((nshots, 2), dtype=np.float32)
source_locations[:, 0] = np.linspace(0., 1000, num=nshots)
source_locations[:, 1] = 30.

plot_velocity(model, source=source_locations)
```

```python
# Run imaging loop over shots
from devito import Function, clear_cache

# Create image symbol and instantiate the previously defined imaging operator
image = Function(name='image', grid=model.grid)
op_imaging = ImagingOperator(model, image)

# Create a wavefield for saving to avoid memory overload
u0 = TimeFunction(name='u', grid=model0.grid, time_order=2, space_order=4,
                  save=geometry.nt)

for i in range(nshots):
    # Important: We force previous wavefields to be destroyed,
    # so that we may reuse the memory.
    clear_cache()

    print('Imaging source %d out of %d' % (i+1, nshots))

    # Update source location
    geometry.src_positions[0, :] = source_locations[i, :]

    # Generate synthetic data from true model
    true_d, _, _ = solver.forward(m=model.m)

    # Compute smooth data and full forward wavefield u0
    u0.data.fill(0.)
    smooth_d, _, _ = solver.forward(m=model0.m, save=True, u=u0)

    # Compute gradient from the data residual
    v = TimeFunction(name='v', grid=model.grid, time_order=2, space_order=4)
    residual = smooth_d.data - true_d.data
    op_imaging(u=u0, v=v, m=model0.m, dt=model0.critical_dt,
               residual=residual)
```

    Imaging source 1 out of 21
    Imaging source 2 out of 21
    Imaging source 3 out of 21
    Imaging source 4 out of 21
    Imaging source 5 out of 21
    Imaging source 6 out of 21
    Imaging source 7 out of 21
    Imaging source 8 out of 21
    Imaging source 9 out of 21
    Imaging source 10 out of 21
    Imaging source 11 out of 21
    Imaging source 12 out of 21
    Imaging source 13 out of 21
    Imaging source 14 out of 21
    Imaging source 15 out of 21
    Imaging source 16 out of 21
    Imaging source 17 out of 21
    Imaging source 18 out of 21
    Imaging source 19 out of 21
    Imaging source 20 out of 21
    Imaging source 21 out of 21

```python
#NBVAL_IGNORE_OUTPUT
from examples.seismic import plot_image

# Plot the inverted image
plot_image(np.diff(image.data, axis=1))
```

```python
assert np.isclose(np.linalg.norm(image.data), 1e6, rtol=1e1)
```

And we have an image of the subsurface with a strong reflector at the original location.

## References

[1] _Versteeg, R.J. & Grau, G. (eds.) (1991): The Marmousi experience. Proc. EAGE workshop on Practical Aspects of Seismic Data Inversion (Copenhagen, 1990), Eur. Assoc. Explor. Geophysicists, Zeist._


# Lecture 4 - Darcy's Law and Conductivity

## Energy and hydraulic head  ##

In the last section we learned that _hydrostatic pressure difference_ $p(z)$ will not allow the fully quantify water flow. In fact in addition to $p(z)$ other form of energy must also be considered.

The energy available for groundwater flow is given the name _hydraulic head_ $(h)$ or also called _piezometric head_:. It consists of three components, related to

> **elevation,
pressure and
velocity**.

The total energy head is expressed by the equation

$$
h = z + \frac{p}{\rho g} + \frac{v^2}{2g}
$$

where,

$z$ is the _elevation_ or _datum head_ [L],

$p$ is the _pressure_
exerted by water column [M L$^{-1}$ T$^{-2}$],

$\rho$ is the density of fluid [M L$^{-3}$],

$g$ is the acceleration due to gravity [LT$^{-2}$], and

$v$ is velocity of flow [LT$^{-1}$].

Note that the $h$ has the dimension of length [L].  In groundwater flow, the velocity is so low
that the energy contained in velocity can be neglected when computing the total energy.
Thus, the hydraulic head (as represented in Figure **@ALEX link it pls.**) is written as

$$
h = z + \frac{p}{\rho g}
$$

The above equation says that Water flow is governed by differences in hydraulic head and not by differences in pressure head alone.

It is to be noted that $z$ depends on the orientation. In the above equation $+z$ is considered oriented upward (based on conventional sign convention). If $z-$axis is oriented downwards, we have

$$
h = \frac{p}{\rho g}-z
$$

## Hydraulic head and discharge - when there is no discharge ##

Consider the figure below:

The pressure head: $p(z) = p_L + \rho \cdot g \cdot (L-z)$

The hydraulic head (piezometric head): $h(z) = \frac{p(z)}{\rho \cdot g } + z = \frac{p_L + \rho \cdot g \cdot (L-z) }{\rho \cdot g } + z = \frac{p_L}{\rho \cdot g}+ L = \text{Const}$

In the figure above the hydraulic head difference between two points ($z=0$ and $z=L$) are exactly equal, i.e., $\Delta h = 0$. This refers to the system with no energy gradient and hence a _no flow_ system.

## Hydraulic head and discharge - when there will be a discharge ##

Now consider the figure below

Here there is clear difference between the elevation head ($z_1$ and $z_2$), which is taken from a reference level ($z=0$ in this case. Average Sea Level (ASL), is often use for this reference). Also differing are pressure heads ($p_1$ and $p_2$). Therefore, the $h(z)$ in this case are:

$$
\begin{align}
h_1 &= \frac{p_1}{\rho \cdot g} + z_1 \\
h_2 &= \frac{p_2}{\rho \cdot g} + z_2
\end{align}
$$

As can be observed from the figure, in this case $h_1<h_2$. Thus, analogus to flow of energy - i.e., from higher energy level to the lower energy level, the flow in this example case is from cross-section 2 towards cross section 1.

It is to be noted that the _magnitude_ of the hydraulic head ($h$) rather than the _orientation_ of the set-up determines the direction of discharge.

Also, important to note is that the _differences of hydraulic head_ $(\Delta h)$ is independent of of the position of the origin of the reference axis  ($z$-axis in the above case).

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

## Darcy's Law

Groundwater in its natural state is invariably moving. This movement is governed by
established hydraulic principles. Darcy's Law is a phenomenologically derived
constitutive equation that describes the flow of a fluid through a porous medium.
Darcy's Law along with the equation of conservation of mass is equivalent to the
groundwater flow equation, one of the basic relationships of hydrogeology. It is also
used to describe oil, water, and gas flows through petroleum reservoirs.

The law was formulated by Henry Darcy (Darcy, 1856) based on the results of experiments on the flow
of water through beds of sand (Figure **@Alex pls. link fig. L4_f3.png**)  shows a schematic setup). In the experiments, _area of cross section_  ($A$ [L$^2$] $= \text{Const}$) was kept constant. Constant discharge ($Q= \text{Const}$) was applied and the sand medium was fully saturated, i.e., voids between sand grains were completely filled with water.

From the experiments Darcy observed that the _volume of water per unit time_ passing through a porous medium

- is _directly proportional_ to the  $A$ [L$^2$] and the head difference between inlet and outlet $(h_1 – h_2)$ [L], and
- is inversely proportional to the _length of the medium_ $L$ [L]

i.e.,

$$
\frac{\text{Vol}}{t}= Q \propto A (h_1 - h_2)\frac{1}{L}
$$

which in terms of specific discharge $q$, or discharge velocity or Darcy velocity $v$ [LT$^{-1}$] is

$$
q = v = \frac{Q}{A}= - K\frac{\partial h}{\partial L} = - K\,i
$$

where constant of proportionality $K =$ _hydraulic conductivity_ [LT$^{-1}$]; and $i = \partial h/ \partial L =$
_hydraulic gradient_ = rate of head loss per unit length of medium [ ]. It iThe _negative sign_
indicates that the total head is decreasing in the direction of flow because of friction or
resistance

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

Darcy's law is a simple mathematical statement which neatly summarizes several
familiar properties that groundwater flowing in aquifers exhibits, including:
> 1.  if there
 is no hydraulic gradient (difference in hydraulic head over a distance), no flow occurs
 (this is hydrostatic conditions),

> 2. if there is a hydraulic gradient, flow will occur from
 a high head towards a low head (opposite the direction of increasing gradient, hence the
negative sign in Darcy's law),

> 3.  the greater the hydraulic gradient (through the same
aquifer material), the greater the discharge, and

> 4.  the discharge may be different
through different aquifer materials (or even through the same material, in a different
direction) even if the same hydraulic gradient exists.

### Darcy's law and analogous physical systems ###

Darcy's law is analogous to pipe flow in which energy is dissipated over the distance to overcome frictional loss resulting
from fluid viscosity.

It also forms the scientific basis of permeability used in the earth
sciences.

It may be noted Darcy's law is analogous to Fourier's law in the field of heat conduction, Ohm's law in the field of electrical networks, or Fick's law in diffusion theory.

## Hydraulic Conductivity and Intrinsic Permeability ##

_Hydraulic Conductivity ($K$)_ appeared in the Darcy's law as a constant of proportionality, i.e., it is the fundamental quantity that is required to describe groundwater flow. Therefore we illustrate this further,

A medium has a _unit hydraulic conductivity_ if it will transmit in _unit time_ a _unit volume of groundwater_ at the prevailing kinematic viscosity through a cross section of _unit area_ measured at _right angles_ to the direction of flow, under a _unit hydraulic gradient_.

The hydraulic conductivity of a soil or rock depends on a variety of
physical factors, important ones are:
- porosity,
- particle size and distribution,
- shape of particles,
- arrangement of particles.

Also, hydraulic conductivity is depends on the property of the fluid e.g., density, viscosity

In general for unconsolidated porous media,$K$ varies with _square_ of particle size; clayey materials exhibit low values of $K$, whereas
sands and gravels display high values

Typical values for hydraulic conductivity (see figure XX for more comprehensive listing):

| Media Type      | hydraulic conductivity (m/s) |
| ----------- | ---: |
| Gravel     | $10^{-2} – 10^{-1}$ |
| coarse sand   | $\approx 10^{-3}$ |
| medium sand   | $10 ^{-4} – 10^{-3}$ |
| fine sand  | $10^{-5} – 10^{-4}$        |
| Silt  | $10^{-9}  – 10^{-6} $     |
| Clay  | $< 10^{-9} $     |

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

### Obtaining Hydraulic Conductivities ###

The hydraulic conductivity depends on properties of the fluid (density, viscosity,
temperature) and on properties of the porous medium (effective porosity, grain size
distribution). It can be obtained by calculation from formulas, laboratory methods, or
field tests

The laboratory method can be indirect method or direct method. For example,
determination of the hydraulic conductivity based on the evaluation of sieve analysis
data is the indirect laboratory method. On the other hand, the direct method of
determination of the hydraulic conductivity (e.g. permeameter) is based on some version of Darcy‘s experiment. Advantages of laboratory methods include controlled
conditions, small sample size (easy handling), lower costs, larger number of
experiments, etc. While, the disadvantages of laboratory methods are disturbed
samples, additional pathways at column walls, small sample size (randomly high or low
K), flushing of fine material, etc.

#### Hydraulic Conductivities estimations from Sieve analysis ####

Sieve analysis data can be evaluated to estimate hydraulic conductivity of
unconsolidated media. There are several _empirical methods_. The simplest one dates back to Hazen (1892):

$$
K = 0.0116 \cdot d_{10}^2
$$

where, $d_{10}$ = grain diameter (mm) corresponding to $10
\%$ of cumulative mass fraction. It can be generalized by including temperature ($\theta$ in $^\circ$C). Thus the Hazen formula becomes

$$
K = 0.0116 \cdot d_{10}^2 \cdot (0.7 + 0.03\cdot\theta)
$$

Hazen's formula is only valid for the indicated units, i.e., conversion of the _unit_ may be required before using the formula.

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

#### Hydraulic Conductivities estimations from Darcy's Law ####

**Permeameter** is an instrument used to determine hydraulic conductivity of soil samples
in the laboratory as _direct method_. The design of permeameters is based on Darcy‘s
experiment.

(**@Alex this figure can go on the right side of the text**)

There are mainly two types of permeameters
1. Constant-head permeameter, and
2. Falling-head permeameter.

In **constant-head permeameters* as
shown in Figure **(@ALEX pls. fix the link)**, the hydraulic heads at inflow and outflow of the Darcy column are
constant in time. As a consequence, the discharge is not changing with time.

The
hydraulic conductivity can be obtained by observing discharge and heads and then
substituting in the below formula that is rearranged form of Darcy’s law from:

$$
K = \frac{QL}{A(h_{in}- h_{out}}
$$

where $Q$ =discharge[L$^3$T$^{-1}$];

$L$ = length of sample [L];
 $A$ = cross-sectional area of sample [L$^2$];

$h_{in}$ = hydraulic head at column inlet [L];

$h_{out}$ = hydraulic head at column outlet [L];

$\Delta h$ = $h_{in} - h_{out}$

$h_{out}$ can be set equal to zero as only head differences are important.

@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.

In falling-head permeameter as shown in Figure **(@Alex, pls. fix the link)** the hydraulic head at the outflow of
the Darcy column is not changing, but the hydraulic head at the inflow is decreasing
with time.

As a result, the discharge also decreases with time. Rewriting Darcy's law for a small time interval

$$
K \frac{\pi d_c^2}{4}\big(h_{in} - h_{out}\big)\frac{1}{L}\text{d}t = \frac{\pi d_t^2}{4}\text{d}h
$$

$K$ can be obtained from the above equation by separating the variables and then integrating. The resulting expression for $K$ will be

$$
K = \frac{d_t^2 L}{d_c^2}\ln \Bigg(\frac{h_{in}(0) - h_{out}}{h_{in}(t)-h_{out}}\Bigg)
$$

where,

$L$ = length of sample [L];

$d_c$ = diameter of sample cylinder [L];

$d_t$ = diameter of piezometer tube [L];

$h_{in}(0)$ = initial hydraulic head at column inlet [L];

$h_{in}(t)$ = final hydraulic head at column inlet [L];

$h_{out}$ = hydraulic head at column outlet [L];

$h_0 = h_{in}(0) - h_{out}$;

$h = h_{in}(t) - h_{out}$

and $h_{out}$ can be set equal to zero as only head differences are
important. The hydraulic conductivity can be obtained from the above equation using observed time and corresponding heads. Larger experimental time periods are needed for the falling-head permeameter, in particular if hydraulic conductivity is low. On the other hand, no measurement of discharge or water volume is required.

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

**Field methods** of determination of the hydraulic conductivity include tracer tests, auger hole tests, pumping tests of wells, etc. Field experiments are much more complicated and expensive than laboratory tests. Resulting hydraulic conductivities represent averages over an aquifer volume, which is covered by the experiment. The size of this
volume depends on subsurface properties and on the experimental method used.

### Intrinsic Permeability ###

A convenient alternative is to write Darcy's equation in a form of **intrinsic permeability**
where the properties of the medium and the fluid are represented explicitly

$$
v = \frac{-k \cdot \rho\cdot g}{\mu}\frac{\partial h}{\partial L}
$$

where, $k$ is the intrinsic permeability [L$^2$], and $\eta$ is the dynamic viscosity of fluid [ML$^{-1}$T$^{-1}$] e.g., (Pa-S). The relation between _hydraulic conductivity_ _intrinsic permeability_, therefore, is

$$
K = k\cdot \frac{ \rho \cdot g}{\eta}
$$

In terms of Kinematic viscosity [L$^2$T$^{-1}$}], $\eta = \rho \cdot \nu$, the above relation becomes

$$
K = k\cdot \frac{ g}{\nu}
$$

Both density and viscosity are temperature dependent quantites. Their values in field conditions $\approx 10 ^\circ$C and in the laboratory conditions $\approx 20 ^\circ$C are:

|                            | 10°C        | 20°C         |
|----------------------------|-------------|--------------|
| density (kg/m$^3$)      | 999.7       | 999.7        |
| kinematic viscosity (m$^2$/s) | 1.3101·10$^{-6}$ | 1.0105·100$^{-6}$ |
| ynamic viscosity (Pa$\cdot$s)  | 1.3097·10$^{-3}$ | 1.3097·100$^{-3}$  |
|||

The _intrinsic permeability_ can be written in terms of specific weight or weight density [ML$^{-2}$T$^{-2}$] (or in metric unit- N/m$^3$), $\gamma = \rho\cdot g$ as

$$
k = \frac{\eta}{\gamma}K
$$

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

### Properties of Intrinsic Permeability ###

- The value of intrinsic permeability of a porous medium
equals 1 m$^2$ if a fluid with dynamic viscosity of 1 Pa$\cdot$s can pass through the porous
medium at a Darcy velocity of 1 m/s under a hydrostatic pressure gradient of 1 Pa/m
(horizontal flow).

- The intrinsic permeability is _independent_ of the fluid moving through the medium and depends only upon the medium properties. Intrinsic
permeability of unconsolidated porous media is roughly proportional to the square of
the pore diameter.

- The intrinsic permeability is used primarily when the density or the viscosity of the
fluid varies with position.

- The dimension of $k$ is [L$^2$], but when expressed in m$^2$ is so small that square
micrometers ($\mu$ m)$^2  = 10^{-12}$ m$^2$ is used. In the petroleum industry it is expressed in **Darcy**
(symbol: D) with conversion factor to SI units is:  1 D $= 0.987\cdot 10^{-12}$ m$^2$.

- Intrinsic permeability for a weakly , well and
highly permeable aquifers vary in the range 10$^{-4}$ to 10$^{-1}$ D, 10$^{-1}$ to 10$^2$ D, and $> 10^2$ D
respectively

- Typical value of conductivity and intrinsic permeability is  provided in the fig (from Todd and Mays, 2004) below.

## Darcy velocity and Interstitial velocity ##

Darcy velocity $v$ is the _apparent velocity_ or _fictitious velocity_ or _Darcy flux_ (discharge per
unit area). This value of velocity, often referred to as the _apparent velocity_, is not the
velocity which the water traveling through the pores is experiencing. The velocity $v$ is
referred to as the Darcy velocity because it assumes that flow occurs through the entire cross section of the material without regard to solids and pores. Actually water can flow
though pores only and the pore spaces vary continuously with location within the
medium. Therefore the actual velocity is nonuniform, involving endless accelerations,
deceleration, and changes in direction. To define the _actual flow velocity_ or **interstitial
velocity**, one must consider the microstructure of the rock material.

For naturally occurring geologic materials, the microstructure cannot be specified three-
dimensionally; hence, actual velocities can only be quantified statistically.

Actually, the flow is limited to the pores (white spaces in Figure) only so that the
_average interstitial velocity_ or _actual velocity_ or _seepage velocity_ $(v_s)$ through pore space
can be determined by applying continuity equation

$$
Q = A_s\cdot v_s = Av
$$
Leading to

$$
v_s = v\frac{A}{A_s} = \frac{v}{\nu_e}
$$

where $A$ = total area of soil specimen, and $A_s$ = area of pores only (see Figure). The velocity
is divided by effective porosity ($\nu_e$) to account for the fact that only a fraction of the total aquifer
volume is available for flow. This indicates that for a sand with a porosity of 33% the $v_s
= 3 v$. Thus the average interstitial velocity or seepage velocity or linear velocity through
pore space is never smaller than Darcy velocity. Sometimes, the average flow velocity of
water in the pore space is termed _linear velocity_.

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

### Typical values of linear velocities ###

Typical values for average interstitial velocities or linear velocities in unconsolidated aquifers are 0.5 m/d to 1 m/d and 30
m/d to 300 m/d in sand and gravel respectively.

Linear velocities in fractured or
karstified aquifers can be rather high along fractures or conduits e.g. 200 m/d to 1.2
km/d along fractures and 3 km/d to 14 km/d in karst conduits.

On the contrary, the
linear velocities are very low in the rock matrix of consolidated aquifers (1 cm/d or
even less).

### Travel time and Pore volume ###

The average linear/pore velocity is the velocity a conservative tracer/dye experiences if
carried by water through the aquifer.

The travel time of water through a column of
length $L$ is given by

$$
t = L/v_s
$$

It is to be noted that the linear/seepage velocity $v_s$ has to be used in travel time computation, not the Darcy velocity. The term _residence time_ is can also be found to be used for referring to _travel time_ .

Yet another important term that is often found in standard texts is _pore volume_. The _pore volume is the travel time through a column. It can be understood as the _time_ needed to replace the water in the column. In this sense, the pore volume is not a _volume_ but a _time_ (i.e., 1 PV corresponds to the ratio $L/v_s$ ). The pore volume (PV) is frequently used for _normalisation_ purposes in order to better compare column
experiments conducted under different flow velocities. This is mostly done for studying the transport behaviour of chemicals dissolved in water and their arrivals at the column outlets

**@Anne/@Sophie - pls add a numerical problem here. Pls. check at the botton of the page problems suggested by Prof. Chahar. Pls. note that the problem should be a few line codes.**

## EXAMPLE PROBLEMS SUGGESTED BY PROF. CHAHAR ##

_@Anne/Sophie pls. see how these fit to the above texts. The problems in this texts should be a very short one. Complex problems can be part of Tutorials. Pls. check T1-T3 and see if these problems fits better there._

**Problem 1.**

If the laboratory hydraulic conductivity of a sample of soil is $3.2\times 10^2$ lpd/m at 20°C, what would be the hydraulic conductivity value at 30°C?

**Problem 2.**

In a field test it was observed that a time of 5 hour was required for a tracer to travel from one observation well to another. The wells are 30 m apart and the difference in
their water table elevations is 50 cm. Samples of the aquifer between the wells indicate that
the porosity is 15%. Compute (a) the hydraulic conductivity of the aquifer assuming it to be
homogeneous, (b) the actual velocity of flow as indicated by the tracer, (c) the seepage
velocity, (d) Reynolds number for the flow assuming an average grain size of 1 mm and $\eta$
(water) at 27°C = 0.008 stoke.

**Problem 3.**

In a homogenous isotropic confined aquifer of constant thickness of 20 m,
effective porosity of 20% and hydraulic conductivity of 15 m/day, two observations well
1200 m apart indicate piezometric heads of 4.4 m and 3.0 m, respectively, above msl.
Assuming uniform flow, average grain diameter of sand 1 mm and kinematic viscosity of
water = 0.01 cm$^2$/sec, state (a) Whether Darcy's Law is applicable? (b) What is the average
flow velocity in pores?

**Problem 4.**

A confined aquifer is 18.5 m thick. The potentiometric surface elevations at two
observation wells 822 m apart 25.96 and 24.62 m. If the horizontal hydraulic conductivity of
the aquifer is 25 m/day, determine flow rate per unit width of the aquifer, specific discharge,
and average linear velocity of the flow assuming steady unidirectional flow. Effective
porosity is 0.25.

**Problem 5.**

A 35 cm gravel packed well is pumped at a constant rate of 5000 m$^3$/hr from a
confined aquifer of thickness 50 m, having $D_{10} = 0.23$ mm and D$_{50} = 0.6$ mm. (i) What is the
domain around the well for which Darcy’s law is applicable, assuming that the law is valid up
to $R_e$ = 6? (ii) If the gravel pack is 15 cm thick and has  $D_{10}$ =1.5 mm and  $D_{50}$ = 3 mm,
estimate the Reynolds’s number and the seepage gradient at mid-pack


# Fitting a Morse Diatomic Absorption spectrum with a non-Condon Moment

In these spectroscopy calculations, we are given $\omega_e$, $\chi_e \omega_e$, the reduced mass $\mu$ and the equilibrium position $r_e$.  For each atom, we want to create a system of units out of these.

\begin{align}
    h &= A \cdot e_u\cdot T_u = A \cdot m_u \frac{l_u^2}{T_u}
\end{align}
lower case means we are setting it still, capital letters mean they are determined.  If we assume right now we want to set $E_u$ to be some spectoscopic value in wavenumbers and set $\hbar$ then we know we have to let time float, which is fine since this code is not a time-dependent one.
\begin{align}
    A \cdot T_u &= \frac{h}{e_u} \\
    e_u &=  m_u \frac{l_u^2}{T_u^2} \\
    T_u &= \sqrt{ \frac{ m_u l_u^2}{e_u} }\\
    A &= \frac{h}{e_u}\sqrt{ \frac{e_u}{ m_u l_u^2} } = \sqrt{ \frac{h^2}{e_u m_u l_u^2} }
\end{align}
so we can clearly only select either the mass or the length to fix in a system of units which is self-consistent.

```python
import math

import numpy as np
from scipy.special import gamma, genlaguerre

import scipy.integrate
import scipy.misc

import sympy.mpmath
import sympy.functions.combinatorial.factorials as fact
import matplotlib.pyplot as plt
%matplotlib inline
```

```python
joules_per_wavenumber = .01 #kg * m^3 / s^2
h_joules_seconds = 6.62607E-34 #Joule*seconds
```

```python
TEST1_SYSTEM_DICTIONARY = {"reduced_mass" : 1.0,
                      "alpha" : 1.0,
                      "center" : 0.0,
                      "D" : 2.0}
TEST2_SYSTEM_DICTIONARY = {"reduced_mass" : 1.0,
                      "alpha" : 1.5,
                      "center" : 0.5,
                      "D" : 1.0}

DEFAULT_UNIVERSE_DICTIONARY = {"hbar" : 1.0 / (2.0 * np.pi),
                               "ZERO_TOLERANCE" : 1.0E-5}
Nitrogen_energy_scale_wavenumbers = 2358.57
Nitrogen_mass_scale_amu = 7.00 #???
Nitrogen_Chi_1_Sigma_g_Plus = {"omega_e_wavenumbers" = Nitrogen_scaling,
                               "omega_e" = 2358.57 / Nitrogen_scaling,
                               "omega_e_chi_e" = 14.324/Nitrogen_scaling,
                               "mu" : 1.0}
```

```python
class UnboundStateIndexError(Exception):
    def __init__(self):
        pass
class Morse(object):
    def __init__(self, system_dictionary = DEFAULT_SYSTEM_DICTIONARY, universe_dictionary = DEFAULT_UNIVERSE_DICTIONARY):

        #define the Universe
        self.hbar = universe_dictionary["hbar"]
        self.ZERO_TOLERANCE = universe_dictionary["ZERO_TOLERANCE"]

        #define the system
        #terminology taken from Matsumoto and Iwamoto, 1993
        self.mu = system_dictionary["reduced_mass"]
        self.center = system_dictionary["center"]
        self.r = self.center

        if "omega_e" not in system_dictionary:
            self.alpha = system_dictionary["alpha"]
            self.D = system_dictionary["D"]

            #Derive Other useful quantities
            self.omega_e = 2.0 * self.alpha * np.sqrt(self.D / (2.0 * self.mu))
            self.chi_e_omega_e = self.alpha**2 * self.hbar / (2.0 * self.mu)
        else:
            self.omega_e = system_dictionary["omega_e"]
            self.chi_e_omega_e = system_dictionary["chi_e_omega_e"]

            self.alpha = np.sqrt(2.0 * self.mu * self.chi_e_omega_e / self.hbar)
            self.D = 2.0 * self.mu *(self.omega_e / (2.0 * self.alpha))**2

        self.a = np.sqrt(2.0 * self.mu * self.D) / (self.alpha * self.hbar)
        self.maximum_index = int(np.floor(self.a - .5))

        #Harmonic Oscillator Approximation:
        k = self.potential_energy_gradientSquared(self.r)
        self.omega_HO = np.sqrt(k / self.mu)
        self.x0 = np.sqrt( self.hbar / (2.0 * self.omega_HO * self.mu))

        #determine the needed spatial parameters:
        self.index_to_xParams_dictionary = {}
        for energy_index in range(self.maximum_index + 1):
            #use the analytically calculated spread of the corresponding HO wavefunction to start guessing the needed spatial parameters
            HO_spatial_spread = self.x0 * np.sqrt(2 * energy_index + 1)

            x_min = self.r - 5.0 * HO_spatial_spread
            while np.abs(self.energy_eigenfunction_amplitude(energy_index, x_min)) > self.ZERO_TOLERANCE:
                x_min += -HO_spatial_spread
            x_max = self.r + 5.0 * HO_spatial_spread
            while np.abs(self.energy_eigenfunction_amplitude(energy_index, x_max)) > self.ZERO_TOLERANCE:
                x_max += HO_spatial_spread

            keep_integrating = True

            number_x_points = 10
            while keep_integrating:
                x_vals = np.linspace(x_min, x_max, number_x_points)
                psi_vals = self.energy_eigenfunction_amplitude(energy_index, x_vals)
                integral = scipy.integrate.simps(np.conj(psi_vals) * psi_vals, x = x_vals)
                if np.abs(integral - 1.0) < self.ZERO_TOLERANCE:
                    keep_integrating = False
                else:
                    number_x_points = number_x_points + 10
            self.index_to_xParams_dictionary[energy_index] = (x_min, x_max, number_x_points)

    #POTENTIAL ENERGY STUFF:
    def potential_energy(self, x):
        return -2 * self.D * np.exp(- self.alpha * (x - self.r)) + self.D * np.exp(-2.0 * self.alpha * (x - self.r))

    def potential_energy_gradient(self, x):
        return 2.0 * self.alpha * self.D *(np.exp(- self.alpha * (x - self.r)) - np.exp(-2.0 * self.alpha * (x - self.r)))

    def potential_energy_gradientSquared(self, x):
        return 2.0 * self.alpha**2 * self.D *(-np.exp(- self.alpha * (x - self.r))  + 2.0 * np.exp(-2.0 * self.alpha * (x - self.r)))

    #ENERGY EIGENFUNCTION STUFF:
    def energy_eigenvalue(self, index):
        return -self.D + self.hbar * ( self.omega_e *(index + .5) - self.chi_e_omega_e *(index + .5)**2 )

    def energy_eigenfunction_amplitude(self, n, x):
        if n > self.maximum_index:
            raise UnboundStateIndexError()
        b_n = self.a - .5 - n
        N_n = np.sqrt(2.0 * self.alpha * b_n * scipy.misc.factorial(n) / gamma(2 * b_n + n + 1))
        z = 2.0 * self.a * np.exp(-self.alpha *(x - self.r))

        z_poly = np.power(z, b_n)
        z_exp = np.exp(-.5 * z)
        lag_part = genlaguerre(n, 2 * b_n)(z)

        return N_n * z_poly * z_exp * lag_part

class OffsetMorse(object):

    def __init__(self, ground_morse, excited_morse, universe_dictionary = DEFAULT_UNIVERSE_DICTIONARY):
        #define the Universe
        self.hbar = universe_dictionary["hbar"]
        self.ZERO_TOLERANCE = universe_dictionary["ZERO_TOLERANCE"]

        #assign variables
        self.ground_morse = ground_morse
        self.excited_morse = excited_morse

        self.franck_condon_factors = np.zeros((self.ground_morse.maximum_index + 1, self.excited_morse.maximum_index + 1))
        for ground_index in range(self.ground_morse.maximum_index + 1):
            ground_xMin, ground_xMax, ground_numPoints = self.ground_morse.index_to_xParams_dictionary[ground_index]
            for excited_index in range(self.excited_morse.maximum_index + 1):
                excited_xMin, excited_xMax, excited_numPoints = self.excited_morse.index_to_xParams_dictionary[excited_index]
                x_min = min([ground_xMin, excited_xMin])
                x_max = max([excited_xMax, ground_xMax])

                keep_integrating = True
                n_points = ground_numPoints * excited_numPoints
                #integrate once
                x_vals = np.linspace(x_min, x_max, n_points)
                g_func_vals = self.ground_morse.energy_eigenfunction_amplitude(ground_index, x_vals)
                e_func_vals = self.excited_morse.energy_eigenfunction_amplitude(excited_index, x_vals)
                gToE_FCF = scipy.integrate.simps(e_func_vals * np.conj(g_func_vals), x= x_vals)
                #check to make sure integral is converged
                while keep_integrating:
                    n_points = n_points * 1.1

                    x_vals = np.linspace(x_min, x_max, n_points)
                    g_func_vals = self.ground_morse.energy_eigenfunction_amplitude(ground_index, x_vals)
                    e_func_vals = self.excited_morse.energy_eigenfunction_amplitude(excited_index, x_vals)
                    new_integral = scipy.integrate.simps(e_func_vals * np.conj(g_func_vals), x= x_vals)

                    if np.abs((new_integral - gToE_FCF) / new_integral ) < self.ZERO_TOLERANCE:
                        keep_integrating = False
                    else:
                        print("NEED MOAR POINTz")

                self.franck_condon_factors[ground_index, excited_index] = gToE_FCF
    def stick_absorption_spectrum(self, starting_ground_index):
        relevant_FCFs = self.franck_condon_factors[starting_ground_index,:]
        frequency_values = []
        ground_energy = self.ground_morse.energy_eigenvalue(starting_ground_index)
        for excited_index in range(self.excited_morse.maximum_index + 1):
            energy_gap = self.excited_morse.energy_eigenvalue(excited_index) - ground_energy
            frequency_values.append(energy_gap / self.hbar)

        return frequency_values, relevant_FCFs**2
```

```python
ground = Morse()
excited = Morse(system_dictionary=ALTERNATE_SYSTEM_DICTIONARY)
test_offsetMorse = OffsetMorse(ground_morse = ground, excited_morse = excited)
for i in range(ground.maximum_index + 1):
    w, I = test_offsetMorse.stick_absorption_spectrum(i)
    plt.plot(w, np.log(I))
```

```python
x_vals = np.linspace(-1, 30, 200, dtype=np.complex)
for n in range(int(test.max_ground_index) + 1):
    print("n="+str(n))
    f = test.ground_eigenfunction(n, x_vals)
    plt.plot(x_vals, np.real(f), label=n)
    print("integral="+str( scipy.integrate.simps(f * np.conj(f)  , x= x_vals)))
    print("\n")
# plt.legend(loc=0)
plt.figure()
for n in range(int(test.max_excited_index) + 1):
    print("n="+str(n))
    f = test.ground_eigenfunction(n, x_vals)
    plt.plot(x_vals, np.real(f), label=n)
    print("integral="+str( scipy.integrate.simps(f * np.conj(f)  , x= x_vals)))
    print("\n")
# plt.legend(loc=0)
```

```python

```

```python

```

```python

```


# Extended Kalman Filter design for bicycle's kinematic motion model

```python
# Import dependencies
from __future__ import division, print_function
%matplotlib inline

import scipy
import sympy
from sympy import Symbol, symbols, Matrix, sin, cos, latex
from sympy.interactive import printing
printing.init_printing()
sympy.init_printing(use_latex="mathjax", fontsize='16pt')

from BicycleTrajectory2D import *
from BicycleUtils import *
from FormatUtils import *
from PlotUtils import *
```

## Simulation of kinematic motion model

```python
[N, dt, wheel_distance] = [300, 0.05, 1.1]  # simulation parameters
add_noise = True  # Enable/disable gaussian noise

# Define initial state --------------------------------------------------------
delta = math.radians(6)  # steering angle
phi = math.radians(0)  # Lean angle
X_init = np.array([1.0, 3.0, 0.0, np.tan(delta)/wheel_distance, 0.0, phi]) # [x, y, z, sigma, psi, phi]

# Define constant inputs ------------------------------------------------------
U_init = np.array([1.0, 0.01, 0.01]) # [v, phi_dot, delta_dot]

# Define standard deviation for gaussian noise model --------------------------
# [xf, xr, yf, yr, zf, zr, za, delta, psi, phi]
if add_noise:
    noise = [0.5, 0.5, 0.5, 0.5, 0.1, 0.1, 0.1, 0.01, 0.01, 0.01]
else:
    noise = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]

# Create object simulator ------------------------------------------------------
bike = BicycleTrajectory2D(X_init=X_init, U_init=U_init, noise=noise)

# Simulate path ----------------------------------------------------------------
(gt_sim, zs_sim, time) = bike.simulate_path(N=N, dt=dt)

# Plot simulation results
#plot_results(xs=[], zs_sim=zs_sim, gt_sim=gt_sim, time=time, plot_xs=False)
```

## Implementation of EKF for $\sigma$-model

### Define state vector:

$$ X =
\begin{bmatrix}x & y & z & v & \sigma & \psi & \phi \end{bmatrix}^\mathsf T$$

### Define measurement vector:

$$ Z =
\begin{bmatrix}x_f & x_r & y_f & y_r & z_f & z_r & z_a & \sigma & \psi & \phi \end{bmatrix}^\mathsf T$$

```python
x, y, v, psi, phi, delta, time, w, sigma, z = symbols('x y v \psi \phi \delta T w sigma z')
delta_dot, v_dot, phi_dot = symbols('delta_dot v_dot phi_dot')

fxu = Matrix([[x + time*v*sympy.cos(psi)],
              [y + time*v*sympy.sin(psi)],
              [z],
              [sigma + (time*delta_dot/w)*(1+(w**2)*(sigma**2))],
              [psi + time*v*sigma/sympy.cos(phi)],
              [phi + time*phi_dot]
             ])

state = Matrix([x, y, z, sigma, psi, phi])   # Define state
F = fxu.jacobian(state)                      # Compute Jacobian of F with respecto to states
inputs = Matrix([v, phi_dot, delta_dot])     # Define inputs
V = fxu.jacobian(inputs)                     # Compute Jacobian of F with respecto to inputs
```

### Define state transition function $F$

```python
fxu
#print (latex(fxu))
```

$$\left[\begin{matrix}T v \cos{\left (\psi \right )} + x\\T v \sin{\left (\psi \right )} + y\\z\\\frac{T \delta_{dot}}{w} \left(\sigma^{2} w^{2} + 1\right) + \sigma\\\frac{T \sigma v}{\cos{\left (\phi \right )}} + \psi\\T \phi_{dot} + \phi\end{matrix}\right]$$

### Compute Jacobian of $F$ with respect to state $x$

```python
F
#print (latex(F))
```

$$\left[\begin{matrix}1 & 0 & 0 & 0 & - T v \sin{\left (\psi \right )} & 0\\0 & 1 & 0 & 0 & T v \cos{\left (\psi \right )} & 0\\0 & 0 & 1 & 0 & 0 & 0\\0 & 0 & 0 & 2 T \delta_{dot} \sigma w + 1 & 0 & 0\\0 & 0 & 0 & \frac{T v}{\cos{\left (\phi \right )}} & 1 & \frac{T \sigma v \sin{\left (\phi \right )}}{\cos^{2}{\left (\phi \right )}}\\0 & 0 & 0 & 0 & 0 & 1\end{matrix}\right]$$

### Compute Jacobian of $F$ with respect to inputs $u$

```python
V
#print (latex(V))
```

$$\left[\begin{matrix}T \cos{\left (\psi \right )} & 0 & 0\\T \sin{\left (\psi \right )} & 0 & 0\\0 & 0 & 0\\0 & 0 & \frac{T}{w} \left(\sigma^{2} w^{2} + 1\right)\\\frac{T \sigma}{\cos{\left (\phi \right )}} & 0 & 0\\0 & T & 0\end{matrix}\right]$$

### Implement EKF filter

```python
class EKF_sigma_model_fusion(object):
    """Implements an EKF to bicycle model"""
    def __init__(self, xs, P, R_std, Q_std, wheel_distance=1.2, dt=0.1, alpha=1.0):
        self.w = wheel_distance        #Set the distance between the wheels
        self.xs = xs *0.0    #Set the initial state
        self.P = P      #Set the initial Covariance
        self.dt = dt
        self.R_std = R_std
        self.Q_std = Q_std
        self.alpha = alpha
        #self.K = np.zeros((6, 6)) # Kalman gain
        self.K = np.eye(6)*0.5 # Kalman gain

        #Set the process noise covariance
        self.Q = np.diag([self.Q_std[0], # v
                          self.Q_std[1], # phi_dot
                          self.Q_std[2]  # delta_dot
                          ])

        # Set the measurement noise covariance
        self.R = np.diag([self.R_std[0],  # xf
                          self.R_std[1],  # xr
                          self.R_std[2],  # yf
                          self.R_std[3],  # yr
                          self.R_std[4],  # zf
                          self.R_std[5],  # zr
                          self.R_std[6],  # za
                          self.R_std[7],  # sigma
                          self.R_std[8],  # psi
                          self.R_std[9]]) # phi

        # Linear relationship H -  z = Hx
        self.H = np.zeros((10, 6))  # 10 measurements x 6 state variables
        [self.H[0, 0], self.H[1, 0]] = [1.0, 1.0]  # x
        [self.H[2, 1], self.H[3, 1]] = [1.0, 1.0]  # y
        [self.H[4, 2], self.H[5, 2], self.H[6, 2]] = [1.0, 1.0, 1.0]  # z
        [self.H[7, 3], self.H[8, 4], self.H[9, 5]] = [1.0, 1.0, 1.0]  # sigma - psi - phi

    def Fx(self, xs, u):
        """ Linearize the system with the Jacobian of the x """
        F_result = np.eye(len(xs))

        v = u[0]
        phi_dot = u[1]
        delta_dot = u[2]

        sigma = xs[3]
        psi = xs[4]
        phi = xs[5]
        t = self.dt

        F04 = -t * v * np.sin(psi)
        F14 = t * v * np.cos(psi)
        F33 = (2 * t * delta_dot * sigma * self.w) + 1
        F43 = (t * v)/np.cos(phi)
        F45 = t * sigma * v * np.sin(phi) / np.cos(phi)**2

        F_result[0, 4] = F04
        F_result[1, 4] = F14
        F_result[3, 3] = F33
        F_result[4, 3] = F43
        F_result[4, 5] = F45

        return F_result

    def Fu(self, xs, u):
        """ Linearize the system with the Jacobian of the u """
        v = u[0]
        phi_dot = u[1]
        delta_dot = u[2]

        sigma = xs[3]
        psi = xs[4]
        phi = xs[5]
        t = self.dt

        V_result = np.zeros((len(xs), len(u)))

        V00 = t * np.cos(psi)
        V10 = t * np.sin(psi)
        V32 = (t/self.w)*((sigma**2)*(self.w**2) + 1)
        V40 = t * sigma / np.cos(phi)
        V51 = t

        V_result[0, 0] = V00
        V_result[1, 0] = V10
        V_result[3, 2] = V32
        V_result[4, 0] = V40
        V_result[5, 1] = V51

        return V_result

    def f(self, xs, u):
        """ Estimate the non-linear state of the system """
        v = u[0]
        phi_dot = u[1]
        delta_dot = u[2]

        sigma = xs[3]
        psi = xs[4]
        phi = xs[5]
        t = self.dt

        fxu_result = np.zeros((len(xs), 1))

        fxu_result[0] = xs[0] + t * v * np.cos(psi)
        fxu_result[1] = xs[1] + t * v * np.sin(psi)
        fxu_result[2] = xs[2]
        fxu_result[3] = xs[3] + (t*phi_dot/self.w)*((sigma**2)*(self.w**2) +1)
        fxu_result[4] = xs[4] + t * v * sigma / np.cos(phi)
        fxu_result[5] = xs[5] + t * phi_dot

        return fxu_result

    def h(self, x):
        """ takes a state variable and returns the measurement
        that would correspond to that state. """
        sensor_out = np.zeros((10, 1))
        sensor_out[0] = x[0]
        sensor_out[1] = x[0]
        sensor_out[2] = x[1]
        sensor_out[3] = x[1]
        sensor_out[4] = x[2]
        sensor_out[5] = x[2]
        sensor_out[6] = x[2]
        sensor_out[7] = x[3] # sigma
        sensor_out[8] = x[4] # psi
        sensor_out[9] = x[5] # phi

        return sensor_out

    def Prediction(self, u):
        x_ = self.xs
        P_ = self.P
        self.xs = self.f(x_, u)
        self.P = self.alpha * self.Fx(x_, u).dot(P_).dot((self.Fx(x_,u)).T) + \
             self.Fu(x_,u).dot(self.Q).dot((self.Fu(x_,u)).T)

    def Update(self, z):
        """Update the Kalman Prediction using the meazurement z"""
        y = z - self.h(self.xs)
        self.K = self.P.dot(self.H.T).dot(np.linalg.inv(self.H.dot(self.P).dot(self.H.T) + self.R))

        self.xs = self.xs + self.K.dot(y)
        self.P = (np.eye(len(self.xs)) - self.K.dot(self.H)).dot(self.P)
```

Execute EKF

```python
np.random.seed(850)

file_name = "filters/EKF/math_model/"
[N, dt, wheel_distance, number_state_variables] = [300, 0.05, 1.1, 6]
delta = math.radians(6)
phi = math.radians(0)

#%prun some_useless_slow_function()

U_init = np.array([1.0, 0.01, 0.01]) # [v, phi_dot, delta_dot]
X_init = np.array([1.0, 3.0, 0.0, np.tan(delta)/wheel_distance, 0.0, phi]) # [x, y, z, sigma, psi, phi]

# noise = [xf, xr, yf, yr, zf, zr, za, delta, psi, phi]
if add_noise:
    noise = [0.5, 0.5, 0.5, 0.5, 0.1, 0.1, 0.1, 0.01, 0.01, 0.01]
    file_name += "noise/"
else:
    noise = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
    file_name += "no_noise/"

bike = BicycleTrajectory2D(X_init=X_init, U_init=U_init, w=wheel_distance, noise=noise)

(gt_sim, zs_sim, time_t) = bike.simulate_path(N=N, dt=dt)

alpha = 1.0

# covariance matrix
P = np.eye(number_state_variables) * 1e0

# Input noise covariance M
M_std = [(0.001)**2, (0.001)**2, (0.001)**2 ] # v, phi_dot, delta_dot

# Measurement noise covariance R
# [xf, xr, yf, yr, zf, zr, za, delta, psi, phi]
R_std = [0.8**2, 0.8**2,  # x
         0.8**2, 0.8**2,  # y
         0.5**2, 0.5**2, 0.5**2,  # z
         0.5**2, 0.4**2, 1.8**2] # delta - psi - phi

filter_ekf = EKF_sigma_model_fusion(X_init, P, R_std=R_std, Q_std=M_std, wheel_distance=wheel_distance, dt=dt, alpha=alpha)

xs = np.zeros((N, number_state_variables))
ps = np.zeros((N, number_state_variables, number_state_variables))
PU = np.zeros((N, number_state_variables))
KU = np.zeros((N, number_state_variables))
time_t = np.zeros((N, 1))
t = 0
z_t = np.zeros((10, 1))

for i in range(N):
    P = filter_ekf.P
    K = filter_ekf.K
    PU[i] = [P[0,0], P[1,1], P[2,2], P[3,3], P[4,4], P[5,5]]
    KU[i] = [K[0,0], K[1,1], K[2,2], K[3,3], K[4,4], K[5,5]]

    xs[i] = filter_ekf.xs.T
    xs[i, 3] = np.arctan2(xs[i, 3], 1/wheel_distance)  # sigma to delta conversion

    # predict
    filter_ekf.Prediction(U_init)

    # update measurements [xf, xr, yf, yr, zf, zr, za, delta, psi, phi]
    z_t[0] = zs_sim[i].xf
    z_t[1] = zs_sim[i].xr
    z_t[2] = zs_sim[i].yf
    z_t[3] = zs_sim[i].yr
    z_t[4] = zs_sim[i].zf
    z_t[5] = zs_sim[i].zr
    z_t[6] = zs_sim[i].za
    z_t[7] = np.tan(zs_sim[i].delta)/wheel_distance  # sigma
    z_t[8] = zs_sim[i].psi # psi
    z_t[9] = zs_sim[i].phi # phi

    filter_ekf.Update(z_t)

    cov = np.array([[P[0, 0], P[2, 0]],
                    [P[0, 2], P[2, 2]]])
    mean = (xs[i, 0], xs[i, 1])

    #plot_covariance_ellipse(mean, cov, fc='g', std=3, alpha=0.3, title="covariance")

    time_t[i] = t
    t += dt
    filter_ekf.time_t = t
```

```python
filter_name = 'EKF'
(gt, zs) = convert_object_to_array(gt_sim, zs_sim)
plot_filter_results(xs, gt, zs, time_t, file_name, filter_name)
```

Plot Kalman gain and process covariance

```python
plot_EKF_gain_covariance(time_t, KU, PU, file_name, autoscale_axis=True)
```

```python

```


O. Pannekoucke$^{1,2,3}$

$^1$ INPT-ENM, Toulouse, France

$^2$ CNRM, Université de Toulouse, Météo-France, CNRS, Toulouse, France

$^3$ CERFACS, Toulouse, France

(olivier.pannekoucke@meteo.fr)

**Abstract**

This notebook illustrates the use of sympkf to build and handle the PKF dynamics associated with the advection in 2D
$$
        \partial_t c +\mathbf{u}\nabla c = 0,
$$
where $c$ is a function $t,x,y$ and  $\mathbf{u}=(u(x,y), v(x,y))$ is the stationnary velocity field.

For this dynamics, the resulting PKF system is closed and reads as (in aspect tensor form)
$$
        \left\{
        \begin{array}{l}
        \partial_t c +\mathbf{u}\nabla c = 0,\\
        \partial_t V_c +\mathbf{u}\nabla V_c = 0,\\
        \partial_t \mathbf{s}_c +\mathbf{u}\nabla\mathbf{s}_c =
            \left(\nabla\mathbf{u}\right) \mathbf{s}_c+ \mathbf{s}_c\left(\nabla\mathbf{u}\right)^T.
        \end{array}
        \right .
$$

# Definition of the 2D advection equation

```python
import sympy
sympy.init_printing()
```

**Definition of the dynamics from sympy tools**

```python
import sympy
from sympy import init_printing
init_printing()
```

```python
from sympy import Function, Derivative, Eq, symbols
from sympkf import t
x, y = symbols('x y')
c = Function('c')(t,x,y)
u = Function('u')(x,y)
v = Function('v')(x,y)
dynamics = [
    Eq(Derivative(c,t), u*Derivative(c,x)+v*Derivative(c,y)),
]
display(dynamics)
```

```python
from sympkf import PDESystem
dynamics = PDESystem(dynamics)
```

# Computation of the PKF dynamics by using SymPKF

```python
from sympkf.symbolic import SymbolicPKF
pkf_advection = SymbolicPKF(dynamics)
```

```python
for equation in pkf_advection.in_metric:    display(equation)
```

```python
for equation in pkf_advection.in_aspect:    display(equation)
```

## Conclusion <a id='conclusion'/>

We found that the PKF dynamics for advection dynamics is the closed system given by
$$
        \left\{
        \begin{array}{l}
        \partial_t c +\mathbf{u}\nabla c = 0,\\
        \partial_t V_c +\mathbf{u}\nabla V_c = 0,\\
        \partial_t \mathbf{s}_c +\mathbf{u}\nabla\mathbf{s}_c =
            \left(\nabla\mathbf{u}\right) \mathbf{s}_c+ \mathbf{s}_c\left(\nabla\mathbf{u}\right)^T.
        \end{array}
        \right .
$$
Which also reads as
$$
\left\{
\begin{array}{l}
\displaystyle \frac{\partial}{\partial t} c{\left(t,x,y \right)} = - u{\left(x,y \right)} \frac{\partial}{\partial x} c{\left(t,x,y \right)} - v{\left(x,y \right)} \frac{\partial}{\partial y} c{\left(t,x,y \right)},\\
\displaystyle \frac{\partial}{\partial t} \operatorname{{V_{c}}}{\left(t,x,y \right)} = - u{\left(x,y \right)} \frac{\partial}{\partial x} \operatorname{{V_{c}}}{\left(t,x,y \right)} - v{\left(x,y \right)} \frac{\partial}{\partial y} \operatorname{{V_{c}}}{\left(t,x,y \right)},\\
\displaystyle \frac{\partial}{\partial t} \operatorname{{s_{c,xx}}}{\left(t,x,y \right)} = - u{\left(x,y \right)} \frac{\partial}{\partial x} \operatorname{{s_{c,xx}}}{\left(t,x,y \right)} - v{\left(x,y \right)} \frac{\partial}{\partial y} \operatorname{{s_{c,xx}}}{\left(t,x,y \right)} + 2 \operatorname{{s_{c,xx}}}{\left(t,x,y \right)} \frac{\partial}{\partial x} u{\left(x,y \right)} + 2 \operatorname{{s_{c,xy}}}{\left(t,x,y \right)} \frac{\partial}{\partial y} u{\left(x,y \right)}\\
\displaystyle \frac{\partial}{\partial t} \operatorname{{s_{c,xy}}}{\left(t,x,y \right)} = - u{\left(x,y \right)} \frac{\partial}{\partial x} \operatorname{{s_{c,xy}}}{\left(t,x,y \right)} - v{\left(x,y \right)} \frac{\partial}{\partial y} \operatorname{{s_{c,xy}}}{\left(t,x,y \right)} + \operatorname{{s_{c,xx}}}{\left(t,x,y \right)} \frac{\partial}{\partial x} v{\left(x,y \right)} + \operatorname{{s_{c,xy}}}{\left(t,x,y \right)} \frac{\partial}{\partial x} u{\left(x,y \right)} + \operatorname{{s_{c,xy}}}{\left(t,x,y \right)} \frac{\partial}{\partial y} v{\left(x,y \right)} + \operatorname{{s_{c,yy}}}{\left(t,x,y \right)} \frac{\partial}{\partial y} u{\left(x,y \right)},\\
\displaystyle \frac{\partial}{\partial t} \operatorname{{s_{c,yy}}}{\left(t,x,y \right)} = - u{\left(x,y \right)} \frac{\partial}{\partial x} \operatorname{{s_{c,yy}}}{\left(t,x,y \right)} - v{\left(x,y \right)} \frac{\partial}{\partial y} \operatorname{{s_{c,yy}}}{\left(t,x,y \right)} + 2 \operatorname{{s_{c,xy}}}{\left(t,x,y \right)} \frac{\partial}{\partial x} v{\left(x,y \right)} + 2 \operatorname{{s_{c,yy}}}{\left(t,x,y \right)} \frac{\partial}{\partial y} v{\left(x,y \right)}.
\end{array}
\right.
$$


# Double Machine Learning: Use Cases and Examples

Double Machine Learning (DML) is an algorithm that applies arbitrary machine learning methods
to fit the treatment and response, then uses a linear model to predict the response residuals
from the treatment residuals.

The EconML SDK implements the following DML classes:
* DMLCateEstimator: suitable for estimating heterogeneous treatment effects.
* SparseLinearDMLCateEstimator: suitable for the case when $W$ is high dimensional vector and both the first stage and second stage estimate are linear.

In ths notebook, we show the performance of the DML on both synthetic data and observational data.

**Notebook contents:**

1. Example usage with single continuous treatment synthetic data
2. Example usage with multiple continuous treatment synthetic data
3. Example usage with single continuous treatment observational data
4. Example usage with multiple continuous treatment, multiple outcome observational data

```python
import econml
```

```python
## Ignore warnings
import warnings
warnings.filterwarnings('ignore')
```

```python
# Main imports
from econml.dml import DMLCateEstimator,SparseLinearDMLCateEstimator

# Helper imports
import numpy as np
from itertools import product
from sklearn.linear_model import Lasso, LassoCV, LogisticRegression, LogisticRegressionCV,LinearRegression,MultiTaskElasticNet,MultiTaskElasticNetCV
from sklearn.ensemble import RandomForestRegressor,RandomForestClassifier
from sklearn.preprocessing import PolynomialFeatures
import matplotlib.pyplot as plt
import matplotlib

%matplotlib inline
```

## 1. Example Usage with Single Continuous Treatment Synthetic Data

### 1.1. DGP
We use the data generating process (DGP) from [here](https://arxiv.org/abs/1806.03467). The DGP is described by the following equations:

\begin{align}
T =& \langle W, \beta\rangle + \eta, & \;\eta \sim \text{Uniform}(-1, 1)\\
Y =& T\cdot \theta(X) + \langle W, \gamma\rangle + \epsilon, &\; \epsilon \sim \text{Uniform}(-1, 1)\\
W \sim& \text{Normal}(0,\, I_{n_w})\\
X \sim& \text{Uniform}(0,1)^{n_x}
\end{align}

where $W$ is a matrix of high-dimensional confounders and $\beta, \gamma$ have high sparsity.

For this DGP,
\begin{align}
\theta(x) = \exp(2\cdot x_1).
\end{align}

```python
# Treatment effect function
def exp_te(x):
    return np.exp(2*x[0])
```

```python
# DGP constants
np.random.seed(123)
n=1000
n_w=30
support_size=5
n_x=1
# Outcome support
support_Y=np.random.choice(range(n_w),size=support_size,replace=False)
coefs_Y=np.random.uniform(0,1,size=support_size)
epsilon_sample=lambda n: np.random.uniform(-1,1,size=n)
# Treatment support
support_T=support_Y
coefs_T=np.random.uniform(0,1,size=support_size)
eta_sample=lambda n: np.random.uniform(-1,1,size=n)

# Generate controls, covariates, treatments and outcomes
W=np.random.normal(0,1,size=(n,n_w))
X=np.random.uniform(0,1,size=(n,n_x))
# Heterogeneous treatment effects
TE=np.array([exp_te(x_i) for x_i in X])
T=np.dot(W[:,support_T],coefs_T)+eta_sample(n)
Y=TE*T+np.dot(W[:,support_Y],coefs_Y)+epsilon_sample(n)

# Generate test data
X_test = np.array(list(product(np.arange(0, 1, 0.01), repeat=n_x)))
```

### 1.2. Train Estimator
We train models in three different ways, and compare their performance.

#### 1.2.1. Default Setting

```python
est = DMLCateEstimator(model_y=RandomForestRegressor(),model_t=RandomForestRegressor())
est.fit(Y, T, X, W)
te_pred = est.const_marginal_effect(X_test)
```

#### 1.2.2. Polynomial Features for Heterogeneity

```python
est = DMLCateEstimator(model_y=RandomForestRegressor(),model_t=RandomForestRegressor(),featurizer=PolynomialFeatures(degree=2))
est.fit(Y, T, X, W)
te_pred1=est.const_marginal_effect(X_test)
```

#### 1.2.3. Polynomial Features with regularization

```python
est = DMLCateEstimator(model_y=RandomForestRegressor(),model_t=RandomForestRegressor(),model_final=LassoCV(),featurizer=PolynomialFeatures(degree=10))
est.fit(Y, T, X, W)
te_pred2=est.const_marginal_effect(X_test)
```

### 1.3. Performance Visualization

```python
plt.figure(figsize=(10,6))
plt.plot(X_test,te_pred,label='DML default')
plt.plot(X_test,te_pred1,label='DML polynomial degree=2')
plt.plot(X_test,te_pred2,label='DML polynomial degree=10 with Lasso')
expected_te=np.array([exp_te(x_i) for x_i in X_test])
plt.plot(X_test,expected_te,'b--',label='True effect')
plt.ylabel('Treatment Effect')
plt.xlabel('x')
plt.legend()
plt.show()
```

## 2. Example Usage with Multiple Continuous Treatment Synthetic Data

### 2.1. DGP
We use the data generating process (DGP) from [here](https://arxiv.org/abs/1806.03467), and modify the treatment to generate multiple treatments. The DGP is described by the following equations:

\begin{align}
T =& \langle W, \beta\rangle + \eta, & \;\eta \sim \text{Uniform}(-1, 1)\\
Y =& T\cdot \theta_{1}(X) + T^{2}\cdot \theta_{2}(X) + \langle W, \gamma\rangle + \epsilon, &\; \epsilon \sim \text{Uniform}(-1, 1)\\
W \sim& \text{Normal}(0,\, I_{n_w})\\
X \sim& \text{Uniform}(0,1)^{n_x}
\end{align}

where $W$ is a matrix of high-dimensional confounders and $\beta, \gamma$ have high sparsity.

For this DGP,
\begin{align}
\theta_{1}(x) = \exp(2\cdot x_1)\\
\theta_{2}(x) = x_1^{2}\\
\end{align}

```python
# DGP constants
np.random.seed(123)
n=1000
n_w=30
support_size=5
n_x=1
# Outcome support
support_Y=np.random.choice(range(n_w),size=support_size,replace=False)
coefs_Y=np.random.uniform(0,1,size=support_size)
epsilon_sample=lambda n: np.random.uniform(-1,1,size=n)
# Treatment support
support_T=support_Y
coefs_T=np.random.uniform(0,1,size=support_size)
eta_sample=lambda n: np.random.uniform(-1,1,size=n)

# Generate controls, covariates, treatments and outcomes
W=np.random.normal(0,1,size=(n,n_w))
X=np.random.uniform(0,1,size=(n,n_x))
# Heterogeneous treatment effects
TE1=np.array([exp_te(x_i) for x_i in X])
TE2=np.array([x_i**2 for x_i in X]).flatten()
T=np.dot(W[:,support_T],coefs_T)+eta_sample(n)
Y=TE1*T+TE2*T**2+np.dot(W[:,support_Y],coefs_Y)+epsilon_sample(n)
# Generate test data
X_test = np.array(list(product(np.arange(0, 1, 0.01), repeat=n_x)))
```

### 2.2. Train Estimator

```python
est = DMLCateEstimator(model_y=RandomForestRegressor(),model_t=RandomForestRegressor())
```

```python
T=T.reshape(-1,1)
est.fit(Y, np.concatenate((T, T**2), axis=1), X, W)
```

```python
te_pred = est.const_marginal_effect(X_test)
```

### 2.3. Performance Visualization

```python
plt.figure(figsize=(10,6))
plt.plot(X_test, te_pred[:,0], label='DML estimate1')
plt.plot(X_test, te_pred[:,1], label='DML estimate2')
expected_te1 = np.array([exp_te(x_i) for x_i in X_test])
expected_te2=np.array([x_i**2 for x_i in X_test]).flatten()
plt.plot(X_test, expected_te1, '--', label='True effect1')
plt.plot(X_test, expected_te2, '--', label='True effect2')
plt.ylabel("Treatment Effect")
plt.xlabel("x")
plt.legend()
plt.show()
```

## 3. Example Usage with Single Continuous Treatment Observational Data

We applied our technique to Dominick’s dataset, a popular historical dataset of store-level orange juice prices and sales provided by University of Chicago Booth School of Business.

The dataset is comprised of a large number of covariates $W$, but researchers might only be interested in learning the elasticity of demand as a function of a few variables $x$ such
as income or education.

We applied the `DMLCateEstimator` to estimate orange juice price elasticity
as a function of income, and our results, unveil the natural phenomenon that lower income consumers are more price-sensitive.

### 3.1. Data

```python
# A few more imports
import os
import pandas as pd
import urllib.request
from sklearn.preprocessing import StandardScaler
```

```python
# Import the data
file_name = "oj_large.csv"

if not os.path.isfile(file_name):
    print("Downloading file (this might take a few seconds)...")
    urllib.request.urlretrieve("https://msalicedatapublic.blob.core.windows.net/datasets/OrangeJuice/oj_large.csv", file_name)
oj_data = pd.read_csv(file_name)
```

```python
oj_data.head()
```

      store
      brand
      week
      logmove
      feat
      price
      AGE60
      EDUC
      ETHNIC
      INCOME
      HHLARGE
      WORKWOM
      HVAL150
      SSTRDIST
      SSTRVOL
      CPDIST5
      CPWVOL5

      0
      2
      tropicana
      40
      9.018695
      0
      3.87
      0.232865
      0.248935
      0.11428
      10.553205
      0.103953
      0.303585
      0.463887
      2.110122
      1.142857
      1.92728
      0.376927

      1
      2
      tropicana
      46
      8.723231
      0
      3.87
      0.232865
      0.248935
      0.11428
      10.553205
      0.103953
      0.303585
      0.463887
      2.110122
      1.142857
      1.92728
      0.376927

      2
      2
      tropicana
      47
      8.253228
      0
      3.87
      0.232865
      0.248935
      0.11428
      10.553205
      0.103953
      0.303585
      0.463887
      2.110122
      1.142857
      1.92728
      0.376927

      3
      2
      tropicana
      48
      8.987197
      0
      3.87
      0.232865
      0.248935
      0.11428
      10.553205
      0.103953
      0.303585
      0.463887
      2.110122
      1.142857
      1.92728
      0.376927

      4
      2
      tropicana
      50
      9.093357
      0
      3.87
      0.232865
      0.248935
      0.11428
      10.553205
      0.103953
      0.303585
      0.463887
      2.110122
      1.142857
      1.92728
      0.376927

```python
# Prepare data
Y = oj_data['logmove'].values
T = np.log(oj_data["price"]).values
scaler = StandardScaler()
W1 = scaler.fit_transform(oj_data[[c for c in oj_data.columns if c not in ['price', 'logmove', 'brand', 'week', 'store','INCOME']]].values)
W2 = pd.get_dummies(oj_data[['brand']]).values
W = np.concatenate([W1, W2], axis=1)
X=scaler.fit_transform(oj_data[['INCOME']].values)
```

```python
## Generate test data
min_income = -1
max_income = 1
delta = (1 - (-1)) / 100
X_test = np.arange(min_income, max_income + delta - 0.001, delta).reshape(-1,1)
```

### 3.2. Train Estimator

```python
est = DMLCateEstimator(model_y=RandomForestRegressor(),model_t=RandomForestRegressor())
est.fit(Y, T, X, W)
te_pred=est.const_marginal_effect(X_test)
```

### 3.3. Performance Visualization

```python
# Plot Oranje Juice elasticity as a function of income
plt.figure(figsize=(10,6))
plt.plot(X_test, te_pred, label="OJ Elasticity")
plt.xlabel(r'Scale(Income)')
plt.ylabel('Orange Juice Elasticity')
plt.legend()
plt.title("Orange Juice Elasticity vs Income")
plt.show()
```

### 3.4. Bootstrap Confidence Intervals

```python
from econml.bootstrap import BootstrapEstimator
boot_est=BootstrapEstimator(DMLCateEstimator(model_y=RandomForestRegressor(),
                                             model_t=RandomForestRegressor()),n_bootstrap_samples=20)
boot_est.fit(Y, T, X, W)
te_pred_interval = boot_est.const_marginal_effect_interval(X_test, lower=1, upper=99)
```

```python
# Plot Oranje Juice elasticity as a function of income
plt.figure(figsize=(10,6))
plt.plot(X_test.flatten(), te_pred, label="OJ Elasticity")
plt.fill_between(X_test.flatten(), te_pred_interval[0], te_pred_interval[1], alpha=.5, label="1-99% CI")
plt.xlabel(r'Scale(Income)')
plt.ylabel('Orange Juice Elasticity')
plt.title("Orange Juice Elasticity vs Income")
plt.legend()
plt.show()
```

## 4. Example Usage with Multiple Continuous Treatment, Multiple Outcome Observational Data

We use the same data, but in this case, we want to fit the demand of multiple brand as a function of the price of each one of them, i.e. fit the matrix of cross price elasticities. It can be done, by simply setting as $Y$ to be the vector of demands and $T$ to be the vector of prices. Then we can obtain the matrix of cross price elasticities.

\begin{align}
Y=[Logmove_{tropicana},Logmove_{minute.maid},Logmove_{dominicks}] \\
T=[Logprice_{tropicana},Logprice_{minute.maid},Logprice_{dominicks}] \\
\end{align}

### 4.1. Data

```python
# Import the data
oj_data = pd.read_csv(file_name)
```

```python
# Prepare data
oj_data['price']=np.log(oj_data["price"])
# Transform dataset.
# For each store in each week, get a vector of logmove and a vector of logprice for each brand.
# Other features are store specific, will be the same for all brands.
groupbylist=["store","week","AGE60","EDUC","ETHNIC","INCOME","HHLARGE","WORKWOM","HVAL150",
             "SSTRDIST","SSTRVOL","CPDIST5","CPWVOL5"]
oj_data1=pd.pivot_table(oj_data,index=groupbylist,columns=oj_data.groupby(groupbylist).cumcount(),
                        values=['logmove','price'],aggfunc='sum').reset_index()
oj_data1.columns=oj_data1.columns.map('{0[0]}{0[1]}'.format)
oj_data1=oj_data1.rename(index=str,columns={"logmove0": "logmove_T", "logmove1": "logmove_M",
                                            "logmove2":"logmove_D","price0":"price_T","price1":"price_M","price2":"price_D"})

# Define Y,T,X,W
Y = oj_data1[['logmove_T',"logmove_M","logmove_D"]].values
T=oj_data1[['price_T',"price_M","price_D"]].values
scaler = StandardScaler()
W=scaler.fit_transform(oj_data1[[c for c in groupbylist if c not in ['week', 'store','INCOME']]].values)
X=scaler.fit_transform(oj_data1[['INCOME']].values)
```

```python
## Generate test data
min_income = -1
max_income = 1
delta = (1 - (-1)) / 100
X_test = np.arange(min_income, max_income + delta - 0.001, delta).reshape(-1,1)
```

### 4.2. Train Estimator

```python
est = DMLCateEstimator(model_y=MultiTaskElasticNetCV(cv=3),model_t=MultiTaskElasticNetCV(cv=3))
est.fit(Y, T, X, W)
te_pred=est.const_marginal_effect(X_test)
```

### 4.3. Performance Visualization

```python
# Plot Oranje Juice elasticity as a function of income
plt.figure(figsize=(18,10))
dic={0:"Tropicana",1:"Minute.maid",2:"Dominicks"}
for i in range(3):
    for j in range(3):
        plt.subplot(3,3, 3*i+j+1)
        plt.plot(X_test, te_pred[:,i,j],color="C{}".format(str(3*i+j)),label="OJ Elasticity {} to {}".format(dic[j],dic[i]))
        plt.xlabel(r'Scale(Income)')
        plt.ylabel('Orange Juice Elasticity')
        plt.legend()
plt.suptitle("Orange Juice Elasticity vs Income",fontsize=16)
plt.show()
```

**Findings**: Look at the diagonal of the matrix, the TE of OJ prices are always negative to the sales across all the brand, but people with higher income are less price-sensitive. By contrast, for the non-diagonal of the matrix, the TE of prices for other brands are always positive to the sales for that brand, the TE is affected by income in different ways for different competitors. In addition, compare to previous plot, the negative TE of OJ prices for each brand are all larger than the TE considering all brand together, which means we would have underestimated the effect of price changes on demand.

### 4.4. Bootstrap Confidence Intervals

```python
from econml.bootstrap import BootstrapEstimator
boot_est=BootstrapEstimator(DMLCateEstimator(model_y=MultiTaskElasticNetCV(cv=3),model_t=MultiTaskElasticNetCV(cv=3))
                            ,n_bootstrap_samples=20)
boot_est.fit(Y,T,X,W)
te_pred_interval = boot_est.const_marginal_effect_interval(X_test, lower=1, upper=99)
```

```python
# Plot Oranje Juice elasticity as a function of income
plt.figure(figsize=(18,10))
dic={0:"Tropicana",1:"Minute.maid",2:"Dominicks"}
for i in range(3):
    for j in range(3):
        plt.subplot(3,3, 3*i+j+1)
        plt.plot(X_test, te_pred[:,i,j],color="C{}".format(str(3*i+j)),label="OJ Elasticity {} to {}".format(dic[j],dic[i]))
        plt.fill_between(X_test.flatten(), te_pred_interval[0][:, i, j],te_pred_interval[1][:, i,j], color="C{}".format(str(3*i+j)),alpha=.5, label="1-99% CI")
        plt.xlabel(r'Scale(Income)')
        plt.ylabel('Orange Juice Elasticity')
        plt.legend()
plt.suptitle("Orange Juice Elasticity vs Income",fontsize=16)
plt.show()
```

**Note**: For expository purpose, we only run 20 samples here to get the Confidence Interval. More samples are needed for validity of the bootstrap.

```python

```

notebooks/Double Machine Learning Examples.ipynb