Skip to content

Commit

Permalink
Merge pull request #43 from dynamicslab/fix-predict-simulate-bug
Browse files Browse the repository at this point in the history
fix bug in prediction and upgrade nndmd
  • Loading branch information
pswpswpsw authored Feb 8, 2024
2 parents 4e38e25 + 24d4ec1 commit 3db097b
Show file tree
Hide file tree
Showing 32 changed files with 5,360 additions and 4,924 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ jobs:
py.test test
- name: Generate coverage report
run: |
pip install pytest
pip install pytest-cov
pip install pytest==7.4.4
pip install pytest-cov==4.1.0
pytest --cov=./ --cov-report=xml
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
Expand Down
39 changes: 39 additions & 0 deletions .gitignore~
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
build
dist
*.egg-info
# automatically generated by setuptools-scm
pysindy/version.py
# sphinx gallery files
docs/examples
# virtual environment
venv

# eggs
.eggs

dev

**/*cache*

.coverage
coverage.xml

.idea

docs/api
docs/_build

.ipynb_checkpoints
*/.ipynb_checkpoints/*
*/lightning_logs/*
lightning_logs
.DS_Store
.vscode

*.pyc

*.sublime*

Pipfile
Pipfile.lock
/htmlcov/
Binary file added .requirements-torch.txt.un~
Binary file not shown.
188 changes: 150 additions & 38 deletions docs/tutorial_compose_observables.ipynb

Large diffs are not rendered by default.

478 changes: 244 additions & 234 deletions docs/tutorial_compute_differentiation.ipynb

Large diffs are not rendered by default.

1,046 changes: 529 additions & 517 deletions docs/tutorial_dmd_failed_for_pde_examples.ipynb

Large diffs are not rendered by default.

833 changes: 424 additions & 409 deletions docs/tutorial_dmd_separating_two_mixed_signals_400d_system.ipynb

Large diffs are not rendered by default.

1,363 changes: 781 additions & 582 deletions docs/tutorial_dmd_succeeds_pde_examples.ipynb

Large diffs are not rendered by default.

66 changes: 38 additions & 28 deletions docs/tutorial_dmd_with_control_128d_system.ipynb

Large diffs are not rendered by default.

758 changes: 392 additions & 366 deletions docs/tutorial_dmd_with_control_2d_system.ipynb

Large diffs are not rendered by default.

786 changes: 367 additions & 419 deletions docs/tutorial_koopman_edmd_with_rbf.ipynb

Large diffs are not rendered by default.

759 changes: 397 additions & 362 deletions docs/tutorial_koopman_edmdc_for_chaotic_duffing_oscillator.ipynb

Large diffs are not rendered by default.

897 changes: 467 additions & 430 deletions docs/tutorial_koopman_edmdc_for_vdp_system.ipynb

Large diffs are not rendered by default.

1,613 changes: 850 additions & 763 deletions docs/tutorial_koopman_eigenfunction_model_slow_manifold.ipynb

Large diffs are not rendered by default.

176 changes: 56 additions & 120 deletions docs/tutorial_koopman_hankel_dmdc_for_vdp_system.ipynb

Large diffs are not rendered by default.

126 changes: 52 additions & 74 deletions docs/tutorial_koopman_havok_3d_lorenz.ipynb

Large diffs are not rendered by default.

210 changes: 87 additions & 123 deletions docs/tutorial_koopman_kdmd_on_slow_manifold.ipynb

Large diffs are not rendered by default.

114 changes: 56 additions & 58 deletions docs/tutorial_koopman_nndmd_examples.ipynb

Large diffs are not rendered by default.

161 changes: 51 additions & 110 deletions docs/tutorial_linear_random_control_system.ipynb

Large diffs are not rendered by default.

Large diffs are not rendered by default.

185 changes: 75 additions & 110 deletions docs/tutorial_sparse_modes_selection_2d_linear_system.ipynb

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pykoopman"
version = "1.0.8"
version = "1.0.9"
authors = [
{ name = "Shaowu Pan", email = "[email protected]" },
{ name = "Eurika Kaiser", email = "[email protected]" },
Expand Down Expand Up @@ -42,6 +42,7 @@ dependencies = [

[project.optional-dependencies]
dev = [
"pytest <= 7.4.4",
"pytest-cov ~= 4.1.0",
"pytest-lazy-fixture ~= 0.6.3",
"flake8-builtins-unleashed ~= 1.3.1",
Expand Down
24 changes: 24 additions & 0 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-e .
-r requirements.txt
# -r requirements-torch.txt --extra-index-url https://download.pytorch.org/whl/cu121/
-f https://download.pytorch.org/whl/cu121/torch_stable.html
-r requirements-torch.txt

pytest <= 7.4.4
pytest-cov ~= 4.1.0
pytest-lazy-fixture ~= 0.6.3
flake8-builtins-unleashed ~= 1.3.1
setuptools_scm ~= 8.0.2
setuptools_scm_git_archive
jupyter >= 1.0.0
notebook > 7.0.0, <= 7.0.4
nbsphinx
sphinx-codeautolink
sphinx >= 3, <= 7.0.0
sphinxcontrib-apidoc
sphinx_rtd_theme
pre-commit
sphinx-nbexamples
jupyter_contrib_nbextensions
PyQt5
osqp
3 changes: 3 additions & 0 deletions requirements-torch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
torch == 2.1.0+cu121
torchvision
lightning
3 changes: 3 additions & 0 deletions requirements-torch.txt~
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
torch
torchvision
lightning
8 changes: 8 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
matplotlib == 3.5.0
derivative ~= 0.6.0
scikit-learn == 1.1.3
numpy >= 1.20, <= 1.26
scipy > 1.6.0, <= 1.11.2
pydmd > 0.4, <= 0.4.1
optht ~= 0.2.0
prettytable > 3.0.0, <= 3.9.0
10 changes: 8 additions & 2 deletions src/pykoopman/common/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@


def validate_input(x, t=T_DEFAULT):
if not isinstance(x, np.ndarray):
raise ValueError("x must be array-like")
if not isinstance(x, np.ndarray) and not isinstance(x, list):
raise ValueError("x must be array-like OR a list of array-like")
elif isinstance(x, list):
for i in range(len(x)):
x[i] = validate_input(x[i], t)
return x
elif x.ndim == 1:
x = x.reshape(-1, 1)
x = check_array(x)

# add another case if x is a list of trajectory

if t is not T_DEFAULT:
if t is None:
raise ValueError("t must be a scalar or array-like.")
Expand Down
67 changes: 36 additions & 31 deletions src/pykoopman/koopman.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import numpy as np
from numpy import empty
from numpy import vstack
from pydmd import DMD
from pydmd import DMDBase
from sklearn.base import BaseEstimator
Expand Down Expand Up @@ -186,7 +185,9 @@ def fit(self, x, y=None, u=None, dt=1):
self.n_control_features_ = self._pipeline.steps[1][1].n_control_features_

# compute amplitudes
if y is None:
if isinstance(x, list):
self._amplitudes = None
elif y is None:
if hasattr(self.observables, "n_consumed_samples"):
# g0 = self.observables.transform(
# x[0 : 1 + self.observables.n_consumed_samples]
Expand All @@ -196,6 +197,7 @@ def fit(self, x, y=None, u=None, dt=1):
)
else:
# g0 = self.observables.transform(x[0:1])

self._amplitudes = np.abs(self.psi(x[0:1].T))
else:
self._amplitudes = None
Expand All @@ -221,12 +223,13 @@ def predict(self, x, u=None):
Time series of external actuation/control.
Returns:
y: numpy.ndarray, shape (n_samples, n_input_features)
x_next: numpy.ndarray, shape (n_samples, n_input_features)
Predicted state one timestep in the future.
"""

check_is_fitted(self, "n_output_features_")
return self.observables.inverse(self._step(x, u))
x_next = self.observables.inverse(self._step(x, u))
return x_next

def simulate(self, x0, u=None, n_steps=1):
"""Simulate an initial state forward in time with the learned Koopman model.
Expand All @@ -247,44 +250,46 @@ def simulate(self, x0, u=None, n_steps=1):
Number of forward steps to be simulated.
Returns:
y: numpy.ndarray, shape (n_steps, n_input_features)
x: numpy.ndarray, shape (n_steps, n_input_features)
Simulated states.
Note that `y[0, :]` is one timestep ahead of `x0`.
Note that `x[0, :]` is one timestep ahead of `x0`.
"""
check_is_fitted(self, "n_output_features_")
# Could have an option to only return the end state and not all
# intermediate states to save memory.

y = empty((n_steps, self.n_input_features_), dtype=self.A.dtype)
if u is None:
y[0] = self.predict(x0)
if x0.ndim == 1: # handle non-time delay input but 1D accidently
x0 = x0.reshape(-1, 1)
elif x0.ndim == 2 and x0.shape[0] > 1: # handle time delay input
x0 = x0.T
else:
y[0] = self.predict(x0, u[0])
raise TypeError("Check your initial condition shape!")

if isinstance(self.observables, TimeDelay):
n_consumed_samples = self.observables.n_consumed_samples
if u is None:
for k in range(n_consumed_samples):
y[k + 1] = self.predict(vstack((x0[k + 1 :], y[: k + 1])))
for k in range(n_consumed_samples, n_steps - 1):
y[k + 1] = self.predict(y[k - n_consumed_samples : k + 1])
else:
for k in range(n_consumed_samples):
y[k + 1] = self.predict(vstack((x0[k + 1 :], y[: k + 1])), u[k + 1])
for k in range(n_consumed_samples, n_steps - 1):
y[k + 1] = self.predict(y[k - n_consumed_samples : k + 1], u[k + 1])
# x = empty((n_steps, self.n_input_features_), dtype=self.A.dtype)
y = empty((n_steps, self.A.shape[0]), dtype=self.W.dtype)

if u is None:
# lifted eigen space and move 1 step forward
y[0] = self.lamda @ self.psi(x0).flatten()

# iterate in the lifted space
for k in range(n_steps - 1):
# tmp = self.W @ self.lamda**(k+1) @ y[0].reshape(-1,1)
y[k + 1] = self.lamda @ y[k]
x = np.transpose(self.W @ y.T)
x = x.astype(self.A.dtype)
else:
if u is None:
for k in range(n_steps - 1):
y[k + 1] = self.predict(y[k].reshape(1, -1))
else:
for k in range(n_steps - 1):
y[k + 1] = self.predict(
y[k].reshape(1, -1), u[k + 1].reshape(1, -1)
)
# lifted space (not eigen)
y[0] = self.A @ self.phi(x0).flatten() + self.B @ u[0]

# iterate in the lifted space
for k in range(n_steps - 1):
tmp = self.A @ y[k].reshape(-1, 1) + self.B @ u[k + 1].reshape(-1, 1)
y[k + 1] = tmp.flatten()
x = np.transpose(self.C @ y.T)
x = x.astype(self.A.dtype)

return y
return x

def get_feature_names(self, input_features=None):
"""Get the names of the individual features constituting the observables.
Expand Down
12 changes: 10 additions & 2 deletions src/pykoopman/observables/_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,20 @@ def fit(self, x, y=None):
Returns:
self: Returns a fit instance of the class `pykoopman.observables.Identity`.
Note:
only identity mapping is supported for list of arb trajectories
"""
x = validate_input(x)
self.n_input_features_ = self.n_output_features_ = x.shape[1]
if not isinstance(x, list):
self.n_input_features_ = self.n_output_features_ = x.shape[1]
self.measurement_matrix_ = np.eye(x.shape[1]).T
else:
self.n_input_features_ = self.n_output_features_ = x[0].shape[1]
self.measurement_matrix_ = np.eye(x[0].shape[1]).T

self.n_consumed_samples = 0

self.measurement_matrix_ = np.eye(x.shape[1]).T
return self

def transform(self, x):
Expand Down
3 changes: 2 additions & 1 deletion src/pykoopman/observables/_time_delay.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def __init__(self, delay=1, n_delays=2):
Initialize the TimeDelay class with given parameters.
Args:
delay (int, optional): The length of each delay. Defaults to 1.
delay (int, optional): The length of each delay. Defaults to 1. Or
we say this is the "stride of delay".
n_delays (int, optional): The number of delays to compute for each
variable. Defaults to 2.
Expand Down
35 changes: 31 additions & 4 deletions src/pykoopman/regression/_nndmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ class FFNN(nn.Module):
layers (nn.ModuleList): A list of the neural network layers.
"""

def __init__(self, input_size, hidden_sizes, output_size, activations):
def __init__(
self, input_size, hidden_sizes, output_size, activations, include_state=False
):
super(FFNN, self).__init__()

activations_dict = {
Expand All @@ -103,6 +105,12 @@ def __init__(self, input_size, hidden_sizes, output_size, activations):
"mish": nn.Mish(),
"linear": nn.Identity(),
}
# whether to directly encode state in observables
self.include_state = include_state
if self.include_state:
output_size_ = output_size - input_size
else:
output_size_ = output_size

# Define the activation
act = activations_dict[activations]
Expand All @@ -117,7 +125,8 @@ def __init__(self, input_size, hidden_sizes, output_size, activations):
bias = True

if len(hidden_sizes) == 0:
self.layers.append(nn.Linear(input_size, output_size, bias))
# if no hidden layer, then entire NN is just a linear one
self.layers.append(nn.Linear(input_size, output_size_, bias))
else:
self.layers.append(nn.Linear(input_size, hidden_sizes[0], bias))
if activations != "linear":
Expand All @@ -132,8 +141,7 @@ def __init__(self, input_size, hidden_sizes, output_size, activations):
self.layers.append(act)

# Define the last output layer
bias_last = False # True # last layer with bias
self.layers.append(nn.Linear(hidden_sizes[-1], output_size, bias_last))
self.layers.append(nn.Linear(hidden_sizes[-1], output_size_, bias=False))

def forward(self, x):
"""Performs a forward pass through the neural network.
Expand All @@ -144,11 +152,24 @@ def forward(self, x):
Returns:
torch.Tensor: The output tensor of the neural network.
"""
in_x = x
for layer in self.layers:
x = layer(x)

if self.include_state:
x = torch.cat((in_x, x), 1)
return x


class HardCodedLinearLayer(nn.Module):
def __init__(self, input_size, output_size):

pass

def forward(self, x):
pass


class BaseKoopmanOperator(nn.Module):
"""Base class for Koopman operator models.
Expand Down Expand Up @@ -186,6 +207,8 @@ def forward(self, x):
"""
Computes the forward pass of the `BaseKoopmanOperator`.
Given `x` as a row vector, return `x @ K.T`
Args:
x (torch.Tensor): The input tensor.
Expand Down Expand Up @@ -362,6 +385,7 @@ def __init__(
config_decoder=dict(),
lbfgs=False,
std_koopman=1e-1,
include_state=False,
):
super(DLKoopmanRegressor, self).__init__()

Expand All @@ -373,6 +397,7 @@ def __init__(
hidden_sizes=config_encoder["hidden_sizes"],
output_size=config_encoder["output_size"],
activations=config_encoder["activations"],
include_state=include_state,
)

self._decoder = FFNN(
Expand Down Expand Up @@ -1076,6 +1101,7 @@ def __init__(
normalize_mode="equal",
normalize_std_factor=2.0,
std_koopman=1e-1,
include_state=False,
trainer_kwargs={},
):
"""Initializes the NNDMD model."""
Expand All @@ -1091,6 +1117,7 @@ def __init__(
self.normalize_std_factor = normalize_std_factor
self.batch_size = batch_size
self.std_koopman = std_koopman
self.include_state = include_state

# build DLK regressor
self._regressor = DLKoopmanRegressor(
Expand Down
Loading

0 comments on commit 3db097b

Please sign in to comment.