[View in Colaboratory](https://colab.research.google.com/github/mforbes/wsu_physics_450/blob/master/Notebooks/Assignment_5_Solutions.ipynb)

# Assignment 5: Chapter 5

$$
  \newcommand{\abs}[1]{\lvert#1\rvert}
  \newcommand{\abs}[1]{\left\lvert#1\right\rvert}
  \newcommand{\ket}[1]{\lvert#1\rangle}
  \newcommand{\bra}[1]{\langle#1\rvert}
  \newcommand{\braket}[1]{\langle#1\rangle}
  \newcommand{\Ket}[1]{\left\lvert#1\right\rangle}
  \newcommand{\Bra}[1]{\left\langle#1\right\rvert}
  \newcommand{\Braket}[1]{\left\langle#1\right\rangle}
  \newcommand{\op}[1]{\hat{\boldsymbol{#1}}}
  \newcommand{\mat}[1]{\boldsymbol{#1}}
  \newcommand{\vect}[1]{\boldsymbol{#1}}
  \newcommand{\I}{\mathrm{i}}
  \newcommand{\d}{\mathrm{d}}
  \newcommand{\floor}[1]{\lfloor\!#1\!\rfloor}
  \newcommand{\Floor}[1]{\left\lfloor\!#1\!\right\rfloor}
  \DeclareMathOperator{\sinc}{sinc}
$$

>[Assignment 5: Chapter 5](#scrollTo=CcKxZpzGeXzo)

>[5.3.4*: Currents in a Plane Wave](#scrollTo=zxqzyStSXw6O)

>[5.4.2*: Scattering](#scrollTo=EkLBFWo-eXzq)

>>[(a) Delta Function](#scrollTo=P9j1tLaGdISZ)

>>>[Comments](#scrollTo=P9j1tLaGdISZ)

>>[(b) Square Well](#scrollTo=aksTUlIEdFnq)

>>>[Think](#scrollTo=6ii0cE6keXzs)

>>>[Strategy](#scrollTo=6ii0cE6keXzs)

>>>[Details](#scrollTo=6ii0cE6keXzs)

>>[Comments](#scrollTo=EZdB4BKWtUQe)

# 5.3.4*: Currents in a Plane Wave

The problem asks to demonstrate that a plane wave with constant energy has independent left- and right-flowing probability current.  Specifically:

$$
  \psi(x) = Ae^{\I p x/\hbar} + B e^{-\I p x/\hbar}.
$$

From Eq. (5.3.8) we have the probability density current:

\begin{equation}
  \vect{j} = \frac{\hbar}{2m\I}(
    \psi^*\vect{\nabla}\psi
    - \psi\vect{\nabla}\psi^* )
    =
    \frac{
      \braket{\psi|x}\braket{x|\op{P}|\psi}
      +
      \text{h.c}}{2m}.
    \tag{5.3.8}
\end{equation}

*(Note: in the alternative formulation, there is no integration over $x$: we have not inserted $\mat{1}$ here: this is the definition of the local current which depends on the position $x$.)*

First we simplify the expression a little, noting that the second term is just the hermitian conjugate of the first, and then noting that this will simply extract the real portion of the result with a factor of $1/2$:

$$
  \vect{j} = \frac{\hbar}{2m\I}\psi^*\vect{\nabla}\psi + \text{h.c.}
  = \Re\left(\frac{\hbar}{m\I}\psi^*\vect{\nabla}\psi\right)
  = \Im\left(\frac{\hbar}{m}\psi^*\vect{\nabla}\psi\right).
$$

I.e. let $z=a+b\I$ where $a$ and $b$ are real:

$$
  \frac{z - z^*}{2\I} = \frac{a + b\I - (a - b\I)}{2\I}
  = \frac{2b\I}{2\I} = b = \Re\frac{z}{\I} = \Im z.
$$

Inserting the state into (5.3.8) we have

$$
  \vect{j} = \frac{\hbar}{2m\I}
    \left(A^*e^{-\I p x/\hbar} + B^*e^{\I p x/\hbar}\right)
    \frac{\I p}{\hbar}
    \left(Ae^{\I p x/\hbar} - Be^{-\I p x/\hbar}\right)
    +
    \text{h.c}
  = \frac{p}{2m}
    \left(A^*e^{-\I p x/\hbar} + B^*e^{\I p x/\hbar}\right)
    \left(Ae^{\I p x/\hbar} - Be^{-\I p x/\hbar}\right)
    +
    \text{h.c}
  = \frac{p}{2m}
    \left(\abs{A}^2 + AB^*e^{\I p x/\hbar} - A^*Be^{-\I p x/\hbar} - \abs{B}^2\right)
    +
    \text{h.c}
 = \frac{\abs{A}^2 - \abs{B}^2}{m}p
   +\frac{p}{2m}
    \left(
      AB^*e^{\I p x/\hbar}
      - A^*Be^{-\I p x/\hbar}
      +
      A^*Be^{-\I p x/\hbar}
      - AB^*e^{\I p x/\hbar}
    \right)
   = \left(\abs{A}^2 - \abs{B}^2\right)\frac{p}{m}.
$$

Thus, we may simply consider the $Ae^{\I x p/\hbar}$ piece as containing the right-flowing current and $Be^{-\I x p/\hbar}$ as the left-flowing current.  The cross-terms cancel, so we can form linear superpositions of right- and left-flowing currents in our scattering problems and they will behave independently.

# 5.4.2*: Scattering

## (a) Delta Function

$$
  V(x) = V_0 a \delta(x).
$$

Everywhere except at $x=0$ the wavefunction must satisfy the free-particle Schrödinger equation, and so must be a sum of plane waves with energy $E = \hbar^2k^2/2m$.  Thus, we define the scattering wavefunctions as

$$
  \psi(x) = \begin{cases}
    \psi_L(x) = A e^{\I k x} + B e^{-\I k x} & x < 0\\
    \psi_R(x) = C e^{\I k x} + D e^{-\I k x} & 0 < x.
  \end{cases}
$$

The problem is solved by matching boundary conditions.  The wavefunction must be continuous, but the derivative can have a kink that is found by integrating the Schrödinger equation across the potential:

$$
  \psi_L(0) = \psi_R(0), \qquad
  \psi_R'(0) - \psi_L'(0) = \int_{-\epsilon}^{\epsilon}\frac{2mV_0 a \delta(x)}{-\hbar^2}\psi(x)\d{x}
  = \frac{-2mV_0 a }{\hbar^2}\\
  A+B = C+D, \qquad
  \I k(A-B -C+D) = \frac{-2mV_0 a }{\hbar^2}(A+B).
$$

The transmission and reflection coefficients can be obtained by setting $A=1$, $B=r$, $C=t$, and $D=0$.  This simple substitution works because $k$ is the same on both left and right sides (otherwise one would need factors of $k_1/k_2$ to match the currents) and the last condition says there is no current flowing in from the right.  Solving, we find:

$$
  1+r = t, \qquad
  \I k(1-r - t) = -2r\I k = \frac{-2mV_0 a }{\hbar^2}(1+r),\\
  \frac{r}{1+r} = \frac{1}{r^{-1}+1} =\frac{mV_0 a }{\I\hbar^2 k} = \frac{1}{\I\beta},\\
  r = \frac{1}{\I\beta - 1}, \qquad
  \beta = \frac{\hbar^2 k^2}{m V_0}\frac{1}{ka}
$$

Hence:

$$
  R = \abs{r}^2 = \frac{1}{1+\beta^2}, \qquad
  T = 1 - R = \frac{1}{1+\beta^2}.
$$

### Comments

Note that the reflection coefficient $R \rightarrow 1$ goes to one when $\beta^2$ becomes large – i.e. when $k^2 \propto E$ is small.  Thus, low energy particles are always reflected from sharp potentials.  Second, notice that the reflection coefficient depends on $(aV_0)^2$, so reflection occurs *even if the potential is attractive*!  This is a purely quantum effect – classical particles will pass through an attractive barrier.

## (b) Square Well

We now consider a finite square-well potential with width $2a$:

$$
  V(x) = \begin{cases}
    V_0 & \abs{x}\leq a\\
    0 & \abs{x} \geq a.
  \end{cases}.
$$

### Think

In the limit $a\rightarrow 0$ holding $2aV_0$ constant, we should reroduce the results of part (a) (with $a->2a$).

### Strategy

In each of the three regions of space, the potential is flat, so we can know the form of the wavefunction since it must satisfy the Schrödinger equation (see Exercise (5.1.2) from the last assignment).  A brute force solution would be to set:

\begin{align}
  x &< -a, &
  \psi_{I}(x) &= A e^{\I k x} + B e^{-\I k x}, &
  \hbar k &= \sqrt{2mE}, \tag{I}\\
  -a \leq &x < a, &
  \psi_{II}(x) &= \alpha e^{-\kappa x} + \beta e^{\kappa x}, &
  \hbar \kappa &= \sqrt{-2m(E-V_0)}, \tag{II}\\
  a &< x, &
  \psi_{III}(x) &= C e^{\I k x} + D e^{-\I k x}, &
  \hbar k &= \sqrt{2mE}. \tag{III}
\end{align}

The strategy will now be to make sure that the wavefunction and its derivative are continuous everywhere - meaning at the boundaries $x = \pm a$.  This should give us some equations we can use to solve for the energy eigenstates.

\begin{align}
  \psi_{I}(-a) &= \psi_{II}(-a), & \psi_{II}(a) &= \psi_{III}(a), \\
  \psi_{I}'(-a) &= \psi_{II}'(-a), & \psi_{II}'(a) &= \psi_{III}'(a).
\end{align}

As before, since $V(x>\abs{a})=0$, we can solve for the reflection and transmission coefficients by setting $A=1$, $B=r$, $C=t$, and $D=0$.
We have 4 unknowns $r$, $t$, $\alpha$,  and $\beta$, and four equations. Thus, we are ready to proceed.

### Details

Plugging in our ansatz and being careful with the signs, we obtain the following equations:

\begin{align}
  Ae^{-\I ka} + Be^{\I ka}
  &= \alpha e^{\kappa a} + \beta e^{-\kappa a},\\
  \frac{\I k}{\kappa}(Ae^{-\I ka} - Be^{\I ka})
  &= -\alpha e^{\kappa a} + \beta e^{-\kappa a},\\
  Ce^{\I ka} + De^{-\I ka}
  &= \alpha e^{-\kappa a} + \beta e^{\kappa a},\\
  \frac{\I k}{\kappa}(Ce^{\I ka} - De^{-\I ka})
  &= -\alpha e^{-\kappa a} + \beta e^{\kappa a}.
\end{align}

Solving for $\alpha$ and $\beta$ in each set of equations, then eliminating gives:

\begin{align}
  \alpha &= \frac{Ae^{-\I ka} + Be^{\I ka} - \frac{\I k}{\kappa}(Ae^{-\I ka} - Be^{\I ka})}{2e^{\kappa a}}
  = \frac{Ce^{\I ka} + De^{-\I ka} - \frac{\I k}{\kappa}(Ce^{\I ka} - De^{-\I ka})}{2e^{-\kappa a}},\\
  \beta &= \frac{Ae^{-\I ka} + Be^{\I ka} + \frac{\I k}{\kappa}(Ae^{-\I ka} - Be^{\I ka})}{2e^{-\kappa a}}
  = \frac{Ce^{\I ka} + De^{-\I ka} + \frac{\I k}{\kappa}(Ce^{\I ka} - De^{-\I ka})}{2e^{\kappa a}}.
\end{align}

Setting $A=1$, $B=r$, $C=t$, and $D=0$, and solving, we have:

\begin{align}
  \frac{\left(1 - \frac{\I k}{\kappa}\right)e^{-\I ka}
  + re^{\I ka}\left(1 + \frac{\I k}{\kappa}\right)}{e^{2\kappa a}}
  = te^{\I ka}\left(1 - \frac{\I k}{\kappa}\right),\\
  \frac{e^{-\I ka}\left(1 + \frac{\I k}{\kappa}\right)
  + re^{\I ka}\left(1 - \frac{\I k}{\kappa}\right)}{e^{-2\kappa a}}
  = te^{\I ka}\left(1 + \frac{\I k}{\kappa}\right).
\end{align}

To simplify, let $Q=1 + \frac{\I k}{\kappa}$ and $\tilde{Q}=1 - \frac{\I k}{\kappa}$.  *Note: Although $\tilde{Q}=Q^*$ for the problem as stated in the book, keeping these independent will allow us to extend our result to the case $E>V_0$ when this is not true.*:

$$
  te^{\I ka}
  = \frac{\tilde{Q}e^{-\I ka} + re^{\I ka}Q}{\tilde{Q}e^{2\kappa a}}
  = \frac{e^{-\I ka}Q + re^{\I ka}\tilde{Q}}{Q e^{-2\kappa a}},\\
  r = \frac{e^{-2\I ka}\left(e^{2\kappa a}
     - e^{-2\kappa a}\right)}{\left(
    \frac{Q}{\tilde{Q}e^{2\kappa a}}
    -
    \frac{\tilde{Q}}{Q e^{-2\kappa a}}
  \right)}
  = \frac{2\sinh(2\kappa a)}{e^{2\I ka}\left(
    \frac{Q}{\tilde{Q}e^{2\kappa a}}
    -
    \frac{\tilde{Q}}{Q e^{-2\kappa a}}
  \right)},\\
  R = \abs{r}^2.
$$

## Comments

A key to working though this type of algebra is to keep everything as symmetric as possible, and then to look for common sub-expressions like $Q$ which you can use to express your answer in a simple form.

As we mentioned before, we expect that in the limit of small $a$, the result should approach that of a $V(x)= 2aV_0\delta(x)$ potential:

$$
  R_{\delta} = \frac{1}{1+\beta^2}, \qquad
  \beta = \frac{\hbar^2 k^2}{2m V_0 ka}
            = \frac{E}{V_0}\frac{1}{ka}.
$$

Our problem has as parameters, $E$, $a$, $V_0$, $\hbar$, and $m$. From these, we can form two dimensionless parammeters.  The combination $\beta$ above characterizes the wavelength $k$ of the incoming wave and is the only parameter governing the reflection from the delta-function potential. The other parameter is:

$$
  \nu = \frac{kaV_0}{\hbar^2/2ma^2} = \frac{2mka^3V_0}{\hbar^2}.
$$

This determines how small the width of the potential is.  For small $\nu$, we expect the potential to behave as a delta-function.

```
%pylab inline --no-import-all
kaV0 = m = hbar = 1
nu = 1.0

def get_R(E, nu=nu):
  k = np.sqrt(2*m*E)/hbar
  a = np.sqrt(abs(nu)*hbar**2/kaV0/2/m)
  V_0 = np.sign(nu)*kaV0/k/a
  kappa = np.sqrt(-2*m*(E-V_0)+0j)/hbar
  Q = 1 + 1j*k/kappa
  Qt = 1 - 1j*k/kappa
  r = (2*np.sinh(2*kappa*a)
       / np.exp(2j*k*a)
       / (Q/Qt/np.exp(2*kappa*a)
          - Qt/Q*np.exp(2*kappa*a)))
  return abs(r)**2

@np.vectorize
def get_R1(E, nu=nu):
  """This is a check from another technique..."""
  k = np.sqrt(2*m*E)/hbar
  a = np.sqrt(abs(nu)*hbar**2/kaV0/2/m)
  V_0 = np.sign(nu)*kaV0/k/a
  k1 = np.sqrt(2*m*(E-V_0)+0j)/hbar
  Q = 1. + k/k1
  Qt = 1. - k/k1
  T_step = np.array([[Q, Qt], [Qt, Q]])/2
  D = np.diag([np.exp(1j*2*a*k1), np.exp(-1j*2*a*k1)])

  T = np.linalg.solve(T_step, D.dot(T_step))
  S = np.array([[np.linalg.det(T), T[0,1]],
                [-T[1,0], 1]])/T[1, 1]
  return(abs(S[0,1])**2)

def get_R_delta(E):
  """Return the reflection coefficient of the
  corresponding delta-function potential."""
  k = np.sqrt(2*m*E)/hbar
  beta = E / kaV0
  return 1/(1+beta**2)

plt.figure(figsize=(10, 5))
E = np.linspace(1e-6, 3, 1000)
assert np.allclose(get_R(E), get_R1(E))
plt.plot(E, get_R_delta(E), ':', label=r'$\delta(x)$')
for nu in [0.01, 0.1, 0.5, 10, 100]:
  plt.plot(E, get_R(E, nu=nu), '-',
           label=r"$\nu={}$".format(nu))
#for nu in [-0.1, -0.5, -1]:
#  plt.plot(E, get_R(E, nu=nu), '--',
#           label=r"$\nu={}$".format(nu))
plt.legend()
plt.xlabel('$E/|V_0|ka$')
plt.ylabel('$R$')
```

We see that our analysis is correct: as $\nu$ gets small, our finite barrier scattering approaches the delta-function results.  Notice that there are some interesting features with the finite barrier where for special energies, the reflection coefficient almost vanishes.  What is special about these energies?  These occur where $E > V_0$ so that $\kappa = \I k_1$ and then the numerator of $R \propto \sinh(2\kappa a) \propto \sin(2ka)$, so these occur where $ka = \pi n$.

```
%pylab inline --no-import-all
V0 = m = hbar = 1
nu = 1.0

def get_R(E, nu=nu):
  k = np.sqrt(2*m*E)/hbar
  a = (hbar**2*abs(nu)/2/m/k/V0)**(1./3.)
  kappa = np.sqrt(-2*m*(E-V0)+0j)/hbar
  Q = 1 + 1j*k/kappa
  Qt = 1 - 1j*k/kappa
  r = (2*np.sinh(2*kappa*a)
       / np.exp(2j*k*a)
       / (Q/Qt/np.exp(2*kappa*a)
          - Qt/Q*np.exp(2*kappa*a)))
  return abs(r)**2

def get_R_delta(E, ka):
  """Return the reflection coefficient of the
  corresponding delta-function potential."""
  k = np.sqrt(2*m*E)/hbar
  beta = E / ka / V0
  return 1/(1+beta**2)

plt.figure(figsize=(10, 5))
E = np.linspace(1e-6, 10, 10000)
for nu in [0.01, 0.1, 0.5, 10, 100]:
  k = np.sqrt(2*m*E)/hbar
  a = (hbar**2*abs(nu)/2/m/k/V0)**(1./3.)
  x = E/V0
  l, = plt.plot(x, get_R(E, nu=nu), '-',
                label=r"$\nu={}$".format(nu))
  plt.plot(x, get_R_delta(E, ka=k*a), ':', c=l.get_c())
#for nu in [-0.1, -0.5, -1]:
#  plt.plot(E, get_R(E, nu=nu), '--',
#           label=r"$\nu={}$".format(nu))
plt.legend()
plt.xlabel('$E/|V_0|$')
plt.ylabel('$R$')
plt.xlim(0,3)
```

This is the same plot with a different scaling on the abscissa to show where $E=V_0$ (at 1).  On this plot, the delta-function is not constant.


```
# default_exp bcml_model
```

# bcml_model

> Core class representing a binary classification (BC) machine learning (ML) model

```
# hide
from nbdev.showdoc import *
```

```
# export
import numpy as np
import sklearn.metrics as skm
import scipy.interpolate
import scipy.optimize
import pandas as pd
import joblib
```

```
# export
class bcml_model:
    """
    Represents a machine learning (ML) binary classification (BC) model.
    """

    def __init__(self, model):
        self.model = model

    def fit(self, preds, labels):
        r"""
        Fits `model` to data.

        `preds` should be ? $\times$ $m$ for model with $m$ features. `labels` should
        be ? $\times$ 1 and take values in $\{0,1\}$.
        """
        self.model.fit(preds, labels)

    def predict_proba(self, preds):
        r"""
        Predicts signal probability for each element of a dataset ($? \times m$ `numpy` array).

        Returns `numpy` array of with values in $[0,1]$ giving predicted signal probabilities for
        each data point.
        """
        try:
            return self.model.predict_proba(preds)[:,1]
        except:
            print("self.model doesn't have a predict_proba function")

    def predict(self, preds, threshold=None):
        r"""
        Predicts signal ($1$) or background ($0$) for each element of a dataset ($? \times m$ `numpy` array).

        Returns `numpy` array of length with values in $\{0,1\}$ giving predicted classifications.

        Uses the `predict` method built into `scikit-learn` models.
        """
        if threshold is not None:
            probs = self.model.predict_proba(preds)[:,1]
            return np.where(probs > threshold, np.ones_like(probs), np.zeros_like(probs))
        else:
            try:
                return self.model.predict(preds)
            except:
                print("self.model doesn't have a predict function")

    def predict_hist(self, preds, labels, num_bins=100, sepbg=False, sig_norm=1, bg_norm=1, dataframe=False):
        r"""
        Constructs a histogram of predicted signal probabilities for signal and background constituents of
        a dataset ($? \times M$ `numpy` array).

        If `sepbg` is `False` (the default), labels are assumed to take values in $\{0,1\}$. Backgrounds are treated in combination:
        a list of $3$ $?_i \times m$ `numpy` arrays are returned, containing bin edges (partitioning $[0,1]$),
        signal bin contents, and background bin contents.

        If `sepbg` is `True`, labels are assumed to take values in $\{-n,\dots,-1,1\}$ (if there are $n$ backgrounds) while
        `bg_norm` should be a list of length $n$. Backgrounds are then differentiated: a list of $2 + n$ `numpy` arrays of shape
        $?_i \times m$ are returned, containing bin edges (partitioning $[0,1]$), signal bin contents, and
        $n$ background bin contents.
        """
        predictions = self.predict_proba(preds)
        labels = np.array(labels)
        sig_bins, bin_edges = np.histogram(predictions[labels==1], bins=num_bins, density=True)
        sig_bins *= sig_norm
        if sepbg:
            bg_norms = bg_norm
            bg_binss = [
                bg_norm * np.histogram(predictions[labels==-(i+1)], bins=num_bins, density=True)[0]
                for i, bg_norm in enumerate(bg_norms)]
            if dataframe:
                return pd.DataFrame(data=[bin_edges, sig_bins] + bg_binss, columns=['Bin Edges', 'Signal'] + ['Background {}'.format(i) for i in range(1, self.num_bgs+1)])
            else:
                return [bin_edges, sig_bins] + bg_binss
        else:
            bg_bins = np.histogram(predictions[labels!=1], bins=num_bins, density=True)[0]
            if dataframe:
                return pd.DataFrame(data=[bin_edges, sig_bins, bg_bins], columns=['Bin Edges', 'Signal', 'Background'])
            else:
                return [bin_edges, sig_bins, bg_bins]

    def feature_importance(self):
        """
        Returns the importance of the $M$ features used to train `self.model`.
        """
        try:
            return self.model.feature_importances_
        except:
            try:
                return self.model[-1].feature_importances_
            except:
                print("It looks like self.model doesn't have an attribute 'feature_importances_'")

    def sorted_feature_importance(self, features):
        r"""
        Returns list of features sorted by importance.

        Given arguments `features` and `importances`, lists of length $M$, returns list of size $M \times 2$ where
        the first column gives features and the second their associated importances, sorted by importance.
        """
        importances = self.feature_importance()
        ranked_indices = np.argsort(-np.abs(importances))
        return [[features[i], importances[i]] for i in ranked_indices]

    def accuracy(self, preds, labels, threshold=None):
        r"""
        Computes model accuracy on a dataset ($? x m$ predictors, length $?$ labels).

        Returns value in $[0,1]$ giving model accuracy on the provided predictors and labels.
        """
        predictions = self.predict(preds=preds, threshold=threshold)
        return len(preds) - np.sum(np.abs(predictions - labels)) / len(preds)

    def conf_matrix(self, labels, predictions=None, preds=None):
        r"""
        Computes the confusion matrix of the trained model on a dataset ($? x M$ predictors, length $?$ labels).

        Returns $2 \times 2$ confusion matrix using `sklearn.metrics.confusion_matrix`.

        If predictors `preds` aren't provided, `self.test_preds` is used.
        If `labels` aren't provided, `self.test_labels` is used.
        """
        if predictions is not None:
            return skm.confusion_matrix(labels, predictions, labels=[0,1])
        elif preds is not None:
            return skm.confusion_matrix(labels, self.predict(preds), labels=[0,1])
        else:
            raise ValueError('Either predictions or preds must be passed.')

    def tpr_cm(self, conf_matrix):
        """
        Computes the true positive rate (tpr; correctly identified signal/total signal)
        of a trained model given a confusion matrix.

        Returns value in $[0,1]$.
        """
        return conf_matrix[1,1]/np.sum(conf_matrix[1])

    def fpr_cm(self, conf_matrix):
        """
        Computes the false positive rate (fpr; misidentified background/total background)
        of a trained model given a confusion matrix.

        Returns value in $[0,1]$.
        """
        return conf_matrix[0,1]/np.sum(conf_matrix[0])

    def tpr(self, labels, predictions=None, preds=None):
        r"""
        Computes the true positive rate (tpr; correctly identified signal/total signal)
        of a trained model given predictions and labels (both `numpy` array of length $?$ with values in $\{0,1\}$)

        Returns value in $[0,1]$.
        """
        return self.tpr_cm(self.conf_matrix(labels, predictions=predictions, preds=preds))

    def fpr(self, labels, predictions=None, preds=None):
        r"""
        Computes the false positive rate (fpr; misidentified background/total background)
        of a trained model given predictions and labels (both `numpy` array of length $?$ with values in $\{0,1\}$)

        Returns value in $[0,1]$.
        """
        return self.fpr_cm(self.conf_matrix(labels, predictions=predictions, preds=preds))

    def significance(self, signal, background, tpr, fpr, sepbg=False):
        r"""
        Computes signal significance of a trained model given signal and background yield.

        Returns a positive real number computed by
        $$\frac{S \cdot TPR}{\sqrt{S \cdot TPR + B \cdot FPR}}$$
        which corresponds to signal significance after selecting only datapoints the model identifies as signal.

        If `sepbg` is `False`, `background` should be a single real number and is multiplied by `fpr`. If `sepbg` is `True`,
        `background` should be a list of length `self.num_bgs` where the $i$th element contains background yield of the $i$th
        background type. `fpr`, if passed, is then also a list of length `self.num_bgs` giving false positive rates for each
        of the background types.
        """
        if sepbg:
            backgrounds = background
            fprs = fpr
            fprXbackground = np.sum(np.multiply(fprs, backgrounds), axis=-1)
            return (signal * tpr) / np.sqrt(signal * tpr + fprXbackground + 1e-10)
        else:
            return (signal * tpr) / np.sqrt(signal * tpr + background * fpr + 1e-10)

    def newvar2thresh(self, newvar):
        r"""
        Helper method for `bcml.max_allowable_threshold()`, `bcml.get_tprs_fprs()`, and `bcml.best_threshold()`,
        performing change of variables from `newvar` to `threshold`

        In particular, threshold $= 1 - 10^{\text{newvar}}$
        """
        return 1 - np.power(10, newvar)

    def thresh2newvar(self, thresh):
        r"""
        Helper method for `bcml.max_allowable_threshold()`, `bcml.get_tprs_fprs()`, and `bcml.best_threshold()`,
        performing change of variables from `threshold` to `newvar`

        In particular, newvar $= \log_{10}(1 - \text{threhold})$
        """
        return np.log10(1 - thresh)

    def max_allowable_threshold(self, preds, labels, sigYield):
        """
        Returns the highest threshold such that only labelling elements of `self.test_pred` with predicted
        probabilities higher than that threshold as signal still yields 25 signal.

        To achieve a discovery potential of $5\sigma$, even in the best case scenario ($TPR = 1, FPR = 0$) we still
        require $5^2 = 25$ signal events, hence we cannot chose a threshold so high that we do not keep at least
        25 signal events.
        """

        sig_indx = np.where(np.array(labels)==1)[0]
        preds = np.array(preds)[sig_indx]
        probs = self.predict_proba(preds)
        num_predicts = np.array(
            [np.sum(np.where(probs > self.newvar2thresh(newvar),np.ones_like(probs), np.zeros_like(probs)))
             for newvar in newvars])
        num_sig_yields = (num_predicts / len(preds)) * sigYield
        return [newvars, num_sig_yields]
#         f = scipy.interpolate.interp1d(num_sig_yield, newvars, kind='cubic')
#         return self.newvar2thresh(f(25))

    def get_tprs_fprs(self, preds, labels, sepbg=False):
        """
        Produces (true positive rate, false positive rate) pairs for various thresholds
        for the trained model on data sets.

        If `sepbg` is `True`, labels should take values in $\{-n,\dots,-1,1\}$. Background is combined and a list of length $4$
        is returned containing a list of $L$ sampled newvars (a convenient change of variable to approach arbitrarily close to 1:
        related to thresholds by  `bcml_model.newvar2thresh()`), an $L$-list of tprs associated to those thresholds, an $L$-list of fprs
        related to those thresholds, and an $L$-list of length $?$ `numpy` arrays giving the predicted signal probabilities
        for the given data set.

        If `sepbg` is `Frue`, labels should take values in $\{0,1\}$. Background is split and a list of length $4$ `self.num_bgs`
        is returned containing a  list of $L$ sampled newvars, an $L$-list of tprs associated to those thresholds, an $L$-list of lists of length
        $n$ (number of backgrounds) containing fprs for each background type for each threshold, and an $L$-list of length $?$
        """
        # setting up variables
        min_newvar, max_newvar = [-10, 0]
        newvars = np.concatenate((np.linspace(min_newvar, -2, 10, endpoint=False), np.linspace(-2, max_newvar, 15, endpoint=False)))

        # computing tprs, fprs
        if sepbg:
            num_bgs = len(np.unique(labels)) - 1
            labelTypes = [1] + [-(i+1) for i in range(num_bgs)]
            labelsIndices = [np.where(np.array(labels)==i)[0] for i in labelTypes]
#             print(np.array(labelsIndices).shape)
            predss = [preds[indices] for indices in labelsIndices]
#             print(predss[0][:10])
            probss = [self.predict_proba(preds) for preds in predss]
#             print(probsss[0][:10])
            predictionsss = np.array(
                [[np.where(probs > self.newvar2thresh(newvar),
                          np.ones_like(probs), np.zeros_like(probs)) for probs in probss] for newvar in newvars])
            sig_conf_matrices = [
                self.conf_matrix(labels=np.ones_like(predictionss[0]), predictions=predictionss[0]) for predictionss in predictionsss]
            bg_conf_matricess = [
                [self.conf_matrix(labels=np.zeros_like(predictions), predictions=predictions) for i, predictions in enumerate(predictionss[1:])] for predictionss in predictionsss]
            tprs = np.array([self.tpr_cm(conf_matrix) for conf_matrix in sig_conf_matrices])
            fprss = np.array([[self.fpr_cm(conf_matrix) for conf_matrix in conf_matrices] for conf_matrices in bg_conf_matricess])
            sums = tprs + np.sum(fprss, axis=1)
            cutoff = len(sums) - np.argmax(np.flip(sums)==0) + 1 if 0 in sums else 0
            return [newvars[cutoff:], tprs[cutoff:], fprss[cutoff:], probss]
        else:
            probs = self.predict_proba(preds)
            predictionss = np.array(
                [np.where(probs > self.newvar2thresh(newvar),
                          np.ones_like(probs), np.zeros_like(probs)) for newvar in newvars])
            conf_matrices = [self.conf_matrix(labels=labels, predictions=predictions) for predictions in predictionss]
            tprs = np.array([self.tpr_cm(conf_matrix) for conf_matrix in conf_matrices])
            fprs = np.array([self.fpr_cm(conf_matrix) for conf_matrix in conf_matrices])
            sums = tprs + fprs
            cutoff = len(sums) - np.argmax(np.flip(sums)==0) + 1 if 0 in sums else 0
            return [newvars[cutoff:], tprs[cutoff:], fprs[cutoff:], probs]

    def best_threshold(self, signal, background, preds, labels, sepbg=False):
        """
        Optimizes the threshold on a given data set ($? x M$ predictors, length $?$ labels).
        """
        newvars, tprs, fprs, probs = self.get_tprs_fprs(preds, labels, sepbg)
        significances = -self.significance(signal, background, tprs, fprs, sepbg=sepbg)

        # interpolating significance as a function of threshold, then maximizing
        max_sig = np.amin(significances)
        significances_list = list(significances)
        i = significances_list.index(max_sig)
        min_i, max_i = [max(0,i-4),min(len(significances),i+5)]
        f = scipy.interpolate.interp1d(newvars[min_i:max_i], significances[min_i:max_i], kind='cubic')
        res = scipy.optimize.minimize(f, [0.5 * (newvars[min_i] + newvars[max_i-1])], bounds=[(newvars[min_i] + 1e-1, newvars[max_i-1] - 1e-1)])

        # computing significance, tpr, fpr for optimized threshold
        best_threshold = self.newvar2thresh(res.x[0])
        if sepbg:
            probss = probs
            fprss = fprs
            best_predictss = [np.where(probs > best_threshold, np.ones_like(probs), np.zeros_like(probs)) for probs in probss]
            sig_conf_matrix = self.conf_matrix(labels=np.ones_like(best_predictss[0]), predictions=best_predictss[0])
            bg_conf_matrices = [self.conf_matrix(labels=np.zeros_like(best_predicts), predictions=best_predicts) for i, best_predicts in enumerate(best_predictss[1:])]
            tpr = self.tpr_cm(sig_conf_matrix)
            fprs = [self.fpr_cm(conf_matrix) for conf_matrix in bg_conf_matrices]
            best_sig = self.significance(signal, background, tpr, fprs, sepbg=sepbg)
            return [best_threshold, best_sig, tpr, fprs, tprs, fprss]
        else:
            best_predicts = np.where(probs > best_threshold, np.ones_like(probs), np.zeros_like(probs))
            conf_matrix = self.conf_matrix(labels=labels, predictions=best_predicts)
            tpr = self.tpr_cm(conf_matrix)
            fpr = self.fpr_cm(conf_matrix)
            best_sig = self.significance(signal, background, tpr, fpr, sepbg=sepbg)
            return [best_threshold, best_sig, tpr, fpr, tprs, fprs]

    def req_sig_cs(self, lumi, bg_cs, tpr, fpr, sig=5, sepbg=False):
        """
        Given a luminosity (in fb$^{-1}$), a background cross section (in pb), a true positive rate, a false positive rate,
        and a signal significance, computes the signal cross section required for the signal significance to be achieved.

        If `sepbg` is False, background is combined and a single FPR is used; if `sepbg` is True, it is assumed that
        `bg_cs`, `fpr` are each lists of length $n$ (number of backgrounds) and their vector dot product is used for background yield.
        """
        conv = 10**15 / 10**12
        if sepbg:
            bg = np.sum(np.multiply(bg_cs, fpr))
            coef = [-tpr**2 * lumi * conv**2, sig**2 * tpr * conv, sig**2 * bg * conv]
        else:
            coef = [-tpr**2 * lumi * conv**2, sig**2 * tpr * conv, sig**2 * fpr * bg_cs * conv]
        return np.amax(np.roots(coef))

    def save_model(self, filename):
        """
        Saves the model to `filename.joblib`
        """
        joblib.dump(self.model, filename + '.joblib')
```

### Getting up and running

#### Creating a `bcml_model` instance

To create a new `bcml_model` instance, all you need to pass is a `model`, a machine learning binary classification model. This library was designed to work with `scikit-learn` classifiers, such as `sklearn.linear_model.LogisticRegression`, `sklearn.ensemble.RandomForestClassifier`, or `sklearn.ensemble.GradientBoostingClassifier`.

### Basic Functionality

```
show_doc(bcml_model.fit)
```

bcml_model.fit[source]

> bcml_model.fit(**`preds`**, **`labels`**)

Fits `model` to data.

`preds` should be ? $\times$ $m$ for model with $m$ features. `labels` should
be ? $\times$ 1 and take values in $\{0,1\}$.

```
show_doc(bcml_model.predict_proba)
```

bcml_model.predict_proba[source]

> bcml_model.predict_proba(**`preds`**)

Predicts signal probability for each element of a dataset ($? \times m$ `numpy` array).

Returns `numpy` array of with values in $[0,1]$ giving predicted signal probabilities for
each data point.

```
show_doc(bcml_model.predict)
```

bcml_model.predict[source]

> bcml_model.predict(**`preds`**, **`threshold`**=*`None`*)

Predicts signal ($1$) or background ($0$) for each element of a dataset ($? \times m$ `numpy` array).

Returns `numpy` array of length with values in $\{0,1\}$ giving predicted classifications.

Uses the `predict` method built into `scikit-learn` models.

```
show_doc(bcml_model.predict_hist)
```

bcml_model.predict_hist[source]

> bcml_model.predict_hist(**`preds`**, **`labels`**, **`num_bins`**=*`100`*, **`sepbg`**=*`False`*, **`sig_norm`**=*`1`*, **`bg_norm`**=*`1`*, **`dataframe`**=*`False`*)

Constructs a histogram of predicted signal probabilities for signal and background constituents of
a dataset ($? \times M$ `numpy` array).

If `sepbg` is `False` (the default), labels are assumed to take values in $\{0,1\}$. Backgrounds are treated in combination:
a list of $3$ $?_i \times m$ `numpy` arrays are returned, containing bin edges (partitioning $[0,1]$),
signal bin contents, and background bin contents.

If `sepbg` is `True`, labels are assumed to take values in $\{-n,\dots,-1,1\}$ (if there are $n$ backgrounds) while
`bg_norm` should be a list of length $n$. Backgrounds are then differentiated: a list of $2 + n$ `numpy` arrays of shape
$?_i \times m$ are returned, containing bin edges (partitioning $[0,1]$), signal bin contents, and
`self.num_bgs` background bin contents.

```
show_doc(bcml_model.feature_importance)
```

bcml_model.feature_importance[source]

> bcml_model.feature_importance()

Returns the importance of the $M$ features used to train `self.model`.

```
show_doc(bcml_model.sorted_feature_importance)
```

bcml_model.sorted_feature_importance[source]

> bcml_model.sorted_feature_importance(**`features`**)

Returns list of features sorted by importance.

Given arguments `features` and `importances`, lists of length $M$, returns list of size $M \times 2$ where
the first column gives features and the second their associated importances, sorted by importance.

```
show_doc(bcml_model.accuracy)
```

bcml_model.accuracy[source]

> bcml_model.accuracy(**`preds`**, **`labels`**, **`threshold`**=*`None`*)

Computes model accuracy on a dataset ($? x m$ predictors, length $?$ labels).

Returns value in $[0,1]$ giving model accuracy on the provided predictors and labels.

```
show_doc(bcml_model.conf_matrix)
```

bcml_model.conf_matrix[source]

> bcml_model.conf_matrix(**`labels`**, **`predictions`**=*`None`*, **`preds`**=*`None`*)

Computes the confusion matrix of the trained model on a dataset ($? x M$ predictors, length $?$ labels).

Returns $2 \times 2$ confusion matrix using `sklearn.metrics.confusion_matrix`.

If predictors `preds` aren't provided, `self.test_preds` is used.
If `labels` aren't provided, `self.test_labels` is used.

```
show_doc(bcml_model.tpr_cm)
```

bcml_model.tpr_cm[source]

> bcml_model.tpr_cm(**`conf_matrix`**)

Computes the true positive rate (tpr; correctly identified signal/total signal)
of a trained model given a confusion matrix.

Returns value in $[0,1]$.

```
show_doc(bcml_model.fpr_cm)
```

bcml_model.fpr_cm[source]

> bcml_model.fpr_cm(**`conf_matrix`**)

Computes the false positive rate (fpr; misidentified background/total background)
of a trained model given a confusion matrix.

Returns value in $[0,1]$.

```
show_doc(bcml_model.tpr)
```

bcml_model.tpr[source]

> bcml_model.tpr(**`predictions`**, **`labels`**)

Computes the true positive rate (tpr; correctly identified signal/total signal)
of a trained model given predictions and labels (both `numpy` array of length $?$ with values in $\{0,1\}$)

Returns value in $[0,1]$.

```
show_doc(bcml_model.fpr)
```

bcml_model.fpr[source]

> bcml_model.fpr(**`predictions`**=*`None`*, **`labels`**=*`None`*)

Computes the false positive rate (fpr; misidentified background/total background)
of a trained model given predictions and labels (both `numpy` array of length $?$ with values in $\{0,1\}$)

Returns value in $[0,1]$.

### Phenomenology

```
show_doc(bcml_model.significance)
```

bcml_model.significance[source]

> bcml_model.significance(**`signal`**, **`background`**, **`tpr`**, **`fpr`**, **`sepbg`**=*`False`*)

Computes signal significance of a trained model given signal and background yield.

Returns a positive real number computed by
$$\frac{S \cdot TPR}{\sqrt{S \cdot TPR + B \cdot FPR}}$$
which corresponds to signal significance after selecting only datapoints the model identifies as signal.

If `sepbg` is `False`, `background` should be a single real number and is multiplied by `fpr`. If `sepbg` is `True`,
`background` should be a list of length `self.num_bgs` where the $i$th element contains background yield of the $i$th
background type. `fpr`, if passed, is then also a list of length `self.num_bgs` giving false positive rates for each
of the background types.

```
show_doc(bcml_model.newvar2thresh)
```

bcml_model.newvar2thresh[source]

> bcml_model.newvar2thresh(**`newvar`**)

Helper method for `bcml.max_allowable_threshold()`, `bcml.get_tprs_fprs()`, and `bcml.best_threshold()`,
performing change of variables from `newvar` to `threshold`

In particular, threshold $= 1 - 10^{\text{newvar}}$

```
show_doc(bcml_model.thresh2newvar)
```

bcml_model.thresh2newvar[source]

> bcml_model.thresh2newvar(**`thresh`**)

Helper method for `bcml.max_allowable_threshold()`, `bcml.get_tprs_fprs()`, and `bcml.best_threshold()`,
performing change of variables from `threshold` to `newvar`

In particular, newvar $= \log_{10}(1 - \text{threhold})$

```
show_doc(bcml_model.max_allowable_threshold)
```

bcml_model.max_allowable_threshold[source]

> bcml_model.max_allowable_threshold(**`preds`**, **`labels`**)

Returns the highest threshold such that only labelling elements of `self.test_pred` with predicted
probabilities higher than that threshold as signal still yields 25 signal.

To achieve a discovery potential of $5\sigma$, even in the best case scenario ($TPR = 1, FPR = 0$) we still
require $5^2 = 25$ signal events, hence we cannot chose a threshold so high that we do not keep at least
25 signal events.

NOTE: this function frequently returns an extrapolation error as $25$ frequently falls outside of the
function interpolation range (i.e., to achieve less than 25 signal events requires coming extremely close to $1$).

```
show_doc(bcml_model.get_tprs_fprs)
```

bcml_model.get_tprs_fprs[source]

> bcml_model.get_tprs_fprs(**`preds`**, **`labels`**, **`sepbg`**=*`False`*)

Produces (true positive rate, false positive rate) pairs for various thresholds
for the trained model on data sets.

If `sepbg` is `True`, labels should take values in $\{-n,\dots,-1,1\}$. Background is combined and a list of length $4$
is returned containing a list of $L$ sampled newvars (a convenient change of variable to approach arbitrarily close to 1:
related to thresholds by  [`bcml_model.newvar2thresh()`](/bcml4pheno/bcml_model.html#bcml_model.newvar2thresh())), an $L$-list of tprs associated to those thresholds, an $L$-list of fprs
related to those thresholds, and an $L$-list of length $?$ `numpy` arrays giving the predicted signal probabilities
for the given data set.

If `sepbg` is `Frue`, labels should take values in $\{0,1\}$. Background is split and a list of length $4$ `self.num_bgs`
is returned containing a  list of $L$ sampled newvars, an $L$-list of tprs associated to those thresholds, an $L$-list of lists of length
$n$ (number of backgrounds) containing fprs for each background type for each threshold, and an $L$-list of length $?$

```
show_doc(bcml_model.best_threshold)
```

bcml_model.best_threshold[source]

> bcml_model.best_threshold(**`signal`**, **`background`**, **`preds`**, **`labels`**, **`sepbg`**=*`False`*)

Optimizes the threshold on a given data set ($? x M$ predictors, length $?$ labels).

```
show_doc(bcml_model.req_sig_cs)
```

bcml_model.req_sig_cs[source]

> bcml_model.req_sig_cs(**`lumi`**, **`bg_cs`**, **`tpr`**, **`fpr`**, **`sig`**=*`5`*, **`sepbg`**=*`False`*)

Given a luminosity (in fb$^{-1}$), a background cross section (in pb), a true positive rate, a false positive rate,
and a signal significance, computes the signal cross section required for the signal significance to be achieved.

If `sepbg` is False, background is combined and a single FPR is used; if `sepbg` is True, it is assumed that
`bg_cs`, `fpr` are each lists of length $n$ (number of backgrounds) and their vector dot product is used for background yield.

The formula used by `req_sig_cs` arises as follows.

We know that
$$\mathcal{S} = \frac{S \cdot \text{TPR}}{\sqrt{S \cdot \text{TPR} + B \cdot \text{FPR}}} = \frac{\mathcal{L} \cdot \sigma_s \cdot \text{TPR}}{\sqrt{\mathcal{L} \cdot \sigma_s \cdot \text{TPR} + \mathcal{L} \cdot \sigma_s \cdot \text{FPR}}}$$
We can work toward solving for $\sigma_s$ as follows.
\begin{align}
    \mathcal{S} &= \frac{\mathcal{L} \cdot \sigma_s \cdot \text{TPR}}{\sqrt{\mathcal{L} \cdot \sigma_s \cdot \text{TPR} + \mathcal{L} \cdot \sigma_b \cdot \text{FPR}}} \\
    \mathcal{S} \left(\sqrt{\mathcal{L} \cdot \sigma_s \cdot \text{TPR} + \mathcal{L} \cdot \sigma_b \cdot \text{FPR}}\right)&= \mathcal{L} \cdot \sigma_s \cdot \text{TPR} \\
    \mathcal{S}^2 \left(\sigma_s \cdot \text{TPR} + \sigma_b \cdot \text{FPR}\right) &= \mathcal{L} \cdot \sigma_s^2 \cdot \text{TPR}^2 \\
    0 &= -\left(\mathcal{L} \cdot \text{TPR}^2\right)\sigma_s^2 + \left(\mathcal{S}^2 \cdot \text{TPR}\right)\sigma_s + \left(\mathcal{S}^2 \cdot \sigma_b \cdot \text{FPR}\right)
\end{align}
This is then easily solvable using the quadratic formula.

### Other utilities

```
show_doc(bcml_model.save_model)
```

bcml_model.save_model[source]

> bcml_model.save_model(**`filename`**)

Saves the model to `filename.joblib`

```
# export
def refresh_model(model):
        """
        If this class gets updated, run this function on your already trained model to have it reflect the updated
        class without retraining being necessary.
        """
        return bcml_model(model.model)
```

```

```


---
## 01. Ecuaciones Diferenciales Parciales I. Generalidades

Eduard Larrañaga (ealarranaga@unal.edu.co)

---

### Resumen

Se presentan las generalidades de los sistemas de Ecuaciones Diferenciales Parciales (PDE).

`A. Garcia. Numerical Methods for Physics. (1999). Chapter 3 `

---

## Ecuaciones Diferenciales Parciales (PDEs)

Una PDE es una relación entre las derivadas parciales de una función desconocida y las variables independientes. El order de un sistema de PDEs corresponde al orden de la derivada más alta presente en las ecuaciones.

Una PDE se denomina *lineal* si solo aparecen terminos de primer grado en la variable dependiente (i.e. la función desconocida) y en sus derivadas parciales.

Si cada uno de los témrinos de la PDE contiene la variable dependiente o una de sus derivadas parciales, se dice que la ecuación es *homogenea*. Si este no es el caso, se dice que la ecuación es *no-homogenea*.

---
### Clases de Ecuaciones Diferenciales Parciales

Exiten tres clases generales de PDEs de segundo orden

1. Hiperbólicas
2. Parabólicas
3. Elípticas

Sin embargo, cabe aclarar que, en general, no todos los sistemas de PDEs caen en alguna de estas clases. Además, no siempre es obvio reconocer (y probar) a que clase pertenece una PDE particular.

Estas clases de PDEs modelan diferentes tipos de fenómenos, muestran comportamientos diferentes y requieren de métodos numéricos diferentes para su solución.

### Ecuación Diferencial Lineal General de Segundo Orden

Considere una función $u=u(x,y)$ que satisface la ecuación diferencial lineal general de segundo orden,

\begin{equation}
a \partial^2_{xx} u + b \partial^2_{xy} u + c \partial^2_{yy} u + d \partial_x u + e \partial_y u + f u = g\,\,,
\end{equation}

Esta ecuación puede ser clasificada rápidamente basandose en el discriminante,

\begin{equation}
\Delta = b^2 - 4ac \left\{ \begin{array}{lcr}
< 0 & \rightarrow & \text{elíptica},\\
= 0 & \rightarrow & \text{parabólica},\\
> 0 & \rightarrow & \text{hiperbólica}.
\end{array}\right.
\end{equation}

Esta clasificación muestra que los nombres han sido elegidos por analogía con las secciones cónicas en la teoría de las elipses.

---
## 1.  PDEs Hiperbólicas

Las PDEs hiperbólicas ($\Delta <0$) en física y astrofísica suelen describir procesos **dinámicos** y procesos que generalmente inician en algún instante $t_0=0$ con ciertas condiciones iniciales. Por ello, usualmente se integran las ecuaciones en el tiempo.

El prototipo de ecuación hiperbólica lineal de segundo orden es la ecuación de onda homogénea,

\begin{equation}
c^2 \partial^2_{xx} u - \partial^2_{tt} u = 0\,\,,
\end{equation}

donde $c$ es la velocidad de propagación de la onda.

---
Por otro lado, un ejemplo de una **ecuación hiperbólica lineal de primer orden** en una dimensión es

\begin{equation}
\partial_t u + A \partial_x u = 0\,\,,
\end{equation}

donde $u(x,t)$ es el vector de estado con $s$ componentes y $A$
es una matriz de tamaño $s \times s$.

Este tipo de problema es *hiperbólico* si $A$ solamente posee valores propios reales y es diagonizable, i.e. existe un conjunto completo de vectores propios linealmente independientes de tal forma que se puede construir una matriz

\begin{equation}
\Lambda = Q^{-1} A Q\,\,,
\end{equation}

donde $\Lambda$ es diagonal y posee numeros reales en la diagonal.

**Ejemplo**

El ejemplo más simple de este tipo de ecuaciones es la **ecuación de advección lineal**, en la que la función $u=u(t,x)$ satisface

\begin{equation}
\partial_t u + v \partial_x u = 0\,\,,
\end{equation}

donde $v$ es un escalar y representa la velocidad de advección.

---

Otro ejemplo de ecuación hiperbólica son los sistemas no-lineales de primer orden,

\begin{equation}
\partial_t u + \partial_x F(u) = 0\,\,,
\end{equation}

donde $F(u)$ se denomina el **flujo** y puede o no ser una función no-lineal de $u(t,x)$. Es posible escribir esta PDE en una forma **cuasi-lineal** introduciendo el Jacobiano

\begin{equation}
\bar{A} = \frac{\partial F}{\partial u}\,\,,
\end{equation}

con lo que obtiene

\begin{equation}
\partial_t u + \bar{A}\partial_x u = 0\,\,.
\end{equation}

Esta PDE es hiperbólica si $\bar{A}$ tiene valores propios reales y es diagonizable.

Las **ecuaciones de la hidrodinámica** son un ejemplo clave de un sistema de PDEs hiperbólicos no-lineales de primer orden.

### Condiciones de Frontera en Problemas Hiperbólicos

Se pueden especificar condiciones de frontera del tipo von Neumann, Dirichlet o Robin. Por ejemplo, para un sistema lineal con fronteras en $x=0$ y $x=L$, se pueden imponer las condiciones

1. **Condiciones de Frontera de Dirichlet**

\begin{equation}
\begin{aligned}
u(x=0,t) &= \Phi_1(t)\,\,,\\
u(x=L,t) &= \Phi_2(t)\,\,.
\end{aligned}
\end{equation}

2. **Condiciones de Frontera de  von Neumann**
\begin{equation}
\begin{aligned}
\partial_x u(x=0,t) &= \Psi_1(t)\,\,,\\
\partial_x u(x=L,t) &= \Psi_2(t)\,\,.
\end{aligned}
\end{equation}

*Note que en un problema multi-dimensional, $\partial_x$ se interpreta como la derivada en la dirección de la normal a la frontera.*

3. **Condiciones de Frontera de Robin**

Sean $a_1, b_1, a_2, b_2$ números reales con $a_i \neq 0$ y $ b_i \neq 0$.

\begin{equation}
\begin{aligned}
a_1 u(x=0,t) + b_1 \partial_x u(x=0,t) &= \Psi_1(t)\,\,,\\
a_2 u(x=L,t) + b_2 \partial_x u(x=L,t) &= \Psi_2(t)\,\,.
\end{aligned}
\end{equation}

Las condiciones de Dirichlet y de von Neuman se recuperan is se anula $a_i$ o $b_i$, respectivamente.

---
## 2.  PDEs Parabólicas

Las PDEs parabólicas suelen describir procesos que cambian lentamente, como por ejemplo la lenta difusión de calor en un medio, de sedimentos en el agua subterranea o la radiación en un medio opaco. El prototipo de PDEs parabólicas de segundo orden es

\begin{equation}
\partial_t u - k \partial^2_{xx} u = f\,\,.
\end{equation}

### Condiciones Iniciales para Problemas Parabólicos

En este tipo de condiciones se debe especificar $u(x,t=0)$ para todos los valores de $x$.

### Condiciones de Frontera para Problemas Parabólicos

Se pueden aplicar condiciones de frontera del tipo Dirichlet, von Neumann o Robin.

*Si las condiciones de frontera son independientes del tiempo, el sistema evolucionará hacia un estado estacionario ($\partial_t u = 0$). En este caso, es posible establecer $\partial_t u = 0$ para todos los tiempos y tratar la ecuación diferencial como una ecuación elíptica.*

---
##  PDEs Elípticas

Las PDEs elípticas suelen describir sistemas estáticos, en estado estacionario o en equilibrio pues no existe dependencia temporal. Una PDE elíptica típica es la ecuación de Poisson,

\begin{equation}
\nabla^2 \phi = \kappa \rho \,\,,
\end{equation}

que describe la gravedad Newtoniana y la electrostática. En esta ecuación $\nabla^2$ es el operador de Laplace y $f$ ies una función escalar de la posición conocida. Los problemas elípticos pueden ser lineales ($f$ no depende de $u$ o sus derivadas) o no-lineales ($f$ depende de $u$ o de sus derivadas).

### Condiciones Iniciales para Problemas Elípticos

No aplican debido a que no existe dependencia temporal.

### Condiciones de Frontera para Problemas Parabólicos

Se pueden aplicar condiciones de frontera del tipo Dirichlet, von Neumann o Robin.

---

---

## Métodos Numéricos para resolver PDEs

No existe un método general y robusto para la solución de todas las PDEs. Cada tipo (o sub-tipo) de las PDEs requieren un tratamiento diferente. Las PDEs de la vida real suelen ser una mezcla de las diferentes clases o pueden poseer propiedades especiales que requieren conocimiento de la física subyacente para obtener una solución adecuada.

Sin embargo, existen tres clases generales de métodos que pueden aplicarse a la resolución de las PDEs.

### 1. Métodos de Diferencias Finitas.

Los operadores diferenciales se aproximan utilizando una representación de diferencias finitas en una malla. Despues de encontrar la forma iterativa de las ecuaciones, el problema diferencial se transforma en un sistema lineal de ecuaciones algebraicas.

Una sub-clase de este método son los métodos de volumenes finitos, que son utiles al trabajar con PDEs relacionadas con leyes de conservación (e.g., las ecuaciones de la hidrodinámica).

Los métodos de diferencias/volumenes finitos tienen una convergencia polinomial para funciones suaves.

### 2. Métodos de Elementos Finitos.

El dominio es dividido en celdas llamadas **elementos**. La solución se representa con una función simple (e.g. un polinomio) en cada una de las celdas y la PDE se transforma en un problema algebraico de encontrar las condiciones de juntura en las interfaces de las celdas.

Los métodos de elementos finitos tienen una convergencia polinomial o exponencial para funciones suaves.

### 3. Métodos Espectrales.

La solución se representa como una combinación lineal de funciones conocidas (e.g. funciones trigonométricas o polinomios especiales). La PDE es transformada en un conjunto de ecuaciones algebraicas o en un conjunto de ODEs para las amplitudes de las funciones componentes.

Una sub-clase son los métodos de co-locación en los cuales la solución se representa en una malla y la descomposición espectral es utilizada para estimar las derivadas parciales en los nodos con un alto grado de precisión.

10. PDE I. Generalidades. Diferencias Finitas/01. Generalidades.ipynb


# Qiskit 소개
Qiskit을 사용하는 경우 사용자 워크플로우는 다음의 네 가지 상위 레벨 단계로 구성된다.

빌드: 사용자가 고려하고 있는 문제를 나타내는 양자 회로(들)을 설계한다.

컴파일: 특정 양자 서비스를 위한 회로를 컴파일한다 (예: 퀀텀 시스템 또는 고전 시뮬레이터).

Run: 지정된 퀀텀 서비스에서 컴파일된 회로를 실행한다. 이러한 서비스는 클라우드 기반 또는 로컬일 수 있습니다.

분석: 요약 통계를 계산하고 실험 결과를 시각화한다.

```python
!pip install qiskit
```

    Requirement already satisfied: qiskit in c:\programdata\anaconda3\lib\site-packages (0.27.0)
    Requirement already satisfied: qiskit-aer==0.8.2 in c:\programdata\anaconda3\lib\site-packages (from qiskit) (0.8.2)
    Requirement already satisfied: qiskit-ibmq-provider==0.14.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit) (0.14.0)
    Requirement already satisfied: qiskit-ignis==0.6.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit) (0.6.0)
    Requirement already satisfied: qiskit-aqua==0.9.2 in c:\programdata\anaconda3\lib\site-packages (from qiskit) (0.9.2)
    Requirement already satisfied: qiskit-terra==0.17.4 in c:\programdata\anaconda3\lib\site-packages (from qiskit) (0.17.4)
    Requirement already satisfied: scipy>=1.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aer==0.8.2->qiskit) (1.6.2)
    Requirement already satisfied: pybind11>=2.6 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aer==0.8.2->qiskit) (2.6.2)
    Requirement already satisfied: numpy>=1.16.3 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aer==0.8.2->qiskit) (1.20.1)
    Requirement already satisfied: quandl in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (3.6.1)
    Requirement already satisfied: setuptools>=40.1.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (52.0.0.post20210125)
    Requirement already satisfied: psutil>=5 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (5.8.0)
    Requirement already satisfied: sympy>=1.3 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (1.8)
    Requirement already satisfied: fastdtw<=0.3.4 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (0.3.4)
    Requirement already satisfied: pandas in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (1.2.4)
    Requirement already satisfied: scikit-learn>=0.20.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (0.24.1)
    Requirement already satisfied: h5py in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (2.10.0)
    Requirement already satisfied: yfinance in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (0.1.60)
    Requirement already satisfied: retworkx>=0.8.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (0.9.0)
    Requirement already satisfied: docplex<=2.20.204 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (2.20.204)
    Requirement already satisfied: dlx<=1.0.4 in c:\programdata\anaconda3\lib\site-packages (from qiskit-aqua==0.9.2->qiskit) (1.0.4)
    Requirement already satisfied: requests-ntlm>=1.1.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit-ibmq-provider==0.14.0->qiskit) (1.1.0)
    Requirement already satisfied: requests>=2.19 in c:\programdata\anaconda3\lib\site-packages (from qiskit-ibmq-provider==0.14.0->qiskit) (2.25.1)
    Requirement already satisfied: dill>=0.3 in c:\programdata\anaconda3\lib\site-packages (from qiskit-ibmq-provider==0.14.0->qiskit) (0.3.4)
    Requirement already satisfied: urllib3>=1.21.1 in c:\programdata\anaconda3\lib\site-packages (from qiskit-ibmq-provider==0.14.0->qiskit) (1.26.4)
    Requirement already satisfied: python-dateutil>=2.8.0 in c:\programdata\anaconda3\lib\site-packages (from qiskit-ibmq-provider==0.14.0->qiskit) (2.8.1)
    Requirement already satisfied: websocket-client>=1.0.1 in c:\programdata\anaconda3\lib\site-packages (from qiskit-ibmq-provider==0.14.0->qiskit) (1.1.0)
    Requirement already satisfied: fastjsonschema>=2.10 in c:\programdata\anaconda3\lib\site-packages (from qiskit-terra==0.17.4->qiskit) (2.15.1)
    Requirement already satisfied: ply>=3.10 in c:\programdata\anaconda3\lib\site-packages (from qiskit-terra==0.17.4->qiskit) (3.11)
    Requirement already satisfied: python-constraint>=1.4 in c:\programdata\anaconda3\lib\site-packages (from qiskit-terra==0.17.4->qiskit) (1.4.0)
    Requirement already satisfied: jsonschema>=2.6 in c:\programdata\anaconda3\lib\site-packages (from qiskit-terra==0.17.4->qiskit) (3.2.0)
    Requirement already satisfied: six in c:\programdata\anaconda3\lib\site-packages (from docplex<=2.20.204->qiskit-aqua==0.9.2->qiskit) (1.15.0)
    Requirement already satisfied: attrs>=17.4.0 in c:\programdata\anaconda3\lib\site-packages (from jsonschema>=2.6->qiskit-terra==0.17.4->qiskit) (20.3.0)
    Requirement already satisfied: pyrsistent>=0.14.0 in c:\programdata\anaconda3\lib\site-packages (from jsonschema>=2.6->qiskit-terra==0.17.4->qiskit) (0.17.3)
    Requirement already satisfied: idna<3,>=2.5 in c:\programdata\anaconda3\lib\site-packages (from requests>=2.19->qiskit-ibmq-provider==0.14.0->qiskit) (2.10)
    Requirement already satisfied: certifi>=2017.4.17 in c:\users\issel\appdata\roaming\python\python38\site-packages (from requests>=2.19->qiskit-ibmq-provider==0.14.0->qiskit) (2020.4.5.1)
    Requirement already satisfied: chardet<5,>=3.0.2 in c:\programdata\anaconda3\lib\site-packages (from requests>=2.19->qiskit-ibmq-provider==0.14.0->qiskit) (4.0.0)
    Requirement already satisfied: ntlm-auth>=1.0.2 in c:\programdata\anaconda3\lib\site-packages (from requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.14.0->qiskit) (1.5.0)
    Requirement already satisfied: cryptography>=1.3 in c:\programdata\anaconda3\lib\site-packages (from requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.14.0->qiskit) (3.4.7)
    Requirement already satisfied: cffi>=1.12 in c:\programdata\anaconda3\lib\site-packages (from cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.14.0->qiskit) (1.14.5)
    Requirement already satisfied: pycparser in c:\programdata\anaconda3\lib\site-packages (from cffi>=1.12->cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.14.0->qiskit) (2.20)
    Requirement already satisfied: joblib>=0.11 in c:\programdata\anaconda3\lib\site-packages (from scikit-learn>=0.20.0->qiskit-aqua==0.9.2->qiskit) (1.0.1)
    Requirement already satisfied: threadpoolctl>=2.0.0 in c:\programdata\anaconda3\lib\site-packages (from scikit-learn>=0.20.0->qiskit-aqua==0.9.2->qiskit) (2.1.0)
    Requirement already satisfied: mpmath>=0.19 in c:\programdata\anaconda3\lib\site-packages (from sympy>=1.3->qiskit-aqua==0.9.2->qiskit) (1.2.1)
    Requirement already satisfied: pytz>=2017.3 in c:\programdata\anaconda3\lib\site-packages (from pandas->qiskit-aqua==0.9.2->qiskit) (2021.1)
    Requirement already satisfied: more-itertools in c:\programdata\anaconda3\lib\site-packages (from quandl->qiskit-aqua==0.9.2->qiskit) (8.7.0)
    Requirement already satisfied: inflection>=0.3.1 in c:\programdata\anaconda3\lib\site-packages (from quandl->qiskit-aqua==0.9.2->qiskit) (0.5.1)
    Requirement already satisfied: lxml>=4.5.1 in c:\programdata\anaconda3\lib\site-packages (from yfinance->qiskit-aqua==0.9.2->qiskit) (4.6.3)
    Requirement already satisfied: multitasking>=0.0.7 in c:\programdata\anaconda3\lib\site-packages (from yfinance->qiskit-aqua==0.9.2->qiskit) (0.0.9)

```python
import numpy as np
from qiskit import QuantumCircuit, transpile
from qiskit.providers.aer import QasmSimulator
from qiskit.visualization import plot_histogram

# Use Aer's qasm_simulator
simulator = QasmSimulator()

# Create a Quantum Circuit acting on the q register
circuit = QuantumCircuit(2, 2)

# Add a H gate on qubit 0
circuit.h(0)

# Add a CX (CNOT) gate on control qubit 0 and target qubit 1
circuit.cx(0, 1)

# Map the quantum measurement to the classical bits
circuit.measure([0,1], [0,1])

# compile the circuit down to low-level QASM instructions
# supported by the backend (not needed for simple circuits)
compiled_circuit = transpile(circuit, simulator)

# Execute the circuit on the qasm simulator
job = simulator.run(compiled_circuit, shots=1000)

# Grab results from the job
result = job.result()

# Returns counts
counts = result.get_counts(circuit)
print("\nTotal count for 00 and 11 are:",counts)

# Draw the circuit
circuit.draw()
```

    Total count for 00 and 11 are: {'00': 498, '11': 502}

<pre style="word-wrap: normal;white-space: pre;background: #fff0;line-height: 1.1;font-family: "Courier New",Courier,monospace">     ┌───┐     ┌─┐
q_0: ┤ H ├──■──┤M├───
     └───┘┌─┴─┐└╥┘┌─┐
q_1: ─────┤ X ├─╫─┤M├
          └───┘ ║ └╥┘
c: 2/═══════════╩══╩═
                0  1 </pre>

```python
# Plot a histogram
plot_histogram(counts)
```