# Programming Assignment

## RealNVP for the LSUN bedroom dataset

### Instructions

In this notebook, you will develop the RealNVP normalising flow architecture from scratch, including the affine coupling layers, checkerboard and channel-wise masking, and combining into a multiscale architecture. You will train the normalising flow on a subset of the LSUN bedroom dataset.

Some code cells are provided for you in the notebook. You should avoid editing provided code, and make sure to execute the cells in order to avoid unexpected errors. Some cells begin with the line:

`#### GRADED CELL ####`

Don't move or edit this first line - this is what the automatic grader looks for to recognise graded cells. These cells require you to write your own code to complete them, and are automatically graded when you submit the notebook. Don't edit the function name or signature provided in these cells, otherwise the automatic grader might not function properly.

### How to submit

Complete all the tasks you are asked for in the worksheet. When you have finished and are happy with your code, press the **Submit Assignment** button at the top of this notebook.

### Let's get started!

We'll start running some imports, and loading the dataset. Do not edit the existing imports in the following cell. If you would like to make further Tensorflow imports, you should add them here.

```python
#### PACKAGE IMPORTS ####

# Run this cell first to import all required packages. Do not make any imports elsewhere in the notebook

import tensorflow as tf
import tensorflow_probability as tfp
import numpy as np
import matplotlib.pyplot as plt

from tensorflow.keras import Model, Input
from tensorflow.keras.layers import Conv2D, BatchNormalization
from tensorflow.keras.optimizers import Adam

tfd = tfp.distributions
tfb = tfp.bijectors

# If you would like to make further imports from tensorflow, add them here
from tensorflow.keras import layers
from tensorflow.keras.regularizers import l2
```

#### The LSUN Bedroom Dataset

In this assignment, you will use a subset of the [LSUN dataset](https://www.yf.io/p/lsun). This is a large-scale image dataset with 10 scene and 20 object categories. A subset of the LSUN bedroom dataset has been provided, and has already been downsampled and preprocessed into smaller, fixed-size images.

* F. Yu, A. Seff, Y. Zhang, S. Song, T. Funkhouser and J. Xia. "LSUN: Construction of a Large-scale Image Dataset using Deep Learning with Humans in the Loop". [arXiv:1506.03365](https://arxiv.org/abs/1506.03365), 10 Jun 2015

Your goal is to develop the RealNVP normalising flow architecture using bijector subclassing, and use it to train a generative model of the LSUN bedroom data subset. For full details on the RealNVP model, refer to the original paper:

* L. Dinh, J. Sohl-Dickstein and S. Bengio. "Density estimation using Real NVP". [arXiv:1605.08803](https://arxiv.org/abs/1605.08803), 27 Feb 2017.

#### Import the data

The dataset required for this project can be downloaded from the following link:

https://drive.google.com/file/d/1scbDZrn5pkRjF_CeZp66uHVQC9o1gIsg/view?usp=sharing

You should upload this file to Drive for use in this Colab notebook. It is recommended to unzip it on Drive, which can be done using the `zipfile` package:

>```
import zipfile
with zipfile.ZipFile("/path/to/lsun_bedroom.zip","r") as zip_ref:
   zip_ref.extractall('lsun_bedroom_data')
```

```python
# Run this cell to connect to your Drive folder

from google_drive_downloader import GoogleDriveDownloader as gdd

gdd.download_file_from_google_drive(file_id='1scbDZrn5pkRjF_CeZp66uHVQC9o1gIsg',
                                    dest_path='/content/lsun_bedroom/lsun_bedroom.zip',
                                    unzip=True,
                                    showsize=True)
```

    Downloading 1scbDZrn5pkRjF_CeZp66uHVQC9o1gIsg into /content/lsun_bedroom/lsun_bedroom.zip...
    115.5 MiB Done.
    Unzipping...Done.

#### Load the dataset

The following functions will be useful for loading and preprocessing the dataset. The subset you will use for this assignment consists of 10,000 training images, 1000 validation images and 1000 test images.

The images have been downsampled to 32 x 32 x 3 in order to simplify the training process.

```python
# Functions for loading and preprocessing the images

def load_image(filepath):
    raw_img = tf.io.read_file(filepath)
    img_tensor_int = tf.image.decode_jpeg(raw_img, channels=3)
    img_tensor_flt = tf.image.convert_image_dtype(img_tensor_int, tf.float32)
    img_tensor_flt = tf.image.resize(img_tensor_flt, [32, 32])
    img_tensor_flt = tf.image.random_flip_left_right(img_tensor_flt)
    return img_tensor_flt, img_tensor_flt

def load_dataset(split):
    train_list_ds = tf.data.Dataset.list_files('/content/lsun_bedroom/{}/*.jpg'.format(split), shuffle=False)
    train_ds = train_list_ds.map(load_image)
    return train_ds
```

```python
# Load the training, validation and testing datasets splits

train_ds = load_dataset('train')
val_ds = load_dataset('val')
test_ds = load_dataset('test')
```

```python
# Shuffle the datasets

shuffle_buffer_size = 1000
train_ds = train_ds.shuffle(shuffle_buffer_size)
val_ds = val_ds.shuffle(shuffle_buffer_size)
test_ds = test_ds.shuffle(shuffle_buffer_size)
```

```python
# Display a few examples

n_img = 4
f, axs = plt.subplots(n_img, n_img, figsize=(14, 14))

for k, image in enumerate(train_ds.take(n_img**2)):
    i = k // n_img
    j = k % n_img
    axs[i, j].imshow(image[0])
    axs[i, j].axis('off')
f.subplots_adjust(wspace=0.01, hspace=0.03)
```

```python
# Batch the Dataset objects

batch_size = 64
train_ds = train_ds.batch(batch_size)
val_ds = val_ds.batch(batch_size)
test_ds = test_ds.batch(batch_size)
```

### Affine coupling layer

We will begin the development of the RealNVP architecture with the core bijector that is called the _affine coupling layer_. This bijector can be described as follows: suppose that $x$ is a $D$-dimensional input, and let $d<D$. Then the output $y$ of the affine coupling layer is given by the following equations:

$$
\begin{align}
y_{1:d} &= x_{1:d} \tag{1}\\
    y_{d+1:D} &= x_{d+1:D}\odot \exp(s(x_{1:d})) + t(x_{1:d}), \tag{2}
\end{align}
$$

where $s$ and $t$ are functions from $\mathbb{R}^d\rightarrow\mathbb{R}^{D-d}$, and define the log-scale and shift operations on the vector $x_{d+1:D}$ respectively.

The log of the Jacobian determinant for this layer is given by $\sum_{j}s(x_{1:d})_j$.

The inverse operation can be easily computed as

$$
\begin{align}
x_{1:d} &= y_{1:d}\tag{3}\\
x_{d+1:D} &= \left(y_{d+1:D} - t(y_{1:d})\right)\odot \exp(-s(y_{1:d})),\tag{4}
\end{align}
$$

In practice, we will implement equations $(1)$ and $(2)$ using a binary mask $b$:

$$
\begin{align}
\text{Forward pass:}\qquad y &= b\odot x + (1-b)\odot\left(x\odot\exp(s(b\odot x)) + t(b\odot x)\right),\tag{5}\\
\text{Inverse pass:}\qquad x &= b\odot y + (1-b)\odot\left(y - t(b\odot x) \odot\exp( -s(b\odot x))\right).\tag{6}
\end{align}
$$

Our inputs $x$ will be a batch of 3-dimensional Tensors with `height`, `width` and `channels` dimensions. As in the original architecture, we will use both spatial 'checkerboard' masks and channel-wise masks:

Figure 1. Spatial checkerboard mask (left) and channel-wise mask (right). From the original paper.

#### Custom model for log-scale and shift

You should now create a custom model for the shift and log-scale parameters that are used in the affine coupling layer bijector. We will use a convolutional residual network, with two residual blocks and a final convolutional layer. Using the functional API, build the model according to the following specifications:

* The function takes the `input_shape` and `filters` as arguments
* The model should use the `input_shape` in the function argument to set the shape in the Input layer (call this layer `h0`).
* The first hidden layer should be a Conv2D layer with number of filters set by the `filters` argument, and a ReLU activation
* The second hidden layer should be a BatchNormalization layer
* The third hidden layer should be a Conv2D layer with the same number of filters as the input `h0` to the model, and a ReLU activation
* The fourth hidden layer should be a BatchNormalization layer
* The fifth hidden layer should be the sum of the fourth hidden layer output and the inputs `h0`. Call this layer `h1`
* The sixth hidden layer should be a Conv2D layer with filters set by the `filters` argument, and a ReLU activation
* The seventh hidden layer should be a BatchNormalization layer
* The eighth hidden layer should be a Conv2D layer with the same number of filters as `h1` (and `h0`), and a ReLU activation
* The ninth hidden layer should be a BatchNormalization layer
* The tenth hidden layer should be the sum of the ninth hidden layer output and `h1`
* The eleventh hidden layer should be a Conv2D layer with the number of filters equal to twice the number of channels of the model input, and a linear activation. Call this layer `h2`
* The twelfth hidden layer should split `h2` into two equal-sized Tensors along the final channel axis. These two Tensors are the shift and log-scale Tensors, and should each have the same shape as the model input
* The final layer should then apply the `tanh` nonlinearity to the log_scale Tensor. The outputs to the model should then be the list of Tensors `[shift, log_scale]`

All Conv2D layers should use a 3x3 kernel size, `"SAME"` padding and an $l2$ kernel regularizer with regularisation coefficient of `5e-5`.

_Hint: use_ `tf.split` _with arguments_ `num_or_size_splits=2, axis=-1` _to create the output Tensors_.

In total, the network should have 14 layers (including the `Input` layer).

```python
#### GRADED CELL ####

# Complete the following function.
# Make sure to not change the function name or arguments.

def get_conv_resnet(input_shape, filters):
    """
    This function should build a CNN ResNet model according to the above specification,
    using the functional API. The function takes input_shape as an argument, which should be
    used to specify the shape in the Input layer, as well as a filters argument, which
    should be used to specify the number of filters in (some of) the convolutional layers.
    Your function should return the model.
    """
    h0 = layers.Input(shape=input_shape)
    h = layers.Conv2D(filters=filters, kernel_size=(3,3), padding="SAME", kernel_regularizer=l2(5e-5), activation="relu")(h0)
    h = layers.BatchNormalization()(h)

    h = layers.Conv2D(filters=input_shape[-1], kernel_size=(3,3), padding="SAME", kernel_regularizer=l2(5e-5), activation="relu")(h)
    h = layers.BatchNormalization()(h)

    h1 = layers.Add()([h0, h])
    h = layers.Conv2D(filters=filters, kernel_size=(3,3), padding="SAME", kernel_regularizer=l2(5e-5), activation="relu")(h1)
    h = layers.BatchNormalization()(h)

    h = layers.Conv2D(filters=input_shape[-1], kernel_size=(3,3), padding="SAME", kernel_regularizer=l2(5e-5), activation="relu")(h)
    h = layers.BatchNormalization()(h)

    h = layers.Add()([h1, h])
    h2 = layers.Conv2D(filters=2*input_shape[-1], kernel_size=(3,3), padding="SAME", kernel_regularizer=l2(5e-5), activation="linear")(h)

    shift, log_scale = layers.Lambda(lambda t: tf.split(t, num_or_size_splits=2, axis=-1))(h2)

    log_scale = layers.Activation(activation="tanh")(log_scale)

    model = Model(inputs=h0, outputs=[shift, log_scale])
    return model
```

```python
# Test your function and print the model summary

conv_resnet = get_conv_resnet((32, 32, 3), 32)
conv_resnet.summary()
```

    Model: "model"
    __________________________________________________________________________________________________
    Layer (type)                    Output Shape         Param #     Connected to
    ==================================================================================================
    input_1 (InputLayer)            [(None, 32, 32, 3)]  0
    __________________________________________________________________________________________________
    conv2d (Conv2D)                 (None, 32, 32, 32)   896         input_1[0][0]
    __________________________________________________________________________________________________
    batch_normalization (BatchNorma (None, 32, 32, 32)   128         conv2d[0][0]
    __________________________________________________________________________________________________
    conv2d_1 (Conv2D)               (None, 32, 32, 3)    867         batch_normalization[0][0]
    __________________________________________________________________________________________________
    batch_normalization_1 (BatchNor (None, 32, 32, 3)    12          conv2d_1[0][0]
    __________________________________________________________________________________________________
    add (Add)                       (None, 32, 32, 3)    0           input_1[0][0]
                                                                     batch_normalization_1[0][0]
    __________________________________________________________________________________________________
    conv2d_2 (Conv2D)               (None, 32, 32, 32)   896         add[0][0]
    __________________________________________________________________________________________________
    batch_normalization_2 (BatchNor (None, 32, 32, 32)   128         conv2d_2[0][0]
    __________________________________________________________________________________________________
    conv2d_3 (Conv2D)               (None, 32, 32, 3)    867         batch_normalization_2[0][0]
    __________________________________________________________________________________________________
    batch_normalization_3 (BatchNor (None, 32, 32, 3)    12          conv2d_3[0][0]
    __________________________________________________________________________________________________
    add_1 (Add)                     (None, 32, 32, 3)    0           add[0][0]
                                                                     batch_normalization_3[0][0]
    __________________________________________________________________________________________________
    conv2d_4 (Conv2D)               (None, 32, 32, 6)    168         add_1[0][0]
    __________________________________________________________________________________________________
    lambda (Lambda)                 [(None, 32, 32, 3),  0           conv2d_4[0][0]
    __________________________________________________________________________________________________
    activation (Activation)         (None, 32, 32, 3)    0           lambda[0][1]
    ==================================================================================================
    Total params: 3,974
    Trainable params: 3,834
    Non-trainable params: 140
    __________________________________________________________________________________________________

You can also inspect your model architecture graphically by running the following cell. It should look something like the following:

```python
# Plot the model graph

tf.keras.utils.plot_model(conv_resnet, show_layer_names=False, rankdir='LR')
```

```python
# Check the output shapes are as expected

print(conv_resnet(tf.random.normal((1, 32, 32, 3)))[0].shape)
print(conv_resnet(tf.random.normal((1, 32, 32, 3)))[1].shape)
```

    (1, 32, 32, 3)
    (1, 32, 32, 3)

#### Binary masks

Now that you have a shift and log-scale model built, we will now implement the affine coupling layer. We will first need functions to create the binary masks $b$ as described above. The following function creates the spatial 'checkerboard' mask.

It takes a rank-2 `shape` as input, which correspond to the `height` and `width` dimensions, as well as an `orientation` argument (an integer equal to `0` or `1`) that determines which way round the zeros and ones are entered into the Tensor.

```python
# Function to create the checkerboard mask

def checkerboard_binary_mask(shape, orientation=0):
    height, width = shape[0], shape[1]
    height_range = tf.range(height)
    width_range = tf.range(width)
    height_odd_inx = tf.cast(tf.math.mod(height_range, 2), dtype=tf.bool)
    width_odd_inx = tf.cast(tf.math.mod(width_range, 2), dtype=tf.bool)
    odd_rows = tf.tile(tf.expand_dims(height_odd_inx, -1), [1, width])
    odd_cols = tf.tile(tf.expand_dims(width_odd_inx, 0), [height, 1])
    checkerboard_mask = tf.math.logical_xor(odd_rows, odd_cols)
    if orientation == 1:
        checkerboard_mask = tf.math.logical_not(checkerboard_mask)
    return tf.cast(tf.expand_dims(checkerboard_mask, -1), tf.float32)
```

This function creates a rank-3 Tensor to mask the `height`, `width` and `channels` dimensions of the input. We can take a look at this checkerboard mask for some example inputs below. In order to make the Tensors easier to inspect, we will squeeze out the single channel dimension (which is always 1 for this mask).

```python
# Run the checkerboard_binary_mask function to see an example
# NB: we squeeze the shape for easier viewing. The full shape is (4, 4, 1)

tf.squeeze(checkerboard_binary_mask((4, 4), orientation=0))
```

    <tf.Tensor: shape=(4, 4), dtype=float32, numpy=
    array([[0., 1., 0., 1.],
           [1., 0., 1., 0.],
           [0., 1., 0., 1.],
           [1., 0., 1., 0.]], dtype=float32)>

```python
# The `orientation` should be 0 or 1, and determines which way round the binary entries are

tf.squeeze(checkerboard_binary_mask((4, 4), orientation=1))
```

    <tf.Tensor: shape=(4, 4), dtype=float32, numpy=
    array([[1., 0., 1., 0.],
           [0., 1., 0., 1.],
           [1., 0., 1., 0.],
           [0., 1., 0., 1.]], dtype=float32)>

You should now complete the following function to create a channel-wise mask. This function takes a single integer `num_channels` as an input, as well as an `orientation` argument, similar to above. You can assume that the `num_channels` integer is even.

The function should return a rank-3 Tensor with singleton entries for `height` and `width`. In the channel axis, the first `num_channels // 2` entries should be zero (for `orientation=0`) and the final `num_channels // 2` entries should be one (for `orientation=0`). The zeros and ones should be reversed for `orientation=1`. The `dtype` of the returned Tensor should be `tf.float32`.

```python
#### GRADED CELL ####

# Complete the following function.
# Make sure to not change the function name or arguments.

def channel_binary_mask(num_channels, orientation=0):
    """
    This function takes an integer num_channels and orientation (0 or 1) as
    arguments. It should create a channel-wise binary mask with
    dtype=tf.float32, according to the above specification.
    The function should then return the binary mask.
    """
    if orientation == 0:
        return tf.concat([tf.zeros((1,1, num_channels//2), dtype=tf.float32),
                          tf.ones((1,1, num_channels - num_channels//2), dtype=tf.float32)], axis=-1)

    return tf.concat([tf.ones((1,1, num_channels//2), dtype=tf.float32),
                      tf.zeros((1,1, num_channels - num_channels//2), dtype=tf.float32)], axis=-1)

```

```python
# Run your function to see an example channel-wise binary mask

channel_binary_mask(6, orientation=0)
```

    <tf.Tensor: shape=(1, 1, 6), dtype=float32, numpy=array([[[0., 0., 0., 1., 1., 1.]]], dtype=float32)>

```python
#### GRADED CELL ####

# Complete the following functions.
# Make sure to not change the function names or arguments.

def forward(x, b, shift_and_log_scale_fn):
    """
    This function takes the input Tensor x, binary mask b and callable
    shift_and_log_scale_fn as arguments.
    This function should implement the forward transformation in equation (5)
    and return the output Tensor y, which will have the same shape as x
    """
    t_shift, s_log_scale = shift_and_log_scale_fn(b*x)
    return b*x + (1-b)*(x*tf.math.exp(s_log_scale) + t_shift)

def inverse(y, b, shift_and_log_scale_fn):
    """
    This function takes the input Tensor x, binary mask b and callable
    shift_and_log_scale_fn as arguments.
    This function should implement the forward transformation in equation (5)
    and return the output Tensor y, which will have the same shape as x
    """
    t_shift, s_log_scale = shift_and_log_scale_fn(b*y)
    return b*y + (1-b)*((y - t_shift)*tf.math.exp(-s_log_scale))
```

The new bijector class also requires the `log_det_jacobian` methods to be implemented. Recall that the log of the Jacobian determinant of the forward transformation is given by $\sum_{j}s(x_{1:d})_j$, where $s$ is the log-scale function of the affine coupling layer.

You should now complete the following functions to define the `forward_log_det_jacobian` and `inverse_log_det_jacobian` methods of the affine coupling layer bijector.

* Both functions `forward_log_det_jacobian` and `inverse_log_det_jacobian` takes an input Tensor `x` (or `y`), a rank-3 binary mask `b`, and the `shift_and_log_scale_fn` callable
* These arguments are the same as the description for the `forward` and `inverse` functions
* The `forward_log_det_jacobian` function should implement the log of the Jacobian determinant for the transformation $(5)$
* The `inverse_log_det_jacobian` function should implement the log of the Jacobian determinant for the transformation $(6)$
* Both functions should reduce sum over the last three axes of the input Tensor (`height`, `width` and `channels`)

```python
#### GRADED CELL ####

# Complete the following functions.
# Make sure to not change the function names or arguments.

def forward_log_det_jacobian(x, b, shift_and_log_scale_fn):
    """
    This function takes the input Tensor x, binary mask b and callable
    shift_and_log_scale_fn as arguments.
    This function should compute and return the log of the Jacobian determinant
    of the forward transformation in equation (5)
    """
    _, s_log_scale = shift_and_log_scale_fn(b*x)
    return tf.reduce_sum((1-b)*s_log_scale, axis=[-1,-2,-3])

def inverse_log_det_jacobian(y, b, shift_and_log_scale_fn):
    """
    This function takes the input Tensor y, binary mask b and callable
    shift_and_log_scale_fn as arguments.
    This function should compute and return the log of the Jacobian determinant
    of the forward transformation in equation (6)
    """
    _, s_log_scale = shift_and_log_scale_fn(b*y)
    return -tf.reduce_sum((1-b)*s_log_scale, axis=[-1,-2,-3])
```

You are now ready to create the coupling layer bijector, using bijector subclassing. You should complete the class below to define the `AffineCouplingLayer`.

* You should complete the initialiser `__init__`, and the internal class method `_get_mask`
* The `_forward`, `_inverse`, `_forward_log_det_jacobian` and `_inverse_log_det_jacobian` methods are completed for you using the functions you have written above. Do not modify these methods
* The initialiser takes the `shift_and_log_scale_fn` callable, `mask_type` string (either `"checkerboard"` or `"channel"`, `orientation` (integer, either `0` or `1`) as required arguments, and allows for extra keyword arguments
  * The required arguments should be set as class attributes in the initialiser (note that the `shift_and_log_scale_fn` attribute is being used in the `_forward`, `_inverse`, `_forward_log_det_jacobian` and `_inverse_log_det_jacobian` methods)
  * The initialiser should call the base class initialiser, and pass in any extra keyword arguments
  * The class should have a required number of event dimensions equal to 3
* The internal method `_get_mask` takes a `shape` as an argument, which is the shape of an input Tensor
  * This method should use the `checkerboard_binary_mask` and `channel_binary_mask` functions above, as well as the `mask_type` and `orientation` arguments passed to the initialiser to compute and return the required binary mask
  * This method is used in each of the `_forward`, `_inverse`, `_forward_log_det_jacobian` and `_inverse_log_det_jacobian` methods

```python
#### GRADED CELL ####

# Complete the following class.
# Make sure to not change the class or method names or arguments.

class AffineCouplingLayer(tfb.Bijector):
    """
    Class to implement the affine coupling layer.
    Complete the __init__ and _get_mask methods according to the instructions above.
    """

    def __init__(self, shift_and_log_scale_fn, mask_type, orientation, **kwargs):
        """
        The class initialiser takes the shift_and_log_scale_fn callable, mask_type,
        orientation and possibly extra keywords arguments. It should call the
        base class initialiser, passing any extra keyword arguments along.
        It should also set the required arguments as class attributes.
        """
        super(AffineCouplingLayer, self).__init__(forward_min_event_ndims=3, **kwargs)
        self.shift_and_log_scale_fn = shift_and_log_scale_fn
        self.mask_type = mask_type
        self.orientation = orientation

    def _get_mask(self, shape):
        """
        This internal method should use the binary mask functions above to compute
        and return the binary mask, according to the arguments passed in to the
        initialiser.
        """
        if self.mask_type == "channel":
            return channel_binary_mask(shape[-1], self.orientation)
        return checkerboard_binary_mask(shape[1:], self.orientation)

    def _forward(self, x):
        b = self._get_mask(x.shape)
        return forward(x, b, self.shift_and_log_scale_fn)

    def _inverse(self, y):
        b = self._get_mask(y.shape)
        return inverse(y, b, self.shift_and_log_scale_fn)

    def _forward_log_det_jacobian(self, x):
        b = self._get_mask(x.shape)
        return forward_log_det_jacobian(x, b, self.shift_and_log_scale_fn)

    def _inverse_log_det_jacobian(self, y):
        b = self._get_mask(y.shape)
        return inverse_log_det_jacobian(y, b, self.shift_and_log_scale_fn)
```

```python
# Test your function by creating an instance of the AffineCouplingLayer class

affine_coupling_layer = AffineCouplingLayer(conv_resnet, 'channel', orientation=1,
                                            name='affine_coupling_layer')
```

```python
# The following should return a Tensor of the same shape as the input

affine_coupling_layer.forward(tf.random.normal((16, 32, 32, 3))).shape
```

    TensorShape([16, 32, 32, 3])

```python
# The following should compute a log_det_jacobian for each event in the batch

affine_coupling_layer.forward_log_det_jacobian(tf.random.normal((16, 32, 32, 3)), event_ndims=3).shape
```

    TensorShape([16])

#### Combining the affine coupling layers

In the affine coupling layer, part of the input remains unchanged in the transformation $(5)$. In order to allow transformation of all of the input, several coupling layers are composed, with the orientation of the mask being reversed in subsequent layers.

```python
# Run this cell to download and view a sketch of the affine coupling layers

!wget -q -O alternating_masks.png --no-check-certificate "https://docs.google.com/uc?export=download&id=1r1vASfLOW3kevxRzFUXhCtHN8dzldHve"
Image("alternating_masks.png", width=800)
```

Figure 2. RealNVP alternates the orientation of masks from one affine coupling layer to the next. From the original paper.

Our model design will be similar to the original architecture; we will compose three affine coupling layers with checkerboard masking, followed by a batch normalization bijector (`tfb.BatchNormalization` is a built-in bijector), followed by a squeezing operation, followed by three more affine coupling layers with channel-wise masking and a final batch normalization bijector.

The squeezing operation divides the spatial dimensions into 2x2 squares, and reshapes a Tensor of shape `(H, W, C)` into a Tensor of shape `(H // 2, W // 2, 4 * C)` as shown in Figure 1.

The squeezing operation is also a bijective operation, and has been provided for you in the class below.

```python
# Bijector class for the squeezing operation

class Squeeze(tfb.Bijector):

    def __init__(self, name='Squeeze', **kwargs):
        super(Squeeze, self).__init__(forward_min_event_ndims=3, is_constant_jacobian=True,
                                      name=name, **kwargs)

    def _forward(self, x):
        input_shape = x.shape
        height, width, channels = input_shape[-3:]
        y = tfb.Reshape((height // 2, 2, width // 2, 2, channels), event_shape_in=(height, width, channels))(x)
        y = tfb.Transpose(perm=[0, 2, 1, 3, 4])(y)
        y = tfb.Reshape((height // 2, width // 2, 4 * channels),
                        event_shape_in=(height // 2, width // 2, 2, 2, channels))(y)
        return y

    def _inverse(self, y):
        input_shape = y.shape
        height, width, channels = input_shape[-3:]
        x = tfb.Reshape((height, width, 2, 2, channels // 4), event_shape_in=(height, width, channels))(y)
        x = tfb.Transpose(perm=[0, 2, 1, 3, 4])(x)
        x = tfb.Reshape((2 * height, 2 * width, channels // 4),
                        event_shape_in=(height, 2, width, 2, channels // 4))(x)
        return x

    def _forward_log_det_jacobian(self, x):
        return tf.constant(0., x.dtype)

    def _inverse_log_det_jacobian(self, y):
        return tf.constant(0., y.dtype)

    def _forward_event_shape_tensor(self, input_shape):
        height, width, channels = input_shape[-3], input_shape[-2], input_shape[-1]
        return height // 2, width // 2, 4 * channels

    def _inverse_event_shape_tensor(self, output_shape):
        height, width, channels = output_shape[-3], output_shape[-2], output_shape[-1]
        return height * 2, width * 2, channels // 4
```

You can see the effect of the squeezing operation on some example inputs in the cells below. In the forward transformation, each spatial dimension is halved, whilst the channel dimension is multiplied by 4. The opposite happens in the inverse transformation.

```python
# Test the Squeeze bijector

squeeze = Squeeze()
squeeze(tf.ones((10, 32, 32, 3))).shape
```

    TensorShape([10, 16, 16, 12])

```python
# Test the inverse operation

squeeze.inverse(tf.ones((10, 4, 4, 96))).shape
```

    TensorShape([10, 8, 8, 24])

We can now construct a block of coupling layers according to the architecture described above. You should complete the following function to chain together the bijectors that we have constructed, to form a bijector that performs the following operations in the forward transformation:

* Three `AffineCouplingLayer` bijectors with `"checkerboard"` masking with orientations `0, 1, 0` respectively
* A `BatchNormalization` bijector
* A `Squeeze` bijector
* Three more `AffineCouplingLayer` bijectors with `"channel"` masking with orientations `0, 1, 0` respectively
* Another `BatchNormalization` bijector

The function takes the following arguments:
* `shift_and_log_scale_fns`: a list or tuple of six conv_resnet models
  * The first three models in this list are used in the three coupling layers with checkerboard masking
  * The last three models in this list are used in the three coupling layers with channel masking
* `squeeze`: an instance of the `Squeeze` bijector

_NB: at this point, we would like to point out that we are following the exposition in the original paper, and think of the forward transformation as acting on the input image. Note that this is in contrast to the convention of using the forward transformation for sampling, and the inverse transformation for computing log probs._

```python
#### GRADED CELL ####

# Complete the following function.
# Make sure to not change the function name or arguments.

def realnvp_block(shift_and_log_scale_fns, squeeze):
    """
    This function takes a list or tuple of six conv_resnet models, and an
    instance of the Squeeze bijector.
    The function should construct the chain of bijectors described above,
    using the conv_resnet models in the coupling layers.
    The function should then return the chained bijector.
    """
    block = [AffineCouplingLayer(shift_and_log_scale_fns[0], 'checkerboard', orientation=0),
             AffineCouplingLayer(shift_and_log_scale_fns[1], 'checkerboard', orientation=1),
             AffineCouplingLayer(shift_and_log_scale_fns[2], 'checkerboard', orientation=0),
             tfb.BatchNormalization(),
             squeeze,
             AffineCouplingLayer(shift_and_log_scale_fns[3], 'channel', orientation=0),
             AffineCouplingLayer(shift_and_log_scale_fns[4], 'channel', orientation=1),
             AffineCouplingLayer(shift_and_log_scale_fns[5], 'channel', orientation=0),
             tfb.BatchNormalization()
            ]

    return tfb.Chain(list(reversed(block)))
```

```python
# Run your function to create an instance of the bijector

checkerboard_fns = []
for _ in range(3):
    checkerboard_fns.append(get_conv_resnet((32, 32, 3), 512))
channel_fns = []
for _ in range(3):
    channel_fns.append(get_conv_resnet((16, 16, 12), 512))

block = realnvp_block(checkerboard_fns + channel_fns, squeeze)
```

```python
# Test the bijector on a dummy input

block.forward(tf.random.normal((10, 32, 32, 3))).shape
```

    TensorShape([10, 16, 16, 12])

#### Multiscale architecture

The final component of the RealNVP is the multiscale architecture. The squeeze operation reduces the spatial dimensions but increases the channel dimensions. After one of the blocks of coupling-squeeze-coupling that you have implemented above, half of the dimensions are factored out as latent variables, while the other half is further processed through subsequent layers. This results in latent variables that represent different scales of features in the model.

```python
# Run this cell to download and view a sketch of the multiscale architecture

!wget -q -O multiscale.png --no-check-certificate "https://docs.google.com/uc?export=download&id=19Sc6PKbc8Bi2DoyupHZxHvB3m6tw-lki"
Image("multiscale.png", width=700)
```

Figure 3. RealNVP creates latent variables at different scales by factoring out half of the dimensions at each scale. From the original paper.

The final scale does not use the squeezing operation, and instead applies four affine coupling layers with alternating checkerboard masks.

The multiscale architecture for two latent variable scales is implemented for you in the following bijector.

```python
# Bijector to implement the multiscale architecture

class RealNVPMultiScale(tfb.Bijector):

    def __init__(self, **kwargs):
        super(RealNVPMultiScale, self).__init__(forward_min_event_ndims=3, **kwargs)

        # First level
        shape1 = (32, 32, 3)  # Input shape
        shape2 = (16, 16, 12)  # Shape after the squeeze operation
        shape3 = (16, 16, 6)  # Shape after factoring out the latent variable
        self.conv_resnet1 = get_conv_resnet(shape1, 64)
        self.conv_resnet2 = get_conv_resnet(shape1, 64)
        self.conv_resnet3 = get_conv_resnet(shape1, 64)
        self.conv_resnet4 = get_conv_resnet(shape2, 128)
        self.conv_resnet5 = get_conv_resnet(shape2, 128)
        self.conv_resnet6 = get_conv_resnet(shape2, 128)
        self.squeeze = Squeeze()
        self.block1 = realnvp_block([self.conv_resnet1, self.conv_resnet2,
                                    self.conv_resnet3, self.conv_resnet4,
                                    self.conv_resnet5, self.conv_resnet6], self.squeeze)

        # Second level
        self.conv_resnet7 = get_conv_resnet(shape3, 128)
        self.conv_resnet8 = get_conv_resnet(shape3, 128)
        self.conv_resnet9 = get_conv_resnet(shape3, 128)
        self.conv_resnet10 = get_conv_resnet(shape3, 128)
        self.coupling_layer1 = AffineCouplingLayer(self.conv_resnet7, 'checkerboard', 0)
        self.coupling_layer2 = AffineCouplingLayer(self.conv_resnet8, 'checkerboard', 1)
        self.coupling_layer3 = AffineCouplingLayer(self.conv_resnet9, 'checkerboard', 0)
        self.coupling_layer4 = AffineCouplingLayer(self.conv_resnet10, 'checkerboard', 1)
        self.block2 = tfb.Chain([self.coupling_layer4, self.coupling_layer3,
                                 self.coupling_layer2, self.coupling_layer1])

    def _forward(self, x):
        h1 = self.block1.forward(x)
        z1, h2 = tf.split(h1, 2, axis=-1)
        z2 = self.block2.forward(h2)
        return tf.concat([z1, z2], axis=-1)

    def _inverse(self, y):
        z1, z2 = tf.split(y, 2, axis=-1)
        h2 = self.block2.inverse(z2)
        h1 = tf.concat([z1, h2], axis=-1)
        return self.block1.inverse(h1)

    def _forward_log_det_jacobian(self, x):
        log_det1 = self.block1.forward_log_det_jacobian(x, event_ndims=3)
        h1 = self.block1.forward(x)
        _, h2 = tf.split(h1, 2, axis=-1)
        log_det2 = self.block2.forward_log_det_jacobian(h2, event_ndims=3)
        return log_det1 + log_det2

    def _inverse_log_det_jacobian(self, y):
        z1, z2 = tf.split(y, 2, axis=-1)
        h2 = self.block2.inverse(z2)
        log_det2 = self.block2.inverse_log_det_jacobian(z2, event_ndims=3)
        h1 = tf.concat([z1, h2], axis=-1)
        log_det1 = self.block1.inverse_log_det_jacobian(h1, event_ndims=3)
        return log_det1 + log_det2

    def _forward_event_shape_tensor(self, input_shape):
        height, width, channels = input_shape[-3], input_shape[-2], input_shape[-1]
        return height // 4, width // 4, 16 * channels

    def _inverse_event_shape_tensor(self, output_shape):
        height, width, channels = output_shape[-3], output_shape[-2], output_shape[-1]
        return 4 * height, 4 * width, channels // 16
```

```python
# Create an instance of the multiscale architecture

multiscale_bijector = RealNVPMultiScale()
```

#### Data preprocessing bijector

We will also preprocess the image data before sending it through the RealNVP model. To do this, for a Tensor $x$ of pixel values in $[0, 1]^D$, we transform $x$ according to the following:

$$
T(x) = \text{logit}\left(\alpha + (1 - 2\alpha)x\right),\tag{7}
$$

where $\alpha$ is a parameter, and the logit function is the inverse of the sigmoid function, and is given by

$$
\text{logit}(p) = \log (p) - \log (1 - p).
$$

You should now complete the following function to construct this bijector from in-built bijectors from the bijectors module.

* The function takes the parameter `alpha` as an input, which you can assume to take a small positive value ($\ll0.5$)
* The function should construct and return a bijector that computes $(7)$ in the forward pass

```python
#### GRADED CELL ####

# Complete the following function.
# Make sure to not change the function name or arguments.

def get_preprocess_bijector(alpha):
    """
    This function should create a chained bijector that computes the
    transformation T in equation (7) above.
    This can be computed using in-built bijectors from the bijectors module.
    Your function should then return the chained bijector.
    """
    return  tfb.Chain([tfb.Invert(tfb.Sigmoid()),
                       tfb.Shift(alpha),
                       tfb.Scale(1 - 2*alpha)])
```

```python
# Create an instance of the preprocess bijector

preprocess = get_preprocess_bijector(0.05)
```

#### Train the RealNVP model

Finally, we will use our RealNVP model to train

We will use the following model class to help with the training process.

```python
# Helper class for training

class RealNVPModel(Model):

    def __init__(self, **kwargs):
        super(RealNVPModel, self).__init__(**kwargs)
        self.preprocess = get_preprocess_bijector(0.05)
        self.realnvp_multiscale = RealNVPMultiScale()
        self.bijector = tfb.Chain([self.realnvp_multiscale, self.preprocess])

    def build(self, input_shape):
        output_shape = self.bijector(tf.expand_dims(tf.zeros(input_shape[1:]), axis=0)).shape
        self.base = tfd.Independent(tfd.Normal(loc=tf.zeros(output_shape[1:]), scale=1.),
                                    reinterpreted_batch_ndims=3)
        self._bijector_variables = (
            list(self.bijector.variables))
        self.flow = tfd.TransformedDistribution(
            distribution=self.base,
            bijector=tfb.Invert(self.bijector),
        )
        super(RealNVPModel, self).build(input_shape)

    def call(self, inputs, training=None, **kwargs):
        return self.flow

    def sample(self, batch_size):
        sample = self.base.sample(batch_size)
        return self.bijector.inverse(sample)
```

```python
# Create an instance of the RealNVPModel class

realnvp_model = RealNVPModel()
realnvp_model.build((1, 32, 32, 3))
```

```python
# Compute the number of variables in the model

print("Total trainable variables:")
print(sum([np.prod(v.shape) for v in realnvp_model.trainable_variables]))
```

    Total trainable variables:
    315180

Note that the model's `call` method returns the `TransformedDistribution` object. Also, we have set up our datasets to return the input image twice as a 2-tuple. This is so we can train our model with negative log-likelihood as normal.

```python
# Define the negative log-likelihood loss function

def nll(y_true, y_pred):
    return -y_pred.log_prob(y_true)
```

It is recommended to use the GPU accelerator hardware on Colab to train this model, as it can take some time to train. Note that it is not required to train the model in order to pass this assignment. For optimal results, a larger model should be trained for longer.

```python
# Compile and train the model

optimizer = Adam()
realnvp_model.compile(loss=nll, optimizer=Adam())
realnvp_model.fit(train_ds, validation_data=val_ds, epochs=30)
```

    Epoch 1/30
    938/938 [==============================] - 177s 174ms/step - loss: -707.8909 - val_loss: -5840.3013
    Epoch 2/30
    938/938 [==============================] - 159s 169ms/step - loss: -6111.2559 - val_loss: -6756.3145
    Epoch 3/30
    938/938 [==============================] - 159s 169ms/step - loss: -6910.6631 - val_loss: -6927.1729
    Epoch 4/30
    938/938 [==============================] - 159s 170ms/step - loss: -7312.1277 - val_loss: -7508.2822
    Epoch 5/30
    938/938 [==============================] - 159s 170ms/step - loss: -7643.3239 - val_loss: -7852.6743
    Epoch 6/30
    938/938 [==============================] - 160s 170ms/step - loss: -7877.9718 - val_loss: -8059.9521
    Epoch 7/30
    938/938 [==============================] - 160s 170ms/step - loss: -8029.5228 - val_loss: -8154.4463
    Epoch 8/30
    938/938 [==============================] - 160s 170ms/step - loss: -8096.0561 - val_loss: -8171.3911
    Epoch 9/30
    938/938 [==============================] - 161s 171ms/step - loss: -8275.6569 - val_loss: -8381.8096
    Epoch 10/30
    938/938 [==============================] - 160s 171ms/step - loss: -8362.6644 - val_loss: -8478.6221
    Epoch 11/30
    938/938 [==============================] - 160s 170ms/step - loss: -8414.7269 - val_loss: -8432.4609
    Epoch 12/30
    938/938 [==============================] - 160s 170ms/step - loss: -8513.4065 - val_loss: -8552.8350
    Epoch 13/30
    938/938 [==============================] - 160s 170ms/step - loss: -8580.6069 - val_loss: -8183.6743
    Epoch 14/30
    938/938 [==============================] - 160s 171ms/step - loss: -8577.6506 - val_loss: -8218.2852
    Epoch 15/30
    938/938 [==============================] - 161s 171ms/step - loss: -8641.1439 - val_loss: -8737.7217
    Epoch 16/30
    938/938 [==============================] - 161s 172ms/step - loss: -8719.1222 - val_loss: -8792.2207
    Epoch 17/30
    938/938 [==============================] - 160s 171ms/step - loss: -8745.2288 - val_loss: -8806.3740
    Epoch 18/30
    938/938 [==============================] - 161s 171ms/step - loss: -8789.5733 - val_loss: -8812.3027
    Epoch 19/30
    938/938 [==============================] - 161s 171ms/step - loss: -8822.5408 - val_loss: -8733.9414
    Epoch 20/30
    938/938 [==============================] - 160s 170ms/step - loss: -8845.0601 - val_loss: -8685.7627
    Epoch 21/30
    938/938 [==============================] - 158s 168ms/step - loss: -8790.0170 - val_loss: -8930.2686
    Epoch 22/30
    938/938 [==============================] - 158s 168ms/step - loss: -8892.3129 - val_loss: -8887.8994
    Epoch 23/30
    938/938 [==============================] - 158s 168ms/step - loss: -8916.0576 - val_loss: -8944.6865
    Epoch 24/30
    938/938 [==============================] - 158s 168ms/step - loss: -8948.5608 - val_loss: -9006.7412
    Epoch 25/30
    938/938 [==============================] - 158s 168ms/step - loss: -8946.5670 - val_loss: -9010.3037
    Epoch 26/30
    938/938 [==============================] - 158s 168ms/step - loss: -8982.3949 - val_loss: -9001.4053
    Epoch 27/30
    938/938 [==============================] - 158s 168ms/step - loss: -8981.5100 - val_loss: -9039.0400
    Epoch 28/30
    938/938 [==============================] - 159s 169ms/step - loss: -9011.4595 - val_loss: -8984.9727
    Epoch 29/30
    938/938 [==============================] - 158s 168ms/step - loss: -9019.1256 - val_loss: -9087.3760
    Epoch 30/30
    938/938 [==============================] - 158s 168ms/step - loss: -9036.3030 - val_loss: -9039.8252

    <tensorflow.python.keras.callbacks.History at 0x7fa8001200d0>

```python
# Evaluate the model

realnvp_model.evaluate(test_ds)
```

    157/157 [==============================] - 8s 49ms/step - loss: -9033.6807

    -9033.6806640625

#### Generate some samples

```python
# Sample from the model

samples = realnvp_model.sample(8).numpy()
```

    /usr/local/lib/python3.7/dist-packages/tensorflow/python/keras/engine/base_layer.py:2273: UserWarning: `layer.apply` is deprecated and will be removed in a future version. Please use `layer.__call__` method instead.
      warnings.warn('`layer.apply` is deprecated and '

```python
# Display the samples

n_img = 8
f, axs = plt.subplots(2, n_img // 2, figsize=(14, 7))

for k, image in enumerate(samples):
    i = k % 2
    j = k // 2
    axs[i, j].imshow(image[0])
    axs[i, j].axis('off')
f.subplots_adjust(wspace=0.01, hspace=0.03)
```

Congratulations on completing this programming assignment! In the next week of the course we will look at the variational autoencoder.


# Finite element analysis of an elastic frame

______________________________

*Date:* 2018-01-14

*Author:* Pierre Jehel (<pierre.jehel@centralesupelec.fr>)

*Course:* Gestion des Risques Structure (CentraleSupélec, option / MS "Aménagement et Construction Durables")
______________________________

This document presents the principal steps in a finite element analysis program for a simple  structure.
For any FE program, the overall procedure is the same.
The three main steps are:
- Pre-processing: the system is described by the user (inputs)
- Processing: the FE mesh is generated and the problem is numerically solved
- Post-processing: plotting and analysis of the outputs

We first import the Python packages and modules we will need:

```python
import numpy as np
import numpy.matlib
from numpy import linalg
import mesGen
import strMod
import impGM
import modAna
```

## 1. Description of the system

User inputs here the description of the structure she/he wants to model:
- Structure geometry
- Element section geometry
- Element material properties
- Boundary conditions (loading)
- Parameterization for the solution procedure
- Output data

### 1.1. Structure geometry

We consider a 2D frame structure.

We first create an array with, for each structural element, the following list: [node I, node J, element type (1: beam/column, 2: rotational spring), element length, global to local rotation angle] (see figure with the description of the structure):

Nodes coordinates (2D problem):

```python
Lf = 8      # bay length [m]
Hf = 4      # story height [m]
nodCoo = []
nodCoo.append([0,0])     # node 0 [X coordinate,Y coordinate]
nodCoo.append([Lf,0])    # node 1 [X,Y]
nodCoo.append([0,Hf])    # ...
nodCoo.append([Lf,Hf])
nodCoo.append([0,2*Hf])
nodCoo.append([Lf,2*Hf])
```

Elements connect a node I to a node J; we consider different:
- element types (0->beam/column; 1->rotational spring)
- element sections

```python
strEle = []
strEle.append([0,2,0,0])  # elt 0 [node I, node J, element type, section type]
strEle.append([2,4,0,0])  # elt 1
strEle.append([1,3,0,1])  # ...
strEle.append([3,5,0,1])
strEle.append([2,3,0,2])
strEle.append([4,5,0,3])
strEle.append([0,3,0,4])
strEle.append([1,2,0,4])
```

### 1.2. Element properties (material and section geometry)

The geometric properties of selected steel beam sections (see figure) can for instance be found here: <http://ds.arcelormittal.com/repo/Catalog/ArcelorMittal_PV_FR_RU.pdf>

*Remark:* We will consider that the beams can be described using Euler-Bernoulli kinematic assumptions. Other options are generally available in FE analysis programs such as Timoshenko kinematics.

E [N/m2]: material Young's modulus
A [m2]  : element cross-section area
Iy [m4] : element cross-section moment of inertia wrt centroid

```python
eltPro = []
eltPro.append([2.1e11,33.4e-4,2772e-8])   # section type 0  [E, A, Iy] -> IPE 220
eltPro.append([2.1e11,33.4e-4,2772e-8])   # section type 1
eltPro.append([2.1e11,33.4e-4,2772e-8])   # ...
eltPro.append([2.1e11,33.4e-4,2772e-8])
eltPro.append([2.1e11,2.5e-3,0])
```

### 1.4. Boundary conditions

In 'fixDof' array, we indicate for every node whether the DOFs are free or not.
The line number of the matrix corresponds to the node number.
For each of the 3 DOFs $(u,v,\theta)$, there is a 1 if the DOF is free and a 0 otherwise:

```python
fixDof = []
fixDof.append([0,0,0])   # node 0
fixDof.append([0,0,0])   # node 1
fixDof.append([1,1,1])   # ...
fixDof.append([1,1,1])
fixDof.append([1,1,1])
fixDof.append([1,1,1])
```

The frame is fixed in the ground at its base.

## 2. Mesh generation

In this simple example, the only thing that has to be done for generating the mesh is computing element length and orientation ($cos(\theta)$ and $sin(\theta)$ where $\theta$ is the angle between local and global coordinate systems). We get these mesh data as:

```python
# mesh data
mesDat = mesGen.eltL0(strEle,nodCoo)

# number of free DOFs
nDof = np.sum(fixDof)
```

## 3. External loading

### 3.1. Static loading

Uniformly distributed static loadings are applied on both beams; self-weight is neglected:

```python
q1 = 1.5e5   # [N] 2nd-floor beam
q2 = 1.5e5   # [N] 3rd-floor beam
H1 = 0.1*q1*Lf/2
```

In 'staLoa' array, we input the static loading actions on the nodes of the FE mesh:

```python
Fy1 = q1*Lf/2
Mz1 = q1*Lf**2/12
Fy2 = q2*Lf/2
Mz2 = q2*Lf**2/12
staLoa = []
staLoa.append([0,0,0])         # node 0 [Dx, Dy, Rz] (displacements)
staLoa.append([0,0,0])         # node 1 [Dx, Dy, Rz] (displacements)
staLoa.append([H1,-Fy1,-Mz1])   # node 2 [Fx, Fy, Mz] (forces)
staLoa.append([0,-Fy1,Mz1])    # ...
staLoa.append([2*H1,-Fy2,-Mz2])
staLoa.append([0,-Fy2,Mz2])
```

### 3.2. Seismic loading

We also consider lumped masses at nodes 2, 3, 4 and 5 which are active for DOFs 1 (horizontal) only:

```python
gra = 9.81       # [m/s2]
mas1 = Fy1/gra   # [kg]
mas2 = Fy2/gra   # [kg]
inpMas = []
inpMas.append([2,mas1,mas1,mas1])   # node,DOF1,DOF2,DOF3
inpMas.append([3,mas1,mas1,mas1])
inpMas.append([4,mas2,mas2,mas2])
inpMas.append([5,mas2,mas2,mas2])

# mass active DOFs in the horizontal direction
r_a1 = [[1],[0],[0],[1],[0],[0],[1],[0],[0],[1],[0],[0]]
r_v = np.matrix(r_a1)
```

Ground acceleration is recorded in file 'ps_fv.csv' [time, acceleration] and we import it in the 2D-array 'GM_a2':

```python
GM_a2,timFin,timSteGM = impGM.impCsv('./GroundMotions/accePS-FV.csv')
```

## 4. Resolution procedure

### 4.1. Static analysis

```python
# external forces vector
fsta_v = strMod.extLoa(nDof,fixDof,staLoa)

# structural stiffness matrix K
K_m = strMod.stiMat(nDof,strEle,mesDat,eltPro,fixDof)

# compute static displacements at free DOFs
dis_v = linalg.solve(K_m,fsta_v)

print(dis_v)
```

    [[ 0.00131453]
     [-0.00602847]
     [-0.05424632]
     [ 0.00425264]
     [-0.00716835]
     [ 0.02642456]
     [ 0.13603423]
     [-0.00930051]
     [-0.11907751]
     [ 0.13202723]
     [-0.01073976]
     [ 0.0706236 ]]

### 4.2. Modal analysis

```python
# structural mass matrix M
M_m = strMod.masMat(nDof,inpMas,fixDof)

# eigen properties of the structure
ome_v,phi_m = modAna.eigPro(M_m,K_m)
print('ome_v =',ome_v)
print('phi1 =', phi_m[:,0])
```

    ome_v = [  2.65622693   9.94067634  12.14197102  15.26394538  16.99656258
      26.51521915  35.19636302  33.63846666  60.4941725   53.71540759
      87.59548225  87.55659549]
    phi1 = [[-0.010847  ]
     [-0.00255332]
     [ 0.07255103]
     [-0.010847  ]
     [ 0.00255332]
     [ 0.07255103]
     [-0.69098495]
     [-0.00337131]
     [ 0.13092213]
     [-0.69098495]
     [ 0.00337131]
     [ 0.13092213]]

```python
# generalized masses
genMas_v = np.zeros((nDof,1))
for i in range(nDof):
    genMas_v[i,0] = np.transpose(phi_m[:,i])*M_m*phi_m[:,i]

# normalize eigen vectors wrt mass
phiNor_m = np.zeros((nDof,nDof))
for i in range(4,nDof):
    for j in range(nDof):
        phiNor_m[j,i] = phi_m[j,i]/np.sqrt(genMas_v[i,0])

#print(np.transpose(phiNor_m[:,6])*M_m*phiNor_m[:,6])

# participation factors
parFac_v = np.zeros((nDof,1))
for i in range(nDof):
    if genMas_v[i,0] == 0:
        parFac_v[i,0] = 0
    else:
        parFac_v[i,0] = np.transpose(phi_m[:,i])*M_m*r_v/genMas_v[i,0]

print(parFac_v)
```

    [[  1.85038500e+00]
     [  7.21726461e-01]
     [ -2.41641374e-16]
     [  1.81701563e-01]
     [ -5.72504177e-16]
     [ -1.48897639e-01]
     [ -1.18961907e-16]
     [ -9.51695256e-16]
     [ -5.46166933e-04]
     [ -6.90118872e-17]
     [ -1.40394832e-05]
     [ -9.59815643e-17]]

### 4.3. Seismic analysis with response spectrum

```python

```

### 4.4. Seismic analysis with time-stepping algorithm

This is a dynamic nonlinear problem. We use Newmark time-stepping algorithm and Newton-Raphson iterative procedure to satisfy the equilibrium equation at each time step.

The equilibrium equation reads:
\begin{equation}
 \begin{pmatrix}
  \mathbf{M}_{bb} & \mathbf{0} \\
  \mathbf{0} & \mathbf{M}
 \end{pmatrix}
 \begin{pmatrix}
  \mathbf{0} \\
  \mathbf{a}(t)
 \end{pmatrix} +
 \begin{pmatrix}
  \mathbf{C}_{bb} & \mathbf{C}_{b} \\
  \mathbf{C}_{b} & \mathbf{C}
 \end{pmatrix} +
 \begin{pmatrix}
  \mathbf{0} \\
  \mathbf{v}(t)
 \end{pmatrix} +
 \begin{pmatrix}
  \mathbf{f}_b^r(t) \\
  \mathbf{f}^r(t)
 \end{pmatrix} = -
 \begin{pmatrix}
  \mathbf{M}_{bb} & \mathbf{0} \\
  \mathbf{0} & \mathbf{M}
 \end{pmatrix}
 \begin{pmatrix}
  \mathbf{\Delta}_b \\
  \mathbf{\Delta}
 \end{pmatrix} a^g(t) +
 \begin{pmatrix}
  \mathbf{f}_b^s(t) \\
  \mathbf{f}^s(t)
 \end{pmatrix}
\end{equation}
where the first lines (with subscript 'b') correspond to the $B$ DOFs that are fixed on the ground (base of the structure); the last $N$ lines correspond to the $N$ free DOFs. Also:

- $\mathbf{M}$, $\mathbf{C}$ are the mass and stiffness matrices, and $\mathbf{f}^r$ is the resisting forces vector
- $\mathbf{a}$, $\mathbf{v}$, and $\mathbf{d}$ are the vectors of the accelerations, velocities and displacements at the structural DOFs
- $\mathbf{\Delta}a^g(t)$ is the seismic ground motion time history vector ($\Delta$ vector components are 1 for the horizontal DOFs and 0 at the other DOFs; $a^g(t)$ is the recorded seimic ground motion)
- $\mathbf{f}^s$ is the vector of the external static loading (as opposed to the seismic loading); the components of the vector $\mathbf{f}_b^s$ are the reactions at the fixed DOFs.

*Remark:* A lumped mass matrix is considered here (diagonal matrix).

*Remark:* In what follows, we only solve the $N$ equations corresponding to the $N$ free DOFs. Then the $B$ other equations can be straightforwardly solved to compute the reactions as:
\begin{equation}
 \mathbf{f}_b^s = \mathbf{C}_b \, \mathbf{v} + \mathbf{f}_b^r + \mathbf{M}_{bb} \mathbf{\Delta}_b a^g
\end{equation}

We know recast the problem introducing the residual
\begin{equation}
 \mathbf{0} := \mathbf{r}(\mathbf{u},\mathbf{v},\mathbf{a}) = \mathbf{f}^e - \left( \mathbf{M} \, \mathbf{a} + \mathbf{C} \, \mathbf{v} + \mathbf{f}^r(\mathbf{u}) \right)
\end{equation}
where the external action is denoted as:
\begin{equation}
 \mathbf{f}^e := -\mathbf{M}\mathbf{\Delta}a^g + \mathbf{f}^s
\end{equation}

Newmark time-stepping method:
\begin{align}
 & \mathbf{u}_{n+1} = \mathbf{u}_n + \Delta t \, \mathbf{v}_n + \Delta t^2 \left( (\frac{1}{2}-\beta) \mathbf{a}_n + \beta \mathbf{a}_{n+1} \right) \\
 & \mathbf{v}_{n+1} = \mathbf{v}_n + \Delta t \left( (1-\gamma) \mathbf{a}_n + \gamma \mathbf{a}_{n+1} \right)
\end{align}

Newton-Raphson iterative procedure to solve the nonlinear equation $\mathbf{r} = \mathbf{0}$:
\begin{align}
 \mathbf{0} & := \mathbf{r}_{n+1}^{(i+1)} \approx \mathbf{r}_{n+1}^{(i)} + d\mathbf{r}_{n+1}^{(i+1)} \\
  & = \mathbf{r}_{n+1}^{(i)} + \frac{\partial \mathbf{r}}{\partial \mathbf{u}}\Big|_{n+1}^{(i)} \, d\mathbf{u}_{n+1}^{(i+1)} + \frac{\partial \mathbf{r}}{\partial \mathbf{v}}\Big|_{n+1}^{(i)} \, d\mathbf{v}_{n+1}^{(i+1)} + \frac{\partial \mathbf{r}}{\partial \mathbf{a}}\Big|_{n+1}^{(i)} \, d\mathbf{a}_{n+1}^{(i+1)} \\
  & = \mathbf{r}_{n+1}^{(i)} + \left( \frac{\partial \mathbf{r}}{\partial \mathbf{u}}\Big|_{n+1}^{(i)} + \frac{\partial \mathbf{r}}{\partial \mathbf{v}}\Big|_{n+1}^{(i)} \frac{d\mathbf{v}_{n+1}^{(i+1)}}{d\mathbf{a}_{n+1}^{(i+1)}} \frac{d\mathbf{a}_{n+1}^{(i+1)}}{d\mathbf{u}_{n+1}^{(i+1)}} + \frac{\partial \mathbf{r}}{\partial \mathbf{a}}\Big|_{n+1}^{(i)} \frac{d\mathbf{a}_{n+1}^{(i+1)}}{d\mathbf{u}_{n+1}^{(i+1)}} \right) d\mathbf{u}_{n+1}^{(i+1)} \\
  & = \mathbf{r}_{n+1}^{(i)} - \left( \mathbf{Kt}_{n+1}^{(i)} + \frac{\gamma}{\beta \, \Delta t} \mathbf{C} + \frac{1}{\beta \, \Delta t^2} \mathbf{M} \right) d\mathbf{u}_{n+1}^{(i+1)}
\end{align}
where $\mathbf{Kt} := \partial \mathbf{f}^r / \partial \mathbf{u}$ is the so-called tangent stiffnes matrix.

From the relation above, we can compute the updated displacements:
\begin{equation}
 \mathbf{u}_{n+1}^{(i+1)} = \mathbf{u}_{n+1}^{(i)} + \left( \mathbf{Kt}^{\star (i)}_{n+1} \right)^{-1} \mathbf{r}_{n+1}^{(i)}
\end{equation}
where $\mathbf{Kt}^{\star} = \mathbf{Kt}_{n+1}^{(i)} + c_1 \mathbf{C} + c_2 \mathbf{M}$ with:
\begin{equation}
 c_1 = \frac{\gamma}{\beta \, \Delta t} \qquad \textrm{and} \qquad c_2 = \frac{1}{\beta \, \Delta t^2}
\end{equation}

Then, the velocity and acceleration vectors can be updated as:
\begin{align}
 & \mathbf{a}_{n+1}^{(i+1)} = \mathbf{a}_{n+1}^{(i)} + \frac{1}{\beta \, \Delta t^2} d\mathbf{u}_{n+1}^{(i+1)} \\
 & \mathbf{v}_{n+1}^{(i+1)} = \mathbf{v}_{n+1}^{(i)} + \frac{\gamma}{\beta \Delta t} d\mathbf{u}_{n+1}^{(i+1)}
\end{align}

*Remark:* Here, the damping matrix $\mathbf{C}$ is considered as time independent, but it is time dependent for some damping models.

#### 4.4.1. Parameterization

```python
# structural damping ratio
strXi = 0.02

# time stepping algorithm (Newmark method)
timSte = 1e-2   # [s] time step
gamma = 0.5
beta = 0.25

# build Rayleigh damping matrix
alp,bet = strMod.rayDamCoe(strXi,ome_v)
C_m = alp*M_m + bet*K_m

# get time stepping coefficients c0, c1, and c2 (Newmark method)
c0 = 1/(beta*timSte)
c1 = c0*gamma
c2 = c0/timSte

# time steps ratio
timSteRat = timSteGM/timSte

# generalized stiffness matrix
K2_m = c1*C_m + c2*M_m
Kstar_m = K_m + K2_m
```

#### 4.4.2. Initialization

```python
# time
time = 0

# initial forces vector
fi_v = np.matlib.zeros((nDof,1))  # !!!change if not zeros

# initial displacements vector
dis_v = np.matlib.zeros((nDof,1))  # !!!change if not zeros

# initial velocities
vel_v = np.matlib.zeros((nDof,1))

# initial ext. seismic forces vector
fsei_v = np.matlib.zeros((nDof,1))

# initial accelerations
acc_v = np.matlib.zeros((nDof,1))
f_v = fsei_v-C_m*vel_v-K_m*dis_v
for i in range(0,nDof):
    if M_m[i,i] == 0:
        acc_v[i] = 0
    else:
        acc_v[i] = 1/M_m[i,i]*f_v[i]

# output quantities
filDis = open('disp.txt','w')
filDis.write('{:1.3f},{:+.6E},{:+.6E},{:+.6E},{:+.6E} \n'
    .format(time,dis_v[0,0],dis_v[1,0],dis_v[2,0],dis_v[3,0]))
```

    63

#### 4.4.3. Step-by-step resolution

```python
# step forward
time += timSte

i = 1
while time < timFin:
#    print('time =',time)

    # interpolate ground acceleration
    xxx = i/timSteRat
    j = np.floor(xxx)
    j = np.int(j)
    res = xxx-j
    if res > 1e-12:
        groAcc = (1-res)*GM_a2[j][1] + res*GM_a2[j+1][1]
    else:
        groAcc = (1-res)*GM_a2[j][1]

    # compute external loading vector
    fsei_v = -M_m*r_v*groAcc*9.81

    # initialize
    vel1_v = (1-gamma/beta)*vel_v
    vel2_v = timSte*(1-gamma/(2*beta))*acc_v
    acc1_v = -1/(beta*timSte)*vel_v
    acc2_v = (1-1/(2*beta))*acc_v
    vel_v = vel1_v+vel2_v
    acc_v = acc1_v+acc2_v

    conv = False
    k = 0

    while conv == False:

        # compute residual
        res_v = fsei_v - (M_m*acc_v + C_m*vel_v + fi_v)

        # update increments
        disInc_v = linalg.solve(Kstar_m,res_v)
        dis_v += disInc_v
        vel_v += c1*disInc_v
        acc_v += c2*disInc_v

        # for linear problems
        fi_v += K_m*disInc_v

        # for nonlinear problems
#        K1_m = strMod.stiMat(nDof,strEle,mesDat,eltPro,fixDof)
#        Kstar_m = K_m + K2_m
#        fi_v += K_m*disInc_v

        # check convergence
        norm = res_v.T*disInc_v
        if norm < 1e-12:
            conv = True
        elif k > 50:
            print('No convergence')
            break
        else:
            k += 1

    # output quantities
    filDis.write('{:1.3f},{:+.6E},{:+.6E},{:+.6E},{:+.6E} \n'
        .format(time,dis_v[0,0],dis_v[1,0],dis_v[2,0],dis_v[3,0]))

    # step forward
    i += 1
    time += timSte

filDis.close()
print('End of the computation')
```

    End of the computation

## 5. Outputs analysis

### 5.1. Internal forces

We are now interested in computing the internal forces in the elements connected to node 2 (elements 0, 1, and 4). This can be computed from the primary unknowns of the problem (the displacements of the free DOFs in the global coordinates system) as follows:
- Retrieve in vector $\texttt{dis_v}$ the displacements at both ends of each element:

```python

```

- Project the displacements into the local coordinates system:

```python

```

- Compute the internal forces (normal force, shear force, bendiing moment):

```python

```

### 5.2. Normal stresses

We can now focus on the normal stresses $\sigma_{xx}(x,y)$ in the structural elements. In an element $e$ of cross-section area $A$, length $L$, and moment of inertia $I_z$ they can be computed from the formula:

\begin{equation}
 \sigma^e_{xx}(x,y) = \frac{N^e(x)}{A} + \frac{M^e(x) \, y}{I_z}
\end{equation}

where

\begin{eqnarray}
 & N^e(x) = \nu(x) + N^e_I\left(1-\frac{x}{L}\right) - N^e_J \frac{x}{L} \\
 & M^e(x) = \mu(x) - M^e_I\left(1-\frac{x}{L}\right) + M^e_J \frac{x}{L}
\end{eqnarray}

with $\nu(x)$ and $\mu(x)$ the normal force and bending moment in the isostatic version of the element $e$. For instance, element 4 is uniformly loaded with distributed vertical load $-q_1$, in this case:
- $\nu(x) = 0$
- $\mu(x) = q_1 x (Lf - x) / 2$

```python

```


```python
# IMPORT PACKAGES
from geoscilabs.em import UXO_TEM_Widget as UXO
from IPython.display import display
from ipywidgets import HBox
```

# Contents

This app contains 3 widgets:

* **Orientation and polarization widget:** This widget allows the user to visualize the orientation, infer the dimensions and change the polarizabilities of compact objects they wish to model.
* **Data visualization widget:** This widget allows the user to visualize the step-off response of compact objects using three commonly used instruments: EM61, TEMTADS, and MPV.
* **Parameter estimation widget:** This widget allows the user to invert synthetic data collected using EM61, TEMTADS or MPV instruments in order to recover the location and primary polarizabilities for a compact object.

# Background Theory

## Polarization Tensor

The magnetic dipole moment ${\bf m}$ being experienced by a compact object is given by:

\begin{equation}
\mathbf{m = Q \, h_p}
\end{equation}

where ${\bf h_p} = [h_x,h_y,h_z]^T$ is the primary magnetic field caused by the transmitter before shut-off and ${\bf Q}$ is the called the **polarizability tensor**. The polarizability tensor is a 3X3 symmetric, positive-definite (SPD) matrix given by:

\begin{equation}
{\bf Q} = \begin{bmatrix} q_{11} & q_{12} & q_{13} \\ q_{12} & q_{22} & q_{23} \\ q_{13} & q_{23} & q_{33} \end{bmatrix}
\end{equation}

where $q_{ij}$ defines hows strongly field component $h_i$ contributes towards $m_j$.

## Coordinates and Primary Polarizations

The polarizability tensor for an object depends on its orientation, dimensions and electromagnetic properties. Because the polarizability tensor is SPD, it can be decomposed using the following eigen-decomposition:

\begin{equation}
{\bf Q = A \, L(t) \, A^T}
\end{equation}

where

\begin{equation}
{\bf A} = \begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{12} & a_{22} & a_{23} \\ a_{13} & a_{23} & a_{33} \end{bmatrix} \;\;\;\; \textrm{and} \;\;\;\;
{\bf L(t)} = \begin{bmatrix} L_{x'}(t) & 0 & 0 \\ 0 & L_{y'}(t) & 0 \\ 0 & 0 & L_{z'}(t) \end{bmatrix}
\end{equation}

${\bf A}$ is a SPD rotation matrix from the coordinate system defined by the UXO ($x',y',z'$) to the field coordinate system ($x,y,z$). ${\bf A}$ is defined by three angles: $\psi,\theta$ and $\phi$. $\theta$ is the azimuthal angle (angle relative to vertical), $\phi$ is the declination (angle relative to North) and $\psi$ is the roll (rotation about z' axis).

${\bf L(t)}$ characterizes the primary polarizabilities of the object. The magnetic dipole moment experienced by the object is a linear combination of polarizabilities $L_{x'},L_{y'}$ and $L_{z'}$. Depending on the dimensions and of the object, $L_{x'},L_{y'}$ and $L_{z'}$ may differ. For example:

* A sphere has primary polarizabilities $L_{x'}=L_{y'}=L_{z'}$
* A UXO has primary polarizabilities $L_{x'}=L_{y'}<L_{z'}$

For a given axis $i$, the primary polarizability for a step-off response at $t>0$ is given by:

\begin{equation}
L_{ii}(t) = k_i \Bigg ( 1 + \frac{t^{1/2}}{\alpha_i^{1/2}} \Bigg )^{-\beta_i} e^{-t/\gamma_i}
\end{equation}

where the decay of the object's polarization is determined by parameters $k_i,\alpha_i,\beta_i$ and $\gamma_i$.

## Predicting Data

There are a multitude of instruments used to measure the time-domain responses exhibited by UXOs (EM61, TEMTADS, MPV). For each individual measurement, a transmitter loop produces a primary magnetic field ${\bf h_p} = [h_x,h_y,h_z]^T$ which is turned off a $t=0$. The primary field polarizes the UXO according to its polarizability tensor ${\bf Q}$. The polarization of the object produces a secondary field which induces an EMF in one or more receiver coils. The field component being measured by each receiver coil depends on its orientation.

Where ${\bf G} = [g_x,g_y,g_z]$ maps the dipole moment experienced by the object to the induced voltage in a receiver coil:

\begin{equation}
d = {\bf G \, m} = {\bf G \, Q \, h_p}
\end{equation}

Because it is SPD, the polarizability tensor may be characterized at each time by 6 parameters $(q_{11},q_{12},q_{13},q_{22},q_{23},q_{33})$. The previous expression can ultimately be reformulated as:

\begin{equation}
d = {\bf P \, q}
\end{equation}

where

\begin{equation}
{\bf q^T} = [q_{11} \;\; q_{12} \;\; q_{13} \;\; q_{22}\;\; q_{23} \;\; q_{33}]
\end{equation}

and

\begin{equation}
{\bf P} = [h_xg_x \;\; h_xg_y \!+\! h_yg_x \;\; h_xg_z \!+\! h_zg_x \;\; h_zg_y \;\; h_yg_z \!+\! h_zg_y \;\; h_zg_z]
\end{equation}

Thus in the case that there are $N$ distinct transmitter-receiver pair, each transmitter-receiver pair is represented as a row within ${\bf P}$. ${\bf q}$ contains all the necessary information to construct ${\bf Q}$ and ${\bf P}$ contains all the geometric information associated with the problem.

## Inversion and Parameter Estimation

When inverting field-collected UXO data there are two primary goals:

* Accurate location of a target object (recover $x,y,z$)
* Accurate characterization of a target object (by recovering $L_{x'},L_{y'},L_{z'}$)

For this widget, we will accomplish these goals in two steps.

### Step 1

In step 1, we intend to recover the location of the target $(x,y,z)$ and the elements of the polarizability tensor $(q_{11},q_{12},q_{13},q_{22},q_{23},q_{33})$ at each time. A basic approach is applied by finding the location and polarizabilities which minimize the following data misfit function:

\begin{equation}
\begin{split}
\Phi &= \sum_{i=k}^K \Big \| {\bf W_k} \big ( {\bf P \, q_k - d_{k,obs}} \big ) \Big \|^2 \\
& \textrm{s.t.} \\
& q_{min} \leq q_{ij}(t) \leq q_{max} \\
& q_{ii}(t) \geq 0 \\
& \big | q_{ij}(t) \big | \leq \frac{1}{2} \big ( \; \big | q_{ii}(t) \big | + \big | q_{jj}(t) \big | \; \big )
\end{split}
\end{equation}

where ${\bf P}$ depends on the location of the target, $i$ refers to the time-channel, $d_{i,obs}$ is the observed data at time $i$ and ${\bf W_i}$ are a set of weights applied to the data misfit. The constraint assures that negative polarizabilities (non-physical) are not recovered in order to fit the data.

### Step 2

Once recovered, ${\bf q}$ at each time can be used to construct the corresponding polarizability tensor ${\bf Q}$. Recall that the eigen-decomposition of ${\bf Q}$ is given by:

\begin{equation}
{\bf Q = A \, L(t) \, A^T}
\end{equation}

Thus $L_{x'}(t),L_{y'}(t),L_{z'}(t)$ are just the eigenvalues of ${\bf Q}$ and the elements of the rotation matrix ${\bf A}$ are the eigenvectors. Once $L_{x'},L_{y'},L_{z'}$ have been recovered at all times, the curves can be compared against the known primary polarizabilities of objects which are stored in a library.

### Practical Considerations

**Sampling Density:** The optimum line and station spacing depends significantly on the dimensions of the target, its depth and the system being used to perform the survey. It is important to use a sampling density which accurately characterizes TEM anomalies without adding unnecessary time and expense.

**Excitation Orientation:** The excitation of a buried target occurs parallel to the inducing field. Thus in order to accurately recover polarizations $L_{x′},L_{y′}$ and $L_{z′}$ for the target, we must excite the target significantly from multiple angles. Ideally, the target would be excited from 3 orthogonal directions; thus assuring the data contains significant contributions from each polarization.

# Orientation and Polarization Widget

### Purpose

This app allows the user to visualize the orientation, approximate dimensions and polarizability of compact objects they wish to model with subsequent apps.

### Parameter Descriptions

* $\Phi$: Clockwise rotation about the z-axis
* $\theta$: Azimuthal angle (angle from vertical)
* $\phi$: Declination angle (Clockwise angle from North)
* $k_i,\alpha_i,\beta_i,\gamma_i$: Parameters which characterize the polarization along axis $i$

```python
# NOTE: INITIATE WIDGET BY ADJUSTING ANY PARAMETER!!!

Out1 = UXO.ImageUXOWidget()
display(HBox(Out1.children[0:3]))
display(HBox(Out1.children[3:7]))
display(HBox(Out1.children[7:11]))
display(HBox(Out1.children[11:15]))
display(Out1.children[15])
Out1.out
```

# Data Visualization Widget

### Purpose

This widget allows the user to visualize the time-domain response using three commonly used instruments: EM61, TEMTADS, and MPV. On the leftmost plot, the TEM anomaly at the center of the transmitter loop is plotted at a specified time. On the rightmost plot, the TEM decays registered by all receiver coils for a particular transmitter loop are plotted.

### Parameter Descriptions

* TxType: Instrument used to predict data. Set as "EM61", "TEMTADS" or "MPV"
* $x_{true},y_{true},z_{true}$: Location of the object
* $\psi,\theta,\phi$: Angles defining the orientation of the object
* $k_i,\alpha_i,\beta_i,\gamma_i$: Parameters which characterize the polarization along axis $i$
* Time channel: Adjusts the time in which the TEM anomaly at the center of the transmitter loop is plotted
* X location, Y location: The transmitter location at which you would like to see all decays measured by the receiver coils.

```python
# NOTE: INITIATE WIDGET BY ADJUSTING ANY PARAMETER!!!

TxType = "EM61" # Set TxType to "EM61", "TEMTADS" or "MPV"

Out2 = UXO.ImageDataWidget(TxType)
display(HBox(Out2.children[0:3]))
display(HBox(Out2.children[3:6]))
display(HBox(Out2.children[6:10]))
display(HBox(Out2.children[10:14]))
display(HBox(Out2.children[14:18]))
display(HBox(Out2.children[18:21]))
if TxType is "MPV":
    display(Out2.children[21])
Out2.out
```

# Parameter Estimation Widget

### Purpose

This widget allows the user to invert synthetic data using EM61, TEMTADS or MPV instruments in order to recover the location and primary polarizabilities for a compact object. The goal of this app is to demonstrate how successful recovery depends on:

* Sampling density
* Excitation orientation

### Parameter Descriptions

* TxType: Instrument used for simulation. Set as "EM61", "TEMTADS" or "MPV"
* $x_{true},y_{true},z_{true}$: True location of the object
* $\psi,\theta,\phi$: True angles defining the orientation of the object
* $k_i,\alpha_i,\beta_i,\gamma_i$: True parameters which characterize the polarization of the object along axis $i$
* $D_x,D_y$: The x-width and y-width for the cued-interrogation region
* $N_x,N_y$: The number of stations in the x and y direction
* $x_0,y_0,z_0$: Starting guess for the location of the object

```python
# NOTE: INITIATE WIDGET BY ADJUSTING ANY PARAMETER!!!

TxType = "EM61" # Set TxType to "EM61", "TEMTADS" or "MPV"

Out3 = UXO.InversionWidget(TxType)
display(HBox(Out3.children[0:3]))
display(HBox(Out3.children[3:6]))
display(HBox(Out3.children[6:10]))
display(HBox(Out3.children[10:14]))
display(HBox(Out3.children[14:18]))
display(HBox(Out3.children[18:22]))
display(HBox(Out3.children[22:25]))
if TxType is "MPV":
    display(HBox(Out3.children[25:27]))
else:
    display(Out3.children[25])
Out3.out
```

```python

```