# Chapter 3: Vector Autoregressive Methods

Prepared by: Maria Eloisa Ventura

Previously, we have introduced the classical approaches in forecasting single/univariate time series like the
ARIMA (autoregressive integrated moving-average) model and the simple linear regression model. We learned that stationarity is a condition that is necessary when using ARIMA while this need not be imposed when using the linear regression model. In this notebook, we extend the forecasting problem to a more generalized framework where we deal with **multivariate time series**--time series which has more than one time-dependent variable. More specifically, we introduce **vector autoregressive (VAR)** models and show how they can be used in forecasting mutivariate time series.

## Multivariate Time Series Model

As shown in the previous chapters, one of the main advantages of using simple univariate methods (e.g., ARIMA) is the ability to forecast future values of one variable by only using past values of itself. However, we know that most if not all of the variables that we observe are actually dependent on other variables. Most of the time, the information that we gather is limited by our capacity to measure the variables of interest. For example, if we want to study weather in a particular city, we could measure temperature, humidity and precipitation over time. But if we only have a thermometer, then we'll only be able to collect data for temperature, effectively reducing our dataset to a univariate time series.

Now, in cases where we have multiple time series (longitudinal measurements of more than one variable), we can actually use multivariate time series models to understand the relationships of the different variables over time.
By utilizing the additional information available from related series, these models can often provide better forecasts than the univariate time series models.

In this section, we cover the foundational information needed to understand Vector Autoregressive Models, a class of multivariate time series models, by using a framework similar to univariate time series laid out in the previous chapters, and extending it to the multivariate case.

### Definition: Univariate vs Multivariate Time Series

Time series can either be univariate or multivariate. The term **univariate time series** consists of single observations recorded sequentially over equal time increments. When dealing with a univariate time series model (e.g., ARIMA), we usually refer to a model that contains lag values of itself as the independent variable.

On the other hand, a **multivariate time series** has more than one time-dependent variable. For a multivariate process, several related time series are observed simultaneously over time. As an extension of the univariate case, the multivariate time series model involves two or more input variables, and leverages the interrelationship among the different time series variables.

import numpy as np
import pandas as pd
import statsmodels.tsa as tsa
from statsmodels.tsa.vector_ar.var_model import VAR, FEVD
from statsmodels.tsa.arima_model import ARIMA
from statsmodels.tsa.stattools import acf, adfuller, ccf, ccovf, kpss
from statsmodels.graphics.tsaplots import plot_acf
import matplotlib.pyplot as plt
import mvts_utils as utils
import seaborn as sns
import warnings
warnings.filterwarnings("ignore")
pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)

%load_ext autoreload
%autoreload 2

### Example 1: Multivariate Time Series

#### A. Air Quality Data from UCI

The dataset contains hourly averaged measurements obtained from an Air Quality Chemical Multisensor Device which was located on the field of a polluted area at an Italian city. The dataset can be downloaded __[here](https://archive.ics.uci.edu/ml/datasets/Air+Quality#)__.

aq_df = pd.read_excel("../data/AirQualityUCI/AirQualityUCI.xlsx", parse_dates=[['Date', 'Time']])\
            .set_index('Date_Time').replace(-200, np.nan).interpolate()
aq_df.head(2)

fig,ax = plt.subplots(5, figsize=(15,8), sharex=True)
plot_cols = ['CO(GT)', 'NO2(GT)', 'C6H6(GT)', 'T', 'RH']
aq_df[plot_cols].plot(subplots=True, legend=False, ax=ax)
for a in range(len(ax)):
    ax[a].set_ylabel(plot_cols[a])
ax[-1].set_xlabel('')
plt.tight_layout()
plt.show()

#### B. Global Health from The World Bank

This dataset combines key health statistics from a variety of sources to provide a look at global health and population trends. It includes information on nutrition, reproductive health, education, immunization, and diseases from over 200 countries. The dataset can be downloaded __[here](https://console.cloud.google.com/marketplace/product/the-world-bank/global-health?filter=solution-type:dataset&filter=category:health&id=f3c38e10-2c45-43c8-8a12-8d749ba987ee)__.

ind_df = pd.read_csv('../data/WorldBankHealth/WorldBankHealthPopulation_SeriesSummary.csv')\
            .loc[:,['series_code', 'indicator_name']].drop_duplicates().reindex()\
            .sort_values('indicator_name').set_index('series_code')
hn_df = pd.read_csv('../data/WorldBankHealth/WorldBankHealthPopulation_HealthNutritionPopulation.csv')\
            .pivot(index='year', columns='indicator_code', values='value')

cols = ['SH.XPD.KHEX.GD.ZS', 'SH.XPD.CHEX.GD.ZS', 'SH.XPD.GHED.GD.ZS']
health_expenditure_df = hn_df.loc[np.arange(2000, 2018), cols]\
    .rename(columns = dict(ind_df.loc[cols].indicator_name\
                           .apply(lambda x: '_'.join(x.split('(')[0].split(' ')[:-1]))))
health_expenditure_df.index = pd.date_range('2000-1-1', periods=len(health_expenditure_df), freq="A-DEC")
health_expenditure_df.head(3)

fig, ax = plt.subplots(3, 1, sharex=True, figsize=(15,7))
health_expenditure_df.plot(subplots=True, ax=ax, legend=False)
y_label = ['Capital health expenditure', 'Current health expenditure',
           'Domestic general government\n health expenditure']
for a in range(len(ax)):
    ax[a].set_ylabel(f"{y_label[a]}\n (% of GDP)")
plt.tight_layout()
plt.show()

#### C. US Treasury Rates
January, 1982 – December, 2016  (Weekly)
https://essentialoftimeseries.com/data/
This sample dataset contains weekly data of US Treasury rates from January 1982 to December 2016. The dataset can be downloaded __[here](https://essentialoftimeseries.com/data/)__.

treas_df = pd.read_excel("../data/USTreasuryRates/us-treasury-rates-weekly.xlsx")
treas_df = treas_df.rename(columns={'Unnamed: 0': 'Date'}).set_index('Date')
treas_df.index = pd.to_datetime(treas_df.index)
treas_df.head(3)

fig,ax = plt.subplots(1, figsize=(15, 3), sharex=True)
data_df = treas_df.iloc[:, 0:9]
data_df.plot(ax=ax)
plt.ylabel('US Treasury yield (%)')
plt.xlabel('')
plt.legend(ncol=2)
plt.tight_layout()
plt.show()

#### D. Jena Climate Data
Time series dataset recorded at the Weather Station at the Max Planck Institute for Biogeochemistry in Jena, Germany from 2009 to 2016. It contains 14 different quantities (e.g., air temperature, atmospheric pressure, humidity, wind direction, and so on) were recorded every 10 minutes. You can download the data __[here](https://www.kaggle.com/stytch16/jena-climate-2009-2016)__.

jena_data = pd.read_csv('../data/jena_climate_2009_2016.csv')
jena_data['Date Time'] = pd.to_datetime(jena_data['Date Time'])
jena_data = jena_data.set_index('Date Time')
jena_data.head(3)

fig,ax = plt.subplots(jena_data.shape[-1], figsize=(15,16), sharex=True)
jena_data.iloc[-1000:].plot(subplots=True, ax=ax)
ax[-1].set_xlabel('')
plt.tight_layout()
plt.show()

### Foundations
Before we discuss VARs, we outline some fundamental concepts below that we will need to understand the model.

##### Weak Stationarity of Multivariate Time Series
As in the univariate case, one of the requirements that we need to satisfy before we can apply VAR models is stationarity--in particular, weak stationarity. Both in the univariate and multivariate case, the first two moments of the time series are time-invariant. More formally, we describe weak stationarity below.

Consider a $N$-dimensional time series, $\mathbf{y}_t = \left[y_{1,t}, y_{2,t}, ..., y_{N,t}\right]^\prime$. This is said to be weakly stationary if the first two moments are finite and constant through time, that is,
1. $E\left[\mathbf{y}_t\right] = \boldsymbol{\mu}$
2. $E\left[(\mathbf{y}_t-\boldsymbol{\mu})(\mathbf{y}_t-\boldsymbol{\mu})^\prime\right] \equiv \boldsymbol\Gamma_0 < \infty$ for all $t$
3. $E\left[(\mathbf{y}_t-\boldsymbol{\mu})(\mathbf{y}_{t-h}-\boldsymbol{\mu})^\prime\right] \equiv \boldsymbol\Gamma_h$ for all $t$ and $h$

where the expectations are taken element-by-element over the joint distribution of $\mathbf{y}_t$:
- $\boldsymbol{\mu}$ is the vector of means $\boldsymbol\mu = \left[\mu_1, \mu_2, ..., \mu_N \right]$
- $\boldsymbol\Gamma_0$ is the $N\times N$ covariance matrix where the $i$th diagonal element is the variance of $y_{i,t}$, and the $(i, j)$th element is the covariance between $y_{i,t}$ and ${y_{j,t}}$
- $\boldsymbol\Gamma_h$ is the cross-covariance matrix at lag $h$

#####  Obtaining Cross-Correlation Matrix from Cross-Covariance Matrix
When dealing with a multivariate time series, we can examine the predictability of one variable on another by looking at the relationship between them using the cross-covariance function (CCVF) and cross-correlation function (CCF). To do this, we begin by defining the cross-covariance between two variables, then we estimate the cross-correlation between one variable and another variable that is time-shifted. This informs us whether one time series may be related to the past lags of the other. In other words, CCF is used for identifying lags of one variable that might be useful as a predictor of the other variable.

**At lag 0**:

Let $\mathbf\Gamma_0$ be the covariance matrix at lag 0, $\mathbf D$ be a $N\times N$ diagonal matrix containing the standard deviations of $y_{i,t}$ for $i=1, ..., N$. The correlation matrix of $\mathbf{y}_t$ is defined as

$$\boldsymbol\rho_0 = \mathbf D^{-1}\mathbf\Gamma_0\mathbf D^{-1}$$

where the $(i, j)$th element of $\boldsymbol\rho_0$ is the correlation coefficient between $y_{i,t}$ and $y_{j,t}$ at time $t$:

$$\rho_{i,j}(0) = \dfrac{Cov\left[y_{i,t}, y_{j,t} \right]}{\sigma_{i,t} \sigma_{j,t}}$$

**At lag h**:

Let $\boldsymbol\Gamma_h = E\left[(\mathbf{y}_t-\boldsymbol{\mu})(\mathbf{y}_{t-h}-\boldsymbol{\mu})^\prime\right]$ be the lag-$h$ covariance cross-covariance matrix of $\mathbf y_{t}$. The lag-$h$ cross-correlation matrix is defined as

$$\boldsymbol\rho_h = \mathbf D^{-1}\mathbf\Gamma_h\mathbf D^{-1}$$

The $(i,j)$th element of $\boldsymbol\rho_h$ is the correlation coefficient between $y_{i,t}$ and $y_{j,t-h}$:

$$\rho_{i,j}(h) = \dfrac{Cov\left[y_{i,t}, y_{j,t-h} \right]}{\sigma_{i,t} \sigma_{j,t}}$$

**What do we get from this?**

|Correlation Coefficient| Interpretation |
|:----|:----|
|$\rho_{i,j}(0)\neq0$|$y_{i,t}$ and $y_{j,t}$ are *contemporaneously linearly correlated*|
|$\rho_{i,j}(h)=\rho_{j,i}(h)=0$ for all $h\geq0$|$y_{i,t}$ and $y_{j,t}$ share *no linear relationship*|
|$\rho_{i,j}(h)=0$ and $\rho_{j,i}(h)=0$ for all $h>0$|$y_{i,t}$ and $y_{j,t}$ are said to be linearly *uncoupled*|
|$\rho_{i,j}(h)=0$ for all $h>0$, but $\rho_{j,i}(q)\neq0$ for at least some $q>0$|There is a *unidirectional (linear) relationship* between $y_{i,t}$ and $y_{j,t}$, where $y_{i,t}$ does not depend on $y_{j,t}$, but $y_{j,t}$ depends on (some) lagged values of $y_{i,t}$|
|$\rho_{i,j}(h)\neq0$ for at least some $h>0$ and $\rho_{j,i}(q)\neq0$ for at least some $q>0$|There is a *bi-directional (feedback) linear relationship* between $y_{i,t}$ and $y_{j,t}$|

## Vector Autoregressive (VAR) Models

The vector autoregressive (VAR) model is one of the most successful models for analysis of multivariate time series. It has been demonstrated to be successful in describing relationships and forecasting economic and financial time series, and providing more accurate forecasts than the univariate time series models and theory-based simultaneous equations models.

The structure of the VAR model is just the **generalization of the univariate AR** model. It is a system regression model that treats all the variables as endogenous, and allows each of the variables to depend on $p$ lagged values of itself and of all the other variables in the system.

A VAR model of order $p$ can be represented as

$$\mathbf y_t=\mathbf a_0 + \mathbf A_1 \mathbf y_{t-1} + \mathbf A_2 \mathbf y_{t-2} + ... + \mathbf A_p \mathbf y_{t-p} + \mathbf u_t = \mathbf a_0  + \sum_{j=1}^{p} \mathbf A_j \mathbf y_{t-j} + \mathbf u_t$$

where $\mathbf y_t$ is a $N\times 1$ vector containing $N$ endogenous variables, $\mathbf a_0$ is a $N\times 1$ vector of constants, $\mathbf A_1, \mathbf A_2, ..., \mathbf A_p$ are the $p$ $N\times N$ matrices of autoregressive coefficients, and $\mathbf u_t$ is a $N\times 1$ vector of white noise disturbances.

### VAR(1) Model
To illustrate, let's consider the simplest VAR model where we have $p=1$.

#### Structural and Reduced Forms of the VAR model

Consider the following bivariate system

$$ y_{1,t} = b_{1,0} - b_{1,2}y_{2,t} +  \varphi_{1,1}y_{1,t-1} + \varphi_{1,2}y_{2,t-1} + \varepsilon_{1,t}$$

$$ y_{2,t} = b_{2,0} - b_{2,1}y_{2,t} +  \varphi_{2,1}y_{1,t-1} + \varphi_{2,2}y_{2,t-1} + \varepsilon_{2,t}$$

where both $y_{1,t}$ and $y_{2,t}$ are assumed to be stationary, and $\varepsilon_{1,t}$ and $\varepsilon_{2,t}$ are the uncorrelated error terms with standard deviation $\sigma_{1,t}$ and $\sigma_{2,t}$, respectively.

In matrix notation:

Let

then

$$\mathbf B \mathbf y_t = \mathbf Q_0 + \mathbf Q_1 \mathbf y_{t-1} + \boldsymbol\varepsilon_t$$

##### Structural VAR (VAR in primitive form)
- Described by equation above
- Captures contemporaneous feedback effects ($b_{1,2}, b_{2,1}$)
- Not very practical to use
- Contemporaneous terms cannot be used in forecasting
- Needs further manipulation to make it more useful (e.g. multiplying the matrix equation by $\mathbf B^{-1}$)

Multiplying the matrix equation by $\mathbf B^{-1}$, we get

$$\mathbf y_t = \mathbf a_0 + \mathbf A_1 \mathbf y_{t-1} + \mathbf u_t$$

or

$$\mathbf y_t = \mathbf a_0 + \mathbf A_1 L\mathbf y_t + \mathbf u_t$$

where $\mathbf a_0 = \mathbf B^{-1}\mathbf Q_0$, $\mathbf A_1 = \mathbf B^{-1}\mathbf Q_1$, $L$ is the lag/backshift operator, and $\mathbf u_t = \mathbf B^{-1}\boldsymbol\varepsilon_t$, equivalently,

$$ y_{1,t} = a_{1,0} + a_{1,1}y_{1,t-1} + a_{1,2}y_{2,t-1} + u_{1,t}$$

$$ y_{2,t} = a_{2,0} + a_{2,1}y_{1,t-1} + a_{2,2}y_{2,t-1} + u_{2,t}$$

##### Reduced-form VAR (VAR in standard form)
- Described by equation above
- Only dependent on lagged endogenous variables (no contemporaneous feedback)
- Can be estimated using ordinary least squares (OLS)

#### VMA infinite representation and Stationarity

Consider the reduced form, standard VAR(1) model

$$\mathbf y_t = \mathbf a_0 + \mathbf A_1 \mathbf y_{t-1} + \mathbf u_t$$

Assuming that the process is weakly stationary and taking the expectation of $\mathbf y_t$, we have

$$E\left[\mathbf y_t\right] = \mathbf a_0 + \mathbf A_1 E\left[\mathbf y_{t-1}\right]$$

where $E\left[\mathbf u_t\right]=0.$ If we let $\tilde{\mathbf y}_{t}\equiv \mathbf y_t - \boldsymbol \mu$ be the mean-corrected time-series, we can write the model as

$$\tilde{\mathbf y}_{t} = \mathbf A_1 \tilde{\mathbf y}_{t-1} + \mathbf u_t$$

Substituting $\tilde{\mathbf y}_{t-1} = \mathbf A_1 \tilde{\mathbf y}_{t-2} + \mathbf u_{t-1}$,

$$\tilde{\mathbf y}_{t} = \mathbf A_1 \left( \mathbf A_1 \tilde{\mathbf y}_{t-2} + \mathbf u_{t-1}\right)+ \mathbf u_t$$

If we keep iterating, we get

$$\mathbf y_{t} = \boldsymbol\mu + \sum_{i=1}^{\infty} \mathbf A_1^i  \mathbf u_{t-i} + \mathbf u_t$$

Letting $\boldsymbol\Theta_i\equiv A_1^i$, we get the **VMA infinite representation**

$$\mathbf y_{t} = \boldsymbol\mu + \sum_{i=1}^{\infty} \boldsymbol\Theta_i  \mathbf u_{t-i} + \mathbf u_t$$

#### Stationarity of the VAR(1) model
- All the N eigenvalues of the matrix $A_1$ must be less than 1 in modulus, to avoid that the coefficient matrix $A_1^j$ from either exploding or converging to a nonzero matrix as $j$ goes to infinity.
- Provided that the covariance matrix of $u_t$ exists, the requirement that all the eigenvalues of $A_1$ are less than one in modulus is a necessary and sufficient condition for $y_t$ to be stable, and thus, stationary.
- All roots of $det\left(\mathbf I_N - \mathbf A_1 z\right)=0$ must lie outside the unit circle.

### VAR(p) Model

Consider the VAR(p) model described by

$$\mathbf y_t=\mathbf a_0 + \mathbf A_1 \mathbf y_{t-1} + \mathbf A_2 \mathbf y_{t-2} + ... + \mathbf A_p \mathbf y_{t-p} + \mathbf u_t = \mathbf a_0  + \sum_{j=1}^{p} \mathbf A_j \mathbf y_{t-j} + \mathbf u_t$$

Using the lag operator $L$, we get

$$\tilde{\mathbf A} (L) \mathbf y_t = \mathbf a_0 + \mathbf u_t$$

where $\tilde{\mathbf A} (L) = (\mathbf I_N - A_1 L - ... - A_p L^p)$. Assuming that $\mathbf y_t$ is weakly stationary, we obtain that

$$\boldsymbol\mu = E\left[\mathbf y_t\right] = (\mathbf I_N - A_1 L - ... - A_p L^p)^{-1} \mathbf a_0$$

Defining $\tilde{\mathbf y}_t=\mathbf y_t -\boldsymbol\mu$, we have

$$\tilde{\mathbf y}_t =  \mathbf A_1 \tilde{\mathbf y}_{t-1} + \mathbf A_2 \tilde{\mathbf y}_{t-2} + ... + \mathbf A_p \tilde{\mathbf y}_{t-p} + \mathbf u_t$$

##### Properties
- $Cov[\mathbf y_t, \mathbf u_t] = \Sigma_u$, the covariance matrix of $\mathbf u_t$
- $Cov[\mathbf y_{t-h}, \mathbf u_t] = \mathbf 0$ for any $h>0$
- $\boldsymbol\Gamma_h = \mathbf A_1 \boldsymbol\Gamma_{h-1} +...+\mathbf A_p \boldsymbol\Gamma_{h-p}$ for $h>0$
- $\boldsymbol\rho_h = \boldsymbol \Psi_1 \boldsymbol\Gamma_{h-1} +...+\boldsymbol \Psi_p \boldsymbol\Gamma_{h-p}$ for $h>0$, where $\boldsymbol \Psi_i = \mathbf D^{-1/2}\mathbf A_i D^{-1/2}$

#####  Stationarity of VAR(p) model
- All roots of $det\left(\mathbf I_N - \mathbf A_1 z - ...-  \mathbf A_p z^p\right)=0$ must lie outside the unit circle.

### Specification of a VAR model: Choosing the order *p*

Fitting a VAR model involves the selection of a single parameter: the model order or lag length $p$. The most common approach in selecting the best model is choosing the $p$ that minimizes one or more information criteria evaluated over a range of model orders. The criterion consists of two terms: the first one characterizes the entropy rate or prediction error, and second one characterizes the number of freely estimated parameters in the model. Minimizing both terms will allow us to identify a model that accurately models the data while preventing overfitting (due to too many parameters).

Some of the commonly used information criteria include: Akaike Information Criterion (AIC), Schwarz's Bayesian Information Criterion (BIC), Hannan-Quinn Criterion (HQ), and Akaike’s Final Prediction Error Criterion (FPE). The equation for each are shown below.

*Akaike's information criterion*

$$(M)\operatorname{AIC} = \operatorname{ln}|\tilde{\boldsymbol\Sigma}_u | + 2\dfrac{k}{T}$$

*Schwarz’s Bayesian information criterion*

$$(M)\operatorname{BIC} = \operatorname{ln}|\tilde{\boldsymbol\Sigma}_u | + \dfrac{k}{T}\operatorname{ln}(T)$$

*Hannan-Quinn’s information criterion*

$$(M)\operatorname{HQIC} = \operatorname{ln}|\tilde{\boldsymbol\Sigma}_u | + \dfrac{k}{T}\operatorname{ln}(\operatorname{ln}(T))$$

*Final prediction error*

$$\operatorname{FPE}(p) = \left(\dfrac{T+Np+1}{T-Np+1} \right)^N |\tilde{\boldsymbol\Sigma}_u | $$

$$\operatorname{ln}(\operatorname{FPE}(p)) = \operatorname{ln}|\tilde{\boldsymbol\Sigma}_u | + N \operatorname{ln}\left(\dfrac{T+Np+1}{T-Np+1} \right) $$

where $M$ stands for multivariate, $\tilde{\boldsymbol\Sigma}_u$ is the estimated covariance matrix of the residuals, $T$ is the number of observations in the sample, and $k$ is the total number of equations in the VAR($p$) (i.e. $N^2p + N$ where $N$ is the number of equations and $p$ is the number of lags).

Among the metrics above, AIC and BIC are the most widely used criteria, but BIC penalizes higher orders more. For moderate and large sample sizes, AIC and FPE are essentially equivalent, but FPE may outperform AIC for small sample sizes. HQ also penalizes higher order models more heavily than AIC, but less than BIC. However, under small sample conditions, AIC/FPE may outperform BIC and/or HQ in selecting true model order.

There are cases when AIC and FPE show no clear minimum over a range or model orders or lag lengths. In this case, there may be a clear "elbow" when we plot the values over model order. This may indicate a suitable model order.

## Building a VAR model
In this section, we show how we can use the VAR model to forecast the air quality data. The following steps are shown below:
1. Check for stationarity.
2. Split data into train and test sets.
3. Select the VAR order *p* that gives.
4. Fit VAR model of order *p* on the train set.
5. Generate forecast.
6. Evaluate model performance.

### Example 2: Forecasting Air Quality Data (CO, NO2 and RH)

For illustration, we consider the carbon monoxide, nitrous dioxide  and relative humidity time series from the Air Quality Dataset from 1 October 2014.

cols = ['CO(GT)', 'NO2(GT)', 'RH']
data_df = aq_df.loc[aq_df.index>'2004-10-01',cols]
fig,ax = plt.subplots(3, figsize=(15,5), sharex=True)
data_df.plot(ax=ax, subplots=True)
plt.xlabel('')
plt.tight_layout()
plt.show()

**Quick inspection before we proceed with modeling...**

To find out whether the multivariate approach is better than treating the signals separately as univariate time series, we examine the relationship between the variables using CCF. The sample below shows the CCF for the last 100 data points of the Air quality data for CO, NO2 and RH.

*CO and NO2*

sample_df = data_df.iloc[-100:]
ccf_y1_y2 = ccf(sample_df['CO(GT)'], sample_df['NO2(GT)'], unbiased=False)
ccf_y2_y1 = ccf(sample_df['NO2(GT)'], sample_df['CO(GT)'], unbiased=False)

fig, ax = plt.subplots(2, figsize=(15, 4), sharex=True, sharey=True)
d=1
ax[0].stem(np.arange(len(sample_df))[::d], ccf_y1_y2[::d], linefmt='C1-', markerfmt='C1o')
ax[1].stem(np.arange(len(sample_df))[::d], ccf_y2_y1[::d], linefmt='C1-', markerfmt='C1o')
ax[-1].set_ylim(-1, 1)
ax[0].set_xlim(0, 100)
ax[-1].set_xlabel('lag $h$', fontsize=14)
ax[0].set_ylabel(r'$\rho_{CO,NO2} (h)$', fontsize=14)
ax[1].set_ylabel(r'$\rho_{NO2,CO} (h)$', fontsize=14)
plt.tight_layout()
plt.show()

*CO and RH*

ccf_y1_y2 = ccf(sample_df['CO(GT)'], sample_df['RH'], unbiased=False)
ccf_y2_y1 = ccf(sample_df['RH'], sample_df['CO(GT)'], unbiased=False)

fig, ax = plt.subplots(2, figsize=(15, 4), sharex=True, sharey=True)
d=1
ax[0].stem(np.arange(len(sample_df))[::d], ccf_y1_y2[::d], linefmt='C1-', markerfmt='C1o')
ax[1].stem(np.arange(len(sample_df))[::d], ccf_y2_y1[::d], linefmt='C1-', markerfmt='C1o')
ax[-1].set_ylim(-1, 1)
ax[0].set_xlim(0, 100)
ax[-1].set_xlabel('lag $h$', fontsize=14)
ax[0].set_ylabel(r'$\rho_{CO,RH} (h)$', fontsize=14)
ax[1].set_ylabel(r'$\rho_{RH,CO} (h)$', fontsize=14)
plt.tight_layout()
plt.show()

*RH and NO2*

ccf_y1_y2 = ccf(sample_df['RH'], sample_df['NO2(GT)'], unbiased=False)
ccf_y2_y1 = ccf(sample_df['NO2(GT)'], sample_df['RH'], unbiased=False)

fig, ax = plt.subplots(2, figsize=(15, 4), sharex=True, sharey=True)
d=1
ax[0].stem(np.arange(len(sample_df))[::d], ccf_y1_y2[::d], linefmt='C1-', markerfmt='C1o')
ax[1].stem(np.arange(len(sample_df))[::d], ccf_y2_y1[::d], linefmt='C1-', markerfmt='C1o')
ax[-1].set_ylim(-1, 1)
ax[0].set_xlim(0, 100)
ax[-1].set_xlabel('lag $h$', fontsize=14)
ax[0].set_ylabel(r'$\rho_{RH,NO2} (h)$', fontsize=14)
ax[1].set_ylabel(r'$\rho_{NO2,RH} (h)$', fontsize=14)
plt.tight_layout()
plt.show()

**Observation/s**:

As shown in the plot above, we can see that there's a relationship between:
* CO and some lagged values of RH and NO2
* NO2 and some lagged values of RH and CO
* RH and some lagged values of CO and NO2

This shows that we can benefit from the multivariate approach, so we proceed with building the VAR model.

#### 1. Check stationarity

To check for stationarity, we use the Kwiatkowski–Phillips–Schmidt–Shin (KPSS) test and the Augmented Dickey-Fuller (ADF) test. For the data to be suitable for VAR modelling, we need each of the variables in the multivariate time series to be stationary. In both tests, we need the test statistic to be less than the critical values to say that a time series (a variable) to be stationary.

###### Kwiatkowski–Phillips–Schmidt–Shin (KPSS) test
Recall: Null hypothesis is that an observable time series is stationary around a deterministic trend (i.e. trend-stationary) against the alternative of a unit root.

test_stat, p_val = [], []
cv_1pct, cv_2p5pct, cv_5pct, cv_10pct = [], [], [], []
for c in data_df.columns:
    kpss_res = kpss(data_df[c].dropna(), regression='ct')
    test_stat.append(kpss_res[0])
    p_val.append(kpss_res[1])
    cv_1pct.append(kpss_res[3]['1%'])
    cv_2p5pct.append(kpss_res[3]['1%'])
    cv_5pct.append(kpss_res[3]['5%'])
    cv_10pct.append(kpss_res[3]['10%'])
kpss_res_df = pd.DataFrame({'Test statistic': test_stat,
                           'p-value': p_val,
                           'Critical value - 1%': cv_1pct,
                           'Critical value - 2.5%': cv_2p5pct,
                           'Critical value - 5%': cv_5pct,
                           'Critical value - 10%': cv_10pct},
                         index=data_df.columns).T
kpss_res_df.round(4)

**Observation/s**:

From the KPSS test, CO and RH are stationary.

###### Augmented Dickey-Fuller (ADF) test
Recall: Null hypothesis is that a unit root is present in a time series sample against the alternative that the time series is stationary.

test_stat, p_val = [], []
cv_1pct, cv_5pct, cv_10pct = [], [], []
for c in data_df.columns:
    adf_res = adfuller(data_df[c].dropna())
    test_stat.append(adf_res[0])
    p_val.append(adf_res[1])
    cv_1pct.append(adf_res[4]['1%'])
    cv_5pct.append(adf_res[4]['5%'])
    cv_10pct.append(adf_res[4]['10%'])
adf_res_df = pd.DataFrame({'Test statistic': test_stat,
                           'p-value': p_val,
                           'Critical value - 1%': cv_1pct,
                           'Critical value - 5%': cv_5pct,
                           'Critical value - 10%': cv_10pct},
                         index=data_df.columns).T
adf_res_df.round(4)

**Observation/s**:

From the ADF test, CO, NO2 and RH are stationary.

#### 2. Split data into train and test sets

We use the dataset from 01 October 2014 to predict the last 24 points (24 hrs/1 day) in the dataset.

forecast_length = 24
train_df, test_df = data_df.iloc[:-forecast_length], data_df.iloc[-forecast_length:]
test_df = test_df.filter(test_df.columns[~test_df.columns.str.contains('-d')])

train_df.reset_index().to_csv('../data/AirQualityUCI/train_data.csv')
test_df.reset_index().to_csv('../data/AirQualityUCI/test_data.csv')

fig,ax = plt.subplots(3, figsize=(15, 5), sharex=True)
train_df.plot(ax=ax, subplots=True)
plt.xlabel('')
plt.tight_layout()
plt.show()

#### 3. Select order *p*

We compute the different multivariate information criteria (AIC, BIC, HQIC), and FPE. We pick the set of order parameters that correspond to the lowest values.

aic, bic, fpe, hqic = [], [], [], []
model = VAR(train_df)
p = np.arange(1,60)
for i in p:
    result = model.fit(i)
    aic.append(result.aic)
    bic.append(result.bic)
    fpe.append(result.fpe)
    hqic.append(result.hqic)
lags_metrics_df = pd.DataFrame({'AIC': aic,
                                'BIC': bic,
                                'HQIC': hqic,
                                'FPE': fpe},
                               index=p)
fig, ax = plt.subplots(1, 4, figsize=(15, 3), sharex=True)
lags_metrics_df.plot(subplots=True, ax=ax, marker='o')
plt.tight_layout()

**Observation/s**:

We find BIC and HQIC to be lowest at $p=26$, and we also observe an elbow in the plots for AIC, and FPE, so we choose the number of lags to be 26.

#### 3. Fit VAR model with chosen order

%%time
var_model = model.fit(26)
var_model.summary()

#### 4. Get forecast

forecast_var = pd.DataFrame(var_model.forecast(train_df.values,
                                              steps=forecast_length),
                           columns=train_df.columns,
                           index=test_df.index)
forecast_var = forecast_var.rename(columns={c: c+'-VAR' for c in forecast_var.columns})

for c in train_df.columns:
    fig, ax = plt.subplots(figsize=[15, 2])
    pd.concat([train_df[[c]], forecast_var[[c+'-VAR']]], axis=1).plot(ax=ax)
    plt.xlim(left=pd.to_datetime('2005-03-01'))
    plt.xlabel('')
#     plt.tight_layout()
    plt.show()

#### Performance Evaluation: Comparison with ARIMA model

When using ARIMA, we treat each variable as a univariate time series, and we perform the forecasting for each variable: 1 for CO, 1 for NO2, and 1 for RH

# For model order selection, refer to Chapter 1
selected_order = {'CO(GT)': [(0, 1, 0)],
                  'NO2(GT)': [(0, 1, 0)],
                  'RH': [(3, 1, 1)]}

%%time
forecast_arima = {}
for c in cols:
    forecast_arima[c+'-ARIMA'] = utils.forecast_arima(train_df[c].values,
                                                      test_df[c].values,
                                                      order=selected_order[c][0])
forecast_arima = pd.DataFrame(forecast_arima, index=forecast_var.index)
forecast_arima.head()

forecasts = pd.concat([forecast_arima, forecast_var], axis=1)
for c in cols:
    fig, ax = utils.plot_forecasts_static(train_df=train_df,
                                          test_df=test_df,
                                          forecast_df=forecasts,
                                          column_name=c,
                                          min_train_date='2005-04-01',
                                          suffix=['-VAR', '-ARIMA'],
                                          title=c)

**Performance Metrics**

pd.concat([utils.test_performance_metrics(test_df, forecast_var, suffix='-VAR'),
           utils.test_performance_metrics(test_df, forecast_arima, suffix='-ARIMA')], axis=1)

**Observation/s**:

- MAE: VAR forecasts have lower errors than ARIMA forecasts for CO and NO2 but not in RH.
- MSE: VAR forecasts have lower errors for all variables (CO, NO2 and RH).
- MAPE: VAR forecasts have lower errors for all variables (CO, NO2 and RH).
- Training time is significantly reduced when using VAR compared to ARIMA (<0.1s run time for VAR while ~20s for ARIMA)

## Structural VAR Analysis

In addition to forecasting, VAR models are also used for structural inference and policy analysis. In macroeconomics, this structural analysis has been extensively employed to investigate the transmission mechanisms of macroeconomic shocks (e.g., monetary shocks, financial shocks) and test economic theories. There are particular assumptions imposed about the causal structure of the dataset, and the resulting causal impacts of unexpected shocks (also called innovations or perturbations) to a specific variable on the different variables in the model are summarized. In this section, we cover two of the common methods in summarizing the effects of these causal impacts: (1) impulse response functions, and (2): forecast error variance decompositions.

### Impulse Response Function (IRF)

- Coefficients of the VAR models are often difficult to interpret so practitioners often estimate the impulse response function.
- IRFs trace out the time path of the effects of an exogenous shock to one (or more) of the endogenous variables on some or all of the other variables in a VAR system.
- IRF traces out the response of the dependent variable of the VAR system to shocks (also called innovations or impulses) in the error terms.

###### IRF in the VAR system for Air Quality

Let $y_{1,t}$, $y_{2,t}$ and $y_{3,t}$ be the time series corresponding to CO signal, NO2 signal, and RH signal, respectively. Consider the moving average representation of the system shown below:

Let

Suppose $u_1$ in the first equation increases by a value of one standard deviation.
- This shock will change $y_1$ in the current as well as the future periods.
- This shock will also have an impact on $y_2$ and $y_3$.

Suppose $u_2$ in the second equation increases by a value of one standard deviation.
- This shock will change $y_2$ in the current as well as the future periods.
- This shock will also have an impact on $y_1$ and $y_3$.

Suppose $u_3$ in the third equation increases by a value of one standard deviation.
- This shock will change $y_3$ in the current as well as the future periods.
- This shock will also have an impact on $y_1$ and $y_2$.

irf = var_model.irf(periods=8)
ax = irf.plot(orth=True,
              subplot_params={'fontsize': 10})

**Observation/s:**

*Effects of exogenous perturbation/shocks (1SD) of a variable on itself:*

- CO $\rightarrow$ CO: A shock in the value of CO has a larger effect CO in the early hours but this decays over time.
- NO2 $\rightarrow$ NO2: A shock in the value of NO2 has a larger effect NO2 in the early hours but this decays over time.
- RH $\rightarrow$ RH: A shock in the value of RH has a largest effect in RH after 1 hour and this effect decays over time.

*Effects of exogenous perturbation/shocks of a variable on another:*

- CO $\rightarrow$ NO2: A shock in the value of CO has a largest effect in NO2 after 1 hour and this effect decays over time.
- CO $\rightarrow$ RH: A shock in the value of CO has an immediate effect in the value of RH. However, the effect decreases immediately after an hour, and the value seems to stay at around 0.2.
- NO2 $\rightarrow$ CO: A shock in NO2 only causes a small effect in the values of CO. There seems to be a delayed effect, peaking after 3 hours, but the magnitude is still small.
- NO2 $\rightarrow$ RH: A shock in NO2 causes a small (negative) effect in the values of RH. The magnitude seems to decline further after 6 hours. The value of the IRF reaches zero in about 7 hours.
- RH $\rightarrow$ CO: A shock in RH only causes a small effect in the values of CO.
- RH $\rightarrow$ NO2: A shock in the value of RH has a largest effect in NO2 after 1 hour and this effect decays over time. The value of the IRF reaches zero after 6 hours.

### Forecast Error Variance Decomposition (FEVD)
- FEVD indicates the amount of information each variable contributes to the other variables in the autoregression
- While impulse response functions trace the effects of a shock to one endogenous variable on to the other variables in the VAR, variance decomposition separates the variation in an endogenous variable into the component shocks to the VAR.
- It determines how much of the forecast error variance of each of the variables can be explained by exogenous shocks to the other variables.

fevd = var_model.fevd(8)
ax = fevd.plot(figsize=(10, 8))
plt.show()

**Observation/s:**
* For CO, the variance is mostly explained by exogenous shocks to CO. This decreases over time but only by a small amount.
* For NO2, the variance is mostly explained by exogenous shocks to NO2 and CO.
* For RH, the variance is mostly explained by exogenous shocks to RH. Over time, the contribution of the exogenous shocks to CO increases.

### Example 3: Forecasting the Jena climate data

We try to forecast the Jena climate data using the method outlined above. We will train the VAR model using hourly weather measurements from January 1, 2019 (00:10) up to December 29, 2014 (18:10). The performance of the model will be evaluated on the test set which contains data from December 29, 2014 (19:10) to December 31, 2014 (23:20) which is equivalent to 17,523 data points for each of the variables.

##### Load dataset

train_df = pd.read_csv('../data/train_series_datetime.csv',index_col=0).set_index('Date Time')
val_df = pd.read_csv('../data/val_series_datetime.csv',index_col=0).set_index('Date Time')
test_df = pd.read_csv('../data/test_series_datetime.csv',index_col=0).set_index('Date Time')
train_df.index = pd.to_datetime(train_df.index)
val_df.index = pd.to_datetime(val_df.index)
test_df.index = pd.to_datetime(test_df.index)

train_val_df = pd.concat([train_df, val_df])

##### Check stationarity of each variable using ADF test

%%time
test_stat, p_val = [], []
cv_1pct, cv_5pct, cv_10pct = [], [], []
for c in train_val_df.columns:
    adf_res = adfuller(train_val_df[c].dropna())
    test_stat.append(adf_res[0])
    p_val.append(adf_res[1])
    cv_1pct.append(adf_res[4]['1%'])
    cv_5pct.append(adf_res[4]['5%'])
    cv_10pct.append(adf_res[4]['10%'])
adf_res_df = pd.DataFrame({'Test statistic': test_stat,
                           'p-value': p_val,
                           'Critical value - 1%': cv_1pct,
                           'Critical value - 5%': cv_5pct,
                           'Critical value - 10%': cv_10pct},
                         index=train_df.columns).T
adf_res_df.round(4)

((adf_res_df.loc['Test statistic']< adf_res_df.loc['Critical value - 1%']) &
(adf_res_df.loc['Test statistic']< adf_res_df.loc['Critical value - 5%']) &
( adf_res_df.loc['Test statistic']< adf_res_df.loc['Critical value - 10%']))

**Observation/s**:

From the values above, all the components of the Jena climate data are stationary, so we'll use all the variables in our VAR model.

##### Select order p

%%time
aic, bic, fpe, hqic = [], [], [], []
model = VAR(train_val_df)
p = np.arange(1,60)
for i in p:
    result = model.fit(i)
    aic.append(result.aic)
    bic.append(result.bic)
    fpe.append(result.fpe)
    hqic.append(result.hqic)
lags_metrics_df = pd.DataFrame({'AIC': aic,
                                'BIC': bic,
                                'HQIC': hqic,
                                'FPE': fpe},
                               index=p)
fig, ax = plt.subplots(1, 4, figsize=(15, 3), sharex=True)
lags_metrics_df.plot(subplots=True, ax=ax, marker='o')
plt.tight_layout()

lags_metrics_df.idxmin()

lags_metrics_df[25:].idxmin()

**Observation/s**:

The model order that resulted to the minimum value varies for each information criteria, showing no clear minimum. We see an elbow at $p=5$, but if we look at AIC and HQIC we're observing another elbow/local minimum at $p=26$. So, we choose $p=26$ as our lag length.

##### Train VAR model using the training and validation data

%%time
var_model = model.fit(26)

test_index = np.arange(len(test_df)- (len(test_df))%24).reshape((-1, 24))
fit_index = [test_index[:i].flatten() for i in range(len(test_index))]

##### Forecast 24-hour weather measurements and evaluate performance on test set

forecasts_df = []
for n in range(len(fit_index)):
    forecast_var = pd.DataFrame(var_model.forecast(
        pd.concat([train_val_df, test_df.iloc[fit_index[n]]]).values, steps=24),
                                columns=train_val_df.columns,
                                index=test_df.iloc[test_index[n]].index)
    forecasts_df.append(forecast_var)
forecasts_df = pd.concat(forecasts_df)
forecasts_df.columns = forecasts_df.columns+['-VAR']

for c in train_val_df.columns:
    fig, ax = plt.subplots(figsize=[14, 2])
    train_val_df[c].plot(ax=ax)
    forecasts_df[[c+'-VAR']].plot(ax=ax)
    plt.ylim(train_val_df[c].min(), train_val_df[c].max())
    plt.legend(loc=2)
    plt.tight_layout()
    plt.show()

***Evaluate forecasts***

utils.test_performance_metrics(test_df.loc[forecasts_df.index], forecasts_df, suffix='-VAR').loc[['MAE', 'MSE']]

T_MAEs = []
for n in range(len(test_index)):
    index = forecasts_df.iloc[test_index[n]].index
    T_MAEs.append(utils.mean_absolute_error(test_df.loc[index, 'T (degC)'].values,
                                            forecasts_df.loc[index, 'T (degC)-VAR'].values))

print(f'VAR(26) MAE: {np.mean(T_MAEs)}')

**Observation/s**: The VAR(26) model outperformed the [naive (MAE= 3.18), seasonal naive (MAE= 2.61)](../08_WinningestMethods/lightgbm_jena_forecasting.ipynb) and [ARIMA (MAE= 3.19)](../01_AutoRegressiveIntegratedMovingAverage/01_AutoRegressiveIntegratedMovingAverage.ipynb) models.

##### Forecast 24 hours beyond test set

all_data = pd.concat([train_df, val_df, test_df])

future_index = [all_data.index[-1]+pd.Timedelta(f'{h} hour') for h in np.arange(24)+1]

forecast_var_future = pd.DataFrame(var_model.forecast(all_data.values,
                                                      steps=24),
                                   columns=all_data.columns,
                                   index=future_index)

forecast_var_future.columns = forecast_var_future.columns+['-VAR']

for c in train_val_df.columns:
    fig, ax = plt.subplots(figsize=[14, 2])
    all_data.iloc[-500:][c].plot(ax=ax)
    forecast_var_future[[c+'-VAR']].plot(ax=ax)
    plt.ylim(train_val_df[c].min(), train_val_df[c].max())
    plt.legend(loc=2)
    plt.tight_layout()
    plt.show()

## Summary
* VAR methods are useful when dealing with multivariate time series, as they allow us to use the relationship between the different variable to forecast.
* These models allow us to forecast the different variables simultaneously, with the added benefit of easy (only 1 hyperparameter) and fast training.
* Using the fitted VAR model, we can also explain the relationship between variables, and how the perturbation in one variable affects the others by getting the impulse response functions and the variance decomposition of the forecasts.
* However, the application of these models is limited due to the stationarity requirement for ALL the variables in the multivariate time series. This method won't work well if there is at least one variable that's non-stationary. When dealing with non-stationary multivariate time series, one can explore the use of vector error correction models (VECM).

## Preview to the Next Chapter
In the next chapter, we further extend the use of VAR models to explain the relationships between variables in a multivariate time series using **Granger causality**, which is one of the most common ways to describe causal mechanisms in time series data.

## References
**Main references**
* Lütkepohl, H. (2005). New introduction to multiple time series analysis. Berlin: Springer.
* Kilian, L., & Lütkepohl, H. (2018). Structural vector autoregressive analysis. Cambridge: Cambridge University Press.

**Supplementary references** are listed [here](VARreferences.txt).

import numpy,import statsmodels,from statsmodels


"""
Provides an API to define epidemiological
models in terms of sympy symbolic expressions.
"""

import warnings

import numpy as np
import scipy.sparse as sprs

import sympy

from epipack.numeric_epi_models import EpiModel, custom_choice
from IPython.display import Math, display
from sympy.printing.theanocode import theano_function

class SymbolicMixin():
    """
    Provides methods that are useful to both
    :class:`epipack.symbolic_epi_models.SymbolicEpiModel`
    and
    :class:`epipack.symbolic_matrix_epi_models.SymbolicMatrixEpiModel`
    """

    def ODEs(self):
        """
        Obtain the equations of motion for this model in form of equations.
        """
        t = sympy.symbols("t")
        Eqs = []
        ynew = self.dydt()
        for compartment, expr in zip(self.compartments,ynew):
            dXdt = sympy.Derivative(compartment, t)
            Eqs.append(sympy.Eq(dXdt, sympy.simplify(expr)))
        return Eqs

    def ODEs_jupyter(self):
        """
        Pretty-print the equations of motion for this model in a Jupyter notebook.
        """

        for ode in self.ODEs():
            display(Math(sympy.latex(ode)))

    def find_fixed_points(self):
        """
        Find states in which this model is stationary (fixed points).
        """
        ynew = [expr for expr in self.dydt()]
        return sympy.nonlinsolve(ynew, self.compartments)

    def jacobian(self,simplify=True):
        """
        Obtain the Jacobian for this model.

        Parameters
        ----------
        simplify : bool, default = True
            If ``True``, `epipack` will try to simplify
            the evaluated Jacobian. This might not be
            desirable in some cases due to its
            long evaluation time, which is why
            it can be turned off.
        """

        try:
            self.linear_rates
            has_matrix = True
        except AttributeError as e:
            has_matrix = False

        if has_matrix and not self.has_functional_rates:
            y = sympy.Matrix(self.compartments)
            J = sympy.Matrix(self.linear_rates)

            for i in range(self.N_comp):
                J[i,:] += (self.quadratic_rates[i] * y + self.quadratic_rates[i].T * y).T

        else:
            y = sympy.Matrix(self.compartments)
            J = sympy.zeros(self.N_comp, self.N_comp)
            dydt = self.dydt()
            for i in range(self.N_comp):
                for j in range(self.N_comp):
                    J[i,j] = sympy.diff(dydt[i], self.compartments[j])

        if simplify:
            J = sympy.simplify(J)

        return J

    def get_jacobian_at_fixed_point(self,fixed_point_dict,simplify=True):
        """
        Obtain the Jacobian at a given fixed point.

        Parameters
        ----------
        fixed_point_dict : dict
            A dictionary where a compartment symbol maps to an expression
            (the value of this compartment in the fixed point).
            If compartments are missing, it is implicitly assumed
            that this compartment has a value of zero.
        simplify : bool
            whether or not to let sympy try to simplify the expressions

        Returns
        -------
        J : sympy.Matrix
            The Jacobian matrix at the given fixed point.

        """

        fixed_point = self._convert_fixed_point_dict(fixed_point_dict)

        J = self.jacobian(False)

        for compartment, value in fixed_point:
            J = J.subs(compartment, value)

        if simplify:
            J = sympy.simplify(J)

        return J

    def get_eigenvalues_at_fixed_point(self,fixed_point_dict):
        """
        Obtain the Jacobian's eigenvalues at a given fixed point.

        Parameters
        ----------
        fixed_point_dict : dict
            A dictionary where a compartment symbol maps to an expression
            (the value of this compartment in the fixed point).
            If compartments are missing, it is implicitly assumed
            that this compartment has a value of zero.

        Returns
        -------
        eigenvalues : dict
            Each entry maps an eigenvalue expression to its multiplicity.
        """
        J = self.get_jacobian_at_fixed_point(fixed_point_dict)
        return J.eigenvals()

    def get_eigenvalues_at_disease_free_state(self,disease_free_state=None):
        """
        Obtain the Jacobian's eigenvalues at the disease free state.

        Parameters
        ----------
        disease_free_state : dict, default = None
            A dictionary where a compartment symbol maps to an expression
            (the value of this compartment in the fixed point).
            If compartments are missing, it is implicitly assumed
            that this compartment has a value of zero.

            If ``None``, the disease_free_state is assumed to be at
            ``disease_free_state = { S: 1 }``.

        Returns
        -------
        eigenvalues : dict
            Each entry maps an eigenvalue expression to its multiplicity.

        """

        if disease_free_state is None:
            S = sympy.symbols("S")
            if S not in self.compartments:
                raise ValueError("The disease free state was not provided to the method. "+\
                                 "I tried to assume the disease free state is at S = 1, "+\
                                 "but no `S`-compartment was found.")
            disease_free_state = {S:1}

        return self.get_eigenvalues_at_fixed_point(disease_free_state)

    def _convert_fixed_point_dict(self,fixed_point_dict):
        """
        Get a fixed point item iterator.

        Parameters
        ----------
        fixed_point_dict : dict
            A dictionary where a compartment symbol maps to an expression
            (the value of this compartment in the fixed point).
            If compartments are missing, it is implicitly assumed
            that this compartment has a value of zero.

        Returns
        -------
        fixed_point : iterator of list of tuple
            A list that is ``N_comp`` entries long.
            Each entry contains a compartment symbol and
            an expression that corresponds to the value
            this compartment assumes in the fixed point.
        """
        fixed_point = {}
        for compartment in self.compartments:
            fixed_point[compartment] = 0
        fixed_point.update(fixed_point_dict)
        return fixed_point.items()

    def set_parameter_values(self,parameter_values):
        """
        Set numerical values for the parameters of this model.
        This might include free symbols that are part of symbolized
        rate functions.

        Parameters
        ==========
        parameter_values : dict
            A dictionary mapping symbols to
            numerical values.
        """
        self.parameter_values = parameter_values

    def get_numerical_dydt(self,lambdify_modules='numpy'):
        r"""
        Returns values of the given compartments at the demanded
        time points (as a numpy.ndarray of shape
        ``(return_compartments), len(time_points)``.

        Parameters
        ==========
        time_points : np.ndarray
            An array of time points at which the compartment values
            should be evaluated and returned.
        return_compartments : list, default = None
            A list of compartments for which the result should be returned.
            If ``return_compartments`` is None, all compartments will
            be returned.
        integrator : str, default = 'dopri5'
            Which method to use for integration. Currently supported are
            ``'euler'`` and ``'dopri5'``. If ``'euler'`` is chosen,
            :math:`\delta t` will be determined by the difference
            of consecutive entries in ``time_points``.
        adopt_final_state : bool, default = False
            Whether or not to adopt the final state of the integration

        Returns
        =======
        dydt : func
            A function ``dydt(t, y, *args, **kwargs)`` that returns
            the numerical momenta of this system at time ``t`` and
            state vector ``y``.
        """

        these_symbols = [sympy.symbols("t")] + self.compartments

        params = list(self.parameter_values.items())
        param_symbols = set([_p[0] for _p in params])

        odes = [ ode.subs(params) for ode in self.dydt() ]

        not_set = []
        for ode in odes:
            not_set.extend(ode.free_symbols)

        not_set = set(not_set) - set(these_symbols)

        if len(not_set) > 0:
            raise ValueError("Parameters", set(not_set), "have not been set. Please set them using",
                             "SymbolicEpiModel.parameter_values()")

        F_sympy = sympy.lambdify(these_symbols, odes, modules=lambdify_modules)

        def dydt(t, y, *args, **kwargs):
            these_args = [t] + y.tolist()
            return np.array(F_sympy(*these_args))

        return dydt

    def get_numerical_event_and_rate_functions(self):
        """
        Converts the symbolic event lists and corresponding
        symbolic rates to functions that return numeric
        event lists and numeric rates based on the current
        time and state vector.

        This function is needed in the
        :class:`epipack.numeric_epi_models.EpiModel`
        base class for stochastic simulations.

        Returns
        -------
        get_event_rates : func
            A function that takes the current time ``t`` and
            state vector ``y``
            and returns numerical event rate lists.
        get_compartment_changes : funx
            A function that takes a numerical list of event ``rates``
            and returns a random event state change vector
            with probability proportional to its entry in ``rates``.
        """
        rates = self.birth_rate_functions + self.linear_rate_functions
        if self.correct_for_dynamical_population_size:
            population_size = sum(self.compartments)
        else:
            population_size = self.initial_population_size
        rates += [ r/population_size for r in self.quadratic_rate_functions ]
        events = self.birth_event_updates + self.linear_event_updates + self.quadratic_event_updates

        these_symbols = [sympy.symbols("t")] + self.compartments

        params = list(self.parameter_values.items())
        param_symbols = set([_p[0] for _p in params])

        rates = [ sympy.sympify(rate).subs(params) for rate in rates ]
        events = [ np.array(event).astype(np.float64).flatten() for event in events ]

        not_set = []
        for rate in rates:
            not_set.extend(rate.free_symbols)

        not_set = set(not_set) - set(these_symbols)

        if len(not_set) > 0:
            raise ValueError("Parameters " + str(set(not_set)) +\
                             "have not been set. Please set them using " +\
                             "SymbolicEpiModel.parameter_values()")

        sympy_rates = sympy.lambdify(these_symbols, rates)

        def get_event_rates(t, y):
            """
            Returns numerical event rate lists based on current
            state vector ``y`` and time ``t``.
            """
            these_args = [t] + y.tolist()
            return np.array(sympy_rates(*these_args))

        def get_compartment_changes(rates):
            """
            Choose an event change vector based on the current ``rates``.
            """
            idy = custom_choice(rates/rates.sum())
            return events[idy]

        return get_event_rates, get_compartment_changes

def get_temporal_interpolation(time_data, value_data, interpolation_degree=1):
    """
    Obtain a symbolic piecewise function that interpolates between values
    given in ``value_data`` for the intervals defined in ``time_data``,
    based on a spline interpolation of degree ``interpolation_degree``.
    If ``interpolation_degree == 0``, the function changes according to step
    functions. In this case ``time_data`` needs to have one value more than
    ``value_data``.

    The values in ``time_data`` and ``value_data`` can be symbols or numeric
    values.

    Parameters
    ----------
    time_data : list
        Sorted list of time values.
    value_data : list
        List of values corresponding to the times given in ``time_data``.
    interpolation_degree : int
        The degree of the polynomial that interpolates between values.
    """
    t = sympy.symbols("t")
    if interpolation_degree == 0:
        if len(time_data) != len(value_data)+1:
            raise ValueError("For ``interpolation_degree == 0``, `time_data`` " +\
                             "needs to have one value more than ``value_data``.")
        return sympy.Piecewise(*[
                    (v, (time_data[i] <= t) & ( t < time_data[i+1])) \
                    for i, v in enumerate(value_data)
                ])
    else:
        return sympy.interpolating_spline(interpolation_degree, t, time_data, value_data)

class SymbolicEpiModel(SymbolicMixin, EpiModel):
    """
    Define a model based on the analytical framework
    offered by `Sympy <https://sympy.org/>`_.

    This class uses the event-based framework
    where state-change vectors are associated with
    event rates.

    Parameters
    ==========
    compartments : list
        A list of :class:`sympy.Symbol` instances that
        symbolize compartments.
    initial_population_size : float, default = 1.0
        The population size at :math:`t = 0`.
    correct_for_dynamical_population_size : bool, default = False
        If ``True``, the quadratic coupling terms will be
        divided by the sum of all compartments, otherwise they
        will be divided by the initial population size.

    Attributes
    ==========
    compartments : list
        A list of :class:`sympy.Symbol` instances that
        symbolize compartments.
    N_comp : :obj:`int`
        Number of compartments (including population number)
    parameter_values : dict
        Maps parameter symbols to numerical values,
    initial_population_size : float
        The population size at :math:`t = 0`.
    correct_for_dynamical_population_size : bool
        If ``True``, the quadratic coupling terms will be
        divided by the sum of all compartments, otherwise they
        will be divided by the initial population size.
    birth_rate_functions : list of symbolic expressions
        A list of functions that return rate values based on time ``t``
        and state vector ``y``. Each entry corresponds to an event update
        in ``self.birth_event_updates``.
    birth_event_updates : list of sympy.Matrix
        A list of vectors. Each entry corresponds to a rate in
        ``birth_rate_functions`` and quantifies the change in
        individual counts in the compartments.
    linear_rate_functions : list of symbolic expressions
        A list of functions that return rate values based on time ``t``
        and state vector ``y``. Each entry corresponds to an event update
        in ``self.linear_event_updates``.
    linear_event_updates : list of sympy.Matrix
        A list of vectors. Each entry corresponds to a rate in
        ``linear_rate_functions`` and quantifies the change in
        individual counts in the compartments.
    quadratic_rate_functions : list of symbolic expressions
        A list of functions that return rate values based on time ``t``
        and state vector ``y``. Each entry corresponds to an event update
        in ``self.quadratic_event_updates``.
    quadratic_event_updates : list of sympy.Matrix
        A list of vectors. Each entry corresponds to a rate in
        ``quadratic_rate_functions`` and quantifies the change in
        individual counts in the compartments.
    y0 : numpy.ndarray
        The initial conditions.
    rates_have_explicit_time_dependence : bool
        Internal switch that's flipped when a non-constant
        rate is passed to the model.

    """

    def __init__(self,compartments,
                      initial_population_size=1,
                      correct_for_dynamical_population_size=False,
                      ):

        EpiModel.__init__(self,
                      compartments,
                      initial_population_size=initial_population_size,
                      correct_for_dynamical_population_size=correct_for_dynamical_population_size,
                )

        self.t = sympy.symbols("t")
        if self.t in self.compartments:
            raise ValueError("Don't use `t` as a compartment symbol, as it is reserved for time.")

        self.parameter_values = {}

    def _check_rate_for_functional_dependency(self,rate):
        """
        Sets the attribute ``rates_have_explicit_time_dependence``
        to ``True`` if ``rate`` has an explicit functional dependency
        on a :class:`sympy.Symbol` called ``"t"``.
        """
        try:
            t = sympy.symbols("t")
            has_time_dependence = t in rate.free_symbols
            self.rates_have_explicit_time_dependence |= has_time_dependence
        except AttributeError as e:
            return

    def set_linear_events(self,event_list,allow_nonzero_column_sums=False,reset_events=True):
        r"""
        Define the linear transition events between compartments.

        Parameters
        ==========
        event_list : :obj:`list` of :obj:`tuple`
            A list of tuples that contains transition events in the
            following format:

            .. code:: python

                [
                    (
                        ("affected_compartment_0",),
                        rate,
                        [
                            ("affected_compartment_0", dN0),
                            ("affected_compartment_1", dN1),
                            ...
                        ],
                     ),
                    ...
                ]

        allow_nonzero_column_sums : :obj:`bool`, default : False
            Traditionally, epidemiological models preserve the
            total population size. If that's not the case,
            switch off testing for this.
        reset_events : bool, default : True
            Whether to reset all linear events to zero before
            converting those.

        Example
        -------
        For an SEIR model with infectious period ``tau``
        and incubation period ``theta``.

        .. code:: python

            epi.set_linear_events([
                ( ("E",),
                  1/theta,
                  [ ("E", -1), ("I", +1) ]
                ),
                ( ("I",),
                  1/tau,
                  [ ("I", -1), ("R", +1) ]
                ),
            ])

        Read as "compartment E reacts with rate :math:`1/\theta`
        which leads to the decay of one E particle to one I particle."

        """

        if reset_events:
            birth_rate_functions = []
            birth_event_updates = []
            linear_rate_functions = []
            linear_event_updates = []
        else:
            linear_event_updates = self.linear_event_updates
            birth_event_updates = self.birth_event_updates
            linear_rate_functions = self.linear_rate_functions
            birth_rate_functions = self.birth_rate_functions

        for acting_compartments, rate, affected_compartments in event_list:

            dy = sympy.zeros(self.N_comp,1)
            for trg, change in affected_compartments:
                _t = self.get_compartment_id(trg)
                dy[_t] += change

            if acting_compartments[0] is None:
                self._check_rate_for_functional_dependency(rate)
                birth_event_updates.append( dy )
                birth_rate_functions.append( rate )
            else:
                # check if compartment was defined as a function
                this_compartment = acting_compartments[0]

                self._check_rate_for_functional_dependency(rate)
                this_rate = rate * this_compartment
                linear_event_updates.append( dy )
                linear_rate_functions.append( this_rate )

        if not allow_nonzero_column_sums and len(linear_rate_functions)>0:
            _y = np.ones(self.N_comp)
            test = sympy.zeros(self.N_comp,1)
            for dy, r in zip (linear_event_updates, linear_rate_functions):
                test += dy * r
            for dy, r in zip (birth_event_updates, birth_rate_functions):
                test += dy * r
            test_sum = sum(test)
            if test_sum != 0:
                warnings.warn("events do not sum to zero for each column:" + str(test_sum))

        self.linear_event_updates = linear_event_updates
        self.linear_rate_functions = linear_rate_functions
        self.birth_event_updates = birth_event_updates
        self.birth_rate_functions = birth_rate_functions

        return self

    def set_quadratic_events(self,event_list,allow_nonzero_column_sums=False,reset_events=True):
        r"""
        Define the quadratic transition events between compartments.

        Parameters
        ----------
        event_list : :obj:`list` of :obj:`tuple`
            A list of tuples that contains transmission events in the following format:

            .. code:: python

                [
                    (
                        ("coupling_compartment_0", "coupling_compartment_1"),
                        rate,
                        [
                            ("affected_compartment_0", dN0),
                            ("affected_compartment_1", dN1),
                            ...
                        ],
                     ),
                    ...
                ]

        allow_nonzero_column_sums : :obj:`bool`, default : False
            Traditionally, epidemiological models preserve the
            total population size. If that's not the case,
            switch off testing for this.
        reset_events : bool, default : True
            Whether to reset all linear events to zero before
            converting those.

        Example
        -------
        For an SEIR model with infection rate ``eta``.

        .. code:: python

            epi.set_quadratic_events([
                ( ("S", "I"),
                  eta,
                  [ ("S", -1), ("E", +1) ]
                ),
            ])

        Read  as

        "Coupling of *S* and *I* leads to
        the decay of one *S* particle to one *E* particle with
        rate :math:`\eta`.".
        """

        if reset_events:
            quadratic_event_updates = []
            quadratic_rate_functions = []
        else:
            quadratic_event_updates = self.quadratic_event_updates
            quadratic_rate_functions = self.quadratic_rate_functions

        for coupling_compartments, rate, affected_compartments in event_list:

            _s0 = coupling_compartments[0]
            _s1 = coupling_compartments[1]

            dy = sympy.zeros(self.N_comp,1)
            for trg, change in affected_compartments:
                _t = self.get_compartment_id(trg)
                dy[_t] += change

            self._check_rate_for_functional_dependency(rate)
            this_rate = rate * _s0 * _s1

            quadratic_event_updates.append( dy )
            quadratic_rate_functions.append( this_rate )

        if not allow_nonzero_column_sums and len(quadratic_rate_functions)>0:
            dy = sympy.zeros(self.N_comp,1)
            test = sympy.zeros(self.N_comp,1)
            for dy, r in zip (quadratic_event_updates, quadratic_rate_functions):
                test += dy * r
            test_sum = sum(test)
            if test_sum != 0:
                warnings.warn("events do not sum to zero for each column:" + str(test_sum))

        self.quadratic_event_updates = quadratic_event_updates
        self.quadratic_rate_functions = quadratic_rate_functions

        return self

    def dydt(self):
        """
        Compute the momenta of the epidemiological model as
        symbolic expressions.

        Parameters
        ----------
        t : :obj:`float`
            Current time
        y : numpy.ndarray
            The entries correspond to the compartment frequencies
            (or counts, depending on population size).
        """
        ynew = sympy.zeros(self.N_comp,1)

        for dy, r in zip(self.birth_event_updates, self.birth_rate_functions):
            ynew += r * dy

        for dy, r in zip(self.linear_event_updates, self.linear_rate_functions):
            ynew += r * dy

        if self.correct_for_dynamical_population_size:
            population_size = sum(self.compartments)
        else:
            population_size = self.initial_population_size

        for dy, r in zip(self.quadratic_event_updates, self.quadratic_rate_functions):
            ynew += r/population_size * dy

        return ynew

class SymbolicODEModel(SymbolicEpiModel):
    """
    Define a model purely based on a list of ODEs.

    Parameters
    ==========
    ODEs : list
        A list of symbolic ODEs in format

        .. code:: python

            sympy.Eq(sympy.Derivative(Y, t), expr)
    """

    def __init__(self, ODEs):

        compartments = self._get_compartments_and_assert_derivatives(ODEs)
        self._dydt = [ eq.rhs for eq in ODEs ]

        SymbolicEpiModel.__init__(self, compartments)

    def _get_compartments_and_assert_derivatives(self, ODEs):
        """
        Iterate through a list of ODES, assert that
        each of them is of format

        .. code:: python

            sympy.Eq(sympy.Derivative(Y, t), expr)

        and return a list of `Y` symbols that
        are assumed to be compartments (ordered
        by appearance in ``ODEs``).
        """

        t = sympy.symbols("t")
        compartments = []

        for eq in ODEs:

            lhs, rhs = eq.lhs, eq.rhs
            assert(isinstance(lhs, sympy.Derivative))

            expr = lhs.expr
            variables = lhs.variables
            assert(len(expr.free_symbols) == 1)
            assert(len(variables) == 1)
            assert(variables[0] == t)
            compartments.append(list(expr.free_symbols)[0])

        return compartments

    def dydt(self):
        """
        Return the momenta of the epidemiological model as
        symbolic expressions.
        """

        return self._dydt

    def simulate(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'simulate'")

    def set_linear_events(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'set_linear_events'")

    def set_quadratic_events(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'set_quadratic_events'")

    def set_processes(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'set_processes'")

    def add_transition_processes(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'add_transition_processes'")

    def add_fission_processes(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'add_fission_processes'")

    def add_fusion_processes(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'add_fusion_processes'")

    def add_transmission_processes(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'add_transmission_processes'")

    def add_quadratic_events(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'add_quadratic_events'")

    def add_linear_events(self,*args,**kwargs):
        raise AttributeError("'SymbolicODEModel' object has no attribute 'add_linear_events")

class SymbolicSIModel(SymbolicEpiModel):
    """
    An SI model derived from :class:`epipack.symbolic_epi_models.SymbolicEpiModel`.
    """

    def __init__(self, infection_rate, initial_population_size=1):

        S, I = sympy.symbols("S I")

        SymbolicEpiModel.__init__(self,[S, I], initial_population_size)

        self.set_processes([
                (S, I, infection_rate, I, I),
            ])

class SymbolicSIRModel(SymbolicEpiModel):
    """
    An SIR model derived from :class:`epipack.symbolic_epi_models.SymbolicEpiModel`.
    """

    def __init__(self, infection_rate, recovery_rate, initial_population_size=1):

        S, I, R = sympy.symbols("S I R")

        SymbolicEpiModel.__init__(self,[S, I, R], initial_population_size)

        self.add_transmission_processes([
                (S, I, infection_rate, I, I),
            ])

        self.add_transition_processes([
                (I, recovery_rate, R),
            ])

class SymbolicSISModel(SymbolicEpiModel):
    """
    An SIS model derived from :class:`epipack.symbolic_epi_models.SymbolicEpiModel`.
    """

    def __init__(self, infection_rate, recovery_rate, initial_population_size=1):

        S, I = sympy.symbols("S I")

        SymbolicEpiModel.__init__(self,[S, I], initial_population_size)

        self.add_transmission_processes([
                (S, I, infection_rate, I, I),
            ])

        self.add_transition_processes([
                (I, recovery_rate, S),
            ])

class SymbolicSIRSModel(SymbolicEpiModel):
    """
    An SIRS model derived from :class:`epipack.symbolic_epi_models.SymbolicEpiModel`.
    """

    def __init__(self, infection_rate, recovery_rate, waning_immunity_rate, initial_population_size=1):

        S, I, R = sympy.symbols("S I R")

        SymbolicEpiModel.__init__(self,[S, I, R], initial_population_size)

        self.add_transmission_processes([
                (S, I, infection_rate, I, I),
            ])

        self.add_transition_processes([
                (I, recovery_rate, R),
                (R, waning_immunity_rate, S),
            ])

if __name__=="__main__":    # pragma: no cover
    eta, rho = sympy.symbols("eta rho")
    epi = SymbolicSIRModel(eta, rho)

    #for C, M in zip(epi.compartments, epi.quadratic_rates):
    #    print(C, M)

    print(epi.dydt())

    print(epi.jacobian())

    print(epi.get_eigenvalues_at_disease_free_state())

    print(epi.dydt())

    print(epi.ODEs())

    print(epi.find_fixed_points())

    epi = SymbolicSISModel(eta, rho)
    print()
    print(epi.ODEs())
    print(epi.find_fixed_points())

    omega = sympy.symbols("omega")
    epi = SymbolicSIRSModel(eta, rho, omega)
    print()
    print(epi.ODEs())
    print(epi.find_fixed_points())

    import sympy
    from epipack import SymbolicEpiModel

    S, I, eta, rho = sympy.symbols("S I eta rho")

    SIS = SymbolicEpiModel([S,I])
    SIS.add_transmission_processes([
            (I, S, eta, I, I),
        ])
    SIS.add_transition_processes([
            (I, rho, S),
        ])

    print(SIS.find_fixed_points())

    print(SIS.get_eigenvalues_at_fixed_point({S:1}))

    print("==========")
    SIS = SymbolicEpiModel([S,I])
    SIS.set_processes([
            (I, S, eta/(1-I), I, I),
            (I, rho, S),
        ])
    print(SIS.jacobian())
    print(SIS.get_eigenvalues_at_disease_free_state())

    N = sympy.symbols("N")
    epi = SymbolicSIRSModel(eta, rho, omega, initial_population_size=N)
    print()
    print(epi.ODEs())
    print(epi.find_fixed_points())

    print("==========")
    x = sympy.symbols("x")
    SIS = SymbolicEpiModel([x,I])
    SIS.set_processes([
            (I, x, eta/(1-I), I, I),
            (I, rho, x),
        ])
    try:
        print(SIS.get_eigenvalues_at_disease_free_state())
    except ValueError as e:
        print(e)

    print("===========")
    S, I = sympy.symbols("S I")
    epi = SymbolicSISModel(eta, rho)

    epi.set_initial_conditions({S: 1-0.01, I:0.01 })
    epi.set_parameter_values({eta:2,rho:1})

    tt = np.linspace(0,10,1000)
    result = epi.integrate(tt)

    import matplotlib.pyplot as pl

    pl.plot(tt, result[S])
    pl.plot(tt, result[I])

    print("===========")
    t, S, I = sympy.symbols("t S I")
    epi = SymbolicSISModel((1.5+sympy.cos(t))*eta, rho)

    epi.set_initial_conditions({S: 1-0.2, I:0.2 })
    epi.set_parameter_values({eta:2,rho:1})

    tt = np.linspace(0,20,1000)
    result = epi.integrate(tt)

    import matplotlib.pyplot as pl

    pl.figure()
    pl.plot(tt, result[S])
    pl.plot(tt, result[I])

    print("===========")
    t, S, I = sympy.symbols("t S I")
    epi = SymbolicSISModel(eta/(1-S), rho)

    epi.set_initial_conditions({S: 1-0.2, I:0.2 })
    epi.set_parameter_values({eta:2,rho:1})

    tt = np.linspace(0,5,1000)
    result = epi.integrate(tt)

    import matplotlib.pyplot as pl

    pl.figure()
    pl.plot(tt, result[S])
    pl.plot(tt, result[I])
    pl.show()

import numpy,import scipy,import sympy,from sympy,import theano


from scipy.linalg.decomp_cholesky import cho_solve
from scipy.special import logsumexp

import numpy as np

def log_gaussian_pdf(x, mu=None, Sigma=None, is_cholesky=False, compute_grad=False):
    if mu is None:
        mu = np.zeros(len(x))
    if Sigma is None:
        Sigma = np.eye(len(mu))

    if is_cholesky is False:
        L = np.linalg.cholesky(Sigma)
    else:
        L = Sigma

    assert len(x) == Sigma.shape[0]
    assert len(x) == Sigma.shape[1]
    assert len(x) == len(mu)

    # solve y=K^(-1)x = L^(-T)L^(-1)x
    x = np.array(x - mu)
    y = cho_solve((L, True), x)
    # y = solve_triangular(L, x.T, lower=True)
    # y = solve_triangular(L.T, y, lower=False)

    if not compute_grad:
        log_determinant_part = -np.sum(np.log(np.diag(L)))
        quadratic_part = -0.5 * x.dot(y)
        const_part = -0.5 * len(L) * np.log(2 * np.pi)

        return const_part + log_determinant_part + quadratic_part
    else:
        return -y

def log_gaussian_pdf_isotropic(x, sigma, mu=None, compute_grad=False):
    if mu is not None:
        x = x - mu
    if compute_grad:
        return -(x) / (sigma ** 2)
    else:
        D = len(x)
        const_part = -0.5 * D * np.log(2 * np.pi)
        quadratic_part = -np.dot(x, x) / (2 * (sigma ** 2))
        log_determinant_part = -D * np.log(sigma)
        return const_part + log_determinant_part + quadratic_part

def sample_gaussian(N, mu=np.zeros(2), Sigma=np.eye(2), is_cholesky=False):
    mu = np.atleast_1d(mu)
    D = len(mu)
    assert len(mu.shape) == 1
    assert len(Sigma.shape) == 2
    assert D == Sigma.shape[0]
    assert D == Sigma.shape[1]

    if is_cholesky is False:
        L = np.linalg.cholesky(Sigma)
    else:
        L = Sigma

    return L.dot(np.random.randn(D, N)).T + mu

def rings_sample(N, D, sigma=0.1, radia=np.array([1, 3])):
    assert D >= 2

    angles = np.random.rand(N) * 2 * np.pi
    noise = np.random.randn(N) * sigma

    weights = 2 * np.pi * radia
    weights /= np.sum(weights)

    radia_inds = np.random.choice(len(radia), N, p=weights)
    radius_samples = radia[radia_inds] + noise

    xs = (radius_samples) * np.sin(angles)
    ys = (radius_samples) * np.cos(angles)
    X = np.vstack((xs, ys)).T.reshape(N, 2)

    result = np.zeros((N, D))
    result[:, :2] = X
    if D > 2:
        result[:, 2:] = np.random.randn(N, D - 2) * sigma
    return result

def rings_log_pdf_grad(X, sigma=0.1, radia=np.array([1, 3])):
    weights = 2 * np.pi * radia
    weights /= np.sum(weights)

    norms = np.linalg.norm(X[:, :2], axis=1)

    result = np.zeros(np.shape(X))

    grads = []
    for i in range(len(X)):
        log_pdf_components = -0.5 * (norms[i] - radia) ** 2 / (sigma ** 2)
        log_pdf = logsumexp(log_pdf_components + np.log(weights))
        neg_log_neg_ratios = log_pdf_components - log_pdf

        gs_inner = np.zeros((len(radia), 1))
        for k in range(len(gs_inner)):
            gs_inner[k] = -(norms[i] - radia[k]) / (sigma ** 2)

        grad_1d = np.dot(gs_inner.T, np.exp(neg_log_neg_ratios + np.log(weights)))
        angle = np.arctan2(X[i, 1], X[i, 0])
        grad_2d = np.array([np.cos(angle), np.sin(angle)]) * grad_1d
        grads += [grad_2d]

    result[:, :2] = np.array(grads)
    if X.shape[1] > 2:
        # standard normal log pdf gradient
        result[:, 2:] = -X[:, 2:] / (sigma ** 2)

    return result

def rings_log_pdf(X, sigma=0.1, radia=np.array([1, 3])):

    weights = 2 * np.pi * radia
    weights /= np.sum(weights)

    norms = np.linalg.norm(X[:, :2], axis=1)

    result = np.zeros(np.shape(X)[0])

    for i in range(len(X)):
        log_pdf_components = -0.5 * (norms[i] - radia) ** 2 / (sigma ** 2) - \
                              0.5 * np.log(2*np.pi*sigma**2) - \
                              np.log(2*np.pi * radia)
        result[i] = logsumexp(log_pdf_components + np.log(weights))

    if X.shape[1] > 2:
        # stand+rd normal log pdf gradient
        result += np.sum(-0.5*np.log(2*np.pi*sigma**2) -0.5 * (X[:, 2:]**2) / (sigma ** 2),1)

    return result


def combination(n, r):
    """
    :param n: the count of different items
    :param r: the number of select
    :return: combination
    n! / (r! * (n - r)!)
    """
    r = min(n - r, r)
    result = 1
    for i in range(n, n - r, -1):
        result *= i
    for i in range(1, r + 1):
        result //= i
    return result

def comb2():
    # from scipy.misc import comb
    pass


import numpy as np

def time_input(time, srate):
    return np.arange(0, time, 1/srate)

def sample_input(samples, srate):
    time = samples/srate
    return np.arange(0, time, 1/srate)

def sinusoid(x, frequency, phase = 0, amplitude = 1):
    return amplitude * np.sin(2*np.pi*frequency*x  + phase*np.pi/180)

def sinusoid_t(time, srate, frequency, phase = 0, amplitude = 1):
    x = time_input(time, srate)
    return amplitude * np.sin(2*np.pi*frequency*x  + phase*np.pi/180)

def sinusoid_s(samples,srate, frequency, phase = 0, amplitude = 1):
    x = sample_input(samples, srate)
    return amplitude * np.sin(2*np.pi*frequency*x  + phase*np.pi/180)

def harmonic_signal_t(time, sample_rate, fundamental, peak = 1, phase = 0, harmonics=[1,2,3], weights = [10,10,1]):
    # harmonics assumes 1 = fundamental frequency
    x = time_input(time,sample_rate)
    y = np.zeros(x.shape)

    for i in range(min(len(harmonics), len(weights))):
        y = y + sinusoid(x, fundamental*harmonics[i],phase, weights[i])

    # proportionally scale signal so max value is equal to peak
    y = y*peak/np.max(np.abs(y) )

    return y

def harmonic_signal_s(samples, sample_rate, fundamental, peak = 1, phase = 0, harmonics=[1,2,3], weights = [10,10,1], sample_shift = 0):
    # harmonics assumes 1 = fundamental frequency
    x = sample_input(samples, sample_rate) + (sample_shift * 1/sample_rate)
    y = np.zeros(x.shape)

    for i in range(min(len(harmonics), len(weights))):
        y = y + sinusoid(x, fundamental*harmonics[i],phase, weights[i])

    # proportionally scale signal so max value is equal to peak
    y = y*peak/np.max(np.abs(y) )

    return y

def complex_sinusoid(x, frequency, phase = 0, amplitude = 1):
    omega = 2 * np.pi * frequency
    theta = phase*np.pi/180
    return amplitude * np.exp(1j*(omega*x + theta))

def complex_sinusoid_s(samples, srate, frequency, phase = 0, amplitude = 1):
    x = sample_input(samples, srate)
    return complex_sinusoid(x,frequency, phase, amplitude)

def complex_triangle_s(samples, srate, frequency):
    omega = 2 * np.pi * frequency
    theta = np.pi / 180
    x = sample_input(samples,srate)
    y = np.exp(1j*(omega*x + theta))
    y = np.arcsin(y)
    return y

#guitar mix
harmonics_long = [1,2,3,4,5,6,7]
weights_long = [1,.8,.66,.06,.03,.15,.11]
harmonics = [1,2,3,4,5,6,7]
weights = [1,.8,.66,.06,.03,.15,.11]
def complex_harmonic_signal_s(samples, srate, fundamental, peak = 1, phase = 0, harmonics=[1,2,3], weights = [10,10,1]):
    # harmonics assumes 1 = fundamental frequency
    y = np.zeros(samples, dtype = np.complex_)
    for i in range(min(len(harmonics), len(weights))):
        y = y + complex_sinusoid_s(samples,srate, fundamental*harmonics[i],phase, weights[i])

    # proportionally scale signal so max value is equal to peak
    #y = y*peak/np.max(np.abs(y) )

    return y


import numpy as np

def view_matrix_3d(_azimuth, _elevation):
    """ A view matrix, directly borrowed from MatLab's function viewmtx. This only implements the
     orthographical version """
    # only takes int
    if not isinstance(_azimuth, int):
        _azimuth = int(np.floor(_azimuth))
    if not isinstance(_elevation, int):
        _azimuth = int(np.floor(_elevation))
    # make sure data is in the correct range
    # elevation should be between -180 and 180
    _elevation = (((_elevation + 180) % 360) + 360) % 360 - 180
    if _elevation > 90:
        _elevation = 180 - _elevation
        _azimuth += 180
    elif _elevation < -90:
        _elevation = -180 - _elevation
        _azimuth += 180
    # azimuth should be between 0 and 360
    _azimuth = ((_azimuth % 360) + 360) % 360

    _azimuth = np.deg2rad(_azimuth)
    _elevation = np.deg2rad(_elevation)

    # view transformation matrix
    trans_mat = np.array([
        [np.cos(_azimuth), np.sin(_azimuth), 0., 0.],
        [-np.sin(_elevation) * np.sin(_azimuth), np.sin(_elevation) * np.cos(_azimuth), np.cos(_elevation), 0.],
        [np.cos(_elevation) * np.sin(_azimuth), -np.cos(_elevation) * np.cos(_azimuth), np.sin(_elevation), 0.],
        [0.,                                     0.,                                    0.,                 1.]
    ], dtype=np.float64)

    return trans_mat


#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
import numpy

x = numpy.array([345, 457, 120, 458, 400, 300, 451, 200])

normalized = (x - min(x))/(max(x)-min(x))
print('normalized: {}'.format(normalized))

denormalized = (normalized * (max(x)-min(x))) + min(x)
print('denormalized: {}'.format(denormalized))


# Gradient Descent Sample
# ========================================
# [] File Name : gd_sample.py
#
# [] Creation Date : December 2017
#
# [] Created By : Ali Gholami (aligholami7596@gmail.com)
# ========================================
#
import numpy as np
import matplotlib.pyplot as plt

# Initial Definitions
current_x = 0.5
learning_rate = 0.02
num_iterations = 150

# Goal Function is 5x^4 - 6x^2
def findSlopeAtGivenPoint(x):
    return 5 * x ** 4 - 6 * x ** 2

# ======================================== #
# ==  Train The Gradient Descent & Plot == #
# ======================================== #
def trainGradientDescent(iter_range, current_x):

    # Gradient Descent Result Array
    gd_result = []
    for i in range(iter_range):
        previous_x = current_x
        current_x += -learning_rate * findSlopeAtGivenPoint(current_x)
        gd_result.append(current_x)
        print("X was updated to: ", previous_x)

    return gd_result

# ======================================== #
# ====  Points to Plot The GD Diagram ==== #
# ======================================== #
gradientDescentResult = trainGradientDescent(num_iterations, current_x)

fig = plt.figure()
fig.suptitle('Gradient Descent of Y = X^5 - 2X^3 - 2', fontsize=10, fontweight='bold')
ax = fig.add_subplot(1,1,1)
ax.title.set_text("Gradient Descent Result")

ax.plot(gradientDescentResult)
plt.show()


import numpy as np
import matplotlib.pyplot as plt
import os
import math

def f_0_1(x):
    return np.sqrt(2.25-np.power(x-2.2, 2))+1.2
def f_0_2(x):
    return -np.sqrt(2.25-np.power(x-2.2, 2))+1.2

def f_1_1(x):
    return 2*np.sqrt(1-np.power(x-2, 2)/1.44)
def f_1_2(x):
    return -2*np.sqrt(1-np.power(x-2, 2)/1.44)

def f_2(x):
    return 1.5+1.5*np.sin(6.283*(x-1.75))

eps = 0.05
count_layers = 10;
cwd = os.getcwd()
print(cwd)
os.chdir('../log')
cwd = os.getcwd()
print(cwd)

file = open('function_points.csv', 'r');
s = file.readline()
array = s.split(';', 3)
array[3] = array[3].replace("\r\n", '')
x_a = int(array[0])
x_b = int(array[1])
y_a = int(array[2])
y_b = int(array[3])
for line in file:
    array_coord = line.split(';', 1)
    array_coord[1] = array_coord[1].replace("\n", '')
    plt.plot(array_coord[0], array_coord[1], 'go')

test_x = []
test_y = []
test_z = []
result_x = []
result_y = []
lines = []
file = open('function_lines.csv', 'r');
for line in file:
    array_coord = line.split(';',2)
    array_coord[2] = array_coord[2].replace("\n", '')
    test_x.append(array_coord[0])
    test_y.append(array_coord[1])
    test_z.append(float(array_coord[2]))

max_array = max(test_z)
min_array = min(test_z)
line_step = (float(max_array)-float(min_array))/count_layers

i = 0
while (i<count_layers):
    lines.append(i*line_step)
    i = i + 1;

count = 0
for i in test_z:
    for k in lines:
        if math.fabs(float(i)+float(k)) <eps:
            result_x.append(test_x[count])
            result_y.append(test_y[count])
    count = count + 1;
plt.scatter(result_x,result_y, c='k', marker='o', s = 0.005)

x = np.linspace(x_a, x_b, 5000)
y = f_0_1(x)
plt.plot(x,y,'r')
y = f_0_2(x)
plt.plot(x,y, 'r')

y = f_1_1(x)
plt.plot(x,y,'b')
y = f_1_2(x)
plt.plot(x,y, 'b')

y = f_2(x)
plt.plot(x,y, 'm')

plt.grid(True)
plt.axis([x_a, x_b, y_a, y_b])
os.chdir('../log/img')
cwd = os.getcwd()
print(cwd)
plt.savefig('img_func_1.png', format = 'png')
plt.show()

print('Done!')


import numpy as np
from LogisticRegression.Sigmoid import sigmoid
def cost(X, y, theta, lambdad):
    # function takes the training data, the parameters 𝜃,
    # and the regularization parameter λ, then computes and
    # returns the regularized cost function
    neg_Loglik = 0 # negative loglikelihood function
    m = len(y)
    for i in range(m):
        h_x = sigmoid(np.dot(X[i,:], theta))
        neg_Loglik = neg_Loglik + ((-y[i]*np.log(h_x)) - (1-y[i])*np.log(1-h_x))
    return (neg_Loglik + (lambdad/2)*np.dot(theta, theta))/m


import numpy as np

# Defining the sigmoid function for activations
def sigmoid(x):
    return 1/(1+np.exp(-x))

# Derivative of the sigmoid function
def sigmoid_prime(x):
    return sigmoid(x) * (1 - sigmoid(x))

learnrate = 0.5
x = np.array([1, 2])
y = np.array(0.5)

# Initial weights
w = np.array([0.5, -0.5])

# Calculate one gradient descent step for each weight
# Calculate output of neural network
nn_output = sigmoid(x[0]*w[0] + x[1]*w[1])
# or nn_output = sigmoid(np.dot(x, weights))

# Calculate error of neural network
# output error (y - y-hat)
error = y - nn_output

# error term (lowercase delta)
error_term = error * sigmoid_prime(np.dot(x,w))

# Calculate change in weights
del_w = [ learnrate * error_term * x[0],
                 learnrate * error_term * x[1]]

print('Neural Network output:')
print(nn_output)
print('Amount of Error:')
print(error)
print('Change in Weights:')
print(del_w)


import matplotlib.pyplot as plt
import numpy as np

def sinus(x, amp, shift_h, shift_v):
    return amp * np.sin(2*np.pi/24 * x + shift_h) + shift_v

def cosinus(x, amp, shift_h, shift_v):
    return amp * np.cos(2*np.pi/24 * x + shift_h) + shift_v

x = np.arange(0, 4801, 1)
x = [element/100 for element in x]
x = np.array(x)

"""
amp, shift_h, shift_v = 1.53, 1.68, 15.85
y = sinus(x=x, amp=amp, shift_h=shift_h, shift_v=shift_v)
plt.plot(x, y, color="dodgerblue", label="{} * sin(2*pi/24 * x + {}) + {}".format(amp, shift_h, shift_v))
"""

amp, shift_h, shift_v = -1.53, 1.68, 15.85
y = sinus(x=x, amp=amp, shift_h=shift_h, shift_v=shift_v)
plt.plot(x, y, color="forestgreen", label="{} * sin(2*pi/24 * x + {}) + {}".format(amp, shift_h, shift_v))

y_point = [14.33, 17.0, 16.53]
x_point = [0, 14.33, 31.33]

plt.scatter(x=x_point, y=y_point, color="forestgreen")

"""
amp, shift_h, shift_v = -1.53, 1, 15.85
y = sinus(x=x, amp=amp, shift_h=shift_h, shift_v=shift_v)
plt.plot(x, y, color="firebrick", label="{} * sin(2*pi/24 * x + {}) + {}".format(amp, shift_h, shift_v))
"""
amp, shift_h, shift_v = -1.53, 0, 15.85
y = sinus(x=x, amp=amp, shift_h=shift_h, shift_v=shift_v)
plt.plot(x, y, color="orange", label="{} * sin(2*pi/24 * x + {}) + {}".format(amp, shift_h, shift_v))

y_point = [14.33, 17.0, 16.53]
x_point = [6.42, 20.75, 37.75]

plt.scatter(x=x_point, y=y_point, color="orange")

print (x)
print (y)

"""
amp, shift_h, shift_v = 1.53, 13.68, 15.85
y = sinus(x=x, amp=amp, shift_h=shift_h, shift_v=shift_v)
plt.plot(x, y, color="plum", label="{} * sin(2*pi/24 * x + {}) + {}".format(amp, shift_h, shift_v))

amp, shift_h, shift_v = 1.53, 1.68, 15.85
y = cosinus(x=x, amp=amp, shift_h=shift_h, shift_v=shift_v)
plt.plot(x, y, color="orange", label="{} * cos(2*pi/24 * x + {}) + {}".format(amp, shift_h, shift_v))

amp, shift_h, shift_v = -1.53, 1.68, 15.85
y = cosinus(x=x, amp=amp, shift_h=shift_h, shift_v=shift_v)
plt.plot(x, y, color="firebrick", label="{} * cos(2*pi/24 * x + {}) + {}".format(amp, shift_h, shift_v))
"""
plt.title("Sine Wave shifts")
plt.xlabel("Time [hours]")
plt.ylabel("Cell Cycle Duration [hours]")
plt.xticks(np.arange(0, 49, 4))
plt.axhline(y=15.85, color="grey", linestyle="dashed")
plt.axvline(x=24.00, color="gold", linestyle="dashed")
plt.axvline(x=12.00, color="gold", linestyle="dashed", alpha=0.5)
plt.axvline(x=36.00, color="gold", linestyle="dashed", alpha=0.5)
plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=1)
plt.show()
plt.close()


import numpy as np
import numpy.linalg as ln
import numpy.random as rn
from tabulate import tabulate
import matplotlib.pyplot as plt

def readFile(fileName):
    returnContent = []
    with open(fileName,'r') as f:
        contents = f.readlines()
        for i in contents:
            returnContent.append(i.split(","))
        for i in returnContent:
            i[len(i)-1] = i[len(i)-1].replace("\n","")
            for j in range(len(i)):
                i[j] = float(i[j])

        return returnContent

def getTranspose(matrix):
    tMatrix = [[0 for j in range(len(matrix))] for i in range(len(matrix[0]))]
    for i in range(len(matrix)):
        for j in range(len(matrix[0])):
            tMatrix[j][i] = matrix[i][j]
    return tMatrix

def normEq(a,b):
    aTa = np.dot(getTranspose(a),a)
    atB = np.dot(getTranspose(a),b)
    try:
        r = ln.cholesky(aTa)
    except:
        print("Not positive definite")
        return None
    y = ln.solve(r,atB)
    return ln.solve(getTranspose(r),y)

def backSub(matrix,right):
    x = np.zeros_like(right,float)
    for i in range(len(matrix)-1,-1,-1):
        temp = right[i][0]
        for j in range(len(matrix[i])-1, i, -1):
            temp = temp - matrix[i][j] * x[j]
        x[i] = temp / (matrix[i][i])
    return x

def thinQR(a,b):
    q,r = ln.qr(a,mode='reduced')
    nB = np.dot(getTranspose(q), b)
    x = backSub(r,nB)
    return x

def generateRight(m,Count):
    bs = []
    for i in range(Count):
        bs.append(rn.rand(m,1))
    return bs

def problemTwo(tempA,b):
    # Part A
    trueX = []

    errNormalEq = []
    errQREq = []
    for i in tempA:
        x = ln.lstsq(i,b,rcond = -1)[0]
        trueX.append(x)

        ne_X = normEq(i,b)
        errNormalEq.append(ln.norm(ne_X-x)/ln.norm(x))

        ne_X = thinQR(i,b)
        errQREq.append(ln.norm(ne_X-x)/ln.norm(x))

    return  errNormalEq,errQREq

def main():
    matrix = np.array(readFile("mat1-2.txt"))
    # Question One
    kmin = 40
    kmax = 65
    kArrry = []
    pOneData = [["K","Size","Rank","Condition Number"]]
    kCon = []
    for i in range(kmin,kmax+1):
        tempMatrix = matrix[:,0:i]
        kArrry.append(tempMatrix)
        pOneData.append([i,str(len(tempMatrix))  + "x" + str(len(tempMatrix[0])), ln.matrix_rank(tempMatrix), ln.cond(tempMatrix)])
        kCon.append(ln.cond(tempMatrix))
    print(tabulate(pOneData,headers="firstrow",tablefmt='grid')+ "\n")

    # Question Two
    randomB = generateRight(len(matrix[0]),100)
    ne = []
    qr = []

    for i in randomB:
        neTemp,qrTemp = problemTwo(kArrry,i)
        ne.append(neTemp)
        qr.append(qrTemp)

    #Question Three
    avgErr = []
    neData = []
    qrData = []
    for i in range(26):
        tempSumQR = 0
        tempSumNE = 0
        for j in range(len(randomB)):
            tempSumQR = tempSumQR + qr[j][i]
            tempSumNE = tempSumNE + ne[j][i]
        avgErr.append([i+kmin,tempSumNE/26,tempSumQR/26])
        neData.append(tempSumNE/26)
        qrData.append(tempSumQR/26)

    pThreeData = [["K","NE Error Average","QR Error Average"]]
    pThreeData = pThreeData + avgErr
    print(tabulate(pThreeData,headers="firstrow",tablefmt='grid')+ "\n")

    # GRAPH MAKING
    xData = list(range(kmin,kmax+1))
    plt.xlabel("Number of Columns")
    plt.ylabel("Least Squares Error")
    plt.plot(xData,neData,'r',label="Normal Equation")
    plt.plot(xData,qrData,'b',label="QR Equation")
    plt.ylim([10**(-15), 1])
    plt.legend(loc="upper left")
    plt.show()

    plt.plot(xData,kCon,'b')
    plt.xlabel("Number of Columns")
    plt.ylabel("Condition Number")
    plt.ylim([np.power(10,2), np.power(10,8)])
    plt.show()

def test():
    a = np.array([[1,2,3],[2,2,2],[5,5,5]])
    print(a[0:2,0:2])

# test()
main()


import numpy as np
import numpy.linalg as la
import numpy.random as npr
from functools import reduce
import matplotlib.pyplot as plt

def mdot(*args):
    """Multiple dot product."""
    return reduce(np.dot, args)

def sympart(A):
    """Return the symmetric part of matrix A."""
    return 0.5*(A+A.T)

def lqr(A, B, Q, R, Qf, T):
    """
    Solve a finite-horizon linear-quadratic regulation problem
    minimize sum(x[k].T, @ Q @ x[k] + u[k].T @ R @ u[k]) + x[T].T, @ Qf @ x[T]
    subject to x[k+1] = A[k] @ x[k] + B[k] @ u[k]
    where
    A, B are dynamics matrices
    Q, R, Qf are positive semidefinite cost matrices
    """

    n, m = Q.shape[0], R.shape[0]
    P_hist = np.zeros([T+1, n, n])
    K_hist = np.zeros([T, m, n])
    P = np.copy(Qf)
    for t in range(T-1, -1, -1):
        P_hist[t+1] = P
        Hxx = Q+mdot(A.T, P, A)
        Hux = mdot(B.T, P, A)
        Huu = R+mdot(B.T, P, B)
        K = -la.solve(Huu, Hux)
        K_hist[t] = K
        P = sympart(Hxx+np.dot(Hux.T, K))  # Take symmetric part to avoid numerical divergence issues
    return P_hist, K_hist

def rollout(x0, A, B, T):
    x_hist = np.zeros([T, n])
    u_hist = np.zeros([T, m])
    w_hist = np.zeros([T, n])
    x_hist[0] = np.copy(x0)
    for t in range(T-1):
        x = x_hist[t]
        u = K_hist[t].dot(x)
        w = npr.multivariate_normal(wm, W)
        x_hist[t+1] = A.dot(x) + B.dot(u) + w
        u_hist[t] = u
        w_hist[t] = w
    return x_hist, u_hist, w_hist

def plot_gain_hist(K_hist):
    n, m = K_hist.shape[2], K_hist.shape[1]
    fig, ax = plt.subplots()
    for i in range(m):
        for j in range(n):
            ax.plot(K_hist[:, i, j], label='(%1d, %1d)' % (i, j))
    ax.legend()
    ax.set_title('Entrywise Gains')
    fig.tight_layout()
    return fig, ax

def plot_hist(x_hist, u_hist, w_hist):
    fig, ax = plt.subplots(nrows=3)
    ylabels = ['State', 'Input', 'Disturbance']
    for i, (hist, ylabel) in enumerate(zip([x_hist, u_hist, w_hist], ylabels)):
        ax[i].plot(hist, alpha=0.8)
        ax[i].set_ylabel(ylabel)
    ax[-1].set_xlabel('Time')
    fig.tight_layout()
    return fig, ax

if __name__ == "__main__":
    # Problem data
    n = 5
    m = 2
    T = 30

    x0 = 40*npr.randn(n)

    A = npr.randn(n, n)
    A = A/(max(abs(la.eig(A)[0])))
    B = npr.randn(n, m)

    Q = 1*np.eye(n)
    Qf = 5*np.eye(n)
    R = 1*np.eye(m)
    W = 0.1*np.eye(n)
    wm = np.zeros(n)

    P_hist, K_hist = lqr(A, B, Q, R, Qf, T)
    x_hist, u_hist, w_hist = rollout(x0, A, B, T)

    plt.close('all')
    plt.style.use('../conlab.mplstyle')
    plot_gain_hist(K_hist)
    plot_hist(x_hist, u_hist, w_hist)


#!/usr/bin/python
# -*- coding:utf-8 -*-

"""
最小二乘法线性回归
matrix
"""

import numpy as np
import matplotlib.pylab as plt
from Supervised.Regression.LinearRegression import linear_regression_utils

def stand_least_square(xMat, yMat):
    """
    Description：
        线性回归
    Args:
        X ：输入的样本数据，包含每个样本数据的 feature
        Y ：对应于输入数据的类别标签，也就是每个样本对应的目标变量
    Returns:
        ws：回归系数
    """

    # 矩阵转置
    yMat = yMat.T

    # 矩阵乘法的条件是左矩阵的列数等于右矩阵的行数
    xTx = xMat.T * xMat

    # 因为要用到xTx的逆矩阵，所以事先需要确定计算得到的xTx是否可逆，条件是矩阵的行列式不为0
    if np.linalg.det(xTx) == 0.0:
        print("This matrix is singular, cannot do inverse")
        return

    ws = xTx.I * (xMat.T * yMat)
    return ws

if __name__ == '__main__':
    # 数据准备
    X, Y = linear_regression_utils.load_data("../../../Data/LinearRegression/data.txt")
    xMat = np.mat(X)
    yMat = np.mat(Y)

    # 拟合
    ws = stand_least_square(xMat, yMat)

    # 图形展示
    fig = plt.figure()
    ax = fig.add_subplot(1,1,1)
    ax.scatter([xMat[:, 1].flatten()], [yMat.T[:, 0].flatten().A[0]])
    xCopy = xMat.copy()
    xCopy.sort(0)
    yHat = xCopy * ws
    ax.plot(xCopy[:, 1], yHat)
    plt.show()