Skip to content

Commit 0c2d473

Browse files
authored
Merge pull request #67 from decarsg/feature/refactor-tests
Feature/refactor tests
2 parents 821ed4b + 6a438e3 commit 0c2d473

33 files changed

+3231
-2741
lines changed

.githooks/pre-commit.sh .githooks/pre-commit

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fi
6060

6161
# Format all staged files, then exit with an error code if any have uncommitted
6262
# changes.
63-
echo 'Formatting staged Python files . . .'
63+
echo 'Formatting staged Python files...'
6464

6565
########## PIP VERSION #############
6666
yapf -i -r "${PYTHON_FILES[@]}"

.github/workflows/test-package.yml

+4-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
name: Test package
22
on:
33
push:
4-
branches: [main, dev]
4+
branches: [main]
55
pull_request:
6-
branches: [main, dev]
6+
branches: [main]
77
jobs:
88
build:
99
runs-on: ubuntu-latest
@@ -20,8 +20,7 @@ jobs:
2020
- name: Install dependencies
2121
run: |
2222
python -m pip install --upgrade pip
23-
python -m pip install pytest
24-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
23+
python -m pip install -r requirements.txt
2524
- name: Test with pytest
2625
run: |
27-
pytest -k 'not slow'
26+
pytest --exitfirst --remote

README.rst

+8-4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
pykoop
44
======
55

6+
.. image:: https://github.com/decarsg/pykoop/actions/workflows/test-package.yml/badge.svg
7+
:target: https://github.com/decarsg/pykoop/actions/workflows/test-package.yml
8+
:alt: Test package
69
.. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.5576490.svg
710
:target: https://doi.org/10.5281/zenodo.5576490
811
:alt: DOI
@@ -37,7 +40,6 @@ mass-spring-damper data. Using ``pykoop``, this can be implemented as:
3740
.. code-block:: python
3841
3942
import pykoop
40-
import numpy as np
4143
from sklearn.preprocessing import MaxAbsScaler, StandardScaler
4244
4345
# Get sample mass-spring-damper data
@@ -59,7 +61,7 @@ mass-spring-damper data. Using ``pykoop``, this can be implemented as:
5961
# Predict using the pipeline
6062
X_initial = X_msd[[0], 1:3]
6163
U = X_msd[:, [3]]
62-
X_pred = kp.predict_state(X_initial, U, episode_feature=False)
64+
X_pred = kp.predict_trajectory(X_initial, U, episode_feature=False)
6365
6466
# Score using the pipeline
6567
score = kp.score(X_msd)
@@ -116,6 +118,7 @@ Additional LMI solvers can be installed using
116118
.. code-block:: sh
117119
118120
$ pip install mosek
121+
$ pip install cvxopt
119122
$ pip install smcp
120123
121124
Mosek is recommended, but is nonfree and requires a license.
@@ -129,11 +132,12 @@ The library can be tested using
129132
130133
Note that ``pytest`` must be run from the repository's root directory.
131134

132-
To skip slow unit tests, including all doctests and examples, run
135+
To skip unit tests that require a MOSEK license, including all doctests and
136+
examples, run
133137

134138
.. code-block:: sh
135139
136-
$ pytest ./tests -k "not slow"
140+
$ pytest ./tests -k "not mosek"
137141
138142
The documentation can be compiled using
139143

conftest.py

+97
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
"""Pytest fixtures for doctests."""
2+
from typing import Any, Dict
3+
24
import numpy as np
35
import pytest
46
import sklearn
7+
from scipy import linalg, integrate
58

69
import pykoop
710

11+
# Add remote option for MOSEK
12+
813

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

3237

38+
# Add mass-spring-damper reference trajectory for all tests
39+
40+
41+
@pytest.fixture
42+
def mass_spring_damper_sine_input() -> Dict[str, Any]:
43+
"""Compute mass-spring-damper response with sine input."""
44+
# Set up problem
45+
t_range = (0, 10)
46+
t_step = 0.1
47+
msd = pykoop.dynamic_models.MassSpringDamper(0.5, 0.7, 0.6)
48+
49+
def u(t):
50+
return 0.1 * np.sin(t)
51+
52+
# Initial condition
53+
x0 = np.array([0, 0])
54+
# Solve ODE for training data
55+
t, x = msd.simulate(
56+
t_range,
57+
t_step,
58+
x0,
59+
u,
60+
rtol=1e-8,
61+
atol=1e-8,
62+
)
63+
# Compute discrete-time A and B matrices
64+
Ad = linalg.expm(msd.A * t_step)
65+
66+
def integrand(s):
67+
return linalg.expm(msd.A * (t_step - s)).ravel()
68+
69+
Bd = integrate.quad_vec(integrand, 0, t_step)[0].reshape((2, 2)) @ msd.B
70+
U_valid = np.hstack((Ad, Bd))
71+
# Split the data
72+
y_train, y_valid = np.split(x, 2, axis=0)
73+
u_train, u_valid = np.split(np.reshape(u(t), (-1, 1)), 2, axis=0)
74+
X_train = np.hstack((y_train[:-1, :], u_train[:-1, :]))
75+
Xp_train = y_train[1:, :]
76+
X_valid = np.hstack((y_valid[:-1, :], u_valid[:-1, :]))
77+
Xp_valid = y_valid[1:, :]
78+
return {
79+
'X_train': X_train,
80+
'Xp_train': Xp_train,
81+
'X_valid': X_valid,
82+
'Xp_valid': Xp_valid,
83+
'n_inputs': 1,
84+
'episode_feature': False,
85+
'U_valid': U_valid,
86+
't_step': t_step,
87+
}
88+
89+
90+
@pytest.fixture
91+
def mass_spring_damper_no_input() -> Dict[str, Any]:
92+
"""Compute mass-spring-damper response with no input."""
93+
# Set up problem
94+
t_range = (0, 10)
95+
t_step = 0.1
96+
msd = pykoop.dynamic_models.MassSpringDamper(0.5, 0.7, 0.6)
97+
# Initial condition
98+
x0 = np.array([1, 0])
99+
# Solve ODE for training data
100+
t, x = msd.simulate(
101+
t_range,
102+
t_step,
103+
x0,
104+
lambda t: np.zeros((1, )),
105+
rtol=1e-8,
106+
atol=1e-8,
107+
)
108+
U_valid = linalg.expm(msd.A * t_step)
109+
# Split the data
110+
y_train, y_valid = np.split(x, 2, axis=0)
111+
X_train = y_train[:-1, :]
112+
Xp_train = y_train[1:, :]
113+
X_valid = y_valid[:-1, :]
114+
Xp_valid = y_valid[1:, :]
115+
return {
116+
'X_train': X_train,
117+
'Xp_train': Xp_train,
118+
'X_valid': X_valid,
119+
'Xp_valid': Xp_valid,
120+
'n_inputs': 0,
121+
'episode_feature': False,
122+
'U_valid': U_valid,
123+
't_step': t_step,
124+
}
125+
126+
127+
# Add common packages and data to doctest namespace
128+
129+
33130
@pytest.fixture(autouse=True)
34131
def add_np(doctest_namespace):
35132
"""Add numpy to namespace."""

constraints.txt

+3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ optht>=0.2.0
77
Deprecated>=1.2.13
88

99
pytest
10+
pytest-regressions
1011
matplotlib
1112

1213
sphinx
1314
sphinx-rtd-theme
15+
16+
mosek>=9.2.49

doc/conf.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@
4949
autosummary_generate = True
5050

5151
intersphinx_mapping = {
52-
'python': ('https://docs.python.org/3', None),
53-
'numpy': ('https://numpy.org/doc/stable', None),
54-
'scipy': ('https://docs.scipy.org/doc/scipy/reference', None),
55-
'matplotlib': ('https://matplotlib.org/', None),
52+
'python': ('https://docs.python.org/3/', None),
53+
'numpy': ('https://numpy.org/doc/stable/', None),
54+
'scipy': ('https://docs.scipy.org/doc/scipy/', None),
55+
'matplotlib': ('https://matplotlib.org/stable/', None),
5656
'scikit-learn': ('https://scikit-learn.org/stable/', None),
5757
}
5858

examples/example_eigenvalue_comparison.py

+18-9
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,26 @@
11
"""Example comparing eigenvalues of different versions of EDMD."""
22

33
import numpy as np
4-
import pykoop
5-
import pykoop.lmi_regressors
64
from matplotlib import pyplot as plt
75
from scipy import integrate, linalg
86

7+
import pykoop
8+
import pykoop.lmi_regressors
9+
910
plt.rc('lines', linewidth=2)
1011
plt.rc('axes', grid=True)
1112
plt.rc('grid', linestyle='--')
1213

1314

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

1922
# Regressor with no constraint
20-
reg_no_const = pykoop.KoopmanPipeline(
21-
regressor=pykoop.lmi_regressors.LmiEdmd())
23+
reg_no_const = pykoop.KoopmanPipeline(regressor=pykoop.Edmd())
2224
reg_no_const.fit(X, n_inputs=1, episode_feature=True)
2325
U_no_const = reg_no_const.regressor_.coef_.T
2426

@@ -30,20 +32,27 @@ def main() -> None:
3032
]) # yapf: disable
3133
reg_diss_const = pykoop.KoopmanPipeline(
3234
regressor=pykoop.lmi_regressors.LmiEdmdDissipativityConstr(
33-
supply_rate=Xi))
35+
supply_rate=Xi,
36+
solver_params={'solver': solver},
37+
))
3438
reg_diss_const.fit(X, n_inputs=1, episode_feature=True)
3539
U_diss_const = reg_diss_const.regressor_.coef_.T
3640

3741
# Regressor with H-infinity regularization
3842
reg_hinf_reg = pykoop.KoopmanPipeline(
39-
regressor=pykoop.lmi_regressors.LmiEdmdHinfReg(alpha=5))
43+
regressor=pykoop.lmi_regressors.LmiEdmdHinfReg(
44+
alpha=5,
45+
solver_params={'solver': solver},
46+
))
4047
reg_hinf_reg.fit(X, n_inputs=1, episode_feature=True)
4148
U_hinf_reg = reg_hinf_reg.regressor_.coef_.T
4249

4350
# Regressor with spectral radius constraint
4451
reg_sr_const = pykoop.KoopmanPipeline(
4552
regressor=pykoop.lmi_regressors.LmiEdmdSpectralRadiusConstr(
46-
spectral_radius=0.8))
53+
spectral_radius=0.8,
54+
solver_params={'solver': solver},
55+
))
4756
reg_sr_const.fit(X, n_inputs=1, episode_feature=True)
4857
U_sr_const = reg_sr_const.regressor_.coef_.T
4958

@@ -72,5 +81,5 @@ def plt_eig(A: np.ndarray,
7281

7382

7483
if __name__ == '__main__':
75-
main()
84+
example_eigenvalue_comparison()
7685
plt.show()

examples/example_pipeline_cv.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import pykoop.dynamic_models
99

1010

11-
def main() -> None:
11+
def example_pipeline_cv() -> None:
1212
"""Cross-validate regressor parameters."""
1313
# Get sample mass-spring-damper data
1414
X_msd = pykoop.example_data_msd()
@@ -96,4 +96,4 @@ def main() -> None:
9696

9797

9898
if __name__ == '__main__':
99-
main()
99+
example_pipeline_cv()

examples/example_pipeline_simple.py

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
"""Example of how to use the Koopman pipeline."""
22

3-
import numpy as np
43
from sklearn.preprocessing import MaxAbsScaler, StandardScaler
54

65
import pykoop
76

87

9-
def main() -> None:
8+
def example_pipeline_simple() -> None:
109
"""Demonstrate how to use the Koopman pipeline."""
1110
# Get sample mass-spring-damper data
1211
X_msd = pykoop.example_data_msd()
@@ -27,11 +26,11 @@ def main() -> None:
2726
# Predict using the pipeline
2827
X_initial = X_msd[[0], 1:3]
2928
U = X_msd[:, [3]]
30-
X_pred = kp.predict_state(X_initial, U, episode_feature=False)
29+
X_pred = kp.predict_trajectory(X_initial, U, episode_feature=False)
3130

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

3534

3635
if __name__ == '__main__':
37-
main()
36+
example_pipeline_simple()

examples/example_pipeline_vdp.py

+7-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import pykoop.dynamic_models
88

99

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

4040
# Predict with re-lifting between timesteps (default)
41-
X_pred_local = kp.predict_state(
41+
X_pred_local = kp.predict_trajectory(
4242
x0,
4343
u,
4444
relift_state=True,
4545
episode_feature=False,
4646
)
4747

4848
# Predict without re-lifting between timesteps
49-
X_pred_global = kp.predict_state(
49+
X_pred_global = kp.predict_trajectory(
5050
x0,
5151
u,
5252
relift_state=False,
@@ -85,7 +85,7 @@ def main() -> None:
8585
Psi_valid = kp.lift(X_valid[:, 1:], episode_feature=False)
8686

8787
# Predict lifted state with re-lifting between timesteps (default)
88-
Psi_pred_local = kp.predict_state(
88+
Psi_pred_local = kp.predict_trajectory(
8989
x0,
9090
u,
9191
relift_state=True,
@@ -95,7 +95,7 @@ def main() -> None:
9595
)
9696

9797
# Predict lifted state without re-lifting between timesteps
98-
Psi_pred_global = kp.predict_state(
98+
Psi_pred_global = kp.predict_trajectory(
9999
x0,
100100
u,
101101
relift_state=False,
@@ -141,8 +141,7 @@ def main() -> None:
141141
ax[0, 0].set_ylabel('$u$')
142142
ax[0, 0].set_title('Exogenous input')
143143

144-
plt.show()
145-
146144

147145
if __name__ == '__main__':
148-
main()
146+
example_pipeline_vdp()
147+
plt.show()

0 commit comments

Comments
 (0)