# Circuitos Lineares de 1ª Ordem

Jupyter Notebook desenvolvido por [Gustavo S.S.](https://github.com/GSimas)

> "Vivemos de nossos atos, não dos anos vividos; de pensamentos, não apenas da respiração;
de sentimentos, não dos números em um disco de telefone. Deveríamos contar o tempo em pulsações.
Vive mais aquele que pensa mais, sente-se o mais nobre, aquele que age melhor." - P.J. Bailey

**Um circuito de primeira ordem é caracterizado por uma equação diferencial
de primeira ordem.**

## Circuito RC sem fonte

Um circuito RC sem fonte ocorre quando sua fonte CC é desconectada abruptamente.
A energia já armazenada no capacitor é liberada para os resistores.

\begin{align}
{\Large v(t) = V_0 e^{\frac{-t}{RC}}}
\end{align}

**A resposta natural de um circuito se refere ao comportamento (em termos
de tensões e correntes) do próprio circuito, sem nenhuma fonte externa de
excitação.**

A resposta natural depende da
natureza do circuito em si, sem
nenhuma fonte externa. De
fato, o circuito apresenta uma
resposta apenas em razão da
energia armazenada inicialmente
no capacitor.

A resposta natural é ilustrada graficamente na Figura 7.2. Observe que em
t = 0 temos a condição inicial correta como na Equação anterior. À medida que t
aumenta, a tensão diminui em direção a zero. A rapidez com que a tensão decresce é expressa em termos da constante de tempo, representada por \tau, a letra grega minúscula tau.

**A constante de tempo de um circuito é o tempo necessário para a resposta
de decaimento a um fator igual a 1/e ou a 36,8% de seu valor inicial**

\begin{align}
{\Large \tau = RC}
\end{align}

Assim, a equação da tensão em função do tempo, fica:

\begin{align}
{\Large v(t) = V_0 e^{\frac{-t}{\tau}}}
\end{align}

Com a tensão v(t) na Equação, podemos determinar a corrente iR(t):

\begin{align}
{\Large iR(t) = \frac{V_0}{R} e^{\frac{-t}{\tau}}}
\end{align}

A energia absorvida pelo resistor até o instante t é:

\begin{align}
{\Large w_R(t) = \int_{0}^{t} p(x)dx = \frac{1}{2} C V_0² (1 - e^{-2 \frac{t}{\tau}})}
\end{align}

**Note que, à medida que t -> ∞, wR(∞) -> CV0²/2, que é o mesmo que wC(0), a energia armazenada inicialmente no capacitor, a qual é finalmente dissipada no resistor.**

**O segredo para se trabalhar com um circuito RC sem
fonte é encontrar:**

1. A tensão inicial v(0) = V0 no capacitor
2. A constante de tempo \tau

**Exemplo 7.1**

Na Figura 7.5, façamos vC(0) = 15 V. Determine vC, vx e ix para t 7 0.

```python
print("Exemplo 7.1")

import numpy as np
from sympy import *

C = 0.1
v0 = 15

t = symbols('t')

Req1 = 8 + 12
Req2 = Req1*5/(Req1 + 5)
tau = C*Req2
vc = v0*exp(-t/tau)
vx = vc*12/(12 + 8)
ix = vx/12

print("Tensão Vc:",vc,"V")
print("Tensão Vx:",vx,"V")
print("Corrente ix:",ix,"A")
```

    Exemplo 7.1
    Tensão Vc: 15*exp(-2.5*t) V
    Tensão Vx: 9*exp(-2.5*t) V
    Corrente ix: 3*exp(-2.5*t)/4 A

**Problema Prático 7.1**

Consulte o circuito da Figura 7.7. Seja, vC(0) = 60 V. Determine vC, vx e io, para t >= 0.

```python
print("Problema Prático 7.1")

v0 = 60
C = 1/3

Req1 = 12*6/(12 + 6)
Req2 = Req1 + 8
tau = C*Req2

vc = v0*exp(-t/tau)
vx = vc*Req1/(Req1 + 8)
vr = vc - vx
i0 = - vr/8

print("Tensão Vc:",vc,"V")
print("Tensão Vx:",vx,"V")
print("Corrente i0:",i0,"A")
```

    Problema Prático 7.1
    Tensão Vc: 60*exp(-0.25*t) V
    Tensão Vx: 20.0*exp(-0.25*t) V
    Corrente i0: -5.0*exp(-0.25*t) A

**Exemplo 7.2**

A chave no circuito da Figura 7.8 foi fechada por um longo período e é aberta em t = 0.
Determine v(t) para t >= 0. Calcule a energia inicial armazenada no capacitor.

```python
print("Exemplo 7.2")

Vf = 20
m = 10**-3
C = 20*m

v0 = Vf*9/(9 + 3)
Req = 9 + 1
tau = Req*C
vc = v0*exp(-t/tau)
wc = (C*v0**2)/2

print("Tensão v(t):",vc,"V")
print("Energia inicial:",wc,"J")
```

    Exemplo 7.2
    Tensão v(t): 15.0*exp(-5.0*t) V
    Energia inicial: 2.25 J

**Problema Prático 7.2**

Se a chave da Figura 7.10 abrir em t = 0, determine v(t) para t >= 0 e wC(0).

```python
print("Problema Prático 7.2")

Vf = 24
C = 1/6

Req1 = 12*4/(12 + 4)
v0 = Vf*Req1/(Req1 + 6)
tau = Req1*C
v = v0*exp(-t/tau)
wc = (C*v0**2)/2

print("Tensão v(t):",v,"V")
print("Energia inicial:",wc,"J")
```

    Problema Prático 7.2
    Tensão v(t): 8.0*exp(-2.0*t) V
    Energia inicial: 5.333333333333333 J


# Machine Learning Part

```
import numpy as np
```

## Data preparation
We are going to use pytorch to help us build our algorithm. First, we define a custom Dataset class to hold our data (train/test). We, also, split the training data into 2 sets (train and validation), in order to test our model's generalization ability without using the test data. Given more data, it would be beneficial to tune the batch size and/or perform k-fold cross validation.

```
from torch.utils.data import SubsetRandomSampler, Dataset

def split_train_val(dataset, batch_train, batch_val, val_size=.2, shuffle=True, seed=17):
    dataset_size = len(dataset)
    indices = list(range(dataset_size))
    val_split = int(np.floor(val_size * dataset_size))

    if shuffle:
        np.random.seed(seed)
        np.random.shuffle(indices)
    train_indices = indices[val_split:]
    val_indices = indices[:val_split]

    train_loader = DataLoader(dataset, batch_size=batch_train, sampler=SubsetRandomSampler(train_indices))
    val_loader = DataLoader(dataset, batch_size=batch_val, sampler=SubsetRandomSampler(val_indices))
    return train_loader, val_loader

class HighEnergyDataset(Dataset):

  def __init__(self, train):
    t = 'training' if train else 'test'
    self.path = np.load(t + '_input.npy', allow_pickle=True)
    self.feats = np.concatenate((self.path.item()['0'], self.path.item()['1']), axis=0)
    self.label = np.concatenate((np.full((50,), 0), np.full((50,), 1)), axis=0)

  def __getitem__(self, item):
    return (self.feats[item], self.label[item])

  def __len__(self):
    return len(self.label)
```

Load data from input files

```
from torch.utils.data import DataLoader

train_loader, val_loader = split_train_val(HighEnergyDataset(train=True), batch_train=1, batch_val=1)
test_loader = DataLoader(HighEnergyDataset(train=False), batch_size=1)
```

## Model architecture
We define our classifier to be a 2 layer neural network, with a ReLU non-linearity in the between. The output dimension is 2 (the input's probability to be $0$ and $1$, respectively)

```
import torch.nn as nn

class BasicMLP(nn.Module):

  def __init__(self):
    super(BasicMLP, self).__init__()

    self.layer1 = nn.Sequential(nn.Linear(5, 20), nn.ReLU(inplace=True))
    self.layer2 = nn.Sequential(nn.Linear(20, 2))

  def forward(self, x):
    x = self.layer1(x)
    x = self.layer2(x)
    return x
```

## Training
We train the classifier with stochastic gradient descent, choosing as loss the Cross Entropy loss. The hyperparameters of learning rate, number of epochs and NN's hidden layer dimension were tuned by checking the accuracy on the validation set.

```
from torch import optim, no_grad, save, load
from matplotlib import pyplot as plt

def accuracy(model, data_loader):
  model.eval()
  correct, total = 0, 0
  for feats, labels in data_loader:
      with no_grad():
          scores = model(feats.float())
          prediction = scores.argmax(dim=1)
          correct += (prediction == labels).item()
          total += 1
  return 100 * correct / total

def train(model, loss_function, optimizer, data_loader, epochs=20):
  train_acc = []
  val_acc = []
  max_val = 0
  for epoch in range(epochs):
    for feats, label in data_loader:
      model.zero_grad()
      optimizer.zero_grad()

      prediction = model(feats.float())
      loss = loss_function(prediction.float(), label)
      loss.backward()
      optimizer.step()

    train_acc.append(accuracy(model, train_loader))
    cur_val = accuracy(model, val_loader)
    val_acc.append(cur_val)

    max_val = cur_val if cur_val > max_val else max_val
    if (epoch > 3 and cur_val == max_val and train_acc[-1] >= cur_val):  # save the best performing model
      save(model, 'best.pt')

  plt.plot(range(epochs), train_acc, 'g', range(epochs), val_acc, 'b')
  plt.xlabel('epochs')
  plt.ylabel('accuracy %')
  plt.legend(['train accuracy', 'validation accuracy'])
  plt.show()
```

```
model = BasicMLP()
loss_function = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.05)

train(model, loss_function, optimizer, train_loader, epochs=200)
```

## Evaluation on test data
We load the best performing model found during the training process and we use it to classify the test data. The metric of evaluation is the accuracy of the classification.

```
model = load('best.pt')
print(str(accuracy(model, test_loader)) + '%')
```

    70.0%

# Quantum Computing Part

```
!pip3 install cirq

import cirq
```

    Requirement already satisfied: cirq in /usr/local/lib/python3.6/dist-packages (0.7.0)
    Requirement already satisfied: scipy in /usr/local/lib/python3.6/dist-packages (from cirq) (1.4.1)
    Requirement already satisfied: matplotlib~=3.0 in /usr/local/lib/python3.6/dist-packages (from cirq) (3.1.3)
    Requirement already satisfied: requests~=2.18 in /usr/local/lib/python3.6/dist-packages (from cirq) (2.21.0)
    Requirement already satisfied: google-api-python-client~=1.6 in /usr/local/lib/python3.6/dist-packages (from cirq) (1.7.11)
    Requirement already satisfied: sympy==1.4 in /usr/local/lib/python3.6/dist-packages (from cirq) (1.4)
    Requirement already satisfied: numpy~=1.16 in /usr/local/lib/python3.6/dist-packages (from cirq) (1.17.5)
    Requirement already satisfied: sortedcontainers~=2.0 in /usr/local/lib/python3.6/dist-packages (from cirq) (2.1.0)
    Requirement already satisfied: protobuf==3.8.0 in /usr/local/lib/python3.6/dist-packages (from cirq) (3.8.0)
    Requirement already satisfied: networkx==2.3 in /usr/local/lib/python3.6/dist-packages (from cirq) (2.3)
    Requirement already satisfied: dataclasses; python_version < "3.7" in /usr/local/lib/python3.6/dist-packages (from cirq) (0.7)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.6/dist-packages (from cirq) (3.6.6)
    Requirement already satisfied: pandas in /usr/local/lib/python3.6/dist-packages (from cirq) (0.25.3)
    Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (0.10.0)
    Requirement already satisfied: pyparsing!=2.0.4,!=2.1.2,!=2.1.6,>=2.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (2.4.6)
    Requirement already satisfied: python-dateutil>=2.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (2.6.1)
    Requirement already satisfied: kiwisolver>=1.0.1 in /usr/local/lib/python3.6/dist-packages (from matplotlib~=3.0->cirq) (1.1.0)
    Requirement already satisfied: idna<2.9,>=2.5 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (2.8)
    Requirement already satisfied: chardet<3.1.0,>=3.0.2 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (3.0.4)
    Requirement already satisfied: urllib3<1.25,>=1.21.1 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (1.24.3)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.6/dist-packages (from requests~=2.18->cirq) (2019.11.28)
    Requirement already satisfied: google-auth-httplib2>=0.0.3 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (0.0.3)
    Requirement already satisfied: google-auth>=1.4.1 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (1.7.2)
    Requirement already satisfied: uritemplate<4dev,>=3.0.0 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (3.0.1)
    Requirement already satisfied: httplib2<1dev,>=0.9.2 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (0.11.3)
    Requirement already satisfied: six<2dev,>=1.6.1 in /usr/local/lib/python3.6/dist-packages (from google-api-python-client~=1.6->cirq) (1.12.0)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.6/dist-packages (from sympy==1.4->cirq) (1.1.0)
    Requirement already satisfied: setuptools in /usr/local/lib/python3.6/dist-packages (from protobuf==3.8.0->cirq) (45.2.0)
    Requirement already satisfied: decorator>=4.3.0 in /usr/local/lib/python3.6/dist-packages (from networkx==2.3->cirq) (4.4.1)
    Requirement already satisfied: pytz>=2017.2 in /usr/local/lib/python3.6/dist-packages (from pandas->cirq) (2018.9)
    Requirement already satisfied: cachetools<3.2,>=2.0.0 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (3.1.1)
    Requirement already satisfied: rsa<4.1,>=3.1.4 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (4.0)
    Requirement already satisfied: pyasn1-modules>=0.2.1 in /usr/local/lib/python3.6/dist-packages (from google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (0.2.8)
    Requirement already satisfied: pyasn1>=0.1.3 in /usr/local/lib/python3.6/dist-packages (from rsa<4.1,>=3.1.4->google-auth>=1.4.1->google-api-python-client~=1.6->cirq) (0.4.8)

## 1st part - Simple circuit

```
import math

qubits = [cirq.GridQubit(i, 0) for i in range(5)]
circuit = cirq.Circuit()

# Hadamard layer
circuit.append([cirq.H(q) for q in qubits])

# CNOT layer
circuit.append([cirq.CNOT(qubits[i], qubits[i+1]) for i in range(len(qubits) - 1)])

# SWAP layer
circuit.append(cirq.SWAP(qubits[0], qubits[4]))

# pi/2 Rotation layer
def rot_x_layer(qubits, rads):
    rot = cirq.rx(rads)
    for i in range(len(qubits)):
       yield rot(qubits[i])

circuit.append([rot_x_layer(qubits, math.pi/2)])
print(circuit)
```

                                                 ┌─────────┐
    (0, 0): ───H───@──────────────────────────────×────────────Rx(0.5π)───
                   │                              │
    (1, 0): ───H───X───@───Rx(0.5π)───────────────┼───────────────────────
                       │                          │
    (2, 0): ───H───────X───@──────────Rx(0.5π)────┼───────────────────────
                           │                      │
    (3, 0): ───H───────────X──────────@───────────┼Rx(0.5π)───────────────
                                      │           │
    (4, 0): ───H──────────────────────X───────────×────────────Rx(0.5π)───
                                                 └─────────┘

## 2nd part - Simulation
We define 10 circuits, with 1 to 10 rotations applied to the qubit before measuring its state. We approximate the probability of |0> state as the frequency of its occurence. We run 500 independent simulations.

```
qubit = cirq.GridQubit(1,0)

circuits = []
for i in range(10):
  circuits.append(cirq.Circuit())
  for j in range(i+1):
    circuits[-1].append(cirq.rx(math.pi/10)(qubit))
  circuits[-1].append(cirq.measure(qubit, key='x'))
  print(circuits[-1])
```

    (1, 0): ───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───
    (1, 0): ───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───Rx(0.1π)───M('x')───

```
from matplotlib import pyplot as plt
simulator = cirq.Simulator()

trials = 500
results = []
probs = []
for i in range(10):
  results.append(simulator.run(circuits[i], repetitions=trials))
  probs.append(results[-1].histogram(key='x')[0]/trials)

plt.title('Simulation results')
plt.ylabel('Probability of |0> state')
plt.xlabel('# of $\pi$/10 rotations')
plt.plot(range(1, 11), probs)
plt.show()
```

As we expect, the probability decreases as the number of rotations increases and goes to 0, when a rotation of  $\pi$ radians total happens (10th circuit).


*Modified from the CIERA IDEAS program python notes (https://github.com/IDEASpy/IDEASpy) and from notes by G. Besla, P. Pinto, E. Hayati (https://github.com/gurtina/P105A_2019)*

# CIERA Summer Camp 2019: Plotting with Matplotlib

## 1. Getting Started with Matplotlib

Matplotlib is a powerful 2D plotting library built for Python, and the package of choice for many researchers these days. Like Python itself, it's fairly intuitive and easy to get started, yet advanced enough to produce amazing, publication-quality figures. Matplotlib can be used in Python scripts, the Python and iPython shell, the jupyter notebook, as well as in other applications and toolkits beyond the scope of this course.

## 2. Installation

If you installed the Anaconda distribution of Python, you should already have matplotlib installed. In your Python terminal, type import matplotlib. If you don't get any errors, you're good to go! If not, visit the matplotlib.org. Links to an external site for help on installing matplotlib on your computer.

## 3. Ways to Use Matplotlib
There are essentially two ways to use matplotlib:

> 1. Quick and dirty using pyplot: a collection of MATLAB-like (if you're familiar) plotting functions that allow for quick and dirty plotting. When you use pyplot functions, they are always acting on the 'current figure' which it keeps track of.

> 2. More fine-grained control through the Figure and Axes classes: an object-oriented approach for plotting

Many people like to use the quick and dirty method for quick plots in jupyter notebooks, but it can be used in scripts as well. When you are making more complex plots, you may opt for the second approach. We will introduce you to both of these plotting styles because as you explore on your own, you'll encounter both types of examples. For a more in-depth conceptual overview of the matplotlib framework and hierarchy check out this page:
 __[https://matplotlib.org/faq/usage_faq.html](https://matplotlib.org/faq/usage_faq.html)__

## 4. Basic Plotting with Matplotlib

This lesson will cover the basics of plotting with matplotlib, but there is much more to learn. In subsequent modules, we'll continue to explore the capabilities of matplotlib, but if you're itching for more, refer to the resources at the end of this page. Also, you might want to print out this handy __[cheat sheet](https://s3.amazonaws.com/assets.datacamp.com/blog_assets/Python_Matplotlib_Cheat_Sheet.pdf)__ from Data Camp and keep it at your desk as you learn.

An important tip for learning matplotlib: when you need documentation on a particular matplotlib object, matplotlib's internal search function will often be much more helpful than a Google search.  The internal search engine can be found on the right-hand side of the matplotlib __[website](https://matplotlib.org/)__.

### 4.1. Approach 1: Pyplot
If you have a list of x and y values, it is easy to make a simple **line plot** of them. See the example in the cell below:

The first line tells the notebook that you want to display any plots inline (i.e. inside the notebook). This is done with the function %matplotlib inline.

The second line in the below code cell imports the pyplot module from matplotlib and says that we will refer to the pyplot as plt. This gives you access to all of pyplot's functions, which can be called as plt.functionname.

```python
%matplotlib inline
import matplotlib.pyplot as plt     # now we'll reference pyplot functions using plt
x = [1, 2, 3, 4]                    # create a list of x-values
y = [2, 4, 6, 8]                    # create a list of y-values
plt.plot(x, y)                      # call the plotting function, preface with plt (as we imported Pyplot)
```

The generic plot command above will create a line by default, but there are many optional parameters you can play with to change the style of the line (or remove it altogether), add markers of different shapes to the datapoints, and to modify the colors.

In the cells below we've entered four examples of basic plots so you can see how some properties of a simple plot can be adjusted. Run the cells one at a time to see the plots they generate.

While the example below is good for basic practice, it does not include all the capabilities that the matplotlib package installed on your computer will have. The code below should work fine, but if you start making too many adjustments, don't be surprised if you find something that doesn't work quite right. :-)

```python
import matplotlib.pyplot as plt

# A quick orientation to pyplot

# Create some data
x = range(10)
y = range(0,20,2)
z = [x[i]**2 for i in range(len(x))]

# (1) By default, the plot function will connect the data points with
#     lines, but won't show the data points
plt.plot(x, y)
```

```python
# (2) We can plot multiple datasets on a single figure
plt.plot(x, y)
plt.plot(x, z)
```

When you include another plot, pyplot will automatically change the color of the line for you. But you can change the color, linewidth and linestyle.

Line properties are controlled with optional keywords to the plot function, namely the commands color, linestyle and linewidth. The first two have required string arguments, and the third (linewidth) requires a numerical argument in multiples of the default linewidth (1).

linestyle options: [ '-' | '--' | '-.' | ':' |]

color options: e.g., 'red','green','yellow','blue', 'orange', 'cyan', 'magenta','black'

```python
# (3) You can adjust many properties of the line
plt.plot(x, y, linewidth=3, linestyle=':', color='b')
```

'b' is short for 'blue'

Here are some especially useful pyplot functions, called with plt.functionname(input(s)) that can make the plots nicer:

xlabel and ylabel set the labels for the x and y axes, respectively and have a required string input.

title sets the title of the plot and also requires a string input.

These can be specified either in the call to the function or separately before or after the plot call.

```python
# (4) You can add labels and title to the plot
plt.plot(x, y)

# let's add axis labels
plt.xlabel("the independent variable (no units)")
plt.ylabel("the dependent variable (no units)")

# let's add a title
plt.title("Title")
```

You can also save the plot to a file using the function savefig and setting the resolution.

```python
plt.plot(x,y)
plt.savefig('thefigure.png', dpi=300) # save the figure to a file, dpi indictes the resolution

##You can find the figiure at your current directory
```

### 4.2. Approach 2: Figure and Axes Classes

For creating more advanced plots, and to have better ability to fine-tune your figures, we recommend also practicing using matplotlib in the object-oriented approach. We won't go too deep into this matplotlib framework, but here's a simple way to think about it: you first create figures, which are like virtual sheets of paper; then you create axes, which are the designated places to draw on those sheets of paper.

We start by using pyplot to create a figure, and through those figures, one or more axes objects can be created. These axes objects are then used for most plotting actions. The figure and axes are defined as:

```python
fig, ax1 = plt.subplots(1, 1)
ax1.plot(x, y)
```

The first command defines a new figure named fig, and an axes object, ax1, on which we can plot our data. Think of the axes object as the area on which you plot your data. Subplots allows us to create multiple plots on one figure; the (1, 1) states that this figure should have 1 row and 1 column, so really just one plot in this case. The second line shows how you create a plot for the axes ax1. The next example shows how you'd create a 2x1 plot (2 rows, 1 column). Note that you have to define a tuple to represent the two plot axes, ax1 and ax2.

```python
fig, (ax1, ax2) = plt.subplots(2, 1)
ax1.plot(x, y)
ax2.plot(x, y)
```

### 4.3. Different Types of Plots

There are other types of plots that are useful, like scatter plot and histogram.

#### 4.3.1. Scatter Plot

We can use numpy arrays to generate arrays with random numbers.

The below code generates 10 random floats from 0 to 1 and stores it as an array.

```python
import numpy as np

N = 10 # number of desired random floats.

x = np.random.rand(N) # this numpy function creates an array of the given shape (N)
 #and populates it with random samples from a uniform distribution over [0, 1).

print(x)
```

We can use arrays with random numbers to generate scatter plots.

```python
# let's define two arrays with 1000 random numbers sampled between 0 and 100
N = 1000
x = np.random.rand(N)*100
y = np.random.rand(N)*100

colors = np.random.rand(N) # This is another random list of 1000 numbers from 0 to 1 that
                             # matplotlib will map onto a color table.

area = (30*np.random.rand(N))**2 # randomly assigning area of each point. 0 to 15 point radii
```

```python
# Create a scatter plot
plt.scatter(x, y, s=area, c=colors, alpha=0.5) # alpha indicates transparency of each point

plt.xlabel('x')
plt.ylabel('y')
plt.title('Scatter Plot')
```

#### 4.3.2. Histogram

Example is from the matplotlib library (https://matplotlib.org/3.1.0/gallery/statistics/hist.html).

To generate a 1D histogram we only need a single vector of numbers. In this example we use approach 2 to plot the figure.

```python
N_points = 100000
n_bins = 20

# Generate a normal distribution, center at x=0 and y=5
x = np.random.randn(N_points)
y = .4 * x + np.random.randn(100000) + 5

fig, axs = plt.subplots(1, 2, sharey=True, tight_layout=True)

# We can set the number of bins with the `bins` kwarg
axs[0].hist(x, bins=n_bins)
axs[1].hist(y, bins=n_bins)
```

## 5. Components of a 'good plot'

Take a look at the figure below, which shows the anatomy of a figure. It's generally a good idea to make sure that you include most of these components on every plot you make. The code and other supporting documents are available via GitHub at https://github.com/rougier/figure-anatomy. Copyright (c) 2016 Nicolas P. Rougier - MIT License). Check out the anatomy.py file to see exactly how to make this plot.

## Mini Challenge

Make a scatter and a linear plot in the same figure using the data provided (x and y) with approach 1.

For the linear plot, use color orange for the line, and use dotted line style.

For the scatter plot, use a for loop to plot the data and use 20 times the x list values for the corresponding sizes of the points. Pick 6 different colors you like and give them to the 6 points in the scatter plot.

Add x and y label as "x label" and "y label".

```python
# Import matplotlib

# Data for plotting. These are two lists.
x=[1, 2, 3, 4, 5, 6]
y=[2, 4, 6, 8, 10, 12]

##Make a list that contains the colors with the valuable name cs

##Make the figure first

##Plot the line

# Plot the points one-by-one here with a for loop using scatter plot

```

## Bonus 1

Make four subplots (2 columns and 2 rows) to show different representations of the same set of data (provided) using approach 2.

Make subplot 1 a histogram of the x values, with histrogram type 'step'. (Hint: look up the matplotlib.pyplot.hist dictionary of how to change the type of a histogram.)

Subplot 2 line plot, with dashed line and red color.

Subplot 3 scatter plot. Instead of plotting solid dots as from default, make them empty circles.

Subplot 4 two-dimensional histogram. (Hint: look up hist2d from the matplotlib library.)

Add x and y label as "x label" and "y label".

```python
##import modules
import numpy as np

##Randomly generate a standard normal distribution.
np.random.seed(19680801)
data=np.random.randn(2, 100)
x=data[0]; y=data[1]

##Make the plots

```

## Bonus 2

Generate a sinusoidal curve data and a cosinusoidal curve data between 0 and 2 pi with approach 1.

Plot the sine curve in orange and the cosine curve in blue. Use line width 2.

Add a legend to the curves, with the sine curve as "sine" and cosine curve as "cosine". (Hint: look up matplotlib legend guide.)

Add x and y label as "x label" and "y label".

```python
##Make the plot here

```

## Bonus 3

You are going to make a simple H-R diagram in this bonus problem.

File HRstars.csv contains the temperature (in Kelvin; first column) and the luminosity (in solar luminosity; second column) of the stars. Use numpy genfromtxt to read the data file and give the first column to the x valuable and the second column to the y valuable.

In the plot, make the hotter stars are bluer, and colder stars are redder. Use a color map called 'RdYlBu' for the colors.

Use equation \begin{equation} \frac{L}{L_{\odot}}=(\frac{R}{R_{\odot}})^2(\frac{T}{T_{\odot}})^4 \end{equation} to approximate the sizes of the points representing the stars. \begin{equation} T_{\odot} = 5780 K \end{equation}

```python
##Import Modules

##Read data from file. Use delimiter = ','
data=np.genfromtxt('', delimiter=',')  ##fill in the blank
x=data[]; y=data[]

##Turn the data lists into arrays

##Approximate the sizes of the stars with the equation above and times them by 15.
Tsun=5780.
sstar=15*   ##Fill in the blank

##Plot the data
plt.figure(figsize=[10, 7])     ##This make the figure and set the figure size
cm = plt.cm.get_cmap('RdYlBu')  ##This is the color map for the stars

##Fill in below for the plot, use c=x, cmap=cm, alpha=0.8 (alpha sets the transparency of the points)

##Make the x and y scale to be logarithmic (Look up how to make logarithmic scale in pyplot)

##Set the limits of the x axis from 50000 to 2000, and set the limits of the y axis from 10**-5 to 5*10**7
##Look up how to set axis limits in pyplot

##Make title and x, y labels.
##X label: Temperature (K), Y label: Luminosity (Lsun)
##Title: HR Diagram

plt.show()   ##Show plot
```

## Learning More

As you start using matplotlib to visualize your own scientific data, it's likely that at some point you may have a need to plot a colormap. A word of caution regarding choosing the right colormap for your data: it is strongly recommended that you stay away from using the rainbow colormap if at all possible, because it can greatly misrepresent your data (not to mention, it's especially problematic for people who are colorblind, because they have a hard time distinguishing between red and green). Refer to this a great post to learn more about choosing colormaps: https://matplotlib.org/users/colormaps.html

To further explore the capabilities of matplotlib, check out these tutorials and resource galleries:

Beginner Pyplot Tutorial: https://matplotlib.org/users/pyplot_tutorial.html (Links to an external site).

Scipy's matplotlib tutorial: http://www.scipy-lectures.org/intro/matplotlib/matplotlib.html (Links to an external site).

Examples by Type:  If you want to explore plots by type (e.g., pie charts, animations, 3D plots), here's where to look: https://matplotlib.org/examples/index.html (Links to an external site).

Matplotlib Plot Gallery: Looking for inspiration? Check out this impressive gallery: https://matplotlib.org/gallery.html (Links to an external site).

Creating Publication Quality Figures (tutorial by Jean-Baptiste Mouret): https://github.com/jbmouret/matplotlib_for_papers (Links to an external site).

## Takeaways

> - Matplotlib is an extremely powerful package for plotting data in Python.

> - With matplotlib, you can make high-quality, publication ready figures.

> - There are two different ways of using matplotlib, and in many cases there may be multiple ways to generate the same plot. It can be confusing at first, but review many examples, and you'll get the hang of it in no time!

```python

```


---
### *The 11th Computational Neuroscience Winter School*
# Tutorial II: Neuronal Dynamics - Spatial Neuron Models (Passive Cable Theory)

---
__Date__: *Jan. 21, 2022*

__Content Creators:__ Songting Li, Kai Chen, Ziling Wang

# Setup

```python
# @title Import most modules and functions needed
import time
import numpy as np
import matplotlib as mpl
mpl.rcParams['lines.linewidth'] = 2
mpl.rcParams['legend.fontsize'] = 14
mpl.rcParams['axes.labelsize'] = 16
mpl.rcParams['axes.titlesize'] = 18
import matplotlib.pyplot as plt
%matplotlib inline
%config InlineBackend.figure_format = 'retina'
```

## Tutorial Objectives

Neurons in the brain exhibit delicate dendritic structures that are crucial for their computations. Spatial neuron models have been developed to capture the feature of dendrites. In this notebook, we will introduce the cable theory that describes the spatiotemporal dynamics of the membrane potential along a dendrite, and develop spatial neuron models based on the cable theory for simulations.

**References:**
* Henry Tuckwell (1988) Introduction to theoretical neurobiology. Vol. 1, Linear cable theory and dendritic structure
* Wulfram Gerstner, Werner M. Kistler, Richard Naud and Liam Paninski (2014) Neuronal Dynamics: From single neurons to networks and models of cognition
From single neurons to networks and models of cognition

## Derivation of the Cable Equation
Let's consider a piece of cylindric dendrite with length $dx$. The circuit diagram of this dendritic segment is shown in the schematic drawing below

Using Kirchhoff’s laws, we find equations that relate the voltage $v(x)$ across the membrane at location $x$ with longitudinal and transversal currents. First, a longitudinal current $i(x)$ passing through the dendrite causes a voltage drop across the longitudinal resistor $R_L$ according to Ohm’s law,

\begin{equation}
v(x+dx,t)−v(x,t)=-R_Li(x,t) \ \ \ \ \ \ \ \   (1)
\end{equation}

where $v(x+dx,t)$ is the membrane potential at the neighbouring point $x+dx$. Second, the transversal current that passes through the RC-circuit is given by $C\frac{\partial }{\partial t}v(x,t)+\sum_{ion}I_{ion}-I_{ext}$, where the sum runs over all ion channel types present in the dendrite. Kirchhoff’s law regarding the conservation of current at each node leads to

\begin{equation}
-i(x+dx,t)+i(x,t)=C\frac{\partial }{\partial t}v(x,t)+\sum_{ion}I_{ion}-I_{ext}(x,t).   \ \ \ \ \ \ \ \   (2)
\end{equation}

The values of the longitudinal resistance $R_L$, the capacity $C$, the ionic currents as well as the externally applied current can be expressed in terms of specific quantities per unit length $r_L$, $c$, $i_{ion}$ and $i_{ext}$, respectively.

\begin{equation}
R_L=r_Ldx, C=cdx, I_{ext}(x,t)=i_{ext}(x,t)dx, I_{ion}(x,t)=i_{ion}(x,t)dx.  \ \ \ \ \ \ \ \   (3)
\end{equation}

These scaling relations express the fact that the longitudinal resistance and the capacitance increases with the length of the cylinder. Similarly, the total amount of transversal current increases with the length $dx$ simply because the surface through which the current can pass is increasing. Substituting these expressions in Eqs. (1) and (2), dividing by $dx$, and taking the limit $dx→0$ leads to

\begin{equation}
\begin{split}
\frac{\partial}{\partial x}v(x,t)&=r_Li(x,t)\ \ \ \ \ \ \ \   (4a)\\
\frac{\partial}{\partial x}i(x,t)&=c\frac{\partial }{\partial t}v(x,t)+\sum_{ion}i_{ion}(x,t)−i_{ext}(x,t).\ \ \ \ \ \ \ \   (4b)
\end{split}
\end{equation}

Taking the derivative of equation (4a) with respect to $x$ and substituting the result into (4b) yields

\begin{equation}
\frac{\partial^2}{\partial x^2}v(x,t)=cr_L\frac{\partial}{\partial t}v(x,t)+r_L\sum_{ion}i_{ion}(x,t)−r_Li_{ext}(x,t). \ \ \ \ \ \ \ \   (5)
\end{equation}

Eq. (5) is called the general cable equation.

##  Cable Equation for Passive Linear Dendrite
The ionic currents $\sum_{ion}i_{ion}(x,t)$ in Eq. (5) can in principle comprise many different types of ion channels. For simplicity, the dendrite is sometimes considered as passive. This means that the current density follows Ohm's law $\sum_{ion}i_{ion}(x,t)=g_L(v−e_L)$ where $g_L=1/r_T$ is the leak conductance per unit length and $e_L$ is the leak reversal potential.

To further simplify Eq. (5), we introduce the characteristic length scale $\lambda^2=r_T/r_L$ (electrotonic length scale) and the membrane time constant $τ=r_Tc$. If we multiply Eq. (5) by $\lambda^2$, we get

\begin{equation}
\lambda^2\frac{\partial^2}{\partial x^2}v(x,t)=\tau\frac{\partial}{\partial t}v(x,t)+[v(x,t)−e_L]-r_Ti_{ext}(x,t).
\end{equation}

After a transformation to unit-free coordinates,

\begin{equation}
x→\hat{x}=x/λ, t→\hat{t}=t/τ,
\end{equation}

and a rescaling of the current and voltage variables,

\begin{equation}
i_{ext}→\hat{i}_{ext}   =r_Ti_{ext}, v→\hat{v}=v−e_L,
\end{equation}

we obtain the cable equation (where we have dropped the hats)

\begin{equation}
\frac{\partial}{\partial t}v(x,t)=\frac{\partial^2}{\partial x^2}v(x,t)−v(x,t)+i_{ext}(x,t),   \ \ \ \ \ \ \ \   (6)
\end{equation}

in an elegant unit-free form.

The cable equation can be easily interpreted. The change in time of the voltage $v$ at location $x$ is determined by three different contributions. On the right-hand side of Eq. (6), the first term $\frac{\partial^2}{\partial x^2}v(x,t)$ is a diffusion term that is positive if the voltage $v(x,t)$ is a convex function of $x$. The voltage $v(x,t)$ thus tends to increase, if the values of $v$ are higher in a neighborhood of $x$ than at $x$ itself. The second term $−v(x,t)$ is a simple decay term that causes the voltage to decay exponentially towards zero (i.e., the resting state). The third term is a source term that drives the voltage away from zero (i.e., the resting state). This source can be due to an externally applied current or to synaptic input arriving at location $x$.

##  Cable Equation for Active Nonlinear Dendrite
In the context of a realistic modeling of biological neurons, two non-linear extensions of the cable equation will be discussed as follows. The obvious one is the inclusion of non-linear elements in the circuit diagram that account for specialized ion channels. As we have seen in the Hodgkin-Huxley model, ion channels can exhibit a complex dynamics that is in itself governed by a system of (ordinary) differential equations. The current through one of these channels could be a non-linear function of the membrane potential value. Using the symbolic notation $i_{ion}[v](x,t)$ for this functional dependence, the extended cable equation takes the form

\begin{equation}
\frac{\partial}{\partial t}v(x,t)=\frac{\partial^2}{\partial x^2}v(x,t)−v(x,t)-i_{ion}[v](x,t)+i_{ext}(x,t).
\end{equation}

A more subtle complication arises from the fact that a synapse can not be treated as an ideal current source. The effect of an incoming action potential is the opening of ion channels. The resulting current is proportional to the difference of the membrane potential and the corresponding ionic reversal potential. Hence, a time-dependent conductivity provides a more realistic description of synaptic input than an ideal current source with a fixed time course.

If we replace the external input current $i_{ext}(x,t)$ in Eq. (6) by an appropriate synaptic input current $i_{syn}(x,t)=-g_{syn}(x,t)[v(x,t)-e_{syn}]$ with $g_{syn}$ being the synaptic conductivity and $e_{syn}$ the corresponding reversal potential, we obtain

\begin{equation}
\frac{\partial}{\partial t}v(x,t)=\frac{\partial^2}{\partial x^2}v(x,t)−v(x,t)−g_{syn}(x,t)[v(x,t)−e_{syn}].
\end{equation}

## Analytical Solutions to Passive Cable Model
In order to get an intuitive understanding of the behavior of the cable equation of a passive dendrite, we look for stationary solutions of Eq. (6), i.e., for solutions with $\frac{\partial}{\partial t}v(t,x)=0$. In that case, the partial differential equation reduces to an ordinary differential equation in $x$,

\begin{equation}
\frac{\partial^2}{\partial x^2}v(x)−v(x)=−i_{ext}(x).
\end{equation}

Solutions for non-vanishing input current can be found by standard techniques such as Green's function method (see Tuckwell 1988). For an infinitely-long cable with boundary conditions $v(±\infty)=0$, given a stationary input current $i_{ext}(t,x)$=$\delta(x)$ localized at $x=0$, we can find the solution as

\begin{equation}
G(t,x)=\frac{1}{2}e^{-|x|} \ \ \ \ \ \ \ \   (7)
\end{equation}

The solution to a $\delta$-pulse input is known as the Green's function of the equation. Note that this solution is given in units of the intrinsic length scale $\lambda=\sqrt{r_T/r_L}$. If we re-substitute the physical units, we see that $\lambda$ is the length over which the stationary membrane potential drops by a factor $1/e$. In the literature $\lambda$ is referred to as the electrotonic length scale. A typical electrotonic length scale for a passive cable is about $\lambda \approx 1 mm$. However, the electrotonic length can be significantly smaller if the transversal conductivity is increased, e.g., due to the open of ion channels.

For arbitrary stationary input current $i_{ext}(x)$, the solution of Eq. (6) for an infinitely long cable can be found by a superposition of translated fundamental solutions [Eq. (7)],

\begin{equation}
v(x)=\int_{-\infty}^{+\infty} G(x-y)i_{ext}(y)  dy=\int_{-\infty}^{+\infty} \frac{1}{2}e^{-|x-y|}i_{ext}(y) dy.  \ \ \ \ \ \ \ \   (8)
\end{equation}

Suppose a current pulse $i_{ext}(t,x)=\delta(x)\delta(t)$ is injected at time $t=0$ at location $x=0$. The time course of the voltage at an arbitrary position $x$ can be derived as

\begin{equation}
G(t,x)=\frac{\Theta(t)}{\sqrt{4πt}}exp\Big(−t−\frac{x^2}{4t}\Big),
\end{equation}

where $\Theta(t)$ is the Heaviside function, i.e., $\Theta(t)=1$ for $t>0$ and $\Theta(t)=0$ otherwise. Knowing the Green’s function, the solution for an infinitely long cable is given by

\begin{equation}
v(t,x)=\int_{0}^{+\infty}dt\int_{-\infty}^{+\infty}  dy G(t-\tau,x-y)i_{ext}(t-\tau,y).
\end{equation}

The Green’s function is therefore a particularly elegant and useful mathematical tool. Once we have solved the linear cable equation for a single short current pulse, you can write down the full solution to arbitrary input as an integral over (hypothetical) pulse-inputs at all places and all times.

The Green's function for other cable models can be found in the book "Introduction to Theoretical Neurobiology" by Henry C. Tuckwell. For example, a dendritic cable with a semi-infinite long length, a dendritic cable with finite length, and a dendritic cable with a finite length connecting to a spherical soma.

## Multi-compartment Cable Model

We have seen that analytical solutions can be solved for the voltage along a passive cable with uniform geometrical and electrical properties. If we want to apply the above results in order to describe the membrane potential along the dendritic tree of a neuron we face several problems. Even if we neglect active conductances formed by non-linear ion channels, a dendritic tree is at most locally equivalent to an uniform cable. Numerous bifurcations and variations in diameter and electrical properties along the dendrite render it difficult to find a solution for the membrane potential analytically. Therefore, we do numerics!

Numerical treatment of the cable equation requires a discretization of the spatial variable. Hence, all derivatives with respect to spatial variables are approximated by the corresponding quotient of differences. Essentially we are led back to the discretized model that has been used as the starting point for the derivation of the cable equation. After the discretization we have a large system of ordinary differential equations for the membrane potential at the chosen discretization points as a function of time. This system of ordinary differential equations can be treated by standard numerical methods.

In order to solve for the membrane potential of a complex dendritic tree numerically, compartmental models are used that are the result of the above mentioned discretization. The dendritic tree is divided into small cylindric compartments with an approximatively uniform membrane potential. Each compartment is characterized by its capacity and transversal conductivity. Adjacent compartments are coupled by the longitudinal resistance that are determined by their geometrical properties (see the schematic diagram below).

Correspondingly, Eq. (6) for a single dendritic compartment changes to be

\begin{equation}
\frac{d}{dt}v_k(t)=\frac{[v_{k-1}(t)-2v_{k}(t)+v_{k+1}(t)]}{(\Delta x)^2} −v_k(t)+i^{ext}_{k}(t)
\end{equation}

for multiple-compartment case, where $v_k$ is the membrane potential at the $k$th compartment, and $i^{ext}_{k}(t)$ is the externally injected current at the $k$th compartment.

## Simulation of a Spatial Neuron
As mentioned above, the analytical solution for a spatial neuron with complicated morphology and nonlinear dynamics is in general difficult to solve. To understand the function of dendrites, computational simulation is a helpful approach. We next introduce how to simulate a spatial neuron with dendrites using the simulation environment **Brian 2**. The introduction of Brian 2 and the corresponding documents can be found in https://brian2.readthedocs.io/en/stable/index.html. The installation of Brian 2 shall follow the [link](https://brian2.readthedocs.io/en/stable/introduction/install.html).

You can either install Brian 2 in the Anaconda root environment, or [create a new environment](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html) for Brian 2. The latter has the advantage that you can update (or not update) the dependencies of Brian 2 independently from the rest of your system.

```bash
$ conda install -c conda-forge brian2
```

*Note: Brian 2 is not part of the main Anaconda distribution, but built using the community-maintained conda-forge project. You will therefore have to to install it from the conda-forge channel, as shown above.*

After the installation of Brian 2, we can initialize the simulation parameters and create a cylindric dendrite with length $3 mm$ (a very long dendrite) and diameter $2 \mu m$ by executing the cell below. Note that every quantity defined in Brain2 needs to specify a correct unit! Brain2 will check the consistancy of dimension during simulation.

```python
from brian2 import *  # import all module contained in Brian2,
                      #   including basic array and plot functions.

#time step to simulate the neuron model
defaultclock.dt = 0.01*ms

# morphology
diameter = 2*um       # diameter of the dendrite
length = 3*mm         # length of the dendrite
N = 600               # segment number of the dendrite
morpho = Cylinder(diameter=diameter,
                  length=length,
                  n=N) # create a cylinder in Brain2 with the geometrical parameters

# passive parameters
Cm = 1*uF/cm**2         # capacitance
Ri = 150*ohm*cm         # axial resisance
gL = 1e-4*siemens/cm**2 # leak conductance
EL = -70*mV             # leakage potential

# current dynamics in each segment
eqs = '''
Im = gL * (EL - v) : amp/meter**2
I : amp (point current)
'''

# Create a neuron in Brain2 who only has a single dendrite
neuron = SpatialNeuron(morphology=morpho, model=eqs, Cm=Cm, Ri=Ri, method='exponential_euler')
neuron.v = EL   # initialize the voltage at the leakage (resting) potential

# membrane time constant
taum = Cm/gL
print("Time constant: %s" % taum)
# electrotonic length
la = neuron.space_constant[0]
print("Electrotonic length: %s" % la)
```

    Time constant: 10. ms
    Electrotonic length: 0.57735027 mm

```python
# show morphology in pure text style
morpho.topology()
```

    ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|  [root]

### Typical Examples of Brian2 Default Morphologies
| |**Examples**| |
|--:|:--|:--:|
|**Soma**|`Soma(diameter=30*um)`| |
|**Cylinder**|`Soma(n=5, diameter=10*um, length=50*um)`| |
|**Section**|`Section(n=5, diameter=[15, 5, 10, 5, 10, 5]*um), length=[10,20,5,5,10]*um`| |

### Example 1:
Let's inject a constant current with amplitude $0.02nA$ in the middle of the dendrite by executing the cell below.

```python
# injecting a current at the middle of the dendrite
neuron.I[len(neuron) // 2] = 0.02*nA  # inject a constant current
run(100*ms, report='text')            # run the simulation and print the simulation specs
```

    Starting simulation at t=0. s for a duration of 100. ms
    100. ms (100%) simulated in < 1s

Let's plot the simulation result, and compare it with the analytical solution [Eq. (8)]. When you plot, it is suggested to make the variables dimensionless.

```python
plot(neuron.distance[::50]/um,         # spatial grid with spatial step size equals 50 segments
     neuron.v[::50]/mV,                # membrane potential for corresponding segments
     'navy', marker='o', linestyle='', # curve styling
     ms=12, label='numerics')

# Theoretical result
x = neuron.distance
ra = la * 4 * Ri / (pi * diameter**2)
theory = EL + ra*neuron.I[len(neuron) // 2] * 0.5 * exp(-abs(length/2 - x) / la)
plot(x/um, theory/mV, 'orange', lw = 2, alpha = 1.0, label='theory')
xlabel('x (um)')
ylabel('v (mV)')
legend()
show()
```

**Thinking**: The simulation result is well approximated by the theortical solution derived from the infinite-length cable model receiving stationary current input [Eq. (8)]. _Can you figure out why the two solutions overlap well in the middle of the dendritic cable but not at the end of the dendrite? What happens if you make the dendritic length longer?_

**Exercise 1:** Now it is your time to stimulate the dendrite. Try inject a constant current with amplitude $0.02nA $ at one end of the dendrite $x=0$.

```python
# initialize the input current and the voltage
neuron.I = 0*amp
neuron.v = EL

# insert your code here to inject current at the left end, i.e., x=0
```

```python
# initialize the input current and the voltage
neuron.I = 0*amp
neuron.v = EL

# injecting at the left end x=0
neuron.I[0] = 0.02*nA
run(100*ms, report='text')
```

    Starting simulation at t=100. ms for a duration of 100. ms
    100. ms (100%) simulated in < 1s

Plot the simulation result, and compare it with the analytical solution.

```python
plot(neuron.distance[::50]/um, neuron.v[::50]/mV, 'navy',marker='o', linestyle='', ms=12,label='numerics')

# Theoretical results
x = neuron.distance
ra = la * 4 * Ri / (pi * diameter**2)
theory = EL + ra * neuron.I[0] * cosh((length - x) / la) / sinh(length / la)
plot(x/um, theory/mV, 'orange', lw = 2, alpha = 1.0,label='theory')
xlabel('x (um)')
ylabel('v (mV)')
legend()
show()
```

### Example 2:
In the brain, the input is in general non-constant. Let's give a triansient pulse-input current in the middle of the dendrite, and simulate the dendrite by executing the cell below. Different from the previous case, here we use net.run() instead of run() to simulate the neuron. This is because run() will continue the previous simulation time instead of starting from zero! See details between them: https://brian2.readthedocs.io/en/stable/user/running.html?highlight=net.run

```python
neuron = SpatialNeuron(morphology=morpho, model=eqs, Cm=Cm, Ri=Ri, method = 'exponential_euler')
neuron.v = EL
neuron.I = 0*amp
T=10*ms

#Decorator to make a function get called every time step of a simulation
@network_operation(dt=defaultclock.dt, when='start')
def update_currents(t):
    neuron.I[len(neuron)//2] = 10*nA*(int(t<0.02*ms))  # injecting a pulse input in the middle
                                                       #  during the first 0.02 ms.

# Monitors is used to record the time trace of variables
mon = StateMonitor(neuron, 'v', record=range(0, N//2, N//10))

net = Network(neuron, update_currents, mon)
net.run(T, report='text')
```

    Starting simulation at t=0. s for a duration of 10. ms
    10. ms (100%) simulated in < 1s

Plot the simulation result, and compare it with the analytical solution.

```python
# print the shape of data stored in the monitor
mon.v.T.shape
```

    (1000, 5)

```python
# specify line colors
colors = ['orangered', 'orange', 'darkgoldenrod', 'springgreen', 'darkcyan']
# plot simulated voltage trace
lines = plot(mon.t/ms, mon.v.T/mV, '-')
# set line colors
[line.set_color(colors[i]) for i, line in enumerate(lines)]
# set legends
legend(lines, [f'No. {i:>3d} segs' for i in np.arange(0,N//2,N//10)])

# Theoretical results (incorrect near cable ends)
t = mon.t
color_id = 0
for i in range(0, len(neuron)//2, N//10):
    x = (len(neuron)/2 - i) * morpho.length[0]
    theory = (1/(la*Cm*pi*diameter) * sqrt(taum / (4*pi*(t + defaultclock.dt))) *
              exp(-(t+defaultclock.dt)/taum -taum / (4*(t+defaultclock.dt))*(x/la)**2))
    theory = EL + theory * 10*nA * 0.02*ms
    plot(t/ms, theory/mV,'--', color=colors[color_id])
    color_id += 1
xlabel('Time (ms)')
ylabel('v (mV')
title('Potential Trace of different segments')
show()
```

### Example 3:
A real neuron has a soma in general. So let's create a soma connecting to the dendrite. This is the so-called ball-and-stick model. Let's inject a pulse current $0.2nA$ into the middle of the dendrite for $10 ms$.

```python
#create a soma
morpho = Soma(diameter=30*um)

#create a dendrite attached to the soma
morpho.dendrite = Cylinder(diameter=1*um, length=600*um, n=600)

#create a neuron with the soma-dendrite structure
neuron = SpatialNeuron(morphology=morpho, model=eqs, Cm=Cm, Ri=Ri, method='exponential_euler')
neuron.v = EL

@network_operation(dt=0.01*ms, when='start')
def update_currents(t):
    neuron.I[len(neuron)//2] = 0.2*nA*(t<10*ms)

mon = StateMonitor(neuron, ['v', 'I'], record=True)

net = Network(neuron, update_currents, mon)
net.run(20*ms, report='text')
```

    Starting simulation at t=0. s for a duration of 20. ms
    20. ms (100%) simulated in < 1s

Plot the simulation result,

```python
figure(figsize=[10,10])
imshow(mon.v /mV)
colorbar(label="voltage",fraction=0.02)
xlabel("time index")
ylabel("location index")
plt.xticks(ticks=np.array([0.,500., 1000., 1500., 2000.]),
           labels=['{:.0f}'.format(v) for v in np.array([0., 500., 1000., 1500.,2000.])*0.01]);
title("vm at (t,x), raw data voltage_monitor.v");
```

**Exercise 2:** Make a plot to show that the voltage response at the soma is smaller than that at the far end of the dendrite. _Can you figure out why this is true?_

```python
# insert your code here
xlabel("Location (um)")
ylabel("voltage (mV)")
```

```python
# plot voltage profile at t = 10ms
plot(neuron.distance/um, mon.v[:,round(10/0.01)]/mV)
xlabel("Location (um)")
ylabel("voltage (mV)")
```

**Exercise 3:** (Direction selectivity) Direction-selective neurons respond to image motion in a preferred direction but not in the opposite NULL direction. They can be found in many species from fly eyes to mammalian cortex, and in all these cases a role for dendritic computation has been proposed. Rall (1964) provided the first model for how dendrites can implement a directionally selective unit.

The idea is simple: Synaptic input generated on the distal part of the dendrite is delayed at the soma by the dendritic filtering compared with proximal input. If synaptic inputs are activated in a sequence starting from the distal location of the dendrite toward the soma (the centripetal direction), then the EPSPs in the soma will sum effectively and the resulting somatic voltage would be large. In contrast, activating the same inputs in the centrifugal direction would result in a much less degree of summation because the proximal EPSP will decay by the time the distally originated EPSP will arrive. Assuming that the voltage peak is translated into action potentials, the neuron will show directional selectivity. Let's test this idea via simulations.

```python
morpho = Soma(diameter=30*um)
morpho.dendrite = Cylinder(diameter=1*um, length=600*um, n=600)
neuron = SpatialNeuron(morphology=morpho, model=eqs, Cm=Cm, Ri=Ri, method='exponential_euler')

neuron.v = EL

@network_operation(dt=1.*ms, when='start')
def update_currents_prefer(t):
    #from x=600um to x=60 um with an spatial interval of 60 um and time interval of 10 ms
    #the current input can last for 1ms at each synapse with an amplitude of 1nA
    neuron.I = 0
    for id_t in range(10):
        neuron.I[len(neuron)//10*(10-id_t)-1] = neuron.I[len(neuron)//10*(10-id_t)-1] \
        + 1*nA * (int(t<=10.*id_t*ms+1.*ms)-int(t<=10.*id_t*ms))

mon = StateMonitor(neuron, ['v', 'I'], record=0)
net_prefer = Network(neuron, update_currents_prefer, mon)
net_prefer.run(100*ms, report='text')

v_prefer = mon.v[0,:]/mV

morpho = Soma(diameter=30*um)
morpho.dendrite = Cylinder(diameter=1*um, length=600*um, n=600)
neuron = SpatialNeuron(morphology=morpho, model=eqs, Cm=Cm, Ri=Ri, method='exponential_euler')

neuron.v = EL
@network_operation(dt=1.*ms, when='start')
def update_currents_nonprefer(t):
    #from x=60um to x=600 um with an spatial interval of 60 um and time interval of 10 ms
    #the current input can last for 1ms at each synapse with an amplitude of 1nA
    neuron.I = 0
    for id_t in range(10):
        neuron.I[len(neuron)//10*(id_t+1)-1] = neuron.I[len(neuron)//10*(id_t+1)-1] \
        + 1*nA * (int(t<=10.*id_t*ms+1.*ms)-int(t<=10.*id_t*ms))

mon = StateMonitor(neuron, ['v', 'I'], record=0)

net_nonprefer = Network(neuron, update_currents_nonprefer, mon)
net_nonprefer.run(100*ms, report='text')

v_nonprefer = mon.v[0,:]/mV
```

    Starting simulation at t=0. s for a duration of 100. ms
    100. ms (100%) simulated in < 1s
    Starting simulation at t=0. s for a duration of 100. ms
    100. ms (100%) simulated in < 1s

```python
#plot the results
plot(mon.t/ms, v_prefer, 'navy', label='prefer')
plot(mon.t/ms, v_nonprefer, 'orange', label='nonprefer')
xlabel('Time (ms)')
ylabel('Somatic voltage (mV)')
legend(loc='best');

```

## Extra question if you still have time!
**Exercise 5:** (Bilinear dendritic integration rule) In recent experiments (Hao et al, PNAS, 2009; **Li et al, PLOS Comp. Biol. 2014**), given a pair of excitatory input and inhibitory input simultaneously to the dendrite of a hippocampal CA1 pyramidal neuron, the SSP measured at the soma denoted by $V_S$ can be well characterized by a bilinear rule as $V_S = V_E +V_I +k_{EI} · V_EV_I$ , where $V_E$ and $V_I$ are the respective EPSP and IPSP measured at the soma when the E or I input is given alone, and $k_{EI}$ is the shunting coefficient which depends on the dendritic locations and arrival times of the E and I inputs but not the strengths of the inputs.

Let's verify this bilinear rule in the passive ball-and-stick model using computer simulations by execute the following code.

```python
# Simulating the neuronal response given synaptic inputs with conductance
start_scope()
# model parameters are the same as previously defined
# Cm = 1*uF/cm**2         # capacitance
# Ri = 150*ohm*cm         # axial resisance
# gL = 1e-4*siemens/cm**2 # leak conductance
# EL = -70*mV             # leakage potential
# define ball and stick morphology
t0 = time.time()
morpho = Soma(diameter=30*um)
morpho.dendrite = Cylinder(diameter=2*um, length=600*um, n=600)

eqs = '''
Im = gL * (EL - v) : amp/meter**2
Ie = gE * (VE-v) : amp (point current)
Ii = gI * (VI-v) : amp (point current)
dgE/dt = -gE/gEtau : siemens
dgI/dt = -gI/gItau : siemens
'''
# the reversal potentials for E and I synaptic conductances
VE=0*mV
VI=-80*mV

# time constants for E and I synaptic condutances
gEtau=2*ms
gItau=5*ms

# create a neuron and set its initial voltage
neuron = SpatialNeuron(morphology=morpho, model=eqs, Cm=Cm, Ri=Ri, method = 'exponential_euler')
neuron.v = EL

# E and I synaptic input locations
E_loc=round(len(neuron)*0.3)
I_loc=round(len(neuron)*0.5)

# run multiple trials and record EPSP, IPSP
trial_n=10
# Synaptic input
E_times = np.arange(3*trial_n)*100*ms
I_times = np.arange(3*trial_n)*100*ms
E_stim = SpikeGeneratorGroup(1, [0]*E_times.shape[0], E_times)
I_stim = SpikeGeneratorGroup(1, [0]*I_times.shape[0], I_times)
E_syn = Synapses(E_stim, neuron, model='w:siemens', on_pre='gE+=w')
I_syn = Synapses(I_stim, neuron, model='w:siemens', on_pre='gI+=w')
E_syn.connect(i=0, j=E_loc)
I_syn.connect(i=0, j=I_loc)
E_syn.w = 0*siemens
I_syn.w = 0*siemens

# Monitors
mon = StateMonitor(neuron, 'v', record=0)

net = Network(neuron, mon, E_stim, I_stim, E_syn, I_syn)
print(f'initialization takes :{time.time()-t0:.3f} s')

def reset():
    neuron.v=EL
    neuron.gE=0*siemens
    neuron.gI=0*siemens
    E_syn.w = 0*siemens
    I_syn.w = 0*siemens

t0 = time.time()
# run the simulation
for i in range(trial_n):
    # stimulate the neuron with a pair of E and I synaptic inputs
    # given either separately or simulaneously the input strength is randomly chosen
    gE_rand = 10.0 *nS * np.random.rand()
    gI_rand = 5.0 *nS * np.random.rand()
    # reset network state, and simulate Exc. input only
    reset()
    E_syn.w = gE_rand
    net.run(100*ms)
    # reset network state, and simulate Inh. input only
    reset()
    I_syn.w = gI_rand
    net.run(100*ms)
    # reset network state, and simulate both Exc. and Inh. input
    reset()
    E_syn.w = gE_rand
    I_syn.w = gI_rand
    net.run(100*ms)

print(f'simulation takes :{time.time()-t0:.3f} s')
# make the variables dimensionless for plot
t = mon.t/ms
V = mon.v[0,:]/mV
```

    initialization takes :0.166 s
    simulation takes :35.019 s

```python
figure(figsize=(12,4))
plot(t, V, label='somatic potential')
[axvline(i*300, color='grey', ls='--') for i in range(0,trial_n+1)];

```

```python
# Summed response at time t=10 ms (you can try different time point)
t_sample = 10

VE = V[((np.arange(trial_n)*300+t_sample)*ms/defaultclock.dt).astype(int)]
VI = V[((np.arange(trial_n)*300+100+t_sample)*ms/defaultclock.dt).astype(int)]
VS = V[((np.arange(trial_n)*300+200+t_sample)*ms/defaultclock.dt).astype(int)]
```

```python
#plot the results
x=-(VE-EL/mV)*(VI-EL/mV)   #dimension of EL shall match V!
y=VE+VI-VS-EL/mV

plot(x, y,'ko',markersize=10)
z=polyfit(x, y, 1)    # linear fitting
xp = linspace(0,max(x)*1.1,100)
plot(xp,xp*z[0]+z[1])
plt.xlabel('EPSP*IPSP (mV^2)')
plt.ylabel('EPSP+IPSP-SSP (mV)');
```

After you understand the code, you can try to modify the code to investigate whether the bilinear rule holds for a pair of E and I current input?

```python
#insert your code here
#no answer yet
```

Also, you can investigate how general the rule is for synaptic condutance inputs. For example, when the neuron receives a pair of E inputs, a pair of I inputs, or multiple inputs, does the bilinear rule still hold?

```python
#insert your code here
#no answer yet
```

Finally, you can investigate how does the shunting coefficient $k_{EI}$ depend on the input location? From your result, can you tell what is the most effective way for an inhibitory input to shunt an excitatory input in this passive neuron?

```python
#insert your code here
#no answer yet
```

## Detailed Reconstruction of a Neuron

Software tools such as [NEURON](https://neuron.yale.edu) enable researchers to construct detailed compartmental models of any type of neuron. The morphology of such a detailed model is constrained by the anatomical reconstruction of the corresponding ‘real’ neuron. This is possible if length, size and orientation of each dendritic segment are measured under a microscope, after the neuron has been filled with a suitable dye. Before the anatomical reconstruction, the electrophysiological properties of the neuron can be characterized by stimulating the neuron with a time-dependent electric current. The presence of specific ion channel types can be inferred, with genetic methods, from the composition of the intracellular liquid, extracted from the neuron. The distribution of ion channels across the dendrite is probably the least constrained parameter. It is sometimes inferred from another set of experiments on neurons belonging to the same class. All the experimental knowledge about the neuron is then condensed in a computational neuron model.