# Dynamic factors and coincident indices

Factor models generally try to find a small number of unobserved "factors" that influence a substantial portion of the variation in a larger number of observed variables, and they are related to dimension-reduction techniques such as principal components analysis. Dynamic factor models explicitly model the transition dynamics of the unobserved factors, and so are often applied to time-series data.

Macroeconomic coincident indices are designed to capture the common component of the "business cycle"; such a component is assumed to simultaneously affect many macroeconomic variables. Although the estimation and use of coincident indices (for example the [Index of Coincident Economic Indicators](http://www.newyorkfed.org/research/regional_economy/coincident_summary.html)) pre-dates dynamic factor models, in several influential papers Stock and Watson (1989, 1991) used a dynamic factor model to provide a theoretical foundation for them.

Below, we follow the treatment found in Kim and Nelson (1999), of the Stock and Watson (1991) model, to formulate a dynamic factor model, estimate its parameters via maximum likelihood, and create a coincident index.

## Macroeconomic data

The coincident index is created by considering the comovements in four macroeconomic variables (versions of these variables are available on [FRED](https://research.stlouisfed.org/fred2/); the ID of the series used below is given in parentheses):

- Industrial production (IPMAN)
- Real aggregate income (excluding transfer payments) (W875RX1)
- Manufacturing and trade sales (CMRMTSPL)
- Employees on non-farm payrolls (PAYEMS)

In all cases, the data is at the monthly frequency and has been seasonally adjusted; the time-frame considered is 1972 - 2005.

```python
%matplotlib inline

import numpy as np
import pandas as pd
import statsmodels.api as sm
import matplotlib.pyplot as plt

np.set_printoptions(precision=4, suppress=True, linewidth=120)
```

```python
from pandas_datareader.data import DataReader

# Get the datasets from FRED
start = '1979-01-01'
end = '2014-12-01'
indprod = DataReader('IPMAN', 'fred', start=start, end=end)
income = DataReader('W875RX1', 'fred', start=start, end=end)
sales = DataReader('CMRMTSPL', 'fred', start=start, end=end)
emp = DataReader('PAYEMS', 'fred', start=start, end=end)
# dta = pd.concat((indprod, income, sales, emp), axis=1)
# dta.columns = ['indprod', 'income', 'sales', 'emp']
```

**Note**: in a recent update on FRED (8/12/15) the time series CMRMTSPL was truncated to begin in 1997; this is probably a mistake due to the fact that CMRMTSPL is a spliced series, so the earlier period is from the series HMRMT and the latter period is defined by CMRMT.

This has since (02/11/16) been corrected, however the series could also be constructed by hand from HMRMT and CMRMT, as shown below (process taken from the notes in the Alfred xls file).

```python
# HMRMT = DataReader('HMRMT', 'fred', start='1967-01-01', end=end)
# CMRMT = DataReader('CMRMT', 'fred', start='1997-01-01', end=end)
```

```python
# HMRMT_growth = HMRMT.diff() / HMRMT.shift()
# sales = pd.Series(np.zeros(emp.shape[0]), index=emp.index)

# # Fill in the recent entries (1997 onwards)
# sales[CMRMT.index] = CMRMT

# # Backfill the previous entries (pre 1997)
# idx = sales.loc[:'1997-01-01'].index
# for t in range(len(idx)-1, 0, -1):
#     month = idx[t]
#     prev_month = idx[t-1]
#     sales.loc[prev_month] = sales.loc[month] / (1 + HMRMT_growth.loc[prev_month].values)
```

```python
dta = pd.concat((indprod, income, sales, emp), axis=1)
dta.columns = ['indprod', 'income', 'sales', 'emp']
```

```python
dta.loc[:, 'indprod':'emp'].plot(subplots=True, layout=(2, 2), figsize=(15, 6));
```

Stock and Watson (1991) report that for their datasets, they could not reject the null hypothesis of a unit root in each series (so the series are integrated), but they did not find strong evidence that the series were co-integrated.

As a result, they suggest estimating the model using the first differences (of the logs) of the variables, demeaned and standardized.

```python
# Create log-differenced series
dta['dln_indprod'] = (np.log(dta.indprod)).diff() * 100
dta['dln_income'] = (np.log(dta.income)).diff() * 100
dta['dln_sales'] = (np.log(dta.sales)).diff() * 100
dta['dln_emp'] = (np.log(dta.emp)).diff() * 100

# De-mean and standardize
dta['std_indprod'] = (dta['dln_indprod'] - dta['dln_indprod'].mean()) / dta['dln_indprod'].std()
dta['std_income'] = (dta['dln_income'] - dta['dln_income'].mean()) / dta['dln_income'].std()
dta['std_sales'] = (dta['dln_sales'] - dta['dln_sales'].mean()) / dta['dln_sales'].std()
dta['std_emp'] = (dta['dln_emp'] - dta['dln_emp'].mean()) / dta['dln_emp'].std()
```

## Dynamic factors

A general dynamic factor model is written as:

$$
\begin{align}
y_t & = \Lambda f_t + B x_t + u_t \\
f_t & = A_1 f_{t-1} + \dots + A_p f_{t-p} + \eta_t \qquad \eta_t \sim N(0, I)\\
u_t & = C_1 u_{t-1} + \dots + C_q u_{t-q} + \varepsilon_t \qquad \varepsilon_t \sim N(0, \Sigma)
\end{align}
$$

where $y_t$ are observed data, $f_t$ are the unobserved factors (evolving as a vector autoregression), $x_t$ are (optional) exogenous variables, and $u_t$ is the error, or "idiosyncratic", process ($u_t$ is also optionally allowed to be autocorrelated). The $\Lambda$ matrix is often referred to as the matrix of "factor loadings". The variance of the factor error term is set to the identity matrix to ensure identification of the unobserved factors.

This model can be cast into state space form, and the unobserved factor estimated via the Kalman filter. The likelihood can be evaluated as a byproduct of the filtering recursions, and maximum likelihood estimation used to estimate the parameters.

## Model specification

The specific dynamic factor model in this application has 1 unobserved factor which is assumed to follow an AR(2) process. The innovations $\varepsilon_t$ are assumed to be independent (so that $\Sigma$ is a diagonal matrix) and the error term associated with each equation, $u_{i,t}$ is assumed to follow an independent AR(2) process.

Thus the specification considered here is:

$$
\begin{align}
y_{i,t} & = \lambda_i f_t + u_{i,t} \\
u_{i,t} & = c_{i,1} u_{1,t-1} + c_{i,2} u_{i,t-2} + \varepsilon_{i,t} \qquad & \varepsilon_{i,t} \sim N(0, \sigma_i^2) \\
f_t & = a_1 f_{t-1} + a_2 f_{t-2} + \eta_t \qquad & \eta_t \sim N(0, I)\\
\end{align}
$$

where $i$ is one of: `[indprod, income, sales, emp ]`.

This model can be formulated using the `DynamicFactor` model built-in to statsmodels. In particular, we have the following specification:

- `k_factors = 1` - (there is 1 unobserved factor)
- `factor_order = 2` - (it follows an AR(2) process)
- `error_var = False` - (the errors evolve as independent AR processes rather than jointly as a VAR - note that this is the default option, so it is not specified below)
- `error_order = 2` - (the errors are autocorrelated of order 2: i.e. AR(2) processes)
- `error_cov_type = 'diagonal'` - (the innovations are uncorrelated; this is again the default)

Once the model is created, the parameters can be estimated via maximum likelihood; this is done using the `fit()` method.

**Note**: recall that we have demeaned and standardized the data; this will be important in interpreting the results that follow.

**Aside**: in their empirical example, Kim and Nelson (1999) actually consider a slightly different model in which the employment variable is allowed to also depend on lagged values of the factor - this model does not fit into the built-in `DynamicFactor` class, but can be accommodated by using a subclass to implement the required new parameters and restrictions - see Appendix A, below.

## Parameter estimation

Multivariate models can have a relatively large number of parameters, and it may be difficult to escape from local minima to find the maximized likelihood. In an attempt to mitigate this problem, I perform an initial maximization step (from the model-defined starting parameters) using the modified Powell method available in Scipy (see the minimize documentation for more information). The resulting parameters are then used as starting parameters in the standard LBFGS optimization method.

```python
# Get the endogenous data
endog = dta.loc['1979-02-01':, 'std_indprod':'std_emp']

# Create the model
mod = sm.tsa.DynamicFactor(endog, k_factors=1, factor_order=2, error_order=2)
initial_res = mod.fit(method='powell', disp=False)
res = mod.fit(initial_res.params, disp=False)
```

## Estimates

Once the model has been estimated, there are two components that we can use for analysis or inference:

- The estimated parameters
- The estimated factor

### Parameters

The estimated parameters can be helpful in understanding the implications of the model, although in models with a larger number of observed variables and / or unobserved factors they can be difficult to interpret.

One reason for this difficulty is due to identification issues between the factor loadings and the unobserved factors. One easy-to-see identification issue is the sign of the loadings and the factors: an equivalent model to the one displayed below would result from reversing the signs of all factor loadings and the unobserved factor.

Here, one of the easy-to-interpret implications in this model is the persistence of the unobserved factor: we find that exhibits substantial persistence.

```python
print(res.summary(separate_params=False))
```

                                                 Statespace Model Results
    =================================================================================================================
    Dep. Variable:     ['std_indprod', 'std_income', 'std_sales', 'std_emp']   No. Observations:                  431
    Model:                                 DynamicFactor(factors=1, order=2)   Log Likelihood               -2048.709
                                                              + AR(2) errors   AIC                           4133.418
    Date:                                                   Tue, 24 Dec 2019   BIC                           4206.608
    Time:                                                           15:04:47   HQIC                          4162.316
    Sample:                                                       02-01-1979
                                                                - 12-01-2014
    Covariance Type:                                                     opg
    ====================================================================================================
                                           coef    std err          z      P>|z|      [0.025      0.975]
    ----------------------------------------------------------------------------------------------------
    loading.f1.std_indprod              -0.8757      0.022    -39.785      0.000      -0.919      -0.833
    loading.f1.std_income               -0.2553      0.044     -5.819      0.000      -0.341      -0.169
    loading.f1.std_sales                -0.4965      0.026    -19.076      0.000      -0.548      -0.446
    loading.f1.std_emp                  -0.2943      0.031     -9.627      0.000      -0.354      -0.234
    sigma2.std_indprod                   0.0007      0.001      0.943      0.346      -0.001       0.002
    sigma2.std_income                    0.9072      0.029     31.047      0.000       0.850       0.964
    sigma2.std_sales                     0.5845      0.033     17.520      0.000       0.519       0.650
    sigma2.std_emp                       0.3694      0.015     25.062      0.000       0.341       0.398
    L1.f1.f1                             0.2390      0.038      6.360      0.000       0.165       0.313
    L2.f1.f1                             0.2970      0.043      6.970      0.000       0.213       0.380
    L1.e(std_indprod).e(std_indprod)    -1.1542      0.013    -87.757      0.000      -1.180      -1.128
    L2.e(std_indprod).e(std_indprod)    -0.9873      0.013    -74.546      0.000      -1.013      -0.961
    L1.e(std_income).e(std_income)      -0.1578      0.021     -7.410      0.000      -0.200      -0.116
    L2.e(std_income).e(std_income)      -0.0855      0.044     -1.940      0.052      -0.172       0.001
    L1.e(std_sales).e(std_sales)        -0.4191      0.042    -10.017      0.000      -0.501      -0.337
    L2.e(std_sales).e(std_sales)        -0.1790      0.049     -3.622      0.000      -0.276      -0.082
    L1.e(std_emp).e(std_emp)             0.2952      0.033      8.975      0.000       0.231       0.360
    L2.e(std_emp).e(std_emp)             0.4868      0.028     17.106      0.000       0.431       0.543
    ========================================================================================================
    Ljung-Box (Q):          53.47, 34.93, 56.91, 69.74   Jarque-Bera (JB):   215.94, 9203.94, 20.33, 3787.16
    Prob(Q):                    0.08, 0.70, 0.04, 0.00   Prob(JB):                    0.00, 0.00, 0.00, 0.00
    Heteroskedasticity (H):     0.76, 4.79, 0.48, 0.42   Skew:                       0.08, -0.99, 0.18, 0.75
    Prob(H) (two-sided):        0.10, 0.00, 0.00, 0.00   Kurtosis:                  6.46, 25.55, 4.00, 17.44
    ========================================================================================================

    Warnings:
    [1] Covariance matrix calculated using the outer product of gradients (complex-step).

### Estimated factors

While it can be useful to plot the unobserved factors, it is less useful here than one might think for two reasons:

1. The sign-related identification issue described above.
2. Since the data was differenced, the estimated factor explains the variation in the differenced data, not the original data.

It is for these reasons that the coincident index is created (see below).

With these reservations, the unobserved factor is plotted below, along with the NBER indicators for US recessions. It appears that the factor is successful at picking up some degree of business cycle activity.

```python
fig, ax = plt.subplots(figsize=(13,3))

# Plot the factor
dates = endog.index._mpl_repr()
ax.plot(dates, res.factors.filtered[0], label='Factor')
ax.legend()

# Retrieve and also plot the NBER recession indicators
rec = DataReader('USREC', 'fred', start=start, end=end)
ylim = ax.get_ylim()
ax.fill_between(dates[:-3], ylim[0], ylim[1], rec.values[:-4,0], facecolor='k', alpha=0.1);
```

## Post-estimation

Although here we will be able to interpret the results of the model by constructing the coincident index, there is a useful and generic approach for getting a sense for what is being captured by the estimated factor. By taking the estimated factors as given, regressing them (and a constant) each (one at a time) on each of the observed variables, and recording the coefficients of determination ($R^2$ values), we can get a sense of the variables for which each factor explains a substantial portion of the variance and the variables for which it does not.

In models with more variables and more factors, this can sometimes lend interpretation to the factors (for example sometimes one factor will load primarily on real variables and another on nominal variables).

In this model, with only four endogenous variables and one factor, it is easy to digest a simple table of the $R^2$ values, but in larger models it is not. For this reason, a bar plot is often employed; from the plot we can easily see that the factor explains most of the variation in industrial production index and a large portion of the variation in sales and employment, it is less helpful in explaining income.

```python
res.plot_coefficients_of_determination(figsize=(8,2));
```

## Coincident Index

As described above, the goal of this model was to create an interpretable series which could be used to understand the current status of the macroeconomy. This is what the coincident index is designed to do. It is constructed below. For readers interested in an explanation of the construction, see Kim and Nelson (1999) or Stock and Watson (1991).

In essence, what is done is to reconstruct the mean of the (differenced) factor. We will compare it to the coincident index on published by the Federal Reserve Bank of Philadelphia (USPHCI on FRED).

```python
usphci = DataReader('USPHCI', 'fred', start='1979-01-01', end='2014-12-01')['USPHCI']
usphci.plot(figsize=(13,3));
```

```python
dusphci = usphci.diff()[1:].values
def compute_coincident_index(mod, res):
    # Estimate W(1)
    spec = res.specification
    design = mod.ssm['design']
    transition = mod.ssm['transition']
    ss_kalman_gain = res.filter_results.kalman_gain[:,:,-1]
    k_states = ss_kalman_gain.shape[0]

    W1 = np.linalg.inv(np.eye(k_states) - np.dot(
        np.eye(k_states) - np.dot(ss_kalman_gain, design),
        transition
    )).dot(ss_kalman_gain)[0]

    # Compute the factor mean vector
    factor_mean = np.dot(W1, dta.loc['1972-02-01':, 'dln_indprod':'dln_emp'].mean())

    # Normalize the factors
    factor = res.factors.filtered[0]
    factor *= np.std(usphci.diff()[1:]) / np.std(factor)

    # Compute the coincident index
    coincident_index = np.zeros(mod.nobs+1)
    # The initial value is arbitrary; here it is set to
    # facilitate comparison
    coincident_index[0] = usphci.iloc[0] * factor_mean / dusphci.mean()
    for t in range(0, mod.nobs):
        coincident_index[t+1] = coincident_index[t] + factor[t] + factor_mean

    # Attach dates
    coincident_index = pd.Series(coincident_index, index=dta.index).iloc[1:]

    # Normalize to use the same base year as USPHCI
    coincident_index *= (usphci.loc['1992-07-01'] / coincident_index.loc['1992-07-01'])

    return coincident_index
```

Below we plot the calculated coincident index along with the US recessions and the comparison coincident index USPHCI.

```python
fig, ax = plt.subplots(figsize=(13,3))

# Compute the index
coincident_index = compute_coincident_index(mod, res)

# Plot the factor
dates = endog.index._mpl_repr()
ax.plot(dates, coincident_index, label='Coincident index')
ax.plot(usphci.index._mpl_repr(), usphci, label='USPHCI')
ax.legend(loc='lower right')

# Retrieve and also plot the NBER recession indicators
ylim = ax.get_ylim()
ax.fill_between(dates[:-3], ylim[0], ylim[1], rec.values[:-4,0], facecolor='k', alpha=0.1);
```

## Appendix 1: Extending the dynamic factor model

Recall that the previous specification was described by:

$$
\begin{align}
y_{i,t} & = \lambda_i f_t + u_{i,t} \\
u_{i,t} & = c_{i,1} u_{1,t-1} + c_{i,2} u_{i,t-2} + \varepsilon_{i,t} \qquad & \varepsilon_{i,t} \sim N(0, \sigma_i^2) \\
f_t & = a_1 f_{t-1} + a_2 f_{t-2} + \eta_t \qquad & \eta_t \sim N(0, I)\\
\end{align}
$$

Written in state space form, the previous specification of the model had the following observation equation:

$$
\begin{bmatrix}
y_{\text{indprod}, t} \\
y_{\text{income}, t} \\
y_{\text{sales}, t} \\
y_{\text{emp}, t} \\
\end{bmatrix} = \begin{bmatrix}
\lambda_\text{indprod} & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
\lambda_\text{income}  & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
\lambda_\text{sales}   & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
\lambda_\text{emp}     & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
\end{bmatrix}
\begin{bmatrix}
f_t \\
f_{t-1} \\
u_{\text{indprod}, t} \\
u_{\text{income}, t} \\
u_{\text{sales}, t} \\
u_{\text{emp}, t} \\
u_{\text{indprod}, t-1} \\
u_{\text{income}, t-1} \\
u_{\text{sales}, t-1} \\
u_{\text{emp}, t-1} \\
\end{bmatrix}
$$

and transition equation:

$$
\begin{bmatrix}
f_t \\
f_{t-1} \\
u_{\text{indprod}, t} \\
u_{\text{income}, t} \\
u_{\text{sales}, t} \\
u_{\text{emp}, t} \\
u_{\text{indprod}, t-1} \\
u_{\text{income}, t-1} \\
u_{\text{sales}, t-1} \\
u_{\text{emp}, t-1} \\
\end{bmatrix} = \begin{bmatrix}
a_1 & a_2 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
1   & 0   & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & c_{\text{indprod}, 1} & 0 & 0 & 0 & c_{\text{indprod}, 2} & 0 & 0 & 0 \\
0   & 0   & 0 & c_{\text{income}, 1} & 0 & 0 & 0 & c_{\text{income}, 2} & 0 & 0 \\
0   & 0   & 0 & 0 & c_{\text{sales}, 1} & 0 & 0 & 0 & c_{\text{sales}, 2} & 0 \\
0   & 0   & 0 & 0 & 0 & c_{\text{emp}, 1} & 0 & 0 & 0 & c_{\text{emp}, 2} \\
0   & 0   & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
\end{bmatrix}
\begin{bmatrix}
f_{t-1} \\
f_{t-2} \\
u_{\text{indprod}, t-1} \\
u_{\text{income}, t-1} \\
u_{\text{sales}, t-1} \\
u_{\text{emp}, t-1} \\
u_{\text{indprod}, t-2} \\
u_{\text{income}, t-2} \\
u_{\text{sales}, t-2} \\
u_{\text{emp}, t-2} \\
\end{bmatrix}
+ R \begin{bmatrix}
\eta_t \\
\varepsilon_{t}
\end{bmatrix}
$$

the `DynamicFactor` model handles setting up the state space representation and, in the `DynamicFactor.update` method, it fills in the fitted parameter values into the appropriate locations.

The extended specification is the same as in the previous example, except that we also want to allow employment to depend on lagged values of the factor. This creates a change to the $y_{\text{emp},t}$ equation. Now we have:

$$
\begin{align}
y_{i,t} & = \lambda_i f_t + u_{i,t} \qquad & i \in \{\text{indprod}, \text{income}, \text{sales} \}\\
y_{i,t} & = \lambda_{i,0} f_t + \lambda_{i,1} f_{t-1} + \lambda_{i,2} f_{t-2} + \lambda_{i,2} f_{t-3} + u_{i,t} \qquad & i = \text{emp} \\
u_{i,t} & = c_{i,1} u_{i,t-1} + c_{i,2} u_{i,t-2} + \varepsilon_{i,t} \qquad & \varepsilon_{i,t} \sim N(0, \sigma_i^2) \\
f_t & = a_1 f_{t-1} + a_2 f_{t-2} + \eta_t \qquad & \eta_t \sim N(0, I)\\
\end{align}
$$

Now, the corresponding observation equation should look like the following:

$$
\begin{bmatrix}
y_{\text{indprod}, t} \\
y_{\text{income}, t} \\
y_{\text{sales}, t} \\
y_{\text{emp}, t} \\
\end{bmatrix} = \begin{bmatrix}
\lambda_\text{indprod} & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
\lambda_\text{income}  & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
\lambda_\text{sales}   & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
\lambda_\text{emp,1}   & \lambda_\text{emp,2} & \lambda_\text{emp,3} & \lambda_\text{emp,4} & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
\end{bmatrix}
\begin{bmatrix}
f_t \\
f_{t-1} \\
f_{t-2} \\
f_{t-3} \\
u_{\text{indprod}, t} \\
u_{\text{income}, t} \\
u_{\text{sales}, t} \\
u_{\text{emp}, t} \\
u_{\text{indprod}, t-1} \\
u_{\text{income}, t-1} \\
u_{\text{sales}, t-1} \\
u_{\text{emp}, t-1} \\
\end{bmatrix}
$$

Notice that we have introduced two new state variables, $f_{t-2}$ and $f_{t-3}$, which means we need to update the  transition equation:

$$
\begin{bmatrix}
f_t \\
f_{t-1} \\
f_{t-2} \\
f_{t-3} \\
u_{\text{indprod}, t} \\
u_{\text{income}, t} \\
u_{\text{sales}, t} \\
u_{\text{emp}, t} \\
u_{\text{indprod}, t-1} \\
u_{\text{income}, t-1} \\
u_{\text{sales}, t-1} \\
u_{\text{emp}, t-1} \\
\end{bmatrix} = \begin{bmatrix}
a_1 & a_2 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
1   & 0   & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 1   & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 0 & 0 & c_{\text{indprod}, 1} & 0 & 0 & 0 & c_{\text{indprod}, 2} & 0 & 0 & 0 \\
0   & 0   & 0 & 0 & 0 & c_{\text{income}, 1} & 0 & 0 & 0 & c_{\text{income}, 2} & 0 & 0 \\
0   & 0   & 0 & 0 & 0 & 0 & c_{\text{sales}, 1} & 0 & 0 & 0 & c_{\text{sales}, 2} & 0 \\
0   & 0   & 0 & 0 & 0 & 0 & 0 & c_{\text{emp}, 1} & 0 & 0 & 0 & c_{\text{emp}, 2} \\
0   & 0   & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 \\
0   & 0   & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
\end{bmatrix}
\begin{bmatrix}
f_{t-1} \\
f_{t-2} \\
f_{t-3} \\
f_{t-4} \\
u_{\text{indprod}, t-1} \\
u_{\text{income}, t-1} \\
u_{\text{sales}, t-1} \\
u_{\text{emp}, t-1} \\
u_{\text{indprod}, t-2} \\
u_{\text{income}, t-2} \\
u_{\text{sales}, t-2} \\
u_{\text{emp}, t-2} \\
\end{bmatrix}
+ R \begin{bmatrix}
\eta_t \\
\varepsilon_{t}
\end{bmatrix}
$$

This model cannot be handled out-of-the-box by the `DynamicFactor` class, but it can be handled by creating a subclass when alters the state space representation in the appropriate way.

First, notice that if we had set `factor_order = 4`, we would almost have what we wanted. In that case, the last line of the observation equation would be:

$$
\begin{bmatrix}
\vdots \\
y_{\text{emp}, t} \\
\end{bmatrix} = \begin{bmatrix}
\vdots &  &  &  &  &  &  &  &  &  &  & \vdots \\
\lambda_\text{emp,1}   & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 & 0 & 0 & 0 \\
\end{bmatrix}
\begin{bmatrix}
f_t \\
f_{t-1} \\
f_{t-2} \\
f_{t-3} \\
\vdots
\end{bmatrix}
$$

and the first line of the transition equation would be:

$$
\begin{bmatrix}
f_t \\
\vdots
\end{bmatrix} = \begin{bmatrix}
a_1 & a_2 & a_3 & a_4 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\
\vdots &  &  &  &  &  &  &  &  &  &  & \vdots \\
\end{bmatrix}
\begin{bmatrix}
f_{t-1} \\
f_{t-2} \\
f_{t-3} \\
f_{t-4} \\
\vdots
\end{bmatrix}
+ R \begin{bmatrix}
\eta_t \\
\varepsilon_{t}
\end{bmatrix}
$$

Relative to what we want, we have the following differences:

1. In the above situation, the $\lambda_{\text{emp}, j}$ are forced to be zero for $j > 0$, and we want them to be estimated as parameters.
2. We only want the factor to transition according to an AR(2), but under the above situation it is an AR(4).

Our strategy will be to subclass `DynamicFactor`, and let it do most of the work (setting up the state space representation, etc.) where it assumes that `factor_order = 4`. The only things we will actually do in the subclass will be to fix those two issues.

First, here is the full code of the subclass; it is discussed below. It is important to note at the outset that none of the methods defined below could have been omitted. In fact, the methods `__init__`, `start_params`, `param_names`, `transform_params`, `untransform_params`, and `update` form the core of all state space models in statsmodels, not just the `DynamicFactor` class.

```python
from statsmodels.tsa.statespace import tools
class ExtendedDFM(sm.tsa.DynamicFactor):
    def __init__(self, endog, **kwargs):
            # Setup the model as if we had a factor order of 4
            super(ExtendedDFM, self).__init__(
                endog, k_factors=1, factor_order=4, error_order=2,
                **kwargs)

            # Note: `self.parameters` is an ordered dict with the
            # keys corresponding to parameter types, and the values
            # the number of parameters of that type.
            # Add the new parameters
            self.parameters['new_loadings'] = 3

            # Cache a slice for the location of the 4 factor AR
            # parameters (a_1, ..., a_4) in the full parameter vector
            offset = (self.parameters['factor_loadings'] +
                      self.parameters['exog'] +
                      self.parameters['error_cov'])
            self._params_factor_ar = np.s_[offset:offset+2]
            self._params_factor_zero = np.s_[offset+2:offset+4]

    @property
    def start_params(self):
        # Add three new loading parameters to the end of the parameter
        # vector, initialized to zeros (for simplicity; they could
        # be initialized any way you like)
        return np.r_[super(ExtendedDFM, self).start_params, 0, 0, 0]

    @property
    def param_names(self):
        # Add the corresponding names for the new loading parameters
        #  (the name can be anything you like)
        return super(ExtendedDFM, self).param_names + [
            'loading.L%d.f1.%s' % (i, self.endog_names[3]) for i in range(1,4)]

    def transform_params(self, unconstrained):
            # Perform the typical DFM transformation (w/o the new parameters)
            constrained = super(ExtendedDFM, self).transform_params(
            unconstrained[:-3])

            # Redo the factor AR constraint, since we only want an AR(2),
            # and the previous constraint was for an AR(4)
            ar_params = unconstrained[self._params_factor_ar]
            constrained[self._params_factor_ar] = (
                tools.constrain_stationary_univariate(ar_params))

            # Return all the parameters
            return np.r_[constrained, unconstrained[-3:]]

    def untransform_params(self, constrained):
            # Perform the typical DFM untransformation (w/o the new parameters)
            unconstrained = super(ExtendedDFM, self).untransform_params(
                constrained[:-3])

            # Redo the factor AR unconstrained, since we only want an AR(2),
            # and the previous unconstrained was for an AR(4)
            ar_params = constrained[self._params_factor_ar]
            unconstrained[self._params_factor_ar] = (
                tools.unconstrain_stationary_univariate(ar_params))

            # Return all the parameters
            return np.r_[unconstrained, constrained[-3:]]

    def update(self, params, transformed=True, **kwargs):
        # Peform the transformation, if required
        if not transformed:
            params = self.transform_params(params)
        params[self._params_factor_zero] = 0

        # Now perform the usual DFM update, but exclude our new parameters
        super(ExtendedDFM, self).update(params[:-3], transformed=True, **kwargs)

        # Finally, set our new parameters in the design matrix
        self.ssm['design', 3, 1:4] = params[-3:]

```

So what did we just do?

**`__init__`**

The important step here was specifying the base dynamic factor model which we were operating with. In particular, as described above, we initialize with `factor_order=4`, even though we will only end up with an AR(2) model for the factor. We also performed some general setup-related tasks.

**`start_params`**

`start_params` are used as initial values in the optimizer. Since we are adding three new parameters, we need to pass those in. If we had not done this, the optimizer would use the default starting values, which would be three elements short.

**`param_names`**

`param_names` are used in a variety of places, but especially in the results class. Below we get a full result summary, which is only possible when all the parameters have associated names.

**`transform_params`** and **`untransform_params`**

The optimizer selects possibly parameter values in an unconstrained way. That's not usually desired (since variances cannot be negative, for example), and `transform_params` is used to transform the unconstrained values used by the optimizer to constrained values appropriate to the model. Variances terms are typically squared (to force them to be positive), and AR lag coefficients are often constrained to lead to a stationary model. `untransform_params` is used for the reverse operation (and is important because starting parameters are usually specified in terms of values appropriate to the model, and we need to convert them to parameters appropriate to the optimizer before we can begin the optimization routine).

Even though we do not need to transform or untransform our new parameters (the loadings can in theory take on any values), we still need to modify this function for two reasons:

1. The version in the `DynamicFactor` class is expecting 3 fewer parameters than we have now. At a minimum, we need to handle the three new parameters.
2. The version in the `DynamicFactor` class constrains the factor lag coefficients to be stationary as though it was an AR(4) model. Since we actually have an AR(2) model, we need to re-do the constraint. We also set the last two autoregressive coefficients to be zero here.

**`update`**

The most important reason we need to specify a new `update` method is because we have three new parameters that we need to place into the state space formulation. In particular we let the parent `DynamicFactor.update` class handle placing all the parameters except the three new ones in to the state space representation, and then we put the last three in manually.

```python
# Create the model
extended_mod = ExtendedDFM(endog)
initial_extended_res = extended_mod.fit(maxiter=1000, disp=False)
extended_res = extended_mod.fit(initial_extended_res.params, method='nm', maxiter=1000)
print(extended_res.summary(separate_params=False))
```

    Optimization terminated successfully.
             Current function value: 4.698591
             Iterations: 259
             Function evaluations: 446
                                                 Statespace Model Results
    =================================================================================================================
    Dep. Variable:     ['std_indprod', 'std_income', 'std_sales', 'std_emp']   No. Observations:                  431
    Model:                                 DynamicFactor(factors=1, order=4)   Log Likelihood               -2025.093
                                                              + AR(2) errors   AIC                           4096.186
    Date:                                                   Tue, 24 Dec 2019   BIC                           4189.706
    Time:                                                           15:05:15   HQIC                          4133.111
    Sample:                                                       02-01-1979
                                                                - 12-01-2014
    Covariance Type:                                                     opg
    ====================================================================================================
                                           coef    std err          z      P>|z|      [0.025      0.975]
    ----------------------------------------------------------------------------------------------------
    loading.f1.std_indprod              -0.6890      0.036    -19.096      0.000      -0.760      -0.618
    loading.f1.std_income               -0.2584      0.038     -6.722      0.000      -0.334      -0.183
    loading.f1.std_sales                -0.4390      0.024    -18.551      0.000      -0.485      -0.393
    loading.f1.std_emp                  -0.4168      0.039    -10.759      0.000      -0.493      -0.341
    sigma2.std_indprod                   0.2455      0.046      5.318      0.000       0.155       0.336
    sigma2.std_income                    0.8737      0.030     29.561      0.000       0.816       0.932
    sigma2.std_sales                     0.5346      0.034     15.532      0.000       0.467       0.602
    sigma2.std_emp                       0.2530      0.024     10.498      0.000       0.206       0.300
    L1.f1.f1                             0.3047      0.059      5.172      0.000       0.189       0.420
    L2.f1.f1                             0.3767      0.062      6.078      0.000       0.255       0.498
    L3.f1.f1                                  0   1.14e-09          0      1.000   -2.24e-09    2.24e-09
    L4.f1.f1                                  0   1.14e-09          0      1.000   -2.24e-09    2.24e-09
    L1.e(std_indprod).e(std_indprod)    -0.3202      0.113     -2.828      0.005      -0.542      -0.098
    L2.e(std_indprod).e(std_indprod)    -0.2252      0.090     -2.495      0.013      -0.402      -0.048
    L1.e(std_income).e(std_income)      -0.1730      0.022     -7.837      0.000      -0.216      -0.130
    L2.e(std_income).e(std_income)      -0.0936      0.044     -2.119      0.034      -0.180      -0.007
    L1.e(std_sales).e(std_sales)        -0.4895      0.046    -10.601      0.000      -0.580      -0.399
    L2.e(std_sales).e(std_sales)        -0.2268      0.050     -4.540      0.000      -0.325      -0.129
    L1.e(std_emp).e(std_emp)             0.2341      0.042      5.530      0.000       0.151       0.317
    L2.e(std_emp).e(std_emp)             0.4956      0.051      9.766      0.000       0.396       0.595
    loading.L1.f1.std_emp               -0.0724      0.038     -1.895      0.058      -0.147       0.002
    loading.L2.f1.std_emp                0.0003      0.036      0.009      0.993      -0.071       0.071
    loading.L3.f1.std_emp               -0.1739      0.028     -6.168      0.000      -0.229      -0.119
    ========================================================================================================
    Ljung-Box (Q):          59.19, 34.27, 68.30, 61.67   Jarque-Bera (JB):   231.73, 9687.98, 25.32, 3386.90
    Prob(Q):                    0.03, 0.73, 0.00, 0.02   Prob(JB):                    0.00, 0.00, 0.00, 0.00
    Heteroskedasticity (H):     0.76, 5.03, 0.44, 0.45   Skew:                       0.22, -0.97, 0.24, 0.75
    Prob(H) (two-sided):        0.11, 0.00, 0.00, 0.00   Kurtosis:                  6.56, 26.15, 4.08, 16.65
    ========================================================================================================

    Warnings:
    [1] Covariance matrix calculated using the outer product of gradients (complex-step).
    [2] Covariance matrix is singular or near-singular, with condition number 4.48e+17. Standard errors may be unstable.

Although this model increases the likelihood, it is not preferred by the AIC and BIC measures which penalize the additional three parameters.

Furthermore, the qualitative results are unchanged, as we can see from the updated $R^2$ chart and the new coincident index, both of which are practically identical to the previous results.

```python
extended_res.plot_coefficients_of_determination(figsize=(8,2));
```

```python
fig, ax = plt.subplots(figsize=(13,3))

# Compute the index
extended_coincident_index = compute_coincident_index(extended_mod, extended_res)

# Plot the factor
dates = endog.index._mpl_repr()
ax.plot(dates, coincident_index, '-', linewidth=1, label='Basic model')
ax.plot(dates, extended_coincident_index, '--', linewidth=3, label='Extended model')
ax.plot(usphci.index._mpl_repr(), usphci, label='USPHCI')
ax.legend(loc='lower right')
ax.set(title='Coincident indices, comparison')

# Retrieve and also plot the NBER recession indicators
ylim = ax.get_ylim()
ax.fill_between(dates[:-3], ylim[0], ylim[1], rec.values[:-4,0], facecolor='k', alpha=0.1);
```


# **`PoissonGeometry`**

## **Downloading from PyPi**

Run the following:

```
!pip install poissongeometry==0.1.2
!pip install galgebra==0.4.3
```

To verify that `PoissonGeometry` module it was installed correctly run:

```
def test_poissongeometry():
  """ This method verifies if the module was installed correctly """
  try:
    import poisson
    result = 'The module was installed correctly'
  except:
    result = 'The module was NOT installed correctly'
  return result

test_poissongeometry()
```

**More information**: see [Github](https://github.com/appliedgeometry/poissongeometry) repository or [PyPi](https://pypi.org/project/poissongeometry/) page.

## **Preparing the Environment to Work with `PoissonGeometry`**

### Obtaining $\LaTeX$ format

With this code you can print the results of certain functions in `PoissonGeometry` with $\LaTeX$ typography:

```
import sympy
def custom_latex_printer(exp, **options):
    from google.colab.output._publish import javascript
    url = "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/latest.js?config=default"
    javascript(url=url)
    return sympy.printing.latex(exp, **options)
sympy.init_printing(use_latex="mathjax", latex_printer=custom_latex_printer)
```

### $\LaTeX$ code

* To print the result of a function in $\LaTeX$ code we just need to add the flag `latex_format=True`, whose default value is `False`:

        function_name(param_1, ..., param_n, latex_format=True)

### Syntax

* A scalar function in `PoissonGeometry` is written using *string literal expressions*.

    For example, the function $f:\mathbb{R}^{3} \to \mathbb{R}$ given by

    $$f(x_1,x_2,x_3) = ax_1^2 + bx_2^2 + cx_3^2, \quad a,b,c \in \mathbb{R}$$

    should be written exactly as follows:
```
"a*x1**2 + b*x2**2 + c*x3**2"
```
Here, `x1, x2, x3` are symbolic variables that `PoissonGeometry` defines by default and that represent in this case the coordinates $(x_1,x_2,x_3)$.

    **Remark.** All characters that are not local coordinates are treated as (symbolic) parameters: for example `a`, `b`, `c` above.

    **Note.** Python supports the following basic operations:

| Expression | Description  || Expression | Description    |
| :--------: | ------------ || :--------: | -------------- |
| +          | Addition     || *          | Multiplication |
| -          | Substraction || **         | Power |
| /          | Division     ||

* A multivector field or a differential form in `Poisson Geometry` is written using *dictionaries* with *tuples of integers* as **keys** and *string* type **values**.

    For example, in $\mathbb{R}^{3}$:
    * The vector field $x_1\frac{\partial}{\partial x_1} + x_2\frac{\partial}{\partial x_2} + x_3 \frac{\partial}{\partial x_3}$ should be written as \\
```{(1,):'x1', (2,):'x2', (3,):'x3'}```

        **Note:** Commas on keys are important!

    * The bivector field $x_1\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_3 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$ should be written as \\
```{(1,2):'x1', (1,3):'-x2', (2,3):'x3'}```

    * The 3-multivector field $x_1\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$ should be written as \\
```{(1,2,3):'x1'}```

**Remarks:**
1. In `Python`, a variable \\
```{key_1: value_1, ..., key_n: value_n}``` \\
it is called 'dictionary'. Each `key_1,...,key_n` is called a *key* of the dictionary and each `value_1,...,value_n` is called the *value* of the corresponding key.

2. In our case, each key is a `tuple` and each value a `string` type variable.

    When we have a multivector $A$ of degree $a$ on $\mathbb{R}^{m}$ $$A = \sum_{1 \leq i_1 < i_2 < \cdots < i_a \leq m} A^{i_1 i_2 \cdots i_a}\,\frac{\partial}{\partial{x_{i_1}}} \wedge \frac{\partial}{\partial{x_{i_2}}} \wedge \cdots \wedge \frac{\partial}{\partial{x_{i_a}}},$$ \\

    the keys of the dictionary are tuples $(i_1,i_2,\ldots,i_a)$ corresponding to the ordered indices $i_1 i_2 \cdots i_a$ of $A^{i_1 i_2 \cdots i_a}$ and the values the corresponding string expression of the coefficient (scalar function) $A^{i_1 i_2 \cdots i_a}$.

    **Note.** We can only write the keys and values of *non-zero coefficients*.

3. We can change the order of the indeces in each `tuple` adding the minus sign in the corresponding `string` value.

    For example,

    * The bivector field $x_1\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}$ can be written as \\
    ```{(1,2): 'x1'}```

        or as

        ```{(2,1): '-x1'}```

     where this last dictionary corresponds to the bivector field $-x_1\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_1}$.

     **Note.** Although we have the option of disregarding the index order, we recommend not doing so, to avoid possible computation errors.

4. The syntax for differential forms is the same as for multivectors.

    For example, the differential 2-form on $\mathbb{R}^4$ $$-\mathrm{d}x_{1} \wedge \mathrm{d}x_{2} - (x_1 + x_4)\mathrm{d}x_{3} \wedge \mathrm{d}x_{4}$$ is written as \\
```{(1,2):'-1', (3,4): '-(x1 + x4)'}```

5. Finally, in `PoissonGeometry` we use the following notation:
    *   `Dxi` is equivalent to  $\frac{\partial}{\partial x_{i}}$.
    *   `dxi` is equivalent to  $\mathrm{d}x_{i}$.

 This assignment is given because in `SymPy` it's not possible to define variables $\frac{\partial}{\partial x_{i}}$ or $\mathrm{d}x_{i}$.

## **Testing the `PoissonGeometry` Class**

### Instantiating and knowing the Class

First, it is necessary to instantiate the class. For this, we must tell to `PoissonGeometry` the dimension and the symbolic variable that names the coordinates.

For example, if we want to work in dimension 4 and use $z$ to name the coordinates:

```
# We import the class and give it a short name for simplicity.
from poisson.poisson import PoissonGeometry as pg

# We declare the variables and the dimension
pg4 = pg(4, variable="z")
```

**Remark:** By default, `variable`=`"x"`.

To know the dimension in which we are working write:

```
pg4.dim
```

To know the (current) coordinates write:

```
pg4.coordinates
```

In addition, `PoissonGeometry` builds a basis $\left\{\frac{\partial}{\partial x_{1}},...,\frac{\partial}{\partial x_{n}}\right\}$ of vector fields. In our running example, for $n=4$.

```
pg4.Dx_basis
```

Moreover, it's possible to operate on this basis. For example, to compute wedge products:

```
Dz1, Dz2, Dz3, Dz4 = pg4.Dx_basis
print(F'Wedge product of Dz1 with Dz2: {Dz1 ^ Dz2}') # The wedge product in Galgebra is denoted with the symbol ^
print(F'Wedge product of Dz1 with Dz2: {Dz1 ^ Dz1}')
```

The following operations can be carried out in `Galgebra`:

| Expression | Description | Expression  | Description |
| :----------: | ----------  | :----------: | ----------  |
| *          | Multiplication | ^         | Wdege Product |
| +          | Addition        | - | Substraction |

## **Functions of `PoissonGeometry`**

###Function: `bivector_to_matrix`

            COMPUTES THE MATRIX OF A BIVECTOR FIELD

For example, the matrix of the bivector field on $\mathbb{R}^4$

$$\Pi = x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

can be computed as follows:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

# We entered the bivector field
bivector = {(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}

pg4.bivector_to_matrix(bivector)
```

**Remember.** If we want the $\LaTeX$ code of the previous matrix we just need to add the flag `latex_format=True`, whose default value is `False`:

```
print(pg4.bivector_to_matrix(bivector, latex_format=True))
```

* The 'power' of the `latex_format`: you can just copy and paste the result into a `.tex` file

###Function: `sharp_morphism`

            COMPUTES THE IMAGE OF A DIFFERENTIAL 1-FORM UNDER THE VECTOR BUNDLE MORPHISM 'SHARP' INDUCED BY A BIVECTOR FIELD

For example, consider the Lie-Poisson bivector field on
 $\mathbb{R}^{3}$

$$\Pi = x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

associated to the Lie algebra $\mathfrak{so}(3)$ [4], and the differential 1-form

$$\alpha = x_1 \mathrm{d}x_{1} + x_{2} \mathrm{d}x_{2} + x_{3} \mathrm{d}x_{3}.$$

To compute *$\Pi^{\natural}(\alpha)$*, run:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

# We entered the bivector field and the 1-form.
bivector = {(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}
alpha = {(1,): 'x1', (2,): 'x2', (3,): 'x3'}

pg3.sharp_morphism(bivector, alpha)
```

Therefore, $\Pi^{\natural}(\alpha)=0$.

###Function: `hamiltonian_vf`

            COMPUTES THE HAMILTONIAN VECTOR FIELD OF A SCALAR FUNCTION RESPECT TO A POISSON BIVECTOR FIELD

For example, consider the Poisson bivector field on $\mathbb{R}^{6}$,
$$\Pi = \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} + \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_5} + \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_6},$$ and the function
$$h = -\frac{1}{x_{2}-x_{1}}-\frac{1}{x_{3}-x_{1}}-\frac{1}{x_{3}-x_{2}} +\frac{1}{2} (x_{4}^{2} + x_{5}^{2} + x_{6}^{2}).$$

The Hamiltonian vector field of $h$ respect to $\Pi$ is given by
\begin{align}
    X_{h} &= - x_4\frac{\partial}{\partial{x_1}}- x_5\frac{\partial}{\partial{x_2}} - x_6\frac{\partial}{\partial{x_3}} + \left[ \frac{1}{(x_1-x_3)|x_1-x_3|} + \frac{1}{(x_1-x_2)|x_1-x_2|} \right]\frac{\partial}{\partial{x_4}} \\
    &+ \left[ \frac{1}{(x_2-x_3)|x_2-x_3|} + \frac{1}{(x_1-x_2)|x_1-x_2|} \right]\frac{\partial}{\partial{x_5}} -\left[ \frac{1}{(x_2-x_3)|x_2-x_3|} + \frac{1}{(x_1-x_3)|x_1-x_3|} \right]\frac{\partial}{\partial{x_6}}.
\end{align}

This vector field is the associated to the Hamiltonian system of a particular case of the three bodies problem [3].
To compute this vector field with `Poisson Geometry` run:

```
# This module is for Python readable printing
import pprint
pp = pprint.PrettyPrinter(indent=2)

# Instantiated the Poisson Geometry class
from poisson.poisson import PoissonGeometry
pg6 = PoissonGeometry(6)

bivector = {(1,4):  '1', (2,5):  '1', (3,6):  '1'}
h = '- 1/sqrt((x2 - x1)**2) - 1/sqrt((x3 - x1)**2) - 1/sqrt((x3 - x2)**2)+ 1/2*(x4**2 + x5**2 + x6**2)'

pp.pprint(pg6.hamiltonian_vf(bivector, h))
```

We must remember that we declared `Dxi` $\equiv \frac{\partial}{\partial x_{i}}$.

### Function: `lichnerowicz_poisson_operator`

            COMPUTES THE IMAGE OF A MULTIVECTOR FIELD UNDER THE COBOUNDARY OPERATOR INDUCED BY A POISSON BIVECTOR FIELD
            RELATIVE TO THE SCHOUTEN-NIJENHUIS BRACKET FOR MULTIVECTOR FIELDS

Consider the bivector field on $\mathbb{R}^{3}$
$$\Pi = x_{1}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - ax_{1}x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_3(2x_{1} - ax_{2}) \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$ and the 3-multivector field $$A = (bx_{2}^{2}x_{3} + c)\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}.$$

Computing the Schouten-Nijenhuis bracket of two multivector fields is a complicated affair. The `PoissonGeometry` class can do this computation very quickly!:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

P = {(1,2): 'x1**2', (1,3): '-a*x1*x3', (2,3): 'x3*(2*x1 - a*x2)'}
A = {(1,2,3): 'b*x2**2*x3 + c'}
pg3.lichnerowicz_poisson_operator(P, A)
```

Therefore, $A$ is a cocycle of $\Pi$. The formal cohomology group of degree 3 of the Lichnerowicz-Poisson complex of $\Pi$ is given by (see [1]):

$$\mathscr{H}^{3}(\Pi) \simeq\ \mathbb{R} \cdot \frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x_{3}}} \bigoplus \mathbb{R} \cdot x_{2}^2x_{3}\,\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x_{3}}}.$$

### Function: `curl_operator`

Consider the following Poisson bivector field (Flaskcha-Ratiu) on $\mathbb{R}^6$,

$$\Pi = x_{1}x_{2}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_{1}x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_2 x_3 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} + \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4} - \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_5} + \frac{\partial}{\partial x_4}\wedge \frac{\partial}{\partial x_6}.$$

With `curl_operator` we can calculate the divergence of $\Pi$ respect to the volume form $f\Omega_{0}$ on $\mathbb{R}^6$, where $\Omega_{0}$ is te euclidean volume form and $f$ is a nonwhere vanishing function:

```
from poisson.poisson import PoissonGeometry
pg6 = PoissonGeometry(6)

bivector = {(1,2): 'x1*x2', (1,3): '-x1*x3', (2,3): 'x2*x3', (3,4): '1', (3,5): '-1', (4,6): '1'}
pg6.curl_operator(bivector, 1)
```

So the divergence of $\Pi$ is trivial.

__Remark.__ The parameter `1` in `curl_operator` means that $f \equiv 1$, therefore the divergence is respect to the euclidean volume $\Omega_{0}$ on $\mathbb{R}^6$.

### Function: `poisson_bracket`

            COMPUTES THE POISSON BRACKET, INDUCED BY A POISSON BIVECTOR FIELD, OF TWO SCALAR FUNCTIONS

For example, consider the Lie-Poisson bivector field on $\mathbb{R}^{3}$,

$$\Pi = x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

associated to the Lie algebra $\mathfrak{sl}(2)$ [7]. It is well known that $\{x_{1},x_{2}\}_{\Pi} = -x_{3}$, $\{x_{2},x_{3}\}_{\Pi} = x_{1}$ y $\{x_{3},x_{1}\}_{\Pi} = x_{2}$ are the commutation relations of this Lie algebra:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector ={(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
x1_x2 = pg3.poisson_bracket(bivector, 'x1', 'x2')
x2_x3 = pg3.poisson_bracket(bivector, 'x2', 'x3')
x3_x1 = pg3.poisson_bracket(bivector, 'x3', 'x1')

print(F'{{x1, x2}} = {x1_x2}')
print(F'{{x2, x3}} = {x2_x3}')
print(F'{{x3, x1}} = {x3_x1}')
```

###Function: `modular_vf`

Consider the bivector field on $\mathbb{R}^{4}$,
$$\Pi = 2x_{4}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} - 2x_{4} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_4} + (x_{1}-x_{2}) \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4}.$$ This bivector field is a particular case of a family of Poisson bivector fields that arise in the analysis of the orbital stability of the Pais-Uhlenbeck oscillator.

* The function `modular_vf` computes the modular field of $\Pi$ respect to a volume form $f\Omega_{0}$ on $\mathbb{R}^4$ ( here $f$ is nonwhere vanishing function and $\Omega_{0}$ the euclidean volume form on $\mathbb{R}^4$ ):

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector ={(1,3):'2*x4', (1,4): '2*x3', (2,3): '-2*x4', (2,4): '2*x3', (3,4):'x1-x2'}
pg4.modular_vf(bivector, 1)
```

So in this case the modular field of $\Pi$ respect to the euclidean volume form is trivial.

__Note__: The second entry for the function `curl_operator`, in this example `1`, can intake any real function $f$, in order to modify the volume form.

For example, with the bivector field on $\mathbb{R}^3$, $$\Pi = x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} -x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

and the function $f(x_{1},x_{2},x_{3}) = \mathrm{exp}(x_1 + x_2 + x_3)$, let's calculate the modular field of $\Pi$ with respect to the form of volume $f\Omega_{0}$:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}
function = 'exp(x1 + x2 + x3)'
pg3.modular_vf(bivector, function)
```

Therfore the modular field of $\Pi$ respect to the volume form $f\Omega_{0}$ is given by
    \begin{equation*}
        (x_{3} - x_{2})\frac{\partial}{\partial{x_{1}}} + (x_{1} - x_{3})\frac{\partial}{\partial{x_{2}}} + (x_{2} - x_{1})\frac{\partial}{\partial{x_{3}}}.
    \end{equation*}

###Function: `flashcka_ratiu_bivector`

            COMPUTES THE FLASCHKA-RATIU BIVECTOR FIELD AND THE CORRESPONDING SYMPLECTIC FORM OF A 'MAXIMAL' SET OF SCALAR FUNCTIONS

For example, consider the functions
$$f(x_1, x_2, x_3, x_4) = x_4$$
and
$$g(x_1, x_2, x_3, x_4) = −x_1^2 + x_2^2 + x_3^2$$

which locally describe a 'broken' singularity of a Lefschetz foliation on a 4-dimensional manifold [6].

We can construct a Poisson bivector field $\Pi$ in such a way that the functions $f$ and $g$ are Casimirs of $\Pi$:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

casimirs = ['x4', '-x1**2 + x2**2 + x3**2']
pg4.flaschka_ratiu_bivector(casimirs)
```

To obtain the symplectic form of $\Pi$ on 2-dimensional leaves, we add the flag `symplectic_form=True`

```
bivector, symplectic_form = pg4.flaschka_ratiu_bivector(casimirs, symplectic_form=True)
print(f'Poisson bivector field: {bivector}')
print(f'Symplectic form: {symplectic_form}')
```

###Function: `linear_normal_form_R3`

            COMPUTES A NORMAL FORM OF ANY LIE-POISSON BIVECTOR FIELD ON R^3

For example, consider the Lie-Poisson bivector field on  $\mathbb{R}^{3}$,
$$\Pi = -10x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} +10x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} -10x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$$

To compute a normal form of $\Pi$, run:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): '-10*x3', (1,3): '10*x2', (2,3): '-10*x1'}
pg3.linear_normal_form_R3(bivector)
```

Note that $\Pi$ is a multiple ($-10$) of the Lie-Poisson bivector field on $\mathbb{R}^{3}$ induced by the Lie algebra $\mathfrak{so}(3)$. Therefore, they belong to the same equivalence class.

### Function: `one_forms_bracket`

            COMPUTES THE LIE BRACKET OF TWO DIFFERENTIAL 1-FORMS INDUCED BY A POISSON BIVECTOR FIELD

For example, consider the following bivector on $\mathbb{R}^{4}$,
$$\Pi = \big( x_3^2 + x_4^2 \big)\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{2}}}
        + \big( x_2x_3 - x_1x_4\big)\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{3}}}
        - \big( x_1x_3 + x_2x_4\big)\frac{\partial}{\partial{x_{1}}} \wedge \frac{\partial}{\partial{x_{4}}}
        + \big( x_1x_3 + x_2x_4\big)\frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x_{3}}}
        + \big( x_2x_3 - x_1x_4\big)\frac{\partial}{\partial{x_{2}}} \wedge \frac{\partial}{\partial{x^{4}}}
        + \big( x_1^2 + x_2^2 \big)\frac{\partial}{\partial{x_{3}}} \wedge \frac{\partial}{\partial{x_{4}}},$$

and the 1-forms
$$\alpha = x_1\mathrm{d}{x_1} - x_2\mathrm{d}{x_2} + x_3\mathrm{d}{x_3} - x_4\mathrm{d}{x_4} \quad \mathrm{and} \quad \beta = x_2\mathrm{d}{x_1} + x_1\mathrm{d}{x_2} + x_4\mathrm{d}{x_3} + x_3\mathrm{d}{x_4}$$.

To compute the Lie bracket, induced by $\Pi$, of $\alpha$ and $\beta$, run:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector ={(1,2): 'x3**2 + x4**2', (1,3): 'x2*x3 - x1*x4', (1,4): '-x1*x3 - x2*x4', (2,3): 'x1*x3 + x2*x4', (2,4): 'x2*x3 - x1*x4', (3,4): 'x1**2 + x2**2'}
alpha = {(1,): 'x1', (2,): '-x2', (3,): 'x3', (4,): '-x4'}
beta = {(1,): 'x2', (2,): 'x1', (3,): 'x4', (4,): 'x3'}
pg4.one_forms_bracket(bivector, alpha, beta)
```

So $\{\alpha, \beta\}_{\Pi} = 0$.

## **Applications**

### Function: `gauge_transformation`

            COMPUTES THE GAUGE TRANSFORMATION OF A BIVECTOR FIELD WITH RESPECT TO A GIVEN DIFFERENTIAL 2-FORM

For example, consider an arbitrary bivector field on $\mathbb{R}^3$,

$$\Pi=\Pi_{12} \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{2}} + \Pi_{13} \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{3}} + \Pi_{23} \frac{\partial}{\partial x_{2}} \wedge \frac{\partial}{\partial x_{3}}$$

and a differential $2$-form,

$$\lambda = \lambda_{12} \mathrm{d}x_{1}\wedge \mathrm{d}x_{2} + \lambda_{13} \mathrm{d}x_{1}\wedge \mathrm{d}x_{3} + \lambda_{23} \mathrm{d}x_{2}\wedge \mathrm{d}x_{3}$$

To compute the gauge transformation of $\Pi$ induced by $\lambda$, run:

```
import pprint
pp = pprint.PrettyPrinter(indent=2)

from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

P = {(1,2):  'P12', (1,3):  'P13', (2,3):  'P23'}
L = {(1,2): 'L12', (1,3): 'L13', (2,3): 'L23'}
gauge_bivector, determinant = pg3.gauge_transformation(P, L)

print('L-gauge transformation of P:')
pp.pprint(gauge_bivector)
print(f'\nIt\'s well-defined on the open subset \n {{{determinant} != 0}} \n of R^3')
```

* Therefore we obtain [5]:

**Proposition 3.1** Let $\Pi$ be a bivector field on a 3-dimensional smooth manifold $M$. Then, given a differential 2-form $\lambda$ on $M$, the $\lambda$-gauge transformation $\overline{\Pi}$ of $\Pi$ is well defined on the open subset of $M$
    \begin{equation*}
        \{F := \big\langle \lambda,\Pi \big\rangle + 1 \neq 0 \} \subseteq M.
    \end{equation*}
Moreover, $\overline{\Pi}$ is given by
    \begin{equation*}
        \overline{\Pi} = \tfrac{1}{F}\Pi.
    \end{equation*}
If $\Pi$ is a Poisson bivector field and $\lambda$ is closed along the leaves of $\Pi$, then $\overline{\Pi}$ is also Poisson.

### Function: `jacobiator`

            COMPUTES THE SCHOUTEN-NIJENHUIS BRACKET OF A BIVECTOR FIELD WITH ITSELF

For example, we can modify the following $4$-parametric bivector field on $\mathbb{R}^{4}$

$$ \Pi=a_1 x_2 \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{2}} + a_2 x_3 \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{3}} + a_3 x_4 \frac{\partial}{\partial x_{1}} \wedge \frac{\partial}{\partial x_{4}} + a_4 x_1 \frac{\partial}{\partial x_{2}} \wedge \frac{\partial}{\partial x_{3}}, $$

using the `jacobiator` function, to construct a family of Poisson bivector fields on $\mathbb{R}^{4}$:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector = {(1,2): 'a1*x2', (1,3): 'a2*x3', (1,4): 'a3*x4', (2,3): 'a4*x1'}
pg4.jacobiator(bivector)
```

Therefore,
    \begin{equation*}
        [\hspace{-0.065cm}[ \Pi,\Pi ]\hspace{-0.065cm}] = -2a_{4}(a_{1}+a_{2})x^1\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{2}}} \wedge \frac{\partial}{\partial{x^{3}}} - 2a_{3}a_{4}x^4\frac{\partial}{\partial{x^{2}}} \wedge \frac{\partial}{\partial{x^{3}}} \wedge \frac{\partial}{\partial{x^{4}}}.
    \end{equation*}

Hence, we have two cases, explained in the following lemma [5].

**Lemma 3.2** If $a_{4}=0$, then $\Pi$ determines a 3-parametric family of Poisson bivector fields on $\mathbb{R}^{4}_{x}$:
    \begin{equation}
        \Pi \,=\, a_{1}x^2\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{2}}}
            + a_{2}x^3\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{3}}}
            + a_{3}x^4\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{4}}}.
    \end{equation}

If $a_{2}=-a_{1}$ and $a_{3}=0$, then $\Pi$ determines a 2-parametric family of Poisson bivector fields on $\mathbb{R}^{4}_{x}$:
\begin{equation}
        \Pi \,=\, a_{1}x^2\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{2}}} - a_{1}x^3\,\frac{\partial}{\partial{x^{1}}} \wedge \frac{\partial}{\partial{x^{3}}} + a_{4}x^1\,\frac{\partial}{\partial{x^{2}}} \wedge \frac{\partial}{\partial{x^{3}}}.
    \end{equation}

## **'Test-Type' Functions**

Functions which allow us to verify whether a given geometric object on a Poisson manifold satisfies certain property.

### Function: `is_homogeneous_unimodular`

            VERIFIES WHETHER AN HOMOGENEOUS POISSON BIVECTOR FIELD ON R^m IS UNIMODULAR, OR NOT

For example, consider the bivector of Poisson on $\mathbb{R}^{4}$,
$$\Pi = 2x_{4}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} - 2x_{4} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} + 2x_{3} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_4} + (x_{1}-x_{2}) \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4}.$$

This Poisson bivector field arises in the analysis of the orbital stability of the Pais-Uhlenbeck oscillator at $\mathbb{R}^{4}$ [2]. As we saw above, $\Pi$ has a trivial modular field relative to the euclidean volume form on $\mathbb{R}^{4}$, so it's an unimodular Poisson bivector on $\mathbb{R}^{4}$. We can verify this with:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

P ={(1,3): '-2*x4', (1,4): '2*x3', (2,3): '-2*x4', (2,4): '2*x3', (3,4): 'x1 + x2'}
pg4.is_homogeneous_unimodular(P)
```

### Function: `isomorphic_lie_poisson`

            VERIFIES WHETHER TWO LIE-POISSON BIVECTOR FIELDS ON R^3 ARE ISOMORPHIC, OR NOT

For example, the bivector fields

$$\Pi_{1} = x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} -x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$
and
$$\Pi_{2} = -x_{3}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} -x_{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{1}\frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$ \\

which are induced by the Lie algebras $\mathfrak{so}(3)$ and $\mathfrak{sl}(2)$, respectively, are NOT isomorphic:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

P1 ={(1,2): 'x3', (1,3): '-x2', (2,3): 'x1'}
P2 ={(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
pg3.isomorphic_lie_poisson_R3(P1, P2)
```

### Function: `is_poisson_bivector`

            VERIFIES IF A GIVEN BIVECTOR FIELD IS A POISSON BIVECTOR FIELD OR NOT

For example, we can verify that the bivector field on $\mathbb{R}^{4}$,

$$\Pi = x_{2}\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} + x_{3} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_{4} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} + x_{1} \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}$$

It's NOT a Poisson bivector field:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector = {(1,2): 'x2', (1,3): 'x3', (1,4): 'x4', (2,3): 'x1'}
pg4.is_poisson_bivector(bivector)
```

###Function: `is_in_kernel`

            VERIFIES WHETHER A DIFFERENTIAL 1-FORM BELONGS TO THE KERNEL OF A (POISSON) BIVECTOR FIELD

For example, for the quadratic Flaschka-Ratiu bivector field on $\mathbb{R}^{4}$ [6]

\begin{align*}
\Pi &= (x_{3}^{2}+x_{4}^{2})\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} + (x_{2}x_{3} - x_{1}x_{4}) \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} - (x_{1}x_{3} + x_{2}x_{4}) \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_4} + (x_{1}x_{3} + x_{2}x_{4}) \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3} \\ &+ (x_{2}x_{3} - x_{1}x_{4}) \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_4} + (x_{1}^{2} + x_{2}^{2}) \frac{\partial}{\partial x_3}\wedge \frac{\partial}{\partial x_4},
\end{align*}

we can verify that the differential 1-form $\alpha = x _{1}\mathrm{d}_{1} + x_{2}\mathrm{d}_{2} + x_{3}\mathrm{d}_{3} + x_{4}\mathrm{d}_{4}$ belongs to the kernel of $\Pi$. In other words, that $\Pi^{\#}(\alpha)=0$:

```
from poisson.poisson import PoissonGeometry
pg4 = PoissonGeometry(4)

bivector = {(1,2): 'x3**2 + x4**2', (1,3): 'x2*x3 - x1*x4', (1,4): '-x1*x3 - x2*x4',
            (2,3): 'x1*x3 + x2*x4', (2,4): 'x2*x3 - x1*x4', (3,4): 'x1**2 + x2**2'}
alpha ={(1,): 'x1', (2,): '-x2', (3,): 'x3', (4,): '-x4'}
pg4.is_in_kernel(bivector, alpha)
```

### Functions: `is_casimir` and `is_poisson_vf`

            GIVEN A POISSON BIVECTOR FIELD P, WITH THESE FUNCTIONS WE CAN VERIFY WHETHER A SCALAR FUNCTION IS A CASIMIR FUNCTION OF P
            OR WHETHER A VECTOR FIELD IS A POISSON VECTOR FIELD FOR P, RESPECTIVELY

Consider the Lie-Poisson bivector field on $\mathbb{R}^{3}$

$$\Pi = -x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + x_1 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3},$$

associated to the Lie algebra $\mathfrak{sl}(2)$ and a Casimir function $K$ of $\Pi$ given by

$$K = x_{1}^{2} + x_{2}^{2} + x_{3}^{2}$$:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
K = 'x1**2 + x2**2 - x3**2'
pg3.is_casimir(bivector, K)
```

This verifies that in effect $K$ is a Casimir function of $\Pi$. Now let's consider the function $f:\mathbb{R}\to\mathbb{R}$ defined by
$$f(t) := \left\{
            \begin{array}{ll}
              e^{-\frac{1}{t^2}} & \hbox{if} \quad t>0, \\
              0 & \hbox{en otro caso.}
            \end{array}
          \right.$$

Let's define a smooth function $F$ by $F:=f\circ K$. Then, The vector field
$$ W := \frac{x_1 F}{x_1^2 + x_2^2}\,\frac{\partial}{\partial{x_1}} \,+\, \frac{x_1 F}{x_2^2 + x_2^2} \frac{\partial}{\partial{x_2}}$$

is a Poisson vector field of $\Pi$:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

bivector = {(1,2): '-x3', (1,3): '-x2', (2,3): 'x1'}
W = {(1,): 'x1*exp(-1/(x1**2 + x2**2 - x3**2))/(x1**2 + x2**2)', (2,): 'x2*exp(-1/(x1**2 + x2**2 - x3**2))/(x1**2 + x2**2)', (3,): 0}
pg3.is_poisson_vf(bivector, W)
```

Notice that  $W$ **IS NOT** a **Hamiltonian** field of $\Pi$ [8].

### Function: `is_poisson_pair`

            VERIFIES WHETHER A COUPLE OF POISSON BIVECTOR FIELDS FORM A POISSON PAIR.

Consider
$$ \Pi = ax_1 x_2\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2} - b x_1 x_3\frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_3} + b x_2 x_3 \frac{\partial}{\partial x_2}\wedge \frac{\partial}{\partial x_3}, $$
and
$$ \Psi = x_3^{2} \frac{\partial}{\partial x_1}\wedge \frac{\partial}{\partial x_2}.$$

Then:

```
from poisson.poisson import PoissonGeometry
pg3 = PoissonGeometry(3)

Pi = {(1,2): 'a*x1*x2', (1,3): '-b*x1*x3', (2,3): 'b*x2*x3'}
Psi = {(1,2): 'x3**2'}
pg3.is_poisson_pair(Pi, Psi)
```

Therefore, $\Pi$ and $\Psi$ form a Poisson pair.

## __Bibliografía__

[1] M. Ammar, G. Kass, M. Masmoudi, N. Poncin, *Strongly R-Matrix Induced Tensors, Koszul Cohomology, and Arbitrary-Dimensional Quadratic Poisson Cohomology*, Pac. J. Math. 245, 1-23 (2010)

[2] M. Avendaño-Camacho, J. A. Vallejo and Yu. Vorobiev, *A Perturbation Theory Approach to the Stability of the Pais-Uhlenbeck Oscillator*, J. Math. Phys. 58, (2017)

[3] P. G. Breen, C. N. Foley, T. Boekholt, S. P. Zwart, *Newton vs the Machine: Solving the Chaotic Three-Body Problem Using Deep Neural Networks*, arXiv:1910.07291 [astro-ph.GA]

[4] J. P. Dufour and N. T. Zung, *Poisson Structures and their Normal Forms*, Progress in Mathematics, 242, Birkh\"auser Verlag, Basel, (2005)

[5] M. Evangelista-Alvarado, J. C. Ruíz-Pantaleón, P. Suárez-Serrato, *On Computational Poisson Geometry I: Symbolic Foundations*

[6] L. C. Garcia-Naranjo,  P.  Suárez-Serrato  and  R.  Vera, *Poisson  Structures  on  Smooth 4-manifolds*, Lett. Math. Phys. 105, 1533-1550 (2015)

[7] C. Laurent-Gengoux, A. Pichereau and P. Vanhaecke, *Poisson Structures*,
Grundlehren der mathematischen Wissenschaften, 347, Springer-Verlag Berlin Heidelberg, (2013)

[8] N. Nakanishi, *On the Structure of Infinitesimal Automorphisms of Linear Poisson Manifolds I*, J. Math. Kyoto Univ. 31, 71-82 (1991)


# "Linear Model with Pytorch"

- toc: true
- branch: master
- badges: true
- comments: true
- author: Kim Jeewoo
- categories: [Python, BDA]
- image: images/gradient_descent.gif

# Packages

```python
import torch
import numpy as np
import matplotlib.pyplot as plt
```

# Data

`-` model : $y_i = \beta_0 + \beta_1 x_i + \epsilon_i \to w_0 + w_1 x_i + \epsilon_i$

`-` model : ${\bf y} = {\bf X}{\bf W} + \boldsymbol{\epsilon}$

`-` ${\bf y}=\begin{bmatrix} y_1 \\ y_2 \\ \dots \\ y_n\end{bmatrix}, \quad {\bf X}=\begin{bmatrix} 1 & x_1 \\ 1 & x_2 \\ \dots \\ 1 & x_n\end{bmatrix}, \quad {\bf W}=\begin{bmatrix} 2.5 \\ 4 \end{bmatrix}, \quad \boldsymbol{\epsilon}= \begin{bmatrix} \epsilon_1 \\ \epsilon_2 \\ \dots \\ \epsilon_n\end{bmatrix}$

```python
n = 100
ones = torch.ones(n)
x,_ = torch.randn(n).sort()

X = torch.vstack([ones, x]).T
W = torch.tensor([2.5, 4])
ϵ = torch.randn(n) * 0.5

y = X@W + ϵ
ytrue = X@W
```

```python
y
```

    tensor([-4.6659, -5.2225, -4.8983, -5.3144, -3.8956, -3.0959, -3.5005, -2.9136,
            -3.5969, -2.9341, -2.5224, -2.3467, -1.4678, -1.6499, -1.8841, -2.6720,
            -0.9285, -1.7103, -0.5669, -0.2297, -1.2239, -0.8258, -1.3861, -0.5816,
             0.3048, -0.1389, -0.3009,  0.0698, -0.4245,  0.2255, -0.2609,  0.2665,
             0.7463,  0.2902,  0.4392, -0.1908,  1.7137,  0.1229,  1.4743,  0.7268,
             0.9497,  1.2753,  2.3952,  0.0112,  1.7579,  1.1195,  2.2118,  2.5507,
             1.6851,  1.2069,  2.0082,  2.0870,  2.0927,  2.6631,  2.1186,  2.4592,
             3.6847,  2.6625,  3.5676,  2.5566,  2.7746,  2.6768,  3.9115,  3.4222,
             4.1293,  3.8311,  4.1674,  3.8551,  3.2833,  3.1882,  4.2983,  5.3517,
             4.2048,  4.8204,  5.6703,  4.8679,  5.0707,  5.4924,  6.1169,  6.0063,
             5.9693,  6.2806,  5.7782,  5.3509,  6.8820,  7.4324,  6.9897,  8.0179,
             7.5910,  7.7848,  6.9408,  7.1198,  7.2888,  8.9348,  8.6057,  9.2441,
             9.8626,  9.9129, 10.9186,  9.5722])

```python
plt.plot(x, y, 'o')
plt.plot(x, ytrue, '--', color = "red")
```

# Learning

학습이란 ` Given data ` 를 이용하여 ` Parameter ` 를 추론하는 것이다.

위 그래프에서는 파란색 점으로 으로 빨간색 선 추론하는 것이 학습이다.

- Given data : $\big\{(x_i,y_i) \big\}_{i=1}^{n}$

- Parameter: ${\bf W}=\begin{bmatrix} w_0 \\ w_1 \end{bmatrix}$

- Estimated parameter: ${\bf \hat{W}}=\begin{bmatrix} \hat{w}_0 \\ \hat{w}_1 \end{bmatrix}$

우선 임의로 $(\hat{w}_0, \hat{w}_1) = (-5, 10)$, 즉 $\hat{y}_i = -5 + 10x_i$ 을 선택하여 선을 그려보고 적절한지 판단해보자.

```python
plt.plot(x, y, 'o')
plt.plot(x, -5+10*x, '--')
```

임의로 값을 선택하였으므로 당연히 적절하지 않다.

`경사하강법`을 사용하여 Parameter를 학습하여 적절한 회귀선을 구할 수 있다.

# Update Parameter

`1` Initial value : 임의의 선을 그린다.

```python
What = torch.tensor([-5.0, 10.0], requires_grad=True)
```

```python
What
```

    tensor([-5., 10.], requires_grad=True)

`requires_grad=True`는 추후 미분을 위한 옵션이다.

```python
yhat = X@What
yhat
```

    tensor([-24.7888, -23.8075, -23.2838, -23.2332, -20.9992, -20.0838, -19.9656,
            -19.4009, -19.3195, -18.4367, -18.0647, -18.0175, -17.0363, -16.8500,
            -16.2803, -16.2501, -15.7194, -14.6962, -14.1416, -13.9496, -13.9427,
            -13.8406, -13.3709, -12.7527, -12.2654, -12.2054, -11.9104, -11.2525,
            -10.8168, -10.7666, -10.6014, -10.5249, -10.5173, -10.4189,  -9.9665,
             -9.7339,  -9.2643,  -9.1379,  -8.9047,  -8.8639,  -8.5671,  -8.4458,
             -8.4279,  -8.4139,  -7.7449,  -7.5671,  -7.2070,  -7.0241,  -6.9773,
             -6.5686,  -6.4497,  -6.4346,  -6.0036,  -5.2298,  -5.1000,  -4.3651,
             -4.2528,  -3.8661,  -3.7358,  -3.2108,  -2.8671,  -2.4344,  -2.1201,
             -2.0760,  -1.8643,  -1.7732,  -1.6981,  -1.4627,  -1.1873,  -1.1713,
             -1.0649,   0.6099,   0.6553,   0.8238,   0.8293,   1.5577,   1.7260,
              1.8208,   2.0080,   2.2109,   2.2773,   2.3636,   2.9487,   3.7465,
              5.0061,   5.5221,   6.1181,   6.8012,   6.8334,   6.8914,   6.9283,
              7.6775,   7.7656,   9.8661,  11.1341,  11.2357,  13.4665,  13.5291,
             14.1737,  14.2314], grad_fn=<MvBackward0>)

```python
plt.plot(x, y, 'o')
plt.plot(x, yhat.data, '--')
```

`requires_grad=True` 옵션은 그래프를 그릴 때 방해가 되는데 `yhat` 대신 `yhat.data` 를 사용하면 해당 옵션을 해제할 수 있다.

`2` 첫 번째 수정 : 선의 `적당한 정도` 를 판단한다.

Parameter가 데이터를 얼마나 잘 적합하고 있는지 객관적으로 판단하기 위해 `Loss Function` 을 도입한다.

$\begin{align}
loss&=\sum_{i=1}^{n}(y_i-\hat{y}_i)^2=\sum_{i=1}^{n}(y_i-(\hat{w}_0+\hat{w}_1x_i))^2 \\
&=({\bf y}-{\bf\hat{y}})^\top({\bf y}-{\bf\hat{y}})=({\bf y}-{\bf X}{\bf\hat{W}})^\top({\bf y}-{\bf X}{\bf \hat{W}})
\end{align}$

$y$와 $\hat{y}$의 값이 비슷할 수록 $loss$는 감소하고 값이 다를 수록 $loss$는 증가한다.

```python
loss = (y-yhat)@(y-yhat)
loss
```

    tensor(9623.6699, grad_fn=<DotBackward0>)

```python
loss = torch.sum((y-yhat)**2)
loss
```

    tensor(9623.6699, grad_fn=<SumBackward0>)

현재 $loss$는 $9595.6709$인데 이 $loss$를 줄여 보다 적당한 회귀선을 찾는 것이 중요하다.

```python

```


# VV10 Non-local correlation kernel
One of the largest deficiencies with semilocal functionals is the lack of long-range correlation effects. This most notable expresses itself as the lack of disperssion in the interactions between molecules. VV10 was expressly created to bridge the gap between the expensive of true non-local correlation and a computational tractable form. We will begin by writing the overall expression:

$$E_c^{\rm{nl}} = \frac{1}{2}\int \int d{\bf r}d{\bf r'}\rho({\bf r})\Phi({\bf r},{\bf r'})\rho({\bf r'})$$

Where the two densities are tied together through the $\Phi$ operator.

For VV10 we have:
$$
\begin{align}
\Phi &= -\frac{3}{2gg'(g + g')}\\
 g &= \omega_0({\rm r}) R^2 + \kappa({\rm r)}\\
 g' &= \omega_0({\rm r}) R^2 + \kappa({\rm r')}
\end{align}
$$

Where $w_{0}$:

$$
\begin{align}
\omega_{0}(r) &= \sqrt{\omega_{g}^2(r) + \frac{\omega_p^2(r)}{3}} \\
\omega_g^2(r) &= C \left | \frac{\nabla \rho({\bf r})}{\rho({\bf r})} \right |^4 \\
\omega_p^2(r) &= 4 \pi \rho({\bf r})
\end{align}
$$

and finally:

$$\kappa({\bf r}) = b * \frac{3 \pi}{2} \left [ \frac{\rho({\bf r})}{9\pi} \right ]^\frac{1}{6}$$

While there are several expression, this is actually quite easy to compute. First let us examine how the VV10 energy is reintegrated:

$$E_c^{\rm{VV10}} = \int d{\bf r} \rho{\bf r} \left [ \beta + \frac{1}{2}\int d{\bf r'} \rho{\bf r'} \Phi({\bf r},{\bf r'}) \right]$$

```python
import psi4
import numpy as np
import ks_helper as ks

mol = psi4.geometry("""
He 0 0 -5
He 0 0  5
symmetry c1
""")
options = {'BASIS':               'aug-cc-pVDZ',
           'DFT_SPHERICAL_POINTS': 110,
           'DFT_RADIAL_POINTS':    20}
```

## VV10 coefficients
First let us build set and build a few static coefficients:

```python
coef_C = 0.0093
coef_B = 5.9
coef_beta = 1.0 / 32.0 * (3.0 / (coef_B ** 2.0)) ** (3.0 / 4.0)
```

## VV10 kernel
First let us construct a function that compute $\omega_0$ and $\kappa$ quantities. To make one piece simpler let us first examine a piece of $\omega_g$:
$$\left |\frac{\nabla \rho({\bf r})}{\rho({\bf r})} \right|^4$$

quantity. Recall that

$$\gamma({\bf r}) = \nabla\rho({\bf r})\cdot\nabla\rho({\bf r})$$

therefore, we can simplify the above to:

$$\frac{\nabla \rho({\bf r})}{\rho({\bf r})} = \left | \frac{\gamma({\bf r})}{\rho({\bf r})\cdot({\bf r})} \right | ^2 $$

```python
def compute_vv10_kernel(rho, gamma):
    kappa_pref = coef_B * (1.5 * np.pi) / ((9.0 * np.pi) ** (1.0 / 6.0))

    # Compute R quantities
    Wp = (4.0 / 3.0) * np.pi * rho
    Wg = coef_C * ((gamma / (rho * rho)) ** 2.0)
    W0 = np.sqrt(Wg + Wp)

    kappa = rho ** (1.0 / 6.0) * kappa_pref
    return W0, kappa
```

## VV10 energy and gradient evaluation

Yup so just integrate it out. Pretty easy.

```python
def compute_vv10(D, Vpot):

    nbf = D.shape[0]
    Varr = np.zeros((nbf, nbf))

    total_e = 0.0
    tD = 2.0 * np.array(D)

    points_func = Vpot.properties()[0]
    superfunc = Vpot.functional()

    xc_e = 0.0
    vv10_e = 0.0

    # First loop over the outer set of blocks
    for l_block in range(Vpot.nblocks()):

        # Obtain general grid information
        l_grid = Vpot.get_block(l_block)
        l_w = np.array(l_grid.w())
        l_x = np.array(l_grid.x())
        l_y = np.array(l_grid.y())
        l_z = np.array(l_grid.z())
        l_npoints = l_w.shape[0]

        points_func.compute_points(l_grid)

        # Compute the functional itself
        ret = superfunc.compute_functional(points_func.point_values(), -1)

        xc_e += np.vdot(l_w, np.array(ret["V"])[:l_npoints])
        v_rho = np.array(ret["V_RHO_A"])[:l_npoints]
        v_gamma = np.array(ret["V_GAMMA_AA"])[:l_npoints]

        # Begin VV10 information
        l_rho = np.array(points_func.point_values()["RHO_A"])[:l_npoints]
        l_gamma = np.array(points_func.point_values()["GAMMA_AA"])[:l_npoints]

        l_W0, l_kappa = compute_vv10_kernel(l_rho, l_gamma)

        phi_kernel = np.zeros_like(l_rho)
        phi_U = np.zeros_like(l_rho)
        phi_W = np.zeros_like(l_rho)

        # Loop over the inner set of blocks
        for r_block in range(Vpot.nblocks()):

            # Repeat as for the left blocks
            r_grid = Vpot.get_block(r_block)
            r_w = np.array(r_grid.w())
            r_x = np.array(r_grid.x())
            r_y = np.array(r_grid.y())
            r_z = np.array(r_grid.z())
            r_npoints = r_w.shape[0]

            points_func.compute_points(r_grid)

            r_rho = np.array(points_func.point_values()["RHO_A"])[:r_npoints]
            r_gamma = np.array(points_func.point_values()["GAMMA_AA"])[:r_npoints]

            r_W0, r_kappa = compute_vv10_kernel(r_rho, r_gamma)

            # Build the distnace matrix
            R2  = (l_x[:, None] - r_x) ** 2
            R2 += (l_y[:, None] - r_y) ** 2
            R2 += (l_z[:, None] - r_z) ** 2

            # Build g
            g = l_W0[:, None] * R2 + l_kappa[:, None]
            gp = r_W0 * R2 + r_kappa

            #
            F_kernal = -1.5 * r_w * r_rho / (g * gp * (g + gp))
            F_U = F_kernal * ((1.0 / g) + (1.0 / (g + gp)))
            F_W = F_U * R2

            phi_kernel += np.sum(F_kernal, axis=1)
            phi_U += -np.sum(F_U, axis=1)
            phi_W += -np.sum(F_W, axis=1)

        # Compute those derivatives
        kappa_dn = l_kappa / (6.0 * l_rho)
        w0_dgamma = coef_C * l_gamma / (l_W0 * l_rho ** 4.0)
        w0_drho = 2.0 / l_W0 * (np.pi/3.0 - coef_C * np.power(l_gamma, 2.0) / (l_rho ** 5.0))

        # Sum up the energy
        vv10_e += np.sum(l_w * l_rho * (coef_beta + 0.5 * phi_kernel))

        # Perturb the derivative quantities
        v_rho += coef_beta + phi_kernel + l_rho * (kappa_dn * phi_U + w0_drho * phi_W)
        v_rho *= 0.5

        v_gamma += l_rho * w0_dgamma * phi_W

        # Recompute to l_grid
        lpos = np.array(l_grid.functions_local_to_global())
        points_func.compute_points(l_grid)
        nfunctions = lpos.shape[0]

        # Integrate the LDA and GGA quantities
        phi = np.array(points_func.basis_values()["PHI"])[:l_npoints, :nfunctions]
        phi_x = np.array(points_func.basis_values()["PHI_X"])[:l_npoints, :nfunctions]
        phi_y = np.array(points_func.basis_values()["PHI_Y"])[:l_npoints, :nfunctions]
        phi_z = np.array(points_func.basis_values()["PHI_Z"])[:l_npoints, :nfunctions]

        # LDA
        Vtmp = np.einsum('pb,p,p,pa->ab', phi, v_rho, l_w, phi)

        # GGA
        l_rho_x = np.array(points_func.point_values()["RHO_AX"])[:l_npoints]
        l_rho_y = np.array(points_func.point_values()["RHO_AY"])[:l_npoints]
        l_rho_z = np.array(points_func.point_values()["RHO_AZ"])[:l_npoints]

        tmp_grid = 2.0 * l_w * v_gamma
        Vtmp += np.einsum('pb,p,p,pa->ab', phi, tmp_grid, l_rho_x, phi_x)
        Vtmp += np.einsum('pb,p,p,pa->ab', phi, tmp_grid, l_rho_y, phi_y)
        Vtmp += np.einsum('pb,p,p,pa->ab', phi, tmp_grid, l_rho_z, phi_z)

        # Sum back to the correct place
        Varr[(lpos[:, None], lpos)] += Vtmp + Vtmp.T

    print("   VV10 NL energy: %16.8f" % vv10_e)

    xc_e += vv10_e
    return xc_e, Varr

ks.ks_solver("VV10", mol, options, compute_vv10)
```

    Number of occupied orbitals: 2
    Number of basis functions:   18

    Total time taken for setup: 0.097 seconds

    Starting SCF iterations:

        Iter            Energy             XC E         Delta E        D RMS

       VV10 NL energy:       0.01904486
    SCF Iter  1:  -5.58360292280094    -2.5904636   -5.58360E+00   3.85770E-02
       VV10 NL energy:       0.01866876
    SCF Iter  2:  -5.77604858287614    -1.9478627   -1.92446E-01   1.64180E-02
       VV10 NL energy:       0.01880748
    SCF Iter  3:  -5.81991363244094    -2.1472560   -4.38650E-02   4.09661E-04
       VV10 NL energy:       0.01880430
    SCF Iter  4:  -5.81994198278402    -2.1423006   -2.83503E-05   2.03490E-06
       VV10 NL energy:       0.01880430
    SCF Iter  5:  -5.81994198341545    -2.1423051   -6.31431E-10   2.69437E-07

    Total time for SCF iterations: 9.229 seconds

    Final SCF energy: -5.81994198 hartree

    -5.819941983415454

Refs:
 - Vydrov O. A.; Van Voorhis T., *J. Chem. Phys.*, **2010**, *133*, 244103


```python
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp, cumtrapz
```

```python
%matplotlib inline
```

In diesem Notebook wird das das dynamische lineare Einspurmodell implementiert.

für die DGL setzen wir:

\begin{equation}
\dot\phi = \omega
\end{equation}

```python
c_v = 100000
c_h = 160000
m = 1900
l_v = 1.5
l_h = 1.4
J = 3400
```

Input function:

```python
def f1(t):
    f = 0.1
    a_rad = 1*2*np.pi/360
    return a_rad*np.sin(2*np.pi*f*t)
```

```python
def step(t, thres=150):
    r = 0.2*2*np.pi/360
    if t<thres and t>10:
        return r
    elif t>=thres and t<290:
        return -r
    else:
        return 0
```

```python
def f2(t):
    if isinstance(t, np.ndarray):
        r = np.ones_like(t)
        for i in range(r.size):
            r[i] = step(t[i])
        return r
    else:
        return step(t)
```

```python
def f3(t):
    f = 0.1
    a_rad = 1*2*np.pi/360
    return 0.001*t*a_rad*np.sin(2*np.pi*f*t)
```

```python
def f4(t):
    if isinstance(t, np.ndarray):
        r = np.ones_like(t)
        return 1/360*2*np.pi*r
    else:
        return 1/360*2*np.pi
```

```python
def δ_v(t):
    return f4(t)
```

```python
T_sim = 300
N_steps = T_sim*1000
```

```python
t = np.linspace(0, T_sim, N_steps)
```

```python
plt.figure(figsize=(15,5))
plt.plot(t, δ_v(t)*360/(2*np.pi))
plt.xlabel("t [s]")
plt.ylabel("steering angle $δ_v$ [°]")
plt.legend()
```

```python
def transition_model(t, state):
    v, β, 𝜔 = state
    v_dot = c_v/m*δ_v(t) + c_v*l_v/(m*v)*δ_v(t)*𝜔 - c_v/m*δ_v(t)**2
    β_dot = - (c_v+c_h)/(m*v)*β + (-1 + (c_h*l_h-c_v*l_v)/(m*v**2))*𝜔 + c_v/(m*v)*δ_v(t)
    𝜔_dot = (c_h*l_h-c_v*l_v)/J*β - (c_h*l_h**2 + c_v*l_v**2)/(J*v)*𝜔 + c_v*l_v/J*δ_v(t)
    return np.r_[v_dot, β_dot, 𝜔_dot]
```

```python
initial_state = np.r_[22, 0, 0]
```

```python
integral_range = [0, t.max()]
```

```python
result = solve_ivp(transition_model, integral_range, y0=initial_state, t_eval=t)
```

```python
def get_position_in_normal_axis(t, v, β, 𝜔):
    phi = cumtrapz(𝜔, t, initial=0)
    v_xN, v_yN = np.zeros_like(t), np.zeros_like(t)
    for i, v_i in enumerate(np.nditer(v)):
        v_x_i, v_y_i = v_i*np.cos(β[i]), v_i*np.sin(β[i])
        v_xN[i] = v_x_i*np.cos(phi[i]) + v_y_i*np.sin(-phi[i])
        v_yN[i] = v_x_i*np.sin(phi[i]) + v_y_i*np.cos(-phi[i])
    s_xN = cumtrapz(v_xN, t, initial=0)
    s_yN = cumtrapz(v_yN, t, initial=0)
    return s_xN, s_yN
```

```python
v, β, 𝜔 = result.y[0, :], result.y[1, :], result.y[2, :]
```

```python
s_xN, s_yN = get_position_in_normal_axis(t, v, β, 𝜔)
```

```python
plt.figure(figsize=(15,15))
plt.plot(s_xN, s_yN)
for sec in range(0, T_sim, 10):
    j = np.argmax(t>sec)
    plt.text(s_xN[j], s_yN[j], "%ds" % sec)
plt.xlabel("$s_{xN}$ [m]")
plt.ylabel("$s_{yN}$ [m]")
smax = max(s_xN.max(), s_yN.max())
#plt.xlim([-smax*0.1, smax])
#plt.ylim([-smax*0.1, smax])
plt.axhline(0, color="k")
plt.axvline(0, color="k")
plt.title("Position im Raum über Zeit")
```

```python

```

```python

```