```python
%matplotlib inline
```

PyTorch를 이용한 딥러닝
**************************

딥러닝 블록 구축 : 아핀 맵(affine maps), 비선형성, 객체
==========================================================================

딥러닝은 영리한 방법으로 비선형성을 가진 선형성을 구성하는 것으로
이루어집니다. 비선형성의 도입은 강력한 모델을 가능하게 합니다.
이 섹션에서 이 핵심 구성 요소를 다루고, 객체 함수를 만들고, 어떻게
모델이 학습되지는 살펴봅시다.

아핀 맵
~~~~~~~~~~~

딥러닝의 핵심 작업자 중 하나는 아핀 맵 입니다.
이 함수 $f(x)$ 는 다음과 같습니다.

\begin{align}f(x) = Ax + b\end{align}

여기서 $A$ 는 행렬, $x, b$ 는 벡터 입니다.
여기서 학습되는 변수는 $A$ 와 $b$ 입니다.
종종 $b$ 는 *편향(Bias)* 이라 불립니다.

PyTorch 와 대부분의 다른 딥러닝 프레임워크들은 고전적인 선형 대수학와
조금 다르게 동작합니다. 입력의 열 대신에 행으로 매핑합니다.
즉 주어진 $A$ 에서 출력의 $i$ 번째 행은
입력의 $i$ 번째 행에 매핑되고 편향(Bias)을 더합니다.
아래 예시를 살펴보십시오.

```python
# Author: Robert Guthrie

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim

torch.manual_seed(1)
```

```python
lin = nn.Linear(5, 3)  # maps from R^5 to R^3, parameters A, b
# data is 2x5.  A maps from 5 to 3... can we map "data" under A?
data = torch.randn(2, 5)
print(lin(data))  # yes
```

비선형성
~~~~~~~~~

먼저 왜 비선형성이 필요한지 설명하는 다음 사실을 주목하십시오.
$f(x) = Ax + b$ 와 $g(x) = Cx + d$ 두개의 아핀맵이 있다고 가정합니다.
$f(g(x))$ 는 무엇일까요?

\begin{align}f(g(x)) = A(Cx + d) + b = ACx + (Ad + b)\end{align}

$AC$ 는 행렬이고 $Ad + b$ 는 벡터이므로 아핀맵 구성은
아핀맵이 주어집니다.

이것으로부터, 신경망이 아핀 구성의 긴 체인이 되길 원한다면,
단일 아핀 맵을 작성하는 것보다 이것이 모델에 추가하는 새로운 힘이
없다는 것을 알 수 있습니다.

아핀 계층 사이에 만약 비선형성을 적용한다면
이것은 위 경우와 달리 더욱 더 강력한 모델을 구축할 수 있습니다.

핵심적인 비선형성 $\tanh(x), \sigma(x), \text{ReLU}(x)$ 들이 가장
일반적입니다. 아마 의문이 생길겁니다 : "왜 이런 함수들이지? 나는 다른 많은
비선형성을 생각할 수 있는데". 그 이유는 그들이 그래디언트를 계산하기 쉽고,
그래디언트 연산은 학습에 필수적이기 때문입니다.
예를 들어서

\begin{align}\frac{d\sigma}{dx} = \sigma(x)(1 - \sigma(x))\end{align}

빠른 참고: AI 클래스에 대한 소개에서 일부 신경망을 배웠지만 $\sigma(x)$ 가 기본이었을 것입니다.
일반적으로 사람들은 실제로 그것을 사용하지 않고 피합니다.
이것은 그래디언트가 인수의 절대 값이 커짐에 따라 매우 빨리 *사라지기* 때문입니다.
작은 그래디언트는 학습하기 어렵다는 것을 의미합니다.
대부분의 사람들은 tanh 또는 ReLU를 기본값으로 사용합니다.

```python
# Pytorch에서 대부분의 비선형성은 torch.functional에 있습니다 ( F 로 가져옵니다)
# 일반적으로 비선형성은 아핀맵과 같은 파라미터를 가지고 있지 않습니다.
# 즉, 학습 중에 업데이트되는 가중치가 없습니다.
data = torch.randn(2, 2)
print(data)
print(F.relu(data))
```

Softmax 및 확률
~~~~~~~~~~~~~~~~~~~~~~~~~

함수 $\text{Softmax}(x)$ 또한 단지 비선형성 이지만, 일반적으로 네트워크에서
마지막으로 수행되는 작업이라는 점에서 특별합니다.
이는 실수의 벡터를 취하여 확률 분포를 반환하기 때문입니다.
정의는 다음과 같습니다. $x$ 는 실수 벡터(음수, 양수 , 제약 없음)라고 하면,
i번째 구성 요소는 $\text{Softmax}(x)$ 는

\begin{align}\frac{\exp(x_i)}{\sum_j \exp(x_j)}\end{align}

출력은 확률 분포라는 것이 분명해야합니다:
각 요소는 음수가 아니며 모든 구성 요소의 합은 1입니다.

모두 음수가 아니게 하기 위해서 입력에 요소 단위의 지수 연산자를 적용한 다음
정규화 상수로 나누는 것도 생각할 수 있습니다.

```python
# Softmax 도 torch.nn.functional 에 있습니다.
data = torch.randn(5)
print(data)
print(F.softmax(data, dim=0))
print(F.softmax(data, dim=0).sum())  # 확률 분포이기 때문에 합이 1 입니다!
print(F.log_softmax(data, dim=0))  # log_softmax 도 있습니다.
```

목적 함수(Objective Functions)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

목적 함수는 네트워크가 최소화하도록 학습되는 함수입니다
( *손실 함수* 또는 *비용 함수* 라고 함).
먼저 학습 인스턴스를 선택하고 신경망을 통해 실행한 다음 출력의 손실을 계산합니다.
그런 다음 손실 함수의 미분을 취함으로써 모델의 파라미터가 업데이트됩니다.
직관적으로 모델이 자신의 대답에 완전히 확신하고 대답이 잘못되면 손실이 높아집니다.
답변에 자신이 있고 답변이 맞으면 손실이 적습니다.

학습 예제에서 손실 함수를 최소화하려는 아이디어는
네트워크가 잘 일반화되고 개발자 세트, 테스트 세트 또는 프로덕션에서
나타나지 않았던 예제(unseen examples)에 대해 작은 손실을 가지기를 바랍니다.
손실 함수의 예로 *음의 로그 우도 손실(negative log likelihood loss)* 있습니다.
이 것은 다중 클래스 분류에서 매우 자주 사용되는 목적 함수입니다.
감독 다중 클래스 분류의 경우에는 올바른 출력(정답을 맞춘 출력)의 음의 로그 확률을
최소화하도록 네트워크를 교육하는 것을 의미합니다.
(또는 이와 동등하게 올바른 출력의 로그 확률을 최대화하십시오)

최적화와 학습
=========================

그럼 인스턴스에 대해 손실 함수를 계산할 수 있다는 것은 무엇입니까? 그걸 어떻게 할까요?
우리는 이전에 Tensor가 그것을 계산하는데 사용된 것들에 해당하는 그래디언트를
계산하는 방법을 알고 있다는 것을 보았습니다.
손실은 Tensor이기 때문에 그것을 계산하는데 사용된 모든 파라미터와 관련하여
그래디언트를 계산할 수 있습니다! 그런 다음 표준 그래디언트 업데이트를 수행 할 수 있습니다.
$\theta$ 가 우리의 파라미터라고 합시다.
$L(\theta)$ 는 손실 함수, 그리고 $\eta$ 는 양의 러닝 레이트 입니다. 그러면

\begin{align}\theta^{(t+1)} = \theta^{(t)} - \eta \nabla_\theta L(\theta)\end{align}

이 기본적인 그레디언트 업데이트 이상의 것을 하기 위해서 많은 알고리즘과
시도되고 있는 활발한 연구들이 있습니다.
많은 시도들은 학습 시간에 일어나는 것에 기반한 러닝 레이트를 변경해봅니다.
당신이 정말 관심이 없다면 특별히 이들 알고리즘이 무엇을 하는지 걱정할
필요가 없습니다. Torch는 torch.optim  패키지에서 많은 것을 제공하며
완전히 공개되어 있습니다. 가장 단순한 그래디언트 업데이트 사용은
더 복잡한 알고리즘을 사용하는 것과 동일합니다.
다른 업데이트 알고리즘과 업데이트 알고리즘을 위한 다른 파라미터(다른 초기 러닝 레이트)를
시도해 보는 것은 네트워크의 성능을 최적화하는데 중요합니다.
종종 기본 SGD를 Adam 또는 RMSprop 으로 교체하는 것이 눈에 띄게 성능
향상 시킵니다.

Pytorch 에서 네트워크 구성요소 생성하기
==========================================

NLP에 초점을 맞추기 전에,  PyTorch에서 아핀 맵과 비선형성만을 사용하여
네트워크를 구축하는 주석 처리된 예제를 수행 할 수 있습니다.
또한 손실 함수를 계산하는 방법, PyTorch에 내장된 음의 로그 우도를 사용하는 방법,
역전파를 통해 매개 변수를 업데이트하는 방법을 볼 것입니다.

모든 네트워크 구성 요소는 nn.Module에서 상속 받아 forward() 메서드를 재정의해야합니다.
이것은 상용구에 관한 것입니다. nn.Module에서의 상속은 구성 요소에 기능을 제공합니다.
예를 들어 그것은 학습 가능한 파라미터를 추적하도록 만들고,
``.to(device)`` 로 CPU와 GPU 를 교환할수 있습니다.
``torch.device("cpu")`` 는 CPU 장치를 ``torch.device("cuda:0")`` 는 GPU 장치를 사용합니다.

희소한 Bag-of-Words Representation 을 받아서 두개의 레이블 "영어"와 "스페인어"의 확률 분포
출력하는 네트워크의 주석이 달린 예시를 작성해 봅시다.
이 모델은 단순한 논리 회귀 입니다.

예제: 논리 회귀 Bag-of-Words 분류기
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

우리 모델은 희소한 BoW 표현을 레이블에 대한 로그 확률로 매핑합니다.
사전의 각 단어에 하나의 색인을 할당합니다.
예를 들어서 전체 사전이 각각 0과 1의 색인을 가진 두개의 단어 "hello" 와 "world" 라고 합시다.
"hello hello hello hello" 문장의 BoW 벡터는 다음과 같습니다.

\begin{align}\left[ 4, 0 \right]\end{align}

 "hello world world hello"는 다음과 같습니다.

\begin{align}\left[ 2, 2 \right]\end{align}

일반화 하면 다음과 같습니다.

\begin{align}\left[ \text{Count}(\text{hello}), \text{Count}(\text{world}) \right]\end{align}

이 BOW 벡터를 $x$ 라하면 네트워크의 출력은 다음과 같습니다:

\begin{align}\log \text{Softmax}(Ax + b)\end{align}

즉, 아핀맵에 입력을 주고 그 다음 Log Softmax 를 합니다.

```python
data = [("me gusta comer en la cafeteria".split(), "SPANISH"),
        ("Give it to me".split(), "ENGLISH"),
        ("No creo que sea una buena idea".split(), "SPANISH"),
        ("No it is not a good idea to get lost at sea".split(), "ENGLISH")]

test_data = [("Yo creo que si".split(), "SPANISH"),
             ("it is lost on me".split(), "ENGLISH")]

# word_to_ix 는 사전의 각 단어를 고유한 정수로 매핑하고
# 그것은 BoW 벡터에서 자신의 색인이 됩니다.
word_to_ix = {}
for sent, _ in data + test_data:
    for word in sent:
        if word not in word_to_ix:
            word_to_ix[word] = len(word_to_ix)
print(word_to_ix)

VOCAB_SIZE = len(word_to_ix)
NUM_LABELS = 2

class BoWClassifier(nn.Module):  # nn.Module로 부터 상속 받기 !

    def __init__(self, num_labels, vocab_size):
        # calls the init function of nn.Module의 초기화 함수 호출.  Dont get confused by syntax,
        # 문법에 혼란스러워 하지 마시고 단지 항상 nn.Module 에서 수행하십시오.
        super(BoWClassifier, self).__init__()

        # 필요한 파라미터를 정의 하십시오. 이 경우에는 아핀 매핑의 매개 변수 인 A와 b가 필요합니다.
        # Torch는 아핀 맵을 제공하는 nn.Linear()를 정의합니다
        # 입력 차원이 vocab_size이고 출력이 num_labels 인 이유를 이해했는지 확인하십시오!
        self.linear = nn.Linear(vocab_size, num_labels)

        # 주의! 비선형성 Log Softmax에는 파라미터가 없습니다!
        # 그래서 여기에 대해 걱정할 필요가 없습니다.

    def forward(self, bow_vec):
        # 선형 계층를 통해 입력을 전달한 다음 log_softmax로 전달합니다.
        # 많은 비선형성 및 기타 기능이 torch.nn.functional 에 있습니다

        return F.log_softmax(self.linear(bow_vec), dim=1)

def make_bow_vector(sentence, word_to_ix):
    vec = torch.zeros(len(word_to_ix))
    for word in sentence:
        vec[word_to_ix[word]] += 1
    return vec.view(1, -1)

def make_target(label, label_to_ix):
    return torch.LongTensor([label_to_ix[label]])

model = BoWClassifier(NUM_LABELS, VOCAB_SIZE)

# 모델은 자신의 파라미터를 알고 있습니다. 아래에 있는 첫번째 출력은 A 두번째는 b 입니다.
# 모듈의 __init__ 함수에서 클래스 변수에 구성 요소를 할당 할 때마다 다음 행을 사용하여 완료합니다.
# self.linear = nn.Linear(...)
# 그런 다음 PyTorch 개발자의 Python 마법을 통해, 모듈(이 경우 BoWClassifier)은
# nn.Linear 파라미터에 대한 지식을 저장합니다
for param in model.parameters():
    print(param)

# 모델을 실행하려면 BoW 벡터를 전달합니다.
# 여기서 우리는 학습 할 필요가 없으므로 코드는 torch.no_grad()로 싸여 있습니다.
with torch.no_grad():
    sample = data[0]
    bow_vector = make_bow_vector(sample[0], word_to_ix)
    log_probs = model(bow_vector)
    print(log_probs)
```

위의 값 중 어느 것이 ENGLISH와 SPANISH의 로그 확률에 해당하는 값일까요?
우리는 정의하지 않았지만, 학습하기를 원한다면 필요합니다.

```python
label_to_ix = {"SPANISH": 0, "ENGLISH": 1}
```

그럼 학습을 해봅시다! 이를 위해 로그 확률을 얻고, 손실 함수를 계산하고,
손실 함수의 그래디언트를 계산한 다음 그래디언트 단계로 파라미터를
업데이트하기 위해 인스턴스를 통과시킵니다.  손실 기능은 nn 패키지의 Torch에서 제공합니다.
nn.NLLLoss()는 원하는 음의 로그 우도 손실입니다. 또한 torch.optim에서 최적화 함수를 정의합니다.
여기서는 SGD 만 사용합니다.

NLLLoss에 대한 *입력* 은 로그 확률의 벡터이고 목표 레이블입니다.
우리를 위한 로그 확률을 계산하지 않습니다. 이것이 네트워크의 마지막 계층이
Log softmax 인 이유입니다. 손실 함수 nn.CrossEntropyLoss()는 Log softmax를 제외하고는 NLLLoss()와 같습니다.

```python
# 훈련하기 전에 테스트 데이터를 실행하여 전후를 볼 수 있습니다.
with torch.no_grad():
    for instance, label in test_data:
        bow_vec = make_bow_vector(instance, word_to_ix)
        log_probs = model(bow_vec)
        print(log_probs)

# "creo"에 해당하는 행렬 열을 인쇄하십시오.
print(next(model.parameters())[:, word_to_ix["creo"]])

loss_function = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# 일반적으로 교육 데이터를 여러 번 전달 합니다.
# 100은 실제 데이터 세트보다 훨씬 더 크지 만 실제 데이터 세트는 두 개 이상의 인스턴스를 가집니다.
# 일반적으로 5 ~ 30 개 에포크가 적당합니다.
for epoch in range(100):
    for instance, label in data:
        # 1 단계. PyTorch는 그라데이션을 축적합니다.
        # 각 인스턴스 전에 그들을 제거해야합니다.
        model.zero_grad()

        # 2 단계. BOW 벡터를 만들고 정수로 텐서로 대상을 싸야합니다.
        # 예를 들어, 대상이 SPANISH이면 정수 0으로 합니다.
        # 손실 함수는 로그 확률의 0번째 요소가 SPANISH에 해당하는 로그 확률임을 알 수 있습니다
        bow_vec = make_bow_vector(instance, word_to_ix)
        target = make_target(label, label_to_ix)

        # 3 단계. 순전파를 실행합니다.
        log_probs = model(bow_vec)

        # 4 단계. optimizer.step()을 호출하여 손실, 그래디언트를 계산하고 파라미터를 업데이트합니다.
        loss = loss_function(log_probs, target)
        loss.backward()
        optimizer.step()

with torch.no_grad():
    for instance, label in test_data:
        bow_vec = make_bow_vector(instance, word_to_ix)
        log_probs = model(bow_vec)
        print(log_probs)

# 스페인어에 해당하는 색인이 올라갑니다. 영어가 내려갑니다!!
print(next(model.parameters())[:, word_to_ix["creo"]])
```

정답을 얻었습니다! 첫 번째 예제에서는 스페인어의 로그 확률이 훨씬 높고
영어의 로그 확률은 테스트 데이터의 두 번째에서 훨씬 높다는 것을 알 수 있습니다.

이제 PyTorch 구성 요소를 만들고 이를 통해 일부 데이터를 전달하고
그래디언트 업데이트를 수행하는 방법을 살펴 보았습니다.
우리는 심도있는 NLP가 제공해야하는 것을 더 깊이 파고들 준비가되었습니다.


# Tutorial 1: Sequential Probability Ratio Test
**Week 3, Day 2: Hidden Dynamics**

**By Neuromatch Academy**

__Content creators:__ Yicheng Fei and Xaq Pitkow

__Content reviewers:__ John Butler, Matt Krause, Spiros Chavlis, Michael Waskom, Jesse Livezey, and Byron Galbraith

---
#Tutorial Objectives

In W3D1, we learned how to combine the sensory evidence and our prior experience with Bayes' Theorem, producing a posterior probability distribution that would let us choose between the most probable of *two* options (fish being on the left or fish being on the right).

Here, we add a *third* option: choosing to collect more evidence before making a decision.

---

In this notebook we will perform a *Sequential Probability Ratio Test* (SPRT) between two hypotheses $s=+1$ and $s=-1$ by running simulations of a *Drift Diffusion Model (DDM)*. As data comes in, we accumulate evidence linearly until a stopping criterion is met before deciding which hypothesis to accept.

In this tutorial, you will
* Simulate the Drift-Diffusion Model.
* Gain intuition about the tradeoff between decision speed and accuracy.

```python
#@title Video 1: Overview of Tutorials on Hidden Dynamics
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="ofNRpSpRxl4", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

---
# Setup

```python
# Imports
import numpy as np
from scipy import stats
import matplotlib.pyplot as plt
```

```python
#@title Figure settings
import ipywidgets as widgets       # interactive display
%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/course-content/NMA2020/nma.mplstyle")
```

```python
#@title Helper functions

def simulate_and_plot_SPRT_fixedtime(sigma, stop_time, num_sample,
                                     verbose=True):
  """Simulate and plot a SPRT for a fixed amount of time given a std.

  Args:
    sigma (float): Standard deviation of the observations.
    stop_time (int): Number of steps to run before stopping.
    num_sample (int): The number of samples to plot.
    """

  evidence_history_list = []
  if verbose:
    print("#Trial\tTotal_Evidence\tDecision")
  for i in range(num_sample):
    evidence_history, decision, Mvec = simulate_SPRT_fixedtime(sigma, stop_time)
    if verbose:
      print("{}\t{:f}\t{}".format(i, evidence_history[-1], decision))
    evidence_history_list.append(evidence_history)

  fig, ax = plt.subplots()
  maxlen_evidence = np.max(list(map(len,evidence_history_list)))
  ax.plot(np.zeros(maxlen_evidence), '--', c='red', alpha=1.0)
  for evidences in evidence_history_list:
    ax.plot(np.arange(len(evidences)), evidences)
    ax.set_xlabel("Time")
    ax.set_ylabel("Cumulated log likelihood ratio")
    ax.set_title("Log likelihood ratio trajectories under the fixed-time " +
                  "stopping rule")

  plt.show(fig)

def plot_accuracy_vs_stoptime(stop_time_list, accuracy_list):
  """Simulate and plot a SPRT for a fixed amount of times given a std.

  Args:
    stop_time_list (int): List of number of steps to run before stopping.
    accuracy_list (int): List of accuracies for each stop time
  """

  fig, ax = plt.subplots()
  ax.plot(stop_time_list, accuracy_list)
  ax.set_xlabel('Stop Time')
  ax.set_ylabel('Average Accuracy')

  plt.show(fig)

def simulate_and_plot_SPRT_fixedthreshold(sigma, num_sample, alpha,
                                          verbose=True):
  """Simulate and plot a SPRT for a fixed amount of times given a std.

  Args:
    sigma (float): Standard deviation of the observations.
    num_sample (int): The number of samples to plot.
    alpha (float): Threshold for making a decision.
  """
  # calculate evidence threshold from error rate
  threshold = threshold_from_errorrate(alpha)

  # run simulation
  evidence_history_list = []
  if verbose:
    print("#Trial\tTime\tCumulated Evidence\tDecision")
  for i in range(num_sample):
    evidence_history, decision, Mvec = simulate_SPRT_threshold(sigma, threshold)
    if verbose:
      print("{}\t{}\t{:f}\t{}".format(i, len(Mvec), evidence_history[-1],
                                      decision))
    evidence_history_list.append(evidence_history)

  fig, ax = plt.subplots()
  maxlen_evidence = np.max(list(map(len,evidence_history_list)))
  ax.plot(np.repeat(threshold,maxlen_evidence + 1), c="red")
  ax.plot(-np.repeat(threshold,maxlen_evidence + 1), c="red")
  ax.plot(np.zeros(maxlen_evidence + 1), '--', c='red', alpha=0.5)

  for evidences in evidence_history_list:
      ax.plot(np.arange(len(evidences) + 1), np.concatenate([[0], evidences]))

  ax.set_xlabel("Time")
  ax.set_ylabel("Cumulated log likelihood ratio")
  ax.set_title("Log likelihood ratio trajectories under the threshold rule")

  plt.show(fig)

def simulate_and_plot_accuracy_vs_threshold(sigma, threshold_list, num_sample):
  """Simulate and plot a SPRT for a set of thresholds given a std.

  Args:
    sigma (float): Standard deviation of the observations.
    alpha_list (float): List of thresholds for making a decision.
    num_sample (int): The number of samples to plot.
  """
  accuracies, decision_speeds = simulate_accuracy_vs_threshold(sigma,
                                                               threshold_list,
                                                               num_sample)

  # Plotting
  fig, ax = plt.subplots()
  ax.plot(decision_speeds, accuracies, linestyle="--", marker="o")
  ax.plot([np.amin(decision_speeds), np.amax(decision_speeds)],
          [0.5, 0.5], c='red')
  ax.set_xlabel("Average Decision speed")
  ax.set_ylabel('Average Accuracy')
  ax.set_title("Speed/Accuracy Tradeoff")
  ax.set_ylim(0.45, 1.05)

  plt.show(fig)

def threshold_from_errorrate(alpha):
  """Calculate log likelihood ratio threshold from desired error rate `alpha`

  Args:
    alpha (float): in (0,1), the desired error rate

  Return:
    threshold: corresponding evidence threshold
  """
  threshold = np.log((1. - alpha) / alpha)
  return threshold
```

---

# Section 1: Introduction to the SPRT

## Section 1.1: The random dot task

A classic experimental task in neuroscience is the random dot kinematogram ([Newsome, Britten, Movshon 1989](https://www.nature.com/articles/341052a0.pdf)), in which a pattern of moving dots are moving in random directions but with some weak coherence that favors a net rightward or leftward motion. The observer must guess the direction. Neurons in the brain are informative about this task, and have responses that correlate with the choice.

Below is a video by Pamela Reinagle of a rat guessing the direction of motion in such a task.

```python
#@title Video 2: Rat performing the Random Dot Motion Task
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="oDxcyTn-0os", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

In this tutorial, we will consider a model for this random dot motion task. In each time bin $t$, we are shown dots moving at net measured velocity $m_t$, either in a negative ($m<0$) or positive ($m>0$) direction. Although the dots' velocities varies over time, the $m_t$ are generated by a fixed probability distribution $p(m|s)$ that depends on a fixed latent variable $s=\pm 1$:
$$
\\
\begin{eqnarray}
p(m|s=+1) &=& \mathcal{N}\left(\mu_+,\sigma^2\right) \\
&&\textrm{or} \\
p(m|s=-1) &=& \mathcal{N}\left(\mu_-,\sigma^2\right) \\
\end{eqnarray}
\\
$$
Here we assume the measurement probabilities have the same variances regardless of $s$, and different means. We, like the rat, want to synthesize our evidence to determine whether $s=+1$ or $-1$.

## Section 1.2: Sequential Probability Ratio Test(SPRT)

```python
#@title Video 3: Decision making: Sequential Probability Ratio Test
# Insert the ID of the corresponding youtube video
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="DGoPoLkDiUw", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

Suppose we obtain a sequence of independent measurements $m_{1:T}$ from a distribution $p(m_{1:T}|s)$. Remember that $s$ is our hidden state and for now, is either -1 or 1. Our measurements come from either $p(m_t|s=-1)$ or $p(m_t|s=1)$. We wish to test which value of $s$ is more likely given our sequence of measurements.

A crucial assumption in Hidden Markov Models is that all measurements are drawn independently given the latent state. In the fishing example, you might have a high or low probability of catching fish depending on where the school of fish is --- but if you already *knew* the location $s$ of the school (which is what we mean by a conditional probability $p(m|s)$), then your chances of catching fish at one time is unaffected by whether you caught fish there previously.

Mathematically, we write this independence as $p(m_1, m_2|s)=p(m_1|s)p(m_2|s)$, using the product rule of probabilities.  When we consider a whole time series of measurements $m_{1:T}$, we can compute the product $p(m_{1:T}|s)=\prod_{t=1}^T p(m_t|s)$.

We can then compare the total evidence up to time $T$ for our two hypotheses (of whether our state is -1 or 1) by taking a ratio
of the likelihoods.

$$L_t=\frac{\prod_{t=1}^T p(m_t|s=+1)}{\prod_{t=1}^T p(m_t|s=-1)}$$

The above tells us the likelihood of the measurements if $s = 1$ divided by the likelihood of the measurements if $s = -1$.

It is convenient to take the _log_ of this likelihood ratio, converting the products to sums:

$$S_t = \log L_t = \sum_{t=1}^T \log \frac{p(m_t|s=+1)}{p(m_t|s=-1)} \tag{1}$$
We can name each term in the sum as
$$\Delta_t= \log \frac{p(m_t|s=+1)}{p(m_t|s=-1)}$$
Due to the independence of measurements, this can be calculated recursively _online_ as new data points arrive:

$$ S_t =  S_{t-1} + \Delta_t \tag{2}$$
where we update our log-likelihood ratio $S_t$ by $\Delta_t$ every time we see a new measurement $m_t$.

We will use $S_t$ to make our decisions! If $S_t$ is positive, the likelihood of $s = 1$ is higher. If it is negative, the likelihood of $s = -1$ is higher. We need to figure out when we make our decision though, as $S_t$ can change with each new measurement.

A rule for making a decision can be implemented in two ways:

1. Fixed time (Section 2): Stop collecting data after a predetermined number of measurements $t$, and accept the hypothesis that $s=+1$ if $S_t>0$, otherwise accept $s=-1$ if $S_t<0$ (and choose randomly if $S_t=0$). The significance level or desired error rate $\alpha$ can then be determined as $\alpha = (1+\exp(|S_t|))^{-1}$.

2. Confidence threshold (Bonus Section 1): Choose an acceptable error rate $\alpha$. Then accept the hypothesis $s=1$ when $S_t \ge b=\log \frac{1-\alpha}{\alpha}$, analogously accept $s=-1$ when $S_t\le -b$, and keep collecting data until one of those confidence thresholds is reached. Historical note: this is the rule that Alan Turing used to break the Enigma code and win World War II!

## Section 1.3: SPRT as a Drift Diffusion Model (DDM)

```python
#@title Video 4: SPRT and the Random Dot Motion Task
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="7WBB4M_Vf58", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

The evidence favoring the two latent states is random, but according to our model it will weakly favor one hypothesis over another. The accumulation of evidence will thus "drift" toward one outcome, while "diffusing" in random directions, hence the term "drift-diffusion model" (DDM). The process is most likely (but not guaranteed) to reach the correct outcome eventually. We can do a little math below to show that the update $\Delta_t$ to the log-likelihood ratio is a gaussian random number. You can derive this yourself, filling in the steps below, or skip to the end result.

**Bonus exercise: derive Drift Diffusion Model from SPRT**

Assume measurements are Gaussian-distributed with different means depending on the discrete latent variable $s$:
$$p(m|s=\pm 1) = \mathcal{N}\left(\mu_\pm,\sigma^2\right)=\frac{1}{\sqrt{2\pi\sigma^2}}\exp{\left[-\frac{(m-\mu_\pm)^2}{2\sigma^2}\right]}$$

In the log likelihood ratio for a single data point $m_i$, the normalizations cancel to give
$$\Delta_t=\log \frac{p(m_t|s=+1)}{p(m_t|s=-1)} = \frac{1}{2\sigma^2}\left[-\left(m_t-\mu_+\right)^2 + (m_t-\mu_-)^2\right] \tag{5}$$

It's convenient to rewrite $m=\mu_\pm + \sigma \epsilon$, where $\epsilon\sim \mathcal{N}(0,1)$ is a standard Gaussian variable with zero mean and unit variance. (Why does this give the correct probability for $m$?). The preceding formula can then be rewritten as
$$\Delta_t = \frac{1}{2\sigma^2}\left( -((\mu_\pm+\sigma\epsilon)-\mu_+)^2 + ((\mu_\pm+\sigma\epsilon)-\mu_-)^2\right) \tag{5}$$
Let's assume that $s=+1$ so $\mu_\pm=\mu_+$ (if $s=-1$ then the result is the same with a reversed sign). In that case, the means in the first term $m_t-\mu_+$ cancel, leaving
$$\Delta_t = \frac{(\mu_+-\mu_-)^2}{2\sigma^2}+\frac{\mu_+-\mu_-}{\sigma}\epsilon \tag{5}$$
where the first term is the constant *drift*, and the second term is the random *diffusion*. Adding these $\Delta_t$ over time gives a biased random walk known as the Drift Diffusion Model, $S_t=\sum_t \Delta_t$. The log-likelihood ratio is then normally distributed with a time-dependent mean and variance,
$$S_t\sim\mathcal{N}\left(\tfrac{1}{2}\frac{\delta\mu^2}{\sigma^2}t,\ \frac{\delta\mu^2}{\sigma^2}t\right)$$
where $\delta\mu=\mu_+-\mu_-$. The mean and the variance both increase linearly with time, so the standard deviation grows more slowly, as only $\sqrt{t}$. This means that the distributions becomes more and more distinct as evidence is acquired over time. You will simulate this process below.

**Neural application**

Neural responses in lateral intraparietal cortex (LIP) to the random-dots kinematogram has been well-described by this drift-diffusion process (Huk and Shadlen 2005), suggesting that these neurons gradually integrate evidence. Interestingly there is also a more recent competing hypothesis that neural activity jumps from low to high at random latent times, such that on average it looks like a gradual ramping (Latimer et al 2015). Scientific evidence about these processes are judged by how well the corresponding Hidden Markov Models fit the data!

---
# Section 2: DDM with fixed-time stopping rule

## Section 2.1: Simulation of DDM with fixed-time stopping rule

```python
#@title Video 5: Simulate the DDM with a fixed-time stopping rule
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="9WNAZnEa64Y", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

### Coding Exercise 1: Simulating an SPRT model

Assume we are performing a random dot motion task and at each time we see a moving dot with a sensory measurement $m_t$ of velocity. All data points are sampled from the same distribution $p$, which is either $p_+=\mathcal{N}\left(\mu,\sigma^2\right)$ or $p_-=\mathcal{N}\left(-\mu,\sigma^2\right)$, depending on which direction the dots are moving in. Let's now generate some simulated data under this setting and perform SPRT using the fixed time stopping rule.

In this exercise, without loss of generality, we assume the true data-generating model is $p_+$.

We will implement a function `simulate_SPRT_fixedtime`, which will generate measurements based on $\mu$, $\sigma$, and the true state. It will then accumulate evidence and output a decision on the state. We will use the helper function `log_likelihood_ratio`, implemented in the next cell, which computes the log of the likelihood of the state being 1 divided by the likelihood of the state being -1.

We will then run and visualize 10 simulations of evidence accumulation and decision.

```python
# @markdown Execute this cell to enable the helper function log_likelihood_ratio

def log_likelihood_ratio(Mvec, p0, p1):
  """Given a sequence(vector) of observed data, calculate the log of
  likelihood ratio of p1 and p0

  Args:
    Mvec (numpy vector):           A vector of scalar measurements
    p0 (Gaussian random variable): A normal random variable with `logpdf'
                                    method
    p1 (Gaussian random variable): A normal random variable with `logpdf`
                                    method

  Returns:
    llvec: a vector of log likelihood ratios for each input data point
  """
  return p1.logpdf(Mvec) - p0.logpdf(Mvec)
```

```python
def simulate_SPRT_fixedtime(sigma, stop_time, true_dist = 1):
  """Simulate a Sequential Probability Ratio Test with fixed time stopping
  rule. Two observation models are 1D Gaussian distributions N(1,sigma^2) and
  N(-1,sigma^2).

  Args:
    sigma (float): Standard deviation of observation models
    stop_time (int): Number of samples to take before stopping
    true_dist (1 or -1): Which state is the true state.

  Returns:
    evidence_history (numpy vector): the history of cumulated evidence given
                                      generated data
    decision (int): 1 for s = 1, -1 for s = -1
    Mvec (numpy vector): the generated sequences of measurement data in this trial
  """

  #################################################
  ## TODO for students ##
  # Fill out function and remove
  raise NotImplementedError("Student exercise: complete simulate_SPRT_fixedtime")
  #################################################

  # Set means of observation distributions
  mu_pos = 1.0
  mu_neg = -1.0

  # Make observation distributions
  p_pos = stats.norm(loc = mu_pos, scale = sigma)
  p_neg = stats.norm(loc = mu_neg, scale = sigma)

  # Generate a random sequence of measurements
  if true_dist == 1:
    Mvec = p_pos.rvs(size = stop_time)
  else:
    Mvec = p_neg.rvs(size = stop_time)

  # Calculate log likelihood ratio for each measurement (delta_t)
  ll_ratio_vec = log_likelihood_ratio(Mvec, p_neg, p_pos)

  # Calculate cumulated evidence (S) given a vector of individual evidences (hint: np.cumsum)
  evidence_history = ...

  # Make decision
  if evidence_history[-1] > 0:
    # Decision given positive S_t (last value of evidence history)
    decision = ...
  elif evidence_history[-1] < 0:
    # Decision given negative S_t (last value of evidence history)
    decision = ...
  else:
    # Random decision if S_t is 0
    decision = np.random.randint(2)

  return evidence_history, decision, Mvec

# Set random seed
np.random.seed(100)

# Set model parameters
sigma = 3.5  # standard deviation for p+ and p-
num_sample = 10  # number of simulations to run
stop_time = 150 # number of steps before stopping

simulate_and_plot_SPRT_fixedtime(sigma, stop_time, num_sample)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_HiddenDynamics/solutions/W3D2_Tutorial1_Solution_85d9b919.py)

*Example output:*

### Interactive Demo 2.1: Trajectories under the fixed-time stopping rule

In the following demo, you can change the noise level in the observation model (sigma) and the number of time steps before stopping (stop_time) using the sliders. You will then observe 10 simulations with those parameters.

1.   Are you more likely to make the wrong decision (choose the incorrect state) with high or low noise?
2. What happens when sigma is very small? Why?
3.   Are you more likely to make the wrong decision (choose the incorrect state) with fewer or more time steps before stopping?

```python
#@markdown Make sure you execute this cell to enable the widget!
def simulate_SPRT_fixedtime(sigma, stop_time, true_dist = 1):
  """Simulate a Sequential Probability Ratio Test with fixed time stopping
  rule. Two observation models are 1D Gaussian distributions N(1,sigma^2) and
  N(-1,sigma^2).

  Args:
    sigma (float): Standard deviation of observation models
    stop_time (int): Number of samples to take before stopping
    true_dist (1 or -1): Which state is the true state.

  Returns:
    evidence_history (numpy vector): the history of cumulated evidence given
                                      generated data
    decision (int): 1 for s = 1, -1 for s = -1
    Mvec (numpy vector): the generated sequences of measurement data in this trial
  """

  # Set means of observation distributions
  mu_pos = 1.0
  mu_neg = -1.0

  # Make observation distributions
  p_pos = stats.norm(loc = mu_pos, scale = sigma)
  p_neg = stats.norm(loc = mu_neg, scale = sigma)

  # Generate a random sequence of measurements
  if true_dist == 1:
    Mvec = p_pos.rvs(size = stop_time)
  else:
    Mvec = p_neg.rvs(size = stop_time)

  # Calculate log likelihood ratio for each measurement (delta_t)
  ll_ratio_vec = log_likelihood_ratio(Mvec, p_neg, p_pos)

  # Calculate cumulated evidence (S) given a vector of individual evidences (hint: np.cumsum)
  evidence_history = np.cumsum(ll_ratio_vec)

  # Make decision
  if evidence_history[-1] > 0:
    # Decision given positive S_t (last value of evidence history)
    decision = 1
  elif evidence_history[-1] < 0:
    # Decision given negative S_t (last value of evidence history)
    decision = -1
  else:
    # Random decision if S_t is 0
    decision = np.random.randint(2)

  return evidence_history, decision, Mvec

np.random.seed(100)
num_sample = 10

@widgets.interact
def plot(sigma=(0.05, 10.0, 0.05), stop_time=(5, 500, 1)):
  simulate_and_plot_SPRT_fixedtime(sigma, stop_time, num_sample, verbose=False)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_HiddenDynamics/solutions/W3D2_Tutorial1_Solution_8ab841e6.py)

## Section 2.2: Accuracy vs stopping time

If you stop taking samples too early, (e.g., make a decision after only seeing 5 samples), or there's a huge amount of observation noise that buries the signal, you are likely to be driven by observation noise to a negative cumulated log likelihood ratio and thus make a wrong decision. You could get a sense of this by increasing noise level or decreasing stopping time in the last exercise.

Now let's look at how decision accuracy varies with the number of samples we see quantitatively.Accuracy is simply defined as the proportion of correct trials across our repeated simulations: $\frac{\# \textrm{ correct decisions}}{\# \textrm{ total simulation runs}}$.

### Coding Exercise 2: The Speed/Accuracy Tradeoff

We will fix our observation noise level. In this exercise you will implement a function to run several repeated simulations for a certain stopping time and calculate the average decision accuracy. We will then visualize the relation average decision accuracy and stopping time.

```python
def simulate_accuracy_vs_stoptime(sigma, stop_time_list, num_sample):
  """Calculate the average decision accuracy vs. stopping time by running
  repeated SPRT simulations for each stop time.

  Args:
      sigma (float): standard deviation for observation model
      stop_list_list (list-like object): a list of stopping times to run over
      num_sample (int): number of simulations to run per stopping time

  Returns:
      accuracy_list: a list of average accuracies corresponding to input
                      `stop_time_list`
      decisions_list: a list of decisions made in all trials
  """

  #################################################
  ## TODO for students##
  # Fill out function and remove
  raise NotImplementedError("Student exercise: complete simulate_accuracy_vs_stoptime")
  #################################################

  # Determine true state (1 or -1)
  true_dist = 1

  # Set up tracker of accuracy and decisions
  accuracies = np.zeros(len(stop_time_list),)
  decisions_list = []

  # Loop over stop times
  for i_stop_time, stop_time in enumerate(stop_time_list):

    # Set up tracker of decisions for this stop time
    decisions = np.zeros((num_sample,))

    # Loop over samples
    for i in range(num_sample):

      # Simulate run for this stop time (hint: last exercise)
      _, decision, _= ...

      # Log decision
      decisions[i] = decision

    # Calculate accuracy
    accuracies[i_stop_time] = ...

    # Log decisions
    decisions_list.append(decisions)

  return accuracies, decisions_list

# Set random seed
np.random.seed(100)

# Set parameters of model
sigma = 4.65  # standard deviation for observation noise
num_sample = 200  # number of simulations to run for each stopping time
stop_time_list = np.arange(1, 150, 10) # Array of stopping times to use

# Calculate accuracies for each stop time
accuracies, _ = simulate_accuracy_vs_stoptime(sigma, stop_time_list,
                                                   num_sample)

# Visualize
plot_accuracy_vs_stoptime(stop_time_list, accuracies)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_HiddenDynamics/solutions/W3D2_Tutorial1_Solution_d5a87445.py)

*Example output:*

### Interactive Demo 2.2: Accuracy versus stop-time

In the following demo, we will show the same visualization as in the previous exercise, but you will be able to vary the noise level `sigma` of the observation distributions. First think and discuss,

1.   What do you expect low levels of noise to do to the accuracy vs stop time plot?
2.   What do you expect high levels of noise to do to the accuracy vs stop time plot?

Play with the demo and see if you were correct or not.

```python
#@markdown Make sure you execute this cell to enable the widget!
def simulate_accuracy_vs_stoptime(sigma, stop_time_list, num_sample):
  """Calculate the average decision accuracy vs. stopping time by running
  repeated SPRT simulations for each stop time.

  Args:
      sigma (float): standard deviation for observation model
      stop_list_list (list-like object): a list of stopping times to run over
      num_sample (int): number of simulations to run per stopping time

  Returns:
      accuracy_list: a list of average accuracies corresponding to input
                      `stop_time_list`
      decisions_list: a list of decisions made in all trials
  """

  # Determine true state (1 or -1)
  true_dist = 1

  # Set up tracker of accuracy and decisions
  accuracies = np.zeros(len(stop_time_list),)
  decisions_list = []

  # Loop over stop times
  for i_stop_time, stop_time in enumerate(stop_time_list):

    # Set up tracker of decisions for this stop time (hint: last exercise)
    decisions = np.zeros((num_sample,))

    # Loop over samples
    for i in range(num_sample):

      # Simulate run for this stop time
      _, decision, _= simulate_SPRT_fixedtime(sigma, stop_time, true_dist)

      # Log decision
      decisions[i] = decision

    # Calculate accuracy
    accuracies[i_stop_time] = np.sum(decisions == true_dist) / decisions.shape[0]

    # Log decisions
    decisions_list.append(decisions)

  return accuracies, decisions_list

np.random.seed(100)
num_sample = 100
stop_time_list = np.arange(1, 150, 10)

@widgets.interact
def plot(sigma=(0.05, 10.0, 0.05)):
 # Calculate accuracies for each stop time
  accuracies, _ = simulate_accuracy_vs_stoptime(sigma, stop_time_list,                                             num_sample)

  # Visualize
  plot_accuracy_vs_stoptime(stop_time_list, accuracies)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_HiddenDynamics/solutions/W3D2_Tutorial1_Solution_a956a168.py)

Please see Bonus Section 1 to learn about and work with a different stopping rule for DDMs: a fixed threshold on confidence.

---
# Summary

Good job! By simulating Drift Diffusion Models to perform decision making, you have learnt how to

1. Calculate individual sample evidence as the log likelihood ratio of two candidate models, accumulate evidence from new data points, and make decision based on current evidence in `Exercise 1`
2. Run repeated simulations to get an estimate of decision accuraries in `Exercise 2`
3. Implement the thresholding stopping rule where we can control our error rate by taking adequate amounts of data, and calculate the evidence threshold from desired error rate in `Exercise 3`
4. Explore and gain intuition about the speed/accuracy tradeoff for perceptual decision making in `Exercise 4`

---
# Bonus

## Bonus Section 1: DDM with fixed thresholds on confidence

```python
#@title Video 6: Fixed threshold on confidence
from IPython.display import YouTubeVideo
video = YouTubeVideo(id="E8lvgFeIGQM", width=854, height=480, fs=1)
print("Video available at https://youtu.be/" + video.id)
video
```

The next exercises consider a variant of the DDM with fixed confidence thresholds instead of fixed decision time. This may be a better description of neural integration. Please complete this material after you have finished the main content of all tutorials, if you would like extra information about this topic.

### Exercise 3: Simulating the DDM with fixed thresholds

In this exercise, we will use thresholding as our stopping rule and observe the behavior of the DDM.

With thresholding stopping rule, we define a desired error rate and will continue making measurements until that error rate is reached. Experimental evidence suggested that evidence accumulation and thresholding stopping strategy happens at neuronal level (see [this article](https://www.annualreviews.org/doi/full/10.1146/annurev.neuro.29.051605.113038) for further reading).

* Complete the function `threshold_from_errorrate` to calculate the evidence threshold from desired error rate $\alpha$ as described in the formulas below. The evidence thresholds $th_1$ and $th_0$ for $p_+$ and $p_-$ are opposite of each other as shown below, so you can just return the absolute value.
$$
\begin{align}
 th_{L} &= \log \frac{\alpha}{1-\alpha} &= -th_{R} \\
 th_{R} &= \log \frac{1-\alpha}{\alpha} &= -th{_1}\\
 \end{align}
 $$

* Complete the function `simulate_SPRT_threshold` to simulate an SPRT with thresholding stopping rule given noise level and desired threshold

* Run repeated simulations for a given noise level and a desired error rate visualize the DDM traces using our provided code

```python
def simulate_SPRT_threshold(sigma, threshold , true_dist=1):
  """Simulate a Sequential Probability Ratio Test with thresholding stopping
  rule. Two observation models are 1D Gaussian distributions N(1,sigma^2) and
  N(-1,sigma^2).

  Args:
    sigma (float): Standard deviation
    threshold (float): Desired log likelihood ratio threshold to achieve
                        before making decision

  Returns:
    evidence_history (numpy vector): the history of cumulated evidence given
                                      generated data
    decision (int): 1 for pR, 0 for pL
    data (numpy vector): the generated sequences of data in this trial
  """
  muL = -1.0
  muR = 1.0

  pL = stats.norm(muL, sigma)
  pR = stats.norm(muR, sigma)

  has_enough_data = False

  data_history = []
  evidence_history = []
  current_evidence = 0.0

  # Keep sampling data until threshold is crossed
  while not has_enough_data:
    if true_dist == 1:
      Mvec = pR.rvs()
    else:
      Mvec = pL.rvs()

    ########################################################################
    # Insert your code here to:
    #      * Calculate the log-likelihood ratio for the new sample
    #      * Update the accumulated evidence
    raise NotImplementedError("`simulate_SPRT_threshold` is incomplete")
    ########################################################################
    # individual log likelihood ratios
    ll_ratio = log_likelihood_ratio(...)
    # cumulated evidence for this chunk
    evidence_history.append(...)
    # update the collection of all data
    data_history.append(Mvec)
    current_evidence = evidence_history[-1]

    # check if we've got enough data
    if abs(current_evidence) > threshold:
      has_enough_data = True

  data_history = np.array(data_history)
  evidence_history = np.array(evidence_history)

  # Make decision
  if evidence_history[-1] > 0:
    decision = 1
  elif evidence_history[-1] < 0:
    decision = 0
  else:
    decision = np.random.randint(2)

  return evidence_history, decision, data_history

np.random.seed(100)
sigma = 2.8
num_sample = 10
log10_alpha = -6.5 # log10(alpha)
alpha = np.power(10.0, log10_alpha)

################################################################################
# Un-comment the following code after completing this exercise
################################################################################
# simulate_and_plot_SPRT_fixedthreshold(sigma, num_sample, alpha)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_HiddenDynamics/solutions/W3D2_Tutorial1_Solution_f93723a3.py)

*Example output:*

### Interactive Demo: DDM with fixed threshold

**Suggestion**

* Play with difference values of `alpha` and `sigma` and observe how that affects the dynamics of Drift-Diffusion Model.

```python
#@title

#@markdown Make sure you execute this cell to enable the widget!
np.random.seed(100)
num_sample = 10

@widgets.interact
def plot(sigma=(0.05, 10.0, 0.05), log10_alpha=(-8, -1, .1)):
  alpha = np.power(10.0, log10_alpha)
  simulate_and_plot_SPRT_fixedthreshold(sigma, num_sample, alpha, verbose=False)
```

### Exercise 4: Speed/Accuracy Tradeoff Revisited

The faster you make a decision, the lower your accuracy often is. This phenomenon is known as the **speed/accuracy tradeoff**. Humans can make this tradeoff in a wide range of situations, and many animal species, including ants, bees, rodents, and monkeys also show similar effects.

To illustrate the speed/accuracy tradeoff under thresholding stopping rule, let's run some simulations under different thresholds and look at how average decision "speed" (1/length) changes with average decision accuracy. We use speed rather than accuracy because in real experiments, subjects can be incentivized to respond faster or slower; it's much harder to precisely control their decision time or error threshold.

* Complete the function `simulate_accuracy_vs_threshold` to simulate and compute average accuracies vs. average decision lengths for a list of error thresholds. You will need to supply code to calculate average decision 'speed' from the lengths of trials. You should also calculate the overall accuracy across these trials.

* We've set up a list of error thresholds. Run repeated simulations and collect average accuracy with average length for each error rate in this list, and use our provided code to visualize the speed/accuracy tradeoff. You should see a positive correlation between length and accuracy.

```python
def simulate_accuracy_vs_threshold(sigma, threshold_list, num_sample):
  """Calculate the average decision accuracy vs. average decision length by
  running repeated SPRT simulations with thresholding stopping rule for each
  threshold.

  Args:
      sigma (float): standard deviation for observation model
      threshold_list (list-like object): a list of evidence thresholds to run
                                          over
      num_sample (int): number of simulations to run per stopping time

  Returns:
      accuracy_list: a list of average accuracies corresponding to input
                      `threshold_list`
      decision_speed_list: a list of average decision speeds
  """
  decision_speed_list = []
  accuracy_list = []
  for threshold in threshold_list:
    decision_time_list = []
    decision_list = []
    for i in range(num_sample):
      # run simulation and get decision of current simulation
      _, decision, Mvec = simulate_SPRT_threshold(sigma, threshold)
      decision_time = len(Mvec)
      decision_list.append(decision)
      decision_time_list.append(decision_time)

    ########################################################################
    # Insert your code here to:
    #      * Calculate mean decision speed given a list of decision times
    #      * Hint: Think about speed as being inversely proportional
    #        to decision_length. If it takes 10 seconds to make one decision,
    #        our "decision speed" is 0.1 decisions per second.
    #      * Calculate the decision accuracy
    raise NotImplementedError("`simulate_accuracy_vs_threshold` is incomplete")
    ########################################################################
    # Calculate and store average decision speed and accuracy
    decision_speed = ...
    decision_accuracy = ...
    decision_speed_list.append(decision_speed)
    accuracy_list.append(decision_accuracy)

  return accuracy_list, decision_speed_list

################################################################################
# Un-comment the following code block after completing this exercise
################################################################################
# np.random.seed(100)
# sigma = 3.75
# num_sample = 200
# alpha_list = np.logspace(-2, -0.1, 8)
# threshold_list = threshold_from_errorrate(alpha_list)
# simulate_and_plot_accuracy_vs_threshold(sigma, threshold_list, num_sample)
```

[*Click for solution*](https://github.com/NeuromatchAcademy/course-content/tree/master//tutorials/W3D2_HiddenDynamics/solutions/W3D2_Tutorial1_Solution_656ae75a.py)

*Example output:*

### Interactive Demo: Speed/Accuracy with a threshold rule

**Suggestions**

* Play with difference values of  noise level `sigma` and observe how that affects the speed/accuracy tradeoff.

```python
#@title

#@markdown Make sure you execute this cell to enable the widget!
np.random.seed(100)
num_sample = 100
alpha_list = np.logspace(-2, -0.1, 8)
threshold_list = threshold_from_errorrate(alpha_list)

@widgets.interact
def plot(sigma=(0.05, 10.0, 0.05)):
  alpha = np.power(10.0, log10_alpha)
  simulate_and_plot_accuracy_vs_threshold(sigma, threshold_list, num_sample)
```


This notebook contains a brief illustration of the updated sky model. For details refer to Parker's thesis.

```python
import numpy as np
import astropy.units as u
# -- feasibgs --
from feasibgs import util as UT
from feasibgs import skymodel as Sky
from feasibgs import forwardmodel as FM
# -- desi --
import desimodel.io
import desisim.simexp
```

    Downloading http://maia.usno.navy.mil/ser7/finals2000A.all [Done]

```python
import matplotlib as mpl
import matplotlib.pyplot as pl
mpl.rcParams['text.usetex'] = True
mpl.rcParams['font.family'] = 'serif'
mpl.rcParams['axes.linewidth'] = 1.5
mpl.rcParams['axes.xmargin'] = 1
mpl.rcParams['xtick.labelsize'] = 'x-large'
mpl.rcParams['xtick.major.size'] = 5
mpl.rcParams['xtick.major.width'] = 1.5
mpl.rcParams['ytick.labelsize'] = 'x-large'
mpl.rcParams['ytick.major.size'] = 5
mpl.rcParams['ytick.major.width'] = 1.5
mpl.rcParams['legend.frameon'] = False
%matplotlib inline
```

```python
params = desimodel.io.load_desiparams()
wavemin = params['ccd']['b']['wavemin']
wavemax = params['ccd']['z']['wavemax']
print('%f < lambda < %f' % (wavemin, wavemax))

waves = np.arange(wavemin, wavemax, 0.2) * u.angstrom
config = desisim.simexp._specsim_config_for_wave((waves).to('Angstrom').value, specsim_config_file='desi')
surface_brightness_dict = config.load_table(config.atmosphere.sky, 'surface_brightness', as_dict=True)
```

    3533.000000 < lambda < 9913.000000

```python
fig = plt.figure(figsize=(15,5))
sub = fig.add_subplot(111)
sub.plot(waves, surface_brightness_dict['dark'], c='k', lw=0.5, label='UVES dark sky')
#sub.plot(uves_sky_bright[0], uves_sky_bright[1], c='C1', lw=0.5, label='UVES bright sky')
sub.legend(loc='upper right', fontsize=20, frameon=True)
sub.set_xlabel(r'Wavelength [$\AA$]', fontsize=25)
sub.set_xlim([3500., 1.e4])
sub.set_ylabel(r"surface brightness ($10^{-17} erg/cm^{2}/s/\AA/arcsec^2$)", fontsize=25)
sub.set_ylim([0., 10.])
```

Above is the default dark sky surface brightness currently used in `specsim` derived from UVES.

```python
desi = FM.SimulatorHacked(config, num_fibers=1, camera_output=True)
airmass = 1.4
alt = 42.18
sep = 69.
ill = 0.42
desi.atmosphere.airmass = airmass
desi.atmosphere.moon.moon_phase = np.arccos(2.*ill - 1)/np.pi #moon_phase/np.pi #np.arccos(2*moonfrac-1)/np.pi
desi.atmosphere.moon.moon_zenith = (90. - alt) * u.deg
desi.atmosphere.moon.separation_angle = sep * u.deg
```

```python
fig = plt.figure(figsize=(15,5))
sub = fig.add_subplot(111)
sub.plot(waves, surface_brightness_dict['dark'],
         c='k', lw=0.5, label='UVES dark sky')
sub.plot(waves, surface_brightness_dict['dark'] + desi.atmosphere.moon.surface_brightness,
         c='C1', lw=0.5, label=r'+Krisciunas $\&$ Schaefer (1991)')
sub.legend(loc='upper right', fontsize=20, frameon=True)
sub.set_xlabel(r'Wavelength [$\AA$]', fontsize=25)
sub.set_xlim([3500., 1.e4])
sub.set_ylabel(r"surface brightness ($10^{-17} erg/cm^{2}/s/\AA/arcsec^2$)", fontsize=25)
sub.set_ylim([0., 10.])
```

The `specsim` bright sky model combines the UVES dark sky with the moon model from Krisciunas & Schaefer (1991).
$$B_\mathrm{moon} = f(\rho) 10^{-0.4 (m + 16.57)} 10^{-0.4 k_V X_m}(1 - 10^{-0.4 k_V X})$$
where
$$m=-12.73 + 0.025|\alpha| + 4 \times 10^{-9} \alpha^4$$
and
$f(\rho)$ is the scattering function dominated by components from Rayleigh and Mie scattering:
$$ f(\rho) = f_R(\rho) + f_M(\rho) = 10^{5.36}(1.06 + cos(\rho))^2 + 10^{6.15 - (\rho/40)}$$

Above is what the sky surface brightness looks like for the following sky and lunar parameters:

- `airmass=1.4`
- moon altitude = $42.18$ degrees
- moon separation = $69.$ degrees
- moon illumination = $0.42$. This corresponds to a moon phase of $0.55$ or $99^o$.

```python
fakedesi = FM.fakeDESIspec()
```

```python
fig = plt.figure(figsize=(15,5))
sub = fig.add_subplot(111)
sub.plot(waves, surface_brightness_dict['dark'],
         c='k', lw=0.5, label='UVES dark sky')
sub.plot(waves, surface_brightness_dict['dark'] + desi.atmosphere.moon.surface_brightness,
         c='C1', lw=0.5, label=r'+Krisciunas $\&$ Schaefer (1991)')
sub.plot(waves, fakedesi._skySurfBright(waves, cond='bright'),
         c='gray', lw=0.5, label='BOSS bright sky')
sub.legend(loc='upper right', fontsize=20, frameon=True)
sub.set_xlabel(r'Wavelength [$\AA$]', fontsize=25)
sub.set_xlim([3500., 1.e4])
sub.set_ylabel(r"surface brightness ($10^{-17} erg/cm^{2}/s/\AA/arcsec^2$)", fontsize=25)
sub.set_ylim([0., 10.])
```

In the previous version of `feasiBGS` the bright sky model was a fixed (_i.e._ no parameter dependence). The bright sky surface brightness was derived from some fiducial BOSS bright sky flux. Above is how it compares to the `specsim` bright sky model.

```python
theta_sky = (airmass, 30., 10., 180., 4.86761390e+09, -25., 150., np.arccos(2.*ill - 1), ill, sep, alt)
sky_obj = Sky.skySpec_manual(*theta_sky)
```

    /Users/chang/anaconda2/lib/python2.7/site-packages/feasibgs/skymodel.py:270: FutureWarning: from_csv is deprecated. Please use read_csv(...) instead. Note that some of the default arguments are different, so please refer to the documentation for from_csv when changing your function calls
      coeffs = pd.DataFrame.from_csv(f)
    /Users/chang/anaconda2/lib/python2.7/site-packages/feasibgs/skymodel.py:285: SettingWithCopyWarning:
    A value is trying to be set on a copy of a slice from a DataFrame.
    Try using .loc[row_indexer,col_indexer] = value instead

    See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy
      self.coeffs[k] = np.array(self.coeffs[k])[self.wave_sort]

```python
fig = plt.figure(figsize=(15,5))
sub = fig.add_subplot(111)
sub.plot(waves, surface_brightness_dict['dark'],
         c='k', lw=0.5, label='UVES dark sky')
sub.plot(waves, surface_brightness_dict['dark'] + desi.atmosphere.moon.surface_brightness,
         c='C1', lw=0.5, label=r'+Krisciunas $\&$ Schaefer (1991)')
sub.plot(waves, fakedesi._skySurfBright(waves, cond='bright'),
         c='gray', lw=0.5, label='BOSS bright sky')
sub.plot(waves, sky_obj.surface_brightness(waves),
         c='r', lw=0.5, label=r'Parker Sky Model')
sub.legend(loc='upper right', fontsize=20, frameon=True)
sub.set_xlabel(r'Wavelength [$\AA$]', fontsize=25)
sub.set_xlim([3500., 1.e4])
sub.set_ylabel(r"surface brightness ($10^{-17} erg/cm^{2}/s/\AA/arcsec^2$)", fontsize=25)
sub.set_ylim([0., 10.])
```

Here's the updated bright sky model in `feasiBGS`. The sky model depends on the following parameters:
- airmass
- ecliptic latitude
- galactic latitude
- galactic longitude
- time of observation
- sun altitude
- sun separation
- moon phase
- moon illumination
- moon separation
- moon altitude

$$\begin{align}
I_{cont} (\lambda) &= [ c_{AM}(\lambda) X + c_{zodi}(\lambda) I_{ZODI}(\beta) + c_{ISL}(\lambda) I_{ISL}(l, b)\\
&+ c_{SF}(\lambda) I_{SF}(\mathrm{sfu})[MJD-i] + c_{seas}(\lambda) I_{seas}(month) \\
&+ c_{hour}(\lambda) I_{hour}(\mathrm{t.s.s})]\times \Delta T(X,\lambda)\\
&+ c_{twilight}(\lambda) I_\mathrm{twilight}(\alpha, \delta) \times e^{\tau_T(\lambda) X}\\
&+ c_{moon}(\lambda) I_{moon}(Alt_M, Ill_M, \delta_M) \times e^{\tau_M(\lambda) X} + c_{AC}
\end{align}$$

Above I compare the updated `feasibGS` bright sky model to the `specsim` bright sky model and the BOSS bright sky. I use the same lunar parameters for both `feasiBGS` and `specsim` models.

```python
desi = FM.SimulatorHacked(config, num_fibers=1, camera_output=True)
airmass = 1.4
alt = 42.18
sep = 69.
ill = 0.8
desi.atmosphere.airmass = airmass
desi.atmosphere.moon.moon_phase = np.arccos(2.*ill - 1)/np.pi #moon_phase/np.pi #np.arccos(2*moonfrac-1)/np.pi
desi.atmosphere.moon.moon_zenith = (90. - alt) * u.deg
desi.atmosphere.moon.separation_angle = sep * u.deg

theta_sky = (airmass, 30., 10., 180., 4.86761390e+09, -25., 150., np.arccos(2.*ill - 1), ill, sep, alt)
sky_obj = Sky.skySpec_manual(*theta_sky)
fig = plt.figure(figsize=(15,5))
sub = fig.add_subplot(111)
sub.plot(waves, surface_brightness_dict['dark'],
         c='k', lw=0.5, label='UVES dark sky')
sub.plot(waves, surface_brightness_dict['dark'] + desi.atmosphere.moon.surface_brightness,
         c='C1', lw=0.5, label=r'+Krisciunas $\&$ Schaefer (1991)')
sub.plot(waves, fakedesi._skySurfBright(waves, cond='bright'),
         c='gray', lw=0.5, label='BOSS bright sky')
sub.plot(waves, sky_obj.surface_brightness(waves),
         c='r', lw=0.5, label=r'Parker Sky Model')
sub.legend(loc='upper right', fontsize=20, frameon=True)
sub.set_xlabel(r'Wavelength [$\AA$]', fontsize=25)
sub.set_xlim([3500., 1.e4])
sub.set_ylabel(r"surface brightness ($10^{-17} erg/cm^{2}/s/\AA/arcsec^2$)", fontsize=25)
sub.set_ylim([0., 10.])
```

Above is the same comparison but for a different set of lunar parameters:
- airmass = `1.4`
- moon alt = `42.18`
- moon sep = `69.`
- moon ill = `0.8`

```python
desi = FM.SimulatorHacked(config, num_fibers=1, camera_output=True)
airmass = 1.1
alt = 30.
sep = 100.
ill = 0.9
desi.atmosphere.airmass = airmass
desi.atmosphere.moon.moon_phase = np.arccos(2.*ill - 1)/np.pi #moon_phase/np.pi #np.arccos(2*moonfrac-1)/np.pi
desi.atmosphere.moon.moon_zenith = (90. - alt) * u.deg
desi.atmosphere.moon.separation_angle = sep * u.deg

theta_sky = (airmass, 30., 10., 180., 4.86761390e+09, -25., 150., np.arccos(2.*ill - 1), ill, sep, alt)
sky_obj = Sky.skySpec_manual(*theta_sky)
fig = plt.figure(figsize=(15,5))
sub = fig.add_subplot(111)
sub.plot(waves, surface_brightness_dict['dark'],
         c='k', lw=0.5, label='UVES dark sky')
sub.plot(waves, surface_brightness_dict['dark'] + desi.atmosphere.moon.surface_brightness,
         c='C1', lw=0.5, label=r'+Krisciunas $\&$ Schaefer (1991)')
sub.plot(waves, fakedesi._skySurfBright(waves, cond='bright'),
         c='gray', lw=0.5, label='BOSS bright sky')
sub.plot(waves, sky_obj.surface_brightness(waves),
         c='r', lw=0.5, label=r'Parker Sky Model')
sub.legend(loc='upper right', fontsize=20, frameon=True)
sub.set_xlabel(r'Wavelength [$\AA$]', fontsize=25)
sub.set_xlim([3500., 1.e4])
sub.set_ylabel(r"surface brightness ($10^{-17} erg/cm^{2}/s/\AA/arcsec^2$)", fontsize=25)
sub.set_ylim([0., 10.])
```

Above is the same comparison but for a less birght set of lunar parameters (_i.e._ low moon altitude, large moon separation):
- airmass = `1.1`
- moon alt = `30.`
- moon sep = `100.`
- moon ill = `0.9`

```python
sky_obj = Sky.skySpec_manual(*theta_sky)
fig = plt.figure(figsize=(15,5))
sub = fig.add_subplot(111)
sub.plot(waves, sky_obj.surface_brightness(waves),
         c='k', lw=0.5, label=r'Parker Sky Model')
wl_sort = np.argsort(np.array(sky_obj.coeffs['wl']))
ww = 10.*np.array(sky_obj.coeffs['wl'])[wl_sort]

Icont = (1./np.pi * sky_obj._dT * sky_obj._Iadd_continuum)
sub.fill_between(ww, np.zeros(len(wl_sort)), np.array(Icont)[wl_sort], color='C5', label='Add.')
Icont0 = np.array(Icont)[wl_sort]
Icont += (1./np.pi * sky_obj._dT * sky_obj._Iairmass)
sub.fill_between(ww, Icont0, np.array(Icont)[wl_sort], color='C0', label='Air mass')
Icont0 = np.array(Icont)[wl_sort]
Icont += (1./np.pi * sky_obj._dT * sky_obj._Izodiacal)
sub.fill_between(ww, Icont0, np.array(Icont)[wl_sort], color='C1', label='Zodiacal')
Icont0 = np.array(Icont)[wl_sort]
Icont += (1./np.pi * sky_obj._dT * sky_obj._Iisl)
sub.fill_between(ww, Icont0, np.array(Icont)[wl_sort], color='C2', label='ISL')
Icont0 = np.array(Icont)[wl_sort]
Icont += (1./np.pi * sky_obj._dT * sky_obj._Isolar_flux)
sub.fill_between(ww, Icont0, np.array(Icont)[wl_sort], color='C3', label='Solar Flux')
Icont0 = np.array(Icont)[wl_sort]
Icont += (1./np.pi * sky_obj._dT * (sky_obj._Iseasonal + sky_obj._Ihourly))
sub.fill_between(ww, Icont0, np.array(Icont)[wl_sort], color='C4', label='Seasonal + Hourly')
Icont0 = np.array(Icont)[wl_sort]
Icont += (1./np.pi * sky_obj._Itwilight)
sub.fill_between(ww, Icont0, np.array(Icont)[wl_sort], color='C6', label='twilight')
Icont0 = np.array(Icont)[wl_sort]
Icont += (1./np.pi * sky_obj._Imoon)
sub.fill_between(ww, Icont0, np.array(Icont)[wl_sort], color='yellow', label='moon')
sub.legend(loc='upper right', fontsize=18, frameon=True)
sub.set_xlabel(r'Wavelength [$\AA$]', fontsize=25)
sub.set_xlim([3500., 1.e4])
sub.set_ylabel(r"surface brightness ($10^{-17} erg/cm^{2}/s/\AA/arcsec^2$)", fontsize=25)
sub.set_ylim([0., 10.])

print sky_obj.obs_time.datetime
```

Here's the different contributions to Parker's sky model continuum

```python

```


# Analysis of schemes for the diffusion equation

The numerical experiments in the sections [diffu:pde1:FE:experiments](#diffu:pde1:FE:experiments) and [diffu:pde1:theta:experiments](#diffu:pde1:theta:experiments)
reveal that there are some
numerical problems with the Forward Euler and Crank-Nicolson schemes:
sawtooth-like noise is sometimes present in solutions that are,
from a mathematical point of view, expected to be smooth.
This section presents a mathematical analysis that explains the
observed behavior and arrives at criteria for obtaining numerical
solutions that reproduce the qualitative properties of the exact
solutions. In short, we shall explain what is observed in
Figures [diffu:pde1:FE:fig:F=0.5](#diffu:pde1:FE:fig:F=0.5)-[diffu:pde1:CN:fig:F=10](#diffu:pde1:CN:fig:F=10).

## Properties of the solution

A particular characteristic of diffusive processes, governed
by an equation like

$$
\begin{equation}
u_t = \dfc u_{xx},
\label{diffu:pde1:eq} \tag{1}
\end{equation}
$$

is that the initial shape $u(x,0)=I(x)$ spreads out in space with
time, along with a decaying amplitude.  Three different examples will
illustrate the spreading of $u$ in space and the decay in time.

### Similarity solution

The diffusion equation ([1](#diffu:pde1:eq)) admits solutions
that depend on $\eta = (x-c)/\sqrt{4\dfc t}$ for a given value
of $c$. One particular solution
is

$$
\begin{equation}
u(x,t) = a\,\mbox{erf}(\eta) + b,
\label{diffu:pdf1:erf:sol} \tag{2}
\end{equation}
$$

where

$$
\begin{equation}
\mbox{erf}(\eta) = \frac{2}{\sqrt{\pi}}\int_0^\eta e^{-\zeta^2}d\zeta,
\label{diffu:analysis:erf:def} \tag{3}
\end{equation}
$$

is the *error function*, and $a$ and $b$ are arbitrary constants.
The error function lies in $(-1,1)$, is odd around $\eta =0$, and
goes relatively quickly to $\pm 1$:

$$
\begin{align*}
\lim_{\eta\rightarrow -\infty}\mbox{erf}(\eta) &=-1,\\
\lim_{\eta\rightarrow \infty}\mbox{erf}(\eta) &=1,\\
\mbox{erf}(\eta) &= -\mbox{erf}(-\eta),\\
\mbox{erf}(0) &=0,\\
\mbox{erf}(2) &=0.99532227,\\
\mbox{erf}(3) &=0.99997791
\thinspace .
\end{align*}
$$

As $t\rightarrow 0$, the error function approaches a step function centered
at $x=c$. For a diffusion problem posed on the unit interval $[0,1]$,
we may choose the step at $x=1/2$ (meaning $c=1/2$), $a=-1/2$, $b=1/2$.
Then

$$
\begin{equation}
u(x,t) = \frac{1}{2}\left(1 -
\mbox{erf}\left(\frac{x-\frac{1}{2}}{\sqrt{4\dfc t}}\right)\right) =
\frac{1}{2}\mbox{erfc}\left(\frac{x-\frac{1}{2}}{\sqrt{4\dfc t}}\right),
\label{diffu:analysis:pde1:step:erf:sol} \tag{4}
\end{equation}
$$

where we have introduced the *complementary error function*
$\mbox{erfc}(\eta) = 1-\mbox{erf}(\eta)$.
The solution ([4](#diffu:analysis:pde1:step:erf:sol))
implies the boundary conditions

$$
\begin{equation}
u(0,t) = \frac{1}{2}\left(1 - \mbox{erf}\left(\frac{-1/2}{\sqrt{4\dfc t}}\right)\right),
\label{diffu:analysis:pde1:p1:erf:uL} \tag{5}
\end{equation}
$$

$$
\begin{equation}
u(1,t) = \frac{1}{2}\left(1 - \mbox{erf}\left(\frac{1/2}{\sqrt{4\dfc t}}\right)\right)
\label{diffu:analysis:pde1:p1:erf:uR} \tag{6}
\thinspace .
\end{equation}
$$

For small enough $t$, $u(0,t)\approx 1$ and $u(1,t)\approx 0$, but as
$t\rightarrow\infty$, $u(x,t)\rightarrow 1/2$ on $[0,1]$.

### Solution for a Gaussian pulse

The standard diffusion equation $u_t = \dfc u_{xx}$ admits a
Gaussian function as solution:

$$
\begin{equation}
u(x,t) = \frac{1}{\sqrt{4\pi\dfc t}} \exp{\left({-\frac{(x-c)^2}{4\dfc t}}\right)}
\label{diffu:pde1:sol:Gaussian} \tag{7}
\thinspace .
\end{equation}
$$

At $t=0$ this is a Dirac delta function, so for computational
purposes one must start to view the solution at some time $t=t_\epsilon>0$.
Replacing $t$ by $t_\epsilon +t$ in ([7](#diffu:pde1:sol:Gaussian))
makes it easy to operate with a (new) $t$ that starts at $t=0$
with an initial condition with a finite width.
The important feature of ([7](#diffu:pde1:sol:Gaussian)) is that
the standard deviation $\sigma$ of a sharp initial Gaussian pulse
increases in time according to $\sigma = \sqrt{2\dfc t}$, making
the pulse diffuse and flatten out.

### Solution for a sine component

Also, ([1](#diffu:pde1:eq)) admits a solution of the form

$$
\begin{equation}
u(x,t) = Qe^{-at}\sin\left( kx\right)
\label{diffu:pde1:sol1} \tag{8}
\thinspace .
\end{equation}
$$

The parameters $Q$ and $k$ can be freely chosen, while
inserting ([8](#diffu:pde1:sol1)) in ([1](#diffu:pde1:eq)) gives the constraint

$$
a = -\dfc k^2
\thinspace .
$$

A very important feature is that the initial shape $I(x)=Q\sin\left( kx\right)$
undergoes a damping $\exp{(-\dfc k^2t)}$, meaning that
rapid oscillations in space, corresponding to large $k$, are very much
faster dampened than slow oscillations in space, corresponding to small
$k$. This feature leads to a smoothing of the initial condition with time.
(In fact, one can use a few steps of the diffusion equation as
a method for removing noise in signal processing.)
To judge how good a numerical method is, we may look at its ability to
smoothen or dampen the solution in the same way as the PDE does.

The following example illustrates the damping properties of
([8](#diffu:pde1:sol1)). We consider the specific problem

$$
\begin{align*}
u_t &= u_{xx},\quad x\in (0,1),\ t\in (0,T],\\
u(0,t) &= u(1,t) = 0,\quad t\in (0,T],\\
u(x,0) & = \sin (\pi x) + 0.1\sin(100\pi x)
\thinspace .
\end{align*}
$$

The initial condition has been chosen such that adding
two solutions like ([8](#diffu:pde1:sol1)) constructs
an analytical solution to the problem:

$$
\begin{equation}
u(x,t) = e^{-\pi^2 t}\sin (\pi x) + 0.1e^{-\pi^2 10^4 t}\sin (100\pi x)
\label{diffu:pde1:sol2} \tag{9}
\thinspace .
\end{equation}
$$

[Figure](#diffu:pde1:fig:damping) illustrates the rapid damping of
rapid oscillations $\sin (100\pi x)$ and the very much slower damping of the
slowly varying $\sin (\pi x)$ term. After about $t=0.5\cdot10^{-4}$ the rapid
oscillations do not have a visible amplitude, while we have to wait
until $t\sim 0.5$ before the amplitude of the long wave $\sin (\pi x)$
becomes very small.

Evolution of the solution of a diffusion problem: initial condition (upper left), 1/100 reduction of the small waves (upper right), 1/10 reduction of the long wave (lower left), and 1/100 reduction of the long wave (lower right).

## Analysis of discrete equations

A counterpart to ([8](#diffu:pde1:sol1)) is the complex representation
of the same function:

$$
u(x,t) = Qe^{-at}e^{ikx},
$$

where $i=\sqrt{-1}$ is the imaginary unit.
We can add such functions, often referred to as wave components,
to make a Fourier representation
of a general solution of the diffusion equation:

$$
\begin{equation}
u(x,t) \approx \sum_{k\in K} b_k e^{-\dfc k^2t}e^{ikx},
\label{diffu:pde1:u:Fourier} \tag{10}
\end{equation}
$$

where $K$ is a set of an infinite number of $k$ values needed to construct
the solution. In practice, however, the series is truncated and
$K$ is a finite set of $k$ values
needed to build a good approximate solution.
Note that ([9](#diffu:pde1:sol2)) is a special case of
([10](#diffu:pde1:u:Fourier)) where $K=\{\pi, 100\pi\}$, $b_{\pi}=1$,
and $b_{100\pi}=0.1$.

The amplitudes $b_k$ of the individual Fourier waves must be determined
from the initial condition. At $t=0$ we have $u\approx\sum_kb_k\exp{(ikx)}$
and find $K$ and $b_k$ such that

$$
\begin{equation}
I(x) \approx \sum_{k\in K} b_k e^{ikx}\thinspace .
\label{_auto1} \tag{11}
\end{equation}
$$

(The relevant formulas for $b_k$ come from Fourier analysis, or
equivalently, a least-squares method for approximating $I(x)$
in a function space with basis $\exp{(ikx)}$.)

Much insight about the behavior of numerical methods can be obtained
by investigating how a wave component $\exp{(-\dfc k^2
t)}\exp{(ikx)}$ is treated by the numerical scheme. mathcal{I}_t appears that
such wave components are also solutions of the schemes, but the
damping factor $\exp{(-\dfc k^2 t)}$ varies among the schemes.  To
ease the forthcoming algebra, we write the damping factor as
$A^n$. The exact amplification factor corresponding to $A$ is $\Aex =
\exp{(-\dfc k^2\Delta t)}$.

## Analysis of the finite difference schemes

We have seen that a general solution of the diffusion equation
can be built as a linear combination of basic components

$$
e^{-\dfc k^2t}e^{ikx} \thinspace .
$$

A fundamental question is whether such components are also solutions of
the finite difference schemes. This is indeed the case, but the
amplitude $\exp{(-\dfc k^2t)}$ might be modified (which also happens when
solving the ODE counterpart $u'=-\dfc u$).
We therefore look for numerical solutions of the form

$$
\begin{equation}
u^n_q = A^n e^{ikq\Delta x} = A^ne^{ikx},
\label{diffu:pde1:analysis:uni} \tag{12}
\end{equation}
$$

where the amplification factor $A$
must be determined by inserting the component into an actual scheme.
Note that $A^n$ means $A$ raised to the power of $n$, $n$ being the
index in the time mesh, while the superscript $n$ in $u^n_q$ just
denotes $u$ at time $t_n$.

### Stability

The exact amplification factor is $\Aex=\exp{(-\dfc^2 k^2\Delta t)}$.
We should therefore require $|A| < 1$ to have a decaying numerical
solution as well. If
$-1\leq A<0$, $A^n$ will change sign from time level to
time level, and we get stable, non-physical oscillations in the numerical
solutions that are not present in the exact solution.

### Accuracy

To determine how accurately a finite difference scheme treats one
wave component ([12](#diffu:pde1:analysis:uni)), we see that the basic
deviation from the exact solution is reflected in how well
$A^n$ approximates $\Aex^n$,
or how well $A$ approximates $\Aex$.
We can plot $\Aex$ and the various expressions for $A$, and we can
make Taylor expansions of $A/\Aex$ to see the error more analytically.

### Truncation error

As an alternative to examining the accuracy of the damping of a wave
component, we can perform a general truncation error analysis as
explained in "Truncation error analysis": ""
[[Langtangen_deqbook_trunc]](#Langtangen_deqbook_trunc). Such results are more general, but
less detailed than what we get from the wave component analysis.  The
truncation error can almost always be computed and represents the
error in the numerical model when the exact solution is substituted
into the equations. In particular, the truncation error analysis tells
the order of the scheme, which is of fundamental importance when
verifying codes based on empirical estimation of convergence rates.

## Analysis of the Forward Euler scheme

The Forward Euler finite difference scheme for $u_t = \dfc u_{xx}$ can
be written as

$$
[D_t^+ u = \dfc D_xD_x u]^n_q\thinspace .
$$

Inserting a wave component ([12](#diffu:pde1:analysis:uni))
in the scheme demands calculating the terms

$$
e^{ikq\Delta x}[D_t^+ A]^n = e^{ikq\Delta x}A^n\frac{A-1}{\Delta t},
$$

and

$$
A^nD_xD_x [e^{ikx}]_q = A^n\left( - e^{ikq\Delta x}\frac{4}{\Delta x^2}
\sin^2\left(\frac{k\Delta x}{2}\right)\right)
\thinspace .
$$

Inserting these terms in the discrete equation and
dividing by $A^n e^{ikq\Delta x}$ leads to

$$
\frac{A-1}{\Delta t} = -\dfc \frac{4}{\Delta x^2}\sin^2\left(
\frac{k\Delta x}{2}\right),
$$

and consequently

$$
\begin{equation}
A = 1 -4F\sin^2 p
\label{_auto2} \tag{13}
\end{equation}
$$

where

$$
\begin{equation}
F = \frac{\dfc\Delta t}{\Delta x^2}
\label{_auto3} \tag{14}
\end{equation}
$$

is the *numerical Fourier number*, and $p=k\Delta x/2$.
The complete numerical solution is then

$$
\begin{equation}
u^n_q = \left(1 -4F\sin^2 p\right)^ne^{ikq\Delta x}
\thinspace .
\label{_auto4} \tag{15}
\end{equation}
$$

### Stability

We easily see that $A\leq 1$. However, the $A$ can be less than $-1$,
which will lead
to growth of a numerical wave component. The criterion $A\geq -1$ implies

$$
4F\sin^2 (p/2)\leq 2
\thinspace .
$$

The worst case is when $\sin^2 (p/2)=1$, so a sufficient criterion for
stability is

$$
\begin{equation}
F\leq {\frac{1}{2}},
\label{_auto5} \tag{16}
\end{equation}
$$

or expressed as a condition on $\Delta t$:

$$
\begin{equation}
\Delta t\leq \frac{\Delta x^2}{2\dfc}\thinspace .
\label{_auto6} \tag{17}
\end{equation}
$$

Note that halving the spatial mesh size, $\Delta x \rightarrow {\frac{1}{2}}
\Delta x$, requires $\Delta t$ to be reduced by a factor of $1/4$.
The method hence becomes very expensive for fine spatial meshes.

### Accuracy

Since $A$ is expressed in terms of $F$ and the parameter we now call
$p=k\Delta x/2$, we should also express $\Aex$ by $F$ and $p$. The exponent
in $\Aex$ is $-\dfc k^2\Delta t$, which equals $-F k^2\Delta x^2=-F4p^2$.
Consequently,

$$
\Aex = \exp{(-\dfc k^2\Delta t)} = \exp{(-4Fp^2)}
\thinspace .
$$

All our $A$ expressions as well as $\Aex$ are now functions of the two
dimensionless parameters $F$ and $p$.

Computing
the Taylor series expansion of $A/\Aex$ in terms of $F$
can easily be done with aid of `sympy`:

```python
def A_exact(F, p):
    return exp(-4*F*p**2)

def A_FE(F, p):
    return 1 - 4*F*sin(p)**2

from sympy import *
F, p = symbols('F p')
A_err_FE = A_FE(F, p)/A_exact(F, p)
print(A_err_FE.series(F, 0, 6))
```

    1 + F*(4*p**2 - 4*sin(p)**2) + F**2*(8*p**4 - 16*p**2*sin(p)**2) + F**3*(32*p**6/3 - 32*p**4*sin(p)**2) + F**4*(32*p**8/3 - 128*p**6*sin(p)**2/3) + F**5*(128*p**10/15 - 128*p**8*sin(p)**2/3) + O(F**6)

The result is

$$
\frac{A}{\Aex} = 1 - 4 F \sin^{2}p + 2F p^{2} - 16F^{2} p^{2} \sin^{2}p + 8 F^{2} p^{4} + \cdots
$$

Recalling that $F=\dfc\Delta t/\Delta x^2$, $p=k\Delta x/2$, and that
$\sin^2p\leq 1$, we
realize that the dominating terms in $A/\Aex$ are at most

$$
1 - 4\dfc \frac{\Delta t}{\Delta x^2} +
\dfc\Delta t - 4\dfc^2\Delta t^2
+ \dfc^2 \Delta t^2\Delta x^2 + \cdots
\thinspace .
$$

### Truncation error

We follow the theory explained in
"Truncation error analysis": ""
[[Langtangen_deqbook_trunc]](#Langtangen_deqbook_trunc). The recipe is to set up the
scheme in operator notation and use formulas from
"Overview of leading-order error terms in finite difference formulas": ""
[[Langtangen_deqbook_trunc]](#Langtangen_deqbook_trunc) to derive an expression for
the residual. The details are documented in
"Linear diffusion equation in 1D": ""
[[Langtangen_deqbook_trunc]](#Langtangen_deqbook_trunc). We end up with a truncation error

$$
R^n_i = \Oof{\Delta t} + \Oof{\Delta x^2}\thinspace .
$$

Although this is not the true error $\uex(x_i,t_n) - u^n_i$, it indicates
that the true error is of the form

$$
E = C_t\Delta t + C_x\Delta x^2
$$

for two unknown constants $C_t$ and $C_x$.

## Analysis of the Backward Euler scheme

Discretizing $u_t = \dfc u_{xx}$ by a Backward Euler scheme,

$$
[D_t^- u = \dfc D_xD_x u]^n_q,
$$

and inserting a wave component ([12](#diffu:pde1:analysis:uni)),
leads to calculations similar to those arising from the Forward Euler scheme,
but since

$$
e^{ikq\Delta x}[D_t^- A]^n = A^ne^{ikq\Delta x}\frac{1 - A^{-1}}{\Delta t},
$$

we get

$$
\frac{1-A^{-1}}{\Delta t} = -\dfc \frac{4}{\Delta x^2}\sin^2\left(
\frac{k\Delta x}{2}\right),
$$

and then

$$
\begin{equation}
A = \left(1  + 4F\sin^2p\right)^{-1}
\label{diffu:pde1:analysis:BE:A} \tag{18}
\thinspace .
\end{equation}
$$

The complete numerical solution can be written

$$
\begin{equation}
u^n_q = \left(1  + 4F\sin^2 p\right)^{-n}
e^{ikq\Delta x} \thinspace .
\label{_auto7} \tag{19}
\end{equation}
$$

### Stability

We see from ([18](#diffu:pde1:analysis:BE:A)) that $0<A<1$, which means
that all numerical wave components are stable and non-oscillatory
for any $\Delta t >0$.

### Truncation error

The derivation of the truncation error for the Backward Euler scheme is almost
identical to that for the Forward Euler scheme. We end up with

$$
R^n_i = \Oof{\Delta t} + \Oof{\Delta x^2}\thinspace .
$$

## Analysis of the Crank-Nicolson scheme

The Crank-Nicolson scheme can be written as

$$
[D_t u = \dfc D_xD_x \overline{u}^x]^{n+\frac{1}{2}}_q,
$$

or

$$
[D_t u]^{n+\frac{1}{2}}_q = \frac{1}{2}\dfc\left( [D_xD_x u]^{n}_q +
[D_xD_x u]^{n+1}_q\right)
\thinspace .
$$

Inserting ([12](#diffu:pde1:analysis:uni)) in the time derivative approximation
leads to

$$
[D_t A^n e^{ikq\Delta x}]^{n+\frac{1}{2}} = A^{n+\frac{1}{2}} e^{ikq\Delta x}\frac{A^{\frac{1}{2}}-A^{-\frac{1}{2}}}{\Delta t} = A^ne^{ikq\Delta x}\frac{A-1}{\Delta t}
\thinspace .
$$

Inserting ([12](#diffu:pde1:analysis:uni)) in the other terms
and dividing by
$A^ne^{ikq\Delta x}$ gives the relation

$$
\frac{A-1}{\Delta t} = -\frac{1}{2}\dfc\frac{4}{\Delta x^2}
\sin^2\left(\frac{k\Delta x}{2}\right)
(1 + A),
$$

and after some more algebra,

$$
\begin{equation}
A = \frac{ 1 - 2F\sin^2p}{1 + 2F\sin^2p}
\thinspace .
\label{_auto8} \tag{20}
\end{equation}
$$

The exact numerical solution is hence

$$
\begin{equation}
u^n_q = \left(\frac{ 1 - 2F\sin^2p}{1 + 2F\sin^2p}\right)^ne^{ikq\Delta x}
\thinspace .
\label{_auto9} \tag{21}
\end{equation}
$$

### Stability

The criteria $A>-1$ and $A<1$ are fulfilled for any $\Delta t >0$.
Therefore, the solution cannot grow, but it will oscillate if
$1-2F\sin^p < 0$. To avoid such non-physical oscillations, we must demand
$F\leq\frac{1}{2}$.

### Truncation error

The truncation error is derived in
"Linear diffusion equation in 1D": ""
[[Langtangen_deqbook_trunc]](#Langtangen_deqbook_trunc):

$$
R^{n+\frac{1}{2}}_i = \Oof{\Delta x^2} + \Oof{\Delta t^2}\thinspace .
$$

## Analysis of the Leapfrog scheme

An attractive feature of the Forward Euler scheme is the explicit
time stepping and no need for solving linear systems. However, the
accuracy in time is only $\Oof{\Delta t}$. We can get an explicit
*second-order* scheme in time by using the Leapfrog method:

$$
[D_{2t} u = \dfc D_xDx u + f]^n_q\thinspace .
$$

Written out,

$$
u_q^{n+1} = u_q^{n-1} + \frac{2\dfc\Delta t}{\Delta x^2}
(u^{n}_{q+1} - 2u^n_q + u^n_{q-1}) + f(x_q,t_n)\thinspace .
$$

We need some formula for the first step, $u^1_q$, but for that we can use
a Forward Euler step.

Unfortunately, the Leapfrog scheme is always unstable for the
diffusion equation. To see this, we insert a wave component $A^ne^{ikx}$
and get

$$
\frac{A - A^{-1}}{\Delta t} = -\dfc \frac{4}{\Delta x^2}\sin^2 p,
$$

or

$$
A^2 + 4F \sin^2 p\, A - 1 = 0,
$$

which has roots

$$
A = -2F\sin^2 p \pm \sqrt{4F^2\sin^4 p + 1}\thinspace .
$$

Both roots have $|A|>1$ so the amplitude always grows, which is not in
accordance with the physics of the problem.
However, for a PDE with a first-order derivative in space, instead of
a second-order one, the Leapfrog scheme performs very well.

## Summary of accuracy of amplification factors

We can plot the various amplification factors against $p=k\Delta x/2$
for different choices of the $F$ parameter. Figures
[diffu:pde1:fig:A:err:C20](#diffu:pde1:fig:A:err:C20), [diffu:pde1:fig:A:err:C0.5](#diffu:pde1:fig:A:err:C0.5), and
[diffu:pde1:fig:A:err:C0.1](#diffu:pde1:fig:A:err:C0.1) show how long and small waves are
damped by the various schemes compared to the exact damping. As long
as all schemes are stable, the amplification factor is positive,
except for Crank-Nicolson when $F>0.5$.

Amplification factors for large time steps.

Amplification factors for time steps around the Forward Euler stability limit.

Amplification factors for small time steps.

The effect of negative amplification factors is that $A^n$ changes
sign from one time level to the next, thereby giving rise to
oscillations in time in an animation of the solution. We see from
[Figure](#diffu:pde1:fig:A:err:C20) that for $F=20$, waves with
$p\geq \pi/4$ undergo a damping close to $-1$, which means that the
amplitude does not decay and that the wave component jumps up and down
(flips amplitude) in time. For $F=2$ we have a damping of a factor of
0.5 from one time level to the next, which is very much smaller than
the exact damping. Short waves will therefore fail to be effectively
dampened.  These waves will manifest themselves as high frequency
oscillatory noise in the solution.

A value $p=\pi/4$ corresponds to four mesh points per wave length of
$e^{ikx}$, while $p=\pi/2$ implies only two points per wave length,
which is the smallest number of points we can have to represent the
wave on the mesh.

To demonstrate the oscillatory behavior of the Crank-Nicolson scheme,
we choose an initial condition that leads to short waves with
significant amplitude. A discontinuous $I(x)$ will in particular serve
this purpose: Figures [diffu:pde1:CN:fig:F=3](#diffu:pde1:CN:fig:F=3) and
[diffu:pde1:CN:fig:F=10](#diffu:pde1:CN:fig:F=10) correspond to $F=3$ and $F=10$,
respectively, and we see how short waves pollute the overall solution.

## Analysis of the 2D diffusion equation

Diffusion in several dimensions is treated later, but it is appropriate to
include the analysis here. We first consider the 2D diffusion equation

$$
u_{t} = \dfc(u_{xx} + u_{yy}),
$$

which has Fourier component solutions of the form

$$
u(x,y,t) = Ae^{-\dfc k^2t}e^{i(k_x x + k_yy)},
$$

and the schemes have discrete versions of this Fourier component:

$$
u^{n}_{q,r} = A\xi^{n}e^{i(k_x q\Delta x + k_y r\Delta y)}\thinspace .
$$

### The Forward Euler scheme

For the Forward Euler discretization,

$$
[D_t^+u = \dfc(D_xD_x u + D_yD_y u)]_{q,r}^n,
$$

we get

$$
\frac{\xi - 1}{\Delta t}
=
-\dfc\frac{4}{\Delta x^2}\sin^2\left(\frac{k_x\Delta x}{2}\right) -
\dfc\frac{4}{\Delta y^2}\sin^2\left(\frac{k_y\Delta y}{2}\right)\thinspace .
$$

Introducing

$$
p_x = \frac{k_x\Delta x}{2},\quad p_y = \frac{k_y\Delta y}{2},
$$

we can write the equation for $\xi$ more compactly as

$$
\frac{\xi - 1}{\Delta t}
=
-\dfc\frac{4}{\Delta x^2}\sin^2 p_x -
\dfc\frac{4}{\Delta y^2}\sin^2 p_y,
$$

and solve for $\xi$:

$$
\begin{equation}
\xi = 1 - 4F_x\sin^2 p_x - 4F_y\sin^2 p_y\thinspace .
\label{diffu:2D:analysis:xi} \tag{22}
\end{equation}
$$

The complete numerical solution for a wave component is

$$
\begin{equation}
u^{n}_{q,r} = A(1 - 4F_x\sin^2 p_x - 4F_y\sin^2 p_y)^n
e^{i(k_xq\Delta x + k_yr\Delta y)}\thinspace .
\label{diffu:2D:analysis:FE:numexact} \tag{23}
\end{equation}
$$

For stability we demand $-1\leq\xi\leq 1$, and $-1\leq\xi$ is the
critical limit, since clearly $\xi \leq 1$, and the worst case
happens when the sines are at their maximum. The stability criterion
becomes

$$
\begin{equation}
F_x + F_y \leq \frac{1}{2}\thinspace .
\label{diffu:2D:analysis:FE:stab} \tag{24}
\end{equation}
$$

For the special, yet common, case $\Delta x=\Delta y=h$, the
stability criterion can be written as

$$
\Delta t \leq \frac{h^2}{2d\dfc},
$$

where $d$ is the number of space dimensions: $d=1,2,3$.

### The Backward Euler scheme

The Backward Euler method,

$$
[D_t^-u = \dfc(D_xD_x u + D_yD_y u)]_{q,r}^n,
$$

results in

$$
1 - \xi^{-1} = - 4F_x \sin^2 p_x - 4F_y \sin^2 p_y,
$$

and

$$
\xi = (1 + 4F_x \sin^2 p_x + 4F_y \sin^2 p_y)^{-1},
$$

which is always in $(0,1]$. The solution for a wave component becomes

$$
\begin{equation}
u^{n}_{q,r} = A(1 + 4F_x\sin^2 p_x + 4F_y\sin^2 p_y)^{-n}
e^{i(k_xq\Delta x + k_yr\Delta y)}\thinspace .
\label{diffu:2D:analysis:BN:numexact} \tag{25}
\end{equation}
$$

### The Crank-Nicolson scheme

With a Crank-Nicolson discretization,

$$
[D_tu]^{n+\frac{1}{2}}_{q,r} =
\frac{1}{2} [\dfc(D_xD_x u + D_yD_y u)]_{q,r}^{n+1} +
\frac{1}{2} [\dfc(D_xD_x u + D_yD_y u)]_{q,r}^n,
$$

we have, after some algebra,

$$
\xi = \frac{1 - 2(F_x\sin^2 p_x + F_x\sin^2p_y)}{1 + 2(F_x\sin^2 p_x + F_x\sin^2p_y)}\thinspace .
$$

The fraction on the right-hand side is always less than 1, so stability
in the sense of non-growing wave components is guaranteed for all
physical and numerical parameters. However,
the fraction can become negative and result in non-physical
oscillations. This phenomenon happens when

$$
F_x\sin^2 p_x + F_x\sin^2p_y > \frac{1}{2}\thinspace .
$$

A criterion against non-physical oscillations is therefore

$$
F_x + F_y \leq \frac{1}{2},
$$

which is the same limit as the stability criterion for the Forward Euler
scheme.

The exact discrete solution is

$$
\begin{equation}
u^{n}_{q,r} = A
\left(
\frac{1 - 2(F_x\sin^2 p_x + F_x\sin^2p_y)}{1 + 2(F_x\sin^2 p_x + F_x\sin^2p_y)}
\right)^n
e^{i(k_xq\Delta x + k_yr\Delta y)}\thinspace .
\label{diffu:2D:analysis:CN:numexact} \tag{26}
\end{equation}
$$

## Explanation of numerical artifacts

The behavior of the solution generated by Forward Euler discretization in time (and centered
differences in space) is summarized at the end of
the section [diffu:pde1:FE:experiments](#diffu:pde1:FE:experiments). Can we, from the analysis
above, explain the behavior?

We may start by looking at [Figure](#diffu:pde1:FE:fig:F=0.51)
where $F=0.51$. The figure shows that the solution is unstable and
grows in time. The stability limit for such growth is $F=0.5$ and
since the $F$ in this simulation is slightly larger, growth is
unavoidable.

[Figure](#diffu:pde1:FE:fig:F=0.5) has unexpected features:
we would expect the solution of the diffusion equation to be
smooth, but the graphs in [Figure](#diffu:pde1:FE:fig:F=0.5)
contain non-smooth noise. Turning to [Figure](#diffu:pde1:FE:fig:gauss:F=0.5),  which has a quite similar
initial condition, we see that the curves are indeed smooth.
The problem with the results in [Figure](#diffu:pde1:FE:fig:F=0.5)
is that the initial condition is discontinuous. To represent it, we
need a significant amplitude on the shortest waves in the mesh.
However, for $F=0.5$, the shortest wave ($p=\pi/2$) gives
the amplitude in the numerical solution as $(1-4F)^n$, which oscillates
between negative and positive values at subsequent time levels
for $F>\frac{1}{4}$. Since the shortest waves have visible amplitudes in
the solution profile, the oscillations becomes visible. The
smooth initial condition in [Figure](#diffu:pde1:FE:fig:gauss:F=0.5),
on the other hand, leads to very small amplitudes of the shortest waves.
That these waves then oscillate in a non-physical way for
$F=0.5$ is not a visible effect. The oscillations
in time in the amplitude $(1-4F)^n$ disappear for $F\leq\frac{1}{4}$,
and that is why also the discontinuous initial condition always leads to
smooth solutions in [Figure](#diffu:pde1:FE:fig:F=0.25), where
$F=\frac{1}{4}$.

Turning the attention to the Backward Euler scheme and the experiments
in [Figure](#diffu:pde1:BE:fig:F=0.5), we see that even the discontinuous
initial condition gives smooth solutions for $F=0.5$ (and in fact all other
$F$ values). From the exact expression of the numerical amplitude,
$(1  + 4F\sin^2p)^{-1}$, we realize that this factor can never flip between
positive and negative values, and no instabilities can occur. The conclusion
is that the Backward Euler scheme always produces smooth solutions.
Also, the Backward Euler scheme guarantees that the solution cannot grow
in time (unless we add a source term to the PDE, but that is meant to
represent a physically relevant growth).

Finally, we have some small, strange artifacts when simulating the
development of the initial plug profile with the Crank-Nicolson scheme,
see [Figure](#diffu:pde1:CN:fig:F=10), where $F=3$.
The Crank-Nicolson scheme cannot give growing amplitudes, but it may
give oscillating amplitudes in time. The critical factor is
$1 - 2F\sin^2p$, which for the shortest waves ($p=\pi/2$) indicates
a stability limit $F=0.5$. With the discontinuous initial condition, we have
enough amplitude on the shortest waves so their wrong behavior is visible,
and this is what we see as small instabilities in
[Figure](#diffu:pde1:CN:fig:F=10). The only remedy is to lower the $F$ value.

# Exercises

## Exercise 1: Explore symmetry in a 1D problem

This exercise simulates the exact solution ([7](#diffu:pde1:sol:Gaussian)).
Suppose for simplicity that $c=0$.

**a)**
Formulate an initial-boundary value problem that has
([7](#diffu:pde1:sol:Gaussian)) as solution in the domain $[-L,L]$.
Use the exact solution ([7](#diffu:pde1:sol:Gaussian)) as Dirichlet
condition at the boundaries.
Simulate the diffusion of the Gaussian peak. Observe that the
solution is symmetric around $x=0$.

**b)**
Show from ([7](#diffu:pde1:sol:Gaussian)) that $u_x(c,t)=0$.
Since the solution is symmetric around $x=c=0$, we can solve the
numerical problem in frac{1}{2} of the domain, using a *symmetry boundary condition*
$u_x=0$ at $x=0$. Set up the
initial-boundary value problem in this case. Simulate the
diffusion problem in $[0,L]$ and compare with the solution in a).

**Solution.**

$$
\begin{align*}
u_t &= \dfc u_xx,\\
u_x(0,t) &= 0,\\
u(L,t)& =\frac{1}{\sqrt{4\pi\dfc t}} \exp{\left({-\frac{x^2}{4\dfc t}}\right)}\thinspace .
\end{align*}
$$

Filename: `diffu_symmetric_gaussian`.

## Exercise 2: Investigate approximation errors from a $u_x=0$ boundary condition

We consider the problem solved in [Exercise 1: Explore symmetry in a 1D problem](#diffu:exer:1D:gaussian:symmetric)
part b). The boundary condition $u_x(0,t)=0$ can be implemented in
two ways: 1) by a standard symmetric finite difference $[D_{2x}u]_i^n=0$,
or 2) by a one-sided difference $[D^+u=0]^n_i=0$.
Investigate the effect of these two conditions on the
convergence rate in space.

**Hint.**
If you use a Forward Euler scheme, choose a discretization parameter
$h=\Delta t = \Delta x^2$ and assume the error goes like $E\sim h^r$.
The error in the scheme is $\Oof{\Delta t,\Delta x^2}$ so one should
expect that the estimated $r$ approaches 1. The question is if
a one-sided difference approximation to $u_x(0,t)=0$ destroys this
convergence rate.

Filename: `diffu_onesided_fd`.

## Exercise 3: Experiment with open boundary conditions in 1D

We address diffusion of a Gaussian function
as in [Exercise 1: Explore symmetry in a 1D problem](#diffu:exer:1D:gaussian:symmetric),
in the domain $[0,L]$,
but now we shall explore different types of boundary
conditions on $x=L$. In real-life problems we do not know
the exact solution on $x=L$ and must use something simpler.

**a)**
Imagine that we want to solve the problem numerically on
$[0,L]$, with a symmetry boundary condition $u_x=0$ at $x=0$,
but we do not know the exact solution and cannot of that
reason assign a correct Dirichlet condition at $x=L$.
One idea is to simply set $u(L,t)=0$ since this will be an
accurate approximation before the diffused pulse reaches $x=L$
and even thereafter it might be a satisfactory condition if the exact $u$ has
a small value.
Let $\uex$ be the exact solution and let $u$ be the solution
of $u_t=\dfc u_{xx}$ with an initial Gaussian pulse and
the boundary conditions $u_x(0,t)=u(L,t)=0$. Derive a diffusion
problem for the error $e=\uex - u$. Solve this problem
numerically using an exact Dirichlet condition at $x=L$.
Animate the evolution of the error and make a curve plot of
the error measure

$$
E(t)=\sqrt{\frac{\int_0^L e^2dx}{\int_0^L udx}}\thinspace .
$$

Is this a suitable error measure for the present problem?

**b)**
Instead of using $u(L,t)=0$ as approximate boundary condition for
letting the diffused Gaussian pulse move out of our finite domain,
one may try $u_x(L,t)=0$ since the solution for large $t$ is
quite flat. Argue that this condition gives a completely wrong
asymptotic solution as $t\rightarrow 0$. To do this,
integrate the diffusion equation from $0$ to $L$, integrate
$u_{xx}$ by parts (or use Gauss' divergence theorem in 1D) to
arrive at the important property

$$
\frac{d}{dt}\int_{0}^L u(x,t)dx = 0,
$$

implying that $\int_0^Ludx$ must be constant in time, and therefore

$$
\int_{0}^L u(x,t)dx = \int_{0}^LI(x)dx\thinspace .
$$

The integral of the initial pulse is 1.

**c)**
Another idea for an artificial boundary condition at $x=L$
is to use a cooling law

$$
\begin{equation}
-\dfc u_x = q(u - u_S),
\label{diffu:pde1:Gaussian:xL:cooling} \tag{27}
\end{equation}
$$

where $q$ is an unknown heat transfer coefficient and $u_S$ is
the surrounding temperature in the medium outside of $[0,L]$.
(Note that arguing that $u_S$ is approximately $u(L,t)$ gives
the $u_x=0$ condition from the previous subexercise that is
qualitatively wrong for large $t$.)
Develop a diffusion problem for the error in the solution using
([27](#diffu:pde1:Gaussian:xL:cooling)) as boundary condition.
Assume one can take $u_S=0$ "outside the domain" since
$\uex\rightarrow 0$ as $x\rightarrow\infty$.
Find a function $q=q(t)$ such that the exact solution
obeys the condition ([27](#diffu:pde1:Gaussian:xL:cooling)).
Test some constant values of $q$ and animate how the corresponding
error function behaves. Also compute $E(t)$ curves as defined above.

Filename: `diffu_open_BC`.

## Exercise 4: Simulate a diffused Gaussian peak in 2D/3D

**a)**
Generalize ([7](#diffu:pde1:sol:Gaussian)) to multi dimensions by
assuming that one-dimensional solutions can be multiplied to solve
$u_t = \dfc\nabla^2 u$. Set $c=0$ such that the peak of
the Gaussian is at the origin.

**b)**
One can from the exact solution show
that $u_x=0$ on $x=0$, $u_y=0$ on $y=0$, and $u_z=0$ on $z=0$.
The approximately correct condition $u=0$ can be set
on the remaining boundaries (say $x=L$, $y=L$, $z=L$), cf. [Exercise 3: Experiment with open boundary conditions in 1D](#diffu:exer:1D:openBC).
Simulate a 2D case and make an animation of the diffused Gaussian peak.

**c)**
The formulation in b) makes use of symmetry of the solution such that we
can solve the problem in the first quadrant (2D) or octant (3D) only.
To check that the symmetry assumption is correct, formulate the problem
without symmetry in a domain $[-L,L]\times [L,L]$ in 2D. Use $u=0$ as
approximately correct boundary condition. Simulate the same case as
in b), but in a four times as large domain. Make an animation and compare
it with the one in b).

Filename: `diffu_symmetric_gaussian_2D`.

## Exercise 5: Examine stability of a diffusion model with a source term

Consider a diffusion equation with a linear $u$ term:

$$
u_t = \dfc u_{xx} + \beta u\thinspace .
$$

**a)**
Derive in detail the Forward Euler, Backward Euler,
and Crank-Nicolson schemes for this type of diffusion model.
Thereafter, formulate a $\theta$-rule to summarize the three schemes.

**b)**
Assume a solution like ([8](#diffu:pde1:sol1)) and find the relation
between $a$, $k$, $\dfc$, and $\beta$.

**Hint.**
Insert ([8](#diffu:pde1:sol1)) in the PDE problem.

**c)**
Calculate the stability of the Forward Euler scheme. Design
numerical experiments to confirm the results.

**Hint.**
Insert the discrete counterpart to ([8](#diffu:pde1:sol1)) in the
numerical scheme. Run experiments at the stability limit and slightly above.

**d)**
Repeat c) for the Backward Euler scheme.

**e)**
Repeat c) for the Crank-Nicolson scheme.

**f)**
How does the extra term $bu$ impact the accuracy of the three schemes?

**Hint.**
For analysis of the accuracy,
compare the numerical and exact amplification factors, in
graphs and/or by Taylor series expansion.

Filename: `diffu_stability_uterm`.