```python
%matplotlib inline
```

Link Prediction using Graph Neural Networks
===========================================

In the [introduction](1_introduction.ipynb), you have already learned
the basic workflow of using GNNs for node classification,
i.e. predicting the category of a node in a graph. This tutorial will
teach you how to train a GNN for link prediction, i.e. predicting the
existence of an edge between two arbitrary nodes in a graph.

By the end of this tutorial you will be able to

-  Build a GNN-based link prediction model.
-  Train and evaluate the model on a small DGL-provided dataset.

```python
import dgl
import torch
import torch.nn as nn
import torch.nn.functional as F
import itertools
import numpy as np
import scipy.sparse as sp
```

    Using backend: pytorch

Overview of Link Prediction with GNN
------------------------------------

Many applications such as social recommendation, item recommendation,
knowledge graph completion, etc., can be formulated as link prediction,
which predicts whether an edge exists between two particular nodes. This
tutorial shows an example of predicting whether a citation relationship,
either citing or being cited, between two papers exists in a citation
network.

This tutorial formulates the link prediction problem as a binary classification
problem as follows:

-  Treat the edges in the graph as *positive examples*.
-  Sample a number of non-existent edges (i.e. node pairs with no edges
   between them) as *negative* examples.
-  Divide the positive examples and negative examples into a training
   set and a test set.
-  Evaluate the model with any binary classification metric such as Area
   Under Curve (AUC).

**Note**: The practice comes from
   [SEAL](https://papers.nips.cc/paper/2018/file/53f0d7c537d99b3824f0f99d62ea2428-Paper.pdf),
   although the model here does not use their idea of node labeling.

In some domains such as large-scale recommender systems or information
retrieval, you may favor metrics that emphasize good performance of
top-K predictions. In these cases you may want to consider other metrics
such as mean average precision, and use other negative sampling methods,
which are beyond the scope of this tutorial.

Loading graph and features
--------------------------

Following the [introduction](1_introduction.ipynb), this tutorial
first loads the Cora dataset.

```python
import dgl.data

dataset = dgl.data.CoraGraphDataset()
g = dataset[0]
```

      NumNodes: 2708
      NumEdges: 10556
      NumFeats: 1433
      NumClasses: 7
      NumTrainingSamples: 140
      NumValidationSamples: 500
      NumTestSamples: 1000
    Done loading data from cached files.

Prepare training and testing sets
---------------------------------

This tutorial randomly picks 10% of the edges for positive examples in
the test set, and leave the rest for the training set. It then samples
the same number of edges for negative examples in both sets.

```python
# Split edge set for training and testing
u, v = g.edges()

eids = np.arange(g.number_of_edges())
eids = np.random.permutation(eids)
test_size = int(len(eids) * 0.1)
train_size = g.number_of_edges() - test_size
test_pos_u, test_pos_v = u[eids[:test_size]], v[eids[:test_size]]
train_pos_u, train_pos_v = u[eids[test_size:]], v[eids[test_size:]]

# Find all negative edges and split them for training and testing
adj = sp.coo_matrix((np.ones(len(u)), (u.numpy(), v.numpy())))
adj_neg = 1 - adj.todense() - np.eye(g.number_of_nodes())
neg_u, neg_v = np.where(adj_neg != 0)

neg_eids = np.random.choice(len(neg_u), g.number_of_edges() // 2)
test_neg_u, test_neg_v = neg_u[neg_eids[:test_size]], neg_v[neg_eids[:test_size]]
train_neg_u, train_neg_v = neg_u[neg_eids[test_size:]], neg_v[neg_eids[test_size:]]
```

When training, you will need to remove the edges in the test set from
the original graph. You can do this via ``dgl.remove_edges``.

**Note**: ``dgl.remove_edges`` works by creating a subgraph from the
   original graph, resulting in a copy and therefore could be slow for
   large graphs. If so, you could save the training and test graph to
   disk, as you would do for preprocessing.

```python
train_g = dgl.remove_edges(g, eids[:test_size])
```

Define a GraphSAGE model
------------------------

This tutorial builds a model consisting of two
[GraphSAGE](https://arxiv.org/abs/1706.02216) layers, each computes
new node representations by averaging neighbor information. DGL provides
``dgl.nn.SAGEConv`` that conveniently creates a GraphSAGE layer.

```python
from dgl.nn import SAGEConv

# ----------- 2. create model -------------- #
# build a two-layer GraphSAGE model
class GraphSAGE(nn.Module):
    def __init__(self, in_feats, h_feats):
        super(GraphSAGE, self).__init__()
        self.conv1 = SAGEConv(in_feats, h_feats, 'mean')
        self.conv2 = SAGEConv(h_feats, h_feats, 'mean')

    def forward(self, g, in_feat):
        h = self.conv1(g, in_feat)
        h = F.relu(h)
        h = self.conv2(g, h)
        return h
```

The model then predicts the probability of existence of an edge by
computing a score between the representations of both incident nodes
with a function (e.g. an MLP or a dot product), which you will see in
the next section.

\begin{align}\hat{y}_{u\sim v} = f(h_u, h_v)\end{align}

Positive graph, negative graph, and ``apply_edges``
---------------------------------------------------

In previous tutorials you have learned how to compute node
representations with a GNN. However, link prediction requires you to
compute representation of *pairs of nodes*.

DGL recommends you to treat the pairs of nodes as another graph, since
you can describe a pair of nodes with an edge. In link prediction, you
will have a *positive graph* consisting of all the positive examples as
edges, and a *negative graph* consisting of all the negative examples.
The *positive graph* and the *negative graph* will contain the same set
of nodes as the original graph.  This makes it easier to pass node
features among multiple graphs for computation.  As you will see later,
you can directly fed the node representations computed on the entire
graph to the positive and the negative graphs for computing pair-wise
scores.

The following code constructs the positive graph and the negative graph
for the training set and the test set respectively.

```python
train_pos_g = dgl.graph((train_pos_u, train_pos_v), num_nodes=g.number_of_nodes())
train_neg_g = dgl.graph((train_neg_u, train_neg_v), num_nodes=g.number_of_nodes())

test_pos_g = dgl.graph((test_pos_u, test_pos_v), num_nodes=g.number_of_nodes())
test_neg_g = dgl.graph((test_neg_u, test_neg_v), num_nodes=g.number_of_nodes())
```

The benefit of treating the pairs of nodes as a graph is that you can
use the ``DGLGraph.apply_edges`` method, which conveniently computes new
edge features based on the incident nodes’ features and the original
edge features (if applicable).

DGL provides a set of optimized builtin functions to compute new
edge features based on the original node/edge features. For example,
``dgl.function.u_dot_v`` computes a dot product of the incident nodes’
representations for each edge.

```python
import dgl.function as fn

class DotPredictor(nn.Module):
    def forward(self, g, h):
        with g.local_scope():
            g.ndata['h'] = h
            # Compute a new edge feature named 'score' by a dot-product between the
            # source node feature 'h' and destination node feature 'h'.
            g.apply_edges(fn.u_dot_v('h', 'h', 'score'))
            # u_dot_v returns a 1-element vector for each edge so you need to squeeze it.
            return g.edata['score'][:, 0]
```

You can also write your own function if it is complex.
For instance, the following module produces a scalar score on each edge
by concatenating the incident nodes’ features and passing it to an MLP.

```python
class MLPPredictor(nn.Module):
    def __init__(self, h_feats):
        super().__init__()
        self.W1 = nn.Linear(h_feats * 2, h_feats)
        self.W2 = nn.Linear(h_feats, 1)

    def apply_edges(self, edges):
        """
        Computes a scalar score for each edge of the given graph.

        Parameters
        ----------
        edges :
            Has three members ``src``, ``dst`` and ``data``, each of
            which is a dictionary representing the features of the
            source nodes, the destination nodes, and the edges
            themselves.

        Returns
        -------
        dict
            A dictionary of new edge features.
        """
        h = torch.cat([edges.src['h'], edges.dst['h']], 1)
        return {'score': self.W2(F.relu(self.W1(h))).squeeze(1)}

    def forward(self, g, h):
        with g.local_scope():
            g.ndata['h'] = h
            g.apply_edges(self.apply_edges)
            return g.edata['score']
```

**Note**: The builtin functions are optimized for both speed and memory.
   We recommend using builtin functions whenever possible.

**Note**: If you have read the [message passing
   tutorial](3_message_passing.ipynb), you will notice that the
   argument ``apply_edges`` takes has exactly the same form as a message
   function in ``update_all``.

Training loop
-------------

After you defined the node representation computation and the edge score
computation, you can go ahead and define the overall model, loss
function, and evaluation metric.

The loss function is simply binary cross entropy loss.

\begin{align}\mathcal{L} = -\sum_{u\sim v\in \mathcal{D}}\left( y_{u\sim v}\log(\hat{y}_{u\sim v}) + (1-y_{u\sim v})\log(1-\hat{y}_{u\sim v})) \right)\end{align}

The evaluation metric in this tutorial is AUC.

```python
model = GraphSAGE(train_g.ndata['feat'].shape[1], 16)
# You can replace DotPredictor with MLPPredictor.
#pred = MLPPredictor(16)
pred = DotPredictor()

def compute_loss(pos_score, neg_score):
    scores = torch.cat([pos_score, neg_score])
    labels = torch.cat([torch.ones(pos_score.shape[0]), torch.zeros(neg_score.shape[0])])
    return F.binary_cross_entropy_with_logits(scores, labels)

def compute_auc(pos_score, neg_score):
    scores = torch.cat([pos_score, neg_score]).numpy()
    labels = torch.cat(
        [torch.ones(pos_score.shape[0]), torch.zeros(neg_score.shape[0])]).numpy()
    return roc_auc_score(labels, scores)
```

The training loop goes as follows:

**Note**: This tutorial does not include evaluation on a validation
   set. In practice you should save and evaluate the best model based on
   performance on the validation set.

```python
# ----------- 3. set up loss and optimizer -------------- #
# in this case, loss will in training loop
optimizer = torch.optim.Adam(itertools.chain(model.parameters(), pred.parameters()), lr=0.01)

# ----------- 4. training -------------------------------- #
all_logits = []
for e in range(100):
    # forward
    h = model(train_g, train_g.ndata['feat'])
    pos_score = pred(train_pos_g, h)
    neg_score = pred(train_neg_g, h)
    loss = compute_loss(pos_score, neg_score)

    # backward
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

    if e % 5 == 0:
        print('In epoch {}, loss: {}'.format(e, loss))

# ----------- 5. check results ------------------------ #
from sklearn.metrics import roc_auc_score
with torch.no_grad():
    pos_score = pred(test_pos_g, h)
    neg_score = pred(test_neg_g, h)
    print('AUC', compute_auc(pos_score, neg_score))
```

    In epoch 0, loss: 0.6184065937995911
    In epoch 5, loss: 0.6056914925575256
    In epoch 10, loss: 0.5802127122879028
    In epoch 15, loss: 0.5393418073654175
    In epoch 20, loss: 0.48020118474960327
    In epoch 25, loss: 0.4126580059528351
    In epoch 30, loss: 0.36391153931617737
    In epoch 35, loss: 0.32281294465065
    In epoch 40, loss: 0.2892597019672394
    In epoch 45, loss: 0.2589336931705475
    In epoch 50, loss: 0.23045368492603302
    In epoch 55, loss: 0.2066962718963623
    In epoch 60, loss: 0.18129807710647583
    In epoch 65, loss: 0.1579950898885727
    In epoch 70, loss: 0.1354110985994339
    In epoch 75, loss: 0.11393584311008453
    In epoch 80, loss: 0.0939987450838089
    In epoch 85, loss: 0.07589612156152725
    In epoch 90, loss: 0.0597052127122879
    In epoch 95, loss: 0.04581739008426666
    AUC 0.8605017856741763


# EOSC 576 Problems

```python
__author__ = 'Yingkai (Kyle) Sha'
__email__  = 'yingkai@eos.ubc.ca'
```

```python
from IPython.core.display import HTML
HTML(open("../custom.css", "r").read())
```

```python
import numpy as np
import sympy as sp
import matplotlib.pyplot as plt
% matplotlib inline
```

#Content
 1. [**Chapter 4 - Organic Matter Production**](#Chapter-4---Organic-Matter-Production)
 1. [**Chapter 10 - Carbon Cycle, CO2, Climate**](#Chapter-10---Carbon-Cycle,-CO2,-Climate)

# Chapter 4 - Organic Matter Production

**4.10** Assume the composition of organic matter is $(CH_2)_{30}(CH_2O)_{76}(NH_3)_{16}(H_3PO_4)$

(a) Calculate the C:N:P stoichiometric ratio of this organic matter

*Ans:*
$$
C:N:P = 106:16:1
$$

(b) Calculate the amount of $O_2$ that would be required to oxidize this material if $H_3PO_4$, $HNO_3$, $H_2O$, and $CO_2$ are the oxidation products of phosphorus, nitrogen, hydrogen, and carbon, respectively. Give the full equation for the oxidation reaction. ...

*Ans:* Since organic matter has $C:N:P = 106:16:1$, 1mol organic reactant finally becomes 106mol $CO_2$, 16mol $HNO_3$, 1mol $H_3PO_4$. Then we add $H_2O$ to balance hydrogen, we will get:
    $$
    (CH_2)_{30}(CH_2O)_{76}(NH_3)_{16}(H_3PO_4) + 193O_2 \longrightarrow 106CO_2 + 16HNO_3 + H_3PO_4 + 122H_2O
    $$

(c)  Suppose water upwelling to the surface has a total carbon concentration of $2000\ mmol/m^3$, an oxygen concentration of $160\ mmol/m^3$, a nitrate concentration of $5\ mmol/m^3$, and a phosphate concentration of $1\ mmol/m^3$.

 * Which of these nutrients is likely to limit production if the light supply is adequate and there is NO nitrogen fixation?
 * Which of the elements will limit production if nitrogen fixation is allowed?
 * In each case, calculate the concentration of the remaining nutrients after the limiting nutrient is exhausted.

    *Ans:* photosynthesis consume nutrients in the ratio of $C:N:P = 106:16:1$. So if there is no nitrogen fixation, nitrate is the main source of $N$ and it is the limiting nutrient, when nitrate runs out, we still have $1 - 5/16 = 0.6875\ mmol/m^3$ phosphate.

    *Ans:* If nitrogen fixation is allowed, then atmospheric bi-nitrogen could also be a source of $N$ and this time phosphate is the limiting nutrient. The concentration of the remaining nutrients depends on the intensity of nitrogen fixation relative to photosynthesis.

**4.11** Nitrate may serve as the terminal electron acceptor (i.e., oxidant) for the remineralization
of organic matter if oxygen is not available. The nitrate loses its oxygen and is converted to
dissolved N2, in the process of which it gains electrons. This is referred to as *denitrification*.

(a) Write a balanced equation for the oxidation of the organic matter in problem 4.10 by
denitrification. Assume that the organic matter reacts with nitrate in the form $HNO_3$, and
that all the nitrogen present in both the organic matter and nitrate is converted to $N_2$. All
other oxidation products are as in problem 4.10 (b)

*Ans:*
$$
(CH_2)_{30}(CH_2O)_{76}(NH_3)_{16}(H_3PO_4) + 107HNO_3 \longrightarrow 106CO_2 + 61.5N_2 + H_3PO_4 + 185H_2O
$$

(b) What fraction of the $N_2$ in (a) comes from nitrate?

*Ans:*
$$
107/(61.5*2) = 0.8699
$$

**4.14** ... In this problem, you are to estimate the diurnally (24 hr) averaged light supply function $\gamma_P(I_0)$ at the surface of the ocean, which we will define as
$\left<\gamma_P(I_0)\right>$. Assume that $I_n = 1000\ W/m^2$, and that the diurnal variation of the irradiance function $f(\tau)$ is given as a triangular function that increases linearly from 0 at 6 AM to 1 at noon, then back to 0 at 6 PM. Do this in two steps:

(a) Starting with (4.2.13), give an equation for the surface irradiance, $I_0$ for the first 6 hours
of daylight in terms of the time $t$ in hours, with $t$ set to 0 at daybreak. Assume that the
fraction of photosynthetically active radiation (PAR) is $f_{PAR} = 0.4$ and that the cloud cover
coefficient $f(C) = 0.8$.

*Ans*: Eq. (4.2.13) is

$$
    I_0 = f_{PAR}\cdot f(C) \cdot f(\tau) \cdot I_n
$$

based on the knowns, we have ($t$ in hours):

\begin{equation}
  I_0 = \left\{
   \begin{array}{c}
   320 \times \left(\frac{1}{6}t-1\right) \qquad 6 < t < 12 \\
   320 \times \left( 3-\frac{1}{6}t\right) \qquad 12 < t < 18 \\
   0 \qquad 0 < t < 6, \qquad 18 < t <24
   \end{array}
  \right.
  \end{equation}

(b) Calculate $\left<\gamma_P(I_0)\right>$. Use the `Platt and Jassby` formulation (4.2.16). To calculate $I_k$ from
(4.2.17), use for $V_P$ the typical $V_{max} = 1.4$ given in the text, and the representative value for $\alpha$ of $0.025$. Solve the problem analytically by stepwise integration over the 24 hours of the day.

*Ans*:
Based on Eq. (4.2.17)
$$
    I_k = \frac{V_P}{\alpha} = 56\ W/m^2
$$

Then based on Eq. (4.2.16)
$$
    \gamma_P(I_0) = \frac{I_0}{\sqrt{I_k^2 + I_0^2}}
$$
So we have:
$$
    \left<\gamma_P(I_0)\right> = \frac1{24}\int_0^{24}{\gamma_P(I_0)dt}
$$
Here we solve it numerically:

```python
t = np.linspace(0, 24, 100)
hit1 = (t>6)&(t<=12)
hit2 = (t>12)&(t<=18)
I0 = np.zeros(np.size(t))
I0[hit1] = 320 * ((1./6) * t[hit1] - 1)
I0[hit2] = 320 * (3 - (1./6) * t[hit2])
Ik = 56
rI0 = I0/np.sqrt(Ik**2 + I0**2)
```

```python
fig=plt.figure(figsize=(11, 5))
ax1=plt.subplot2grid((1, 2), (0, 0), colspan=1, rowspan=1)
ax2=plt.subplot2grid((1, 2), (0, 1), colspan=1, rowspan=1)
ax1.plot(t, I0, 'k-', linewidth=3); ax1.grid();
ax1.set_xlabel('t in hours', fontweight=12)
ax1.set_ylabel('$I_0$', fontweight=12)
ax1.set_xlim(0, 24); ax1.set_ylim(0, 320)
ax2.plot(t, rI0, 'k-', linewidth=3); ax2.grid();
ax2.set_xlabel('t in hours', fontweight=12)
ax2.set_ylabel('$\gamma_P(I_0)$', fontweight=12)
ax2.set_xlim(0, 24); ax2.set_ylim(0, 1)
```

```python
delta_t = t[1]-t[0]
result = (1./24) * np.sum(rI0*delta_t)
print('Daily average of rI0 is: {}'.format(result))
```

    Daily average of rI0 is: 0.420146160518

So light limits is important.

**4.15** In this problem, you are to find the depth at which the diurnally averaged light supply $\left<\gamma_P\left(I(z)\right)\right>$ crosses the threshold necessary for phytoplankton to achieve the minimum concentration at which zooplankton can survive, $0.60\ mmol/m^3$. Use the temperature dependent growth rate given by the `Eppley relationship` (4.2.8) for a temperature of $10^\circ C$, a mortality rate $\lambda_P$ of $0.05\ d^{-1}$, and a nitrate half-saturation constant $K_N$ of $0.1\ mmol/m^3$. Assume that the total nitrate concentration $N_T$ is $10\ mmol/m^3$. Do this in two steps:

(a) Find the minimum light supply function $\gamma_P(I)$ that is required in order for phytoplankton
to cross the threshold concentration (assume zooplankton concentration $Z = 0$)

*Ans:*
The steady state of phytoplankton in N-P-Z model:
$$
SMS(P) = 0 = V_{max}\gamma_P(N)\gamma_P(I) - \lambda_P
$$

And now we try to solve light limits $\gamma_P(I)$.

The threshold of phytoplankton $P = 0.60\ mmol/m^3$, so we have the concentration of nutrient:
$$
N = N_T - P - Z = 9.4\ mmol/m^3
$$
Then calling Eq. 4.2.11., nutrient limits is:
$$
\gamma_P(N) = \frac{N}{K_N+N} = 0.99
$$
For the maximum growth rate, we have Eq. 4.2.8:
$$
V_{max} = V_P(T) = ab^{cT} = 0.6*1.066^{10} = 0.637
$$
Thus the minimum light supply function is:
$$
\gamma_P(I) = \frac{\lambda_P}{V_{max}\gamma(N)} = 0.079
$$

(b) Assuming that $\gamma_P(I)$ from (a) is equal to the diurnal average $\left<\gamma_P\left(I(z)\right)\right>$, at what depth $H$ in
the ocean will the diurnally averaged light supply function cross the threshold you
estimated in (a)? Assume that P is constant with depth and use a total attenuation
coefficient of $0.12\ m^{-1}$.

*Ans:*

Here I borrowed 2 values from problem **4.14** $\alpha = 0.025$, and $I_0 = 1000$.

Based on Eq. (4.2.16), Eq. (4.2.17):

$$
I = \frac{\gamma_P(I)I_k}{\sqrt{1-\gamma_P(I)^2}}, \qquad\  I_k = \frac{V_P}{\alpha}
$$

For the critical depth, growth equals to death, $V_P = \lambda_P=0.05$, and we get $I = 0.1584$

Then from Beer's Law:

$$
I = I_0\exp(-KH), \qquad\ K=0.12
$$

So we have:

$$
H = -\frac1K\ln\frac{I}{I_0} = 72.92\ m
$$

This is the deepest place for zooplankton to survive, and phytoplankton has a concentration of $60\ mmol/m^3$.

#Chapter 10 - Carbon Cycle, CO2, Climate

**10.4** Explain why the surface ocean concentration of anthropogenic CO2 is higher
in low latitudes than it is in high latitudes. Why is it higher in the Atlantic
than in the Pacific ?

*Ans:*

The basic idea is the variation of buffering factor $\gamma_{DIC}$ is more important than the solubility of $\mathrm{CO_2}$

If we integrate eq(10.2.16) begin with *Anthropocene*, $C_{ant}$ is a function of $\gamma_{DIC}$:
$$
    C_{ant}(t) = \int_{t=t_\pi}^{t_0}{\frac{\partial DIC}{\partial t}dt} = \frac1{\gamma_{DIC}}\frac{DIC}{pCO_2^{oc}}\left(\left.pCO_2^{atm}\right|_{t_0}^{t_\pi}\right)
$$

   * Tropics has low $\gamma_{DIC}$ so high accumulated $C_{ant}$ takeup;
   * High-latitude regions has high $\gamma_{DIC}$ so ...
   * Atlantic has a lower $\gamma_{DIC}$ than Pacific due to its high *Alk* (see eq(10.2.11))

**10.5** How long will it take for a pulse of $\mathrm{CO_2}$ emitted into the atmosphere to be reduced to 50%, 20%, 10%, and 1% of its original value? For each answer list? which process is the primary one responsible for the removal of $\mathrm{CO_2}$ from the atmosphere at the point in time the threshold is crossed.

*Ans:*

We have many choices of impulse response functions (IRF), a simple one used by IPCC-SAR is:
$$
IRF = A_0 + \sum_{i=1}^5{A_i\exp\left(-\frac{t}{\tau_i}\right)}
$$
$A_i$ and $\tau_i$ are empirical values, $t$ for "year" (details here)

```python
def IRF_IPCC(A, tau, t):
    IRF = A[0]*np.ones(t.shape)
    for i in range(5):
        IRF = IRF + A[i+1]*np.exp(-1*t/tau[i])
    return IRF
```

```python
A_std    = np.array([0.1369, 0.1298, 0.1938, 0.2502, 0.2086, 0.0807])
tau_std  = np.array([371.6, 55.7, 17.01, 4.16, 1.33])
t = np.linspace(0, 500, 501)
IRF = IRF_IPCC(A_std, tau_std, t)
```

```python
fig = plt.figure(figsize=(10, 4)); ax = fig.gca();ax.grid()
plt.plot(t, IRF, 'k-', linewidth=3.5)
ax.set_title('IRF v.s. time', fontsize=14)
```

```python
hit = np.flipud(t)[np.searchsorted(np.flipud(IRF), [0.5, 0.2])]
print('Time to reduced to 50% is {} year, to 20% is {} year'.format(hit[0], hit[1]))
```

    Time to reduced to 50% is 16.0 year, to 20% is 276.0 year

For 50%, DIC buffering is dominate. For 20%, it costs 276 yr and DIC buffering is nearly saturate (see Fig.10.2.3), and $\mathrm{CaCO_3}$ buffering begin to dominate.

**10.8** Explain the apparent paradox that the tropical Pacific is viewed as being a
large sink for anthropogenic $\mathrm{CO_2}$, despite the fact that it is a region of net
outgassing of $\mathrm{CO_2}$.

*Ans:*

According to **10.4** we know that tropical ocean takes up more $Ant_{C}$ because it has a lower $\gamma_{DIC}$. The outgassing in tropical Pacific is due to the upwelling and inefficient biological pump, these are the business of natural carbon (and since natural carbon cycle is in equilibrium, this outgassing is balanced by some other downwelling regions).

**10.13**


```javascript
%%javascript
$('#appmode-leave').hide();
$('#copy-binder-link').hide();
$('#visit-repo-link').hide();
```

# Water anomalies
Despite its simple molecular structure, water is an exceptionally complicated fluid.
Many of its properties do not follow the trends obeyed by other liquids and are often referred to as water anomalies.
Scientists often report more than 50 _anomalous_ properties of water, and some of the most well-known examples are
1. Water has an unusually high melting point for a molecule of such a low molecular weight.
2. Water has an unusually high boiling point for a molecule of such a low molecular weight.
3. A liquid-liquid transition occurs at about 330 K.
4. Pressure reduces ice's melting point.
5. Cold liquid water has a high density that increases on warming (up to 3.984 °C).

Most of these anomalous behaviours have been explained and there is ample scientific (and non-scientific) literature discussing them. As an example, this website [https://water.lsbu.ac.uk/water/water_anomalies.html](https://water.lsbu.ac.uk/water/water_anomalies.html) will provide a good overview, and plenty of references, on the topic.

In this numerical workshop you will use a computational technique called Molecular Dynamics (MD) to study the how the water density changes with temperature.
MD is one of the most widely used type of atomistic simulations, and it is now routinely used in many research groups to complement experimental studies.

## Molecular dynamics
Molecular dynamics is conceptually very simple, an iterative solution of Newton's equations of motions at the atomic level, but subtly complicated to use for direct quantitative comparison with experiments.
Although a detailed description of MD is beyond the scope of this laboratory, it is worth discussing some basic ideas for you to start appreciating the power and limitations of this technique. There are plenty of webpage and tutorials that describe the working principles of MD; Wikipedia has a fairly good an general overview of this topic [https://en.wikipedia.org/wiki/Molecular_dynamics]( https://en.wikipedia.org/wiki/Molecular_dynamics)

In MD the atoms are treated as point particles with a mass and a partial charge. Their interactions are described by simple empirical equations, such as the Coulomb and the van der Waals  (dispersion) forces, supplemented with two-, three- or four-body interactions to better capture the covalent nature of the intramolecular bonds.
For example, in classical molecular dynamics the interaction *energy* between two non-bonded atoms separated by a distance, $r$, can be written as

\begin{equation}
U_{ij} = \frac{1}{4\pi\varepsilon_0}\frac{q_i q_j}{r} + \frac{A}{r^12} - \frac{B}{r^6} \tag{1}
\end{equation}

Where the first term is the Coulomb interaction and the last two the repulsive and attractive parts of the van der Waals interactions.
On the other hand the bonded two-, three- and four-body interactions between covalently bonded atoms are typically described by *harmonic* potentials

\begin{eqnarray}
U_{ij}^b &=& K_b(b_{ij}-b_0)^2 \tag{2} \\
U_{ijk}^a &=& K_\theta(\theta_{ijk}-\theta_0)^2 \tag{3} \\
U_{ijkl}^t &=& K_\phi[1+\cos(n\phi_{jikl}-\phi_0)]^2 \tag{3} \\
\end{eqnarray}

where $b_{ij}$, $\theta_{ijk}$ and $\phi_{jikl}$ are the bond lengths, angle and torsional angle between the atoms and the other quantities are fitting parameters, which are key to determine the accuracy of the simulations.

Once the interaction energy is known we can then compute the forces on the atoms as the sum of all pair-wise interactions

\begin{equation}
F_i = \sum_{j\neq i} F_{ij} = -\Bigg[
  \sum \frac{\partial U_{ij}}{\partial x_i} +
  \sum \frac{\partial U_{ij}^a}{\partial x_i} +
  \sum \frac{\partial U_{ijk}^b}{\partial x_i} +
  \sum \frac{\partial U_{ijkl}^t}{\partial x_i} \Bigg] \tag{5}
\end{equation}

Then, by knowing the positions, velocities and forces for all the atoms at a certain time $t$, we can use the Newton's equations of motions to *predict* the positions and velocities of the particles after a certain (short) amount of time as passed

\begin{eqnarray}
a_i(t) &=& \frac{F_i(t)}{m_i} \tag{6} \\
v_i(t+\delta t) &=& v_i(t) + a_i(t)\delta t \tag{7} \\
x_i(t+\delta t) &=& x_i(t) + v_i(t)\delta t + \frac{1}{2}a_i(t)\delta t^2 \tag{8} \\
\end{eqnarray}

where $a_i$, $v_i$ and $x_i$ are the acceleration, velocity and position of particle $i$, and $\delta t$ is called the time step.
These three equations (or some variants of them) are usually called **equations of motions**.
Now that we have the new atomic positions we can compute the new forces on the atoms, and use again the Newton's equations of motions to *propagate* the atoms' positions further.
This iterative procedure will generate a **trajectory** for the atoms, and by using energies and velocities collected along the way we will also get information about the temperature, pressure and other thermodynamic quantities of the system.

### Importance of the time step
The time step is one of the most important quantities in MD, and it is key to understand the potentials and limitations of atomistic molecular dynamics simulations.
In fact, for the above equations of motions to be valid, the time step has to be short enough to describe the fastest **atomic** motion in the system, which in the case of water is the O-H stretching mode.
The O-H stretching has a vibrational frequency of approximately $1\times10^{14}$Hz, *i.e* it takes about $1\times10^{-14}$s to complete one oscillation. Therefore, if we want to describe this very fast atomic motion using discrete points in time we would need 10-20 snapshots. Hence the time step has to be of the order of 1~fs ($1\times10^{-15}$s) or less.

Now, let’s imagine running a simulation with a 1 fs time step and that the computer take 10 ms to calculate energies, forces and do one cycle of the equations of motions.
In the table below you can see how long it would you take to simulate a chemical or physical process depending on the time scale it experimentally occurs

| Experimental time scale  | Phenomenon   | Simulation time
| :-----: | :--------: |:---------
| 10 fs   | O-H vibration                     | 0.1 s
| 1 ps    | H-bond persistence                | 10 s
| 1 nm    | Ion permeation through a membrane | 3 hours
| 1$\mu$s | Conformational rearrangement      | 115 days
| 1 ms    | Protein folding (fast)            | 317 days
| 1 s     | Protein folding (typical)         | 317,000 days

Obviously, the time requires to do on MD cycles depends on the number of operations the computer has to perform, hence it increases with the system size.
Although computational power has increased exponentially since MD was first introduced in the 1940s, we we can now afford to study systems of millions of atoms or hundreds of nm i size, there are still strong limitations to what can be reliably simulated due to the finite (small) number of atoms is included in the system (compared to Avogadro's number) and the short time scale that the simulation can span.

### Ensemble
MD codes are more complicated that a simple iterative solution of Newton's equations of motions and they include algorithms to control the temperature, pressure and other thermodynamics quantities of the system.
Of particular relevance for this experience is the need to use **thermostats** and **barostats** to fix the boundary conditions of the simulations.
For this laboratory is not important to know the working details of these algorithms, but is key that you are aware that the variables relating to the temperature and pressure of the simulations are input parameters that may need to be changed.

## Scope of the laboratory
The scope of this virtual experiment is to introduce you to atomistic molecular dynamics simulations and to compute the variations of the water density as a function of temperature, and to compare it with experimental values. As briefly mentioned above, the accuracy of the simulation depends on the parameters that are used to compute the intermolecular interactions. This laboratory will be run in groups and each of you will run individually MD calculations for a chosen water model and share the results with the other group members to be included in the final report (with proper acknowledgment of their origin).

The water models available for this laboratory are
* SPC/E
* TIP3P
* TIP4P
* TIP4P/ew
* TIP5P

which is a very small selections of the available models for water.
The models are listed in increasing level of complexity and one would reasonably expect that the more complex model gives more accurate results.

You will choose one model, in consultation with the other members of your team, and run a series of simulations to compute the water density at various conditions, to locate the water density maximum, if it exists for that model.
Each simulation may take 5/10 minutes, you do not have to look at the simulation as it runs but it is important that your computed does not go to sleep or disconnect during the run time of the simulation.
Although the files should remain on the server you should save a copy of the files on your local computer before disconnecting.

You can start by performing a few calculations around ambient conditions, say between 273 and 310 K, and then move to much lower temperature if needed.

Note that the water will not freeze during your simulation even at fairly high undercooling, so you can go to quite low temperature without any real problems.

## Final report
The final report for this experience should include your calculations for the water density as a function of temperature for a selection of the models, literature experimental data and a comparison with published MD results. Optionally you can also include data obtained by your peers for a different water potential.
The comparison with previous simulations should be done at least for the same water model that you have used in your simulations.

Your report should show how you extracted the average density from the simulation output, and an estimate of the errors.

## Launch virtual experiment
Let's now have a look at the python notebook to run MD simulations
[Molecular dynamics Simulation](md.ipynb)


```python
#we may need some code in the ../python directory and/or matplotlib styles
import sys
import os
sys.path.append('../python/')

#set up matplotlib
os.environ['MPLCONFIGDIR'] = '../mplstyles'
print(os.environ['MPLCONFIGDIR'])
import matplotlib as mpl
from matplotlib import pyplot as plt
#got smarter about the mpl config: see mplstyles/ directory
plt.style.use('standard')
print(mpl.__version__)
print(mpl.get_configdir())

#fonts
# Set the font dictionaries (for plot title and axis titles)
title_font = {'fontname':'Arial', 'size':'16', 'color':'black', 'weight':'normal',
              'verticalalignment':'bottom'} # Bottom vertical alignment for more space
axis_font = {'fontname':'Arial', 'size':'32'}
legend_font = {'fontname':'Arial', 'size':'22'}

#fonts global settings
mpl.rc('font',family=legend_font['fontname'])

#set up numpy
import numpy as np
```

    ../mplstyles
    3.0.3
    /home/phys/villaa/analysis/misc/nrFano_paper2019/mplstyles

# Summary

In this notebook we follow the logic for our analysis that defines an effective nuclear-recoil Fano factor for germanium. We use the other notebooks in this directory as supporting references and present the line of logic for our publication [REF].

It is planned that all of the plots that go into the publication will be produced here from the data we have placed in the `data/` directory below this one. It is planned that all the data is referenced and where it came from is clear.

The basic idea of this analysis is that there are measurements in the literature that constrain the width (second moment) of the ionization distribution for various materials. This has also been predicted by Lindhard [REF]. This variance in the number of charges produced by a nuclear recoil of a given energy far exceeds what is measured from electron recoils. In the electron recoils this is parameterized by the Fano factor and so here we define the "effective" Fano factor for nuclear recoils.

While the width of the ionization distribution has not been important in the past because of excellent discrimination between electron- and nuclear-recoil events above about 10 keV, it is becoming more important for dark matter searches interested in lower recoil energies [REF] (SuperCDMS low threshold) and discriminationless [REF] (CDMSlite & HVeV) searches.

# 1. Lindhard has Predicted this Variance and Dougherty has Measured it for Silicon

Dougherty has measured this effect in silicon and shown that it is near the predicted values from Lindhard [[Dough92][Dough92]]. See the notebook `silicon_Fano.ipynb` for the details of how an effective Fano factor is extracted from this silicon measurement.

[Dough92]: https://journals.aps.org/pra/abstract/10.1103/PhysRevA.45.2104 "Dougherty paper 1992"

The following table is a summary of those data along with the effective Fano estimate and uncertainty for each recoil energy data point measured in that publication.

Experimental uncertainties are quoted as values following the "$\pm$" symbol. The Observed width and Expected width are both in FWHM execpt for the 25.3 keV recoil energy point, which is quoted in half width at half max (HWHM). The excess fluctuation are given in 1$\sigma$.

## Table 1 of the paper:

Si recoil energy (keV)|Observed ionization (keV)|Lindhard shift (keV)|Ionization efficiency (%)|Observed width (keV)|Expected width (keV)|Excess fluct. (%)| effective Fano
:-|:-|:-|:-|:-|:-|:-|:-
109.1$\pm$0.7|55.5$\pm$2|0.55|51.4$\pm$2|16$\pm$3|3.5$\pm$0.4|6.1$\pm$1.2|208$\pm$40
75.7 $\pm$0.4|33.3$\pm$0.4|0.31+0.94|45.6$\pm$0.5|9.6$\pm$1.0|1.1$\pm$0.3|5.3$\pm$0.6|123$\pm$5
25.3$\pm$0.3|8.90$\pm$0.1|0.074|35.5$\pm$0.6|1.30$\pm$0.04|0.75$\pm$0.1|3.6$\pm$0.3|24.3$\pm$3.6
7.50$\pm$0.03|2.01$\pm$0.02|0.012|26.9$\pm$0.4|0.55$\pm$0.07|0.24$\pm$0.01|2.8$\pm$0.4|5.75$\pm$3.07
4.15$\pm$0.15|0.93$\pm$0.02|0.008|22.5$\pm$0.5|0.32$\pm$0.06|0.236$\pm$0.005|2.2$\pm$0.9|2.35$\pm$9.21

```python
import dataPython as dp
import numpy as np

lind_data0 = dp.getXYdata('data/lindhard2_OmegaepsD_fmt.txt')
lind_data1 = dp.getXYdata('data/lindhard2_OmegaepsE_fmt.txt')

lindD_e = np.asarray(lind_data0['xx'])
lindD = np.asarray(lind_data0['yy'])
lindE_e = np.asarray(lind_data1['xx'])
lindE = np.asarray(lind_data1['yy'])
```

```python
Esi = np.vectorize(lambda x: np.sqrt(2)*2*x/(6.87758e-5*1000))
```

```python
#create a yield model
import lindhard as lind

#lindhard
lpar = lind.getLindhardPars('Si',True) #use the "calculated" value of k
print(lpar)
#ylind = lind.getLindhard(lpar)
ylind = lind.getLindhardSi_k(0.15)
ylindv = np.vectorize(ylind) #careful, this expects inputs in eV
```

    {'Z': 14, 'A': 28, 'k': 0.14600172346755985, 'a': 3.0, 'b': 0.15, 'c': 0.7, 'd': 0.6}

```python
#convert the vectors
epsg = 3.8e-3 #keV average energy per electron-hole pair created

F_D = Esi(lindD_e)*(1/(epsg*ylindv(1000*Esi(lindD_e))))*lindD
F_E = Esi(lindE_e)*(1/(epsg*ylindv(1000*Esi(lindE_e))))*lindE
```

```python
#get Dougherty Data
ddataY = dp.getXYdata_wXYerr('data/Dougherty_Yield.txt')
ddataFluct = dp.getXYdata_wXYerr('data/Dougherty_Fluct.txt')

ddataY_G = dp.getXYdata_wXYerr('data/Gerbier_Yield.txt')
ddataFluct_G = dp.getXYdata_wXYerr('data/Gerbier_Fluct.txt')

#convert to numpy arrays
ddata_e = np.asarray(ddataFluct['xx'])
ddata_fluct = np.asarray(ddataFluct['yy'])
ddata_fluct_err = np.asarray(ddataFluct['ey'])

ddata_Y = np.asarray(ddataY['yy'])
ddata_Y_err = np.asarray(ddataY['ey'])
print(ddata_Y)
print(ddata_Y_err)

ddataG_e = np.asarray(ddataFluct_G['xx'])
ddataG_fluct = np.asarray(ddataFluct_G['yy'])/1000
ddataG_fluct_err = np.asarray(ddataFluct_G['ey'])/1000
print(ddataG_e)
print(ddataG_fluct)

ddataG_Y = np.asarray(ddataY_G['yy'])
ddataG_Y_err = np.asarray(ddataY_G['ey'])
print(ddataG_Y)
print(ddataG_Y_err)

epsg = 3.8e-3 #epsilon-gamma for silicon in keV per pair
ddata_fluct_F = (ddata_fluct/100)**2 * (ddata_e/(epsg*(ddata_Y/100)))
#ddata_fluct_F_err = (ddata_fluct_err/100)**2 * (ddata_e/(epsg*(ddata_Y/100)))
ddata_fluct_F_err = np.sqrt(((ddata_fluct/100)*(2*ddata_e/(epsg*(ddata_Y/100))))**2*(ddata_fluct_err/100)**2 \
                           +((ddata_fluct/100)**2*(ddata_e/(epsg*(ddata_Y/100)**2)))**2*(ddata_Y_err/100)**2 )
print(ddata_fluct_F_err)
ddata_fluct_F_err_A = np.sqrt(((ddata_fluct/100)*(2*ddata_e/(epsg*(ddata_Y/100))))**2*(ddata_fluct_err/100)**2)
ddata_fluct_F_err_B = np.sqrt(((ddata_fluct/100)**2*(ddata_e/(epsg*(ddata_Y/100)**2)))**2*(ddata_Y_err/100)**2 )
ddata_fluct_F_err = np.sqrt(ddata_fluct_F_err_A**2 + ddata_fluct_F_err_B**2)

ddata_fluct_F_G = (ddataG_fluct/ddataG_e)**2 * (ddataG_e/(epsg*(ddataG_Y/100)))
#ddata_fluct_F_err = (ddata_fluct_err/100)**2 * (ddata_e/(epsg*(ddata_Y/100)))
ddata_fluct_F_err_G = np.sqrt(((ddataG_fluct/ddataG_e)*(2*ddataG_e/(epsg*(ddataG_Y/100))))**2*(ddataG_fluct_err/ddataG_e)**2 \
                           +((ddataG_fluct/ddataG_e)**2*(ddataG_e/(epsg*(ddataG_Y/100)**2)))**2*(ddataG_Y_err/100)**2 )

ddata_fluct_F_err_G_A = np.sqrt(((ddataG_fluct/ddataG_e)*(2*ddataG_e/(epsg*(ddataG_Y/100))))**2*(ddataG_fluct_err/ddataG_e)**2)
ddata_fluct_F_err_G_B = np.sqrt(((ddataG_fluct/ddataG_e)**2*(ddataG_e/(epsg*(ddataG_Y/100)**2)))**2*(ddataG_Y_err/100)**2 )
ddata_fluct_F_err_G = np.sqrt(ddata_fluct_F_err_G_A**2 + ddata_fluct_F_err_G_B**2)

print(ddata_fluct_F)
print(ddata_fluct_F_err)
print(ddata_fluct_F_G)
print(ddata_fluct_F_err_G)
```

    [51.4 45.6 35.5 26.9 22.5]
    [2.  0.5 0.6 0.4 0.5]
    [21.7  19.5  13.5   8.6   4.7   4.15  3.9   3.3 ]
    [1.    1.101 0.601 0.348 0.185 0.166 0.241 0.131]
    [40.7 38.7 33.6 31.1 26.6 27.4 22.9 25.9]
    [0.5 0.7 0.7 0.5 0.8 0.8 2.  1.6]
    [82.17366352 27.81718869  4.07177705  1.64573833  1.92281409]
    [207.84410199 122.71543167  24.30600445   5.75229896   2.34923977]
    [82.17366352 27.81718869  4.07177705  1.64573833  1.92281409]
    [29.79629465 42.27128645 20.95522371 11.91560371  7.2041105   6.37725701
     17.11395553  5.28378686]
    [3.53496608 8.32817692 2.96120797 0.91062459 2.81212102 3.00232178
     9.49203685 4.44875839]

## Figure 1 of the Paper:

```python
#set up a plot
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

xmax=10

ax1.errorbar(ddata_e,ddata_fluct_F,yerr=[ddata_fluct_F_err,ddata_fluct_F_err], marker='o', markersize=8, \
             ecolor='k',color='k', linestyle='none', label='Dougherty eff. F', linewidth=2)
ax1.errorbar(ddataG_e,ddata_fluct_F_G,yerr=[ddata_fluct_F_err_G,ddata_fluct_F_err_G], marker='^', markersize=8, \
             ecolor='k',color='k', linestyle='none', label='Gerbier eff. F', linewidth=2)

#ax1.plot (X, diff, 'm-', label='Thomas-Fermi (newgrad)')
#ax1.plot (Esi(epr), 100*np.sqrt(f_Omega2_eta2(epr))*ylindv(1000*Esi(epr)), 'g-', label='$\Omega/\epsilon$ (NAC III approx. D)')
ax1.plot (Esi(lindD_e), F_D, 'k-', label='eff. F (Lind. approx. D)')
ax1.plot (Esi(lindE_e), F_E, 'k--', label='eff. F (Lind. approx. E)')

ax1.set_yscale('linear')
ax1.set_xscale('linear')
ax1.set_xlim(Esi(0), 150)
ax1.set_ylim(0.1,300)
ax1.set_xlabel('recoil energy ($E_r$) [keV]',**axis_font)
ax1.set_ylabel('effective Fano factor',**axis_font)
#ax1.grid(True)
#ax1.xaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=4,prop={'size':22})

###Make inset
bbox_ll_x = 0.07
bbox_ll_y = -0.0225
bbox_w = 1
bbox_h = 1
eps = 0.01
axins = inset_axes(ax1, height="35%", width="55%", bbox_to_anchor=(bbox_ll_x,bbox_ll_y,bbox_w-bbox_ll_x,bbox_h), loc='upper left',bbox_transform=ax1.transAxes)
#ax1.add_patch(plt.Rectangle((bbox_ll_x, bbox_ll_y+eps), bbox_w-eps-bbox_ll_x, bbox_h-eps, ls="--", ec="c", fc="None",
#                           transform=ax1.transAxes))

#axins = plt.axes([0,0,1,1])
#axins_pos = InsetPosition(ax3, [0.25, 0.65, 0.7, 0.3])
#axins.set_axes_locator(axins_pos)

# larger region than the original image
x1, x2, y1, y2 = 0, 10, 0, 20
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)

axins.errorbar(ddata_e,ddata_fluct_F,yerr=[ddata_fluct_F_err,ddata_fluct_F_err], marker='o', markersize=8, \
             ecolor='k',color='k', linestyle='none', label='', linewidth=2)
axins.errorbar(ddataG_e,ddata_fluct_F_G,yerr=[ddata_fluct_F_err_G,ddata_fluct_F_err_G], marker='^', markersize=8, \
             ecolor='k',color='k', linestyle='none', label='', linewidth=2)
axins.plot (Esi(lindD_e), F_D, 'k-', label='')
axins.plot (Esi(lindE_e), F_E, 'k--', label='')

axins.yaxis.grid(True,which='minor',linestyle='--')
axins.xaxis.grid(True,which='minor',linestyle='--')
axins.grid(True)
####

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

#plt.tight_layout()
plt.savefig('figures/paper_figures/SiFano_Figure1.eps')
plt.savefig('figures/paper_figures/SiFano_Figure1.pdf')
plt.show()
```

# 2. Edelweiss has Observed Anomalous NR Widening in Germanium

The 2004 EDELWEISS publication [[Edw04][Edw04]] has published a detailed and complete analysis of the measured resolutions for 7 cryogenic germanium detectors that they used for dark matter searches about ~10 keV analysis thresholds.

In this paper it was recognized (in a similar way to the Doughterty measurement) that the measured ionization yield width was larger than expected. This effect remained even after estimating the nuclear recoil band widening based on multiple-scatters. The point in this section is to estimate how much wider the measured nuclear recoil band was than the _single-scatter_ prediction.

[Edw04]: https://doi.org/10.1016/j.nima.2004.04.218 "EDELWEISS 2004 Publication"

The single-scatter prediction for the ionization yield width can be analytically estimated based on the ionization and heat channel resolutions. Each of those resolutions is extracted for each detector in the publication by the following functional forms (see the notebook `edelweiss_res.ipynb`):

\begin{equation}
\begin{aligned}
\sigma_I(E_I) &= \sqrt{(\sigma_I^0)^2 + (a'_I E_I)^2} \\
\sigma_H(E_H) &= \sqrt{(\sigma_H^0)^2 + (a'_H E_H)^2},
\end{aligned}
\end{equation}

where $E_H$ is a recoil energy estimator (unbiased for electron-recoils) based on the heat signal, and $E_I$ is a recoil energy estimator (again unbiased for electron-recoils) based on the ionization signal.

In the EDELWEISS paper the recoil energy, an estimator for the true recoil energy of an event, is defined as follows:

\begin{equation}
E_r = \left(1+\frac{V}{\epsilon_{\gamma}}\right)E_H - \frac{V}{\epsilon_{\gamma}} E_I,
\end{equation}

where $V$ is the voltage, and $\epsilon_{\gamma}$ is the average energy to create a single electron-hole pair. Finally, the ionization yield, Q, is defined as:

\begin{equation}
Q = \frac{E_I}{E_r}
\end{equation}

Given these definitons, if one _assumes_ a normal distribution for the resulting ionization yield distribution and propagates the uncertainty on Q via the equations above and a first-order Taylor expansion the result is the one published by EDELWEISS [[Edw04][Edw04]]:

\begin{equation}
\sigma_{Q}^0(E_r) = \sqrt{\frac{1}{E^2_r} \left( \left(1+\frac{V}{\epsilon_{\gamma}}\langle Q\rangle\right)^2\sigma_I^2 + \left( 1+\frac{V}{\epsilon_{\gamma}}\right)^2\langle Q\rangle^2\sigma_H^2\right)},
\end{equation}

where $\langle Q \rangle$ is the average ionization yield as a function of recoil energy.

[Edw04]: https://doi.org/10.1016/j.nima.2004.04.218 "EDELWEISS 2004 Publication"

In order to discover how much the effective Fano factor for nuclear recoils is contributing we want to first see how much wider the nuclear recoil band is than this estimate. In the EDELWEISS paper [[Edw04][Edw04]], this is done by simply adding a constant in quadrature:

\begin{equation}
\sigma_{Q}(E_r) = \sqrt{(\sigma_{Q}^{0})^2 + C^2}
\end{equation}

The constant C comes out to be _around_ 0.04, and this is larger than the expected effect of multiple-scattering (see Section 3).

To duplicate this fit for the EDELWEISS detector "GGA3" and add fitting uncertainties, we have first computed the full non-normal ionization yield distribution from the resolutions, it shows that the EDELWEISS analytical form very slightly underpredicts the ionization yield width. We call this function $\tilde{\sigma}_{Q}^0$.

[Edw04]: https://doi.org/10.1016/j.nima.2004.04.218 "EDELWEISS 2004 Publication"

```python
# import data from Edelweiss
import pandas as pds
res_data = pds.read_csv("data/edelweiss_NRwidth_GGA3_data.txt", skiprows=1, \
                       names=['E_recoil', 'sig_NR', 'E_recoil_err', 'sig_NR_err'], \
                       delim_whitespace=True)

resER_data = pds.read_csv("data/edelweiss_ERwidth_GGA3_data.txt", skiprows=1, \
                         names=['E_recoil', 'sig_ER', 'sig_ER_err'], \
                         delim_whitespace=True)

resER_data = resER_data.sort_values(by='E_recoil')

print (res_data.head(10))
E_recoil = res_data["E_recoil"]
sig_NR = res_data["sig_NR"]
sig_NR_err = res_data['sig_NR_err']
E_recoil_ER = resER_data["E_recoil"]
sig_ER = resER_data["sig_ER"]
sig_ER_err = resER_data['sig_ER_err']
```

       E_recoil    sig_NR  E_recoil_err  sig_NR_err
    0   16.1946  0.062345      0.946176    0.001157
    1   16.4428  0.062345      0.945278    0.001157
    2   44.2627  0.046528      0.992477    0.001543
    3   24.5012  0.059397      0.992477    0.001185
    4   97.7172  0.044847      1.033260    0.002783
    5   58.4014  0.050082      0.991830    0.002288
    6   34.2156  0.053417      1.033260    0.001102

```python
import h5py
filename = 'data/sims.h5'
#remove vars
f = h5py.File(filename,'r')

#save the results for the Edw fit
path='{}/'.format('ER')

xE = np.asarray(f[path+'xE'])
qbootsigs = np.asarray(f[path+'qbootsigs'])
qbootsigerrsu = np.asarray(f[path+'qbootsigerrsu'])
qbootsigerrsl = np.asarray(f[path+'qbootsigerrsl'])

f.close()
```

```python
#get the resolutions for GGA3
import EdwRes as er

aH=0.0381
V=4.0
C=0.0
sigHv,sigIv,sigQerv,sigH_NRv,sigI_NRv,sigQnrv = er.getEdw_det_res('GGA3',V,'data/edw_res_data.txt',aH,C)

import fano_calc as fc

#recall defaults (filename='test.h5',
#det='GGA3',band='ER',F=0.00001,V=4.0,alpha=(1/10000.0),aH=0.035,Erv=None,sigv=None,erase=False)
E,sig = fc.RWCalc(filename='data/res_calc.h5')

print(np.shape(E))
```

    (200,)

In the figure below, the functon $\tilde{\sigma}_{QER}^0$ is shown as the solid curve. This curve is the electron-recoil version of the correct single-scatter ionization yield width $\tilde{\sigma}_{Q}^0$. The dashed curve is the resolution predicted from the EDELWEISS publication assuming a normal distribution for the ionization at each measured recoil energy.

Both of these are using the adjusted resolution parameter $a_H^{\prime}$ equal to:

\begin{equation}
a_H^{\prime} = \frac{0.0386}{2\sqrt{2\log(2)}}.
\end{equation}

This adjustment was done in the EDELWEISS publication to fit the measured width of the electron recoil band [[Edw04][Edw04]].

[Edw04]: https://doi.org/10.1016/j.nima.2004.04.218 "EDELWEISS 2004 Publication"

Also shown in the figure is a high-statistics set of simulated data with the same resolutions (same value of $a_H^{\prime}$) and the experimental data of EDELWEISS [[Edw04][Edw04]].

## Figure 2a of the Paper:

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

mask = [True, True, False, False, True, True, True, True, True]

X=np.arange(0.1,200,0.1)

ax1.plot(X,sigQerv(X),color='r',linestyle="--",linewidth=2, \
         label='single-scatter res. model (ER) (aH={})'.format(aH))
ax1.plot(E,sig,color='r',linestyle="-",linewidth=2, \
         label='single-scatter res. model (ER) (aH={})'.format(aH))
ax1.errorbar(xE,qbootsigs, yerr=(qbootsigerrsl,qbootsigerrsu), \
             color='k', marker='o',markersize=4,linestyle='none',label='ER scatters', linewidth=2)
ax1.errorbar(E_recoil_ER[mask],sig_ER[mask], yerr=sig_ER_err[mask], \
             color='k', marker='^',markersize=8,linestyle='none',label='Edw. ER scatters', linewidth=2)

ymin = 0.04
ymax = 0.066

ax1.set_yscale('linear')
#ax1.set_yscale('log')
ax1.set_xlim(40, 200)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'recoil energy [keV]',**axis_font)
ax1.set_ylabel('ionization yield width',**axis_font)
ax1.grid(True)
ax1.yaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=1,prop={'size':22})
#ax1.legend(bbox_to_anchor=(1.04,1),borderaxespad=0,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

plt.tight_layout()
plt.savefig('figures/paper_figures/ERyieldWidth_Figure2a.eps')
plt.savefig('figures/paper_figures/ERyieldWidth_Figure2a.pdf')
plt.show()
```

With the value of $a_H^{\prime}$ specified by the fit above, it is possible to calculate the expected single-scatter nuclear recoil ionization yield width as a function of energy, $\tilde{\sigma}_{Q}^0(E_r)$.

With this function in hand, we can then repeat the fit done in the EDELWEISS paper using the corrected version of the equation above:

\begin{equation}
\sigma_{Q} = \sqrt{(\tilde{\sigma}_{Q}^0)^2 + C^2}.
\end{equation}

Furthermore, we allow the parameter C to be a linear function of energy, to improve the fit quality, $C = C_0 + mE_r$. This fit is displayed in the figure below.

```python
filename = 'data/systematic_error_fits.h5'
#remove vars
f = h5py.File(filename,'r')
for i in f['mcmc/edwdata_sys_error']:
    print(i)

#save the results for the Edw fit
path='{}/{}/'.format('mcmc','edwdata_sys_error')

Cms = np.asarray(f[path+'Cms'])
slope = np.asarray(f[path+'m'])
a_yield = np.asarray(f[path+'A'])
b_yield = np.asarray(f[path+'B'])
aH = np.asarray(f[path+'aH'])
scale = np.asarray(f[path+'scale'])
samples = np.asarray(f[path+'samples'])
sampsize = np.asarray(f[path+'sampsize'])
xl = np.asarray(f[path+'Er'])
upvec = np.asarray(f[path+'Csig_u'])
dnvec = np.asarray(f[path+'Csig_l'])
Sigtot = np.asarray(f[path+'Sigss'])
Sigss = np.sqrt(Sigtot**2 - (Cms+slope*xl)**2)

print(Cms)
print(samples[0:5,:])
f.close()
print(np.shape(samples))
```

    A
    B
    Cms
    Csig_l
    Csig_u
    Er
    Sigss
    aH
    m
    samples
    sampsize
    scale
    0.0401182258
    [[1.65991113e-02 3.17938117e-02 1.72848169e-04 9.60623060e-01
      2.53571472e-01 3.89854952e-02]
     [1.64140930e-02 3.62294874e-02 1.21912111e-04 9.89509101e-01
      1.72416566e-01 1.04542858e-01]
     [1.64008065e-02 3.62266219e-02 1.25921253e-04 9.95054885e-01
      1.72930975e-01 1.00686204e-01]
     [1.62507246e-02 3.65956482e-02 1.11214649e-04 1.00693739e+00
      1.64393892e-01 1.03460830e-01]
     [1.61375604e-02 3.63839307e-02 1.10043601e-04 1.01873876e+00
      1.64451219e-01 1.12642479e-01]]
    (470000, 6)

## Figure 2b of the Paper:

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

print(np.shape(samples[np.random.randint(len(samples), size=100)]))
#for Cms_em, m_em in samples[np.random.randint(len(samples), size=100)]:
for aH_em, Cms_em, m_em, scale_em, A_em, B_em in samples[np.random.randint(len(samples), size=100)]:
    ax1.plot(xl, np.sqrt(Sigss**2+(Cms_em+m_em*xl)**2), color="orange", alpha=0.1)

ax1.plot(xl,upvec,color='r',linestyle="--",linewidth=2, \
         label='1$\sigma$ fluct.')
ax1.plot(xl,dnvec,color='r',linestyle="--",linewidth=2, \
         label='')

ax1.plot(xl,np.sqrt(Sigss**2+(Cms+xl*slope)**2),color='g',linestyle="-",linewidth=3, \
         label='(C$_0$={:01.3}; m={:01.2E})'.format(Cms,slope))

ax1.errorbar(E_recoil[2::],sig_NR[2::], yerr=sig_NR_err[2::], \
         color='k', marker='o', markersize=4,linestyle='none',label='NR Edw. Measurement', linewidth=2)

ymin = 0.04
ymax = 0.1

ax1.set_yscale('linear')
#ax1.set_yscale('log')
ax1.set_xlim(10, 200)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'recoil energy [keV]',**axis_font)
ax1.set_ylabel('ionization yield width',**axis_font)
ax1.grid(True)
ax1.yaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=1,prop={'size':22})
#ax1.legend(bbox_to_anchor=(1.04,1),borderaxespad=0,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

plt.tight_layout()
plt.savefig('figures/paper_figures/EdwyieldWidthFit_Figure2b.eps')
plt.savefig('figures/paper_figures/EdwyieldWidthFit_Figure2b.pdf')
plt.show()
```

# 3. Multiple-Scattering Cannot Account for All of the Yield Broadening

An obvious candidate, _aside_ from an intrinsic effective Fano factor, that might account for the yield broadening observed over the single-scatter prediction is multiple-scattering. If a neutron enters the detector and scatters more than once, the known non-linearity of the average ionization yield for each collison **_guarantees_** that the total yield will fluctuate to lower values than expected given the **_total_** energy deposited.

We use a Monte Carlo simulation of neutron scattering from a $^{252}$Cf source to approximate this effect. The following empirical single-scatter yield model is used since it approximates the EDELWEISS data fairly well:

\begin{equation}
\langle Q \rangle = 0.16E_r^{0.18}.
\end{equation}

We apply this yield model to _each individual scatter_ in the simulated data and then sum to obtain the expected measured ionization yield, also folding in the measured EDELWEISS sensor resolutions appropriately.

```python
import observable_simulations as osim

Q,Ernr,Q_ss,Ernr_ss = osim.simQEr()

Emin = 20
Emax = 30

import histogram_yield as hy

bindf, bindfE = hy.QEr_Ebin(Q, Ernr, bins=[Emin, Emax],silent=True)

qbins = np.linspace(0,0.6,40)
xcq = (qbins[:-1] + qbins[1:]) / 2

for i,Qv in enumerate(bindf):
    n,nx = np.histogram(Qv,bins=qbins)

bindf_ss, bindfE_ss = hy.QEr_Ebin(Q_ss, Ernr_ss, bins=[Emin, Emax],silent=True)

for i,Qv in enumerate(bindf_ss):
    n_ss,nx_ss = np.histogram(Qv,bins=qbins)

```

## Figure 3a of the Paper:

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

mshist = n/np.sum(n)/np.diff(xcq)[0]
sshist = n_ss/np.sum(n_ss)/np.diff(xcq)[0]

estring = r'${}\mathrm{{keV}}< E_r \leq {}\mathrm{{keV}}$'.format(Emin,Emax)
#print(estring)
ax1.step(xcq,mshist, where='mid',color='m', linestyle='-', \
            label='multiple scatters {}'.format(estring), linewidth=2)
ax1.step(xcq,sshist, where='mid',color='b', linestyle='-', \
            label='single scatters'.format(estring), linewidth=2)

ymin = 0.0
ymax = 10

blue = '#118DFA'
ax1.fill_between(xcq,np.zeros(np.shape(xcq)),mshist,step='mid',facecolor='m',alpha=0.4, \
                 label='')
ax1.fill_between(xcq,np.zeros(np.shape(xcq)),sshist,step='mid',facecolor='b',alpha=0.4, \
                 label='')

ax1.set_yscale('linear')
#ax1.set_yscale('log')
ax1.set_xlim(0.0, 0.6)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'ionization yield',**axis_font)
ax1.set_ylabel('PDF',**axis_font)
ax1.grid(True)
ax1.yaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=1,prop={'size':22})
#ax1.legend(bbox_to_anchor=(1.04,1),borderaxespad=0,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

plt.tight_layout()
plt.savefig('figures/paper_figures/MSyieldHist_Figure3a.eps')
plt.savefig('figures/paper_figures/MSyieldHist_Figure3a.pdf')
plt.show()
```

```python
filename = 'data/mcmc_fits.h5'
#remove vars
f = h5py.File(filename,'r')

#save the results for the Edw fit
path='{}/{}/'.format('mcmc','multiples')

Cms = np.asarray(f[path+'Cms'])
slope = np.asarray(f[path+'m'])
samples = np.asarray(f[path+'samples'])
sampsize = np.asarray(f[path+'sampsize'])
xl = np.asarray(f[path+'Er'])
upvec = np.asarray(f[path+'Csig_u'])
dnvec = np.asarray(f[path+'Csig_l'])
Sigss = np.asarray(f[path+'Sigss'])

f.close()
print(np.shape(samples))
```

    (40000, 2)

```python
filename = 'data/sims.h5'
#remove vars
f = h5py.File(filename,'r')

#save the results for the Edw fit
path='{}/'.format('NR')

xE = np.asarray(f[path+'xE'])
qbootsigs = np.asarray(f[path+'qbootsigs'])
qbootsigerrsu = np.asarray(f[path+'qbootsigerrsu'])
qbootsigerrsl = np.asarray(f[path+'qbootsigerrsl'])

f.close()
```

## Figure 3b of the Paper:

```python
#set up a 1d plot
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

for Cms_em, m_em in samples[np.random.randint(len(samples), size=100)]:
    ax1.plot(xl, np.sqrt(Sigss**2+(Cms_em+m_em*xl)**2), color="orange", alpha=0.1)

ax1.plot(xl,upvec,color='r',linestyle="--",linewidth=2, \
         label='1$\sigma$ fluct.')
ax1.plot(xl,dnvec,color='r',linestyle="--",linewidth=2, \
         label='')

ax1.plot(xl,np.sqrt(Sigss**2+(Cms+xl*slope)**2),color='g',linestyle="-",linewidth=3, \
         label='(C$_0$={:01.3}; m={:01.2E})'.format(Cms,slope))

ax1.errorbar(xE,qbootsigs, yerr=(qbootsigerrsl,qbootsigerrsu), \
         color='k', marker='o', markersize=4,linestyle='none',label='simulated NR scatters', linewidth=2)

ymin = 0.025
ymax = 0.045

ax1.set_yscale('linear')
#ax1.set_yscale('log')
ax1.set_xlim(10, 200)
ax1.set_ylim(ymin,ymax)
ax1.set_xlabel(r'recoil energy [keV]',**axis_font)
ax1.set_ylabel('ionization yield width',**axis_font)
ax1.grid(True)
ax1.yaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=1,prop={'size':22})
#ax1.legend(bbox_to_anchor=(1.04,1),borderaxespad=0,prop={'size':22})

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)

plt.tight_layout()
plt.savefig('figures/paper_figures/MSyieldWidthFit_Figure3b.eps')
plt.savefig('figures/paper_figures/MSyieldWidthFit_Figure3b.pdf')
plt.show()
```

# 4. This Implies a Certain "Effective" Fano Factor

It is clear from the previous sections that the additions that need to be added to the account for the simulated multiple scatters are not as large as those required to explain the EDELWEISS data. This means that there is an "extra" unaccounted variance that needs to be added to the ionization yield.

We take the position that this variance is the intrinsic variance in the number of electron-hole pairs produced in a primary nuclear recoil reaction. Since this is analagous the the variance that is parameterized by the Fano factor for electron recoils, we call this the effective Fano factor for nuclear recoils.

We note that physically, this variance comes from a different mechanism than for electron recoils. In the nuclear recoil case most of the variance comes from the variation of the energy put into the phonon system from the primary recoil. In electron recoils there is little to no energy put into the phonon system from the primary recoil. Therefore, it is not surprising that the effective Fano factor can be significantly larger than the electron-recoil counterpart.

Since we have accurately modeled the ionization yield variance without severe approximation (we do assume the number of electron-hole pairs is distributed normally; a very mild assumption when large numbers of pairs are expected), we can also include an intrinsic Fano factor in the modeling. Effectively we make the following replacement:

\begin{equation}
\tilde{\sigma}_{Q}^0(E_r) \rightarrow \tilde{\sigma}_{Q}^0(E_r;F_n),
\end{equation}

Where $F_n$ is the effective nuclear recoil Fano factor.

To extract the effective Fano factor for the nuclear recoils we need to come up with a parameter C$_F$ which is a function of recoil energy and is a corrected version of the measured "widening" parameter C from the EDELWEISS data and the widening parameter C$^{\prime}$ from the effect of multiple-scattering.

The corrected parameter C$_F$ is assumed to be due to the effective Fano factor for nuclear recoils and is given by:

\begin{equation}
C_F = \sqrt{C^2 - C^{\prime 2}}.
\end{equation}

This parameter can be used to extract the effective Fano factor at a given recoil energy by applying our $\tilde{E}_r$-Q plane model (from `QEr_2D_joint.ipynb`), with an arbitrary Fano factor, until the correct ionization yield (Q) width is obtained (see `Qwidth_confirm.ipynb`). Mathematically this corresponds to adjusting F$_n$ until the following equality is satisfied:

\begin{equation}
\tilde{\sigma}_{Q}^0(E_r;F_n) = \sqrt{\left(\tilde{\sigma}_{Q}^0(E_r)\right)^2 + C_F^2}.
\end{equation}

## Uncertainties on F$_n$

Since both C and C$^{\prime}$ have uncertainty it is necessary to propagate that uncertainty to F$_n$. If we call the uncertainty (1$\sigma$) on C $\sigma$ and on C$^{\prime}$ $\sigma^{\prime}$, then the uncertainty on C$_F$ is given by:

\begin{equation}
\sigma_{C_F} = \frac{1}{\sqrt{C^2 - C^{\prime 2}}} \sqrt{C^2 \sigma^2 + C^{\prime 2} \sigma^{\prime 2}}.
\end{equation}

These uncertainties are propagated to the extracted F$_n$ by solving the following equality for F$^+_n$ and F$^-_n$ which represent the corresponding upper and lower boundaries on F$_n$.

\begin{equation}
\begin{aligned}
\tilde{\sigma}_{Q}^0(E_r;F^+_n) &= \sqrt{\left(\tilde{\sigma}_{Q}^0(E_r)\right)^2 + \left(C_F + \sigma_{C_F}\right)^2} \\
\tilde{\sigma}_{Q}^0(E_r;F^-_n) &= \sqrt{\left(\tilde{\sigma}_{Q}^0(E_r)\right)^2 + \left(C_F - \sigma_{C_F}\right)^2}
\end{aligned}
\end{equation}

```python
import fano_calc as fc

(Er,F,Fup,Fdn) = fc.RWCalcFMCMC('data/mcmc_fano.h5')
```

    GGA3/4.0/5.556E-02/0.0381/
    True

# Figure 4 of the Paper:

```python
#set up a plot
from mpl_toolkits.axes_grid1.inset_locator import inset_axes
from mpl_toolkits.axes_grid1.inset_locator import InsetPosition
fig,axes = plt.subplots(1,1,figsize=(9.0,8.0),sharex=True)
ax1 = axes

xmax=10

#ax1.errorbar(ddata_e,ddata_fluct_F,yerr=[ddata_fluct_F_err,ddata_fluct_F_err], marker='o', markersize=8, \
#             ecolor='k',color='k', linestyle='none', label='Dougherty eff. F', linewidth=2)

#ax1.plot (X, diff, 'm-', label='Thomas-Fermi (newgrad)')
#ax1.plot (Esi(epr), 100*np.sqrt(f_Omega2_eta2(epr))*ylindv(1000*Esi(epr)), 'g-', label='$\Omega/\epsilon$ (NAC III approx. D)')
ax1.plot (Er, F, 'k-', label='extracted Ge eff. Fano')
ax1.plot (Er, Fup, 'b', label='')
ax1.plot (Er, Fdn, 'b', label='')

blue = '#118DFA'
ax1.fill_between(Er,Fdn,Fup,facecolor=blue,alpha=0.5,label='1$\sigma$ statistical region')

ax1.set_yscale('linear')
ax1.set_xscale('linear')
ax1.set_xlim(10, 200)
ax1.set_ylim(6,300)
ax1.set_xlabel('recoil energy ($E_r$) [keV]',**axis_font)
ax1.set_ylabel('effective Fano factor (F$_n$)',**axis_font)
#ax1.grid(True)
#ax1.xaxis.grid(True,which='minor',linestyle='--')
ax1.legend(loc=4,prop={'size':22})

###Make inset
bbox_ll_x = 0.07
bbox_ll_y = -0.0225
bbox_w = 1
bbox_h = 1
eps = 0.01
axins = inset_axes(ax1, height="25%", width="50%", bbox_to_anchor=(bbox_ll_x,bbox_ll_y,bbox_w-bbox_ll_x,bbox_h), loc='upper left',bbox_transform=ax1.transAxes)
#ax1.add_patch(plt.Rectangle((bbox_ll_x, bbox_ll_y+eps), bbox_w-eps-bbox_ll_x, bbox_h-eps, ls="--", ec="c", fc="None",
#                           transform=ax1.transAxes))

#axins = plt.axes([0,0,1,1])
#axins_pos = InsetPosition(ax3, [0.25, 0.65, 0.7, 0.3])
#axins.set_axes_locator(axins_pos)

# larger region than the original image
x1, x2, y1, y2 = 7, 30, 0, 30
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
axins.plot (Er, F, 'k-', label='')
axins.plot (Er, Fup, 'b', label='')
axins.plot (Er, Fdn, 'b', label='')
axins.fill_between(Er,Fdn,Fup,facecolor=blue,alpha=0.5,label='')
axins.yaxis.grid(True,which='minor',linestyle='--')
axins.xaxis.grid(True,which='minor',linestyle='--')
axins.grid(True)
####

for axis in ['top','bottom','left','right']:
  ax1.spines[axis].set_linewidth(2)
  axins.spines[axis].set_linewidth(2)

#plt.tight_layout()
#plt.savefig('figures/figure.png')
plt.savefig('figures/paper_figures/GeFano_Figure4.eps')
plt.savefig('figures/paper_figures/GeFano_Figure4.pdf')
plt.show()
```

```python

```


```python
from IPython.core.display import display_html
from urllib.request import urlopen

cssurl = 'http://j.mp/1DnuN9M'
display_html(urlopen(cssurl).read(), raw=True)
```

<link href='http://fonts.googleapis.com/css?family=EB+Garamond' rel='stylesheet' type='text/css'>
<link href='http://fonts.googleapis.com/css?family=Lato:100,100italic' rel='stylesheet' type='text/css'>

# Filtro de suavizado

## El problema

Queremos recuperar una imagen corrupta, es decir, una imagen que a traves de un proceso desconocido, perdió información.

Pero como te puedes imaginar la información simplemente no es recuperable de la nada, en esta ocasión intentaremos recuperar algo de la definición de la imagen tratando de minimizar los bordes visibles en imagen, es decir suavizarla.

Empecemos primero por mostrar nuestra imagen:

```python
# Se importan funciones para graficar y se inicializa con graficas en linea
%matplotlib inline
from matplotlib.pyplot import imshow, cm, figure
```

```python
# Se importa funcion para cargar imagenes
from scipy.ndimage import imread
```

```python
# Se guardan las rutas a los archivos en variables para facil acceso
correcta = "imagenes/stones.jpg"
corrupta = "imagenes/stones_c.jpg"
```

```python
# Se lee la imagen del archivo a una variable de python y se grafica
im_corrupta = imread(corrupta)

f  = figure(figsize=(8,6))
ax = imshow(im_corrupta, cmap=cm.gray, interpolation='none');

ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)

ax.axes.spines["right"].set_color("none")
ax.axes.spines["left"].set_color("none")
ax.axes.spines["top"].set_color("none")
ax.axes.spines["bottom"].set_color("none")
```

Como podemos ver la imagen a perdido definición al utilizar un metodo de compresión muy ingenuo, el cual simplemente repite la misma información una y otra vez, tomemos una muestra de los datos para ilustrar esto mejor:

```python
tamano_muestra = 12
muestra = im_corrupta[0:tamano_muestra, 0:tamano_muestra]
muestra
```

    array([[44, 44, 44, 44, 33, 33, 33, 33, 17, 17, 17, 17],
           [44, 44, 44, 44, 33, 33, 33, 33, 17, 17, 17, 17],
           [44, 44, 44, 44, 33, 33, 33, 33, 17, 17, 17, 17],
           [44, 44, 44, 44, 33, 33, 33, 33, 17, 17, 17, 17],
           [43, 43, 43, 43, 34, 34, 34, 34, 20, 20, 20, 20],
           [43, 43, 43, 43, 34, 34, 34, 34, 20, 20, 20, 20],
           [43, 43, 43, 43, 34, 34, 34, 34, 20, 20, 20, 20],
           [43, 43, 43, 43, 34, 34, 34, 34, 20, 20, 20, 20],
           [45, 45, 45, 45, 34, 34, 34, 34, 26, 26, 26, 26],
           [45, 45, 45, 45, 34, 34, 34, 34, 26, 26, 26, 26],
           [45, 45, 45, 45, 34, 34, 34, 34, 26, 26, 26, 26],
           [45, 45, 45, 45, 34, 34, 34, 34, 26, 26, 26, 26]], dtype=uint8)

Lo cual graficamente se ve:

```python
imshow(muestra, cmap=cm.gray, interpolation='none');
```

## La solución

Muy bien, es momento de pensar!

Si lo que queremos es **minimizar** las *diferencias* entre dos valores contiguos, es decir

$$
x_{(i+1)j} - x_{ij}
$$

podemos empezar restandolos y ver que pasa:

```python
from numpy import matrix, eye, array
```

```python
# Creamos una matriz identidad y la trasladamos para obtener el valor de la celda
# contigua derecha
I = eye(tamano_muestra, dtype=int).tolist()
# Agregamos un vector cero, por el momento
ceros = [0 for i in range(tamano_muestra)]
id_trasladada = matrix(array(I[1:tamano_muestra] + [ceros]))
id_trasladada
```

    matrix([[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
            [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]])

```python
muestra_rest = matrix(muestra) * id_trasladada - matrix(muestra)
muestra_rest
```

    matrix([[-44,   0,   0,   0,  11,   0,   0,   0,  16,   0,   0,   0],
            [-44,   0,   0,   0,  11,   0,   0,   0,  16,   0,   0,   0],
            [-44,   0,   0,   0,  11,   0,   0,   0,  16,   0,   0,   0],
            [-44,   0,   0,   0,  11,   0,   0,   0,  16,   0,   0,   0],
            [-43,   0,   0,   0,   9,   0,   0,   0,  14,   0,   0,   0],
            [-43,   0,   0,   0,   9,   0,   0,   0,  14,   0,   0,   0],
            [-43,   0,   0,   0,   9,   0,   0,   0,  14,   0,   0,   0],
            [-43,   0,   0,   0,   9,   0,   0,   0,  14,   0,   0,   0],
            [-45,   0,   0,   0,  11,   0,   0,   0,   8,   0,   0,   0],
            [-45,   0,   0,   0,  11,   0,   0,   0,   8,   0,   0,   0],
            [-45,   0,   0,   0,  11,   0,   0,   0,   8,   0,   0,   0],
            [-45,   0,   0,   0,  11,   0,   0,   0,   8,   0,   0,   0]])

Esta matriz nos muestra la diferencia con el elemento contiguo, si lo analizamos graficamente:

```python
imshow(muestra_rest, cmap=cm.gray, interpolation='none');
```

podemos ver que solo hay diferencia entre los conjuntos de pixeles de la imagen que fueron eliminados.

Ahora, el punto es minimizar estas diferencias segun un factor de desempeño, y como pudiste notar en el ejemplo, pueden haber valores negativos, por lo que una buena idea es hacer el factor de desempeño un factor cuadratico:

$$
\left|\left| x_{(i+1)j} - x_{ij} \right|\right|^2
$$

y utilizando la forma matricial, que honestamente es mucho mas util en el caso de estas imagenes, nos queda:

$$
\left|\left| X (I_t - I) \right|\right|^2 = \left|\left| X D_1 \right|\right|^2
$$

en donde:

$$
I =
\begin{pmatrix}
1 & 0 & 0 & \dots & 0 & 0 & 0 \\
0 & 1 & 0 & \dots & 0 & 0 & 0 \\
0 & 0 & 1 & \dots & 0 & 0 & 0 \\
\vdots & \vdots & \vdots & & \vdots & \vdots & \vdots \\
0 & 0 & 0 & \dots & 1 & 0 & 0 \\
0 & 0 & 0 & \dots & 0 & 1 & 0
\end{pmatrix}
$$

$$
I_t =
\begin{pmatrix}
0 & 1 & 0 & \dots & 0 & 0 & 0 \\
0 & 0 & 1 & \dots & 0 & 0 & 0 \\
0 & 0 & 0 & \dots & 0 & 0 & 0 \\
\vdots & \vdots & \vdots & & \vdots & \vdots & \vdots \\
0 & 0 & 0 & \dots & 0 & 1 & 0 \\
0 & 0 & 0 & \dots & 0 & 0 & 1
\end{pmatrix}
$$

y por lo tanto $D_1$ es de la forma:

$$
D_1 =
\begin{pmatrix}
-1 & 1 & 0 & \dots & 0 & 0 & 0 \\
0 & -1 & 1 & \dots & 0 & 0 & 0 \\
0 & 0 & -1 & \dots & 0 & 0 & 0 \\
\vdots & \vdots & \vdots & & \vdots & \vdots & \vdots \\
0 & 0 & 0 & \dots & -1 & 1 & 0 \\
0 & 0 & 0 & \dots & 0 & -1 & 1
\end{pmatrix}
$$

Cabe mencionar que $I$ e $I_t$ no son matrices cuadradas, ya que tienen una columna mas, principalmente para ajustar el hecho de que la operación de resta es binaria y necesitamos hacer una operación por cada uno de las $n$ columnas, por lo que necesitaremos $n + 1$ operandos; sin embargo al obtener el factor cuadrado, nos quedará una matriz de las dimensiones adecuadas.

Asi pues, este factor cuadrado, lo denotaremos por la función $f_1(X)$ de la siguiente manera:

$$
f_1(X) = \left|\left| X D_1 \right|\right|^2 = D_1^T X^T X D_1
$$

y de la misma manera obtendremos un operador para la diferencia entre los elementos contiguos verticalmente, el cual se verá:

$$
f_2(X) = \left|\left| D_2 X \right|\right|^2 = X^T D_2^T D_2 X
$$

en donde $D_2$ es de la forma:

$$
D_2 =
\begin{pmatrix}
-1 & 0 & 0 & \dots & 0 & 0 \\
1 & -1 & 0 & \dots & 0 & 0 \\
0 & 1 & -1 & \dots & 0 & 0 \\
\vdots & \vdots & \vdots & & \vdots & \vdots \\
0 & 0 & 0 & \dots & -1 & 0 \\
0 & 0 & 0 & \dots & 1 & -1 \\
0 & 0 & 0 & \dots & 0 & 1
\end{pmatrix}
$$

Asi pues, nuestro objetivo es minimizar la siguiente expresión:

$$
\min_{X \in \mathbb{R}^{n \times m}} f_1(X) + f_2(X)
$$

Sin embargo tenemos que considerar que una optimización perfecta nos llevaria al caso en que todos los valores son exactamente iguales, por lo que agregaremos un termino para penalizar una diferencia demasiado grande con la imagen a suavizar, el cual simplemente es la diferencia entre la imagen obtenida y la imagen corrupta:

$$
f_3(X) = \left|\left| X - X_C \right|\right|^2
$$

Por lo que nuestra expresión a minimizar se vuelve:

$$
\min_{X \in \mathbb{R}^{n \times m}} V(X) = \min_{X \in \mathbb{R}^{n \times m}} \delta \left( f_1(X) + f_2(X) \right) + f_3(X) \quad \delta > 0
$$

en donde $\delta$ es la ponderación que le damos al termino *suavizante*.

  $\DeclareMathOperator{\trace}{tr}$

Cabe hacer la aclaración de que hasta el momento hemos utilizado una norma matricial, normal, sin embargo ahora utilizaremos la norma de Frobenius, la cual se define como:

$$
f_1(X) = \left|\left| X D_1 \right|\right|_F^2 = \trace{(D_1^T X^T X D_1)}
$$

y esta nos provee una manera facil de calcular la forma cuadratica que queremos. Mas aún, esta $f_1(X) \in \mathbb{R}$, por lo que podemos usar los conceptos de calculo variacional que hemos aprendido.

Ahora empezamos a calcular el valor de estas funciones alrededor de $X$ con una variación $H$.

$$
\begin{align}
f_1(X + H) &= \trace{\left( D_1^T (X + H)^T (X + H) D_1 \right)} \\
&= \trace{\left( D_1^T (X^T + H^T) (X + H) D_1 \right)} \\
&= \trace{\left( D_1^T (X^T X + X^T H + H^T X + H^T H) D_1 \right)} \\
&= \trace{\left( D_1^T X^T X D_1 + D_1^T X^T H D_1 + D_1^T H^T X D_1 + D_1^T H^T H D_1 \right)} \\
&= \trace{\left( D_1^T X^T X D_1 \right)} + \trace{\left( D_1^T X^T H D_1 \right)} + \trace{\left( D_1^T H^T X D_1 \right)} + \trace{\left( D_1^T H^T H D_1 \right)} \\
\end{align}
$$

Aqui hacemos notar que el primer termino es $f_1(X) = \trace{\left( D_1^T X^T X D_1 \right)}$, el segundo y tercer termino son el mismo, ya que la traza es invariante ante la transposición y el ultimo termino es de orden superior,  $o\left(\left|\left|H\right|\right|_F\right)$.

Recordemos que la variable con respecto a la que estamos haciendo estos calculos es la perturbación $H$, por lo que los terminos de orden superior estan relacionados a $H$ y no a $X$ la cual asumimos es nuestro optimo.

Si desarrollamos la expasión de la serie de Taylor alrededor de $X$ con una perturbación $H$, notaremos que los terminos que obtuvimos corresponden a los de esta expansión:

$$
f_1(X + H) = f_1(X) + f_1'(X) \cdot H + o\left(\left|\left| H \right|\right|_F\right)
$$

y por lo tanto:

$$
f_1'(X) \cdot H = 2 \trace{\left( D_1^T X^T H D_1 \right)}
$$

Si expandimos las otras dos funciones alrededor de X con una perturbación $H$, podremos ver que:

$$
f_2'(X) \cdot H = 2 \trace{\left( X^T D_2^T D_2 H \right)}
$$

$$
f_3'(X) \cdot H = 2 \trace{\left( \left( X - X_C \right)^T H \right)}
$$

Ahora, por superposición podemos asegurar que nuestro criterio de desempeño $V(X)$ tiene una derivada de la forma:

$$
\begin{align}
V'(X) \cdot H &= \left( f_1'(X) \cdot H + f_2'(X) \cdot H \right) \delta + f_3'(X) \cdot H \\
&= \left( 2 \trace{\left( D_1^T X^T H D_1 \right)} + 2 \trace{\left( X^T D_2^T D_2 H \right)} \right) \delta + 2 \trace{\left( \left( X - X_C \right)^T H \right)} \\
&= 2 \trace{\left[ \left( \left( D_1^T X^T H D_1 \right) + \left( X^T D_2^T D_2 H \right) \right) \delta + \left( X - X_C \right)^T H \right]}
\end{align}
$$

y al utilizar la condición de optimalidad de primer orden tenemos que:

$$
V'(X) \cdot H = 2 \trace{\left[ \left( \left( D_1^T X^T H D_1 \right) + \left( X^T D_2^T D_2 H \right) \right) \delta + \left( X - X_C \right)^T H \right]} = 0
$$

y al hacer manipulación algebraica, obtenemos que:

$$
\begin{align}
\trace{\left[ \left( \left( D_1^T X^T H D_1 \right) + \left( X^T D_2^T D_2 H \right) \right) \delta + \left( X - X_C \right)^T H \right]} &= 0 \\
\trace{\left[ \left( \left( D_1^T H^T X D_1 \right) + \left( H^T D_2^T D_2 X \right) \right) \delta + H^T \left( X - X_C \right) \right]} &= 0 \\
\trace{\left[ \left( \left( H^T X D_1 D_1^T \right) + \left( H^T D_2^T D_2 X \right) \right) \delta + H^T \left( X - X_C \right) \right]} &= 0 \\
\trace{\left[ H^T \left( X D_1 D_1^T + D_2^T D_2 X \right) \delta + \left( X - X_C \right) \right]} &= 0
\end{align}
$$

En este punto nos preguntamos, para que condiciones de perturbación queremos que nuestra condición de optimalidad se cumpla, por lo que si exigimos que esto se cumpla para toda $H$, tenemos que:

$$
\left( X D_1 D_1^T + D_2^T D_2 X \right) \delta + \left( X - X_C \right) = 0
$$

lo cual implica que:

$$
X \delta D_1 D_1^T + ( \delta D_2^T D_2 + I) X = X_C
$$

lo cual tiene la forma de la ecuación de Lyapunov:

$$
A X + X B = Q
$$

en donde $A$ y $B$ son de la forma:

$$
A = \delta D_2^T D_2 + I \quad B = \delta D_1 D_1^T
$$

por lo que ya encontramos una forma de programar este algoritmo de suavizado, utilizando la función ```solve_sylvester``` proporcionada por el paquete Scipy.

Ahora regresemos a la programación; lo que tenemos que construir son las matrices $D_1$ y $D_2$ para incorporarlas a una función que calcule todo en linea.

Empecemos construyendo una de las filas de esta matriz. Recordemos que $D_1$ es de la forma:

$$
D_1 =
\begin{pmatrix}
-1 & 1 & 0 & \dots & 0 & 0 & 0 \\
0 & -1 & 1 & \dots & 0 & 0 & 0 \\
0 & 0 & -1 & \dots & 0 & 0 & 0 \\
\vdots & \vdots & \vdots & & \vdots & \vdots & \vdots \\
0 & 0 & 0 & \dots & -1 & 1 & 0 \\
0 & 0 & 0 & \dots & 0 & -1 & 1
\end{pmatrix}
$$

por lo que primero tenemos que construir un arreglo de la forma:

$$
\begin{pmatrix}
-1 & 1 & 0 & \dots & 0 & 0 & 0
\end{pmatrix}
$$

La siguiente función describe una manera **dificil** de conseguir esto, sin embargo para efectos de demostración servirá:

```python
def fun(i, tot):
    '''Arreglo especial
    Esta funcion crea un arreglo de tamaño tot con un -1 en el elemento i y un
    1 en el elemento i+1, siendo los demas lugares del arreglo ceros:

     indice ->  0, 1, ..., i-1, i, i+1, i+2, ..., tot
    arreglo -> [0, 0, ...,   0, -1,  1,   0, ...,   0].

    Ejemplo
    -------
    >>> fun(3, 5)
    array([ 0,  0,  -1, 1,  0])
    '''

    # Se importan funciones necesarias
    from numpy import array

    # Se define el inicio del arreglo
    if i == 0:
        a = [-1]
        a.append(1)
    else:
        a = [0]

    # Se incluyen numeros restantes en el arreglo
    for t in range(tot - 1)[1:]:
        if i == t:
            a.append(-1)
            a.append(1)
        else:
            a.append(0)

    # Se convierte en arreglo de numpy el resultado
    return array(a)
```

Cuando mandamos llamar esta función para que nos de un arreglo de diez elementos, con el $-1$ en el segundo lugar, obtendremos:

```python
fun(1, 10)
```

    array([ 0, -1,  1,  0,  0,  0,  0,  0,  0,  0])

Python lista los arreglos, y en general todas sus estructuras, empezando en ```0```, por lo que el indice ```1``` corresponde al segundo lugar.

Y ahora, utilizando una función especial de Python, crearemos un arreglo de arreglos, utilizando una sintaxis muy parecida a la de una definición matemática de la forma:

$$
\left\{ f(i) : i \in [0, 10] \right\}
$$

```python
arreglo_de_arreglos = [fun(i, 11) for i in range(10)]
arreglo_de_arreglos
```

    [array([-1,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0]),
     array([ 0, -1,  1,  0,  0,  0,  0,  0,  0,  0,  0]),
     array([ 0,  0, -1,  1,  0,  0,  0,  0,  0,  0,  0]),
     array([ 0,  0,  0, -1,  1,  0,  0,  0,  0,  0,  0]),
     array([ 0,  0,  0,  0, -1,  1,  0,  0,  0,  0,  0]),
     array([ 0,  0,  0,  0,  0, -1,  1,  0,  0,  0,  0]),
     array([ 0,  0,  0,  0,  0,  0, -1,  1,  0,  0,  0]),
     array([ 0,  0,  0,  0,  0,  0,  0, -1,  1,  0,  0]),
     array([ 0,  0,  0,  0,  0,  0,  0,  0, -1,  1,  0]),
     array([ 0,  0,  0,  0,  0,  0,  0,  0,  0, -1,  1])]

Esto se puede convertir facilmente en una matriz por medio de la instrucción ```matrix```.

```python
matrix(arreglo_de_arreglos)
```

    matrix([[-1,  1,  0,  0,  0,  0,  0,  0,  0,  0,  0],
            [ 0, -1,  1,  0,  0,  0,  0,  0,  0,  0,  0],
            [ 0,  0, -1,  1,  0,  0,  0,  0,  0,  0,  0],
            [ 0,  0,  0, -1,  1,  0,  0,  0,  0,  0,  0],
            [ 0,  0,  0,  0, -1,  1,  0,  0,  0,  0,  0],
            [ 0,  0,  0,  0,  0, -1,  1,  0,  0,  0,  0],
            [ 0,  0,  0,  0,  0,  0, -1,  1,  0,  0,  0],
            [ 0,  0,  0,  0,  0,  0,  0, -1,  1,  0,  0],
            [ 0,  0,  0,  0,  0,  0,  0,  0, -1,  1,  0],
            [ 0,  0,  0,  0,  0,  0,  0,  0,  0, -1,  1]])

Por lo que estamos listos para juntar todos estos elementos en una función que ejecute todo este flujo de trabajo:

```python
def suavizado_imagen(imagen_corrupta, delta):
    '''Suavizado de imagen

    Esta funcion toma la imagen especificada en la primer variable por su ruta, y
    le aplica un suavizado en proporcion al flotante pasado a la segunda variable.

    Ejemplo
    -------
    >>> suavizado_imagen("ruta/de/la/imagen.png", 0.1)
    '''

    # Se importan funciones necesarias
    from matplotlib.pyplot import imshow, cm, figure
    from scipy.linalg import solve_sylvester
    from scipy.ndimage import imread
    from numpy import matrix, eye, array

    # Se define funcion auxiliar para las filas de la matriz D
    def fun(i, tot):
        '''Arreglo especial
        Esta funcion crea un arreglo de tamaño tot con un -1 en el elemento i y un
        1 en el elemento i+1, siendo los demas lugares del arreglo ceros:

         indice ->  0, 1, ..., i-1, i, i+1, i+2, ..., tot
        arreglo -> [0, 0, ...,   0, -1,  1,   0, ...,   0].

        Ejemplo
        -------
        >>> fun(3, 5)
        array([ 0,  0,  -1, 1,  0])
        '''

        # Se importan funciones necesarias
        from numpy import array

        # Se define el inicio del arreglo
        if i == 0:
            a = [-1]
            a.append(1)
        else:
            a = [0]

        # Se incluyen numeros restantes en el arreglo
        for t in range(tot - 1)[1:]:
            if i == t:
                a.append(-1)
                a.append(1)
            else:
                a.append(0)

        # Se convierte en arreglo de numpy el resultado
        return array(a)

    # Se importa la imagen a tratar y se obtiene sus dimensiones
    im_corrupta = imread(imagen_corrupta)
    n = im_corrupta.shape[0]
    m = im_corrupta.shape[1]

    # Se obtienen las matrices D1 y D2
    D1 = matrix(array([fun(i, n + 1) for i in range(n)]))
    D2 = matrix(array([fun(i, m + 1) for i in range(m)]))

    # Se obtiene la imagen suavizada al resolver la ecuacion de Lyapunov (o Sylvester)
    imagen_suavizada = solve_sylvester(eye(n) + delta*D1*D1.T,
                                       delta*D2*D2.T,
                                       im_corrupta)

    # Se dibuja la imagen suavizada
    f  = figure(figsize=(8,6))
    ax = imshow(imagen_suavizada, cmap=cm.gray, interpolation='none')

    # Se quitan bordes de la grafica
    ax.axes.get_xaxis().set_visible(False)
    ax.axes.get_yaxis().set_visible(False)

    # Se hacen transparentes las lineas de los bordes
    ax.axes.spines["right"].set_color("none")
    ax.axes.spines["left"].set_color("none")
    ax.axes.spines["top"].set_color("none")
    ax.axes.spines["bottom"].set_color("none")
```

```python
# Se prueba la funcion con un suavizado de 10
suavizado_imagen(corrupta, 10)
```

Y hemos obtenido el resultado deseado...

## La cereza del pastel

Contentos con nuestros resultados podriamos irnos a descansar, pero aun queda un truco mas. Ya que hemos obtenido una función que ejecuta todo nuestro código, podemos hacer que IPython la ejecute en linea al momento de darle un parametro diferente.

Para esto utilizaremos un Widget de IPython:

```python
# Se importan widgets de IPython para interactuar con la funcion
from IPython.html.widgets import interact, fixed
```

    :0: FutureWarning: IPython widgets are experimental and may change in the future.

Dada la función que obtuvimos, ahora solo tenemos que mandar llamar a la función:

```python
interact(funcion_con_codigo,
         parametro_fijo=fixed(param),
         parametro_a_variar=(inicio, fin))
```

```python
# Se llama a la funcion interactiva
interact(suavizado_imagen, imagen_corrupta=fixed(corrupta), delta=(0.0, 10.0))
```

Con lo que solo tenemos que mover el deslizador para cambiar ```delta``` y ver los resultados de estos cambios.

```python
# Se muestra la imagen correcta
im_correcta = imread(correcta)

f  = figure(figsize=(8,6))
ax = imshow(im_correcta, cmap=cm.gray, interpolation='none');

ax.axes.get_xaxis().set_visible(False)
ax.axes.get_yaxis().set_visible(False)

ax.axes.spines["right"].set_color("none")
ax.axes.spines["left"].set_color("none")
ax.axes.spines["top"].set_color("none")
ax.axes.spines["bottom"].set_color("none")
```

Espero te hayas divertido con esta larga explicación y al final sepas un truco mas.

Si deseas compartir este Notebook de IPython utiliza la siguiente dirección:

http://bit.ly/1CJNEBn

o bien el siguiente código QR:

```python
# Codigo para generar codigo :)
from qrcode import make
img = make("http://bit.ly/1CJNEBn")
img.save("codigos/suave.jpg")
```