Please find jax implementation of this notebook here: https://colab.research.google.com/github/probml/pyprobml/blob/master/notebooks/book1/15/rnn_jax.ipynb

# Recurrent neural networks

We show how to implement RNNs from scratch.
Based on sec 8.5 of http://d2l.ai/chapter_recurrent-neural-networks/rnn-scratch.html.

```python
import numpy as np
import matplotlib.pyplot as plt
import math
from IPython import display

try:
    import torch
except ModuleNotFoundError:
    %pip install torch
    import torch
from torch import nn
from torch.nn import functional as F
from torch.utils import data

import collections
import re
import random
import os
import requests
import hashlib
import time

np.random.seed(seed=1)
torch.manual_seed(1)
!mkdir figures # for saving plots
```

# Data

 As data, we use the book "The Time Machine" by H G Wells,
preprocessed using the code in [this colab](https://colab.research.google.com/github/probml/pyprobml/blob/master/notebooks/text_preproc_torch.ipynb).

```python
class SeqDataLoader:
    """An iterator to load sequence data."""

    def __init__(self, batch_size, num_steps, use_random_iter, max_tokens):
        if use_random_iter:
            self.data_iter_fn = seq_data_iter_random
        else:
            self.data_iter_fn = seq_data_iter_sequential
        self.corpus, self.vocab = load_corpus_time_machine(max_tokens)
        self.batch_size, self.num_steps = batch_size, num_steps

    def __iter__(self):
        return self.data_iter_fn(self.corpus, self.batch_size, self.num_steps)

class Vocab:
    """Vocabulary for text."""

    def __init__(self, tokens=None, min_freq=0, reserved_tokens=None):
        if tokens is None:
            tokens = []
        if reserved_tokens is None:
            reserved_tokens = []
        # Sort according to frequencies
        counter = count_corpus(tokens)
        self.token_freqs = sorted(counter.items(), key=lambda x: x[1], reverse=True)
        # The index for the unknown token is 0
        self.unk, uniq_tokens = 0, ["<unk>"] + reserved_tokens
        uniq_tokens += [token for token, freq in self.token_freqs if freq >= min_freq and token not in uniq_tokens]
        self.idx_to_token, self.token_to_idx = [], dict()
        for token in uniq_tokens:
            self.idx_to_token.append(token)
            self.token_to_idx[token] = len(self.idx_to_token) - 1

    def __len__(self):
        return len(self.idx_to_token)

    def __getitem__(self, tokens):
        if not isinstance(tokens, (list, tuple)):
            return self.token_to_idx.get(tokens, self.unk)
        return [self.__getitem__(token) for token in tokens]

    def to_tokens(self, indices):
        if not isinstance(indices, (list, tuple)):
            return self.idx_to_token[indices]
        return [self.idx_to_token[index] for index in indices]
```

```python
def tokenize(lines, token="word"):
    """Split text lines into word or character tokens."""
    if token == "word":
        return [line.split() for line in lines]
    elif token == "char":
        return [list(line) for line in lines]
    else:
        print("ERROR: unknown token type: " + token)

def count_corpus(tokens):
    """Count token frequencies."""
    # Here `tokens` is a 1D list or 2D list
    if len(tokens) == 0 or isinstance(tokens[0], list):
        # Flatten a list of token lists into a list of tokens
        tokens = [token for line in tokens for token in line]
    return collections.Counter(tokens)

def seq_data_iter_random(corpus, batch_size, num_steps):
    """Generate a minibatch of subsequences using random sampling."""
    # Start with a random offset (inclusive of `num_steps - 1`) to partition a
    # sequence
    corpus = corpus[random.randint(0, num_steps - 1) :]
    # Subtract 1 since we need to account for labels
    num_subseqs = (len(corpus) - 1) // num_steps
    # The starting indices for subsequences of length `num_steps`
    initial_indices = list(range(0, num_subseqs * num_steps, num_steps))
    # In random sampling, the subsequences from two adjacent random
    # minibatches during iteration are not necessarily adjacent on the
    # original sequence
    random.shuffle(initial_indices)

    def data(pos):
        # Return a sequence of length `num_steps` starting from `pos`
        return corpus[pos : pos + num_steps]

    num_batches = num_subseqs // batch_size
    for i in range(0, batch_size * num_batches, batch_size):
        # Here, `initial_indices` contains randomized starting indices for
        # subsequences
        initial_indices_per_batch = initial_indices[i : i + batch_size]
        X = [data(j) for j in initial_indices_per_batch]
        Y = [data(j + 1) for j in initial_indices_per_batch]
        yield torch.tensor(X), torch.tensor(Y)

def seq_data_iter_sequential(corpus, batch_size, num_steps):
    """Generate a minibatch of subsequences using sequential partitioning."""
    # Start with a random offset to partition a sequence
    offset = random.randint(0, num_steps)
    num_tokens = ((len(corpus) - offset - 1) // batch_size) * batch_size
    Xs = torch.tensor(corpus[offset : offset + num_tokens])
    Ys = torch.tensor(corpus[offset + 1 : offset + 1 + num_tokens])
    Xs, Ys = Xs.reshape(batch_size, -1), Ys.reshape(batch_size, -1)
    num_batches = Xs.shape[1] // num_steps
    for i in range(0, num_steps * num_batches, num_steps):
        X = Xs[:, i : i + num_steps]
        Y = Ys[:, i : i + num_steps]
        yield X, Y
```

```python
def download(name, cache_dir=os.path.join("..", "data")):
    """Download a file inserted into DATA_HUB, return the local filename."""
    assert name in DATA_HUB, f"{name} does not exist in {DATA_HUB}."
    url, sha1_hash = DATA_HUB[name]
    os.makedirs(cache_dir, exist_ok=True)
    fname = os.path.join(cache_dir, url.split("/")[-1])
    if os.path.exists(fname):
        sha1 = hashlib.sha1()
        with open(fname, "rb") as f:
            while True:
                data = f.read(1048576)
                if not data:
                    break
                sha1.update(data)
        if sha1.hexdigest() == sha1_hash:
            return fname  # Hit cache
    print(f"Downloading {fname} from {url}...")
    r = requests.get(url, stream=True, verify=True)
    with open(fname, "wb") as f:
        f.write(r.content)
    return fname

def read_time_machine():
    """Load the time machine dataset into a list of text lines."""
    with open(download("time_machine"), "r") as f:
        lines = f.readlines()
    return [re.sub("[^A-Za-z]+", " ", line).strip().lower() for line in lines]

def load_corpus_time_machine(max_tokens=-1):
    """Return token indices and the vocabulary of the time machine dataset."""
    lines = read_time_machine()
    tokens = tokenize(lines, "char")
    vocab = Vocab(tokens)
    # Since each text line in the time machine dataset is not necessarily a
    # sentence or a paragraph, flatten all the text lines into a single list
    corpus = [vocab[token] for line in tokens for token in line]
    if max_tokens > 0:
        corpus = corpus[:max_tokens]
    return corpus, vocab

def load_data_time_machine(batch_size, num_steps, use_random_iter=False, max_tokens=10000):
    """Return the iterator and the vocabulary of the time machine dataset."""
    data_iter = SeqDataLoader(batch_size, num_steps, use_random_iter, max_tokens)
    return data_iter, data_iter.vocab
```

```python
DATA_HUB = dict()
DATA_URL = "http://d2l-data.s3-accelerate.amazonaws.com/"
DATA_HUB["time_machine"] = (DATA_URL + "timemachine.txt", "090b5e7e70c295757f55df93cb0a180b9691891a")

batch_size, num_steps = 32, 35
train_iter, vocab = load_data_time_machine(batch_size, num_steps)
```

    Downloading ../data/timemachine.txt from http://d2l-data.s3-accelerate.amazonaws.com/timemachine.txt...

# Model

We fit an unconditional RNN, for language modeling (ie. not vec2seq or seq2seq. Following the D2L notation, the model has the form
$$\begin{align}
H_t &= \phi(X_t W_{xh} + H_{t-1}  W_{hh} + b_h) \\
O_t &= H_t W_{hq} + b_q
\end{align}
$$
where $X_t$ is the $(n,d)$ matrix of (one-hot) inputs
(for batch size $n$ and vocabulary size $d$),
$H_t$ is the $(n,h)$ matrix of hidden states
(for $h$ hidden states),
and $O_t$ is the $(n,q)$ matrix of output logits
(for $q$ output labels, often $q=d$).

```python
# Create the initial parameters

def get_params(vocab_size, num_hiddens, device):
    num_inputs = num_outputs = vocab_size

    def normal(shape):
        return torch.randn(size=shape, device=device) * 0.01

    # Hidden layer parameters
    W_xh = normal((num_inputs, num_hiddens))
    W_hh = normal((num_hiddens, num_hiddens))
    b_h = torch.zeros(num_hiddens, device=device)
    # Output layer parameters
    W_hq = normal((num_hiddens, num_outputs))
    b_q = torch.zeros(num_outputs, device=device)
    # Attach gradients
    params = [W_xh, W_hh, b_h, W_hq, b_q]
    for param in params:
        param.requires_grad_(True)
    return params
```

```python
# Create the initial state
# We assume this is a tuple of one element (later we will use longer tuples)
def init_rnn_state(batch_size, num_hiddens, device):
    return (torch.zeros((batch_size, num_hiddens), device=device),)
```

```python
# Forward function.
# Input sequence is (T,B,V), where T is length of the sequence, B is batch size, V is vocab size.
# We iterate over each time step, and process the batch (for that timestep in parallel).
# Output sequence is (T*B, V), since concatante all the time steps.
# We also return the final state, so we can process the next subsequence.

def rnn(inputs, state, params):
    # Here `inputs` shape: (`num_steps`, `batch_size`, `vocab_size`)
    W_xh, W_hh, b_h, W_hq, b_q = params
    (H,) = state
    outputs = []
    # Shape of `X`: (`batch_size`, `vocab_size`)
    for X in inputs:
        H = torch.tanh(torch.mm(X, W_xh) + torch.mm(H, W_hh) + b_h)
        Y = torch.mm(H, W_hq) + b_q
        outputs.append(Y)
    return torch.cat(outputs, dim=0), (H,)
```

```python
# Make the model class
# Input X to call is (B,T) matrix of integers (from vocab encoding).
# We transpse this to (T,B) then one-hot encode to (T,B,V), where V is vocab.
# The result is passed to the forward function.
# (We define the forward function as an argument, so we can change it later.)

class RNNModelScratch:
    """A RNN Model implemented from scratch."""

    def __init__(self, vocab_size, num_hiddens, device, get_params, init_state, forward_fn):
        self.vocab_size, self.num_hiddens = vocab_size, num_hiddens
        self.params = get_params(vocab_size, num_hiddens, device)
        self.init_state, self.forward_fn = init_state, forward_fn

    def __call__(self, X, state):
        X = F.one_hot(X.T, self.vocab_size).type(torch.float32)
        return self.forward_fn(X, state, self.params)

    def begin_state(self, batch_size, device):
        return self.init_state(batch_size, self.num_hiddens, device)
```

```python
def try_gpu(i=0):
    """Return gpu(i) if exists, otherwise return cpu()."""
    if torch.cuda.device_count() >= i + 1:
        return torch.device(f"cuda:{i}")
    return torch.device("cpu")
```

```python
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, try_gpu(), get_params, init_rnn_state, rnn)
```

```python
X = torch.arange(10).reshape((2, 5))  # batch 2, sequence length is 5
state = net.begin_state(X.shape[0], try_gpu())
print(len(state))  # length 1
print(state[0].shape)  # (2,512)

Y, new_state = net(X.to(try_gpu()), state)
print(len(vocab))  # 28
print(Y.shape)  # (2x5, 28)
```

    1
    torch.Size([2, 512])
    28
    torch.Size([10, 28])

# Prediction (generation)

We pass in an initial prefix string, that is not generated. This is used to "warm-up" the hidden state. Specifically, we update the hidden state given the observed prefix, but don't generate anything. After that, for each of the T steps, we compute the (1,V) output tensor, pick the argmax index, and append it to the output. Finally, we convert the to indices to readable token sequence of size (1,T). (Note that this is a greedy, deterministic procedure.)

(D2L calls this `predict_ch8` since it occurs in their chapter 8.)

```python
def predict(prefix, num_preds, net, vocab, device):
    """Generate new characters following the `prefix`."""
    state = net.begin_state(batch_size=1, device=device)
    outputs = [vocab[prefix[0]]]
    get_input = lambda: torch.tensor([outputs[-1]], device=device).reshape((1, 1))
    for y in prefix[1:]:  # Warm-up period
        _, state = net(get_input(), state)
        outputs.append(vocab[y])
    for _ in range(num_preds):  # Predict `num_preds` steps
        y, state = net(get_input(), state)
        outputs.append(int(y.argmax(dim=1).reshape(1)))
    return "".join([vocab.idx_to_token[i] for i in outputs])
```

```python
# sample 10 characters after the prefix.
# since the model is untrained, the results will be garbage.
predict("time traveller ", 10, net, vocab, try_gpu())
```

    'time traveller hmykxtqtqt'

# Training

To ensure the gradient doesn't blow up when doing backpropagation through many layers, we use gradient clipping, which corresponds to the update
$$
g := \min(1, \theta /||g||) g
$$
where $\theta$ is the scaling parameter, and $g$ is the gradient vector.

```python
def grad_clipping(net, theta):
    """Clip the gradient."""
    if isinstance(net, nn.Module):
        params = [p for p in net.parameters() if p.requires_grad]
    else:
        params = net.params
    norm = torch.sqrt(sum(torch.sum((p.grad**2)) for p in params))
    if norm > theta:
        for param in params:
            param.grad[:] *= theta / norm
```

The training step is fairly standard, except for the use of gradient clipping, and the issue of the hidden state.
If the data iterator uses random ordering of the sequences, we need to initialize the hidden state for each minibatch. However, if the data iterator uses sequential ordering, we only initialize the hidden state at the very beginning of the process. In the latter case, the hidden state will depend on the value at the previous minibatch. We detach the state vector to prevent gradients flowing across minibatch boundaries.

The state vector may be a tensor or a tuple, depending on what kind of RNN we are using. In addition, the parameter updater can be a built-in optimizer, or the simpler D2L sgd optimizer.

```python
class Animator:
    """For plotting data in animation."""

    def __init__(
        self,
        xlabel=None,
        ylabel=None,
        legend=None,
        xlim=None,
        ylim=None,
        xscale="linear",
        yscale="linear",
        fmts=("-", "m--", "g-.", "r:"),
        nrows=1,
        ncols=1,
        figsize=(3.5, 2.5),
    ):
        # Incrementally plot multiple lines
        if legend is None:
            legend = []
        display.set_matplotlib_formats("svg")
        self.fig, self.axes = plt.subplots(nrows, ncols, figsize=figsize)
        if nrows * ncols == 1:
            self.axes = [
                self.axes,
            ]
        # Use a lambda function to capture arguments
        self.config_axes = lambda: set_axes(self.axes[0], xlabel, ylabel, xlim, ylim, xscale, yscale, legend)
        self.X, self.Y, self.fmts = None, None, fmts

    def add(self, x, y):
        # Add multiple data points into the figure
        if not hasattr(y, "__len__"):
            y = [y]
        n = len(y)
        if not hasattr(x, "__len__"):
            x = [x] * n
        if not self.X:
            self.X = [[] for _ in range(n)]
        if not self.Y:
            self.Y = [[] for _ in range(n)]
        for i, (a, b) in enumerate(zip(x, y)):
            if a is not None and b is not None:
                self.X[i].append(a)
                self.Y[i].append(b)
        self.axes[0].cla()
        for x, y, fmt in zip(self.X, self.Y, self.fmts):
            self.axes[0].plot(x, y, fmt)
        self.config_axes()
        display.display(self.fig)
        display.clear_output(wait=True)

class Timer:
    """Record multiple running times."""

    def __init__(self):
        self.times = []
        self.start()

    def start(self):
        """Start the timer."""
        self.tik = time.time()

    def stop(self):
        """Stop the timer and record the time in a list."""
        self.times.append(time.time() - self.tik)
        return self.times[-1]

    def avg(self):
        """Return the average time."""
        return sum(self.times) / len(self.times)

    def sum(self):
        """Return the sum of time."""
        return sum(self.times)

    def cumsum(self):
        """Return the accumulated time."""
        return np.array(self.times).cumsum().tolist()

class Accumulator:
    """For accumulating sums over `n` variables."""

    def __init__(self, n):
        self.data = [0.0] * n

    def add(self, *args):
        self.data = [a + float(b) for a, b in zip(self.data, args)]

    def reset(self):
        self.data = [0.0] * len(self.data)

    def __getitem__(self, idx):
        return self.data[idx]

def set_axes(axes, xlabel, ylabel, xlim, ylim, xscale, yscale, legend):
    """Set the axes for matplotlib."""
    axes.set_xlabel(xlabel)
    axes.set_ylabel(ylabel)
    axes.set_xscale(xscale)
    axes.set_yscale(yscale)
    axes.set_xlim(xlim)
    axes.set_ylim(ylim)
    if legend:
        axes.legend(legend)
    axes.grid()

def sgd(params, lr, batch_size):
    """Minibatch stochastic gradient descent."""
    with torch.no_grad():
        for param in params:
            param -= lr * param.grad / batch_size
            param.grad.zero_()
```

```python
def train_epoch(net, train_iter, loss, updater, device, use_random_iter):
    state, timer = None, Timer()
    metric = Accumulator(2)  # Sum of training loss, no. of tokens
    for X, Y in train_iter:
        if state is None or use_random_iter:
            # Initialize `state` when either it is the first iteration or
            # using random sampling
            state = net.begin_state(batch_size=X.shape[0], device=device)
        else:
            if isinstance(net, nn.Module) and not isinstance(state, tuple):
                # `state` is a tensor for `nn.GRU`
                state.detach_()
            else:
                # `state` is a tuple of tensors for `nn.LSTM` and
                # for our custom scratch implementation
                for s in state:
                    s.detach_()
        y = Y.T.reshape(-1)  # (B,T) -> (T,B)
        X, y = X.to(device), y.to(device)
        y_hat, state = net(X, state)
        l = loss(y_hat, y.long()).mean()
        if isinstance(updater, torch.optim.Optimizer):
            updater.zero_grad()
            l.backward()
            grad_clipping(net, 1)
            updater.step()
        else:
            l.backward()
            grad_clipping(net, 1)
            # batch_size=1 since the `mean` function has been invoked
            updater(batch_size=1)
        metric.add(l * y.numel(), y.numel())
    return math.exp(metric[0] / metric[1]), metric[1] / timer.stop()
```

The main training function is fairly standard.
The loss function is per-symbol cross-entropy, $-\log q(x_t)$, where $q$ is the model prediction from the RNN. Since we compute the average loss across time steps within a batch, we are computing $-\frac{1}{T} \sum_{t=1}^T \log p(x_t|x_{1:t-1})$. The exponential of this is the perplexity (ppl). We plot this metric during training, since it is independent of document length. In addition, we print the MAP sequence prediction following the suffix 'time traveller', to get a sense of what the model is doing.

```python
def train(net, train_iter, vocab, lr, num_epochs, device, use_random_iter=False):
    loss = nn.CrossEntropyLoss()
    animator = Animator(xlabel="epoch", ylabel="perplexity", legend=["train"], xlim=[10, num_epochs])
    # Initialize
    if isinstance(net, nn.Module):
        updater = torch.optim.SGD(net.parameters(), lr)
    else:
        updater = lambda batch_size: sgd(net.params, lr, batch_size)
    num_preds = 50
    predict_ = lambda prefix: predict(prefix, num_preds, net, vocab, device)
    # Train and predict
    for epoch in range(num_epochs):
        ppl, speed = train_epoch(net, train_iter, loss, updater, device, use_random_iter)
        if (epoch + 1) % 10 == 0:
            print(predict_("time traveller"))
            animator.add(epoch + 1, [ppl])
    print(f"perplexity {ppl:.1f}, {speed:.1f} tokens/sec on {str(device)}")
    print(predict_("time traveller"))
    print(predict_("traveller"))
```

```python
num_epochs, lr = 500, 1
device = try_gpu()
torch.manual_seed(42)
num_hiddens = 512
net = RNNModelScratch(len(vocab), num_hiddens, device, get_params, init_rnn_state, rnn)
train(net, train_iter, vocab, lr, num_epochs, try_gpu())
```

    perplexity 1.0, 118807.5 tokens/sec on cuda:0
    time travelleryou can show black is white by argument said filby
    travelleryou can show black is white by argument said filby

```python
num_preds = 100
predict_ = lambda prefix: predict(prefix, num_preds, net, vocab, device)

print(predict_("time traveller"))
print(predict_("the"))
```

    time travelleryou can show black is white by argument said filby but you willnever convince mepossibly not said th
    the earth s surfacebut the great difficulty is this interrupted the psychologistyou can move about in a

```python
num_preds = 500
predict_ = lambda prefix: predict(prefix, num_preds, net, vocab, device)

print(predict_("the"))
```

    the earth s surfacebut the great difficulty is this interrupted the psychologistyou can move about in all directions of space but you cannotmove about in timethat is the germ of my great discovery but you are wrong to saythat we cannot move about in time for instance if i am recalling said the medical man there are balloonsbut before the barket y forit failllentwatler sion ssane the persancablestoubec soug of hhe iten ohthet ne hal so shatlided a conis trea ligut an eediminnyof oslase that us seamo

# Creating a PyTorch module

We now show how to use create an RNN as a module, which is faster than our pure Python implementation.

First we create a single hidden chain to represent the state.

```python
num_hiddens = 256
rnn_layer = nn.RNN(len(vocab), num_hiddens)

batch_size, num_steps = 32, 35
num_layers = 1
state = torch.zeros((num_layers, batch_size, num_hiddens))
state.shape
```

    torch.Size([1, 32, 256])

Now we update the state with a random one-hot tensor of inputs.

```python
X = torch.rand(size=(num_steps, batch_size, len(vocab)))
Y, state_new = rnn_layer(X, state)
Y.shape, state_new.shape
```

    (torch.Size([35, 32, 256]), torch.Size([1, 32, 256]))

Now we make an RNN module.

```python
class RNNModel(nn.Module):
    """The RNN model."""

    def __init__(self, rnn_layer, vocab_size, **kwargs):
        super(RNNModel, self).__init__(**kwargs)
        self.rnn = rnn_layer
        self.vocab_size = vocab_size
        self.num_hiddens = self.rnn.hidden_size
        # If the RNN is bidirectional (to be introduced later),
        # `num_directions` should be 2, else it should be 1.
        if not self.rnn.bidirectional:
            self.num_directions = 1
            self.linear = nn.Linear(self.num_hiddens, self.vocab_size)
        else:
            self.num_directions = 2
            self.linear = nn.Linear(self.num_hiddens * 2, self.vocab_size)

    def forward(self, inputs, state):
        X = F.one_hot(inputs.T.long(), self.vocab_size)
        X = X.to(torch.float32)
        Y, state = self.rnn(X, state)
        # The fully connected layer will first change the shape of `Y` to
        # (`num_steps` * `batch_size`, `num_hiddens`). Its output shape is
        # (`num_steps` * `batch_size`, `vocab_size`).
        output = self.linear(Y.reshape((-1, Y.shape[-1])))
        return output, state

    def begin_state(self, device, batch_size=1):
        if not isinstance(self.rnn, nn.LSTM):
            # `nn.GRU` takes a tensor as hidden state
            return torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), device=device)
        else:
            # `nn.LSTM` takes a tuple of hidden states
            return (
                torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), device=device),
                torch.zeros((self.num_directions * self.rnn.num_layers, batch_size, self.num_hiddens), device=device),
            )
```

Test the untrained model.

```python
device = try_gpu()
net = RNNModel(rnn_layer, vocab_size=len(vocab))
net = net.to(device)
predict("time traveller", 50, net, vocab, device)
```

    'time travellervv<unk>vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv'

Train it. The results are similar to the 'from scratch' implementation, but much faster.

```python
num_epochs, lr = 500, 1
train(net, train_iter, vocab, lr, num_epochs, device)
```

    perplexity 1.3, 396725.0 tokens/sec on cuda:0
    time traveller held in his hand was a glitteringenteo im the gat
    traveller amonccofllenisuon anding thes ou brectassor ghe s

    'time traveller held in his hand was a glitteringenteo im the gat'

```python

```


# Basic python

```python
# Running code
results = [1, 1, 2, 3]
print(max(results))
```

    3

```python
min_value = min(results)
max_value = max(results)
range_values = max_value - min_value
```

```python
range_values
```

    2

```python
scaled = [(r - min_value)/range_values for r in results]
```

```python
scaled
```

    [0.0, 0.0, 0.5, 1.0]

# Executing bash commands

```python
!pip3 freeze
```

    appnope==0.1.0
    attrs==19.1.0
    backcall==0.1.0
    bleach==3.1.0
    certifi==2019.3.9
    chardet==3.0.4
    cycler==0.10.0
    decorator==4.3.2
    defusedxml==0.5.0
    entrypoints==0.3
    idna==2.8
    ipykernel==5.1.0
    ipympl==0.2.1
    ipython==7.3.0
    ipython-genutils==0.2.0
    ipywidgets==7.4.2
    jedi==0.13.3
    Jinja2==2.10
    jsonschema==3.0.1
    jupyter==1.0.0
    jupyter-client==5.2.4
    jupyter-console==6.0.0
    jupyter-core==4.4.0
    jupyterlab==0.35.4
    jupyterlab-server==0.2.0
    kiwisolver==1.0.1
    MarkupSafe==1.1.1
    matplotlib==3.0.3
    mistune==0.8.4
    nbconvert==5.4.1
    nbformat==4.4.0
    notebook==5.7.6
    numpy==1.16.2
    pandas==0.24.1
    pandocfilters==1.4.2
    parso==0.3.4
    pexpect==4.6.0
    pickleshare==0.7.5
    plotly==3.7.0
    prometheus-client==0.6.0
    prompt-toolkit==2.0.9
    ptyprocess==0.6.0
    Pygments==2.3.1
    pyparsing==2.3.1
    pyrsistent==0.14.11
    python-dateutil==2.8.0
    pytz==2018.9
    pyzmq==18.0.1
    qgrid==1.1.1
    qtconsole==4.4.3
    requests==2.21.0
    retrying==1.3.3
    scipy==1.2.1
    seaborn==0.9.0
    Send2Trash==1.5.0
    six==1.12.0
    terminado==0.8.1
    testpath==0.4.2
    tornado==6.0.1
    traitlets==4.3.2
    urllib3==1.24.1
    wcwidth==0.1.7
    webencodings==0.5.1
    widgetsnbextension==3.4.2
    You are using pip version 9.0.1, however version 19.0.3 is available.
    You should consider upgrading via the 'pip install --upgrade pip' command.

```python
! head README.md
```

    # research_code_jupyter

# Jupyter Magics

```python
%time [(r - min_value)/range_values for r in results]
```

    CPU times: user 8 µs, sys: 3 µs, total: 11 µs
    Wall time: 14.1 µs

    [0.0, 0.0, 0.5, 1.0]

```python
%lsmagic
```

    Available line magics:
    %alias  %alias_magic  %autoawait  %autocall  %automagic  %autosave  %bookmark  %cat  %cd  %clear  %colors  %conda  %config  %connect_info  %cp  %debug  %dhist  %dirs  %doctest_mode  %ed  %edit  %env  %gui  %hist  %history  %killbgscripts  %ldir  %less  %lf  %lk  %ll  %load  %load_ext  %loadpy  %logoff  %logon  %logstart  %logstate  %logstop  %ls  %lsmagic  %lx  %macro  %magic  %man  %matplotlib  %mkdir  %more  %mv  %notebook  %page  %pastebin  %pdb  %pdef  %pdoc  %pfile  %pinfo  %pinfo2  %pip  %popd  %pprint  %precision  %prun  %psearch  %psource  %pushd  %pwd  %pycat  %pylab  %qtconsole  %quickref  %recall  %rehashx  %reload_ext  %rep  %rerun  %reset  %reset_selective  %rm  %rmdir  %run  %save  %sc  %set_env  %store  %sx  %system  %tb  %time  %timeit  %unalias  %unload_ext  %who  %who_ls  %whos  %xdel  %xmode

    Available cell magics:
    %%!  %%HTML  %%SVG  %%bash  %%capture  %%debug  %%file  %%html  %%javascript  %%js  %%latex  %%markdown  %%perl  %%prun  %%pypy  %%python  %%python2  %%python3  %%ruby  %%script  %%sh  %%svg  %%sx  %%system  %%time  %%timeit  %%writefile

    Automagic is ON, % prefix IS NOT needed for line magics.

```python

```

# Rich text format

## LaTeX !!

%%latex
\begin{align}
\nabla \cdot \vec{\mathbf{E}} & = 4 \pi \rho \\
\nabla \times \vec{\mathbf{E}}\, +\, \frac1c\, \frac{\partial\vec{\mathbf{B}}}{\partial t} & = \vec{\mathbf{0}} \\
\nabla \cdot \vec{\mathbf{B}} & = 0
\end{align}

$$c = \sqrt{a^2 + b^2}$$

## Free text + images

Supports markdown syntax:
- lists
- headings
- links

# Load python functions

```python
from utils import load_csv
```

```python
data = load_csv('zoo.data')
```

```python
data[0]
```

    ['aardvark',
     '1',
     '0',
     '0',
     '1',
     '0',
     '0',
     '1',
     '1',
     '1',
     '1',
     '0',
     '0',
     '4',
     '0',
     '0',
     '1',
     '1']

```python

```


```python
from IPython.display import HTML
tag = HTML('''
Promijeni vidljivost <a href="javascript:code_toggle()">ovdje</a>.''')
display(tag)
```

Promijeni vidljivost ovdje.

```python
# Erasmus+ ICCT project (2018-1-SI01-KA203-047081)

%matplotlib notebook
import numpy as np
import math
import matplotlib.pyplot as plt
from scipy import signal
import ipywidgets as widgets
import control as c
import sympy as sym
from IPython.display import Latex, display, Markdown # For displaying Markdown and LaTeX code
from fractions import Fraction
import matplotlib.patches as patches
```

## Sustavi prvog reda bez nula

### Uvod

Sustavi prvog reda bez nula su karakterizirani sljedećom prijenosnom funkcijom:

\begin{equation}
    G(s)=\frac{k}{s+k}.
\end{equation}

Vrijednost $k$ je važna jer definira sljdeće parametre:
- $1/k$ označava *vremensku konstantu* odziva, koja definira vrijeme potrebno da step-odziv dosegne $\approx$ 63% svoje konačne vrijednosti.
- $t_r$ označava *vrijeme porasta*, tj. vrijeme potrebno da odziv sustava poraste s 10\% na 90\% stacionarne vrijednosti.
- $t_s$ označava *vrijeme smirivanja*, tj. vrijeme potrebno da odziv sustava posljednji put bude izvan opsega pogreške  (npr. 2\% kako je postavljeno u primjeru dolje).

Step-odziv ovih sustava izražava se kao:

\begin{equation}
    c(t)=1-e^{-at},
\end{equation}

gdje je prisilni odziv sustava jednak $1$, a prirodni odziv $-e^{-at}$.

---

### Kako koristiti ovaj interaktivni primjer?

Pomičite klizač za definiranje vrijednosti $k$ u prijenosnoj funkciji sustava prvog reda - $G(s)=\frac{k}{s+k}$ i promatrajte vremenski odziv tako definiranog sustava.

```python
# set up plot
fig, ax = plt.subplots(figsize=[9.8,4],num='Sustav prvog reda')
ax.set_ylim([-1, 2])
ax.set_xlim([0, 5])
ax.grid(True)
ax.set_title ('Vremenski odziv')
ax.set_xlabel('$t$ [s]')
ax.set_ylabel('ulaz, izlaz')
xaxis = ax.axhline(y=0,color='k',lw=1)

response, = ax.plot([], [])
slope, = ax.plot([], [])
x1a, = ax.plot([], [])
y1a, = ax.plot([], [])
tr11, = ax.plot([], [])
trv1, = ax.plot([], [])
trv2, = ax.plot([], [])
trh1, = ax.plot([], [])
trh2, = ax.plot([], [])
ts11, = ax.plot([], [])
ts1, = ax.plot([], [])
ts2, = ax.plot([], [])
texttr=ax.text(0,0,'')
textts=ax.text(0,0,'')

ax.step([0,5],[0,1],color='C0',label='ulaz')

# generate x values
t = np.linspace(0, 2 * np.pi, 10000)

def response_func(t, k):
    """"Return response function"""
    return 1-np.exp(-k*t)

@widgets.interact(k=(1, 5, 1))

def update(k=1):
    """Remove old lines from plot and plot new one"""
    global response,slope,x1a,y1a,tr11,trv1,trv2,trh1,trh2,ts11,ts1,ts2,texttr,textts
    ax.lines.remove(response)
    ax.lines.remove(slope)
    ax.lines.remove(x1a)
    ax.lines.remove(y1a)
    ax.lines.remove(tr11)
    ax.lines.remove(trv1)
    ax.lines.remove(trv2)
    ax.lines.remove(trh1)
    ax.lines.remove(trh2)
    ax.lines.remove(ts11)
    ax.lines.remove(ts1)
    ax.lines.remove(ts2)
    texttr.remove()
    textts.remove()
    response, = ax.plot(t, response_func(t,k), color='C1',lw=2)
    response.set_label('izlaz')
    slope, = ax.plot([0,1/k], [0,1], color='C2',lw=2)
    slope.set_label('inicijalni nagib')
    x1a, = ax.plot([1/k,1/k],[0,1-np.exp(-1)],'--',color='k',lw=.8)
    y1a, = ax.plot([0,1/k],[1-np.exp(-1),1-np.exp(-1)],'--',color='k',lw=.8)
# rise time
    tr11, = ax.plot([-np.log(0.9)/k,-np.log(0.1)/k],[-0.5,-0.5],color='k',lw=.8)
    trv1, = ax.plot([-np.log(0.9)/k,-np.log(0.9)/k],[-0.5,0.1],'--',color='k',lw=.8)
    trv2, = ax.plot([-np.log(0.1)/k,-np.log(0.1)/k],[-0.5,0.9],'--',color='k',lw=.8)
    trh1, = ax.plot([0,-np.log(0.9)/k],[0.1,0.1],'--',color='k',lw=.8)
    trh2, = ax.plot([0,-np.log(0.1)/k],[0.9,0.9],'--',color='k',lw=.8)
# settling time
    ts11, = ax.plot([0,-np.log(0.02)/k],[-0.7,-0.7],color='k',lw=.8)
    ts1, = ax.plot([0,0],[-0.7,0],'--',color='k',lw=.8)
    ts2, = ax.plot([-np.log(0.02)/k,-np.log(0.02)/k],[-0.7,0.98],'--',color='k',lw=.8)
    ax.legend()
    texttr=ax.text((-np.log(0.1)/k-(-np.log(0.9)/k))/2,-0.45, '$t_r$',fontsize=13)
    textts=ax.text((-np.log(0.02)/k)/2-0.1,-0.65, '$t_s$',fontsize=13)

    plt.xticks([0,1/k,2,4], [0,'${1}/{%s}$'%k,2,4],fontsize=8)
    plt.yticks([0.1,0.5,0.63,0.9,1,1.5,2], [0.1,0.5,0.63,0.9,1,1.5,2],fontsize=8)

    num1=[k]
    den1=[1,k]
    display(Markdown('Prijenosna funkcija sustava $G(s)$ je jednaka:'))
    tf_sys1=c.TransferFunction(num1,den1)
    s=sym.Symbol('s')
    eq=(k/(s+k))
    display(eq)
```

    <IPython.core.display.Javascript object>

    interactive(children=(IntSlider(value=1, description='k', max=5, min=1), Output()), _dom_classes=('widget-inte…

```python

```

## Sustavi drugog reda

### Uvod

Za razliku od gore predstavljenih sustava prvog reda, u kojima parametar $k$ utječe isključivo na brzinu odziva, promjene odgovarajućih parametara u sustavima drugog reda mogu utjecati na stvarni oblik odziva. Sljedeća su četiri odziva moguća kod ovih sustava:
- *pretjerano prigušeni* odziv,
- *nedovoljno prigušeni* odziv,
- *neprigušeni* odziv, i
- *kritično prigušeni* odziv.

### Kako koristiti ovaj interaktivni primjer?

Pomičite klizače za definiranje vrijednosti $a$ i $b$ u prijenosnoj funkciji sustava drugog reda oblika $G(s)=\frac{b}{s^2+as+b}$ te promatrajte dijagram polova i nula, kao i vremenski odziv definiranog sustava.

```python
# set up plot
fig1, ax1 = plt.subplots(1,2,figsize=[9.8,4],num='Sustav drugog reda')
ax1[0].set_ylim([-3.5, 3])
ax1[1].set_ylim([0, 2.5])
# ax1.set_xlim([0, 5])
ax1[0].grid(True)
ax1[1].grid(True)
ax1[0].axhline(y=0,color='k',lw=.8)
ax1[1].axhline(y=0,color='k',lw=.8)
ax1[0].axvline(x=0,color='k',lw=.8)
ax1[1].axvline(x=0,color='k',lw=.8)
ax1[0].set_xlabel('Re')
ax1[0].set_ylabel('Im')
ax1[1].set_xlabel('$t$ [s]')
ax1[1].set_ylabel('ulaz, izlaz')
ax1[0].set_title('Dijagram polova i nula')
ax1[1].set_title('Vremenski odziv')

t = np.linspace(0, 20, 10000)

textGs = ax1[0].text(0,0,'')

ax1[1].step([0,20],[0,1],color='C0',label='ulaz')

plotzero, = ax1[0].plot([], [])
response2, = ax1[1].plot([], [])

def response_func2(t, a, b):
    num_sys=np.array([b])
    den_sys=np.array([1,a,b])
    tf_sys=c.TransferFunction(num_sys,den_sys)
    poles_sys,zeros_sys=c.pzmap(tf_sys, Plot=False)
    T, yout = c.step_response(tf_sys,t)
    return T, yout, poles_sys, tf_sys

@widgets.interact(a=(0, 10, 1),b=(1,10,1))

def update(a=7,b=9):
    """ Update plots """
    global response2, plotzero, textGs
    ax1[0].lines.remove(plotzero)
    ax1[1].lines.remove(response2)
#     textGs.remove()
    T, yout, poles_sys, tf_sys = response_func2(t, a, b)
    plotzero, = ax1[0].plot(np.real(poles_sys), np.imag(poles_sys), 'xg', markersize=10, label = 'Polovi')
#     textGs = ax1[0].text(-7,1,tf_sys)
    response2, = ax1[1].plot(T,yout,color='C1',label='izlaz')
    s=sym.Symbol('s')
    eq=b/(s**2+a*s+b)
    coeff = [1,a,b]
    rootsdenom=np.roots(coeff)
    eq2=b/((s-rootsdenom[0])*(s-rootsdenom[1]))
    display(Markdown('Prijenosna funkcija sustava $G(s)$ je jednaka:'))
    display(eq),display(Markdown('ili')),display(eq2)

    if np.imag(poles_sys)[0] == 0 and np.imag(poles_sys)[1] == 0 and np.real(poles_sys)[0] < 0 and np.real(poles_sys)[1] < 0 and np.real(poles_sys)[0]!=np.real(poles_sys)[1]:
        display(Markdown('Sustav je **pretjerano prigušen** jer oba pola imaju samo negativne realne vrijednosti.'))
    elif math.isclose(0, np.imag(poles_sys)[0], abs_tol=10**-6) and math.isclose(0, np.imag(poles_sys)[1], abs_tol=10**-6) and np.real(poles_sys)[1] < 0 and np.real(poles_sys)[0]==np.real(poles_sys)[1]:
        display(Markdown('Sustav je **kritično prigušen** jer postoji dupli pol samo s negativnim realnim vrijednostima.'))
    elif np.real(poles_sys)[0] == 0 and np.real(poles_sys)[1] == 0:
        display(Markdown('Sustav je **neprigušen** jer polovi imaju samo imaginarne dijelove.'))
    elif np.imag(poles_sys)[0] != 0 and np.imag(poles_sys)[1] != 0 and np.real(poles_sys)[0] != 0 and np.real(poles_sys)[1] != 0:
        display(Markdown('Sustav je **nedovoljno prigušen** jer oba pola imaju negativne realne vrijednosti i kompleksne vrijednosti različite od nule.'))
    ax1[0].legend()
    ax1[1].legend()
```

    <IPython.core.display.Javascript object>

    interactive(children=(IntSlider(value=7, description='a', max=10), IntSlider(value=9, description='b', max=10,…


```python
%matplotlib inline
```

01: Model Description
=====================

A description and introduction to the power spectrum model.

Introduction
------------

Welcome to the tutorials!

In this first tutorial, we will introduce an overview and description of power spectrum
model, as well as visualizing some examples.

Keep in mind as you go, that if you want more information that describes, motivates, and
justifies our modeling approach, you can also check out the associated
`paper <https://www.biorxiv.org/content/early/2018/04/11/299859>`_,
and/or the
`motivations <https://fooof-tools.github.io/fooof/auto_tutorials/index.html>`_
section of the site.

Example Power Spectra and Models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

First, we will import and run some code to simulate some example power spectra, and
fit some power spectrum models to them, to use as examples.

For the purpose of this tutorial, you don't need to know how this code works
yet, and can skip past reading the code itself.

```python
# Import required code for visualizing example models
from fooof import FOOOF
from fooof.sim.gen import gen_power_spectrum
from fooof.sim.utils import set_random_seed
from fooof.plts.spectra import plot_spectrum
from fooof.plts.annotate import plot_annotated_model
```

```python
# Set random seed, for consistency generating simulated data
set_random_seed(21)

# Simulate example power spectra
freqs1, powers1 = gen_power_spectrum([3, 40], [1, 1],
                                     [[10, 0.2, 1.25], [30, 0.15, 2]])
freqs2, powers2 = gen_power_spectrum([1, 150], [1, 125, 1.25],
                                     [[8, 0.15, 1.], [30, 0.1, 2]])
```

```python
# Initialize a power spectrum models and fit the power spectra
fm1 = FOOOF(min_peak_height=0.05, verbose=False)
fm2 = FOOOF(min_peak_height=0.05, aperiodic_mode='knee', verbose=False)
fm1.fit(freqs1, powers1)
fm2.fit(freqs2, powers2)
```

Now, we have some data & models to work with.

To start with, we can plot one of our simulated power spectra.

Plotted below is an example power spectrum, plotted in semi-log space (log10 power values
and linear frequencies). This is our data, that we will be trying to model.

In the plot, we see a power spectrum in which there is decreasing power across increasing
frequencies. In some frequency regions, there is a 'peak' of power, over and above the general
trend across frequencies. These properties - a pattern of power across all frequencies, with
overlying peaks - are considered to be hallmarks of neural field data.

```python
# Plot one of the example power spectra
plot_spectrum(freqs1, powers1, log_powers=True,
              color='black', label='Original Spectrum')
```

Conceptual Overview
-------------------

The goal of this module is to fit models to parameterize neural power spectra.

One reason to do so is the idea that there are multiple distinct 'components' within
neural field data. The model is therefore designed to measure these different
'components' of the data.

By components, we mean that we are going to conceptualize neural field data as consisting
of a combination of periodic (oscillatory) and aperiodic activity. Restated, we could say
that neural data contains both periodic and aperiodic components (or activity).

The goal of the model is to measure these components, separately and explicitly,
from frequency representations of neural field data (neural power spectra).

Visualizing Power Spectrum Models
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

First, let's start by revisiting the example power spectrum, this time also adding the model
fit to the plot.

In the plot below, black is the data, same as before, and in red we've added the
model fit of the data. If things are working, this line should be good approximation
of the data.

```python
# Plot an example power spectrum, with a model fit
fm1.plot(plot_peaks='shade', peak_kwargs={'color' : 'green'})
```

Given the full model, in red, we can then describe this spectrum in terms of the
'components' that make up the model fit, which are:

- `aperiodic`: activity, with no characteristic frequency (or 'non-frequency specific' activity)

  - in power spectra, this looks like a trend across all frequencies
  - in the plot above, this is what's captured by the dashed blue line
- `periodic` : activity, with a characteristic frequency

  - in power spectra, this looks like a 'peak', or 'bump', reflecting frequency specific power
  - in the plot above, this is what's captured by the green shaded peaks

Each of these components also has different 'features' or 'properties', that we can and
want to describe and measure. Since these 'features' are things that we will be fitting in
the model, we will call them `parameters` (as in, the 'model parameters').

The full model of the power spectrum is the combination of the two components:
the aperiodic component and the periodic component, which is the set of peaks.

The goal of the model is to measure these two components, to create the full model
fit, in a way that accurately and quantitatively describes the data.

Mathematical Description of Overall Model
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To enact the conceptual idea and description above, we will need to formalize the
model described above. To do so, throughout the rest of this tutorial, we will lay out the
mathematical description of how neural power spectra can be modeled as a combination of
periodic and aperiodic activity.

Overall, the goal is to describe a neural power spectrum, described as $NPS$, as
a combination of periodic and aperiodic components. We will describe each of these
components in their own section.

Note that as we go, we will continue to consider power in log10 spacing. You will
also see each component includes $(F)$, where $F$ is the array of
frequency values (in linear spacing) for the power spectrum, which just indicates that
each component is a function of frequency.

Periodic Component
------------------

By periodic activity, we mean activity that has a characteristic frequency.
This includes what are typically referred to as neural oscillations, often described
in particular frequency bands such as delta, theta, alpha, beta, gamma, etc.

In the frequency domain, putative oscillations are frequency regions in which
there are 'bumps' of power over and above the aperiodic component.
We will generally refer to the these as 'peaks' in the neural power spectrum.

To measure the periodic activity, we would like to describe these peaks, without our
measures of these peaks being influenced by co-occurring aperiodic activity.
This is important, since as we can see in the plots above, the aperiodic and periodic
components of the data can 'overlap', in frequency space. This means the total power
at a given frequency may have contributions from both components. To measure periodic power,
specifically, we need to measure the power relative to the aperiodic component of the data.

Next, let's explore an annotated version of our power spectrum model.

```python
# Plot an annotated version of the power spectrum model
plot_annotated_model(fm1, annotate_aperiodic=False)
```

In the labeled plot above, we can again see the different components of the model,
as well as the labeled peak parameters.

Note that vertical labels reflect parameters measured in the x-axis units, so in frequency,
where as horizontal labels reflect parameters measured in y-axis units, so power.

The periodic parameters are:

- the `center frequency` of the peak, in units of frequency
- the `power` of the peak, over the aperiodic component, in units of power
- the `bandwidth`, or the width of the peak, units of frequency

Wherever we detect a peak, these are the parameters that we will fit to the peak,
to describe this component of the data.

Mathematical Description of the Periodic Component
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To fit these periodic components - the regions of power over above the aperiodic component,
or 'peaks' - the model uses Gaussians. As we've seen, there can be multiple peaks in the model.

Each Gaussian, $n$, referred to as $G(F)_n$, is of the form:

\begin{align}G(F)_n = a * exp (\frac{- (F - c)^2}{2 * w^2})\end{align}

This describes each peak in terms of parameters `a`, `c` and `w`, where:

- $a$ is the height of the peak, over and above the aperiodic component
- $c$ is the center frequency of the peak
- $w$ is the width of the peak
- $F$ is the array of frequency values

Aperiodic Component
-------------------

By 'aperiodic' activity, we mean activity that is not rhythmic, or activity that has
no characteristic frequency.

In the power spectrum, we typically see this as 1/f-like activity, whereby (in linear space)
the power across frequencies decreases with a $\frac{1}{F^\chi}$ relationship.

To measure the aperiodic activity, we would like to describe the pattern of activity
across all frequencies, without our measure being influenced by any co-occurring periodic
activity (peaks).

```python
# Plot an annotated version of the power spectrum model
plot_annotated_model(fm1, annotate_peaks=False)
```

The aperiodic parameters in the above plot are:

- the `offset`, or overall up/down translation of the whole spectrum
- the `exponent`, which describes the 'curve', or overall 'line' of the aperiodic component

Note that diagonal labels indicate unit-less measures (in neither units of frequency or power).

Mathematical Description of the Aperiodic Component
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

To fit the aperiodic component, we will use the function $L$:

\begin{align}L(F) = b - \log(k + F^\chi)\end{align}

Note that this function is fit on the semi-log power spectrum, meaning linear frequencies
and $log_{10}$ power values.

In this formulation, the parameters $b$, $k$, and $\chi$
define the aperiodic component, as:

- $b$ is the broadband 'offset'
- $k$ relates to the 'knee'
- $\chi$ is the 'exponent' of the aperiodic fit
- $F$ is the array of frequency values

Note that fitting the knee parameter is optional. If used, the knee parameter defines a
'bend' in the aperiodic `1/f` like component of the data. If not used, the 'knee'
parameter is set to zero.

This function form is technically described as a Lorentzian function. We use the option
of adding a knee parameter, since even though neural data is often discussed in terms
of having `1/f` activity, across broader frequency ranges, there is typically not a single
`1/f` characteristic. Using this form allows for modeling bends in the power spectrum of
the aperiodic component, if and when they occur.

Note that if we were to want the equivalent function in linear power, using $AP$
to indicate the aperiodic component in linear spacing, it would be:

\begin{align}AP(F) = 10^b * \frac{1}{(k + F^\chi)}\end{align}

A Note on Logging
~~~~~~~~~~~~~~~~~

So far, we have been plotting in semi-log, meaning the x-axis (frequency) is in linear
spacing, and the y-axis (power) is in log10 space. This is common practice, as power values
are exponentially distributed.

It can also be useful, for visualization, to plot with both axes on a log scale.
Note that in this module, plotting in log-log is just a visualization choice, and does
not affect how the data is stored and/or how models are fit.

Below we can see the same spectrum again, with all the annotations on, plotted in log-log.
The most notable difference, is that the aperiodic component is a straight line in log-log
spacing. This is a hallmark of `1/f` activity.

```python
# Plot the power spectrum model, in log-log space
plot_annotated_model(fm1, plt_log=True)
```

Relating Exponents to Power Spectrum Slope
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Another way to measure 1/f properties in neural power spectra is to measure the slope
of the spectrum in log-log spacing, fitting a linear equation as:

\begin{align}L(log(F)) = aF + b\end{align}

Where:

- $a$ is the power spectrum slope
- $b$ is the offset
- $F$ is the array of frequency values

In this formulation, the data is considered in log-log space, meaning the frequency values
are also in log space. Since 1/f is a straight line in log-log spacing, this approach captures
1/f activity.

This is equivalent to the power spectrum model in this module, when fitting with no knee,
with a direct relationship between the slope ($a$) and the exponent ($\chi$):

\begin{align}\chi = -a\end{align}

Fitting Knees
~~~~~~~~~~~~~

In the original model we fit and explored, there was no aperiodic 'knee'. Depending on
the data, or the frequency range, there may or may not be a knee present in the data
(more on that later in the tutorial).

In the next plot, there is another annotated model, from a broader frequency range that also
includes a knee. When plotted in log-log, the presence of a knee can be seen as 'bend' or
'knee' in the  aperiodic component.

```python
# Plot an annotated version of the power spectrum model
plot_annotated_model(fm2, plt_log=True)
```

The knee parameter fit to the model is a unit-less value that describes the curve of the
aperiodic component (when plotted in log-log spacing). On the plot above, the annotation
references the knee as the dot on the plot at the bend point.

Conclusion
----------

So far, we have explored how neural power spectra, $NPS$, across a set of frequencies
$F$ can be modeled as a combination of an aperiodic component, $L$, and the
periodic component, which is comprised of `N` peaks, where each $G_n$ is a Gaussian.

To summarize, the full model is:

\begin{align}NPS(F) = L(F) + G(F)_n\end{align}

where:

\begin{align}L(F) = b - \log(k + F^\chi) \quad \quad G(F)_n = a * exp (\frac{- (F - c)^2}{2 * w^2})\end{align}

This approach allows us to measure periodic and aperiodic activity from neural
power spectra, and describe them with model parameters:

- $b$, $k$, and $\chi$ of the aperiodic component which reflect
  the `offset`, `knee` and `exponent`, respectively
- $c$, $a$, $w$ for each periodic peak, relating to the
  `center frequency`, `power` and `bandwidth` of putative periodic activity

For more technical details on the model formulation and fitting process, check out the
`paper <https://www.biorxiv.org/content/early/2018/04/11/299859>`_.

In the next tutorial, we will start to use this model.


```python
import numpy as np
import pydae.build as db
import sympy as sym
from pydae.models import grid2dae_dq
import json
```

```python
sin = sym.sin
cos = sym.cos
sqrt = sym.sqrt
```

```python
Omega_b = 2*np.pi*50
L_t = 1.25e-3; #Converter side Filter Inductance H
R_t = L_t*Omega_b/10; #Converter side Filter resistance Ohms. Quality Factor of 10
L_s = 1.25e-3; #Grid Side Filter Inductance H
R_s = L_s*Omega_b/10; #Grid side Filter resistance Ohms. Quality Factor of 10
R_d = 20; #Dmaping resistor
C_m = 4e-6; #Filter capacitance
tau_ctrl_1 = 5e-3; #Time constant of CTRL 1
Ts_Power=50e-6; #Sample time of power plant
Ts_Control=50e-6; #Sample time of controller
K_pi = (L_t+L_s)/tau_ctrl_1; #Proportional gain of CTRL 1
K_ii = (R_t+R_s)/tau_ctrl_1; #Integral gain of CTRL 1
H = 5.0; # desired virtual inertia
K_p = 0.01; # active power proportinal gain
#H = T_p/K_p/2

T_p = K_p*2*H;  # active power integral time constant

K_q = 0.1; # reactive power proportinal gain
T_q = 0.1; # reactive power integral time constant

X_v = 0.05; # virtual reactance
R_v = 0.01;  # virtual resistance

K_pv = 0.0004
K_iv = 11.313708

G10 = {"name":"G10","bus": "R10","U_b":400.0,"S_b_kVA":200,"v_dc":750,'type':'sm2_i',
           'R_t':R_t,'L_t':L_t,'R_s':R_s,'L_s':L_s,'C_m':C_m,'K_phi':0.00,'omega':Omega_b, 'G_d':1e-2, 'eta_D':0,'eta_Q':0.8, #VSC
           'eta_d_ref':0,'eta_q_ref':0.8,'phi':0.0,   # CTRL0
           'K_pi':K_pi,'K_ii':K_ii,'Gv_in':0.1,   # CTRL1
           'K_pv':K_pv,'K_iv':K_iv,   # CTRL2
           'K_p':K_p,'T_p':T_p,'K_q':K_q,'T_q':T_q,'R_v':R_v,'X_v':X_v,'p_m_ref':0.0,'q_s_ref':0.0,   # CTRL3
           'H':5.0,'D':5.0,  # CTRL3
           'T_vpoi':0.1,'K_vpoi':10,'T_f':0.1,'K_f':20.0,'p_r':0.0,'q_r':0.0,'v_s_ref':1.0,'omega_ref':1.0}   # CTRL4

G14 = {"name":"G14","bus": "R14","U_b":400.0,"S_b_kVA":200,"v_dc":750,'type':'sm2_i',
           'R_t':R_t,'L_t':L_t,'R_s':R_s,'L_s':L_s,'C_m':C_m,'K_phi':0.00,'omega':Omega_b, 'G_d':1e-2, 'eta_D':0,'eta_Q':0.8, #VSC
           'eta_d_ref':0,'eta_q_ref':0.8,'phi':0.0,   # CTRL0
           'K_pi':K_pi,'K_ii':K_ii,'Gv_in':0.1,   # CTRL1
           'K_pv':K_pv,'K_iv':K_iv,   # CTRL2
           'K_p':K_p,'T_p':T_p,'K_q':K_q,'T_q':T_q,'R_v':R_v,'X_v':X_v,'p_m_ref':0.0,'q_s_ref':0.0,   # CTRL3
           'H':5.0,'D':5.0,  # CTRL3
           'T_vpoi':0.1,'K_vpoi':10,'T_f':0.1,'K_f':20.0,'p_r':0.0,'q_r':0.0,'v_s_ref':1.0,'omega_ref':1.0}   # CTRL4

data = {
"system":{"f_hz":50,"model_type":"ae"},
"buses": [{"bus":"R00"},{"bus":"R01"},{"bus":"R02"},{"bus":"R03"},{"bus":"R04"},{"bus":"R05"},{"bus":"R06"},
        {"bus":"R07"},{"bus":"R08"},{"bus":"R09"},{"bus":"R10"},{"bus":"R11"},{"bus":"R12"},
        {"bus":"R13"},{"bus":"R14"},{"bus":"R15"},{"bus":"R16"},{"bus":"R17"},{"bus":"R18"}],
"lines":     [  {"bus_j":"R00", "bus_k":"R01", "R":0.0032, "X":0.0128, "B":0.0},
                {"bus_j":"R02", "bus_k":"R01", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R02", "bus_k":"R03", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R03", "bus_k":"R04", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R04", "bus_k":"R05", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R04", "bus_k":"R12", "R":0.02877350, "X":0.002983400, "B":0.0},
                {"bus_j":"R05", "bus_k":"R06", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R06", "bus_k":"R07", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R07", "bus_k":"R08", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R08", "bus_k":"R09", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R09", "bus_k":"R10", "R":0.00567210, "X":0.002839550, "B":0.0},
                {"bus_j":"R09", "bus_k":"R17", "R":0.02466300, "X":0.002557200, "B":0.0},
                {"bus_j":"R11", "bus_k":"R03", "R":0.02466300, "X":0.002557200, "B":0.0},
                {"bus_j":"R12", "bus_k":"R13", "R":0.02877350, "X":0.002983400, "B":0.0},
                {"bus_j":"R13", "bus_k":"R14", "R":0.02877350, "X":0.002983400, "B":0.0},
                {"bus_j":"R14", "bus_k":"R15", "R":0.02877350, "X":0.002983400, "B":0.0},
                {"bus_j":"R16", "bus_k":"R06", "R":0.02466300, "X":0.002557200, "B":0.0},
                {"bus_j":"R18", "bus_k":"R10", "R":0.02466300, "X":0.002557200, "B":0.0}],
"loads" : [
        {"bus": "R01", "kVA": 200.0, "pf": 0.95, "T_i":0.01,"I_max":434.78},
        {"bus": "R11", "kVA": 15.0, "pf": 0.95, "T_i":0.01,"I_max":32.6},
        {"bus": "R15", "kVA": 52.0, "pf": 0.95, "T_i":0.01,"I_max":120},
        {"bus": "R16", "kVA": 55.0, "pf": 0.95, "T_i":0.01,"I_max":120},
        {"bus": "R17", "kVA": 35.0, "pf": 0.95, "T_i":0.01,"I_max":120},
        {"bus": "R18", "kVA": 47.0, "pf": 0.95, "T_i":0.01,"I_max":120}
        ],
"grid_formers" : [
    #{"bus": "R00","V_phph":400.0, "deg":0.0}
],
"vsgs" : [G10,G14]
}

#grid_dae = grid2dae_dq(data)
```

```python
for item in data['loads']:
    bus = item['bus']
    fp = item["pf"]
    phi = np.arccos(fp)
    p = item["kVA"]*fp*1000
    q = item["kVA"]*np.sin(phi)*1000
    print(f'{{"name":"{bus}","P_W":{p:0.3f},"Q_var":{q:0.3f},"U_kV":0.4}},')
```

    {"name":"R01","P_W":190000.000,"Q_var":62449.980,"U_kV":0.4},
    {"name":"R11","P_W":14250.000,"Q_var":4683.748,"U_kV":0.4},
    {"name":"R15","P_W":49400.000,"Q_var":16236.995,"U_kV":0.4},
    {"name":"R16","P_W":52250.000,"Q_var":17173.744,"U_kV":0.4},
    {"name":"R17","P_W":33250.000,"Q_var":10928.746,"U_kV":0.4},
    {"name":"R18","P_W":44650.000,"Q_var":14675.745,"U_kV":0.4},

### VSGs

```python
xi_f_sec = sym.Symbol('xi_f_sec', real=True)
K_f_sec = sym.Symbol('K_f_sec', real=True)
omega_v_G10,omega_v_G14 = sym.symbols('omega_v_G10,omega_v_G14', real=True)
H_G10,H_G14 = sym.symbols('H_G10,H_G14', real=True)
omega_coi = (H_G10*omega_v_G10 + H_G14*omega_v_G14)/(H_G10+H_G14)
dxi_f_sec = 1 - omega_coi
p_sec_G10 = K_f_sec*xi_f_sec/2
p_sec_G14 = K_f_sec*xi_f_sec/2

f_ctrl_5 = [dxi_f_sec]
x_ctrl_5 = [xi_f_sec]
g_ctrl_5 = []
y_ctrl_5 = []
x_0_ctrl_5 = []
y_0_ctrl_5 = []
params_ctrl_5 = {'K_f_sec':0.001}
u_ctrl_5 = {}
h_ctrl_5 = {}

p_sec = {'G10':p_sec_G10,'G14':p_sec_G14}

#omega_coi = 1.0
grid_dae = grid2dae_dq(data)

vsgs = data['vsgs']
f_vsg = []
x_vsg = []
g_vsg = []
y_vsg = []
x_0_vsg = []
y_0_vsg = []
params_vsg = {}
u_vsg = {}
h_vsg = {}

for vsg in vsgs:
    name = vsg['name']
    bus = vsg['bus']

    U_b = vsg['U_b']
    S_b = vsg['S_b_kVA']*1000
    I_b = S_b/(np.sqrt(3)*U_b)

    U_bdq = U_b*(np.sqrt(2))
    V_bdq = U_bdq/np.sqrt(3)
    I_bdq = I_b*np.sqrt(2)

    ## Transformations #########################################################################
    ## feedbacks:
    feedbacks = ['i_tD','i_tQ'] + ['v_mD','v_mQ'] + ['i_sD','i_sQ'] + ['v_sD','v_sQ'] + ['phi']
    for item in feedbacks:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")

    v_md,v_mq = sym.symbols(f'v_md_{name},v_mq_{name}', real=True)
    v_sd,v_sq = sym.symbols(f'v_sd_{name},v_sq_{name}', real=True)
    i_sd,i_sq = sym.symbols(f'i_sd_{name},i_sq_{name}', real=True)
    i_td,i_tq = sym.symbols(f'i_td_{name},i_tq_{name}', real=True)
    phi,dum= sym.symbols(f'phi_{name},dum_{name}', real=True)

    eq_v_md = -v_md + v_mD*cos(phi) + v_mQ*sin(phi) # original park
    eq_v_mq = -v_mq - v_mD*sin(phi) + v_mQ*cos(phi) # original park

    eq_v_sd = -v_sd + v_sD*cos(phi) + v_sQ*sin(phi) # original park
    eq_v_sq = -v_sq - v_sD*sin(phi) + v_sQ*cos(phi) # original park

    eq_i_sd = -i_sd + i_sD*cos(phi) + i_sQ*sin(phi) # original park
    eq_i_sq = -i_sq - i_sD*sin(phi) + i_sQ*cos(phi) # original park

    # jmm: medimos i_t?
    eq_i_td = -i_td + i_tD*cos(phi) + i_tQ*sin(phi) # original park
    eq_i_tq = -i_tq - i_tD*sin(phi) + i_tQ*cos(phi) # original park

    g_aux = [eq_v_md,eq_v_mq,eq_v_sd,eq_v_sq,eq_i_td,eq_i_tq,eq_i_sd,eq_i_sq]
    y_aux = [   v_md,   v_mq,   v_sd,   v_sq,   i_td,   i_tq,   i_sd,   i_sq]
    y_0_aux = [  0.0,  V_bdq,    0.0,  V_bdq,       0,      0]

    #v_sd = v_md
    #v_sq = v_mq

   # S_b_kVA,U_b = sym.symbols(f'S_b_kVA_{name},U_b_{name}', real=True) # params

    ## per unit

    i_sd_pu = i_sd/I_bdq; # input in SI that is coverted to pu
    i_sq_pu = i_sq/I_bdq; # input in SI that is coverted to pu
    v_sd_pu = v_sd/V_bdq; # input in SI that is coverted to pu
    v_sq_pu = v_sq/V_bdq; # input in SI that is coverted to pu
    i_td_pu = i_td/I_bdq; # input in SI that is coverted to pu
    i_tq_pu = i_tq/I_bdq; # input in SI that is coverted to pu

    ## PLL #########################################################################

    # CTRL4 #########################################################################
    ## parameters:
    params_ctrl_4 = {}
    for item in ['T_vpoi','K_vpoi','T_f','K_f']:
        params_ctrl_4.update({f'{item}_{name}':vsg[item]})
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## inputs:
    u_ctrl_4 = {}
    for item in ['v_s_ref','omega_ref','p_r','q_r']:
        u_ctrl_4.update({f'{item}_{name}':vsg[item]})
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## dynamic states:
    x_list_ctrl_4 = ['omega_v_filt','v_s_filt']
    for item in x_list_ctrl_4:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## algebraic states
    y_list_ctrl_4 = ['p_m_ref','q_s_ref']
    for item in y_list_ctrl_4:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## feedbacks:
    feedbacks = ['omega_v']
    for item in feedbacks:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")

    domega_v_filt = 1/T_f*(omega_v - omega_v_filt)
    dv_s_filt = 1/T_vpoi*((v_sd_pu**2+v_sq_pu**2)**0.5 - v_s_filt)

    eq_p_m_ref = -p_m_ref + p_r + K_f*(omega_ref - omega_v_filt) + p_sec[name]   # PFR and secondary input
    eq_q_s_ref = -q_s_ref + q_r + K_vpoi*(v_s_ref - v_s_filt)

    # from derivatives to the integrator
    f_ctrl_4 = [domega_v_filt,dv_s_filt];
    x_ctrl_4 = [ omega_v_filt, v_s_filt];
    g_ctrl_4 = [ eq_p_m_ref, eq_q_s_ref] #eq_i_sd_ref, eq_i_sq_ref, ]
    y_ctrl_4 = [    p_m_ref,    q_s_ref] #   i_sd_ref,    i_sq_ref,    omega_v,    DV_sat];

    # CTRL3 #########################################################################
    ## parameters:
    params_ctrl_3 = {}
    for item in ['K_p','T_p','K_q','T_q','R_v','X_v','S_b_kVA','U_b','K_phi','H','D']:
        params_ctrl_3.update({f'{item}_{name}':vsg[item]})
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## inputs:
    u_ctrl_3 = {}
    for item in ['p_m_ref','q_s_ref']: # []
        u_ctrl_3.update({f'{item}_{name}':vsg[item]})
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## dynamic states:
    x_list_ctrl_3 = ['phi','omega_v','xi_q','omega_rads']
    for item in x_list_ctrl_3:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## algebraic states
    y_list_ctrl_3 = ['DV_sat','p_s_pu','q_s_pu']
    for item in y_list_ctrl_3:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## feedbacks:
    v_dc,dum = sym.symbols(f'v_dc_{name},dum_{name}', real=True)

    fault_flag = 0;
    e_0 = 1.0;
    eq_p_s_pu = -p_s_pu + i_sd_pu*v_sd_pu + i_sq_pu*v_sq_pu; # pu
    eq_q_s_pu = -q_s_pu + i_sd_pu*v_sq_pu - i_sq_pu*v_sd_pu; # pu

    # from the integrator to the states
    epsilon_p = (p_m_ref - p_s_pu)*(1.0-fault_flag);
    epsilon_q =  q_s_ref - q_s_pu;

    if vsg['type'] == 'uvsg_i':
        eq_omega_v = -omega_v + K_p*(epsilon_p + xi_p/T_p) + 1.0;

        dphi =  Omega_b*(omega_v-1.0) - K_phi*phi;
        dxi_p = epsilon_p;
        dxi_q = epsilon_q;
        domega_rads = 1.0/1.0*(Omega_b*omega_v - omega_rads);

    if vsg['type'] == 'sm2_i':
        dphi =  Omega_b*(omega_v-omega_coi) - K_phi*phi;
        domega_v = 1/(2*H)*(p_m_ref - p_s_pu - D*(omega_v - 1.0))
        dxi_q = epsilon_q;
        domega_rads = 1.0/1.0*(Omega_b*omega_v - omega_rads);

    DV =   K_q*(epsilon_q + xi_q/T_q);

    eq_DV_sat = DV_sat - DV;
    #if DV_sat > 0.1
    #    DV_sat = 0.1;
    #    dxi_q = 0.0;
    #end
    #if DV_sat < -0.1
    #    DV_sat = -0.1;
    #    dxi_q = 0.0;
    #end

    e = e_0 + DV_sat;

    v_t_d_pu = 0.0
    v_t_q_pu = e

    v_t_d = (v_t_d_pu - R_v*i_sd_pu + X_v*i_sq_pu)*V_bdq
    v_t_q = (v_t_q_pu - R_v*i_sq_pu - X_v*i_sd_pu)*V_bdq

    #v_t_d = v_t_d_pu*V_bdq
    #v_t_q = v_t_q_pu*V_bdq

    eta_d_ref = v_t_d/v_dc*2
    eta_q_ref = v_t_q/v_dc*2

#    i_sd_pu = -(X_v*e + R_v*v_sd_pu + X_v*v_sq_pu)/(R_v**2 + X_v**2);
#    i_sq_pu = -(R_v*e + R_v*v_sq_pu - X_v*v_sd_pu)/(R_v**2 + X_v**2);
#    eq_i_sd_ref = -i_sd_ref + i_sd_pu*I_bdq
#    eq_i_sq_ref = -i_sq_ref + i_sq_pu*I_bdq

    #v_sd_ref_pu =    - R_v*i_sd_pu + X_v*i_sq_pu;
    #v_sq_ref_pu = -e - R_v*i_sq_pu - X_v*i_sd_pu;
    #eq_v_sd_ref = -v_sd_ref + v_sd_ref_pu*V_bdq
    #eq_v_sq_ref = -v_sq_ref + v_sq_ref_pu*V_bdq

    # from derivatives to the integrator
    f_ctrl_3 = [dphi,domega_v,dxi_q,domega_rads];
    x_ctrl_3 = [ phi, omega_v, xi_q, omega_rads];
    g_ctrl_3 = [ eq_DV_sat, eq_p_s_pu, eq_q_s_pu] #eq_i_sd_ref, eq_i_sq_ref, ]
    y_ctrl_3 = [    DV_sat,    p_s_pu,    q_s_pu] #   i_sd_ref,    i_sq_ref,    omega_v,    DV_sat];
    x_0_ctrl_3 = [ 0.0, 0.0, 0.0, 2*np.pi*50]
    y_0_ctrl_3 = [ ] #     0.0,       V_bdq,          1]

    # CTRL0 #########################################################################

    ## inputs:
    params_ctrl_0 = {}
    u_ctrl_0 = {}
    for item in []: # ['eta_d_ref','eta_q_ref','phi']: #+['eta_D','eta_Q']:
        u_ctrl_0.update({f'{item}_{name}':vsg[item]})
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## algebraic states
    y_list_ctrl_0 = ['eta_d','eta_q'] + ['eta_D','eta_Q']
    for item in y_list_ctrl_0:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")

    eq_eta_d = eta_d - eta_d_ref
    eq_eta_q = eta_q - eta_q_ref

    eq_eta_D = -eta_D + eta_d*cos(phi) - eta_q*sin(phi) # comment for test 1
    eq_eta_Q = -eta_Q + eta_d*sin(phi) + eta_q*cos(phi) # comment for test 1

    # from derivatives to the integrator
    f_ctrl_0 = [];
    x_ctrl_0 = [];
    g_ctrl_0 = [eq_eta_d,eq_eta_q,eq_eta_D,eq_eta_Q]
    y_ctrl_0 = [   eta_d,   eta_q,   eta_D,   eta_Q];
    x_0_ctrl_0 = [ ]
    y_0_ctrl_0 = [0.0,  0.8, 0.0,  0.8]

    ## VSC and Filters #########################################################################
    ## parameters:
    params_vsc_lcl = {}
    for item in ['L_t','R_t','C_m','L_s','R_s','omega','G_d']:
        params_vsc_lcl.update({f'{item}_{name}':vsg[item]})
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## inputs:
    u_vsc_lcl = {}
    for item in ['v_dc']: #+['eta_D','eta_Q']:
        u_vsc_lcl.update({f'{item}_{name}':vsg[item]})
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## dynamic states:
    x_list_vsc_lcl = []
    for item in x_list_vsc_lcl:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## algebraic states
    y_list_vsc_lcl = ['i_tD','i_tQ'] + ['v_mD','v_mQ'] + ['i_sD','i_sQ'] + ['v_sD','v_sQ']
    for item in y_list_vsc_lcl:
        exec(f"{item} = sym.Symbol('{item}_{name}', real=True)")
    ## feedbacks:
    v_poiD,v_poiQ = sym.symbols(f'v_{bus}_D,v_{bus}_Q', real=True)
    i_poiD,i_poiQ = sym.symbols(f'i_{bus}_D,i_{bus}_Q', real=True)

    #eta_D = eta_D_ref # - Gv_in*(i_tD - i_sD)
    #eta_Q = eta_Q_ref #- Gv_in*(i_tQ - i_sQ)

    # LCL filter
    di_tD = 1/L_t*(eta_D/2*v_dc - R_t*i_tD + omega*L_t*i_tQ - v_mD)
    di_tQ = 1/L_t*(eta_Q/2*v_dc - R_t*i_tQ - omega*L_t*i_tD - v_mQ)
    dv_mD = 1/C_m*(i_tD + C_m*omega*v_mQ - G_d*v_mD - i_sD)
    dv_mQ = 1/C_m*(i_tQ - C_m*omega*v_mD - G_d*v_mQ - i_sQ)
    di_sD = 1/L_s*(v_mD - R_s*i_sD + omega*L_s*i_sQ - v_sD)
    di_sQ = 1/L_s*(v_mQ - R_s*i_sQ - omega*L_s*i_sD - v_sQ)

    # Grid interaction
    eq_i_poiD =  i_sD - i_poiD
    eq_i_poiQ =  i_sQ - i_poiQ

    eq_v_sD =  v_sD - v_poiD
    eq_v_sQ =  v_sQ - v_poiQ

    grid_dae['params'].pop(f'i_{bus}_D')
    grid_dae['params'].pop(f'i_{bus}_Q')

    # DAE
    f_vsc_lc = []
    x_vsc_lc = []
    g_vsc_lc = [di_tD, di_tQ, dv_mD, dv_mQ, di_sD, di_sQ, eq_i_poiD, eq_i_poiQ, eq_v_sD, eq_v_sQ]
    y_vsc_lc = [ i_tD,  i_tQ,  v_mD,  v_mQ,  i_sD,  i_sQ,    i_poiD,    i_poiQ,    v_sD,    v_sQ]
    x_0_vsc_lc = [ ]
    y_0_vsc_lc = [ 0.0,  0.0,   0.0, V_bdq,     0,     0,         0,         0,       0,   V_bdq]

    ## Model integration
    f_vsg += f_vsc_lc + f_ctrl_0 + f_ctrl_3 + f_ctrl_4
    x_vsg += x_vsc_lc + x_ctrl_0 + x_ctrl_3 + x_ctrl_4
    g_vsg += g_vsc_lc + g_ctrl_0 + g_aux + g_ctrl_3 + g_ctrl_4
    y_vsg += y_vsc_lc + y_ctrl_0 + y_aux + y_ctrl_3 + y_ctrl_4
    params_vsg.update(params_vsc_lcl)
    params_vsg.update(params_ctrl_0)
    params_vsg.update(params_ctrl_3)
    params_vsg.update(params_ctrl_4)
    u_vsg.update(u_vsc_lcl)
    u_vsg.update(u_ctrl_0)
    u_vsg.update(u_ctrl_3)
    u_vsg.update(u_ctrl_4)

    h_vsg.update({f'i_sD_{name}':i_sD,f'i_sQ_{name}':i_sQ})

   # x_0_vsg += x_0_vsc_lc
   # y_0_vsg += y_0_vsc_lc

f_vsg += f_ctrl_5
x_vsg += x_ctrl_5
params_vsg.update(params_ctrl_5)
u_vsg.update(u_ctrl_5)
```

```python
#dum,indum  = sym.symbols('dum,indum', real=True)
#f_vsg = [indum-dum]
#x_vsg = [      dum]
#u_run_dict = {'indum':1.0}

params_dict = grid_dae['params']
u_run_dict = grid_dae['u']
f_list = grid_dae['f'] + f_vsg
x_list = grid_dae['x'] + x_vsg
g_list = grid_dae['g'] + g_vsg
y_list = grid_dae['y'] + y_vsg
params_dict.update(params_vsg)
u_run_dict.update(u_vsg)
#params_dict.update(params_vsg)

#u_run_dict.pop('v_1_D')
#u_run_dict.pop('v_1_Q')
#u_run_dict.pop('v_2_D')
#u_run_dict.pop('v_2_Q')
#u_run_dict.pop('v_2_D')
#u_run_dict.pop('v_2_Q')
#u_run_dict.pop('v_R10_d')
#u_run_dict.pop('v_R10_q')

#params_dict.pop('i_1_D')
#params_dict.pop('i_1_Q')
#params_dict.pop('i_3_D')
#params_dict.pop('i_3_Q')

sys = {'name':'cigre_lv_res_vsg',
       'params_dict':params_dict,
       'f_list':f_list,
       'g_list':g_list,
       'x_list':x_list,
       'y_ini_list':y_list,
       'y_run_list':y_list,
       'u_run_dict':u_run_dict,
       'u_ini_dict':u_run_dict,
       'h_dict':h_vsg
      }

sys = db.system(sys)
db.sys2num(sys)

```

```python

```