# From R-corr. to HAL QCD pot. and eigenfunc.

This notebook is a set of sample codes for the HAL QCD method, and finite volume eigensolver from the R-correlator.

```python
%pylab inline
```

    Populating the interactive namespace from numpy and matplotlib

```python
import seaborn as sns
sns.set_style('ticks', {'axes.grid': True})
sns.set_context('poster', font_scale=2.0)
%config InlineBackend.figure_format = 'retina'

plt.rcParams['figure.figsize'] = (12.8, 9.6)
plt.rcParams['figure.facecolor'] = 'white'
```

```python
import pickle
```

```python
import scipy

import scipy.constants as sc
hbarc = ( sc.hbar * sc.speed_of_light
         / sc.mega / sc.electron_volt / sc.femto )
ainv = 2.194e3 # lattice cutoff in GeV
lat_unit = hbarc/ainv # lattice spacing in fm

mxi = 0.665 # mass of Xi in lattice unit
mpi = 510/ainv # mass of pion in lattice unit
m_red = 0.5 * mxi # reduced mass
```

```python
from scipy.sparse.linalg import LinearOperator
```

```python
print(np.__version__)
print(scipy.__version__)
print(sns.__version__)
print(matplotlib.__version__)
```

    1.14.2
    1.0.0
    0.8.1
    2.2.2

# import R-corr.

We consider $L = 48$ and potential at $t=13$ (R-corr. t = 12, 13, 14).

```python
ls ../data/Rcorr
```

    Rcorr_xixi_1s0_L40_t010_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t010_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t011_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t011_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t012_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t012_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t013_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t013_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t014_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t014_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t015_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t015_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t016_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L40_t016_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t010_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t010_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t011_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t011_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t012_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t012_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t013_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t013_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t014_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t014_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t015_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t015_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t016_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L48_t016_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t010_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t010_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t011_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t011_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t012_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t012_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t013_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t013_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t014_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t014_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t015_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t015_wall_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t016_exp_20jkbins.pkl
    Rcorr_xixi_1s0_L64_t016_wall_20jkbins.pkl

```python
Rcorr_jk_compressed = {}
L = 48
src = 'wall'
it0 = 13
its = [it0-1, it0, it0+1] # list of t
bin_num = 20

for it in its:
    with open(f'../data/Rcorr/Rcorr_xixi_1s0_L{L}_t{it:03d}_{src}_20jkbins.pkl', 'rb') as fin:
        Rcorr_jk_compressed[it] = pickle.load(fin)
```

decompress R-corr.

```python
Rcorr_jk = {}
```

```python
def gen_xyz_index(L):
    a1_uniq = np.array([ix + L * (iy + L * iz)
                       for ix in range(0, L//2+1)
                       for iy in range(0, ix+1)
                       for iz in range(0, iy+1)])
    a1_uniq_ind = {}
    ind = 0
    for ix in range(0, L//2+1):
        for iy in range(0, ix+1):
            for iz in range(0, iy+1):
                a1_uniq_ind[(ix, iy, iz)] = ind
                ind += 1
    xyz_ind = {}
    for ix in range(L):
        for iy in range(L):
            for iz in range(L):
                _ind = np.sort([min(ix,L-ix), min(iy,L-iy), min(iz,L-iz)])[::-1]
                xyz_ind[(ix,iy,iz)] = a1_uniq_ind[(_ind[0], _ind[1], _ind[2])]
    return xyz_ind
```

```python
xyz_ind = gen_xyz_index(L)
for it in its:
    wave_full = np.zeros((bin_num, L, L, L))
    for ix in range(L):
        for iy in range(L):
            for iz in range(L):
                wave_full[:,ix,iy,iz] = Rcorr_jk_compressed[it][:,xyz_ind[(ix,iy,iz)]]
    Rcorr_jk[it] = wave_full.reshape(bin_num, L, L, L)
```

```python
rs = np.array([np.sqrt(ix**2 + iy**2 + iz**2)
              for ix in range(-L//2,L//2) for iy in range(-L//2,L//2) for iz in range(-L//2, L//2)])
rs = rs.reshape(L,L,L)
rs = np.roll(rs, (L//2,L//2,L//2), (0, 1, 2))
```

The spatial profile of the R-corr.

```python
Rcorr_jk[it0].shape
```

    (20, 48, 48, 48)

```python
fig, ax = plt.subplots()
fac = 1e6
for it, c, fmt in zip(its, ['red', 'blue', 'green'],
                      ['o', 's', 'v']):
    ax.errorbar(rs.flatten()*lat_unit, Rcorr_jk[it].mean(axis=0).flatten()*fac,
             Rcorr_jk[it].std(axis=0).flatten() * np.sqrt(bin_num - 1)*fac,
                color=c,  capsize=10, capthick=2, fmt=fmt,
               label=rf'$t/a = {it}$')

ax.set_xlabel(r'$r$ [fm]', size=48)
ax.set_ylabel(r'$R$-cor. $\times 10^{:d}$'.format(int(np.log10(fac))), size=48)
leg = ax.legend(frameon=True)
leg.get_frame().set_edgecolor('black')
leg.get_frame().set_linewidth(2.0)
ax.axhline(0, color='black')
```

# potential

\begin{equation}
V(r) = - \frac{H_0 R}{R} - \frac{(\partial/\partial t)R}{R}
+ \frac{1}{4m_B} \frac{(\partial/\partial t)^2 R}{R}
\end{equation}
Hamiltonian  $H_0 = - \frac{\nabla^2}{m_B}$

```python
# \nabla^2 operator
lap = lambda vec: - 6.0 * vec + ( np.roll(vec,+1,0) + np.roll(vec,-1,0)
                                + np.roll(vec,+1,1) + np.roll(vec,-1,1)
                                + np.roll(vec,+1,2) + np.roll(vec,-1,2))

it0  = 13
pot_jk = {}
pot_jk['lap'] = np.array([lap(Rcorr_jk[it0][ibin,:,:,:])/(mxi*Rcorr_jk[it0][ibin,:,:,:])
                          for ibin in range(bin_num)])

pot_jk['dt']  = np.array([ - (Rcorr_jk[it0+1][ibin,:,:,:] - Rcorr_jk[it0-1][ibin,:,:,:])
                          / (2 * Rcorr_jk[it0][ibin,:,:,:]) for ibin in range(bin_num)])

pot_jk['dt2'] = np.array([(Rcorr_jk[it0+1][ibin,:,:,:] - 2 * Rcorr_jk[it0][ibin,:,:,:]
                           + Rcorr_jk[it0-1][ibin,:,:,:])
                          / (4 * mxi * Rcorr_jk[it0][ibin,:,:,:]) for ibin in range(bin_num)])

pot_jk['tot'] = pot_jk['lap'] + pot_jk['dt'] + pot_jk['dt2']
```

```python
a1_uniq = np.array([ix + L * (iy + L * iz)
                   for ix in range(0, L//2+1)
                   for iy in range(0, ix+1)
                   for iz in range(0, iy+1)])
```

```python
# reduce redundant points
pot_jk_a1 = {}
for key in pot_jk.keys():
    pot_jk_a1[key] = pot_jk[key].reshape(bin_num, L**3)[:,a1_uniq]

rs_a1  = rs.flatten()[a1_uniq]
```

plot potential

```python
fig, ax = plt.subplots()
ax.errorbar(rs_a1*lat_unit, pot_jk_a1['tot'].mean(axis=0)*ainv,
           pot_jk_a1['tot'].std(axis=0)*np.sqrt(bin_num - 1)*ainv,
           fmt='o', color='red', mfc='none', mew=2.0, capsize=10, capthick=2.0, label='total')

ax.errorbar(rs_a1*lat_unit, pot_jk_a1['lap'].mean(axis=0)*ainv,
           pot_jk_a1['lap'].std(axis=0)*np.sqrt(bin_num - 1)*ainv,
           fmt='s', color='blue', mfc='none', mew=2.0, capsize=10, capthick=2.0, label=r'$H_0$-term')

ax.errorbar(rs_a1*lat_unit, pot_jk_a1['dt'].mean(axis=0)*ainv,
           pot_jk_a1['dt'].std(axis=0)*np.sqrt(bin_num - 1)*ainv,
           fmt='^', color='green', mfc='none', mew=2.0, capsize=10, capthick=2.0, label=r'$\partial/\partial t$-term')

ax.errorbar(rs_a1*lat_unit, pot_jk_a1['dt2'].mean(axis=0)*ainv,
           pot_jk_a1['dt2'].std(axis=0)*np.sqrt(bin_num - 1)*ainv,
           fmt='X', color='black', mfc='none', mew=2.0, capsize=10, capthick=2.0, label=r'$\partial^2/\partial t^2$-term')

ax.axhline(0, color='black')
ax.set_ylim(-50, 50)
ax.set_xlim(-0.05, 3.55)
ax.set_ylabel(r'$V(r)$ [MeV]', size=48)
ax.set_xlabel(r'$r$ [fm]', size=48)
leg = ax.legend(frameon=True)
leg.get_frame().set_edgecolor('black')
leg.get_frame().set_linewidth(2.0)
```

# eigenfunction of the finite volume hamiltonian

We consider the eigenfunction of the finite volume Hamiltonian, which is given by
\begin{equation}
H \equiv H_0 + V
\end{equation}
using the HAL QCD potential $V(r)$.

```python
# solve eigenvalue problem using scipy.sparse.linalg
Vol = L**3
ibin =  0
Nev = 5 # number of low-lying
pot = pot_jk['tot'][ibin,:,:,:]
H = LinearOperator((Vol, Vol),
                  matvec=lambda vec: pot.flatten() * vec
                    - lap(vec.reshape(L,L,L)).flatten()/mxi, dtype='float64')

vals, vecs  = scipy.sparse.linalg.eigsh(H,  which='SA', k=Nev)
```

```python
# extract A1 component of the wavefunction
def A1_projection(wave_in):
    Ns = round(len(wave_in)**(1/3))
    wave = wave_in.reshape(Ns,Ns,Ns)
    wave_tmp1 = (wave[:,:,:] + np.roll(wave,-1,0)[::-1,:,:]
                + np.roll(wave,-1,1)[:,::-1,:]
                + np.roll(wave,-1,2)[:,:,::-1]
                + np.roll(np.roll(wave,-1,0),-1,1)[::-1,::-1,:]
                + np.roll(np.roll(wave,-1,1),-1,2)[:,::-1,::-1]
                + np.roll(np.roll(wave,-1,2),-1,0)[::-1,:,::-1]
                + np.roll(np.roll(np.roll(wave,-1,0),-1,1),-1,2)[::-1,::-1,::-1])/8.0
    wave_tmp2 = (wave_tmp1
                + np.swapaxes(wave_tmp1,0,1)
                + np.swapaxes(wave_tmp1,1,2)
                + np.swapaxes(wave_tmp1,2,0)
                + np.swapaxes(np.swapaxes(wave_tmp1,0,1),1,2)
                + np.swapaxes(np.swapaxes(wave_tmp1,0,2),2,1))/6.0e0

    return wave_tmp2.flatten()

```

```python
# check eigenfunction and its A1 norm
print('# level   eigenval   A1norm')
for nth, val in enumerate(vals):
    vecs[:,nth] *= np.sign(vecs[0,nth]) # psi(0) > 0 positive
    a1proj = A1_projection(vecs[:,nth])
    a1norm  = np.dot(a1proj, a1proj)
    print(f'{nth}        {val*ainv:3.2f} [MeV] {a1norm:2.1f}')
```

    # level   eigenval   A1norm
    0        -2.86 [MeV] 1.0
    1        52.62 [MeV] 1.0
    2        53.10 [MeV] 0.0
    3        53.10 [MeV] 0.0
    4        53.10 [MeV] 0.0

The lowest and 1st excited states are A1 wavefunction.

```python
fig, ax = plt.subplots()
ax.plot(rs_a1*lat_unit, vecs[a1_uniq,0], 'ro', label=r'$\Psi_0$',
       mec='black',  mew=1)
ax.plot(rs_a1*lat_unit, vecs[a1_uniq,1], 'bs', label=r'$\Psi_1$',
       mec='black', mew=1)

leg = ax.legend(frameon=True)
leg.get_frame().set_edgecolor('black')
leg.get_frame().set_linewidth(2.0)

ax.set_xlim(-0.05, 3.55)
ax.set_xlabel(r'$r$ [fm]', size=48)
ax.set_ylabel(r'$\Psi$', size=48)
ax.axhline(0, color='black')
```

```python

```

notes/(Test) R-corr. to potential and eigenfunction.ipynb


# Orthogonal Random Forest: Use Cases and Examples

Orthogonal Random Forest (ORF) combines orthogonalization,
a technique that effectively removes the confounding effect in two-stage estimation,
with generalized random forests, a flexible method for estimating treatment effect heterogeneity. Due to the orthogonalization aspect of this method, the ORF performs especially well in the presence of high-dimensional confounders. For more details, see [this paper](https://arxiv.org/abs/1806.03467).

The EconML SDK implements the following OrthoForest variants:

* ContinuousTreatmentOrthoForest: suitable for continuous treatments

* DiscreteTreatmentOrthoForest: suitable for discrete treatments

In this notebook, we show the performance of the ORF on synthetic data.

**Notebook contents:**

1. Example usage with continuous treatment synthetic data

2. Example usage with binary treatment synthetic data

3. Example usage with multiple discrete treatment synthetic data

4. Example usage with real continuous treatment observational data

```python
import econml
```

```python
# Main imports
from econml.ortho_forest import ContinuousTreatmentOrthoForest, WeightedModelWrapper, DiscreteTreatmentOrthoForest

# Helper imports
import numpy as np
from itertools import product
from sklearn.linear_model import Lasso, LassoCV, LogisticRegression, LogisticRegressionCV
import matplotlib.pyplot as plt

%matplotlib inline
```

## 1. Example Usage with 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)

# ORF parameters and test data
# The following parameters are set according to theory
subsample_power = 0.88
subsample_ratio = ((n/np.log(n_w))**(subsample_power)) / n
lambda_reg = np.sqrt(np.log(n_w) / (10 * subsample_ratio * n))
X_test = np.array(list(product(np.arange(0, 1, 0.01), repeat=n_x)))
```

### 1.2. Train Estimator

**Note:** The models in the final stage of the estimation (``model_T_final``, ``model_Y_final``) need to support sample weighting.

If the models of choice do not support sample weights (e.g. ``sklearn.linear_model.LassoCV``), the ``econml`` packages provides a convenient wrapper for these models ``WeightedModelWrapper`` in order to allow sample weights.

If the model of choice is a linear (regression) model such as Lasso, you should set ``sample_type="weighted"``. Otherwise, set ``sample_type="sampled"``.

```python
est = ContinuousTreatmentOrthoForest(
    n_trees=200, min_leaf_size=5,
    max_depth=50, subsample_ratio=2*subsample_ratio, bootstrap=False,
    model_T=Lasso(alpha=lambda_reg),
    model_Y=Lasso(alpha=lambda_reg),
    model_T_final=WeightedModelWrapper(Lasso(alpha=lambda_reg), sample_type="weighted"),
    model_Y_final=WeightedModelWrapper(Lasso(alpha=lambda_reg), sample_type="weighted"),
    random_state=123)
```

```python
est.fit(Y, T, X, W)
```

    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    2.6s
    [Parallel(n_jobs=-1)]: Done 200 out of 200 | elapsed:    3.8s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.2s
    [Parallel(n_jobs=-1)]: Done 200 out of 200 | elapsed:    1.3s finished

    <econml.ortho_forest.ContinuousTreatmentOrthoForest at 0x11df7eda0>

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

    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.3s
    [Parallel(n_jobs=-1)]: Done 100 out of 100 | elapsed:    1.2s finished

### 1.3. Performance Visualization

```python
y = treatment_effects[:, 0]
plt.plot(X_test, y, label='ORF estimate')
expected_te = np.array([exp_te(x_i) for x_i in X_test])
plt.plot(X_test[:, 0], expected_te, 'b--', label='True effect')
plt.ylabel("Treatment Effect")
plt.xlabel("x")
plt.legend()
plt.show()
```

## 2. Example Usage with Binary Treatment Synthetic Data

### 2.1. DGP
We use the following DGP:

\begin{align}
T \sim & \text{Bernoulli}\left(f(W)\right), &\; f(W)=\sigma(\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, $\beta, \gamma$ have high sparsity and $\sigma$ is the sigmoid function.

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

```python
# DGP constants
np.random.seed(1234)
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])
# Define treatment
log_odds = np.dot(W[:, support_T], coefs_T) + eta_sample(n)
T_sigmoid = 1/(1 + np.exp(-log_odds))
T = np.array([np.random.binomial(1, p) for p in T_sigmoid])
# Define the outcome
Y = TE * T + np.dot(W[:, support_Y], coefs_Y) + epsilon_sample(n)

# ORF parameters and test data
# The following parameters are set according to theory
subsample_power = 0.88
subsample_ratio = ((n/np.log(n_w))**(subsample_power)) / n
lambda_reg = np.sqrt(np.log(n_w) / (10 * subsample_ratio * n))
X_test = np.array(list(product(np.arange(0, 1, 0.01), repeat=n_x)))
```

### 2.2. Train Estimator

```python
est = DiscreteTreatmentOrthoForest(
    n_trees=200, min_leaf_size=10,
    max_depth=30, subsample_ratio=2*subsample_ratio, bootstrap=False,
    propensity_model = LogisticRegression(C=1/(X.shape[0]*lambda_reg), penalty='l1', solver='saga'),
    model_Y = Lasso(alpha=lambda_reg),
    propensity_model_final=LogisticRegression(C=1/(X.shape[0]*lambda_reg), penalty='l1', solver='saga'),
    model_Y_final=WeightedModelWrapper(Lasso(alpha=lambda_reg), sample_type="weighted")
)
```

```python
est.fit(Y, T, X, W)
```

    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.2s
    [Parallel(n_jobs=-1)]: Done 200 out of 200 | elapsed:    1.4s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.2s
    [Parallel(n_jobs=-1)]: Done 118 out of 200 | elapsed:    1.0s remaining:    0.7s
    [Parallel(n_jobs=-1)]: Done 200 out of 200 | elapsed:    1.3s finished

    <econml.ortho_forest.DiscreteTreatmentOrthoForest at 0x11e7822e8>

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

    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.4s
    [Parallel(n_jobs=-1)]: Done 100 out of 100 | elapsed:    2.1s finished

### 2.3. Performance Visualization

```python
y = treatment_effects[:, 0]
plt.plot(X_test, y, label='ORF estimate')
expected_te = np.array([exp_te(x_i) for x_i in X_test])
plt.plot(X_test[:, 0], expected_te, 'b--', label='True effect')
plt.ylabel("Treatment Effect")
plt.xlabel("x")
plt.legend()
plt.show()
```

## 3. Example Usage with Multiple Treatment Synthetic Data

### 3.1 DGP
We use the following DGP:

\begin{align}
Y = & \sum_{t=1}^{n_{\text{treatments}}} 1\{T=t\}\cdot \theta_{T}(X) + \langle W, \gamma\rangle + \epsilon, \; \epsilon \sim \text{Unif}(-1, 1), \\
\text{Pr}[T=t \mid W] \propto & \exp\{\langle W, \beta_t \rangle\}, \;\;\;\; \forall t\in \{0, 1, \ldots, n_{\text{treatments}}\}
\end{align}

where $W$ is a matrix of high-dimensional confounders, $\beta_t, \gamma$ are sparse.

For this particular example DGP we used $n_{\text{treatments}}=3$ and
\begin{align}
\theta_1(x) = & \exp( 2 x_1 ),\\
\theta_2(x) = &  3 \cdot \sigma(100\cdot (x_1 - .5)),\\
\theta_3(x) = & -2 \cdot \sigma(100\cdot (x_1 - .25)),
\end{align}
where $\sigma$ is the sigmoid function.

```python
def get_test_train_data(n, n_w, support_size, n_x, te_func, n_treatments):
    # 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, n_treatments))
    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([te_func(x_i, n_treatments) for x_i in X])
    log_odds = np.dot(W[:, support_T], coefs_T)
    T_sigmoid = np.exp(log_odds)
    T_sigmoid = T_sigmoid/np.sum(T_sigmoid, axis=1, keepdims=True)
    T = np.array([np.random.choice(n_treatments, p=p) for p in T_sigmoid])
    TE = np.concatenate((np.zeros((n,1)), TE), axis=1)
    Y = TE[np.arange(n), T] + np.dot(W[:, support_Y], coefs_Y) + epsilon_sample(n)
    X_test = np.array(list(product(np.arange(0, 1, 0.01), repeat=n_x)))

    return (Y, T, X, W), (X_test, np.array([te_func(x, n_treatments) for x in X_test]))
```

```python
import scipy.special
def te_func(x, n_treatments):
    return [np.exp(2*x[0]), 3*scipy.special.expit(100*(x[0] - .5)) - 1, -2*scipy.special.expit(100*(x[0] - .25))]

np.random.seed(123)
(Y, T, X, W), (X_test, te_test) = get_test_train_data(1000, 3, 3, 1, te_func, 4)
```

### 3.2 Train Estimator

```python
est = DiscreteTreatmentOrthoForest(n_trees=500,
                                   model_Y = WeightedModelWrapper(Lasso(alpha=lambda_reg)))
```

```python
est.fit(Y, T, X, W)
```

    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.5s
    [Parallel(n_jobs=-1)]: Done 208 tasks      | elapsed:    4.4s
    [Parallel(n_jobs=-1)]: Done 500 out of 500 | elapsed:   10.6s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.5s
    [Parallel(n_jobs=-1)]: Done 208 tasks      | elapsed:    4.7s
    [Parallel(n_jobs=-1)]: Done 500 out of 500 | elapsed:   11.1s finished

    <econml.ortho_forest.DiscreteTreatmentOrthoForest at 0x11e4f0978>

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

    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:    0.7s
    [Parallel(n_jobs=-1)]: Done 100 out of 100 | elapsed:    3.5s finished

### 3.3 Performance Visualization

```python
y = treatment_effects
for it in range(y.shape[1]):
    plt.plot(X_test, y[:, it], label='ORF estimate T={}'.format(it))
    plt.plot(X_test[:, 0], te_test[:, it], '--', label='True effect T={}'.format(it))
plt.ylabel("Treatment Effect")
plt.xlabel("x")
plt.legend()
plt.show()
```

## 4. Example usage with real 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 `ContinuousTreatmentOrthoForest` 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.

### 4.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)
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']]].values)
W2 = pd.get_dummies(oj_data[['brand']]).values
W = np.concatenate([W1, W2], axis=1)
X = oj_data[['INCOME']].values
```

### 4.2. Train Estimator

```python
# Define some parameters
n_trees = 2000
min_leaf_size = 50
max_depth = 20
subsample_ratio = 0.02
bootstrap = False
```

```python
est = ContinuousTreatmentOrthoForest(
        n_trees=n_trees, min_leaf_size=min_leaf_size, max_depth=max_depth,
        subsample_ratio=subsample_ratio, bootstrap=bootstrap,
        model_T=Lasso(alpha=0.1),
        model_Y=Lasso(alpha=0.1),
        model_T_final=WeightedModelWrapper(LassoCV(cv=3), sample_type="weighted"),
        model_Y_final=WeightedModelWrapper(LassoCV(cv=3), sample_type="weighted")
       )
```

```python
est.fit(Y, T, X, W)
```

    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 852 tasks      | elapsed:    1.0s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.9s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 940 tasks      | elapsed:    1.1s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    2.0s finished

    <econml.ortho_forest.ContinuousTreatmentOrthoForest at 0x12d8bf908>

```python
min_income = 10.0
max_income = 11.1
delta = (max_income - min_income) / 100
X_test = np.arange(min_income, max_income + delta - 0.001, delta).reshape(-1, 1)
```

```python
import time
t0 = time.time()
te_pred = est.const_marginal_effect(X_test)
print(time.time() - t0)
```

    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   30.0s

    198.3771231174469

    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.3min finished

### 4.3. Performance Visualization

```python
# Plot Oranje Juice elasticity as a function of income
plt.plot(np.ndarray.flatten(X_test), te_pred[:, 0], label="OJ Elasticity")
plt.xlabel(r'$\log$(Income)')
plt.ylabel('Orange Juice Elasticity')
plt.legend()
plt.title("Orange Juice Elasticity vs Income")
plt.show()
```

### 4.4 Bootstrap Confidence Intervals

We can also use a bootstrap estimator to generate confidence intervals; in order to return results in a few minutes we're limiting the number of trees to 100 and the number of bootstrap samples to 10 in the code below, but for better estimates these numbers can be increased at the cost of increased runtime.

```python
from econml.bootstrap import BootstrapEstimator
boot_est = BootstrapEstimator(ContinuousTreatmentOrthoForest(
        n_trees=100, min_leaf_size=min_leaf_size, max_depth=max_depth,
        subsample_ratio=subsample_ratio, bootstrap=bootstrap,
        model_T=Lasso(alpha=0.1),
        model_Y=Lasso(alpha=0.1),
        model_T_final=WeightedModelWrapper(LassoCV(cv=3), sample_type="weighted"),
        model_Y_final=WeightedModelWrapper(LassoCV(cv=3), sample_type="weighted")
       ), n_bootstrap_samples=10, n_jobs=-1)
```

```python
boot_est.fit(Y, T, X, W)
te_pred_interval = boot_est.const_marginal_effect_interval(X_test, lower=1, upper=99)
```

    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 582 tasks      | elapsed:    0.8s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 800 tasks      | elapsed:    1.0s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 1142 tasks      | elapsed:    1.2s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 632 tasks      | elapsed:    0.8s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.8s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 236 tasks      | elapsed:    0.4s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 688 tasks      | elapsed:    0.8s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 458 tasks      | elapsed:    0.6s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.8s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 1376 tasks      | elapsed:    1.6s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    2.0s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 196 tasks      | elapsed:    0.4s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 224 tasks      | elapsed:    0.4s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.8s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 534 tasks      | elapsed:    0.8s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 276 tasks      | elapsed:    0.5s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 112 tasks      | elapsed:    0.2s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.8s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 438 tasks      | elapsed:    0.6s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 880 tasks      | elapsed:    1.0s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.9s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 436 tasks      | elapsed:    0.7s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    2.0s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  64 tasks      | elapsed:    0.3s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    2.1s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 752 tasks      | elapsed:    0.9s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.8s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 672 tasks      | elapsed:    0.8s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.8s finished
    [Parallel(n_jobs=-1)]: Using backend LokyBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done 960 tasks      | elapsed:    1.1s
    [Parallel(n_jobs=-1)]: Done 2000 out of 2000 | elapsed:    1.7s finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   31.0s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.4min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   31.9s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.5min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   31.8s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.5min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   31.0s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.3min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   30.9s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.4min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   30.7s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.3min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   28.4s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.3min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   28.2s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.4min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   29.2s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.3min finished
    [Parallel(n_jobs=-1)]: Using backend ThreadingBackend with 8 concurrent workers.
    [Parallel(n_jobs=-1)]: Done  16 tasks      | elapsed:   29.8s
    [Parallel(n_jobs=-1)]: Done 101 out of 101 | elapsed:  3.3min finished

```python
plt.plot(np.ndarray.flatten(X_test), te_pred[:, 0], label="OJ Elasticity")
plt.fill_between(np.ndarray.flatten(X_test),
                 te_pred_interval[0][:, 0],
                 te_pred_interval[1][:, 0], alpha=.5, label="1-99% CI")
plt.xlabel(r'$\log$(Income)')
plt.ylabel('Orange Juice Elasticity')
plt.title("Orange Juice Elasticity vs Income")
plt.legend()
plt.show()
```

```python

```

notebooks/Orthogonal Random Forest Examples.ipynb


# Coaddition and Subtraction

Version 0.1

By Yusra AlSayyad (Princeton University)

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

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

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

### Problem 1) Expected depth of a coadd

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

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

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

    /Users/rmorgan/anaconda3/envs/DSFP/lib/python3.6/site-packages/matplotlib/style/core.py:167: UserWarning: In /Users/rmorgan/.matplotlib/stylelib/alex.mplstyle:
    The text.latex.unicode rcparam was deprecated in Matplotlib 2.2 and will be removed in 3.1.
      styles = read_style_directory(stylelib_path)

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

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

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

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

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

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

    Returns
    -------
    flux : array-like of shape (n_pixels,)
        Flux in each pixel of the input array
    """
    sigma = fwhm / (2 * np.sqrt(2 * np.log(2)))
    flux = norm(loc=mu, scale=sigma)
    return flux.pdf(x)

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

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

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

    Returns
    -------
    noisy_counts : array-like (same shape as x)
        the (noisy) number of counts in each pixel
    """
    flux = phi(x, mu, fwhm)
    signal = flux * F + S

    noisy_counts = np.random.normal(loc=signal, scale=np.sqrt(signal))

    return noisy_counts
```

### Problem 1.1) Make a simple mean coadd

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

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

x = np.arange(100)

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

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

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

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

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

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

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

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

```

```python

# Plot the coadds of N=[1, 10, and 100] on the same plot:
# complete
fig, ax = pixel_plot(x, np.mean(simulateN(x, MU, FWHM, S, F, 1), axis=0), label="N=1")
fig, ax = pixel_plot(x, np.mean(simulateN(x, MU, FWHM, S, F, 10), axis=0), label="N=10", fig=fig, ax=ax)
fig, ax = pixel_plot(x, np.mean(simulateN(x, MU, FWHM, S, F, 100), axis=0), label="N=100", fig=fig, ax=ax)
ax.legend()
```

### Problem 1.2)  SNR vs N

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

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

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

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

```python
std, flux, flux_err = [], [], []

Ns = np.arange(1, 1001)
coadd = simulateN(x, MU, FWHM, S, F, Ns.max())
for idx in range(len(Ns)):
    running_coadd = np.mean(coadd[0:idx, :], axis=0)
    std.append(np.std(running_coadd[70:]))
    psf_flux = running_coadd.sum() - S*len(x)
    flux.append(psf_flux)
    flux_err.append(np.sqrt(psf_flux))

```

```python
plt.figure()
plt.step(Ns, std, label="Measured")
plt.step(Ns, np.sqrt(S)/np.sqrt(Ns), label="Expected")
plt.xlabel('Coadd Stack Depth')
plt.ylabel('Pixel Noise')
plt.legend()
plt.show()
```

```python
plt.figure()
plt.step(Ns, np.array(flux) / np.array(flux_err), label="Measured")
plt.step(Ns, [F / np.sqrt(S)] * len(Ns), label="Expected")
plt.xlabel('Coadd Stack Depth')
plt.ylabel('Pixel Noise')
plt.legend()
plt.show()
```

```python

```

## Problem 2) PSFs and Image weights in coadds

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

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

#### Some background:

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

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

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

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

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

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

### Problem 2.1  Weighting images in Variable Sky

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

```python
S_arr = np.linspace(100.0, 1000.0, 50)
stars = np.empty((len(S_arr), len(x)))
for jj in range(len(S_arr)):
    stars[jj,:] = simulate(x, MU, FWHM, S_arr[jj], F)
```

```python
mean_coadd = np.mean(stars - S_arr[:,np.newaxis], axis=0)
weights = 1. / np.std(stars, axis=0) ** 2
weights = weights / weights.sum()
min_var_coadd = np.sum(stars - S_arr[:,np.newaxis], axis=0) * weights
```

```python
fig, ax = pixel_plot(x, mean_coadd, label="mean")
pixel_plot(x, min_var_coadd, fig=fig, ax=ax, label="min var")
ax.legend()
```

### Problem 2.2  Weighting images in Variable Seeing

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

```python
fwhm_arr = np.linspace(2.0, 10.0, 50)
stars = np.empty((len(fwhm_arr), len(x)))
for jj in range(len(fwhm_arr)):
    stars[jj,:] = simulate(x, MU, fwhm_arr[jj], S, F)
```

```python
weights = 1. / np.std(stars, axis=0) ** 2
weights = weights / weights.sum()
min_var_coadd = np.sum(stars - S, axis=0) * weights

weights = np.sum(stars, axis=0) / np.std(stars, axis=0)
weights = weights / weights.sum()
max_snr_coadd = np.sum(stars - S, axis=0) * weights
```

```python
fig, ax = pixel_plot(x, max_snr_coadd, label="max snr")
pixel_plot(x, min_var_coadd, fig=fig, ax=ax, label="min var")
ax.legend()
```

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

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

```python
# complete
```

## Problem 3) Dipoles in Image Subtraction

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

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

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

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

```

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

```python
diff_1 = y1 - y2
plt.plot(x, diff_1)
```

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

```python
ASTROM_OFFSET = 0.0001
PSF = 1.
FLUX_SCALE = 1000

x = np.linspace(-5, 5)
y1 = FLUX_SCALE * norm.pdf(x, ASTROM_OFFSET, PSF)
y2 = FLUX_SCALE * norm.pdf(x, -ASTROM_OFFSET, PSF)

# Plot both dipoles (for the offset=0.1 and the offset=0.0001 in the same figure.
# Same or different subplots up to you.
diff_2 = y1 - y2

plt.plot(x, diff_1, label="0.1")
plt.plot(x, diff_2, label="0.0001")
plt.legend()
```

### Problem 3.3) Distance between peaks.

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

```python
ASTROM_OFFSET_arr = np.logspace(-3,0, 50)  # units of e.g. pixels
FLUX_SCALE = 1. # units of e.g nanojansky
PSF = 1  # rms pixels

x = np.linspace(-5, 5)
peak_dists = []
for ASTROM_OFFSET in ASTROM_OFFSET_arr:
    y1 = FLUX_SCALE * norm.pdf(x, ASTROM_OFFSET, PSF)
    y2 = FLUX_SCALE * norm.pdf(x, -ASTROM_OFFSET, PSF)
    peak_dists.append(np.abs(x[np.argmax(y1)] - x[np.argmax(y2)]))

plt.figure()
plt.plot(ASTROM_OFFSET_arr, peak_dists)
plt.xscale('log')
plt.show()
```

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


# Simulate CFHT Images Using HST Ones

In this notebook, we are going to simulate step by step a CFHT ground based telescope image using a HST one.

## 1. Preparing the workspace

First things first, we start by preparing the workspace.

```python
# to correctly show figures
%matplotlib inline

# import libraries here
import galsim
from astropy.io import fits
import numpy as np
from seeing_distribution_class import seeing_distribution
import matplotlib.pyplot as plt
```

Here we are going to load the CFHT and HST required parameters.
> CFHT telescope specifications can be found on [CFHT webpage](https://www.cfht.hawaii.edu/Instruments/Imaging/Megacam/generalinformation.html) (`r` filter and `Dark sky, Moon 0%` for the sky brightness) and [inside of LSST GitHub page](https://github.com/LSSTDESC/WeakLensingDeblending/blob/9f851f79f6f820f815528d11acabf64083b6e111/descwl/survey.py#L288) (for the telescope effective area).

```python
pixel_scale_hst = 0.03 # HST pixel scale
pixel_scale = 0.187 # CFHT pixel scale
wcs = galsim.wcs.PixelScale(pixel_scale) #wcs: world coordinate system. Variable used to draw images in galsim
exp_time = 200 #seconds # exposure time #value corresponding to CFIS # provided by A. Guinot
sky_brightness = 21.30 #mag/arcsec^2 # for Dark sky, Moon 0%
zero_point = 10.72 #ADU/sec
cfht_eff_area = 8.022 #m^2 #effective area
qe = 0.77 # Quantum Efficiency (converts photon number to electrons)
gain = 1.62 #e-/ADU #converts electrons to ADU
```

Load the [COSMOS](https://github.com/GalSim-developers/GalSim/wiki/RealGalaxy%20Data) catalog and generate a galaxy and a PSF.

```python
catalog = galsim.COSMOSCatalog(dir='/gpfswork/rech/xdy/uze68md/data/COSMOS_25.2_training_sample/') # load catalog
```

## 2. Loading the Galaxy and Building the PSF

The catalog `COSMOS_25.2` contains galaxies with magnitude values up to 25.2. The maximal depth of CFIS is below the magnitude 24 (more or less). Beyond this magnitude, if a galaxy is simulated as observed by CFIS, it will not be visible because it will be fully submerged by noise.

One solution is to use the catalog `COSMOS_23.5` which contains galaxies of magnitude below 23.5.

Another solution is to filter the galaxies in the catalog considering their magnitude. To do so:
1. Fetch the `original index` of the galaxy. It is done using the method `catalog.getOrigIndex`.
2. Then with this index, print the galaxy information from the catalog. It is done using the method `catalog.real_cat.cat[orig_ind]`. The galaxy magnitude is the 4$^\text{th}$ entry.
> To get the labels of the entries concerning the galax information run the command `print(catalog.real_cat.cat.dtype)`.

Now choose a galaxy index and verify that its magnitude is below 24.

```python
gal_ind = 142 # galaxy index
orig_ind = catalog.getOrigIndex(gal_ind)
print('The entries are :')
print(catalog.real_cat.cat.dtype)
mag_gal = catalog.real_cat.cat[orig_ind][3] # fetch galaxy magnitude
print('\nThe magnitude of galaxy #{0} is {1}'.format(gal_ind, mag_gal))
```

    The entries are :
    (numpy.record, [('IDENT', '>i4'), ('RA', '>f8'), ('DEC', '>f8'), ('MAG', '>f8'), ('BAND', 'S5'), ('WEIGHT', '>f8'), ('GAL_FILENAME', 'S32'), ('PSF_FILENAME', 'S36'), ('GAL_HDU', '>i4'), ('PSF_HDU', '>i4'), ('PIXEL_SCALE', '>f8'), ('NOISE_MEAN', '>f8'), ('NOISE_VARIANCE', '>f8'), ('NOISE_FILENAME', 'S26'), ('stamp_flux', '>f8')])

    The magnitude of galaxy #142 is 20.799999237060547

Load galaxy and build PSF.

```python
img_len = 64 # arbitrary value, practical because power of 2
gal = catalog.makeGalaxy(gal_ind, noise_pad_size=img_len * pixel_scale_hst * np.sqrt(2))
seed = 1995
fwhm_sampler = seeing_distribution('seeing_distribution.npy',seed=seed)
cfht_fwhm = fwhm_sampler.get(1)[0]
psf = galsim.Kolmogorov(fwhm=cfht_fwhm,flux=1.0)#, scale_unit=galsim.arcsec)
                                                #ommit units and let be handled withinin wcs

# shear the PSF
np.random.seed(seed=seed)

g_sigma = 0.01 # standard deviation of the shear distribution
def get_g(n_g):
    g = np.random.normal(0., g_sigma, n_g)
    while np.linalg.norm(g) > 1:
        g = np.random.normal(0., g_sigma, n_g)
    return g

e1, e2 = get_g(2) #+[cst1, cst2] because the mean of the PSF ellipticity can be different from zero
psf = psf.shear(g1=e1, g2=e2)
```

Now that we have loaded a galaxy from the catalog, let's rescale its flux such that it corresponds to a CFHT flux.

> Flux rescaling formula telescope specifications can be found [here](https://github.com/GalSim-developers/GalSim/blob/releases/2.2/examples/demo11.py#L110). Additionally we need to convert the flux from $photons/cm^2/s$ to ADU , for more information check the [GalSim page on units](https://galsim-developers.github.io/GalSim/_build/html/units.html#flux-units).

```python
hst_eff_area = 2.4**2 * (1.-0.33**2)
flux_scaling = (cfht_eff_area/hst_eff_area) * exp_time * qe / gain
gal *= flux_scaling
```

Apply the simulated CFHT PSF on the galaxy image.

```python
gal = galsim.Convolve(gal, psf)
```

Let's have a look at the galaxy and the PSF.

```python
gal_im = gal.drawImage(wcs=wcs, nx=img_len,ny=img_len) # Turn the galsim Galaxy object into a galsim Image object

plt.figure(figsize=(20,20))
plt.subplot(121)
plt.title('Convolved COSMOS Galaxy {}'.format(gal_ind))
plt.imshow(gal_im.array)
plt.colorbar(fraction=0.046, pad=0.04)
plt.subplot(122)
plt.imshow(psf.drawImage(wcs=wcs, nx=img_len,ny=img_len).array)
plt.title(r'CFHT PSF')
plt.colorbar(fraction=0.046, pad=0.04)
plt.show()
```

## 3. Adding Noise

The noise that we see in image above corresponds to HST noise (depending on the choice of the galaxy indice it may be barely visible).This noise correlated due to the division by the HST PSF (in Fourier space) and the multiplication by the CFHT one.

Now, we are going to adapt this noise to CFHT. First we give CFHT global noise variance. Its value correspond to the `mean_sky_level`.

> The $\lambda$ parameter corresponds to the `mean_sky_level` which expression can be find [here](https://github.com/LSSTDESC/WeakLensingDeblending/blob/9f851f79f6f820f815528d11acabf64083b6e111/descwl/survey.py#L110)

```python
def get_flux(ab_magnitude):
    # compared to the formula on github, we divide by the gain to convert the flux from e- to ADU
    return exp_time*zero_point*10**(-0.4*(ab_magnitude-24)) / gain

mean_sky_level = get_flux(sky_brightness) * pixel_scale ** 2
# mean_sky_level = np.sqrt(mean_sky_level_real)
print('mean_sky_level_sqrt',mean_sky_level)

sigma = np.sqrt(mean_sky_level) # we modelize the noise as a Gaussian noise such that it std
                                # is the sqrt of the Poisson parameter
print('CFHT global noise standard deviation: {:.2f}'.format(sigma))
```

    mean_sky_level_sqrt 556.4075053722729
    CFHT global noise standard deviation: 23.59

Then we estimate the value of HST noise variance and we take it into to account while adding the noise such that we end up with CFHT noise variance.
> Reminder: For any independent random variables, the variance of the sum of those variables is equal to the sum of the variances.

> Technical precision: Usually the noise standard deviation is estimated with more accurate methods such as using a window to mask the galaxy then estimate the standard deviation on the rest of the samples which only contain noise. For sake of simplicity, in this example, we considered an arbitrary area of the image that only contains noise and estimated the noise standard deviation in it.

```python
# Add noise

# Get the standard deviation value of the noise for real images
# Empirically estimate the standard deviation by considering a part of the image containing only noise
hst_std = np.std(gal_im.array[0:25,0:25])
hst_var = np.var(gal_im.array[0:25,0:25])

print('HST noise standard deviation: {:.2f}'.format(hst_std))

delta_var = mean_sky_level - hst_var
random_seed = 24783923 #same as galsim demo 11
noise = galsim.GaussianNoise(galsim.BaseDeviate(random_seed), sigma=np.sqrt(delta_var))
# noise = galsim.PoissonNoise(rng=galsim.BaseDeviate(random_seed), sky_level=delta_var)
gal_im.addNoise(noise)
image = gal_im.array
```

    HST noise standard deviation: 0.80

Now that we simulated the CFIS observed image, let's show it and estimate its standard deviation as a check. By noting the galaxy image $X$, we try to visually separate the noise (which standard deviation is noted $\sigma$) from the useful signal by applying the following transform:
\begin{equation}
\text{ArcSinh}\left(\frac{X}{k\sigma}\right).k\sigma
\end{equation}

```python
plt.figure(2, figsize=(10,10))
k=4
plt.imshow(np.arcsinh(image/(k*sigma)*k*sigma))
plt.colorbar(fraction=0.046, pad=0.04)
plt.title('ArcSinh of CFHT Image')
plt.show()
print('Standard Deviation Value of CFHT Simulated Image: {:.2f}'.format(np.std(image[0:25,0:25])))
```

```python
plt.figure(2, figsize=(10,10))
plt.imshow(image)
plt.colorbar(fraction=0.046, pad=0.04)
plt.title('CFHT Image')
plt.show()
```


```python
try:
    from openmdao.utils.notebook_utils import notebook_mode
except ImportError:
    !python -m pip install openmdao[notebooks]
```

# Optimizing the Sellar Problem
In the previous tutorials we showed you how to define the Sellar model and run it directly. Now let’s see how we can optimize the Sellar problem to minimize the objective function. Here is the mathematical problem formulation for the Sellar optimization problem:

\begin{align}
\text{min}: & \ \ \ & x_1^2 + z_2 + y_1 + e^{-y_2} \\
\text{w.r.t.}: & \ \ \ &  x_1, z_1, z_2 \\
\text{subject to}: & \ \ \ & \\
& \ \ \ & 3.16 - y_1 <=0 \\
& \ \ \ & y_2 - 24.0 <=0
\end{align}

Remember that we built our Sellar model as follows:

```python
import openmdao.api as om

class SellarMDA(om.Group):
    """
    Group containing the Sellar MDA.
    """

    def setup(self):
        cycle = self.add_subsystem('cycle', om.Group(), promotes=['*'])
        cycle.add_subsystem('d1', SellarDis1(), promotes_inputs=['x', 'z', 'y2'],
                            promotes_outputs=['y1'])
        cycle.add_subsystem('d2', SellarDis2(), promotes_inputs=['z', 'y1'],
                            promotes_outputs=['y2'])

        cycle.set_input_defaults('x', 1.0)
        cycle.set_input_defaults('z', np.array([5.0, 2.0]))

        # Nonlinear Block Gauss Seidel is a gradient free solver
        cycle.nonlinear_solver = om.NonlinearBlockGS()

        self.add_subsystem('obj_cmp', om.ExecComp('obj = x**2 + z[1] + y1 + exp(-y2)',
                                                  z=np.array([0.0, 0.0]), x=0.0),
                           promotes=['x', 'z', 'y1', 'y2', 'obj'])

        self.add_subsystem('con_cmp1', om.ExecComp('con1 = 3.16 - y1'), promotes=['con1', 'y1'])
        self.add_subsystem('con_cmp2', om.ExecComp('con2 = y2 - 24.0'), promotes=['con2', 'y2'])
```

All the variables we need to set up the optimization are there. So now we just need the run script to execute the optimization.

```python
from openmdao.utils.notebook_utils import get_code
from myst_nb import glue
glue("code_src7", get_code("openmdao.test_suite.components.sellar_feature.SellarMDA"), display=False)
```

:::{Admonition} `SellarMDA` class definition
:class: dropdown

{glue:}`code_src7`
:::

```python
import openmdao.api as om
from openmdao.test_suite.components.sellar_feature import SellarMDA

prob = om.Problem()
prob.model = SellarMDA()

prob.driver = om.ScipyOptimizeDriver()
prob.driver.options['optimizer'] = 'SLSQP'
# prob.driver.options['maxiter'] = 100
prob.driver.options['tol'] = 1e-8

prob.model.add_design_var('x', lower=0, upper=10)
prob.model.add_design_var('z', lower=0, upper=10)
prob.model.add_objective('obj')
prob.model.add_constraint('con1', upper=0)
prob.model.add_constraint('con2', upper=0)

# Ask OpenMDAO to finite-difference across the model to compute the gradients for the optimizer
prob.model.approx_totals()

prob.setup()
prob.set_solver_print(level=0)

prob.run_driver()

print('minimum found at')
print(prob.get_val('x')[0])
print(prob.get_val('z'))

print('minumum objective')
print(prob.get_val('obj')[0])
```

```python
from openmdao.utils.assert_utils import assert_near_equal

assert_near_equal(prob.get_val('x')[0], 0., 1e-5)
assert_near_equal(prob.get_val('z'), [1.977639, 0.], 1e-5)
assert_near_equal(prob.get_val('obj')[0], 3.18339395045, 1e-5)
```

## Controlling the Solver Print Output
Notice the call to `prob.set_solver_print()`,
which sets the solver output to level 0.
This is the semi-quiet setting where you will only be notified if the solver failed to converge.
There are lots of ways to [configure the solver print](../../features/core_features/controlling_solver_behavior/solver_options) output in your model to suit your needs.

## Approximate the total derivatives with finite difference

In this case we're using the `SLSQP` algorithm, which is a gradient-based optimization approach.
Up to this point, none of our components have provided any analytic derivatives,
so we'll just finite difference across the whole model to approximate the derivatives.
This is accomplished by this line of code:
```
prob.model.approx_totals()
```

```{Note}
We're using finite difference here for simplicity, but for larger models, finite differencing results in a high computational cost, and can have limited accuracy.
It's much better to use analytic derivatives with your models. You can learn more about that in the [Advanced User Guide](../../advanced_user_guide/advanced_user_guide).
```


```python
from IPython.display import Image
from IPython.core.display import HTML
from sympy import *; x,h,y,t = symbols("x h y t")
Image(url= "https://i.imgur.com/sCApXGG.png")
```

```python
# source https://nathancarter.github.io/how2data/site/how-to-find-the-critical-numbers-of-a-function-in-python-using-sympy/
f = (4*x)/(8*x+2)
d = diff(f)
#solve(Eq(d,0)),
Min(f,6).subs(x,2), Max(f,6).subs(x,2),f.subs(x,6)
```

    (4/9, 6, 12/25)

```python
p = plot(f,d,show = False, legend = True, )#xlim = (-0.4,0.4), ylim = (-110,110))
p[1].line_color = 'red'
p.show()

```

```python
Image(url= "https://i.imgur.com/NaigCSh.png")
```

```python

```


```python
import sympy as sp
from sympy.parsing.sympy_parser import parse_expr
import pandas as pd
```

```python
#This functions returns the lines from a .txt file
def get_lines(filename):
    file = open(filename, 'r+')
    lines = file.readlines()
    lines = map(lambda line : line[:-1],lines)
    file.close()
    return lines
```

```python
#This function returns the type of the statement
def get_type(line):
    #If not line.lower().find('x') returns true if x starts at line[0]
    if not line.lower().find('inicio'):
        return "inicio"
    if not line.lower().find('pare'):
        return "pare"
    if not line.lower().find('para'):
        return "para"
    if not line.lower().find('lea'):
        return "lea"
    if not line.lower().find('esc'):
        return "esc"
    if not line.lower().find('fpara'):
        return "fpara"
    if not line.lower().find('sino'):
        return "sino"
    if not line.lower().find('si'):
        return "si"
    if not line.lower().find('fsi'):
        return "fsi"
    if line.lower().find('='):
        return "assignment"
    return "Indefinite so far"
```

```python
#This function extracts for loop control data: bounds and increment
def process_for(line):
        raw_data = line[line.index('=')+1:]
        split_data = raw_data.split(',')
        lower_bound = parse_expr(split_data[0])
        upper_bound = parse_expr(split_data[1])
        increment = parse_expr(split_data[2])
        return {
            'lower_b' : lower_bound,
            'upper_b' : upper_bound,
            'inc' : increment
        }
```

```python
#Finds the number of comparisons in an if statement
def process_if(line):
    comparisons = [pos for pos, char in enumerate(line) if char == '(']
    return {
        'comparisons' : len(comparisons)
    }
```

```python
#First pass at getting statements runtime. Omits runtime for for loops and conditionals
def new_get_statement_runtime(syntax):
    lines_dict_list = lines = syntax.to_dict('records')
    order = 0
    for index in range(len(lines_dict_list)):
        line = lines[index]
        line_type = line['type']
        if line_type in ('inicio','pare','sino'):
            #order does not change
            line['runtime'] = 0
            line['order'] = order
        elif line_type in ("assignment",'lea','esc'):
            line['runtime'] = 1
            line['order'] = order
        elif  line_type in ('fsi','fpara'):
            order -= 1
            line['runtime'] = 0
            line['order'] = order
        elif line_type is 'para':
            line['runtime'] = 'Nan'
            line['data'] = process_for(line['line'])
            line['order'] = order
            order += 1
        elif line_type is 'si':
            line['runtime'] = 'Nan'
            line['data'] = process_if(line['line'])
            line['order'] = order
            order +=1

    return pd.DataFrame.from_dict(lines)
```

```python
#returns the runtime for an if block
def get_if_block_runtime(block_lines):
    runtime = 0
    for line in block_lines:
        runtime += line['runtime']
    return runtime
```

```python
#Returns an updated dataframe with the if statements runtime calculed
def get_if_blocks_runtime(syntax):
    lines_dict_list = lines = syntax.to_dict('records')
    if_indices = [pos for pos, line in enumerate(lines) if line['type'] is 'si']
    else_indices = [pos for pos, line in enumerate(lines) if line['type'] is 'sino']
    end_if_indices = [pos for pos, line in enumerate(lines) if line['type'] is 'fsi']
    # done = False

    #Let's begin by processing the ifs statements
    if_statements = []
    for x, if_index in enumerate(if_indices):
        #Find closing endif
        end_if_index = end_if_indices[x]
        #Is there an else?
        else_index = False
        for line_index in range(if_index,end_if_index):
            if  line_index in else_indices:
                else_index = line_index
                break
        # print((if_index,else_index,end_if_index))
        comparisons = lines[if_index]['data']['comparisons']
        if_runtime = comparisons
        if else_index:
            block_a = lines[if_index+1:else_index]
            block_b = lines[else_index+1: end_if_index]
            bloc_a_runtime = get_if_block_runtime(block_a)
            bloc_b_runtime = get_if_block_runtime(block_b)
            if_runtime += max(bloc_a_runtime,bloc_b_runtime)
        else:
            block = lines[if_index+1:end_if_index]
            bloc_runtime = get_if_block_runtime(block)
            if_runtime += bloc_runtime
        # print((if_index,else_index,end_if_index,if_runtime))
        lines[if_index]['runtime'] = if_runtime
    return pd.DataFrame.from_dict(lines)
```

```python
#Returns runtime of a for block
def for_runtime_formula(for_data,content_runtime):
    lower_bound = for_data['lower_b']
    upper_bound = for_data['upper_b']
    try:
        lower_bound = int(lower_bound)
    except TypeError:
        lower_bound = lower_bound
    try:
        upper_bound = int(upper_bound)
    except TypeError:
        upper_bound = upper_bound

    increment = parse_expr(str(for_data['inc']))
    if increment < 0:
        lower_bound, upper_bound = upper_bound, lower_bound
        increment = -1 * increment
    ceil = sp.Function('ceil')
    iterations = (ceil((upper_bound-lower_bound+1)/increment)*(content_runtime +2)) + 2
    return iterations
```

```python
#For blocks are classified in inner (order 1) and outer (order 0). Runtime of inners are calculated first in order to calculate runtime of outers. If a for block is not nested it is assumed to be inner since its runtime does not depend on other for loop. The for order should not be confused with the line order (regarding indentation).
def get_for_blocks_runtime(syntax):
    lines_dict_list = lines = syntax.to_dict('records')
    for_indices = [pos for pos, line in enumerate(lines) if line['type'] is 'para']
    # print('for_indices',for_indices)
    endfor_indices = [pos for pos,line in enumerate(lines) if line['type'] is 'fpara']
    # print('endfor_indices',endfor_indices)
    #get for blocks and their orders
    block_orders = []
    for x ,for_index in enumerate(for_indices):
        if x < len(for_indices) -1 :
            next_end_for = endfor_indices[x]
            next_for = for_indices[x+1]
            if next_for < next_end_for:
                block_orders.append((for_index,0))
            else:
                block_orders.append((for_index,1))
        else:
            block_orders.append((for_index,1))
    # print(block_orders)

    #get inner for runtime
    for for_index in [bloc_order[0] for bloc_order in block_orders if bloc_order[1] is 1]:
        # print(for_index)
        for end_for in endfor_indices:
            if end_for > for_index:
                break
        for_order = lines[for_index]['order']
        instruction_order = for_order + 1
        inner_instructions = lines[for_index+1:end_for]
        content_runtime = 0 #placeholder
        for line in inner_instructions:
            #Selects elements that have +1 order above the loop
            if(line['order'] is instruction_order):
                content_runtime+= line['runtime']
        for_runtime = for_runtime_formula(lines[for_index]['data'],content_runtime)
        lines[for_index]['runtime'] = for_runtime

    #get outer for runtimes
    for for_index in [bloc_order[0] for bloc_order in block_orders if bloc_order[1] is 0]:
        for x, end_for in enumerate(endfor_indices):
            if lines[end_for]['order'] == lines[for_index]['order'] and end_for >for_index:
                break
        for_order = lines[for_index]['order']
        instruction_order = for_order + 1
        inner_instructions = lines[for_index+1:end_for]
        content_runtime = "" #placeholder
        for line in inner_instructions:
            #selects instructions that are 1 order above the for loop line order
            if(line['order'] is instruction_order):
                content_runtime += '+'+str(line['runtime'])
        for_runtime =for_runtime_formula(lines[for_index]['data'],parse_expr(str(content_runtime)))
        lines[for_index]['runtime'] = for_runtime
    return pd.DataFrame.from_dict(lines)
```

```python
#Based on the runtime data extracted from the previous steps, sum all the first order
def calculate_runtime(syntax_complete):
    lines = syntax_complete.to_dict('records')
    runtime = parse_expr('0')
    for line in lines:
        if line['order'] is 0:
            runtime += line['runtime']
    return sp.simplify(runtime)
```

```python
def get_total_runtime(filename):
    lines = get_lines(filename)
    syntax = pd.DataFrame(data=lines, columns=['line'])
    syntax["length"] = syntax["line"].map(lambda line: len(line))
    syntax["type"] = syntax["line"].map(get_type)
    new_syntax = new_get_statement_runtime(syntax)
    syntax_with_ifs = get_if_blocks_runtime(new_syntax)
    syntax_complete = get_for_blocks_runtime(syntax_with_ifs)
    runtime = calculate_runtime(syntax_complete)
    return (runtime, syntax_complete)
```

```python
index = 2
filename = './tests/ejercicio' + str(index) + '.txt'
total_runtime = get_total_runtime(filename)
total_runtime[0]
```

$\displaystyle 5 \operatorname{ceil}{\left(n \right)} + 6$

```python
total_runtime[1]
```

      line
      length
      type
      runtime
      order
      data

      0
      Inicio
      6
      inicio
      0
      0
      NaN

      1
      Lea n
      5
      lea
      1
      0
      NaN

      2
      s=0
      3
      assignment
      1
      0
      NaN

      3
      Para i=n,1,-1
      13
      para
      5*ceil(n) + 2
      0
      {'lower_b': n, 'upper_b': 1, 'inc': -1}

      4
      t=nmodi
      7
      assignment
      1
      1
      NaN

      5
      Si (t=0)
      8
      si
      2
      1
      {'comparisons': 1}

      6
      s=s+1
      5
      assignment
      1
      2
      NaN

      7
      Fsi
      3
      fsi
      0
      1
      NaN

      8
      Fpara
      5
      fpara
      0
      0
      NaN

      9
      Si (s>2)
      8
      si
      2
      0
      {'comparisons': 1}

      10
      Esc “es primo”
      14
      esc
      1
      1
      NaN

      11
      sino
      4
      sino
      0
      1
      NaN

      12
      Esc “no es primo”
      17
      esc
      1
      1
      NaN

      13
      Fsi
      3
      fsi
      0
      0
      NaN

      14
      Pare
      4
      pare
      0
      0
      NaN

```python

```


```python
import matplotlib as mpl

#for an angle we import Pi
from math import pi

# Importing standard Qiskit libraries and configuring account
from qiskit import QuantumCircuit, execute, Aer

#Inorder to get exact visulization of Quntum circuit execution, results, etc
from qiskit.visualization import plot_bloch_multivector
from qiskit.visualization import plot_state_qsphere
from qiskit.visualization import plot_histogram
from qiskit.quantum_info import Statevector

```

This cell contains some macros. If there is a problem with displaying mathematical formulas, please run this cell to load these macros.

$ \newcommand{\bra}[1]{\langle #1|} $
$ \newcommand{\ket}[1]{|#1\rangle} $
$ \newcommand{\braket}[2]{\langle #1|#2\rangle} $
$ \newcommand{\dot}[2]{ #1 \cdot #2} $
$ \newcommand{\biginner}[2]{\left\langle #1,#2\right\rangle} $
$ \newcommand{\mymatrix}[2]{\left( \begin{array}{#1} #2\end{array} \right)} $
$ \newcommand{\myvector}[1]{\mymatrix{c}{#1}} $
$ \newcommand{\myrvector}[1]{\mymatrix{r}{#1}} $
$ \newcommand{\mypar}[1]{\left( #1 \right)} $
$ \newcommand{\mybigpar}[1]{ \Big( #1 \Big)} $
$ \newcommand{\sqrttwo}{\frac{1}{\sqrt{2}}} $
$ \newcommand{\dsqrttwo}{\dfrac{1}{\sqrt{2}}} $
$ \newcommand{\onehalf}{\frac{1}{2}} $
$ \newcommand{\donehalf}{\dfrac{1}{2}} $
$ \newcommand{\hadamard}{ \mymatrix{rr}{ \sqrttwo & \sqrttwo \\ \sqrttwo & -\sqrttwo }} $
$ \newcommand{\vzero}{\myvector{1\\0}} $
$ \newcommand{\vone}{\myvector{0\\1}} $
$ \newcommand{\stateplus}{\myvector{ \sqrttwo \\  \sqrttwo } } $
$ \newcommand{\stateminus}{ \myrvector{ \sqrttwo \\ -\sqrttwo } } $
$ \newcommand{\myarray}[2]{ \begin{array}{#1}#2\end{array}} $
$ \newcommand{\X}{ \mymatrix{cc}{0 & 1 \\ 1 & 0}  } $
$ \newcommand{\Z}{ \mymatrix{rr}{1 & 0 \\ 0 & -1}  } $
$ \newcommand{\Htwo}{ \mymatrix{rrrr}{ \frac{1}{2} & \frac{1}{2} & \frac{1}{2} & \frac{1}{2} \\ \frac{1}{2} & -\frac{1}{2} & \frac{1}{2} & -\frac{1}{2} \\ \frac{1}{2} & \frac{1}{2} & -\frac{1}{2} & -\frac{1}{2} \\ \frac{1}{2} & -\frac{1}{2} & -\frac{1}{2} & \frac{1}{2} } } $
$ \newcommand{\CNOT}{ \mymatrix{cccc}{1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0} } $
$ \newcommand{\norm}[1]{ \left\lVert #1 \right\rVert } $
$ \newcommand{\pstate}[1]{ \lceil \mspace{-1mu} #1 \mspace{-1.5mu} \rfloor } $
$ \newcommand{\Y}{ \mymatrix{rr}{0 & -i \\ i & 0} } $ $ \newcommand{\S}{ \mymatrix{rr}{1 & 0 \\ 0 & i} } $
$ \newcommand{\T}{ \mymatrix{rr}{1 & 0 \\ 0 & e^{i \frac{pi}{4}}} } $
$ \newcommand{\Sdg}{ \mymatrix{rr}{1 & 0 \\ 0 & -i} } $
$ \newcommand{\Tdg}{ \mymatrix{rr}{1 & 0 \\ 0 & e^{-i \frac{pi}{4}}} } $

#
$\textbf{Classical Computing vs Quantum computing }$

$\textbf{‘Quantum physics’}$ is a term widely used but much less understood. It is a mathematical model first used to describe the behavior of small things in a laboratory, which exposed gaps in the preceding theory of ‘classical’ physics. Quantum theory explains this behavior and gives us a more complete picture of our universe. We have realized we can use this previously unexplained behavior to perform certain computations that we previously did not believe possible. We call this quantum computing. Quantum computing is the perfect way to dip your toes into quantum physics. It distills the core concepts from quantum physics into their simplest forms, stripping away the complications of the physical world.

In classical computers, information is represented as the binary digits 0 or 1. These are called bits.In fact, every classical computer translates these bits into the human readable information on your electronic device.The word document you read or video you watch is encoded in the computer binary language in terms of these 1’s and 0’s.

 $\textbf{Quantum bits}$, called qubits, are similar to bits in that there are two measurable states called
the 0 and 1 states. However, unlike classical bits, qubits can also be in a superposition state of these 0 and 1 states.

#
$\textbf{Qubit Representation in Vector Space }$

The state of a qubit is enclosed in the right half of an angled bracket,called the “ket”. A qubit |$\psi$>, could be in a |0> or |1> state which is a superposition of both |0> and |1>. The general state of a qubit ($|q\rangle$) is:

$$
|\psi\rangle = \alpha|0\rangle + \beta|1\rangle
$$

$$
\alpha, \beta \in \mathbb{C}
$$
where  $\alpha$ and $\beta$ are called the amplitudes of the states.
Amplitudes are very important because they tell us the probability of finding the particle in that
specific state when performing a measurement. The probability of measuring the particle in state
|0> is $\alpha^2$, and the probability of measuring the particle in state |1> is $\beta^2$. The matrix representation of qubits are

\begin{equation}|0\rangle=\left[\begin{array}{l}
1 \\
0
\end{array}\right] \quad|1\rangle=\left[\begin{array}{l}
0 \\
1
\end{array}\right]\end{equation}

Firstly, since we cannot measure global phase, we can only measure the difference in phase between the states $|0\rangle$ and $|1\rangle$. Instead of having $\alpha$ and $\beta$ be complex, we can confine them to the real numbers and add a term to tell us the relative phase between them:

$$
|\psi\rangle = \alpha|0\rangle + e^{i\phi}\beta|1\rangle
$$

$$
\alpha, \beta, \phi \in \mathbb{R}
$$

Finally, since the qubit state must be normalised, i.e.

$$
\sqrt{\alpha^2 + \beta^2} = 1
$$

we can use the trigonometric identity:

$$
\sqrt{\sin^2{x} + \cos^2{x}} = 1
$$

to describe the real $\alpha$ and $\beta$ in terms of one variable, $\theta$:

$$
\alpha = \cos{\tfrac{\theta}{2}}, \quad \beta=\sin{\tfrac{\theta}{2}}
$$

From this we can describe the state of any qubit using the two variables $\phi$ and $\theta$:

$$
|\psi\rangle = \cos{\tfrac{\theta}{2}}|0\rangle + e^{i\phi}\sin{\tfrac{\theta}{2}}|1\rangle
$$

$$
\theta, \phi \in \mathbb{R}
$$

 Bloch sphere

The state of a qubit $\ket{\psi} = \cos{\frac{\theta}{2}} \ket{0} + e^{i\phi} \sin{\frac{\theta}{2}} \ket{1}$ by separatly drawing angles $\frac{\theta}{2}$ and $\phi$. Bloch sphere geometrically represents the state of a qubit with both angles giving a vector from the center of a unit sphere (sphere with radius equal to one unit) to the surface of the sphere. Two angles precisely determine the point, at which the vector is pointing. The only exception is when $\ket{\psi} = \ket{0}$ or $\ket{\psi} = \ket{1}$ - in that case for any angle $\phi$ it will be the same point on the surface. Note that we don't take into account the global phase. To summarize:

Bloch sphere as a geometrical object covers all possible states of a single qubit (upto global phase);

Each point on the surface of the Bloch sphere represents state of a qubit.

Here we can see how the Bloch sphere looks like:

### The Bloch sphere as 3D object has three axes. States on the opposite ends of each axis are orthogonal (in fact, any two states on the opposite sides of a Bloch sphere are orthogonal.
- On the poles of the z-axis there are states $\ket{0}$ and $\ket{1}$.

- The angle $\theta$ shows how far the state is from state $\ket{0}$.

- The angle $\phi$ shows the local phase, that is how much the state is "rotated" around z-axis.

- Poles of the x-axis represent the states $\frac{1}{\sqrt{2}} (\ket{0} + \ket{1})$ and $\frac{1}{\sqrt{2}} (\ket{0} - \ket{1})$ and they differ by a local phase of $\phi = \pi$.

- Poles of the y-axis represent the states $\frac{1}{\sqrt{2}} (\ket{0} + i\ket{1})$ and $\frac{1}{\sqrt{2}} (\ket{0} - i\ket{1})$ and they differ by a local phase of $\phi = \pi$..

Install qiskit textbook using following command:
pip install git+https://github.com/qiskit-community/qiskit-textbook.git#subdirectory=qiskit-textbook-src

```python
from math import pi
from qiskit_textbook.widgets import plot_bloch_vector_spherical
coords = [pi/2,pi/2,1] # [Theta, Phi, Radius]
plot_bloch_vector_spherical(coords)
```

#
$\textbf{Single Qubit Gates}$

 ###
 $\textbf{1.Pauli X-gate}$

The $\mathrm{X}$ gate has the matrix
$$
\sigma_{x}=\left[\begin{array}{ll}
0 & 1 \\
1 & 0
\end{array}\right]
$$
and this is the Pauli X matrix, named after Wolfgang Pauli.
It has the property that

$$
\sigma_{x}|0\rangle=|1\rangle \quad \text { and } \quad \sigma_{x}|1\rangle=|0\rangle
$$

$$ X|0\rangle = \begin{bmatrix} 0 & 1 \\ 1 & 0 \end{bmatrix}\begin{bmatrix} 1 \\ 0 \end{bmatrix} = \begin{bmatrix} 0 \\ 1 \end{bmatrix} = |1\rangle$$

The Pauli-X gate is a single qubit operation gate that is similar to the classical not gate. It takes a value and flips it to the opposite one. It maps |0＞ to |1＞ and |1＞ to |0＞.

```python
ram  = QuantumCircuit(1)     # Making a quntum circuit with 1 classical bit and 1 quantum bit
ram.x(0)                   # Adding x gate to the first qubit
ram.draw('mpl')              #Visual representation using matplotlib
```

```python
state = Statevector.from_instruction(ram)
plot_bloch_multivector(state, title="x gate", reverse_bits=False)
```

```python
# Let's see the result
backend = Aer.get_backend('statevector_simulator')
final_state = execute(ram,backend).result().get_statevector()
from qiskit_textbook.tools import array_to_latex
array_to_latex(final_state, pretext="\\text{Statevector} = ")
```

$\displaystyle
\text{Statevector} = \begin{bmatrix}
0 \\
1
\end{bmatrix}
$

####
 $\textbf{2.Pauli Y-gate}$

The Y gate has the matrix
$$
\sigma_{y}=\left[\begin{array}{cc}
0 & -i \\
i & 0
\end{array}\right]=i\left[\begin{array}{cc}
0 & -1 \\
1 & 0
\end{array}\right]
$$

and this is the Pauli Y matrix.
The Pauli-Y gate is a single qubit operation. It maps |0＞ to -i|1＞ and |1＞ to -i|0＞. It equates to a rotation around the Y-axis of the Bloch sphere by pi radians.

```python
qcy = QuantumCircuit(1)
qcy.x(0)
qcy.y(0)
qcy.draw('mpl')
```

```python
state = Statevector.from_instruction(qcy)
plot_bloch_multivector(state, title="y gate ", reverse_bits=False)
```

```python
# Let's see the result
final_state = execute(qcy,backend).result().get_statevector()
from qiskit_textbook.tools import array_to_latex
array_to_latex(final_state, pretext="\\text{Statevector} = ")
```

$\displaystyle
\text{Statevector} = \begin{bmatrix}
-i \\
0
\end{bmatrix}
$

 $\textbf{3.Pauli Z-gate}$

The Z gate has the matrix
$$
\sigma_{z}=\left[\begin{array}{cc}
1 & 0 \\
0 & -1
\end{array}\right]
$$

and this is the Pauli Z matrix. It rotates qubit states by $\pi$ around the $z$ axis on the Bloch sphere.
 The Pauli-Z gate is a single qubit operation. It maps |1＞ to -|1＞ and it leaves |0＞ unchanged. It equates to a rotation around the Z-axis of the Bloch sphere by pi radians.

```python
qcz = QuantumCircuit(1)
qcz.x(0)
qcz.z(0)
qcz.draw('mpl')
```

```python
state = Statevector.from_instruction(qcz)
plot_bloch_multivector(state, title="New Bloch Multivector", reverse_bits=False)
```

```python
final_state = execute(qcz,backend).result().get_statevector()
from qiskit_textbook.tools import array_to_latex
array_to_latex(final_state, pretext="\\text{Statevector} = ")
```

$\displaystyle
\text{Statevector} = \begin{bmatrix}
0 \\
-1
\end{bmatrix}
$

 $\textbf{4.The Hadamard Gate}$

The Hadamard gate (H-gate) is a fundamental quantum gate. It allows us to move away from the poles of the Bloch sphere and create a superposition of $|0\rangle$ and $|1\rangle$. It has the matrix:

$$ H = \tfrac{1}{\sqrt{2}}\begin{bmatrix} 1 & 1 \\ 1 & -1 \end{bmatrix} $$

We can see that this performs the transformations below:

$$ H|0\rangle = |+\rangle $$

$$ H|1\rangle = |-\rangle $$

This can be thought of as a rotation around the Bloch vector `[1,0,1]` (the line between the x & z-axis), or as transforming the state of the qubit between the X and Z bases.

```python
qch = QuantumCircuit(1)
qch.h(0)
qch.draw('mpl')
```

```python
sv = Statevector.from_label('0')
new_sv = sv.evolve(qch)
plot_state_qsphere(new_sv.data)
```

 We have the state $$\frac{1}{\sqrt{2}}\left(\vert0\rangle + \vert1\rangle\right)$$ which is termed as ket plus state  as $$
\begin{array}{l}
H|0\rangle=|+\rangle
\end{array}
$$

```python
sv = Statevector.from_label('1')
new_sv = sv.evolve(qch)
plot_state_qsphere(new_sv.data)
```

This time, the bottom circle, corresponding to the basis state $\vert1\rangle$ has a different color corresponding to the phase of $\phi = \pi$. This is because the coefficient of $\vert1\rangle$ in the state $$\frac{1}{\sqrt{2}}\left(\vert0\rangle - \vert1\rangle\right)$$ is $-1$, which is equal to $e^{i\pi}$. The given state is termed as ket minus as $$
\begin{array}{l}
H|1\rangle=|-\rangle
\end{array}
$$

 $\textbf{5. P Gate}$

The P-gate (phase gate) is parametrised, that is, it needs a number ($\phi$) to tell it exactly what to do. The P-gate performs a rotation of ϕ around the Z-axis direction. It has the matrix form:

$$ P(\phi) = \begin{bmatrix} 1 & 0 \\ 0 & e^{{i\phi}} \end{bmatrix}$$

```python
qcp = QuantumCircuit(1)
qcp.p(pi/2, 0)
qcp.draw("mpl")
```

You may notice that the Z-gate is a special case of the P-gate, with  $\phi$  = $\pi$
 .

 $\textbf{5. S or P Gate}$

The next gate to mention is the S-gate (sometimes known as the $\sqrt{Z}$-gate), this is a P-gate with $\phi = \pi/2$. It does a quarter-turn around the Bloch sphere. It is important to note that unlike every gate introduced in this chapter so far, the S-gate is **not** its own inverse! As a result, you will often see the S†-gate, (also “S-dagger”, “Sdg” or $\sqrt{Z}^\dagger$-gate).  The S†-gate is clearly an P-gate with $\phi = -\pi/2$:

$$ S = \begin{bmatrix} 1 & 0 \\ 0 & e^{\frac{i\pi}{2}} \end{bmatrix}, \quad  S^\dagger = \begin{bmatrix} 1 & 0 \\ 0 & e^{-\frac{i\pi}{2}} \end{bmatrix}$$

The name "$\sqrt{Z}$-gate" is due to the fact that two successively applied S-gates has the same effect as one Z-gate:

$$ SS|q\rangle = Z|q\rangle $$

```python
qcp = QuantumCircuit(1)
qcp.s(0)
qcp.draw('mpl')
```

 $\textbf{6. T-gate}$

## The T-gate is a very commonly used gate, it is an P-gate with  ϕ=π/4

```python
qct = QuantumCircuit(1)
qct.t(0)
qct.draw('mpl')
```

```python
# Run the code in this cell to see the widget
from qiskit_textbook.widgets import gate_demo
gate_demo()
```

    VBox(children=(HBox(children=(Button(description='I', layout=Layout(height='3em', width='3em'), style=ButtonSt…

    Image(value=b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01 \x00\x00\x01 \x08\x06\x00\x00\x00\x14\x83\xae\x8…