# Tutorial 2: Learning Hyperparameters
**Week 1, Day 2: Linear Deep Learning**

**By Neuromatch Academy**

__Content creators:__ Andrew Saxe, Saeed Salehi

__Content reviewers:__ Polina Turishcheva, Antoine De Comite

__Content editors:__ Anoop Kulkarni

__Production editors:__ Khalid Almubarak, Spiros Chavlis

**Our 2021 Sponsors, including Presenting Sponsor Facebook Reality Labs**

---
# Tutorial Objectives

* Training landscape
* The effect of depth
* Choosing a learning rate
* Initialization matters

```python
# @title Tutorial slides

# @markdown These are the slides for the videos in the tutorial
from IPython.display import IFrame
IFrame(src=f"https://mfr.ca-1.osf.io/render?url=https://osf.io/sne2m/?direct%26mode=render%26action=download%26mode=render", width=854, height=480)
```

---
# Setup

This a GPU-Free tutorial!

```python
# Imports
import time
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
```

```python
# @title Figure settings

from ipywidgets import interact, IntSlider, FloatSlider, fixed
from ipywidgets import HBox, interactive_output, ToggleButton, Layout
from mpl_toolkits.axes_grid1 import make_axes_locatable

%config InlineBackend.figure_format = 'retina'
plt.style.use("https://raw.githubusercontent.com/NeuromatchAcademy/content-creation/main/nma.mplstyle")
```

```python
# @title Plotting functions

def plot_x_y_(x_t_, y_t_, x_ev_, y_ev_, loss_log_, weight_log_):
  """
  """
  plt.figure(figsize=(12, 4))
  plt.subplot(1, 3, 1)
  plt.scatter(x_t_, y_t_, c='r', label='training data')
  plt.plot(x_ev_, y_ev_, c='b', label='test results', linewidth=2)
  plt.xlabel('x')
  plt.ylabel('y')
  plt.legend()
  plt.subplot(1, 3, 2)
  plt.plot(loss_log_, c='r')
  plt.xlabel('epochs')
  plt.ylabel('mean squared error')
  plt.subplot(1, 3, 3)
  plt.plot(weight_log_)
  plt.xlabel('epochs')
  plt.ylabel('weights')
  plt.show()

def plot_vector_field(what, init_weights=None):
  """
  """
  n_epochs=40
  lr=0.15
  x_pos = np.linspace(2.0, 0.5, 100, endpoint=True)
  y_pos = 1. / x_pos
  xx, yy = np.mgrid[-1.9:2.0:0.2, -1.9:2.0:0.2]
  zz = np.empty_like(xx)
  x, y = xx[:, 0], yy[0]

  x_temp, y_temp = gen_samples(10, 1.0, 0.0)

  cmap = matplotlib.cm.plasma
  plt.figure(figsize=(8, 7))
  ax = plt.gca()

  if what == 'all' or what == 'vectors':
    for i, a in enumerate(x):
      for j, b in enumerate(y):
        temp_model = ShallowNarrowLNN([a, b])
        da, db = temp_model.dloss_dw(x_temp, y_temp)
        zz[i, j] = temp_model.loss(temp_model.forward(x_temp), y_temp)
        scale = min(40 * np.sqrt(da**2 + db**2), 50)
        ax.quiver(a, b, - da, - db, scale=scale, color=cmap(np.sqrt(da**2 + db**2)))

  if what == 'all' or what == 'trajectory':
    if init_weights is None:
      for init_weights in [[0.5, -0.5], [0.55, -0.45], [-1.8, 1.7]]:
        temp_model = ShallowNarrowLNN(init_weights)
        _, temp_records = temp_model.train(x_temp, y_temp, lr, n_epochs)
        ax.scatter(temp_records[:, 0], temp_records[:, 1],
                    c=np.arange(len(temp_records)), cmap='Greys')
        ax.scatter(temp_records[0, 0], temp_records[0, 1], c='blue', zorder=9)
        ax.scatter(temp_records[-1, 0], temp_records[-1, 1], c='red', marker='X', s=100, zorder=9)
    else:
      temp_model = ShallowNarrowLNN(init_weights)
      _, temp_records = temp_model.train(x_temp, y_temp, lr, n_epochs)
      ax.scatter(temp_records[:, 0], temp_records[:, 1],
                  c=np.arange(len(temp_records)), cmap='Greys')
      ax.scatter(temp_records[0, 0], temp_records[0, 1], c='blue', zorder=9)
      ax.scatter(temp_records[-1, 0], temp_records[-1, 1], c='red', marker='X', s=100, zorder=9)

  if what == 'all' or what == 'loss':
    contplt = ax.contourf(x, y, np.log(zz+0.001), zorder=-1, cmap='coolwarm', levels=100)
    divider = make_axes_locatable(ax)
    cax = divider.append_axes("right", size="5%", pad=0.05)
    cbar = plt.colorbar(contplt, cax=cax)
    cbar.set_label('log (Loss)')

  ax.set_xlabel("$w_1$")
  ax.set_ylabel("$w_2$")
  ax.set_xlim(-1.9, 1.9)
  ax.set_ylim(-1.9, 1.9)

  plt.show()

def plot_loss_landscape():
  """
  """
  x_temp, y_temp = gen_samples(10, 1.0, 0.0)

  xx, yy = np.mgrid[-1.9:2.0:0.2, -1.9:2.0:0.2]
  zz = np.empty_like(xx)
  x, y = xx[:, 0], yy[0]

  for i, a in enumerate(x):
    for j, b in enumerate(y):
      temp_model = ShallowNarrowLNN([a, b])
      zz[i, j] = temp_model.loss(temp_model.forward(x_temp), y_temp)

  temp_model = ShallowNarrowLNN([-1.8, 1.7])
  loss_rec_1, w_rec_1 = temp_model.train(x_temp, y_temp, 0.02, 240)

  temp_model = ShallowNarrowLNN([1.5, -1.5])
  loss_rec_2, w_rec_2 = temp_model.train(x_temp, y_temp, 0.02, 240)

  plt.figure(figsize=(12, 8))
  ax = plt.subplot(1, 1, 1, projection='3d')
  ax.plot_surface(xx, yy, np.log(zz+0.5), cmap='coolwarm', alpha=0.5)
  ax.scatter3D(w_rec_1[:, 0], w_rec_1[:, 1], np.log(loss_rec_1+0.5),
                c='k', s=50, zorder=9)
  ax.scatter3D(w_rec_2[:, 0], w_rec_2[:, 1], np.log(loss_rec_2+0.5),
                c='k', s=50, zorder=9)
  plt.axis("off")
  ax.view_init(45, 260)

  plt.show()

def depth_widget(depth):
  if depth == 0:
    depth_lr_init_interplay(depth, 0.02, 0.9)
  else:
    depth_lr_init_interplay(depth, 0.01, 0.9)

def lr_widget(lr):
  depth_lr_init_interplay(50, lr, 0.9)

def depth_lr_interplay(depth, lr):
  depth_lr_init_interplay(depth, lr, 0.9)

def depth_lr_init_interplay(depth, lr, init_weights):
  n_epochs = 600

  x_train, y_train = gen_samples(100, 2.0, 0.1)
  model = DeepNarrowLNN(np.full((1, depth+1), init_weights))

  plt.figure(figsize=(10, 5))
  plt.plot(model.train(x_train, y_train, lr, n_epochs),
            linewidth=3.0, c='m')

  plt.title("Training a {}-layer LNN with"
  " $\eta=${} initialized with $w_i=${}".format(depth, lr, init_weights), pad=15)
  plt.yscale('log')
  plt.xlabel('epochs')
  plt.ylabel('Log mean squared error')
  plt.ylim(0.001, 1.0)
  plt.show()

def plot_init_effect():
  depth = 15
  n_epochs = 250
  lr = 0.02

  x_train, y_train = gen_samples(100, 2.0, 0.1)

  plt.figure(figsize=(12, 6))
  for init_w in np.arange(0.7, 1.09, 0.05):
      model = DeepNarrowLNN(np.full((1, depth), init_w))
      plt.plot(model.train(x_train, y_train, lr, n_epochs),
              linewidth=3.0, label="initial weights {:.2f}".format(init_w))
  plt.title("Training a {}-layer narrow LNN with $\eta=${}".format(depth, lr), pad=15)
  plt.yscale('log')
  plt.xlabel('epochs')
  plt.ylabel('Log mean squared error')
  plt.legend(loc='lower left', ncol=4)
  plt.ylim(0.001, 1.0)
  plt.show()

class InterPlay:
  def __init__(self):
    self.lr = [None]
    self.depth = [None]
    self.success = [None]
    self.min_depth, self.max_depth = 5, 65
    self.depth_list = np.arange(10, 61, 10)
    self.i_depth = 0
    self.min_lr, self.max_lr = 0.001, 0.105
    self.n_epochs = 600
    self.x_train, self.y_train = gen_samples(100, 2.0, 0.1)
    self.converged = False
    self.button = None
    self.slider = None

  def train(self, lr, update=False, init_weights=0.9):
    if update and self.converged and self.i_depth < len(self.depth_list):
      depth = self.depth_list[self.i_depth]
      self.plot(depth, lr)
      self.i_depth += 1
      self.lr.append(None)
      self.depth.append(None)
      self.success.append(None)
      self.converged = False
      self.slider.value = 0.005
      if self.i_depth < len(self.depth_list):
        self.button.value = False
        self.button.description = 'Explore!'
        self.button.disabled = True
        self.button.button_style = 'danger'
      else:
        self.button.value = False
        self.button.button_style = ''
        self.button.disabled = True
        self.button.description = 'Done!'
      time.sleep(1.0)

    elif self.i_depth < len(self.depth_list):
      depth = self.depth_list[self.i_depth]
      # assert self.min_depth <= depth <= self.max_depth
      assert self.min_lr <= lr <= self.max_lr
      self.converged = False

      model = DeepNarrowLNN(np.full((1, depth), init_weights))
      self.losses = np.array(model.train(self.x_train, self.y_train, lr, self.n_epochs))
      if np.any(self.losses < 1e-2):
        success = np.argwhere(self.losses < 1e-2)[0][0]
        if np.all((self.losses[success:] < 1e-2)):
          self.converged = True
          self.success[-1] = success
          self.lr[-1] = lr
          self.depth[-1] = depth
          self.button.disabled = False
          self.button.button_style = 'success'
          self.button.description = 'Register!'
        else:
          self.button.disabled = True
          self.button.button_style = 'danger'
          self.button.description = 'Explore!'
      else:
        self.button.disabled = True
        self.button.button_style = 'danger'
        self.button.description = 'Explore!'
      self.plot(depth, lr)

  def plot(self, depth, lr):
    fig = plt.figure(constrained_layout=False, figsize=(10, 8))
    gs = fig.add_gridspec(2, 2)
    ax1 = fig.add_subplot(gs[0, :])
    ax2 = fig.add_subplot(gs[1, 0])
    ax3 = fig.add_subplot(gs[1, 1])

    ax1.plot(self.losses, linewidth=3.0, c='m')
    ax1.set_title("Training a {}-layer LNN with"
    " $\eta=${}".format(depth, lr), pad=15, fontsize=16)
    ax1.set_yscale('log')
    ax1.set_xlabel('epochs')
    ax1.set_ylabel('Log mean squared error')
    ax1.set_ylim(0.001, 1.0)

    ax2.set_xlim(self.min_depth, self.max_depth)
    ax2.set_ylim(-10, self.n_epochs)
    ax2.set_xlabel('Depth')
    ax2.set_ylabel('Learning time (Epochs)')
    ax2.set_title("Learning time vs depth", fontsize=14)
    ax2.scatter(np.array(self.depth), np.array(self.success), c='r')

    # ax3.set_yscale('log')
    ax3.set_xlim(self.min_depth, self.max_depth)
    ax3.set_ylim(self.min_lr, self.max_lr)
    ax3.set_xlabel('Depth')
    ax3.set_ylabel('Optimial learning rate')
    ax3.set_title("Empirically optimal $\eta$ vs depth", fontsize=14)
    ax3.scatter(np.array(self.depth), np.array(self.lr), c='r')

    plt.show()
```

```python
# @title Helper functions

def gen_samples(n, a, sigma):
  """
  Generates `n` samples with `y = z * x + noise(sgma)` linear relation.

  Args:
    n : int
    a : float
    sigma : float
  Retutns:
   x : np.array
   y : np.array
  """
  assert n > 0
  assert sigma >= 0

  if sigma > 0:
    x = np.random.rand(n)
    noise = np.random.normal(scale=sigma, size=(n))
    y = a * x + noise
  else:
    x = np.linspace(0.0, 1.0, n, endpoint=True)
    y = a * x
  return x, y

class ShallowNarrowLNN:
  """
  Shallow and narrow (one neuron per layer) linear neural network
  """
  def __init__(self, init_ws):
    """
    init_ws: initial weights as a list
    """
    assert isinstance(init_ws, list)
    assert len(init_ws) == 2
    self.w1 = init_ws[0]
    self.w2 = init_ws[1]

  def forward(self, x):
    """
    The forward pass through netwrok y = x * w1 * w2
    """
    y = x * self.w1 * self.w2
    return y

  def loss(self, y_p, y_t):
    """
    Mean squared error (L2) with 1/2 for convenience
    """
    assert y_p.shape == y_t.shape
    mse = ((y_t - y_p)**2).mean()
    return mse

  def dloss_dw(self, x, y_t):
    """
    partial derivative of loss with respect to weights

    Args:
      x : np.array
      y_t : np.array
    """
    assert x.shape == y_t.shape
    Error = y_t - self.w1 * self.w2 * x
    dloss_dw1 = - (2 * self.w2 * x * Error).mean()
    dloss_dw2 = - (2 * self.w1 * x * Error).mean()
    return dloss_dw1, dloss_dw2

  def train(self, x, y_t, eta, n_ep):
    """
    Gradient descent algorithm

    Args:
      x : np.array
      y_t : np.array
      eta: float
      n_ep : int
    """
    assert x.shape == y_t.shape

    loss_records = np.empty(n_ep)  # pre allocation of loss records
    weight_records = np.empty((n_ep, 2))  # pre allocation of weight records

    for i in range(n_ep):
      y_p = self.forward(x)
      loss_records[i] = self.loss(y_p, y_t)
      dloss_dw1, dloss_dw2 = self.dloss_dw(x, y_t)
      self.w1 -= eta * dloss_dw1
      self.w2 -= eta * dloss_dw2
      weight_records[i] = [self.w1, self.w2]

    return loss_records, weight_records

class DeepNarrowLNN:
  """
  Deep but thin (one neuron per layer) linear neural network
  """
  def __init__(self, init_ws):
    """
    init_ws: initial weights as a numpy array
    """
    self.n = init_ws.size
    self.W = init_ws.reshape(1, -1)

  def forward(self, x):
    """
    x : np.array
      input features
    """
    y = np.prod(self.W) * x
    return y

  def loss(self, y_t, y_p):
    """
    mean squared error (L2 loss)

    Args:
      y_t : np.array
      y_p : np.array
    """
    assert y_p.shape == y_t.shape
    mse = ((y_t - y_p)**2 / 2).mean()
    return mse

  def dloss_dw(self, x, y_t, y_p):
    """
    analytical gradient of weights

    Args:
      x : np.array
      y_t : np.array
      y_p : np.array
    """
    E = y_t - y_p  # = y_t - x * np.prod(self.W)
    Ex = np.multiply(x, E).mean()
    Wp = np.prod(self.W) / (self.W + 1e-9)
    dW = - Ex * Wp
    return dW

  def train(self, x, y_t, eta, n_epochs):
    """
    training using gradient descent

    Args:
      x : np.array
      y_t : np.array
      eta: float
      n_epochs : int
    """
    loss_records = np.empty(n_epochs)
    loss_records[:] = np.nan
    for i in range(n_epochs):
      y_p = self.forward(x)
      loss_records[i] = self.loss(y_t, y_p).mean()
      dloss_dw = self.dloss_dw(x, y_t, y_p)
      if np.isnan(dloss_dw).any() or np.isinf(dloss_dw).any():
        return loss_records
      self.W -= eta * dloss_dw
    return loss_records
```

```python
#@title Set random seed

#@markdown Executing `set_seed(seed=seed)` you are setting the seed

# for DL its critical to set the random seed so that students can have a
# baseline to compare their results to expected results.
# Read more here: https://pytorch.org/docs/stable/notes/randomness.html

# Call `set_seed` function in the exercises to ensure reproducibility.
import random
import torch

def set_seed(seed=None, seed_torch=True):
  if seed is None:
    seed = np.random.choice(2 ** 32)
  random.seed(seed)
  np.random.seed(seed)
  if seed_torch:
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.cuda.manual_seed(seed)
    torch.backends.cudnn.benchmark = False
    torch.backends.cudnn.deterministic = True

  print(f'Random seed {seed} has been set.')

# In case that `DataLoader` is used
def seed_worker(worker_id):
  worker_seed = torch.initial_seed() % 2**32
  np.random.seed(worker_seed)
  random.seed(worker_seed)
```

```python
#@title Set device (GPU or CPU). Execute `set_device()`
# especially if torch modules used.

# inform the user if the notebook uses GPU or CPU.

def set_device():
  device = "cuda" if torch.cuda.is_available() else "cpu"
  if device != "cuda":
    print("WARNING: For this notebook to perform best, "
        "if possible, in the menu under `Runtime` -> "
        "`Change runtime type.`  select `GPU` ")
  else:
    print("GPU is enabled in this notebook.")

  return device
```

```python
SEED = 2021
set_seed(seed=SEED)
DEVICE = set_device()
```

---
# Section 1: A Shallow Narrow Linear Neural Network

## Section 1.1: A neural network from scratch

```python
# @title Video 1: Shallow Narrow Linear Net
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1F44y117ot", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"6e5JIYsqVvU", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

To better understand the behavior of neural network training with gradient descent, we start with the incredibly simple case of a shallow narrow linear neural net, since state-of-the-art models are impossible to dissect and comprehend with our current mathematical tools.

The model we use has one hidden layer, with only one neuron, and two weights. We consider the squared error (or L2 loss) as the cost function. As you may have already guessed, we can visualize the model as a neural network:

or by its computation graph:

or on a rare occasion, even as a reasonably compact mapping:

$$ loss = (y - w_1 \cdot w_2 \cdot x)^2 $$

### Analytical Exercise 1.1: Loss Gradients

#### Part i: Calculate gradients
Once again, we ask you to calculate the network gradients analytically, since you will need them for the next exercise. We understand how annoying this is.

$\dfrac{\partial{loss}}{\partial{w_1}} = ?$

$\dfrac{\partial{loss}}{\partial{w_2}} = ?$

---
#### Solution

$\dfrac{\partial{loss}}{\partial{w_1}} = -2 \cdot w_2 \cdot x \cdot (y - w_1 \cdot w_2 \cdot x)$

$\dfrac{\partial{loss}}{\partial{w_2}} = -2 \cdot w_1 \cdot x \cdot (y - w_1 \cdot w_2 \cdot x)$

---

### Coding Exercise 1.1: Implement simple narrow LNN

Next, we ask you to implement the `forward` pass for our model from scratch without using PyTorch.

Also, although our model gets a single input feature and outputs a single prediction, we could calculate the loss and perform training for multiple samples at once. This is the common practice for neural networks, since computers are incredibly fast doing matrix (or tensor) operations on batches of data, rather than processing samples one at a time through `for` loops. Therefore, for the `loss` function, please implement the **mean** squared error (MSE), and adjust your analytical gradients accordingly when implementing the `dloss_dw` function.

Finally, complete the `train` function for the gradient descent algorithm:

\begin{equation}
\mathbf{w}^{(t+1)} = \mathbf{w}^{(t)} - \eta \nabla loss (\mathbf{w}^{(t)})
\end{equation}

```python
class ShallowNarrowExercise:
  """Shallow and narrow (one neuron per layer) linear neural network
  """
  def __init__(self, init_weights):
    """
    Args:
      init_weights (list): initial weights
    """
    assert isinstance(init_weights, (list, np.ndarray, tuple))
    assert len(init_weights) == 2
    self.w1 = init_weights[0]
    self.w2 = init_weights[1]

  def forward(self, x):
    """The forward pass through netwrok y = x * w1 * w2

    Args:
      x (np.ndarray): features (inputs) to neural net

    returns:
      (np.ndarray): neural network output (prediction)
    """
    #################################################
    ## Implement the forward pass to calculate prediction
    ## Note that prediction is not the loss
    # Complete the function and remove or comment the line below
    raise NotImplementedError("Forward Pass `forward`")
    #################################################
    y = ...
    return y

  def dloss_dw(self, x, y_true):
    """Gradient of loss with respect to weights

    Args:
      x (np.ndarray): features (inputs) to neural net
      y_true (np.ndarray): true labels

    returns:
      (float): mean gradient of loss with respect to w1
      (float): mean gradient of loss with respect to w2
    """
    assert x.shape == y_true.shape
    #################################################
    ## Implement the gradient computation function
    # Complete the function and remove or comment the line below
    raise NotImplementedError("Gradient of Loss `dloss_dw`")
    #################################################
    dloss_dw1 = ...
    dloss_dw2 = ...
    return dloss_dw1, dloss_dw2

  def train(self, x, y_true, lr, n_ep):
    """Training with Gradient descent algorithm

    Args:
      x (np.ndarray): features (inputs) to neural net
      y_true (np.ndarray): true labels
      lr (float): learning rate
      n_ep (int): number of epochs (training iterations)

    returns:
      (list): training loss records
      (list): training weight records (evolution of weights)
    """
    assert x.shape == y_true.shape

    loss_records = np.empty(n_ep)  # pre allocation of loss records
    weight_records = np.empty((n_ep, 2))  # pre allocation of weight records

    for i in range(n_ep):
      y_prediction = self.forward(x)
      loss_records[i] = loss(y_prediction, y_true)
      dloss_dw1, dloss_dw2 = self.dloss_dw(x, y_true)
      #################################################
      ## Implement the gradient descent step
      # Complete the function and remove or comment the line below
      raise NotImplementedError("Training loop `train`")
      #################################################
      self.w1 -= ...
      self.w2 -= ...
      weight_records[i] = [self.w1, self.w2]

    return loss_records, weight_records

def loss(y_prediction, y_true):
  """Mean squared error

  Args:
    y_prediction (np.ndarray): model output (prediction)
    y_true (np.ndarray): true label

  returns:
    (np.ndarray): mean squared error loss
  """
  assert y_prediction.shape == y_true.shape
  #################################################
  ## Implement the MEAN squared error
  # Complete the function and remove or comment the line below
  raise NotImplementedError("Loss function `loss`")
  #################################################
  mse = ...
  return mse

set_seed(seed=SEED)
n_epochs = 211
learning_rate = 0.02
initial_weights = [1.4, -1.6]
x_train, y_train = gen_samples(n=73, a=2.0, sigma=0.2)
x_eval = np.linspace(0.0, 1.0, 37, endpoint=True)
## Uncomment to run
# sn_model = ShallowNarrowExercise(initial_weights)
# loss_log, weight_log = sn_model.train(x_train, y_train, learning_rate, n_epochs)
# y_eval = sn_model.forward(x_eval)
# plot_x_y_(x_train, y_train, x_eval, y_eval, loss_log, weight_log)
```

```python
# to_remove solution
class ShallowNarrowExercise:
  """Shallow and narrow (one neuron per layer) linear neural network
  """
  def __init__(self, init_weights):
    """
    Args:
      init_weights (list): initial weights
    """
    assert isinstance(init_weights, (list, np.ndarray, tuple))
    assert len(init_weights) == 2
    self.w1 = init_weights[0]
    self.w2 = init_weights[1]

  def forward(self, x):
    """The forward pass through netwrok y = x * w1 * w2

    Args:
      x (np.ndarray): features (inputs) to neural net

    returns:
      (np.ndarray): neural network output (prediction)
    """
    y = x * self.w1 * self.w2
    return y

  def dloss_dw(self, x, y_true):
    """Gradient of loss with respect to weights

    Args:
      x (np.ndarray): features (inputs) to neural net
      y_true (np.ndarray): true labels

    returns:
      (float): mean gradient of loss with respect to w1
      (float): mean gradient of loss with respect to w2
    """
    assert x.shape == y_true.shape
    dloss_dw1 = - (2 * self.w2 * x * (y_true - self.w1 * self.w2 * x)).mean()
    dloss_dw2 = - (2 * self.w1 * x * (y_true - self.w1 * self.w2 * x)).mean()
    return dloss_dw1, dloss_dw2

  def train(self, x, y_true, lr, n_ep):
    """Training with Gradient descent algorithm

    Args:
      x (np.ndarray): features (inputs) to neural net
      y_true (np.ndarray): true labels
      lr (float): learning rate
      n_ep (int): number of epochs (training iterations)

    returns:
      (list): training loss records
      (list): training weight records (evolution of weights)
    """
    assert x.shape == y_true.shape

    loss_records = np.empty(n_ep)  # pre allocation of loss records
    weight_records = np.empty((n_ep, 2))  # pre allocation of weight records

    for i in range(n_ep):
      y_prediction = self.forward(x)
      loss_records[i] = loss(y_prediction, y_true)
      dloss_dw1, dloss_dw2 = self.dloss_dw(x, y_true)
      self.w1 -= lr * dloss_dw1
      self.w2 -= lr * dloss_dw2
      weight_records[i] = [self.w1, self.w2]

    return loss_records, weight_records

def loss(y_prediction, y_true):
  """Mean squared error

  Args:
    y_prediction (np.ndarray): model output (prediction)
    y_true (np.ndarray): true label

  returns:
    (np.ndarray): mean squared error loss
  """
  assert y_prediction.shape == y_true.shape
  mse = ((y_true - y_prediction)**2).mean()
  return mse

set_seed(seed=SEED)
n_epochs = 211
learning_rate = 0.02
initial_weights = [1.4, -1.6]
x_train, y_train = gen_samples(n=73, a=2.0, sigma=0.2)
x_eval = np.linspace(0.0, 1.0, 37, endpoint=True)
## Uncomment to run
sn_model = ShallowNarrowExercise(initial_weights)
loss_log, weight_log = sn_model.train(x_train, y_train, learning_rate, n_epochs)
y_eval = sn_model.forward(x_eval)
with plt.xkcd():
  plot_x_y_(x_train, y_train, x_eval, y_eval, loss_log, weight_log)
```

## Section 1.2: Learning landscapes

```python
# @title Video 2: Training Landscape
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1Nv411J71X", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"k28bnNAcOEg", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

As you may have already asked yourself, we can analytically find $w_1$ and $w_2$ without using gradient descent:

\begin{equation}
w_1 \cdot w_2 = \dfrac{y}{x}
\end{equation}

In fact, we can plot the gradients, the loss function and all the possible solutions in one figure. In this example, we use the $y = 1x$ mapping:

**Blue ribbon**: shows all possible solutions: $~ w_1 w_2 = \dfrac{y}{x} = \dfrac{x}{x} = 1 \Rightarrow w_1 = \dfrac{1}{w_2}$

**Contour background**: Shows the loss values, red being higher loss

**Vector field (arrows)**: shows the gradient vector field. The larger yellow arrows show larger gradients, which correspond to bigger steps by gradient descent.

**Scatter circles**: the trajectory (evolution) of weights during training for three different initializations, with blue dots marking the start of training and red crosses ( **x** ) marking the end of training. You can also try your own initializations (keep the initial values between `-2.0` and `2.0`) as shown here:
```python
plot_vector_field('all', [1.0, -1.0])
```

Finally, if the plot is too crowded, feel free to pass one of the following strings as argument:

```python
plot_vector_field('vectors')  # for vector field
plot_vector_field('trajectory')  # for training trajectory
plot_vector_field('loss')  # for loss contour
```

### Think and Talk: Learning Landscape

Explore the next two plots. Try different initial values.  Can you find the saddle point? Why does training slow down near the minima? Discuss your findings with your pod.

```python
plot_vector_field('all')
```

Here, we also visualize the loss landscape in a 3-D plot, with two training trajectories for different initial conditions.
Note: the trajectories from the 3D plot and the previous plot are independent and different.

```python
plot_loss_landscape()
```

```python
# @title Video 3: Training Landscape - Discussion
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1py4y1j7cv", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"0EcUGgxOdkI", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

---
# Section 2: Depth, Learning rate, and initialization

Successful deep learning models are often developed by a team of very clever people, spending many many hours "tuning" learning hyperparameters, and finding effective initializations. In this section, we look at three basic (but often not simple) hyperparameters: depth, learning rate, and initialization.

## Section 2.1: The effect of depth

```python
# @title Video 4: Effect of Depth
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1z341167di", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"Ii_As9cRR5Q", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

In 1989, George Cybenko published the paper *Approximation by superpositions of a sigmoidal function*, mathematically proving that:

> arbitrary decision regions can be arbitrarily well approximated by continuous feedforward neural networks with only a single internal, hidden layer and any continuous sigmoidal nonlinearity.

So if a neural net with a single hidden layer can approximate any function, why might depth be useful? What makes a network or learning system "deep"? The reality is that shallow neural nets are often incapable of learning complex functions due to data limitations. On the other hand, depth seems like magic. Depth can change the functions a network can represent, the way a network learns, and how a network generalizes to unseen data.

So let's look at the challenges that depth poses in training a neural network. Imagine a single input, single output linear network with 50 hidden layers and only one neuron per layer (i.e. a narrow deep neural network). The output of the network is easy to calculate:

$$ prediction = x \cdot w_1 \cdot w_2 \cdot \cdot \cdot w_{50} $$

If the initial value for all the weights is $w_i = 2$, the prediction for $x=1$ would be **exploding**: $y_p = 2^{50} \approx 1.1256 \times 10^{15}$. On the other hand, for weights initialized to $w_i = 0.5$, the output is **vanishing**: $y_p = 0.5^{50} \approx 8.88 \times 10^{-16}$. Similarly, if we recall the chain rule, as the graph gets deeper, the number of elements in the chain multiplication increases, which could lead to exploding or vanishing gradients. To avoid such numerical vulnerablities that could impair our training algorithm, we need to understand the effect of depth.

### Interactive Demo 2.1: Depth widget

Use the widget to explore the impact of depth on the training curve (loss evolution) of a deep but narrow neural network and discuss your findings with your pod. More specifically, try answering the following questions.

**Questions**:
Which networks trained the fastest? Did all networks eventually "work" (converge)? What is the shape of their learning trajectory?

```python
# @markdown Make sure you execute this cell to enable the widget!

_ = interact(depth_widget,
    depth = IntSlider(min=0, max=51,
                      step=5, value=0,
                      continuous_update=False))
```

```python
# @title Video 5: Effect of Depth - Discussion
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1Qq4y1H7uk", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"EqSDkwmSruk", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

## Section 2.2: Choosing a learning rate

The learning rate is a common hyperparameter for most optimization algorithms. How should we set it? Sometimes the only option is to try all the possibilities, but sometimes knowing some key trade-offs will help guide our search for good hyperparameters.

```python
# @title Video 6: Learning Rate
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV11f4y157MT", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"w_GrCVM-_Qo", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

### Interactive Demo 2.2: Learning rate widget

Here, we fix the network depth to 50 layers. Use the widget to explore the impact of learning rate $\eta$ on the training curve (loss evolution) of a deep but narrow neural network and discuss your findings with your pod. More specifically, try answering the following questions.

**Questions**:
Can we say that larger learning rates always lead to faster learning? Why not?

```python
# @markdown Make sure you execute this cell to enable the widget!

_ = interact(lr_widget,
    lr = FloatSlider(min=0.005, max=0.045, step=0.005, value=0.005,
                     continuous_update=False, readout_format='.3f',
                     description='eta'))
```

```python
# @title Video 7: Learning Rate - Discussion
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1Aq4y1p7bh", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"cmS0yqImz2E", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

## Section 2.3: Depth vs Learning Rate

```python
# @title Video 8: Depth and Learning Rate Interplay
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1V44y1177e", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"J30phrux_3k", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

### Interactive Demo 2.3: Depth and Learning-Rate

**Important instruction**
The exercise starts with 10 hidden layers. Your task is to find the learning rate that delivers fast but robust convergence (learning). When you are confident about the learning rate, you can **Register** the optimal learning rate for the given depth. Once you press register, a deeper model is instantiated, so you can find the next optimal learning rate. The Register button turns green only when the training converges, but does not imply the fastest convergence. Finally, Be patient :-) the widgets are slow.

Discuss your findings with your pod. More specifically, try answering the following questions.

**Questions**: Can you explain the relationship between the depth and optimal learning rate?

```python
# @markdown Make sure you execute this cell to enable the widget!
intpl_obj = InterPlay()

intpl_obj.slider = FloatSlider(min=0.005, max=0.105, step=0.005, value=0.005,
                               layout=Layout(width='500px'),
                               continuous_update=False,
                               readout_format='.3f',
                               description='eta')

intpl_obj.button = ToggleButton(value=intpl_obj.converged, description='Register')

widgets_ui = HBox([intpl_obj.slider, intpl_obj.button])
widgets_out = interactive_output(intpl_obj.train,
                                 {'lr': intpl_obj.slider,
                                  'update': intpl_obj.button,
                                  'init_weights': fixed(0.9)})

display(widgets_ui, widgets_out)
```

```python
# @title Video 9: Depth and Learning Rate - Discussion
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV15q4y1p7Uq", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"7Fl8vH7cgco", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

## Section 2.4: Why initialization is important

```python
# @title Video 10: Initialization Matters
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1UL411J7vu", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"KmqCz95AMzY", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

We’ve seen, even in the simplest of cases, that depth can slow learning. Why? From the chain rule, gradients are multiplied by the current weight at each layer, so the product can vanish or explode. Therefore, weight initialization is a fundamentally important hyperparameter.

Although in practice initial values for learnable parameters are often sampled from different $\mathcal{Uniform}$ or $\mathcal{Normal}$ probability distribution, here we use a single value for all the parameters.

The figure below shows the effect of initialization on the speed of learning for the deep but narrow LNN. We have excluded initializations that lead to numerical errors such as `nan` or `inf`, which are the consequence of smaller or larger initializations.

```python
# @markdown Make sure you execute this cell to see the figure!

plot_init_effect()
```

```python
# @title Video 11: Initialization Matters Explained
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1hM4y1T7gJ", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"vKktGdiQDsE", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

---
# Summary

In the second tutorial, we have learned what is the training landscape, and also we have see in depth the effect of the depth of the network and the learning rate, and their interplay. Finally, we have seen that initialization matters and why we need smart ways of initialization.

```python
# @title Video 12: Tutorial 2 Wrap-up
from ipywidgets import widgets

out2 = widgets.Output()
with out2:
  from IPython.display import IFrame
  class BiliVideo(IFrame):
    def __init__(self, id, page=1, width=400, height=300, **kwargs):
      self.id=id
      src = "https://player.bilibili.com/player.html?bvid={0}&page={1}".format(id, page)
      super(BiliVideo, self).__init__(src, width, height, **kwargs)

  video = BiliVideo(id=f"BV1P44y117Pd", width=854, height=480, fs=1)
  print("Video available at https://www.bilibili.com/video/{0}".format(video.id))
  display(video)

out1 = widgets.Output()
with out1:
  from IPython.display import YouTubeVideo
  video = YouTubeVideo(id=f"r3K8gtak3wA", width=854, height=480, fs=1, rel=0)
  print("Video available at https://youtube.com/watch?v=" + video.id)
  display(video)

out = widgets.Tab([out1, out2])
out.set_title(0, 'Youtube')
out.set_title(1, 'Bilibili')

display(out)
```

---
# Appendix

## Hyperparameter interaction

Finally, let's put everything we learned together and find best initial weights and learning rate for a given depth. By now you should have learned the interactions and know how to find the optimal values quickly. If you get `numerical overflow` warnings, don't be discouraged! They are often caused by "exploding" or "vanishing" gradients.

**Question**: Did you experience any surprising behaviour or difficulty finding the optimal parameters?

```python
# @markdown Make sure you execute this cell to enable the widget!

_ = interact(depth_lr_init_interplay,
             depth = IntSlider(min=10, max=51, step=5, value=25,
                               continuous_update=False),
             lr = FloatSlider(min=0.001, max=0.1,
                              step=0.005, value=0.005,
                              continuous_update=False,
                              readout_format='.3f',
                              description='eta'),
             init_weights = FloatSlider(min=0.1, max=3.0,
                                        step=0.1, value=0.9,
                                        continuous_update=False,
                                        readout_format='.3f',
                                        description='initial weights'))
```


# Atmospheric* Data Science Workshops
<div style="text-align: right"> *and oceanic </div>

## Python : xArray and multidimensionnal gridded data

Pascal Bourgault, 05-04-2019

In this workshop we will use `xArray` and friends to read, analyse, plot and write multidimensionnal gridded data.

- `xArray` is built on `Pandas`, a lot of the functionalities are similar and use similar syntax.
- It is coded to follow the [CF conventions](http://cfconventions.org/), the same that define de `netCDF` format, making fully compatible with `nc` files.
- We can already read `netCDF` files to `numpy` arrays through  `scipy` or `netCDF4`, but `xArray` unleashes Python's power and blends in with `matplotlib` and `pandas` to offer a **perfect** data exploration tool for the natural sciences.

`xArray` doesn't come with the default anaconda distribution, we install it first! I reccommend using the anaconda navigator or a terminal, but you can run the following line if you feel adventurous:

```python
#!conda install xarray
```

Now we can import what we will need for the first part:

```python
import xarray as xr
import numpy as np
import matplotlib.pyplot as plt
```

### Opening and selecting

```python
data = xr.open_dataset('DurackandWijffels_GlobalOceanChanges_1950-2000.nc')
```

`xArray` mainly uses two objects : `DataArray` and `Dataset`.

 - A `DataArray` is a named N-D array with named axes or *dimensions* that can have values, making them *coordinates*.
 - A `Dataset` contains many `DataArray` instances. If a `DataArray` as only one dimension, it can be promoted to a coordinate.
 By default, a `DataArray` with the same name as its dimension is automatically promoted to coordinate.

 - Both can have any number of *attributes*. The CF conventions regulate some of them. Most important for us are : `units` and `long_name` (or `standard_name` if the latter is absent).

```python
data
```

```python
data['thetao_change']
```

```python
data.depth
```

The values are stored as a numpy array:

```python
# data.thetao_change.values
data.latitude.values
```

The attributes are stored in an *ordered* dictionary, so we can modify them as we would a dict!

```python
data.latitude.attrs['units'] = '°N'  # By the bracket-getter
data.longitude.attrs.update(units='°W') # My favorite : by the update() method.
data.latitude
```

Most of the data exploration can be done in one-liners. First useful functions `.sel()` and `.isel()` to select data based on the coordinates *or* the indexes.

```python
# We can use keywords with lists, scalars or slices
data.thetao_change.sel(depth=[20, 60], longitude=200, latitude=slice(-10.5, 10.5))
```

```python
# Or a dictionary. Here with isel, that takes indices
data.thetao_change.isel({'depth': [3, 7],
                         'longitude': 100,
                         'latitude':slice(60, 81)})
# this is equivalent to
#data.thetao_change[:, [3, 7], 60:81, 100]
```

```python
# Even with approximate values!
data.thetao_change.sel(depth=0.1, latitude=-10.5, longitude=199.5, method='nearest')
```

```python
# Or by criteria
data.thetao_change.where((data.depth < 30) & (data.latitude == -10), drop=True)
```

### Simple plotting

With the help of the `.plot()` function, visualisation of data has never been this easy!

```python
data.thetao_change.sel(depth=0).plot()
```

`.plot()` automatically chooses the plot type based on the number dimensions.

 - 2D -> polormesh
 - 1D -> lines
 - ND -> histogram

 But you can access other types (contour, imshow, step).

 Most of the plot control is done through keywords.

The colormaps can be changed. By default, xarray checks if 0 is in the value range, if it is, it uses the Red-Blue divergent colormap, if not, the default sequential viridis.

To see the low details more, we can cut out the extreme values:

```python
fig = plt.figure(figsize=(12, 5)) # Create an empty figure so we can choose it's size
data.thetao_change.sel(depth=0).plot(robust=True, cmap=plt.cm.BrBG)
data.salinity_change.isel(depth=0, time=0).plot.contour(robust=True, colors='k')
plt.gca().set_facecolor('lightgrey')
```

Multiple faces can be shown at the same time using `x`, `y`, `col` and/or `row`.

```python
data.salinity_mean.sel(longitude=[200, 340]).plot(robust=True, col='longitude', y='depth', yincrease=False, aspect=2)
```

Same story with line plots and `hue`:

```python
data.thetao_change.sel(depth=[0, 200, 1000], longitude=[80, 200, 340]).plot(col='longitude', hue='depth')
```

Look at how it uses the `units` and `long_name` attributes of the `DataArray`s. We can modify them to have better labels. Or change the labels directly, using `matplotlib.pyplot`'s functions.

```python
data.depth.attrs.update(units='db', long_name='Pressure')
data.thetao_change.attrs.update(units='°C')

fig, ax = plt.subplots()  # Create an empty fig with 1 axis

data.thetao_change.sel(longitude=200, latitude=np.arange(-60, 61, 30)).plot(y='depth', hue='latitude', yincrease=False, ax=ax)

ax.set_title(r'$\Delta\theta(z)$ 1950-2000 in the Pacific Ocean')
```

### Array operations

As with pandas, we can do most default numpy operations on the arrays. Instead of using `axis` like in numpy, we can use the powerful dimension syntax.

Notice how, xarray *always* returns a `DataArray`, even with only one point.

```python
data.salinity_change.sel(latitude=np.arange(-60, 61, 30)).mean('longitude').plot(y='depth', hue='latitude', yincrease=False)
```

Also powerful, the possibility to group values in bins :

```python
data.salinity_change.sel(depth=[0, 20, 200]).groupby_bins('latitude', 10).mean(['latitude', 'longitude']).plot(hue='depth')
```

Or a running mean:

```python
data.salinity_mean.sel(depth=[0, 20, 200]).mean('longitude').rolling(latitude=10, center=True).mean().plot(hue='depth')
```

In the idea of keeping a single idea writable in a single line, `dataset`s and `DataArray`s provide the useful `.pipe()` method allowing apply a function on itself.

For example, this could be useful for a unit transformation, when you don't want to create a new array just for this.

```python
data.thetao_mean.sel(latitude=-60).pipe(lambda x: x + 273.15).plot(y='depth', yincrease=False,
                                                                   center=273.15, robust=True,
                                                                   cbar_kwargs={'label':r'$\overline{\theta}$ [K]'})
# Equivalent to
# theta_kelvin = data.thetao_change.sel(longitude=200) + 273.15
# theta_kelvin.plot(...)
```

And many more. Very useful can be the `.interp()` and `.interpolate_na()` methodes to interpolate to new coordinates (needs `scipy` installed). Not showns here but good examples are available in [the documentation](http://xarray.pydata.org/en/stable/interpolation.html)

### Combining and adding variables

We use this dumb linear EOS to compute our mean potential density:

\begin{equation}
    \sigma = \sigma_0\left( 1 - \alpha (T - T_0) + \beta (S - S_0)\right)
\end{equation}

With $\sigma_0 = 1028$ kg/m³, $\alpha = 2\times 10^{-4}$ K$^{-1}$, $T_0 = 10$°C, $\beta = 7\times 10^{-4}$ and $S_0 = 35$ g/kg.

```python
fake_sigma = 1028 * (1 - 2e-4 * (data.thetao_mean - 10) + 7e-4 * (data.salinity_mean - 35))
```

```python
fake_sigma
```

```python
fake_sigma.attrs.update(units='kg/m³', long_name='Dumb potential density')
fake_sigma
```

```python
fake_sigma.sel(latitude=[-70, 0, 70]).mean('longitude').plot(y='depth', yincrease=False, hue='latitude')
```

In xArray, datasets are non-mutable, meaning you normally cannot modify it inplace. Most methods return copies and so we usually reassign to the same variable name.

```python
data = data.assign(sigma=fake_sigma)
```

```python
data.to_netcdf('Ocean_changes_with_density.nc')
```

```python
!ls -alh *.nc
```

## Exercices

### 1.
Show the meridional mean salinity change of the southern ocean, but only where the temperature change was negative. (Let's define the Southern Ocean as everything below 40°S)

```python

```

## Bonus One : Maps

xArray integrates correctly with Python's main mapping package : [Cartopy](https://scitools.org.uk/cartopy/).
Cartopy is the Phenix that remerged from the defunct basemap. It is used seemingly the same way and is fully integrated to matplotlib.

It's not installed with the default anaconda, so we risk our system integrity and run:

```python
#! conda install -c conda-forge cartopy
```

```python
import cartopy.crs as ccrs
```

Cartopy adds a "projection" to your classic Matplotlib axes and ask for "transform"s to be able to plot data.

Because we are heartless heretics, we will use Mercator:

```python
fig, ax = plt.subplots(subplot_kw={'projection':ccrs.Mercator(min_latitude=-70, max_latitude=70)},
                       figsize=(15, 6))
data.salinity_change.isel(time=0, depth=0).plot.contourf(ax=ax, transform=ccrs.PlateCarree(), robust=True)
#data.thetao_change.isel(time=0, depth=0).plot.contour(ax=ax, transform=ccrs.PlateCarree(), robust=True, cmap=plt.cm.BrBG)
ax.coastlines()
```

## Bonus Two : Parallel and memory-safe computing

Here we use dask, the under-the-hood friend of xArray, to do operations in parallel. Or, even more interestingly, on datasets larger than what our RAM allows.

When you open a multi-file dataset with xArray, it uses dask by default. Instead of reading in all values of all variables, it only reads the metadata and stores the information in dask arrays lazily. Meaning, the values will only be read from the file if you need them. By default, each different file is stored in a different "chunk", but this behavior can be modified and the data chunked at will.

Furthermore, when computing things, the process is done chunk by chunk. So you only need enough ram to store one chunk. And it can do operations on more than one chunk at a time!

```python
#! conda install dask
```

```python
from dask.diagnostics import ProgressBar
```

```python
d = xr.open_mfdataset('so_Omon_ORAreanalysis_ORAS4*')
d
```

```python
%time
a = d.so.rolling(time=3, center=True).construct('roll').dot(xr.DataArray([0.25, 0.5, 0.25], name='weights', dims=['roll'])).mean('lon')
```

```python
with ProgressBar():
    out = a.compute()
```

```python
out.sel(time='1985-05').plot(robust=True, yincrease=False)
```

```python

```

```python

```


# Notes on Coase, Property rights and the 'Coase Theorem'

Coase, R. H. 1960. “The Problem of Social Cost.” *The Journal of Law and Economics* 3:1–44.

Coase, Ronald H. 1937. “The Nature of the Firm.” *Economica* 4 (16):386–405.

**Slideshow mode**: this notebook can be viewed as a slideshow by pressing Alt-R if run on a server.

## Coase (1960) The Problem of Social Cost

### A rancher and wheat farmer.

Both are utilizing adjacent plots of land. No fence separates the lands.

**The Wheat Farmer:** chooses a production method that delivers a maximum profit of $\Pi_W =8$.
- to keep this simple suppose this is the farmer's only production choice.

**The Rancher:**  chooses herd size $x$ to maximize profits $\Pi_C(x) = P \cdot F(x) - c \cdot x^2$

- $P$ is cattle price and $c$ is the cost of feeding each animal.

- The herd size $x^*$ that maximizes profits given by:

$$P \cdot F'(x^*) = c$$

**Example:** If $F(x) = x$, $c=\frac{1}{2}$.

The FOC are $x^{*} = P_c$

With $P_c=4$ and $c=\frac{1}{2}$, the rancher's privately optimal herd size of  $x^* = 4$

#### Missing Property Rights impose external costs

With no effective barrier separating the fields cattle sometimes strays into the wheat farmer's fields, damaging crops and reducing wheat farmer's profits.

Assume that if the rancher keeps a herd size $x$ net profits in wheat are reduced from $\Pi_W$ to:

$$\Pi_W(x) = \Pi_W - d \cdot x^2$$

**The external cost**

Suppose $d=\frac{1}{2}$

At the rancher's private optimum herd size of $x^*=4$, the farmer's profit is reduced from 8 to zero:

$$\begin{align}
\Pi_W(x) &= \Pi_W - d \cdot x^2  \\
 & = 8 - \frac{1}{2} \cdot 4^2 = 0
 \end{align}$$

```python
%load_ext autoreload
%autoreload 2
```

```python
from coase import *
```

At private optimum Rancher earns \$8 but imposes external costs that drive the farmer's earnings to zero.

```python
coaseplot1()
```

Private and social marginal benefits and costs can be plotted to see deadweight loss (DWL) differently:

```python
coaseplot2()
```

## The assignment of property rights (liability)

**Scenario 1:** Farmer is given the right to enjoin (i.e. limit or prohibit) cattle herding.

If the farmer enforces a prohibition on all cattle herding:

- Rancher now earns \$0.
- Farmer earns \$8.

- But this is not efficient!  Total output is smaller than it could be.
- If transactions costs are low the two parties can bargain to a more efficient outcome.

**Scenario 1:** Farmer is given the right to enjoin (i.e. limit or prohibit) cattle herding.

Rancher reasons that if she were permitted to herd 2 cattle she'd earn $\$6$ while imposing \$2 in damage.
   - She could offer $\$2$ in full compensation for damage, pocketing remaining \$4
   - or they could bargain over how to divide the gains to trade of \$4 in other ways.

**Scenario 2:**  Rancher is granted right to graze with impunity.

Farmer reasons that if herd size could be reduced from 4 to 2
- farm profits would rise from $\$0$ to $\$6$
- rancher's profits would fall from $\$8$ to $\$6$

- So farmer could offer to fully compensate rancher for $\$2$ loss and pocket remaining $\$4$
- or they could bargain to divide those gains to trade of $\$4$ in other ways.

### Who causes the externality?

- The rancher, because his cows trample the crops?
- The farmer, for placing his field too close to the rancher?
- Ronald Coase point is that there is no clear answer to this question.
   - Hence Pigouvian tax/subsidy 'solutions' are not obvious. Should we tax the rancher, or subsidize them to keep their herd size down?
   - 'Externality' problem is due to the non-assignment of property rights.

## The 'Coase Theorem'

### With zero/low transactions costs

- **The initial assignment of property rights does not matter for efficiency:**  The parties traded to an efficient solution no matter who first got the rights.

- **The 'emergence' of property rights**: Even with no initial third-party assignment of property rights, it should be in the interests of the parties to create such rights and negotiate/trade to an efficient outcome.

- **The initial allocation does matter for the distribution of benefits between parties.** Legally tradable entitlements are valuable, generate income to those who can then sell.

### Coase Theorem: True, False or Tautology?

> "Costless bargaining is efficient tautologically; if I assume people can agree on socially efficient bargains, then of course they will...  In the absence of property rights, a bargain *establishes* a contract between parties with novel rights that needn’t exist ex-ante."
Cooter (1990)

In the Farmer and Rancher example there was a missing market for legal entitlements.

Once the market is made complete (by an assumed third party) then the First Welfare Theorem applies: complete competitive markets will lead to efficient allocations, regardless of initial allocation of property rights.

The "Coase Theorem" makes legal entitlements tradable.

In this view insuring efficiency is  matter or removing impediments to free exchange of legal entitlements. However,

>"The interesting case is when transaction costs make bargaining difficult. What you should take from Coase is that social efficiency can be enhanced by institutions (including the firm!) which allow socially efficient bargains to be reached by removing restrictive transaction costs, and particularly that the assignment of property rights to different parties can either help or hinder those institutions."

Good further discussions from [D. Mcloskey](http://www.deirdremccloskey.com/docs/pdf/Article_306.pdf) and [here](https://afinetheorem.wordpress.com/2013/09/03/on-coases-two-famous-theorems/):

## With positive transactions costs initial rights allocations matter for efficiency

- 'Coase Theorem' (Stigler) interpretation sweeps under the rug the complicated political question of who gets initial rights.
    - Parties may engage in costly conflict, expend real resources to try to establish control over initial allocation of rights.
    - The [Myerson Satterthaite theorem](https://en.wikipedia.org/wiki/Myerson%E2%80%93Satterthwaite_theorem) establishes that when parties are asymmetrically informed about each other's valuations (here of damages or benefits) then efficient exchange may become difficult/impossible. Each party tries to extract rents, hold-up the other.
    - Suppose we had many farmers and ranchers.  It might be costly/difficult to bring all relevant ranchers and farmers together and to agree on bargain terms.
- Coase himself thought transactions costs mattered and hence initial allocation mechanisms had to be thought through carefully (e.g. spectrum auctions).

## A Coasian view of the development of a land market

Suppose there is an open field.  In the absence of a land market whoever gets to the land first (possibly the more powerful in the the village) will prepare/clear land until the marginal value product of the last unit of land is equal to the clearing cost.  We contrast two situations:

(1) Open frontier: where land is still abundant

(2) Land Scarcity.

There will be a misallocation in (2) shown by DWL in the diagram... but also an incentive for the parties to bargain to a more efficient outcome.  A well functionining land market would also deliver that outcome.

#### Abundant land environment

$\bar T$ units of land and $N$=2 households.

Land clearing cost $c$.  Frontier land not yet exhausted.

Maximize profits at $P \cdot F_T(T) = c$

Land demand for each farmer is given by $P\cdot F_T(T_i) = r$. So for this production  $P \frac{1}{\sqrt T_i} = r$ or $P \frac{1}{\sqrt T_i} = cl$  so we can write

$$T^*_i(r) = (P/r)^2$$

If there is an open frontier the sum or demands falls short of total land supply and the marginal cost of land is the cost of clearing $r=c_l$.

'Land scarcity' results on the other hand when there is an equilibrium price of land $r>c_l$ where $r$ is found from

$$\sum T^*_i(r) = \bar T$$

Now land rent $r-c$ can be charged on the right to access and use land.  Trade in these legal entitlements can raise output and efficiency. But there may be conflict and a 'scramble' to establish those rights of first access.

#### 'Customary' land rights

- Suppose norm is that all in the village can use as much land as they can farm
- Higher status individuals get allocation first
- As long as land is abundant everyone gets the land they want
- No "land rent" -- cannot charge rent above $c$ since villagers are free to clear at cost $c$

```python
landmarket(P=5, cl = 3, title = 'Open Frontier')
```

### The closing of the frontier
- Rising population or improving price or technology increases demand for land.
- Suppose price at which product can be sold increases
  - demand for land increases.
- Suppose total demandat clearing cost $c$ exceedsavailable land supply.
   - High-status individuals (who have first-access) leave less land available than is needed to satisfy remaining villagers demand.
- Inefficient allocation of land
   - marginal products of land not equalized across households.
   - output would increase if we establish a market for trading land

```python
landmarket(P=8, cl = 3, title = 'Land Scarcity')
```

We can solve for the equilibrium rental rate $r$ given environmental paramters including the price $P$, land endowment  $\bar T$, population size $N$ and technology parameters $A)

To do:
(things to still do in this notebook)
 - indicate DWL on landmarket diagrams
 - create widget to see how diagram shifts with changing parameters


İmport some useful packages and increase the default figure size.

```python
import eecs598
import torch
import torchvision
import matplotlib.pyplot as plt
import statistics
import numpy as np
plt.rcParams['figure.figsize'] = (10.0, 8.0)
plt.rcParams['font.size'] = 16
```

```python

```

```python
import tensorflow as tf
import tensorflow_quantum as tfq

import cirq
import sympy
import numpy as np
import seaborn as sns
import collections

# visualization tools
%matplotlib inline
import matplotlib.pyplot as plt
from cirq.contrib.svg import SVGCircuit
```

    /home/basak/anaconda3/lib/python3.7/site-packages/requests/__init__.py:91: RequestsDependencyWarning: urllib3 (1.26.3) or chardet (4.0.0) doesn't match a supported version!
      RequestsDependencyWarning)

```python
x_train, y_train, x_test, y_test = eecs598.data.cifar10()

print('Training set:', )
print('  data shape:', x_train.shape)
print('  labels shape: ', y_train.shape)
print('Test set:')
print('  data shape: ', x_test.shape)
print('  labels shape', y_test.shape)
```

    Training set:
      data shape: torch.Size([50000, 3, 32, 32])
      labels shape:  torch.Size([50000])
    Test set:
      data shape:  torch.Size([10000, 3, 32, 32])
      labels shape torch.Size([10000])

Visualize the dataset

```python
import random
from torchvision.utils import make_grid

classes = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
samples_per_class = 12
samples = []
for y, cls in enumerate(classes):
    plt.text(-4, 34 * y + 18, cls, ha='right')
    idxs, = (y_train == y).nonzero(as_tuple=True)
    for i in range(samples_per_class):
        idx = idxs[random.randrange(idxs.shape[0])].item()
        samples.append(x_train[idx])
img = torchvision.utils.make_grid(samples, nrow=samples_per_class)
plt.imshow(eecs598.tensor_to_image(img))
plt.axis('off')
plt.show()
```

The label data is just a list of 10,000 numbers ranging from 0 to 9, which corresponds to each of the 10 classes in CIFAR-10.

airplane : 0
automobile : 1
bird : 2
cat : 3
deer : 4
dog : 5
frog : 6
horse : 7
ship : 8
truck : 9

```python

```

```python
from keras.datasets import cifar10
(train_images, train_labels), (test_images, test_labels) = cifar10.load_data()
```

```python
y_train=train_labels.flatten()
y_test=test_labels.flatten()
# We have reduced the dimension
```

```python
x_train=tf.image.rgb_to_grayscale(train_images)
x_test=tf.image.rgb_to_grayscale(test_images)
#to convert grayscale
```

Filter the dataset to keep just the cat  and frog, remove the other classes

```python
def filter_36(x, y):
    keep = (y == 3) | (y == 6)
    x, y = x[keep], y[keep]
    y = y == 3
    return x,y
```

```python
x_train, y_train = filter_36(x_train, y_train)
x_test, y_test = filter_36(x_test, y_test)

print("Number of filtered training examples:", len(x_train))
print("Number of filtered test examples:", len(x_test))
```

    Number of filtered training examples: 10000
    Number of filtered test examples: 2000

```python
plt.imshow(x_train[0, :, :, 0])
plt.colorbar()
```

 Resize the image 36x36 to down to 4x4

```python
x_train_s = tf.image.resize(x_train, (4,4)).numpy()
x_test_s = tf.image.resize(x_test, (4,4)).numpy()
```

```python

```

## Encode the data as quantum circuits

The first step is to convert to a binary encoding.

```python

THRESHOLD = 0.5

x_train_bin = np.array(x_train_s > THRESHOLD, dtype=np.float32)
x_test_bin = np.array(x_test_s > THRESHOLD, dtype=np.float32)
```

The qubits at pixel indices with values that exceed a threshold, are rotated through an gate.

```python
def convert_to_circuit(image):
    """Encode truncated classical image into quantum datapoint."""
    values = np.ndarray.flatten(image)
    qubits = cirq.GridQubit.rect(6, 6)
    circuit = cirq.Circuit()
    for i, value in enumerate(values):
        if value:
            circuit.append(cirq.X(qubits[i]))

    return circuit

x_train_circ = [convert_to_circuit(x) for x in x_train_bin]
x_test_circ = [convert_to_circuit(x) for x in x_test_bin]
```

```python
SVGCircuit(x_train_circ[0])

```

    findfont: Font family ['Arial'] not found. Falling back to DejaVu Sans.

Convert  Cirq circuits to tensors for tfq

```python
x_train_tfcirc = tfq.convert_to_tensor(x_train_circ)
x_test_tfcirc = tfq.convert_to_tensor(x_test_circ)
```

```python

```

```python

```

## Quantum neural network

add a layer of these gates to a circuit

```python
class CircuitLayerBuilder():
    def __init__(self, data_qubits, readout):
        self.data_qubits = data_qubits
        self.readout = readout

    def add_layer(self, circuit, gate, prefix):
        for i, qubit in enumerate(self.data_qubits):
            symbol = sympy.Symbol(prefix + '-' + str(i))
            circuit.append(gate(qubit, self.readout)**symbol)

```

```python
demo_builder = CircuitLayerBuilder(data_qubits = cirq.GridQubit.rect(4,1),
                                   readout=cirq.GridQubit(-1,-1))

circuit = cirq.Circuit()
demo_builder.add_layer(circuit, gate = cirq.XX, prefix='xx')
SVGCircuit(circuit)

```

```python
def create_quantum_model():
    """Create a QNN model circuit and readout operation to go along with it."""
    data_qubits = cirq.GridQubit.rect(4, 4)  # a 4x4 grid.
    readout = cirq.GridQubit(-1, -1)         # a single qubit at [-1,-1]
    circuit = cirq.Circuit()

    # Prepare the readout qubit.
    circuit.append(cirq.X(readout))
    circuit.append(cirq.H(readout))

    builder = CircuitLayerBuilder(
        data_qubits = data_qubits,
        readout=readout)

    # Then add layers (experiment by adding more).
    builder.add_layer(circuit, cirq.XX, "xx1")
    builder.add_layer(circuit, cirq.ZZ, "zz1")

    # Finally, prepare the readout qubit.
    circuit.append(cirq.H(readout))

    return circuit, cirq.Z(readout)

```

```python
model_circuit, model_readout = create_quantum_model()

```

 Build the Keras model.

```python
model = tf.keras.Sequential([
    # The input is the data-circuit, encoded as a tf.string
    tf.keras.layers.Input(shape=(), dtype=tf.string),
    # The PQC layer returns the expected value of the readout gate, range [-1,1].
    tfq.layers.PQC(model_circuit, model_readout),
])
```

```python
y_train_hinge = 2.0*y_train-1.0
y_test_hinge = 2.0*y_test-1.0

```

```python
def hinge_accuracy(y_true, y_pred):
    y_true = tf.squeeze(y_true) > 0.0
    y_pred = tf.squeeze(y_pred) > 0.0
    result = tf.cast(y_true == y_pred, tf.float32)

    return tf.reduce_mean(result)
```

```python
model.compile(
    loss=tf.keras.losses.Hinge(),
    optimizer=tf.keras.optimizers.Adam(),
    metrics=[hinge_accuracy])

```

```python
print(model.summary())
```

    Model: "sequential"
    _________________________________________________________________
    Layer (type)                 Output Shape              Param #
    =================================================================
    pqc (PQC)                    (None, 1)                 32
    =================================================================
    Total params: 32
    Trainable params: 32
    Non-trainable params: 0
    _________________________________________________________________
    None

```python
EPOCHS = 3
BATCH_SIZE = 128

NUM_EXAMPLES = len(x_train_tfcirc)

```

```python
x_train_tfcirc_sub = x_train_tfcirc[:NUM_EXAMPLES]
y_train_hinge_sub = y_train_hinge[:NUM_EXAMPLES]

```

```python
import time
start_time = time.time()
```

```python
qnn_history = model.fit(
      x_train_tfcirc_sub, y_train_hinge_sub,
      batch_size=32,
      epochs=EPOCHS,
      verbose=1,
      validation_data=(x_test_tfcirc, y_test_hinge))

qnn_results = model.evaluate(x_test_tfcirc, y_test)

```

    Epoch 1/3
     25/313 [=>............................] - ETA: 35:50 - loss: 0.9904 - hinge_accuracy: 0.5250

```python
qnn_accuracy = qnn_results[1]
qnn_accuracy

```

    0.5014880895614624

```python
model.predict_classes(x_train_tfcirc[0:7])
```

    WARNING:tensorflow:From <ipython-input-56-42c649e1ba03>:1: Sequential.predict_classes (from tensorflow.python.keras.engine.sequential) is deprecated and will be removed after 2021-01-01.
    Instructions for updating:
    Please use instead:* `np.argmax(model.predict(x), axis=-1)`,   if your model does multi-class classification   (e.g. if it uses a `softmax` last-layer activation).* `(model.predict(x) > 0.5).astype("int32")`,   if your model does binary classification   (e.g. if it uses a `sigmoid` last-layer activation).

    array([[0],
           [0],
           [0],
           [0],
           [0],
           [0],
           [0]], dtype=int32)

```python

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.gridspec import GridSpec

def format_axes(fig):
    for i, ax in enumerate(fig.axes):
        ax.tick_params(labelbottom=False, labelleft=False)

fig = plt.figure(figsize=(10, 10))

gs = GridSpec(3, 3, figure=fig)
ax1 = fig.add_subplot(gs[0, 0])
# identical to ax1 = plt.subplot(gs.new_subplotspec((0, 0), colspan=3))

for i in range(3):
  for j in range(3):
    ax = fig.add_subplot(gs[i, j])

    ax.imshow(x_train[i+j, :, :, 0])

fig.suptitle("GridSpec")
format_axes(fig)

plt.show()

```

```python
plt.plot(qnn_history.history['hinge_accuracy'], label='QNN')
#plt.plot(hybrid_history.history['val_custom_accuracy'], label='Hybrid CNN')
#plt.title('Quantum vs Hybrid CNN performance')
plt.xlabel('Epochs')
plt.legend()
plt.ylabel('Validation Accuracy')
plt.show()
```

```python

```


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

# Toggle cell visibility

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

# Hide the code completely

# from IPython.display import HTML
# tag = HTML('''<style>
# div.input {
#     display:none;
# }
# </style>''')
# display(tag)
```

Promijeni vidljivost ovdje.

## Dizajn regulatora za sustav masa-opruga-prigušivač

Ovaj primjer prikazuje dizajn regulatora za sustav masa-opruga-prigušivač opisan sljedećim jednadžbama prostora stanja ($m=2,\,k=4\,\text{i}\,c=1$):

\begin{cases}
\dot{\textbf{x}}=\underbrace{\begin{bmatrix}0&1\\-\frac{4}{2}&-\frac{1}{2}\end{bmatrix}}_{A}\textbf{x}+\underbrace{\begin{bmatrix}0\\\frac{1}{2}\end{bmatrix}}_{B}\textbf{u} \\ \\
\textbf{y}=\underbrace{\begin{bmatrix}1&0\end{bmatrix}}_{C}\textbf{x}\,.
\end{cases}

Cilj upravljanja je dobiti maksimalno prekoračenje od 10% i zajamčiti nultu pogrešku u odzivu za referentni step-signal.

Odabrani polovi su $\lambda_{c\,1,2}=-1\pm1.5i$ za upravljivi sustav i $\lambda_{o\,1,2}=-15$ rad/s za promatrača, oba dobivena s obzirom na matrice pojačanja  $K=\begin{bmatrix}\frac{5}{2}&3\end{bmatrix}$ i $L=\begin{bmatrix}\frac{59}{2}&\frac{833}{4}\end{bmatrix}^T$. Rezultirajuća struktura kontrolera je:

i, da bi se postigla nulta pogreška odziva, referentni signal se dijeli statičkim pojačanjem prijenosne funkcije zatvorene petlje od referentne vrijednosti do $y$.

Sustav je simuliran u nastavku.

### Kako koristiti ovaj interaktivni primjer?

- Izmijenite početne vrijednosti stanja promatrača i svojstvene vrijednosti promatrača te promatrajte odziv sustava.
- Pokušajte postići savršeno praćenje sinusoidalnog referentnog signala s periodom od 3 s.

```python
%matplotlib inline
import control as control
import numpy
import sympy as sym
from IPython.display import display, Markdown
import ipywidgets as widgets
import matplotlib.pyplot as plt

#print a matrix latex-like
def bmatrix(a):
     """Returns a LaTeX bmatrix - by Damir Arbula (ICCT project)

     :a: numpy array
     :returns: LaTeX bmatrix as a string
     """
     if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
     lines = str(a).replace('[', '').replace(']', '').splitlines()
     rv = [r'\begin{bmatrix}']
     rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
     rv +=  [r'\end{bmatrix}']
     return '\n'.join(rv)

# Display formatted matrix:
def vmatrix(a):
    if len(a.shape) > 2:
         raise ValueError('bmatrix can at most display two dimensions')
    lines = str(a).replace('[', '').replace(']', '').splitlines()
    rv = [r'\begin{vmatrix}']
    rv += ['  ' + ' & '.join(l.split()) + r'\\' for l in lines]
    rv +=  [r'\end{vmatrix}']
    return '\n'.join(rv)

#matrixWidget is a matrix looking widget built with a VBox of HBox(es) that returns a numPy array as value !
class matrixWidget(widgets.VBox):
    def updateM(self,change):
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.M_[irow,icol] = self.children[irow].children[icol].value
                #print(self.M_[irow,icol])
        self.value = self.M_

    def dummychangecallback(self,change):
        pass

    def __init__(self,n,m):
        self.n = n
        self.m = m
        self.M_ = numpy.matrix(numpy.zeros((self.n,self.m)))
        self.value = self.M_
        widgets.VBox.__init__(self,
                             children = [
                                 widgets.HBox(children =
                                              [widgets.FloatText(value=0.0, layout=widgets.Layout(width='90px')) for i in range(m)]
                                             )
                                 for j in range(n)
                             ])

        #fill in widgets and tell interact to call updateM each time a children changes value
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
                self.children[irow].children[icol].observe(self.updateM, names='value')
        #value = Unicode('example@example.com', help="The email value.").tag(sync=True)
        self.observe(self.updateM, names='value', type= 'All')

    def setM(self, newM):
        #disable callbacks, change values, and reenable
        self.unobserve(self.updateM, names='value', type= 'All')
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].unobserve(self.updateM, names='value')
        self.M_ = newM
        self.value = self.M_
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].value = self.M_[irow,icol]
        for irow in range(0,self.n):
            for icol in range(0,self.m):
                self.children[irow].children[icol].observe(self.updateM, names='value')
        self.observe(self.updateM, names='value', type= 'All')

                #self.children[irow].children[icol].observe(self.updateM, names='value')

#overlaod class for state space systems that DO NOT remove "useless" states (what "professor" of automatic control would do this?)
class sss(control.StateSpace):
    def __init__(self,*args):
        #call base class init constructor
        control.StateSpace.__init__(self,*args)
    #disable function below in base class
    def _remove_useless_states(self):
        pass
```

```python
# Preparatory cell

A = numpy.matrix('0 1;-2 -0.5')
B = numpy.matrix('0; 0.5')
C = numpy.matrix('1 0')
X0 = numpy.matrix('0.5; 0.5')
K = numpy.matrix([5/2,3.])
L = numpy.matrix([[59/2],[833/4]])

Aw = matrixWidget(2,2)
Aw.setM(A)
Bw = matrixWidget(2,1)
Bw.setM(B)
Cw = matrixWidget(1,2)
Cw.setM(C)
X0w = matrixWidget(2,1)
X0w.setM(X0)
Kw = matrixWidget(1,2)
Kw.setM(K)
Lw = matrixWidget(2,1)
Lw.setM(L)

eig1c = matrixWidget(1,1)
eig2c = matrixWidget(2,1)
eig1c.setM(numpy.matrix([-1.]))
eig2c.setM(numpy.matrix([[-1.],[-1.5]]))

eig1o = matrixWidget(1,1)
eig2o = matrixWidget(2,1)
eig1o.setM(numpy.matrix([-15.]))
eig2o.setM(numpy.matrix([[-15.],[0.]]))
```

```python
# Misc

#create dummy widget
DW = widgets.FloatText(layout=widgets.Layout(width='0px', height='0px'))

#create button widget
START = widgets.Button(
    description='Test',
    disabled=False,
    button_style='', # 'success', 'info', 'warning', 'danger' or ''
    tooltip='Test',
    icon='check'
)

def on_start_button_clicked(b):
    #This is a workaround to have intreactive_output call the callback:
    #   force the value of the dummy widget to change
    if DW.value> 0 :
        DW.value = -1
    else:
        DW.value = 1
    pass
START.on_click(on_start_button_clicked)

# Define type of method
selm = widgets.Dropdown(
    options= ['Postavi K i L', 'Postavi svojstvene vrijednosti'],
    value= 'Postavi svojstvene vrijednosti',
    description='',
    disabled=False
)

# Define the number of complex eigenvalues
sele = widgets.Dropdown(
    options= ['0 kompleksnih svojstvenih vrijednosti', '2 kompleksne svojstvene vrijednosti'],
    value= '2 kompleksne svojstvene vrijednosti',
    description='Kompleksne svojstvene vrijednosti:',
    style = {'description_width': 'initial'},
    disabled=False
)

#define type of ipout
selu = widgets.Dropdown(
    options=['impuls', 'step', 'sinus', 'Pravokutni val'],
    value='step',
    description='Tip referentnog signala:',
    style = {'description_width': 'initial'},
    disabled=False
)
# Define the values of the input
u = widgets.FloatSlider(
    value=1,
    min=0,
    max=20.0,
    step=0.1,
    description='Referentni signal:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)
period = widgets.FloatSlider(
    value=0.5,
    min=0.01,
    max=4,
    step=0.01,
    description='Period: ',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.2f',
)

gain_w = widgets.FloatText(
    value=1.,
    description='',
    disabled=True
)

gain_id_w = widgets.FloatText(
    value=1.,
    description='',
    disabled=True
)

m = widgets.FloatSlider(
    value=2,
    min=0.1,
    max=10.0,
    step=0.1,
    description='m [kg]:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)
k = widgets.FloatSlider(
    value=4,
    min=0,
    max=10.0,
    step=0.1,
    description='k [N/m]:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)
c = widgets.FloatSlider(
    value=1,
    min=0,
    max=10.0,
    step=0.1,
    description='c [Ns/m]:',
    disabled=False,
    continuous_update=False,
    orientation='horizontal',
    readout=True,
    readout_format='.1f',
)

time = widgets.FloatText(
    value=5.,
    description='Time',
    disabled=False
)
```

```python
# Support functions

def eigen_choice(sele):
    if sele == '0 kompleksnih svojstvenih vrijednosti':
        eig1c.children[0].children[0].disabled = False
        eig2c.children[1].children[0].disabled = True
        eig1o.children[0].children[0].disabled = False
        eig2o.children[1].children[0].disabled = True
        eig = 0
    if sele == '2 kompleksne svojstvene vrijednosti':
        eig1c.children[0].children[0].disabled = True
        eig2c.children[1].children[0].disabled = False
        eig1o.children[0].children[0].disabled = True
        eig2o.children[1].children[0].disabled = False
        eig = 2
    return eig

def method_choice(selm):
    if selm == 'Postavi K i L':
        method = 1
        sele.disabled = True
    if selm == 'Postavi svojstvene vrijednosti':
        method = 2
        sele.disabled = False
    return method
```

```python
sols = numpy.linalg.eig(A)

def main_callback(m, k, c, X0w, K, L, eig1c, eig2c, eig1o, eig2o, u, period, selm, sele, selu, time, DW):
    A = numpy.matrix([[0,1],[-k/m,-c/m]])
    B = numpy.matrix([[0],[1/m]])
    eige = eigen_choice(sele)
    method = method_choice(selm)

    if method == 1:
        solc = numpy.linalg.eig(A-B*K)
        solo = numpy.linalg.eig(A-L*C)
    if method == 2:
        if eige == 0:
            K = control.acker(A, B, [eig1c[0,0], eig2c[0,0]])
            Kw.setM(K)
            L = control.acker(A.T, C.T, [eig1o[0,0], eig2o[0,0]]).T
            Lw.setM(L)
        if eige == 2:
            K = control.acker(A, B, [numpy.complex(eig2c[0,0],eig2c[1,0]),
                                     numpy.complex(eig2c[0,0],-eig2c[1,0])])
            Kw.setM(K)
            L = control.acker(A.T, C.T, [numpy.complex(eig2o[0,0],eig2o[1,0]),
                                         numpy.complex(eig2o[0,0],-eig2o[1,0])]).T
            Lw.setM(L)

    Gs = sss(A,B,numpy.vstack((C,[0,0])),[[0],[1]])
    Os = sss(A-L*C,numpy.hstack((L,B)),-K,[0,0])
    Gas = control.append(Gs,Os)
    sys = control.connect(Gas,[[2,1],[3,2],[1,3]],[1],[1])

    Gs_id = sss(A,B,sym.eye(2),sym.zeros(2,1))
    Fs_id = control.series(K,Gs_id)
    A1 = numpy.matrix(Fs_id.A-Fs_id.B*Fs_id.C)
    B1 = numpy.matrix(Fs_id.B*sym.Matrix([[1],[0]]))
    C1 = numpy.matrix(sym.Matrix([1,0]).T*Fs_id.C)
    sys_id = sss(A1,B1,C1,0)

    dcgain = control.dcgain(sys)
    t = numpy.linspace(0, 1000, 2)
    t, y = control.step_response(sys_id,t)
    dcgain_id = y[-1]
    gain_w.value = dcgain
    gain_id_w.value = dcgain_id
    if dcgain != 0 and dcgain_id != 0:
        u1 = u/gain_w.value
        u2 = u/gain_id_w.value
    else:
        print('Inverzno pojačanje postavljeno na 0 i promijenjeno na 1')
        u1 = u/1
        u2 = u/1

    solc = numpy.linalg.eig(sys.A)
    solo = numpy.linalg.eig(A-L*C-B*K)
    print('Svojstvene vrijednosti sustava su:', round(sols[0][0],2),'i', round(sols[0][1],2))
    print('Svojstvene vrijednosti upravljivog sustava u zatvorenoj petlji su:',
          round(solc[0][0],2),',',
          round(solc[0][1],2),',',
          round(solc[0][2],2),'i',
          round(solc[0][3],2))
    print('')
    print('Statičko pojačanje sustava zatvorene petlje(od referentnog ulaza do izlaza) je: %.5f' %dcgain)
    print('Statičko pojačanje idealnog sustava zatvorene petlje(od referentnog ulaza do izlaza) je: %.5f' %dcgain_id)

    X0w1 = numpy.matrix([[0],[0],[X0w[0,0]],[X0w[1,0]]])
    if time != 0:
        T = numpy.linspace(0, time, 10000)
    else:
        T = numpy.linspace(0, 1, 10000)

    if selu == 'impuls': #selu
        U = [0 for t in range(0,len(T))]
        U[0] = u
        U1 = [0 for t in range(0,len(T))]
        U1[0] = u1
        U2 = [0 for t in range(0,len(T))]
        U2[0] = u2
        T, yout, xout = control.forced_response(sys,T,U1,X0w1)
        T, yout_id, xout_id = control.forced_response(sys_id,T,U2,[0, 0])
    if selu == 'step':
        U = [u for t in range(0,len(T))]
        U1 = [u1 for t in range(0,len(T))]
        U2 = [u2 for t in range(0,len(T))]
        T, yout, xout = control.forced_response(sys,T,U1,X0w1)
        T, yout_id, xout_id = control.forced_response(sys_id,T,U2,[0, 0])
    if selu == 'sinus':
        U = u*numpy.sin(2*numpy.pi/period*T)
        U1 = u1*numpy.sin(2*numpy.pi/period*T)
        U2 = u2*numpy.sin(2*numpy.pi/period*T)
        T, yout, xout = control.forced_response(sys,T,U1,X0w1)
        T, yout_id, xout_id = control.forced_response(sys_id,T,U2,[0, 0])
    if selu == 'Pravokutni val':
        U = u*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        U1 = u1*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        U2 = u2*numpy.sign(numpy.sin(2*numpy.pi/period*T))
        T, yout, xout = control.forced_response(sys,T,U1,X0w1)
        T, yout_id, xout_id = control.forced_response(sys_id,T,U2,[0, 0])

    fig = plt.figure(num='Simulacija', figsize=(14,12))
    mag, phase, omega = control.bode_plot(sys,Plot = False)
    mag = control.mag2db(mag)
    phase = phase*180/numpy.pi
    fig.add_subplot(221)
    plt.title('Bodeov graf: amplituda')
    plt.semilogx(omega,mag)
    plt.xlabel('$\omega$ [rad/s]')
    plt.ylabel('A [dB]')
    plt.grid(True,which="both")

    fig.add_subplot(223)
    plt.title('Bodeov graf: faza')
    plt.semilogx(omega,phase)
    plt.xlabel('$\omega$ [rad/s]')
    plt.ylabel('Faza [deg]')
    plt.grid(True,which="both")

    fig.add_subplot(222)
    plt.title('Izlazni odziv')
    plt.ylabel('Izlaz ($x_1$)')
    plt.plot(T,xout[2],T,xout[0],T,xout_id[0],'g',T,U,'r--')
    plt.xlabel('$t$ [s]')
    plt.legend(['$x_{1estimirani}$','$y = x_{1realni}$','$x_{1idealni}$','referentni signal'])
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

    fig.add_subplot(224)
    plt.title('Odziv drugog stanja')
    plt.ylabel('$x_2$')
    plt.plot(T,xout[3],T,xout[1],T,xout_id[1],'g')
    plt.xlabel('$t$ [s]')
    plt.legend(['$x_{2estimirani}$','$x_{2realni}$','$x_{2idealni}$'])
    plt.axvline(x=0,color='black',linewidth=0.8)
    plt.axhline(y=0,color='black',linewidth=0.8)
    plt.grid()

alltogether = widgets.VBox([widgets.HBox([selm,
                                          sele,
                                          selu]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('K:',border=3), Kw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Svojstvene vrijednosti:',border=3),
                                          eig1c,
                                          eig2c,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('X0 est.:',border=3), X0w]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([widgets.Label('L:',border=3), Lw,
                                          widgets.Label(' ',border=3),
                                          widgets.Label(' ',border=3),
                                          widgets.Label('Svojstvene vrijednosti:',border=3),
                                          eig1o,
                                          eig2o,
                                          widgets.Label(' ',border=3),
                                          widgets.VBox([widgets.Label('Inverzno referentno pojačanje:',border=3),
                                                        widgets.Label('Inverzno idealno referentno pojačanje:',border=3)]),
                                          widgets.VBox([gain_w,gain_id_w])]),
                            widgets.Label(' ',border=3),
                            widgets.HBox([m,
                                          k,
                                          c]),
                            widgets.HBox([u,
                                          period,
                                          time]),
                            START])
out = widgets.interactive_output(main_callback, {'m':m, 'k':k, 'c':c, 'X0w':X0w, 'K':Kw, 'L':Lw,
                                                 'eig1c':eig1c, 'eig2c':eig2c, 'eig1o':eig1o, 'eig2o':eig2o,
                                                 'u':u, 'period':period, 'selm':selm, 'sele':sele, 'selu':selu,
                                                 'time':time, 'DW':DW})
out.layout.height = '900px'
display(out, alltogether)
```

    Output(layout=Layout(height='900px'))

    VBox(children=(HBox(children=(Dropdown(index=1, options=('Postavi K i L', 'Postavi svojstvene vrijednosti'), v…

```python

```