Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/refactor tests #67

Merged
merged 49 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
3512ef5
Remove unneeded import of numpy in examples
sdahdah Aug 27, 2022
ff5cd70
Add pytest-regressions to requirements
sdahdah Aug 29, 2022
2186547
Update intersphinx URLs
sdahdah Aug 29, 2022
8841adb
Stop plots from appearing when run from pytest, but keep them when ru…
sdahdah Aug 29, 2022
450abf4
Refactor sklearn compatibility tests
sdahdah Aug 29, 2022
9b68a8b
Start splitting unit tests by module. Created temp folders for now
sdahdah Aug 29, 2022
bb438e2
Remove duplicate tests from temporary folder
sdahdah Aug 29, 2022
83b235f
Move MOSEK solver parameter setting to a fixture
sdahdah Aug 30, 2022
30308ab
Move angle preprocessor test to utils
sdahdah Aug 30, 2022
c8223e1
Adjust parameterization of angle feature test
sdahdah Aug 30, 2022
61157be
Refactor angle preprocessor tests to parameterize by class
sdahdah Aug 30, 2022
d7d152d
Add doc for class attribute
sdahdah Aug 30, 2022
9f90485
Refactor delay lifting function tests
sdahdah Aug 30, 2022
a84f857
Fully refactor lifting function unit tests
sdahdah Aug 30, 2022
697d41c
Refactor SplitPipeline and lift/retract testing
sdahdah Aug 30, 2022
0c8f3a7
Add :mod: to module docstrings
sdahdah Aug 30, 2022
94b5ca0
Refactor Koopman pipeline transformer tests
sdahdah Aug 30, 2022
dc63ad0
Add more info to conftest
sdahdah Aug 30, 2022
5648d0f
Refactor multistep prediction and add fixture
sdahdah Sep 1, 2022
4c60705
Refactor episode manipulation tests
sdahdah Sep 1, 2022
137af57
Correct Koopman matrix in fixture
sdahdah Sep 1, 2022
2e4046e
Fully refactor Koopman pipeline
sdahdah Sep 1, 2022
bea159a
Rename pre-commit Git hook
sdahdah Sep 1, 2022
49bf5be
Refactor HinfZpkMeta tests
sdahdah Sep 1, 2022
5dab59a
Tweak formatting of pre-commit hook
sdahdah Sep 1, 2022
7495962
Update test names
sdahdah Sep 1, 2022
08eb284
Refactor LMI regressions with known answers
sdahdah Sep 2, 2022
74fd2e0
Refactor LMI Tikhonvov regressor tests
sdahdah Sep 2, 2022
7cef7a8
Refactor exact tests of EDMD, DMDc, DMD
sdahdah Sep 2, 2022
0035f2f
Explicitly highlight prediction
sdahdah Sep 2, 2022
f6a691a
Create regression tests
sdahdah Sep 2, 2022
989bf17
Remove temporary directory used in refactor
sdahdah Sep 2, 2022
42f69e2
Adjust GitHub workflow command
sdahdah Sep 2, 2022
ec9c318
Fix broken unit test and add regression tests results
sdahdah Sep 2, 2022
9b37655
Add test badge to README
sdahdah Sep 2, 2022
22cc045
Add missing mosek mark
sdahdah Sep 2, 2022
dcf23b6
Only run unit tests on GitHub
sdahdah Sep 2, 2022
cb6c7b7
Rename predict_state to predict_trajectory
sdahdah Sep 2, 2022
a3fb293
Fix capitalization of SkLearn
sdahdah Sep 2, 2022
2465679
Use remote flag for MOSEK tests
sdahdah Sep 2, 2022
3a26319
Reduce LMI regression test tolerance
sdahdah Sep 2, 2022
d34d0f9
Loosen tolerance on Hinf weight test
sdahdah Sep 2, 2022
58fe486
Add CVXOPT to list of solvers
sdahdah Sep 2, 2022
020f557
Fix missing tolerance
sdahdah Sep 2, 2022
66eed2e
Update README since slow mark is not currently used
sdahdah Sep 2, 2022
2ed3876
Remove MOSEK requirement from examples
sdahdah Sep 2, 2022
4067396
Skip doctests that need MOSEK
sdahdah Sep 2, 2022
9628e20
Make GitHub actions run doctests and examples
sdahdah Sep 2, 2022
6a438e3
Re-enable deprecation warnings
sdahdah Sep 2, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .githooks/pre-commit.sh → .githooks/pre-commit
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ fi

# Format all staged files, then exit with an error code if any have uncommitted
# changes.
echo 'Formatting staged Python files . . .'
echo 'Formatting staged Python files...'

########## PIP VERSION #############
yapf -i -r "${PYTHON_FILES[@]}"
Expand Down
9 changes: 4 additions & 5 deletions .github/workflows/test-package.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
name: Test package
on:
push:
branches: [main, dev]
branches: [main]
pull_request:
branches: [main, dev]
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
Expand All @@ -20,8 +20,7 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
python -m pip install -r requirements.txt
- name: Test with pytest
run: |
pytest -k 'not slow'
pytest --exitfirst --remote
12 changes: 8 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
pykoop
======

.. image:: https://github.com/decarsg/pykoop/actions/workflows/test-package.yml/badge.svg
:target: https://github.com/decarsg/pykoop/actions/workflows/test-package.yml
:alt: Test package
.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.5576490.svg
:target: https://doi.org/10.5281/zenodo.5576490
:alt: DOI
Expand Down Expand Up @@ -37,7 +40,6 @@ mass-spring-damper data. Using ``pykoop``, this can be implemented as:
.. code-block:: python

import pykoop
import numpy as np
from sklearn.preprocessing import MaxAbsScaler, StandardScaler

# Get sample mass-spring-damper data
Expand All @@ -59,7 +61,7 @@ mass-spring-damper data. Using ``pykoop``, this can be implemented as:
# Predict using the pipeline
X_initial = X_msd[[0], 1:3]
U = X_msd[:, [3]]
X_pred = kp.predict_state(X_initial, U, episode_feature=False)
X_pred = kp.predict_trajectory(X_initial, U, episode_feature=False)

# Score using the pipeline
score = kp.score(X_msd)
Expand Down Expand Up @@ -116,6 +118,7 @@ Additional LMI solvers can be installed using
.. code-block:: sh

$ pip install mosek
$ pip install cvxopt
$ pip install smcp

Mosek is recommended, but is nonfree and requires a license.
Expand All @@ -129,11 +132,12 @@ The library can be tested using

Note that ``pytest`` must be run from the repository's root directory.

To skip slow unit tests, including all doctests and examples, run
To skip unit tests that require a MOSEK license, including all doctests and
examples, run

.. code-block:: sh

$ pytest ./tests -k "not slow"
$ pytest ./tests -k "not mosek"

The documentation can be compiled using

Expand Down
97 changes: 97 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
"""Pytest fixtures for doctests."""
from typing import Any, Dict

import numpy as np
import pytest
import sklearn
from scipy import linalg, integrate

import pykoop

# Add remote option for MOSEK


def pytest_addoption(parser) -> None:
"""Add ``--remote`` command line flag to run regressions remotely."""
Expand All @@ -30,6 +35,98 @@ def remote_url() -> str:
return 'http://solve.mosek.com:30080'


# Add mass-spring-damper reference trajectory for all tests


@pytest.fixture
def mass_spring_damper_sine_input() -> Dict[str, Any]:
"""Compute mass-spring-damper response with sine input."""
# Set up problem
t_range = (0, 10)
t_step = 0.1
msd = pykoop.dynamic_models.MassSpringDamper(0.5, 0.7, 0.6)

def u(t):
return 0.1 * np.sin(t)

# Initial condition
x0 = np.array([0, 0])
# Solve ODE for training data
t, x = msd.simulate(
t_range,
t_step,
x0,
u,
rtol=1e-8,
atol=1e-8,
)
# Compute discrete-time A and B matrices
Ad = linalg.expm(msd.A * t_step)

def integrand(s):
return linalg.expm(msd.A * (t_step - s)).ravel()

Bd = integrate.quad_vec(integrand, 0, t_step)[0].reshape((2, 2)) @ msd.B
U_valid = np.hstack((Ad, Bd))
# Split the data
y_train, y_valid = np.split(x, 2, axis=0)
u_train, u_valid = np.split(np.reshape(u(t), (-1, 1)), 2, axis=0)
X_train = np.hstack((y_train[:-1, :], u_train[:-1, :]))
Xp_train = y_train[1:, :]
X_valid = np.hstack((y_valid[:-1, :], u_valid[:-1, :]))
Xp_valid = y_valid[1:, :]
return {
'X_train': X_train,
'Xp_train': Xp_train,
'X_valid': X_valid,
'Xp_valid': Xp_valid,
'n_inputs': 1,
'episode_feature': False,
'U_valid': U_valid,
't_step': t_step,
}


@pytest.fixture
def mass_spring_damper_no_input() -> Dict[str, Any]:
"""Compute mass-spring-damper response with no input."""
# Set up problem
t_range = (0, 10)
t_step = 0.1
msd = pykoop.dynamic_models.MassSpringDamper(0.5, 0.7, 0.6)
# Initial condition
x0 = np.array([1, 0])
# Solve ODE for training data
t, x = msd.simulate(
t_range,
t_step,
x0,
lambda t: np.zeros((1, )),
rtol=1e-8,
atol=1e-8,
)
U_valid = linalg.expm(msd.A * t_step)
# Split the data
y_train, y_valid = np.split(x, 2, axis=0)
X_train = y_train[:-1, :]
Xp_train = y_train[1:, :]
X_valid = y_valid[:-1, :]
Xp_valid = y_valid[1:, :]
return {
'X_train': X_train,
'Xp_train': Xp_train,
'X_valid': X_valid,
'Xp_valid': Xp_valid,
'n_inputs': 0,
'episode_feature': False,
'U_valid': U_valid,
't_step': t_step,
}


# Add common packages and data to doctest namespace


@pytest.fixture(autouse=True)
def add_np(doctest_namespace):
"""Add numpy to namespace."""
Expand Down
3 changes: 3 additions & 0 deletions constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ optht>=0.2.0
Deprecated>=1.2.13

pytest
pytest-regressions
matplotlib

sphinx
sphinx-rtd-theme

mosek>=9.2.49
8 changes: 4 additions & 4 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,10 @@
autosummary_generate = True

intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
'numpy': ('https://numpy.org/doc/stable', None),
'scipy': ('https://docs.scipy.org/doc/scipy/reference', None),
'matplotlib': ('https://matplotlib.org/', None),
'python': ('https://docs.python.org/3/', None),
'numpy': ('https://numpy.org/doc/stable/', None),
'scipy': ('https://docs.scipy.org/doc/scipy/', None),
'matplotlib': ('https://matplotlib.org/stable/', None),
'scikit-learn': ('https://scikit-learn.org/stable/', None),
}

Expand Down
27 changes: 18 additions & 9 deletions examples/example_eigenvalue_comparison.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
"""Example comparing eigenvalues of different versions of EDMD."""

import numpy as np
import pykoop
import pykoop.lmi_regressors
from matplotlib import pyplot as plt
from scipy import integrate, linalg

import pykoop
import pykoop.lmi_regressors

plt.rc('lines', linewidth=2)
plt.rc('axes', grid=True)
plt.rc('grid', linestyle='--')


def main() -> None:
def example_eigenvalue_comparison() -> None:
"""Compare eigenvalues of different versions of EDMD."""
# Get example data
X = pykoop.example_data_msd()
# Set solver (you can switch to ``'mosek'`` if you have a license).
solver = 'cvxopt'

# Regressor with no constraint
reg_no_const = pykoop.KoopmanPipeline(
regressor=pykoop.lmi_regressors.LmiEdmd())
reg_no_const = pykoop.KoopmanPipeline(regressor=pykoop.Edmd())
reg_no_const.fit(X, n_inputs=1, episode_feature=True)
U_no_const = reg_no_const.regressor_.coef_.T

Expand All @@ -30,20 +32,27 @@ def main() -> None:
]) # yapf: disable
reg_diss_const = pykoop.KoopmanPipeline(
regressor=pykoop.lmi_regressors.LmiEdmdDissipativityConstr(
supply_rate=Xi))
supply_rate=Xi,
solver_params={'solver': solver},
))
reg_diss_const.fit(X, n_inputs=1, episode_feature=True)
U_diss_const = reg_diss_const.regressor_.coef_.T

# Regressor with H-infinity regularization
reg_hinf_reg = pykoop.KoopmanPipeline(
regressor=pykoop.lmi_regressors.LmiEdmdHinfReg(alpha=5))
regressor=pykoop.lmi_regressors.LmiEdmdHinfReg(
alpha=5,
solver_params={'solver': solver},
))
reg_hinf_reg.fit(X, n_inputs=1, episode_feature=True)
U_hinf_reg = reg_hinf_reg.regressor_.coef_.T

# Regressor with spectral radius constraint
reg_sr_const = pykoop.KoopmanPipeline(
regressor=pykoop.lmi_regressors.LmiEdmdSpectralRadiusConstr(
spectral_radius=0.8))
spectral_radius=0.8,
solver_params={'solver': solver},
))
reg_sr_const.fit(X, n_inputs=1, episode_feature=True)
U_sr_const = reg_sr_const.regressor_.coef_.T

Expand Down Expand Up @@ -72,5 +81,5 @@ def plt_eig(A: np.ndarray,


if __name__ == '__main__':
main()
example_eigenvalue_comparison()
plt.show()
4 changes: 2 additions & 2 deletions examples/example_pipeline_cv.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pykoop.dynamic_models


def main() -> None:
def example_pipeline_cv() -> None:
"""Cross-validate regressor parameters."""
# Get sample mass-spring-damper data
X_msd = pykoop.example_data_msd()
Expand Down Expand Up @@ -96,4 +96,4 @@ def main() -> None:


if __name__ == '__main__':
main()
example_pipeline_cv()
7 changes: 3 additions & 4 deletions examples/example_pipeline_simple.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
"""Example of how to use the Koopman pipeline."""

import numpy as np
from sklearn.preprocessing import MaxAbsScaler, StandardScaler

import pykoop


def main() -> None:
def example_pipeline_simple() -> None:
"""Demonstrate how to use the Koopman pipeline."""
# Get sample mass-spring-damper data
X_msd = pykoop.example_data_msd()
Expand All @@ -27,11 +26,11 @@ def main() -> None:
# Predict using the pipeline
X_initial = X_msd[[0], 1:3]
U = X_msd[:, [3]]
X_pred = kp.predict_state(X_initial, U, episode_feature=False)
X_pred = kp.predict_trajectory(X_initial, U, episode_feature=False)

# Score using the pipeline
score = kp.score(X_msd)


if __name__ == '__main__':
main()
example_pipeline_simple()
15 changes: 7 additions & 8 deletions examples/example_pipeline_vdp.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import pykoop.dynamic_models


def main() -> None:
def example_pipeline_vdp() -> None:
"""Demonstrate how to use the Koopman pipeline."""
# Get sample data
X_vdp = pykoop.example_data_vdp()
Expand Down Expand Up @@ -38,15 +38,15 @@ def main() -> None:
u = X_valid[:, 3:]

# Predict with re-lifting between timesteps (default)
X_pred_local = kp.predict_state(
X_pred_local = kp.predict_trajectory(
x0,
u,
relift_state=True,
episode_feature=False,
)

# Predict without re-lifting between timesteps
X_pred_global = kp.predict_state(
X_pred_global = kp.predict_trajectory(
x0,
u,
relift_state=False,
Expand Down Expand Up @@ -85,7 +85,7 @@ def main() -> None:
Psi_valid = kp.lift(X_valid[:, 1:], episode_feature=False)

# Predict lifted state with re-lifting between timesteps (default)
Psi_pred_local = kp.predict_state(
Psi_pred_local = kp.predict_trajectory(
x0,
u,
relift_state=True,
Expand All @@ -95,7 +95,7 @@ def main() -> None:
)

# Predict lifted state without re-lifting between timesteps
Psi_pred_global = kp.predict_state(
Psi_pred_global = kp.predict_trajectory(
x0,
u,
relift_state=False,
Expand Down Expand Up @@ -141,8 +141,7 @@ def main() -> None:
ax[0, 0].set_ylabel('$u$')
ax[0, 0].set_title('Exogenous input')

plt.show()


if __name__ == '__main__':
main()
example_pipeline_vdp()
plt.show()
Loading