# Anomaly Detection with LSTM in Keras

Predict Anomalies using Confidence Intervals
https://towardsdatascience.com/anomaly-detection-with-lstm-in-keras-8d8d7e50ab1b

Detection of anomaly is useful in every business and the difficultness to detect these observations depends on the field of applications. If you are engaged in a problem of anomaly detection, which involves human activities (like prediction of sales or demand), you can take advantages from fundamental assumptions of human behaviors and plan a more efficient solution.
We try to predict the Taxi demand in NYC in a critical time period. We formulate easy and important assumptions about human behaviors, which will permit us to detect an easy solution to forecast anomalies. All the dirty job is made by a loyalty LSTM, developed in Keras, which makes predictions and detection of anomalies at the same time!

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import re
import os
import tqdm
import random
import datetime
from sklearn.metrics import mean_squared_log_error

import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
import tensorflow.keras.backend as K

import statsmodels.api as sm
import statsmodels.formula.api as smf

%matplotlib inline
```

## THE DATASET

I took the dataset for our analysis from [Numenta](https://numenta.org/) community. In particular I chose the [NYC Taxi Dataset](https://github.com/numenta/NAB/blob/master/data/realKnownCause/nyc_taxi.csv). This dataset shows the NYC taxi demand from 2014–07–01 to 2015–01–31 with an observation every half hour.

```python
### READ DATA
df = pd.read_csv('../dataset/nyc_taxi.csv',index_col='timestamp')

# Converting the index as date
df.index = pd.to_datetime(df.index)

### CREATE FEATURES FOR year, month, day, hour ###
df['yr'] = df.index.year
df['mt'] = df.index.month
df['d'] = df.index.day
df['H'] = df.index.hour

print(df.shape)
df.head()
```

    (10320, 5)

      value
      yr
      mt
      d
      H

      timestamp

      2014-07-01 00:00:00
      10844
      2014
      7
      1
      0

      2014-07-01 00:30:00
      8127
      2014
      7
      1
      0

      2014-07-01 01:00:00
      6210
      2014
      7
      1
      1

      2014-07-01 01:30:00
      4656
      2014
      7
      1
      1

      2014-07-01 02:00:00
      3820
      2014
      7
      1
      2

In this period 5 anomalies are present, in terms of deviation from normal behavior. They occur respectively during the
- NYC marathon,
- Thanksgiving,
- Christmas,
- New Year’s day, and a
- snowstorm.

### Exemple of Weekly NORMAL observations

```python
### PLOT SAMPLE OF DATA ###
df.iloc[4000:4000+7*48,:].value.plot(title='NORMAL observations');
```

### NYC Marathon

```python
### PLOT SAMPLE OF DATA ###
df.iloc[4000+37*48:4000+37*48+7*48,:].value.plot(title='NYC Marathon anomality');
```

### Christmas

```python
### PLOT SAMPLE OF DATA ###
df.iloc[8400:8400+7*48,:].value.plot(title='Christmas anomality');
```

### Anormality (outlier) detection:
The objective is to learn what "normal" data look like, and then use that to detect abnormal instances, such as new trends in the time series.

Our purpose is to detect these abnormal observetions in advance!

The first consideration we noticed, looking at the data, is the presence of an obvious daily pattern (during the day the demand is higher than night hours). The taxi demand seems to be driven also by a weekly trend: in certain days of the week the taxi demand is higher than others. We simply prove this computing autocorrelation.

```python
### WEEKLY AUTOCORR PLOT (10 WEEKS DEPTH) ###
timeLags = np.arange(1,10*48*7)
autoCorr = [df.value.autocorr(lag=dt) for dt in timeLags]

plt.plot(1.0/(48*7)*timeLags, autoCorr);
plt.xlabel('time lag [weeks]'); plt.ylabel('correlation coeff', fontsize=12);
plt.title('AutoCorrelation 10 weeks depth')
plt.show()
```

What we can do now is to take note of this important behaviours for our further analysis. I compute and store the means for every days of the weeks at every hours. This will be useful when we’ll standardized the data to build our model in order to reduce every kind of temporal dependency (I compute the means for the first 5000 observations that will become our future train set).

## THE MODEL

We need a strategy to detect outliers in advance. To do this, we decided to care about taxi demand predictions. We want to develop a model which is able to forecast demand taking into account uncertainty. One way to do this is to develop [quantile regression](https://en.wikipedia.org/wiki/Quantile_regression).

### Quantile Regression

Let’s examine the python [statsmodels example for QuantReg](https://www.statsmodels.org/dev/examples/notebooks/generated/quantile_regression.html), which takes a look at the relationship between income and expenditures on food for a sample of working class Belgian households in 1857 [Engel], and see what kind of statistical analysis we can do.

We first need to load some modules and to retrieve the data. Conveniently, the Engel dataset is shipped with statsmodels.

```python
data = sm.datasets.engel.load_pandas().data
data.head()
```

      income
      foodexp

      0
      420.157651
      255.839425

      1
      541.411707
      310.958667

      2
      901.157457
      485.680014

      3
      639.080229
      402.997356

      4
      750.875606
      495.560775

```python
data.plot.scatter(x='income',y='foodexp');
```

#### Least Absolute Deviation (LAD)

The LAD model is a special case of quantile regression where $q=0.5$.

```python
mod = smf.quantreg('foodexp ~ income', data)
res = mod.fit(q=.5)
print(res.summary())
print('Parameters: ', res.params)
print('R2: ', res.rsquared)

# plot
x = np.arange(data.income.min(), data.income.max(), 50)
get_y = lambda a, b: a + b * x

y = get_y(res.params['Intercept'], res.params['income'])

fig, ax = plt.subplots(figsize=(8, 6))
ax.plot(x, y, color='red', label='LAD $p=0.5$')
ax.legend()
data.plot.scatter(x='income',y='foodexp',ax=ax);
plt.show()
```

We estimate the quantile regression model for many quantiles between .05 and .95, and compare best fit line from each of these models to Ordinary Least Squares results.

```python
quantiles = np.arange(.05, .96, .15)
def fit_model(q):
    res = mod.fit(q=q)
    return [q, res.params['Intercept'], res.params['income']] + \
            res.conf_int().loc['income'].tolist()

models = [fit_model(x) for x in quantiles]
models = pd.DataFrame(models, columns=['q', 'a', 'b', 'lb', 'ub'])
models = models.set_index('q')
print(models)

# plot
x = np.arange(data.income.min(), data.income.max(), 50)
get_y = lambda a, b: a + b * x

fig, ax = plt.subplots(figsize=(8, 6))

for i in quantiles:
    y = get_y(models.a[i], models.b[i])
    ax.plot(x, y, linestyle='dotted', label='LAD $q={:.2f}$'.format(i))

ax.legend()
data.plot.scatter(x='income',y='foodexp',ax=ax);
plt.show()
```

#### Ordinary Least Squares (OLS)

[OLS](https://en.wikipedia.org/wiki/Ordinary_least_squares) is a type of linear least squares method for estimating the unknown parameters in a linear regression model.

**Principle of least squares**: minimizing the sum of the squares of the differences between the observed dependent variable $Y$ and those predicted by the linear function of the independent variable $X$.

```python
mod=smf.ols('foodexp ~ income', data)
res=mod.fit()
print(res.summary())
print('Parameters: ', res.params)
print('Limits: ', res.conf_int().loc['income'].tolist())
print('R2: ', res.rsquared)

# plot
x = np.arange(data.income.min(), data.income.max(), 50)
get_y = lambda a, b: a + b * x

fig, ax = plt.subplots(figsize=(8, 6))
for i in quantiles:
    y = get_y(models.a[i], models.b[i])
    ax.plot(x, y, linestyle='dotted', label='LAD $q={:.2f}$'.format(i))

y = get_y(res.params['Intercept'], res.params['income'])

ax.plot(x, y, color='red', label='OLS')
ax.legend()
data.plot.scatter(x='income',y='foodexp',ax=ax);
plt.show()
```

**note**: LAD for $q=0.5$ and OLS are not the same. The first can be interpreted as the median and the second as the mean.

```python
n = models.shape[0]
p1 = plt.plot(models.index, models.b, color='black', label='Quantile Reg.')
p2 = plt.plot(models.index, models.ub, linestyle='dotted', color='black')
p3 = plt.plot(models.index, models.lb, linestyle='dotted', color='black')
p4 = plt.plot(models.index, [res.params['income']] * n, color='red', label='OLS')
p5 = plt.plot(models.index, [res.conf_int().loc['income'].tolist()[0]] * n, linestyle='dotted', color='red')
p6 = plt.plot(models.index, [res.conf_int().loc['income'].tolist()[1]] * n, linestyle='dotted', color='red')
plt.ylabel(r'$\beta_{income}$')
plt.xlabel('Quantiles of the conditional food expenditure distribution')
plt.legend()
plt.show()
```

The dotted black lines form 95% point-wise confidence band around 10 quantile regression estimates (solid black line). The red lines represent OLS regression results along with their 95% confidence interval.

In most cases, the quantile regression point estimates lie outside the OLS confidence interval, which suggests that the effect of income on food expenditure may not be constant across the distribution.

#### [Deep Quantile Regression](https://github.com/sachinruk/KerasQuantileModel/blob/master/Keras%20Quantile%20Model.ipynb)

One area that Deep Learning has not explored extensively is the uncertainty in estimates. However, as far as decision making goes, most people actually require quantiles as opposed to true uncertainty in an estimate. eg. For a given age the weight of an individual will vary. What would be interesting is the (for arguments sake) the 10th and 90th percentile. The uncertainty of the estimate of an individuals weight is less interesting.

Standardise the inputs and outputs so that it is easier to train. I have't saved the mean and standard deviations, but you should.

```python
data['income_st']= (data.income - data.income.mean())/data.income.std()
data['foodexp_st']= (data.foodexp - data.foodexp.mean())/data.foodexp.std()
data
```

      income
      foodexp
      income_st
      foodexp_st

      0
      420.157651
      255.839425
      -1.082978
      -1.332253

      1
      541.411707
      310.958667
      -0.849451
      -1.132876

      2
      901.157457
      485.680014
      -0.156608
      -0.500874

      3
      639.080229
      402.997356
      -0.661349
      -0.799954

      4
      750.875606
      495.560775
      -0.446039
      -0.465133

      ...
      ...
      ...
      ...
      ...

      230
      440.517424
      306.519079
      -1.043766
      -1.148935

      231
      541.200597
      299.199328
      -0.849858
      -1.175412

      232
      581.359892
      468.000798
      -0.772514
      -0.564823

      233
      743.077243
      522.601906
      -0.461058
      -0.367320

      234
      1057.676711
      750.320163
      0.144837
      0.456382

235 rows × 4 columns

```python
model = Sequential()
model.add(Dense(units=10, input_dim=1,activation='relu'))
model.add(Dense(units=10, input_dim=1,activation='relu'))
model.add(Dense(1))
model.compile(loss='mae', optimizer='adadelta')
model.fit(data.income_st.values, data.foodexp_st.values, epochs=50000, batch_size=32, verbose=0)
model.evaluate(data.income_st.values, data.foodexp_st.values)
```

    8/8 [==============================] - 0s 4ms/step - loss: 0.2606

    0.2605687081813812

```python
t_test = np.linspace(data.income_st.min(),data.income_st.max(),200)
y_test = model.predict(t_test)

plt.scatter(data.income_st,data.foodexp_st)
plt.plot(t_test, y_test,'r')
plt.show()
```

##### Quantile Regression Loss function

In regression the most commonly used loss function is the mean squared error function. If we were to take the negative of this loss and exponentiate it, the result would correspond to the gaussian distribution. The mode of this distribution (the peak) corresponds to the mean parameter. Hence, when we predict using a neural net that minimised this loss we are predicting the mean value of the output which may have been noisy in the training set.

The loss in Quantile Regression for an individual data point is defined as:

$$
\begin{align}
\mathcal{L}(\xi_i|\alpha)=\begin{cases}
\alpha \xi_i &\text{if }\xi_i\ge 0, \\
(\alpha-1) \xi_i &\text{if }\xi_i<0.
\end{cases}
\end{align}
$$

where $\alpha$ is the required quantile (a value between 0 and 1) and $\xi_i = y_i - f(\mathbf{x}_i)$ and, $f(\mathbf{x}_i)$ is the predicted (quantile) model and $y$ is the observed value for the corresponding input $x$. The final overall loss is defines as:

$$
\mathcal{L}(\mathbf{y},\mathbf{f}|\alpha)=\frac{1}{N} \sum_{i=1}^N \mathcal{L}(y_i-f(\mathbf{x}_i)|\alpha)
$$

If we were to take the negative of the individual loss and exponentiate it, we get the distribution know as the Asymmetric Laplace distribution, shown below. The reason that this loss function works is that if we were to find the area under the graph to the left of zero it would be alpha, the required quantile.

https://miro.medium.com/max/700/1*gUrYM90-7NNKwYO6-ONc5A.png

The following function defines the loss function for a quantile model.
Note: The following 4 lines is ALL that you change in comparison to a normal Deep Learning method, i.e. The loss function is all that changes.

```python
def tilted_loss(q,y,f):
    e = (y-f)
    return K.mean(K.maximum(q*e, (q-1)*e), axis=-1)
```

```python
def engelModel():
    model = Sequential()
    model.add(Dense(units=10, input_dim=1,activation='relu'))
    model.add(Dense(units=10, input_dim=1,activation='relu'))
    model.add(Dense(1))

    return model
```

```python
qs = [0.1, 0.5, 0.9]

t_test = np.linspace(data.income_st.min(),data.income_st.max(),200)
plt.scatter(data.income_st,data.foodexp_st)

for q in qs:
    model = engelModel()
    model.compile(loss=lambda y,f: tilted_loss(q,y,f), optimizer='adadelta')
    model.fit(data.income_st.values, data.foodexp_st.values, epochs=50000, batch_size=32, verbose=0)

    # Predict the quantile
    y_test = model.predict(t_test)
    plt.plot(t_test, y_test, label=q) # plot out this quantile

plt.legend()
plt.show()

```

##### Final Notes

1. Note that the quantile 0.5 is the same as median, which you can attain by minimising Mean Absolute Error, which you can attain in Keras regardless with loss='mae'.
2. Uncertainty and quantiles are not the same thing. But most of the time you care about quantiles and not uncertainty.
3. If you really do want uncertainty with deep nets checkout http://mlg.eng.cam.ac.uk/yarin/blog_3d801aa532c1ce.html

*** back to the outlyer detection Model ***

We focus on predictions of extreme values: lower (10th quantile), upper (90th quantile) and the classical 50th quantile. Computing also the 90th and 10th quantile we cover the most likely values the reality can assume. The width of this range can be very depth; we know that it is small when our model is sure about the future and it can be huge when our model isn’t able to see important changes in the domain of interest. We took advantage from this behaviour and let our model says something about outliers detection in the field of taxi demand preditcion. We are expecting to get a tiny interval (90–10 quantile range) when our model is sure about the future because it has all under control; on the other hand we are expecting to get an anomaly when the interval becomes bigger. This is possible because our model isn’t trained to handle this kind of scenario which can results in anomaly.

We make all this magic reality building a simple LSTM Neural Network in Keras. Our model will receive as input the past observations. We resize our data for feeding our LSTM with daily window size (48 observations: one observation for every half hour). When we were generating data, as I cited above, we operated logarithmic transformation and standardization subtracting the mean daily hour values, in order to see an observation as the logarithmic variation from its daily mean hour value. We build our target variables in the same way with half hour shifting (we want to predict the demand values for the next thirty minutes).

```python
### CREATE WEEKDAY FEATURE AND COMPUTE THE MEAN FOR WEEKDAYS AT EVERY HOURS ###

df['weekday'] = df.index.weekday
df['weekday_hour'] = df.weekday.astype(str) +' '+ df.H.astype(str)
df['m_weekday'] = df.weekday_hour.replace(df[:5000].groupby('weekday_hour')['value'].mean().to_dict())
df
```

      value
      yr
      mt
      d
      H
      weekday
      weekday_hour
      m_weekday

      timestamp

      2014-07-01 00:00:00
      10844
      2014
      7
      1
      0
      1
      1 0
      8774.433333

      2014-07-01 00:30:00
      8127
      2014
      7
      1
      0
      1
      1 0
      8774.433333

      2014-07-01 01:00:00
      6210
      2014
      7
      1
      1
      1
      1 1
      5242.933333

      2014-07-01 01:30:00
      4656
      2014
      7
      1
      1
      1
      1 1
      5242.933333

      2014-07-01 02:00:00
      3820
      2014
      7
      1
      2
      1
      1 2
      3170.433333

      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...
      ...

      2015-01-31 21:30:00
      24670
      2015
      1
      31
      21
      5
      5 21
      21298.033333

      2015-01-31 22:00:00
      25721
      2015
      1
      31
      22
      5
      5 22
      23126.666667

      2015-01-31 22:30:00
      27309
      2015
      1
      31
      22
      5
      5 22
      23126.666667

      2015-01-31 23:00:00
      26591
      2015
      1
      31
      23
      5
      5 23
      24726.433333

      2015-01-31 23:30:00
      26288
      2015
      1
      31
      23
      5
      5 23
      24726.433333

10320 rows × 8 columns

```python
### CREATE GENERATOR FOR LSTM ###
sequence_length = 48

def gen_index(id_df, seq_length, seq_cols):

    data_matrix =  id_df[seq_cols]
    num_elements = data_matrix.shape[0]

    for start, stop in zip(range(0, num_elements-seq_length, 1), range(seq_length, num_elements, 1)):

        yield data_matrix[stop-sequence_length:stop].values.reshape((-1,len(seq_cols)))
```

```python
### CREATE AND STANDARDIZE DATA FOR LSTM ###
cnt, mean = [], []
for sequence in gen_index(df, sequence_length, ['value']):
    cnt.append(sequence)

for sequence in gen_index(df, sequence_length, ['m_weekday']):
    mean.append(sequence)

cnt, mean = np.log(cnt), np.log(mean)
cnt = cnt - mean
cnt.shape
```

    (10272, 48, 1)

```python
### CREATE AND STANDARDIZE LABEL FOR LSTM ###
init = df.m_weekday[sequence_length:].apply(np.log).values
label = df.value[sequence_length:].apply(np.log).values - init
label.shape
```

    (10272,)

Operate quantile regression in Keras is very simple (I took inspiration from [this post](https://towardsdatascience.com/deep-quantile-regression-c85481548b5a)). We easily define the custom quantile loss function which penalizes errors based on the quantile and whether the error was positive (actual > predicted) or negative (actual < predicted). Our network has 3 outputs and 3 losses, one for every quantile we try to predict.

```python
### DEFINE QUANTILE LOSS ###
def q_loss(q,y,f):
    e = (y-f)
    return K.mean(K.maximum(q*e, (q-1)*e), axis=-1)
```

```python
### TRAIN TEST SPLIT ###
X_train, X_test = cnt[:5000], cnt[5000:]
y_train, y_test = label[:5000], label[5000:]
train_date, test_date = df.index.values[sequence_length:5000+sequence_length], df.index.values[5000+sequence_length:]
```

```python
tf.random.set_seed(33)
os.environ['PYTHONHASHSEED'] = str(33)
np.random.seed(33)
random.seed(33)

session_conf = tf.compat.v1.ConfigProto(
    intra_op_parallelism_threads=1,
    inter_op_parallelism_threads=1
)
sess = tf.compat.v1.Session(
    graph=tf.compat.v1.get_default_graph(),
    config=session_conf
)
tf.compat.v1.keras.backend.set_session(sess)

### CREATE MODEL ###
losses = [lambda y,f: q_loss(0.1,y,f), lambda y,f: q_loss(0.5,y,f), lambda y,f: q_loss(0.9,y,f)]

inputs = Input(shape=(X_train.shape[1], X_train.shape[2]))
lstm = Bidirectional(LSTM(64, return_sequences=True, dropout=0.3))(inputs, training = True)
lstm = Bidirectional(LSTM(16, return_sequences=False, dropout=0.3))(lstm, training = True)
dense = Dense(50)(lstm)
out10 = Dense(1)(dense)
out50 = Dense(1)(dense)
out90 = Dense(1)(dense)
model = Model(inputs, [out10,out50,out90])

model.compile(loss=losses, optimizer='adam', loss_weights = [0.3,0.3,0.3])
model.fit(X_train, [y_train,y_train,y_train], epochs=50, batch_size=128, verbose=2)
```

    Epoch 1/50
    WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x14cd1a680> and will run it as-is.
    Cause: could not parse the source code of <function <lambda> at 0x14cd1a680>: found multiple definitions with identical signatures at the location. This error may be avoided by defining each lambda on a single line and with unique argument names.
    Match 0:
    (lambda y, f: q_loss(0.1, y, f))

    Match 1:
    (lambda y, f: q_loss(0.5, y, f))

    Match 2:
    (lambda y, f: q_loss(0.9, y, f))

    To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
    WARNING: AutoGraph could not transform <function <lambda> at 0x14cd1a680> and will run it as-is.
    Cause: could not parse the source code of <function <lambda> at 0x14cd1a680>: found multiple definitions with identical signatures at the location. This error may be avoided by defining each lambda on a single line and with unique argument names.
    Match 0:
    (lambda y, f: q_loss(0.1, y, f))

    Match 1:
    (lambda y, f: q_loss(0.5, y, f))

    Match 2:
    (lambda y, f: q_loss(0.9, y, f))

    To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
    WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x14cd1a290> and will run it as-is.
    Cause: could not parse the source code of <function <lambda> at 0x14cd1a290>: found multiple definitions with identical signatures at the location. This error may be avoided by defining each lambda on a single line and with unique argument names.
    Match 0:
    (lambda y, f: q_loss(0.1, y, f))

    Match 1:
    (lambda y, f: q_loss(0.5, y, f))

    Match 2:
    (lambda y, f: q_loss(0.9, y, f))

    To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
    WARNING: AutoGraph could not transform <function <lambda> at 0x14cd1a290> and will run it as-is.
    Cause: could not parse the source code of <function <lambda> at 0x14cd1a290>: found multiple definitions with identical signatures at the location. This error may be avoided by defining each lambda on a single line and with unique argument names.
    Match 0:
    (lambda y, f: q_loss(0.1, y, f))

    Match 1:
    (lambda y, f: q_loss(0.5, y, f))

    Match 2:
    (lambda y, f: q_loss(0.9, y, f))

    To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
    WARNING:tensorflow:AutoGraph could not transform <function <lambda> at 0x14cb12c20> and will run it as-is.
    Cause: could not parse the source code of <function <lambda> at 0x14cb12c20>: found multiple definitions with identical signatures at the location. This error may be avoided by defining each lambda on a single line and with unique argument names.
    Match 0:
    (lambda y, f: q_loss(0.1, y, f))

    Match 1:
    (lambda y, f: q_loss(0.5, y, f))

    Match 2:
    (lambda y, f: q_loss(0.9, y, f))

    To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
    WARNING: AutoGraph could not transform <function <lambda> at 0x14cb12c20> and will run it as-is.
    Cause: could not parse the source code of <function <lambda> at 0x14cb12c20>: found multiple definitions with identical signatures at the location. This error may be avoided by defining each lambda on a single line and with unique argument names.
    Match 0:
    (lambda y, f: q_loss(0.1, y, f))

    Match 1:
    (lambda y, f: q_loss(0.5, y, f))

    Match 2:
    (lambda y, f: q_loss(0.9, y, f))

    To silence this warning, decorate the function with @tf.autograph.experimental.do_not_convert
    40/40 - 8s - loss: 0.0285 - dense_13_loss: 0.0274 - dense_14_loss: 0.0438 - dense_15_loss: 0.0238
    Epoch 2/50
    40/40 - 3s - loss: 0.0259 - dense_13_loss: 0.0233 - dense_14_loss: 0.0413 - dense_15_loss: 0.0216
    Epoch 3/50
    40/40 - 3s - loss: 0.0252 - dense_13_loss: 0.0223 - dense_14_loss: 0.0405 - dense_15_loss: 0.0213
    Epoch 4/50
    40/40 - 3s - loss: 0.0248 - dense_13_loss: 0.0221 - dense_14_loss: 0.0398 - dense_15_loss: 0.0207
    Epoch 5/50
    40/40 - 3s - loss: 0.0241 - dense_13_loss: 0.0210 - dense_14_loss: 0.0390 - dense_15_loss: 0.0205
    Epoch 6/50
    40/40 - 3s - loss: 0.0235 - dense_13_loss: 0.0203 - dense_14_loss: 0.0383 - dense_15_loss: 0.0198
    Epoch 7/50
    40/40 - 3s - loss: 0.0233 - dense_13_loss: 0.0200 - dense_14_loss: 0.0379 - dense_15_loss: 0.0198
    Epoch 8/50
    40/40 - 3s - loss: 0.0224 - dense_13_loss: 0.0191 - dense_14_loss: 0.0368 - dense_15_loss: 0.0189
    Epoch 9/50
    40/40 - 3s - loss: 0.0219 - dense_13_loss: 0.0186 - dense_14_loss: 0.0360 - dense_15_loss: 0.0184
    Epoch 10/50
    40/40 - 3s - loss: 0.0211 - dense_13_loss: 0.0177 - dense_14_loss: 0.0347 - dense_15_loss: 0.0180
    Epoch 11/50
    40/40 - 3s - loss: 0.0207 - dense_13_loss: 0.0176 - dense_14_loss: 0.0340 - dense_15_loss: 0.0175
    Epoch 12/50
    40/40 - 3s - loss: 0.0205 - dense_13_loss: 0.0176 - dense_14_loss: 0.0336 - dense_15_loss: 0.0173
    Epoch 13/50
    40/40 - 3s - loss: 0.0199 - dense_13_loss: 0.0171 - dense_14_loss: 0.0327 - dense_15_loss: 0.0165
    Epoch 14/50
    40/40 - 3s - loss: 0.0198 - dense_13_loss: 0.0170 - dense_14_loss: 0.0326 - dense_15_loss: 0.0163
    Epoch 15/50
    40/40 - 3s - loss: 0.0195 - dense_13_loss: 0.0169 - dense_14_loss: 0.0320 - dense_15_loss: 0.0161
    Epoch 16/50
    40/40 - 3s - loss: 0.0194 - dense_13_loss: 0.0164 - dense_14_loss: 0.0322 - dense_15_loss: 0.0162
    Epoch 17/50
    40/40 - 3s - loss: 0.0189 - dense_13_loss: 0.0163 - dense_14_loss: 0.0312 - dense_15_loss: 0.0155
    Epoch 18/50
    40/40 - 3s - loss: 0.0189 - dense_13_loss: 0.0161 - dense_14_loss: 0.0312 - dense_15_loss: 0.0157
    Epoch 19/50
    40/40 - 3s - loss: 0.0188 - dense_13_loss: 0.0160 - dense_14_loss: 0.0312 - dense_15_loss: 0.0153
    Epoch 20/50
    40/40 - 3s - loss: 0.0184 - dense_13_loss: 0.0157 - dense_14_loss: 0.0308 - dense_15_loss: 0.0150
    Epoch 21/50
    40/40 - 3s - loss: 0.0184 - dense_13_loss: 0.0155 - dense_14_loss: 0.0310 - dense_15_loss: 0.0147
    Epoch 22/50
    40/40 - 3s - loss: 0.0182 - dense_13_loss: 0.0152 - dense_14_loss: 0.0305 - dense_15_loss: 0.0149
    Epoch 23/50
    40/40 - 3s - loss: 0.0178 - dense_13_loss: 0.0152 - dense_14_loss: 0.0299 - dense_15_loss: 0.0142
    Epoch 24/50
    40/40 - 3s - loss: 0.0177 - dense_13_loss: 0.0151 - dense_14_loss: 0.0299 - dense_15_loss: 0.0140
    Epoch 25/50
    40/40 - 3s - loss: 0.0174 - dense_13_loss: 0.0148 - dense_14_loss: 0.0293 - dense_15_loss: 0.0140
    Epoch 26/50
    40/40 - 3s - loss: 0.0180 - dense_13_loss: 0.0151 - dense_14_loss: 0.0305 - dense_15_loss: 0.0145
    Epoch 27/50
    40/40 - 3s - loss: 0.0174 - dense_13_loss: 0.0147 - dense_14_loss: 0.0294 - dense_15_loss: 0.0140
    Epoch 28/50
    40/40 - 3s - loss: 0.0173 - dense_13_loss: 0.0147 - dense_14_loss: 0.0293 - dense_15_loss: 0.0136
    Epoch 29/50
    40/40 - 3s - loss: 0.0172 - dense_13_loss: 0.0145 - dense_14_loss: 0.0292 - dense_15_loss: 0.0135
    Epoch 30/50
    40/40 - 3s - loss: 0.0171 - dense_13_loss: 0.0143 - dense_14_loss: 0.0294 - dense_15_loss: 0.0135
    Epoch 31/50
    40/40 - 3s - loss: 0.0170 - dense_13_loss: 0.0142 - dense_14_loss: 0.0288 - dense_15_loss: 0.0137
    Epoch 32/50
    40/40 - 3s - loss: 0.0171 - dense_13_loss: 0.0145 - dense_14_loss: 0.0291 - dense_15_loss: 0.0133
    Epoch 33/50
    40/40 - 3s - loss: 0.0171 - dense_13_loss: 0.0146 - dense_14_loss: 0.0293 - dense_15_loss: 0.0132
    Epoch 34/50
    40/40 - 3s - loss: 0.0167 - dense_13_loss: 0.0142 - dense_14_loss: 0.0285 - dense_15_loss: 0.0131
    Epoch 35/50
    40/40 - 3s - loss: 0.0169 - dense_13_loss: 0.0144 - dense_14_loss: 0.0287 - dense_15_loss: 0.0132
    Epoch 36/50
    40/40 - 3s - loss: 0.0169 - dense_13_loss: 0.0142 - dense_14_loss: 0.0288 - dense_15_loss: 0.0135
    Epoch 37/50
    40/40 - 3s - loss: 0.0168 - dense_13_loss: 0.0139 - dense_14_loss: 0.0287 - dense_15_loss: 0.0136
    Epoch 38/50
    40/40 - 3s - loss: 0.0170 - dense_13_loss: 0.0141 - dense_14_loss: 0.0288 - dense_15_loss: 0.0136
    Epoch 39/50
    40/40 - 3s - loss: 0.0169 - dense_13_loss: 0.0140 - dense_14_loss: 0.0288 - dense_15_loss: 0.0134
    Epoch 40/50
    40/40 - 3s - loss: 0.0167 - dense_13_loss: 0.0141 - dense_14_loss: 0.0283 - dense_15_loss: 0.0133
    Epoch 41/50
    40/40 - 3s - loss: 0.0171 - dense_13_loss: 0.0142 - dense_14_loss: 0.0296 - dense_15_loss: 0.0132
    Epoch 42/50
    40/40 - 3s - loss: 0.0166 - dense_13_loss: 0.0138 - dense_14_loss: 0.0284 - dense_15_loss: 0.0131
    Epoch 43/50
    40/40 - 3s - loss: 0.0167 - dense_13_loss: 0.0137 - dense_14_loss: 0.0286 - dense_15_loss: 0.0132
    Epoch 44/50
    40/40 - 3s - loss: 0.0167 - dense_13_loss: 0.0139 - dense_14_loss: 0.0284 - dense_15_loss: 0.0134
    Epoch 45/50
    40/40 - 3s - loss: 0.0169 - dense_13_loss: 0.0141 - dense_14_loss: 0.0289 - dense_15_loss: 0.0134
    Epoch 46/50
    40/40 - 3s - loss: 0.0169 - dense_13_loss: 0.0140 - dense_14_loss: 0.0288 - dense_15_loss: 0.0135
    Epoch 47/50
    40/40 - 3s - loss: 0.0165 - dense_13_loss: 0.0138 - dense_14_loss: 0.0284 - dense_15_loss: 0.0130
    Epoch 48/50
    40/40 - 3s - loss: 0.0167 - dense_13_loss: 0.0141 - dense_14_loss: 0.0286 - dense_15_loss: 0.0130
    Epoch 49/50
    40/40 - 3s - loss: 0.0166 - dense_13_loss: 0.0138 - dense_14_loss: 0.0285 - dense_15_loss: 0.0131
    Epoch 50/50
    40/40 - 3s - loss: 0.0168 - dense_13_loss: 0.0140 - dense_14_loss: 0.0286 - dense_15_loss: 0.0133

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

## CROSSOVER PROBLEM

When dealing with Neural Network in Keras, one of the tedious problem is the uncertainty of results due to the internal weigths initialization. With its formulation, our problem seems to particularly suffer of this kind of problem; i.e. computing quantile predictions we can’t permit quantiles overlapping, this not make sense! To avoid this pitfall I make use of bootstrapping in prediction phase: I reactivate dropout of my network (trainable: true in the model), iterate predition for 100 times, store them and finally calculate the desired quantiles (I make use of this clever technique also in this post).

```python
### QUANTILEs BOOTSTRAPPING ###
pred_10, pred_50, pred_90 = [], [], []

for i in tqdm.tqdm(range(0,100)):

    predd = model.predict(X_test)
    pred_10.append(predd[0])
    pred_50.append(predd[1])
    pred_90.append(predd[2])

pred_10 = np.asarray(pred_10)[:,:,0]
pred_50 = np.asarray(pred_50)[:,:,0]
pred_90 = np.asarray(pred_90)[:,:,0]
```

    100%|██████████| 100/100 [02:43<00:00,  1.64s/it]

This process is graphically explained below with a little focus on a subset of predictions. Given quantile bootstraps, we calculated summary measures (red lines) of them, avoiding crossover.

```python
### REVERSE TRANSFORM PREDICTIONS ###
pred_90_m = np.exp(np.quantile(pred_90,0.9,axis=0) + init[5000:])
pred_50_m = np.exp(pred_50.mean(axis=0) + init[5000:])
pred_10_m = np.exp(np.quantile(pred_10,0.1,axis=0) + init[5000:])
```

```python
### EVALUATION METRIC ###
mean_squared_log_error(np.exp(y_test + init[5000:]), pred_50_m)
```

    0.06907365954001285

## RESULTS

As I previously cited, I used the firstly 5000 observations for training and the remaining (around 5000) for testing.

Our model reaches a great performance forecasting taxi demand with the 50th quantile. Around 0.055 Mean Squared Log Error is a brilliant result! This means that the LSTM Network is able to understand the underling rules that drive taxi demand. So our approach for anomaly detection sounds great… We computed the difference among the 90th quantile predictions and 10th quantile predictions and see what’s appened.

```python
### PLOT QUANTILE PREDICTIONS ###
plt.figure(figsize=(16,8))
plt.plot(test_date, pred_90_m, color='cyan')
plt.plot(test_date, pred_50_m, color='blue')
plt.plot(test_date, pred_10_m, color='green')

### CROSSOVER CHECK ###
plt.scatter(np.where(np.logical_or(pred_50_m>pred_90_m, pred_50_m<pred_10_m))[0],
            pred_50_m[np.logical_or(pred_50_m>pred_90_m, pred_50_m<pred_10_m)], c='red', s=50)
```

The quantile interval range (blue dots) is higher in period of uncertainty. In the other cases, the model tends to generalize well, as we expected. Going deeper, we start to investigate about these periods of high uncertainty. We noticed that these coincide with our initial assumptions. The orange circles plotted below are respectively: NYC marathon, Thanksgiving, Christmas, New Years day, and a snow storm.

```python
### PLOT UNCERTAINTY INTERVAL LENGHT WITH REAL DATA ###
plt.figure(figsize=(16,8))
plt.plot(test_date, np.exp(y_test + init[5000:]), color='red', alpha=0.4)
plt.scatter(test_date, pred_90_m - pred_10_m)
```

We can conclude that we reach our initial targets: achive a great forecating power and exploit the strength of our model to identificate uncertainty. We also make use of this to say something about anomalies detection.

## SUMMARY

Wereproduce a good solution for anomaly detection and forecasting. We make use of a LSTM Network to learn the behaviour of taxi demand in NYC. We utilized what we learned to make predition and estimate uncertainty at the same time. We implicitly define an anomaly as an unpredictable observation — i.e. with a great amout of uncertainty. This simple assumption permits to our LSTM to make all the work for us.


Donatella Merlini

Massimo Nocentini

October 27, 2016: fix fetching error, refactoring

October 20, 2016: some Riordan patterns

Abstract

A notebook to support an ongoing work Algebraic generating functions for languages
avoiding Riordan patterns.

```python
from sympy import *

from IPython.display import Markdown, Latex
from oeis import oeis_search

init_printing()
```

```python
%run ~/Developer/working-copies/programming-contests/competitive-programming/python-libs/oeis.py
```

## defs

```python
j = symbols('j', positive=True)
t = symbols('t')

j_range = range(1, 10)
```

```python
def make_expander(gf, t, terms_in_expansion = 15):

    def worker(j_index):
        term = Subs(gf, j, j_index)
        return Eq(j, j_index), Eq(term, term.doit().series(t, n=terms_in_expansion))

    return worker

def coeffs(res, t, limit):
    return [res.rhs.coeff(t, n) for n in range(limit)]

def match_table(results, gf_col_width="12cm"):
    rows = []
    for j_eq, res in results:
        j, j_index = j_eq.lhs, j_eq.rhs
        terms_in_expansion = res.rhs.getn()
        searchable = oeis_search(seq=coeffs(res, t, limit=terms_in_expansion),
                                 only_possible_matchings=True, progress_indicator=None)
        row_src = searchable(term_src=latex(res.lhs))
        rows.append(row_src)

    header_row = r'<tr><th style="width:{width};" >gf</th><th>matches</th></tr>'.format(width=gf_col_width)
    return Markdown('<table style="width:100%">\n {header}\n {rows}\n </table>'.format(
            header=header_row, rows='\n'.join(rows)))

def coeffs_table(results):

    def coeffs_table_rows():

        rows = []
        for j_eq, res in results:
            j, j_index = j_eq.lhs, j_eq.rhs
            terms_in_expansion = res.rhs.getn()
            coefficients = coeffs(res, t, limit=terms_in_expansion)
            rows.append(latex(j_eq) + ' & ' + latex(coefficients))

        return rows

    return Latex(r'\begin{{array}}{{r|l}} {rows} \end{{array}}'.format(rows=r'\\'.join(coeffs_table_rows())))
```

## testing

```python
def fib(t):
    return t/(1-t-t**2)
```

```python
f = fib(t)
f
```

```python
s = f.series(t, n=10)
s
```

```python
s.getn()
```

```python
results = list(map(make_expander(fib(t), t), [1]))
```

```python
results
```

```python
coeffs_table(results)
```

\begin{array}{r|l} j = 1 & \left [ 0, \quad 1, \quad 1, \quad 2, \quad 3, \quad 5, \quad 8, \quad 13, \quad 21, \quad 34, \quad 55, \quad 89, \quad 144, \quad 233, \quad 377\right ] \end{array}

```python
match_table(results)
```

gf	matches

$$\left. \frac{t}{- t^{2} - t + 1} \right|_{\substack{ j=1 }}$$	A000045, A212804, A147316, A248740, A025086, A039834, A152163, A236191

## $\mathfrak{p}={1^{j+1}0^{j}}$

```python
def S(t):
    radix_term = sqrt(1-4*t+4*t**(j+1))
    return 2/(radix_term * (1 + radix_term))
```

```python
S(t)
```

```python
results = list(map(make_expander(S(t), t), j_range))
# results
```

```python
coeffs_table(results)
```

\begin{array}{r|l} j = 1 & \left [ 1, \quad 3, \quad 7, \quad 15, \quad 31, \quad 63, \quad 127, \quad 255, \quad 511, \quad 1023, \quad 2047, \quad 4095, \quad 8191, \quad 16383, \quad 32767\right ]\\j = 2 & \left [ 1, \quad 3, \quad 10, \quad 32, \quad 106, \quad 357, \quad 1222, \quad 4230, \quad 14770, \quad 51918, \quad 183472, \quad 651191, \quad 2319626, \quad 8288135, \quad 29691760\right ]\\j = 3 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 123, \quad 442, \quad 1611, \quad 5931, \quad 22010, \quad 82187, \quad 308427, \quad 1162218, \quad 4394603, \quad 16665771, \quad 63361962\right ]\\j = 4 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 459, \quad 1696, \quad 6330, \quad 23806, \quad 90068, \quad 342430, \quad 1307138, \quad 5006576, \quad 19231518, \quad 74057340\right ]\\j = 5 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1713, \quad 6415, \quad 24205, \quad 91874, \quad 350406, \quad 1341782, \quad 5155265, \quad 19863925, \quad 76728114\right ]\\j = 6 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6432, \quad 24290, \quad 92273, \quad 352212, \quad 1349768, \quad 5190004, \quad 20013255, \quad 77364290\right ]\\j = 7 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24307, \quad 92358, \quad 352611, \quad 1351574, \quad 5197990, \quad 20048004, \quad 77513715\right ]\\j = 8 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24310, \quad 92375, \quad 352696, \quad 1351973, \quad 5199796, \quad 20055990, \quad 77548464\right ]\\j = 9 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24310, \quad 92378, \quad 352713, \quad 1352058, \quad 5200195, \quad 20057796, \quad 77556450\right ] \end{array}

```python
match_table(results)
```

gf	matches

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=1 }}$$	A000225, A126646, A168604, A255047, A239678, A060152, A225883

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=2 }}$$	A261058

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=3 }}$$

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=4 }}$$

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=5 }}$$

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=6 }}$$

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=7 }}$$

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=8 }}$$

$$\left. \frac{2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=9 }}$$

## $\mathfrak{p}={0^{j+1}1^{j}}$

```python
def S(t):
    radix_term = sqrt(1-4*t+4*t**(j+1))
    return 2*(1-t**j)/(radix_term * (1 + radix_term))
```

```python
S(t)
```

```python
results = list(map(make_expander(S(t), t), j_range))
```

```python
coeffs_table(results)
```

\begin{array}{r|l} j = 1 & \left [ 1, \quad 2, \quad 4, \quad 8, \quad 16, \quad 32, \quad 64, \quad 128, \quad 256, \quad 512, \quad 1024, \quad 2048, \quad 4096, \quad 8192, \quad 16384\right ]\\j = 2 & \left [ 1, \quad 3, \quad 9, \quad 29, \quad 96, \quad 325, \quad 1116, \quad 3873, \quad 13548, \quad 47688, \quad 168702, \quad 599273, \quad 2136154, \quad 7636944, \quad 27372134\right ]\\j = 3 & \left [ 1, \quad 3, \quad 10, \quad 34, \quad 120, \quad 432, \quad 1576, \quad 5808, \quad 21568, \quad 80576, \quad 302496, \quad 1140208, \quad 4312416, \quad 16357344, \quad 62199744\right ]\\j = 4 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 125, \quad 456, \quad 1686, \quad 6295, \quad 23680, \quad 89609, \quad 340734, \quad 1300808, \quad 4982770, \quad 19141450, \quad 73714910\right ]\\j = 5 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 461, \quad 1710, \quad 6405, \quad 24170, \quad 91748, \quad 349944, \quad 1340069, \quad 5148850, \quad 19839720, \quad 76636240\right ]\\j = 6 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1715, \quad 6429, \quad 24280, \quad 92238, \quad 352086, \quad 1349306, \quad 5188288, \quad 20006823, \quad 77340000\right ]\\j = 7 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6434, \quad 24304, \quad 92348, \quad 352576, \quad 1351448, \quad 5197528, \quad 20046288, \quad 77507280\right ]\\j = 8 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24309, \quad 92372, \quad 352686, \quad 1351938, \quad 5199670, \quad 20055528, \quad 77546748\right ]\\j = 9 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24310, \quad 92377, \quad 352710, \quad 1352048, \quad 5200160, \quad 20057670, \quad 77555988\right ] \end{array}

```python
match_table(results)
```

gf	matches

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=1 }}$$	A000079, A011782, A131577, A034008, A118655, A247208, A138882, A166444, A139248, A220493

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=2 }}$$

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=3 }}$$

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=4 }}$$

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=5 }}$$

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=6 }}$$

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=7 }}$$

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=8 }}$$

$$\left. \frac{- 2 t^{j} + 2}{\left(\sqrt{- 4 t + 4 t^{j + 1} + 1} + 1\right) \sqrt{- 4 t + 4 t^{j + 1} + 1}} \right|_{\substack{ j=9 }}$$

## $\mathfrak{p}={1^{j}0^{j}}$

```python
def S(t):
    radix_term = sqrt(1-4*t+2*t**j+t**(2*j))
    return 2/(radix_term * (1 -t**j + radix_term))
```

```python
S(t)
```

```python
results = list(map(make_expander(S(t), t), j_range))
```

```python
coeffs_table(results)
```

\begin{array}{r|l} j = 1 & \left [ 1, \quad 2, \quad 3, \quad 4, \quad 5, \quad 6, \quad 7, \quad 8, \quad 9, \quad 10, \quad 11, \quad 12, \quad 13, \quad 14, \quad 15\right ]\\j = 2 & \left [ 1, \quad 3, \quad 9, \quad 27, \quad 82, \quad 253, \quad 791, \quad 2499, \quad 7960, \quad 25520, \quad 82248, \quad 266221, \quad 864818, \quad 2817963, \quad 9206339\right ]\\j = 3 & \left [ 1, \quad 3, \quad 10, \quad 34, \quad 118, \quad 417, \quad 1493, \quad 5400, \quad 19684, \quad 72196, \quad 266122, \quad 985003, \quad 3658441, \quad 13628032, \quad 50894650\right ]\\j = 4 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 125, \quad 454, \quad 1671, \quad 6211, \quad 23261, \quad 87641, \quad 331821, \quad 1261398, \quad 4811427, \quad 18405703, \quad 70585514\right ]\\j = 5 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 461, \quad 1708, \quad 6390, \quad 24086, \quad 91328, \quad 347965, \quad 1331072, \quad 5108906, \quad 19665318, \quad 75884110\right ]\\j = 6 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1715, \quad 6427, \quad 24265, \quad 92154, \quad 351666, \quad 1347326, \quad 5179280, \quad 19966795, \quad 77165064\right ]\\j = 7 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6434, \quad 24302, \quad 92333, \quad 352492, \quad 1351028, \quad 5195548, \quad 20037279, \quad 77467241\right ]\\j = 8 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24309, \quad 92370, \quad 352671, \quad 1351854, \quad 5199250, \quad 20053548, \quad 77537739\right ]\\j = 9 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24310, \quad 92377, \quad 352708, \quad 1352033, \quad 5200076, \quad 20057250, \quad 77554008\right ] \end{array}

```python
match_table(results)
```

gf	matches

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=1 }}$$	A000027, A001477, A028310, A067080, A038566, A053645, A194029, A023443, A053186, A271980

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=2 }}$$

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=3 }}$$

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=4 }}$$

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=5 }}$$

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=6 }}$$

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=7 }}$$

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=8 }}$$

$$\left. \frac{2}{\left(- t^{j} + \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1} + 1\right) \sqrt{- 4 t + t^{2 j} + 2 t^{j} + 1}} \right|_{\substack{ j=9 }}$$

## $\mathfrak{p}={(10)}^{j}1$

```python
def S(t):
    radix_term = sqrt(1-4*t+2*t**(j+1)+4*t**(j+2)-3*t**(2*j+2))
    return 2*(1-t**j)/(1-4*t**j+3*t**(j+1)+radix_term)
```

```python
S(t)
```

```python
results = list(map(make_expander(S(t), t), j_range))
```

```python
coeffs_table(results)
```

\begin{array}{r|l} j = 1 & \left [ 1, \quad 2, \quad 5, \quad 13, \quad 35, \quad 96, \quad 267, \quad 750, \quad 2123, \quad 6046, \quad 17303, \quad 49721, \quad 143365, \quad 414584, \quad 1201917\right ]\\j = 2 & \left [ 1, \quad 1, \quad 3, \quad 6, \quad 18, \quad 48, \quad 147, \quad 444, \quad 1407, \quad 4503, \quad 14718, \quad 48657, \quad 162831, \quad 549939, \quad 1872993\right ]\\j = 3 & \left [ 1, \quad 1, \quad 2, \quad 6, \quad 15, \quad 44, \quad 138, \quad 437, \quad 1433, \quad 4801, \quad 16330, \quad 56333, \quad 196547, \quad 692304, \quad 2458680\right ]\\j = 4 & \left [ 1, \quad 1, \quad 2, \quad 5, \quad 15, \quad 43, \quad 134, \quad 433, \quad 1438, \quad 4862, \quad 16723, \quad 58293, \quad 205478, \quad 731132, \quad 2622675\right ]\\j = 5 & \left [ 1, \quad 1, \quad 2, \quad 5, \quad 14, \quad 43, \quad 133, \quad 431, \quad 1434, \quad 4868, \quad 16796, \quad 58710, \quad 207511, \quad 740339, \quad 2662594\right ]\\j = 6 & \left [ 1, \quad 1, \quad 2, \quad 5, \quad 14, \quad 42, \quad 133, \quad 430, \quad 1432, \quad 4866, \quad 16802, \quad 58784, \quad 207936, \quad 742396, \quad 2671871\right ]\\j = 7 & \left [ 1, \quad 1, \quad 2, \quad 5, \quad 14, \quad 42, \quad 132, \quad 430, \quad 1431, \quad 4864, \quad 16800, \quad 58792, \quad 208010, \quad 742822, \quad 2673936\right ]\\j = 8 & \left [ 1, \quad 1, \quad 2, \quad 5, \quad 14, \quad 42, \quad 132, \quad 429, \quad 1431, \quad 4863, \quad 16798, \quad 58790, \quad 208018, \quad 742898, \quad 2674362\right ]\\j = 9 & \left [ 1, \quad 1, \quad 2, \quad 5, \quad 14, \quad 42, \quad 132, \quad 429, \quad 1430, \quad 4863, \quad 16797, \quad 58788, \quad 208016, \quad 742906, \quad 2674438\right ] \end{array}

```python
match_table(results)
```

gf	matches

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=1 }}$$	A005773

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=2 }}$$

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=3 }}$$

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=4 }}$$

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=5 }}$$

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=6 }}$$

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=7 }}$$

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=8 }}$$

$$\left. \frac{- 2 t^{j} + 2}{- 4 t^{j} + 3 t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1} \right|_{\substack{ j=9 }}$$

## $\mathfrak{p}={(01)}^{j}0$

```python
def S(t):
    radix_term = sqrt(1-4*t+2*t**(j+1)+4*t**(j+2)-3*t**(2*j+2))
    return 2*(1-t**j-t**(j+1)+t**(2*j+1))/(radix_term * (1 -2*t**j +t**(j+1) + radix_term))
```

```python
S(t)
```

```python
results = list(map(make_expander(S(t), t), j_range))
```

```python
coeffs_table(results)
```

\begin{array}{r|l} j = 1 & \left [ 1, \quad 3, \quad 8, \quad 22, \quad 61, \quad 171, \quad 483, \quad 1373, \quad 3923, \quad 11257, \quad 32418, \quad 93644, \quad 271219, \quad 787333, \quad 2290200\right ]\\j = 2 & \left [ 1, \quad 3, \quad 10, \quad 33, \quad 113, \quad 393, \quad 1384, \quad 4920, \quad 17618, \quad 63456, \quad 229642, \quad 834342, \quad 3041474, \quad 11118903, \quad 40748389\right ]\\j = 3 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 124, \quad 449, \quad 1647, \quad 6099, \quad 22754, \quad 85394, \quad 322022, \quad 1219205, \quad 4631572, \quad 17645242, \quad 67391439\right ]\\j = 4 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 460, \quad 1703, \quad 6366, \quad 23974, \quad 90818, \quad 345691, \quad 1321092, \quad 5065645, \quad 19479631, \quad 75093420\right ]\\j = 5 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1714, \quad 6422, \quad 24241, \quad 92042, \quad 351156, \quad 1345049, \quad 5169273, \quad 19923353, \quad 76978309\right ]\\j = 6 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6433, \quad 24297, \quad 92309, \quad 352380, \quad 1350518, \quad 5193271, \quad 20027269, \quad 77423772\right ]\\j = 7 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24308, \quad 92365, \quad 352647, \quad 1351742, \quad 5198740, \quad 20051271, \quad 77527729\right ]\\j = 8 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24310, \quad 92376, \quad 352703, \quad 1352009, \quad 5199964, \quad 20056740, \quad 77551731\right ]\\j = 9 & \left [ 1, \quad 3, \quad 10, \quad 35, \quad 126, \quad 462, \quad 1716, \quad 6435, \quad 24310, \quad 92378, \quad 352714, \quad 1352065, \quad 5200231, \quad 20057964, \quad 77557200\right ] \end{array}

```python
match_table(results, gf_col_width="18cm")
```

gf	matches

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=1 }}$$	A025566

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=2 }}$$

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=3 }}$$

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=4 }}$$

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=5 }}$$

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=6 }}$$

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=7 }}$$

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=8 }}$$

$$\left. \frac{- 2 t^{j} - 2 t^{j + 1} + 2 t^{2 j + 1} + 2}{\left(- 2 t^{j} + t^{j + 1} + \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1} + 1\right) \sqrt{- 4 t + 2 t^{j + 1} + 4 t^{j + 2} - 3 t^{2 j + 2} + 1}} \right|_{\substack{ j=9 }}$$

---

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.


# Quantization of Signals

*This jupyter notebook is part of a [collection of notebooks](../index.ipynb) on various topics of Digital Signal Processing. Please direct questions and suggestions to [Sascha.Spors@uni-rostock.de](mailto:Sascha.Spors@uni-rostock.de).*

## Quantization Error of a Linear Uniform Quantizer

As illustrated in the [preceding section](linear_uniform_characteristic.ipynb), quantization results in two different types of distortions. Overload distortions are a consequence of exceeding the minimum/maximum amplitude of the quantizer. Granular distortions are a consequence of the quantization process when no clipping occurs. Various measures are used to quantify the distortions of a quantizer. We limit ourselves to the signal-to-noise ratio as commonly used measure.

### Signal-to-Noise Ratio

A quantizer can be evaluated by its [signal-to-noise ratio](https://en.wikipedia.org/wiki/Signal-to-noise_ratio) (SNR), which is defined as the power of the continuous amplitude signal $x[k]$ divided by the power of the quantization error $e[k]$. Under the assumption that both signals are drawn from a zero-mean wide-sense stationary (WSS) process, the average SNR is given as

\begin{equation}
SNR = 10 \cdot \log_{10} \left( \frac{\sigma_x^2}{\sigma_e^2} \right) \quad \text{ in dB}
\end{equation}

where $\sigma_x^2$ and $\sigma_e^2$ denote the variances of the signals $x[k]$ and $e[k]$, respectively. The SNR quantifies the average impact of the distortions introduced by quantization. The statistical properties of the signal $x[k]$ and the quantization error $e[k]$ are required in order to evaluate the SNR of a quantizer. First, a statistical model for the quantization error is introduced.

### Model for the Quantization Error

In order to derive the statistical properties of the quantization error, the probability density functions (PDFs) of the quantized signal $x_\text{Q}[k]$ and the error $e[k]$, as well as its bivariate PDFs have to be derived. The underlying calculus is quite tedious due to the nonlinear nature of quantization. Please refer to [[Widrow](../index.ipynb#Literature)] for a detailed treatment. The resulting model is summarized in the following. We focus on the non-clipping case $x_\text{min} \leq x[k] < x_\text{max}$ first, hence on granular distortions. Here the quantization error is in general bounded $|e[k]| < \frac{Q}{2}$.

Under the assumption that the input signal has a wide dynamic range compared to the quantization step size $Q$, the quantization error $e[k]$ can be approximated by the following statistical model

1. The quantization error $e[k]$ is not correlated with the input signal $x[k]$

2. The quantization error is [white](../random_signals/white_noise.ipynb)

    $$ \Phi_{ee}(\mathrm{e}^{\,\mathrm{j}\,\Omega}) = \sigma_e^2 $$

3. The probability density function (PDF) of the quantization error is given by the zero-mean [uniform distribution](../random_signals/important_distributions.ipynb#Uniform-Distribution)

    $$ p_e(\theta) = \frac{1}{Q} \cdot \text{rect} \left( \frac{\theta}{Q} \right) $$

The variance of the quantization error is then [derived from its PDF](../random_signals/important_distributions.ipynb#Uniform-Distribution) as

\begin{equation}
\sigma_e^2 = \frac{Q^2}{12}
\end{equation}

Let's assume that the quantization index is represented as binary or [fixed-point number](https://en.wikipedia.org/wiki/Fixed-point_arithmetic) with $w$-bits. The common notation for the mid-tread quantizer is that $x_\text{min}$ can be represented exactly. Half of the $2^w$ quantization indexes is used for the negative signal values, the other half for the positive ones including zero. The quantization step is then given as

\begin{equation}
Q = \frac{ |x_\text{min}|}{2^{w-1}} = \frac{ x_\text{max}}{2^{w-1} - 1}
\end{equation}

where $x_\text{max} = |x_\text{min}| - Q$. Introducing the quantization step, the variance of the quantization error can be expressed by the word length $w$ as

\begin{equation}
\sigma_e^2 = \frac{x^2_\text{max}}{3 \cdot 2^{2w}}
\end{equation}

The average power of the quantization error quarters per additional bit spend. Introducing the variance into the definition of the SNR yields

\begin{equation}
\begin{split}
SNR &= 10 \cdot \log_{10} \left( \frac{3 \sigma_x^2}{x^2_\text{max}}  \right) + 10 \cdot \log_{10} \left( 2^{2w} \right) \\
& \approx 10 \cdot \log_{10} \left( \frac{3 \sigma_x^2}{x^2_\text{max}}  \right) + 6.02 w \quad \text{in dB}
\end{split}
\end{equation}

It now can be concluded that the SNR decays approximately by 6 dB per additional bit spend. This is often referred to as the 6 dB per bit rule of thumb for linear uniform quantization. Note, this holds only under the assumptions stated above.

### Uniformly Distributed Signal

A statistical model for the input signal $x[k]$ is required in order to calculate the average SNR of a linear uniform quantizer. For a signal that conforms to a zero-mean uniform distribution and under the assumption $x_\text{max} \gg Q$ its PDF is given as

\begin{equation}
p_x(\theta) = \frac{1}{2 x_\text{max}} \text{rect}\left( \frac{\theta}{2 x_\text{max}} \right)
\end{equation}

Hence, all amplitudes between $-x_\text{max}$ and $x_\text{max}$ occur with the same probability. The variance of the signal is then calculated to

\begin{equation}
\sigma_x^2 = \frac{4 x_\text{max}^2}{12}
\end{equation}

Introducing $\sigma_x^2$ and $\sigma_e^2$ into the definition of the SNR yields

\begin{equation}
SNR = 10 \cdot \log_{10} \left( 2^{2 w} \right) \approx 6.02 \, w \quad \text{in dB}
\end{equation}

The word length $w$ and resulting SNRs for some typical digital signal representations are

| | $w$ | SNR |
|----|:----:|:----:|
| Compact Disc (CD) | 16 bit | 96 dB |
| Digital Video Disc (DVD) | 24 bit | 144 dB |
| Video Signals | 8 bit | 48 dB |

Note that the SNR values hold only if the continuous amplitude signal conforms reasonably well to a uniform PDF and if it uses the full amplitude range of the quantizer. If the latter is not the case this can be considered by introducing the level $0 < A \leq 1$ into above considerations, such that $x_\text{min} \leq \frac{x[k]}{A} < x_\text{max}$. The resulting variance is given as

\begin{equation}
\sigma_x^2 = \frac{4 x_\text{max}^2 A^2}{12}
\end{equation}

introduced into the definition of the SNR yields

\begin{equation}
SNR = 10 \cdot \log_{10} \left( 2^{2 w} \right) + 20 \cdot \log_{10} ( A ) \approx 6.02 \, w + 20 \cdot \log_{10} ( A ) \quad \text{in dB}
\end{equation}

From this it can be concluded that a level of -6 dB is equivalent to a loss of one bit in terms of SNR of the quantized signal.

#### Example - Quantization of a uniformly distributed signal

In this example the linear uniform quantization of a random signal drawn from a uniform distribution is evaluated. The amplitude range of the quantizer is $x_\text{min} = -1$ and $x_\text{max} = 1 - Q$.

```python
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import scipy.signal as sig

w = 8  # wordlength of the quantized signal
xmin = -1  # mimimum amplitude of input signal
N = 8192  # number of samples
K = 30  # maximum lag for cross-correlation

def uniform_midtread_quantizer(x, Q):
    # limiter
    x = np.copy(x)
    idx = np.where(x <= -1)
    x[idx] = -1
    idx = np.where(x > 1 - Q)
    x[idx] = 1 - Q
    # linear uniform quantization
    xQ = Q * np.floor(x/Q + 1/2)

    return xQ

def analyze_quantizer(x, e):
    # estimated PDF of error signal
    pe, bins = np.histogram(e, bins=20, normed=True, range=(-Q, Q))
    # estimate cross-correlation between input and error
    ccf = 1/len(x) * np.correlate(x, e, mode='full')
    # estimate PSD of error signal
    nf, Pee = sig.welch(e, nperseg=128)
    # estimate SNR
    SNR = 10*np.log10((np.var(x)/np.var(e)))
    print('SNR = %f in dB' %SNR)

    # plot statistical properties of error signal
    plt.figure(figsize=(9,4))

    plt.subplot(121)
    plt.bar(bins[:-1]/Q, pe*Q, width = 2/len(pe))
    plt.title('Estimated histogram of quantization error')
    plt.xlabel(r'$\theta / Q$')
    plt.ylabel(r'$\hat{p}_x(\theta) / Q$')
    plt.axis([-1, 1, 0, 1.2])

    plt.subplot(122)
    plt.plot(nf*2*np.pi, Pee*6/Q**2)
    plt.title('Estimated PSD of quantization error')
    plt.xlabel(r'$\Omega$')
    plt.ylabel(r'$\hat{\Phi}_{ee}(e^{j \Omega}) / \sigma_e^2$')
    plt.axis([0, np.pi, 0, 2])
    plt.tight_layout()

    plt.figure(figsize=(10,6))
    ccf = ccf[N-K-1:N+K-1]
    kappa = np.arange(-len(ccf)//2,len(ccf)//2)
    plt.stem(kappa, ccf)
    plt.title('Cross-correlation function between input signal and error')
    plt.xlabel(r'$\kappa$')
    plt.ylabel(r'$\varphi_{xe}[\kappa]$')

# quantization step
Q = 1/(2**(w-1))
# compute input signal
np.random.seed(1)
x = np.random.uniform(size=N, low=xmin, high=(-xmin-Q))
# quantize signal
xQ = uniform_midtread_quantizer(x, Q)
e = xQ - x
# analyze quantizer
analyze_quantizer(x, e)
```

**Exercise**

* Change the number of bits `w` and check if the derived SNR holds
* How does the SNR change if you lower the magnitude of the minimum amplitude `xmin` of the input signal?
* What happens if you chose the magnitude of the minimum amplitude `xmin` in the range of the quantization step? Why?

Solution: The numerically computed SNR conforms well to the theoretic result derived above. Lowering the magnitude of the minimum amplitude results in a lower SNR as predicted above. The input signal $x[k]$ is correlated to the quantization error $e[k]$ if the magnitude of the minimum amplitude is lowered such that it is close to the quantization step. Here the assumptions made for the statistical model of the quantization error do not hold.

### Harmonic Signal

For a harmonic input signal $x[k] = x_\text{max} \cdot \cos[\Omega_0 k]$ the variance $\sigma_x^2$ is given by its squared [root mean square](https://en.wikipedia.org/wiki/Root_mean_square) (RMS) value

\begin{equation}
\sigma_x^2 = \frac{x_\text{max}^2}{2}
\end{equation}

Introducing this into the definition of the SNR together with the variance $\sigma_e^2$ of the quantization error yields

\begin{equation}
SNR = 10 \cdot \log_{10} \left(2^{2 w} \cdot \frac{3}{2} \right) \approx 6.02 \, w + 1.76 \quad \text{in dB}
\end{equation}

The gain of 1.76 dB with respect to the case of a uniformly distributed input signal is due to the fact that the amplitude distribution of a harmonic signal is not uniform

\begin{equation}
p_x(\theta) = \frac{1}{\pi \sqrt{1 - (\frac{\theta}{x_\text{max}})^2}}
\end{equation}

for $|\theta| < x_\text{max}$. High amplitudes are more likely to occur. The relative power of the quantization error is lower for higher amplitudes which results in an increase of the average SNR.

### Normally Distributed Signal

So far, we did not consider clipping of the input signal $x[k]$, e.g. by ensuring that its minimum/maximum values do not exceed the limits of the quantizer. However, this cannot always be ensured for practical signals. Moreover, many practical signals cannot be modeled as a uniform distribution. For instance, a [normally distributed](../random_signals/important_distributions.ipynb#Normal-Distribution) random signal exceeds a given maximum value with non-zero probability. Hence, clipping will occur for such an input signal. Clipping results in overload distortions whose amplitude can be much higher that $\frac{Q}{2}$. For the overall average SNR both granular and overload distortions have to be included.

The root mean square (RMS) of the normal distributed input signal is given by its standard deviation $\sigma_x$. The RMS level $A$ of the input signal normalized to the maximum level of the quantizer as

\begin{equation}
A = \frac{\sigma_x}{x_\text{max}}
\end{equation}

The probability that clipping occurs can be derived from the [cumulative distribution function](../random_signals/important_distributions.ipynb#Normal-Distribution) (CDF) of the normal distribution as

\begin{equation}
\Pr \{ |x[k]| > x_\text{max} \} = 1 + \text{erf} \left( \frac{-1}{\sqrt{2} A} \right)
\end{equation}

where $x_\text{max} = - x_\text{min}$ was assumed. For a normally distributed signal with a given probability that clipping occurs $\Pr \{ |x[k]| > x_\text{max} \} = 10^{-5}$ the SNR can be approximately calculated to [[Zölzer](../index.ipynb#Literature)]

\begin{equation}
SNR \approx 6.02 \, w - 8.5 \quad \text{in dB}
\end{equation}

The reduction of the SNR by 8.5 dB results from the fact that small signal values are more likely to occur for a normally distributed signal. The relative quantization error for small signals is higher, which results in a lower average SNR. Overload distortions due to clipping result in a further reduction of the average SNR.

#### Example - Quantization of a normal distributed signal

The following example evaluates the SNR of a linear uniform quantizer with $w=8$ for a normally distributed signal $x[k]$. The SNR is computed and plotted for various RMS levels, the probabilities for clipping are shown additionally.

```python
from scipy.special import erf

w = 8  # wordlength of the quantizer
A = np.logspace(-2, 0, num=500)  # RMS levels
N = int(1e6)  # number of samples
np.random.seed(1)

def compute_SNR(a):
    # compute input signal
    x = np.random.normal(size=N, scale=a)
    # quantize signal
    xQ = uniform_midtread_quantizer(x, Q)
    e = xQ - x
    # compute SNR
    SNR = 10*np.log10((np.var(x)/np.var(e)))

    return SNR

def plot_SNR(A, SNR):
    # plot results
    plt.figure(figsize=(8,4))
    plt.plot(20*np.log10(A), SNR)
    plt.xlabel(r'RMS level $\sigma_x / x_\mathrm{min}$ in dB')
    plt.ylabel('SNR in dB')
    plt.grid()

# quantization step
Q = 1/(2**(w-1))
# compute SNR for given RMS levels
SNR = [compute_SNR(a) for a in A]
# plot results
plot_SNR(A, SNR)
# find maximum SNR
Amax = A[np.argmax(SNR)]
Pc = 1 + erf(-1/(np.sqrt(2)*Amax))
print(r'Maximum SNR = {0:2.3f} dB for A = {1:2.1f} dB with clipping probability {2:2.1e}'
      .format(np.array(SNR).max(), 20*np.log10(Amax), Pc))
```

**Exercise**

* Can you explain the overall shape of the SNR?
* For which RMS level and probability of clipping is the SNR optimal?
* Change the wordlength `w` of the quantizer. How does the SNR change?

Solution: The SNR is low for low RMS levels of the input signal since the relative level of the quantization error is high. The SNR increases with increasing level until the clipping errors become dominant which make the SNR decay after its maximum. The SNR is optimal for $A \approx -12$ dB which is equivalent to $\Pr \{ |x[k]| > x_\text{max} \} \approx 10^{-4}$. Increasing the wordlength by one bit increases the SNR approximately by 6 dB.

### Laplace Distributed Signal

The [Laplace distribution](../random_signals/important_distributions.ipynb#Laplace-Distribution) is a commonly applied model for speech and music signals. As for the normal distribution, clipping will occur with a non-zero probability. The probability that clipping occurs can be derived from the [cumulative distribution function](../random_signals/important_distributions.ipynb#Laplace-Distribution) (CDF) of the normal distribution as

\begin{equation}
\Pr \{ |x[k]| > x_\text{max} \} = e^{- \frac{\sqrt{2}}{A}}
\end{equation}

The SNR for a Laplace distributed signal is in general lower compared to a normal distributed signal. The reason for this is, that the Laplace distribution features low signal values with a higher and large values with a lower probability in comparison to the normal distribution. The relative quantization error for small signals is higher, which results in a lower average SNR. The probability of overload distortions is also higher compared to the normal distribution.

#### Example - Quantization of a Laplace distributed signal

The following example evaluates the SNR of a linear uniform quantizer with $w=8$ for a Laplace distributed signal $x[k]$. The SNR is computed and plotted for various RMS levels.

```python
w = 8  # wordlength of the quantizer
A = np.logspace(-2, 0, num=500)  # relative RMS levels
N = int(1e6)  # number of samples
np.random.seed(1)

def compute_SNR(a):
    # compute input signal
    x = np.random.laplace(size=N, scale=a/np.sqrt(2))
    # quantize signal
    xQ = uniform_midtread_quantizer(x, Q)
    e = xQ - x
    # compute SNR
    SNR = 10*np.log10((np.var(x)/np.var(e)))

    return SNR

# quantization step
Q = 1/(2**(w-1))
# compute SNR for given RMS levels
SNR = [compute_SNR(a) for a in A]
# plot results
plot_SNR(A, SNR)
# find maximum SNR
Amax = A[np.argmax(SNR)]
Pc = np.exp(-np.sqrt(2)/Amax)
print(r'Maximum SNR = {0:2.3f} dB for A = {1:2.1f} dB with clipping probability {2:2.1e}'
      .format(np.array(SNR).max(), 20*np.log10(Amax), Pc))
```

**Exercise**

* Compare the SNR for the Laplace distributed signal to the case of a normally distributed signal. What is different?

Solution: The overall SNR is lower compared to the case of a normally distributed signal. Its maximum is also at lower RMS levels. Both can be explained by the properties of the Laplace distribution discussed above.

**Copyright**

This notebook is provided as [Open Educational Resource](https://en.wikipedia.org/wiki/Open_educational_resources). Feel free to use the notebook for your own purposes. The text is licensed under [Creative Commons Attribution 4.0](https://creativecommons.org/licenses/by/4.0/), the code of the IPython examples under the [MIT license](https://opensource.org/licenses/MIT). Please attribute the work as follows: *Sascha Spors, Digital Signal Processing - Lecture notes featuring computational examples, 2016-2018*.


```python
import numpy as np
import sympy as sy
```

```python
s = sy.symbols('s', real=False)
h = sy.symbols('h')
```

```python
Ac = sy.Matrix([[-2, -0.02, 0],[1,0,0], [0,2,0]])
Bc = sy.Matrix([[1],[0],[0]])
Cc = sy.Matrix([[0,0,2]])
```

```python
sIminAc = s*sy.Matrix.eye(3) - Ac
charactPoly = sIminAc.det()
sIminAcinv = sIminAc.inv()
AA = sIminAcinv * charactPoly
for el in AA:
    print sy.simplify(sy.expand(el))

```

    1.0*s**2
    -0.02*s
    0
    s
    s*(s**3 + 4*s**2 + 4.02*s + 0.04)/(s**2 + 2*s + 0.02)
    0
    2.00000000000000
    (2*s**3 + 8*s**2 + 8.04*s + 0.08)/(s**2 + 2*s + 0.02)
    s**2 + 2*s + 0.02

```python
charactPoly
```

    s**3 + 2*s**2 + 0.02*s

```python
q = sy.symbols('q')
Hq = (0.32*q**2 + 1.21*q+0.29)/(q**3 -2.82*q**2 + 2.64*q-0.82 )
sy.apart(Hq)
```

    46.2150617283951/(1.0*q - 0.82) - 45.8950617283951/(q - 1) + 10.1111111111111/(q - 1)**2

```python
sy.together(- 45.8950617283951/(q - 1) + 10.1111111111111/(q - 1)**2)
```

    (-45.8950617283951*q + 56.0061728395062)/(q - 1)**2

```python

```


[View in Colaboratory](https://colab.research.google.com/github/ktaletsk/CPF/blob/master/1D_Example/CPF_1D_toy.ipynb)

# Coarse-graining of Brownian dynamics of toy model
Predicting the first passage time using the constant probability flux (CPF) method.
 This is Jupyter notebook written in Python language. You can run each cell by pressing CTRL+SHIFT on your keyboard. Reproduce results pulished in paper and feel free to make changes and use this code in your research

## Define variables and functions and generate simulation code

```
from sympy import *
from sympy.utilities.codegen import codegen
import numpy as np
from IPython.display import Markdown
import matplotlib.pyplot as plt
# import scipy.integrate as integrate
from IPython.display import HTML, Math
```

```
def latex_print(expr):
  display(HTML(""))
  return Math(latex(expr))
```

Consider a detailed dynamics, say MD, with the trajectory of some interesting species can reasonably be mapped to dynamics along a single reaction coordinate $x$.
It might then be desirable to coarse grain these simulations to a nonlinear, stochastic differential equation.
It is well established that such dynamics are equivalently described by a Fokker-Planck (or Smoluchowski) equation

\begin{equation}
\frac{\partial p}{\partial t} =
    \frac{\partial}{\partial x}\frac{1}{\zeta} \left[ \left(\frac{dF^\text{CG}}{d x}\right) p
    + k_\text{B}T \frac{\partial p}{\partial x}
    \right],
\end{equation}

where $p\equiv p(x;t|x_0;t_0)$ is the probability density of finding the species in the neighborhood of $x$ at time $t$, given that it had coordinate $x_0$ at some earlier time $t_0$.

The equivalent Ito-form stochastic differential equation is
\begin{equation}
dx = \left( \frac{d \zeta^{-1}}{d x} - \zeta^{-1} \frac{d F}{dx} \right) dt + \sqrt{2 \zeta^{-1}} d W_t
\end{equation}

We chose free energy of our toy system in a form:
\begin{equation}
\frac{F}{k_B T} = H_0 (1-x_0)^2 + \sum_{i=1}^{n-1} H_i (x_i - \mu_i)^2
\end{equation}
where $\mu_i=\frac{\sin A(x_0-C_i)}{B_i}$

```
#Number of dimensions
n=1
```

```
A = []
B = []
C = []
H = [4]
```

```
#Define variables
x = symbols('x0:%d'%n)

def floor(x,A,B,C):
    return sin(A*(x-C))/B

def wall(x,y,A,B,C):
    return (y - floor(x,A,B,C))**2

def free_energy(n, x):
    result = H[0]*(1-x[0])**2
    for i in range(1, n):
        result += H[i] * wall(x[0], x[i], A[i-1], B[i-1], C[i-1])
    return result

def free_energy_CG(x):
    return H[0]*(1-x[0])**2

def zeta_inv(n, x):
    return 0.5*(1+x[0]**2)**2 #friction

def tshift(n,x,i):
    return diff(zeta_inv(n,x), x[i])-zeta_inv(n, x)*diff(free_energy(n,x), x[i])

def wshift(n,x):
    return sqrt(2.0*zeta_inv(n, x))
```

**Free energy of our one-dimensional toy model**

```
latex_print(free_energy(n, x))
```

```
xx = MatrixSymbol('x', 1, n)
dx = MatrixSymbol('dx', 1, n)
dWt = MatrixSymbol('dWt', 1, n)
dt = symbols('dt')
subs_rule = list(zip(x,xx))
array = [(tshift(n,x,i) * dt + wshift(n,x) * dWt[i]).subs(subs_rule) for i in range(n)]
m = Matrix(1, n, array)
equation = Eq(dx, m)
```

```
latex_print(equation)
```

**Calculate equilibrium distribution**

\begin{equation}
p(x_i | x_0) = \frac{p(x_i, x_0)}{p(x_0)}
\end{equation}

```
p = symbols('p0:%d'%(n-1))
pp = MatrixSymbol('p', 1, n)
xx = MatrixSymbol('x', 1, n)
xa = symbols('xa')

subs_rule2 = list(zip(p,pp))
subs_rule2.insert(0,(x[0],xa))

array2 = [(floor(x[0],A[i],B[i],C[i]) + p[i]/sqrt(2*H[i+1])).subs(subs_rule2)  for i in range(0,n-1)]
array2.insert(0,xa)
m2 = Matrix(1, n, array2)
equation2 = Eq(xx, m2)
```

```
latex_print(equation2)
```

### Generate code for timestep and initialization

```
#Put source files 'FPT-gen.cpp' and 'CPF-gen.cpp' in the same folder as this notebook (for Google Colab)
!git clone https://github.com/ktaletsk/CPF.git; cp CPF/1D_Example/CPF-gen.cpp .; cp CPF/1D_Example/FPT-gen.cpp .; rm -rf CPF
```

```
!ls
```

```
#Compile Ito equation source files for given free energy and friction
codegen(("shift",equation), "C", "shift", header=False, empty=False, to_files=True)
codegen(("init",equation2), "C", "init", header=False, empty=False, to_files=True)
```

```
#Complie CPF code
! g++ FPT-gen.cpp shift.c init.c -o FPT-gen
! g++ CPF-gen.cpp shift.c init.c -o CPF-gen
```

```
#Run CPF simulation
!./CPF-gen
```

## Processing CPF results

```
with open('cpf.dat') as f:
    lines = f.readlines()
    x0 = np.array([float(line.split()[0]) for line in lines])
    x0.sort()
```

```
x0.shape[0]
```

```
P = np.arange(0.0, 1.0, 1.0/x0.shape[0])
```

```
fig = plt.figure(figsize=(12, 9))
ax1 = fig.add_subplot(111)
ax1.set_title("Steady state probability density distribution")
ax1.set_xlabel(r'$x$')
ax1.set_ylabel(r'$\mathbb{P}^{ss}$')
ax1.plot(x0,P, c='r', label=r'$f_d(t)$')
plt.axvline(x=0, color='k', linestyle='--')
plt.axvline(x=1,  color='k', linestyle='--')
plt.show()
```

### Using [Kernel Density Estimation](http://scikit-learn.org/stable/modules/density.html) to find PDF in steady state

```
#Rule of thumb bandwith for kernel density estimstor
h = 1.06*np.std(x0)*np.size(x0)**(-0.2)
h
```

```
def myKernelDensity(x, h, xi):
    return np.sum(np.exp(-(x-xi)**2/(2*h**2)))/len(xi)
def myKernelDensityDerivative(x, h, xi):
    return np.sum(-(x-xi)/h**2*np.exp(-(x-xi)**2/(2*h**2)))/len(xi)
```

```
X_plot = np.linspace(0.0, 1.0, 1000)
```

```
mydens = [myKernelDensity(i, h, x0) for i in X_plot]
```

```
mydensder = [myKernelDensityDerivative(i, h, x0) for i in X_plot]
```

```
fig2 = plt.figure(figsize=(12, 9))
ax2 = fig2.add_subplot(111)
ax2.set_title("Steady state probability density distribution")
ax2.set_xlabel(r'$x$')
ax2.set_ylabel(r'$p^{ss}$')
ax2.plot(X_plot, np.array(mydens)/np.sqrt(2*np.pi*h**2), c='r')
plt.axvline(x=0, color='k', linestyle='--')
plt.axvline(x=1,  color='k', linestyle='--')
plt.show()
```

```
pCG = lambdify(x[0], exp(-free_energy_CG(x))/simplify(integrate(exp(-free_energy_CG(x)),(x[0],-oo,oo))), "sympy")
```

```
PCG = lambdify(x[0], simplify(integrate(exp(-free_energy_CG(x)),(x[0],-oo,x[0])))/simplify(integrate(exp(-free_energy_CG(x)),(x[0],-oo,oo))), "sympy")
```

```
with open('flux.dat') as f:
    lines = f.readlines()
J0 = float(lines[-1].split()[1])/float(lines[-1].split()[0])
```

```
xr=1
```

```
arr1 = np.array([(N(PCG(xr))-N(PCG(xT)))/N(pCG(xT))/J0 for xT in X_plot], dtype="float")
```

```
N(PCG(2))
```

## Friction

### 1. Friction from KDE of $p^{ss}$

```
dFdx = lambdify(x[0],diff(free_energy_CG(x)))
```

```
actual_friction = lambdify(x[0],zeta_inv(n, x))
```

```
dFdX_arr = [dFdx(i) for i in X_plot]
```

```
friction = -(np.multiply(mydens,dFdX_arr)+mydensder)/J0/np.sqrt(2*np.pi*h**2)
```

```
act_f = [1/actual_friction(i) for i in X_plot]
```

```
fig5 = plt.figure(figsize=(12, 9))

ax5 = fig5.add_subplot(111)

ax5.set_title("Friction")
ax5.set_xlabel(r'$x$')
ax5.set_ylabel(r'$\zeta(x)/k_B T$')

ax5.plot(X_plot, friction, c='k',label='KDE estimate')
ax5.plot(X_plot, act_f, c='g', label='Actual friction')
plt.axvline(x=h, color='r', linestyle='--')
plt.axvline(x=xr-h, color='r', linestyle='--')
plt.legend()
plt.show()
```

### 2. Fitting friction with a sum of Chebyshev polynomials

\begin{equation}
\mathbb{P}^\text{ss}(x) =
\int_{x_s}^{x}
 \mathbb{P}^\text{eq}(x')
 \frac{\tilde{\zeta}(x')}{p^\text{eq}(x')} dx' +
 \mathbb{P}^\text{eq}(x)
 \int_{x}^{x_a}
 \frac{\tilde{\zeta}(x')}{p^\text{eq}(x')} dx'
\end{equation}
where
\begin{equation}
\tilde{\zeta}(x) := \frac{J_0 \zeta(x)}{k_\text{B}T}
\end{equation}

We fit friction with the sum of Chebychev's polynomials, so that
\begin{equation}
\tilde{\zeta}(x) = \sum_i c_i T_i(x)
\end{equation}

\begin{equation}
\mathbb{P}^\text{ss}(x) = \sum_i c_i \left( \int_{x_s}^{x}
 \mathbb{P}^\text{eq}(x')
 \frac{T_i(x')}{p^\text{eq}(x')} dx' +
 \mathbb{P}^\text{eq}(x)
 \int_{x}^{x_a}
 \frac{T_i(x')}{p^\text{eq}(x')} dx' \right) =
\sum_i c_i I_i(x)
\end{equation}

We define the set of reference points $\{ x_j \}$ and solve matrix equation
\begin{equation}
c_i I_i(x_j) = \mathbb{P}^{ss}(x_j)
\end{equation}
to find the set of weights $\{c_i\}$

```
import scipy.integrate
def integral1(n, lower, upper):
    return scipy.integrate.quad(lambda x: PCG(x)*chebyshevt_poly(n,x)/pCG(x), lower, upper)[0]

def integral2(n, lower, upper):
    return scipy.integrate.quad(lambda x: chebyshevt_poly(n,x)/pCG(x), lower, upper)[0]

def Pss_cheb(x, n, L0, La):
    return integral1(n,L0,x)+N(PCG(x))*integral2(n,x,La)

# def PSS_func(x):

```

```
from scipy.interpolate import interp1d
PSS_func = interp1d(x0, P)
```

```
PSS_func(0.5)
```

```
nmodes=7 #number of Chebyshev polynomials (starting from 0-th)
npoints=500 #reference points in domain
III = [[Pss_cheb(float(j)/npoints, i, 0, 1) for i in range(nmodes)] for j in range(npoints)]
rhs = [PSS_func(float(j)/npoints) for j in range(npoints)]
fit = np.linalg.lstsq(III, rhs, rcond=-1)[0]
fit
```

```
fig = plt.figure(figsize=(12, 9))

ax1 = fig.add_subplot(111)

ax1.set_title("Steady state probability density distribution (Chebychev fit)")
# ax1.set_xlabel(r'$t/\tau_c$')
# ax1.set_ylabel(r'$f_d(t)$')

ax1.plot(x0,P, c='r', label=r'$f_d(t)$')
ax1.plot(np.arange(float(npoints))/npoints, [sum([fit[i]*Pss_cheb(float(j)/npoints, i, 0, 1) for i in range(nmodes)]) for j in range(npoints)], c='k', label='Chebychev approximation')

#plt.xlim(xmin=-1.1, xmax=-0.9)
#plt.ylim(ymin=0, ymax=0.05)
# leg = ax1.legend()
#ax1.set_xscale('log')
# ax1.set_yscale('log')

plt.show()
```

```
cheb_friction = [sum([fit[i]*chebyshevt_poly(i,j)/J0 for i in range(nmodes)]) for j in X_plot]
```

```
fig5 = plt.figure(figsize=(12, 9))

ax5 = fig5.add_subplot(111)

ax5.set_title("Friction")
ax5.set_xlabel(r'$x_0$')
ax5.set_ylabel(r'$\zeta(x_0)/k_B T$')

ax5.plot(X_plot, friction, c='k', label='KDE friction')
ax5.plot(X_plot, cheb_friction, c='r', label='Chebychev friction')
ax5.plot(X_plot, act_f, c='g', label='Actual friction')
leg = ax5.legend()

plt.show()
```

**This is it! Feel free now to try different friction function and/or free energy for this system**


# Quantum mechanics 1 - Vibrations

In this laboratory you will be solving the Schrodinger equation on simple systems to enable you to gain a **quantum intuition**.

This lab will focus on quantum vibrations that will introduce you to the principle of quantisation and confinement.
- quantisation - only discrete energy levels are allowed
- confinement - as the position becomes more defined the energy/momentum becomes less well defined

The widget has been constructed based on a recent code developed by by Matthew Srnec and Shiv Upadhyay. You can use the sliders to set the values and then click run and wait for the simulation to be done and a plot will be generated.

**Make sure you close the figure before running the next simulation**

The tasks to complete with this simulations are below the app.

## Note about units
We will be using atomic units throughout that makes the Schrodinger equation

```python
#Importing the important libraries
%matplotlib notebook
#The Schrodinger solver by Matthew Srnec and Shiv Upadhyay will be used
from utils3 import * #This is another file with functions in it to clean up the plotting.
import numpy as np
from IPython.display import clear_output
import ipywidgets as widgets
# atomic units
hbar=1.0
m=1.0
#set precision of numerical approximation
steps=2000
```

```python
# atomic units
hbar=1.0
m=1.0
#set precision of numerical approximation
steps=2000
```

```python
#The Schrodinger solver by Matthew Srnec and Shiv Upadhyay will be used
from utils3 import * #This is another file with functions in it to clean up the plotting.

# set force constant and depth of harmonic well/morse potential
k = widgets.FloatSlider(value=0.25,min=0.25,max=1.4,step=0.05, description="k")
D = widgets.FloatSlider(value=4,min=1,max=5,step=1.0, description="D")
Case = widgets.Dropdown(
    options=[('Harmonic', 5), ('Morse', 6)],
    value=5,
    description='Potential:',
)

@debounce(0.1) #https://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html#Debouncing
def handle_slider_change_1(change):

    clear_output()

    if Case.value==5:
        # divide by two so a well from -W to W is of input width
        W=np.sqrt(np.abs(2.0*-D.value)/(k.value**2))
        # set length variable for xvec
        A=W*2.0
        # create x-vector from -A to A
        xvec=np.linspace(-A,A,steps,dtype=np.float_)
        # get step size
        h=xvec[1]-xvec[0]
        # create the potential from harmonic potential function
        U=harmonic_potential(xvec,k.value,-D.value)
    if Case.value==6:
        # create the potential from morse potential function
        xvec,h,U=morse_potential(k.value,D.value,steps)

    # create Laplacian via 3-point finite-difference method
    Laplacian=(-2.0*np.diag(np.ones(steps))+np.diag(np.ones(steps-1),1)\
        +np.diag(np.ones(steps-1),-1))/(float)(h**2)
    # 'create the Hamiltonian'
    Hamiltonian=np.zeros((steps,steps))
    [i,j] = np.indices(Hamiltonian.shape)
    Hamiltonian[i==j]=U
    Hamiltonian+=(-0.5)*((hbar**2)/m)*Laplacian
    # diagonalize the Hamiltonian yielding the wavefunctions and energies
    E,V=diagonalize_hamiltonian(Hamiltonian)
    # determine theoretical number of energy levels (n)
    n=0
    while E[n]<0:
        n+=1
    # print output
    #output(Case,['Force Constant','Depth'],[omega,D],E,n)
    # create plot
    display(Case,k,D,squared_1,button_1)
    finite_well_plot(E,V,xvec,steps,n,Case,U,param=[k,D],ask_squared=squared_1.value)
    plt.xlim((-20,20))

case = widgets.Dropdown(options="", value=None, description='State:')

button_1 = widgets.Button(description="Run")

squared_1 = widgets.Checkbox(value=False,description="Probability")

button_1.on_click(handle_slider_change_1)

display(Case,k,D,squared_1,button_1)
```

    Dropdown(description='Potential:', options=(('Harmonic', 5), ('Morse', 6)), value=5)

    FloatSlider(value=0.25, description='k', max=1.4, min=0.25, step=0.05)

    FloatSlider(value=4.0, description='D', max=5.0, min=1.0, step=1.0)

    Checkbox(value=False, description='Probability')

    Button(description='Run', style=ButtonStyle())

# Lab report

## Section 1 - Quantum confinement

For the first section on quantum confinement complete the following tasks using screenshots of the simulated plot to aid your answers.

**Task 1** - Plot the wavefunctions for the harmonic oscillator for k=0.5 and D=2.0. Comment on the quantisation of the vibrations compared to classical vibrations.

**Task 2** - The exact energy solutions to the quantum harmonic oscillator in atomic units ($\hbar=1,\; m=1$) are

\begin{equation}
E_n=\omega\left(n+\frac{1}{2}\right)
\end{equation}

where $\omega=\sqrt{\frac{k}{m}}$

Compare the energies computed using the numerical approach and the exact approach.

**Task 3** - Change the spring constant from 0.25 to 1.35. How does the zero point energy change (lowest energy level)? Comment on how this demonstrates the uncertainty principle. Describe what the zero point energy is.

**Task 4** - How much energy is absorbed when the system is excited from the n=0 to n=1 energy level? How does this compare with the energy that is absorbed by a driven classical harmonic oscillator?

**Task 5** - Plot the morse potential wavefunctions for k=0.5, D=2.0. How many bound states are there compared to the harmonic oscillator? How do the energies of the lowest energy states compare with the harmonic oscillator? How does the interstate energy change for the morse potential?

**Task 6** - Plot the probabiliy for k=0.8 and D=4.0. How does this compare with the classical probability of finding the particle in the well? For the morse potential which side of the potential does it spend the most amount of time.

```python

```