Skip to content

Commit

Permalink
Merge pull request #129 from decargroup/feature/125-allow-creation-of…
Browse files Browse the repository at this point in the history
…-a-koopmanregressor-object-from-a-koopman-matrix

Allow creation of a `KoopmanRegressor` object from a Koopman matrix
  • Loading branch information
sdahdah committed Feb 10, 2023
2 parents 2bd980c + db17340 commit 2a7c24a
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 8 deletions.
6 changes: 6 additions & 0 deletions doc/pykoop.rst
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,18 @@ The following class and function implementations are located in
``pykoop.regressors``, but have been imported into the ``pykoop`` namespace for
convenience.

The :class:`pykoop.DataRegressor` regressor is a dummy regressor if you want to
force the Koopman matrix to take on a specific value (maybe you know what it
should be, or you got it from another library).

.. autosummary::
:toctree: _autosummary/

pykoop.Dmd
pykoop.Dmdc
pykoop.Edmd
pykoop.EdmdMeta
pykoop.DataRegressor


Kernel approximation methods
Expand Down Expand Up @@ -262,6 +267,7 @@ The following class and function implementations are located in
:toctree: _autosummary/

pykoop.dynamic_models.DiscreteVanDerPol
pykoop.dynamic_models.DuffingOscillator
pykoop.dynamic_models.MassSpringDamper
pykoop.dynamic_models.Pendulum

Expand Down
2 changes: 1 addition & 1 deletion pykoop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
KernelApproxLiftingFn,
SkLearnLiftingFn,
)
from .regressors import Dmd, Dmdc, Edmd, EdmdMeta
from .regressors import Dmd, Dmdc, Edmd, EdmdMeta, DataRegressor
from .tsvd import Tsvd
from .util import (
AnglePreprocessor,
Expand Down
7 changes: 5 additions & 2 deletions pykoop/koopman_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -1193,8 +1193,11 @@ def fit(self,
X = sklearn.utils.validation.check_array(
X, **self._check_array_params)
else:
X, y = sklearn.utils.validation.check_X_y(X, y,
**self._check_X_y_params)
X, y = sklearn.utils.validation.check_X_y(
X,
y,
**self._check_X_y_params,
)
# Validate constructor parameters
self._validate_parameters()
# Compute fit attributes
Expand Down
78 changes: 78 additions & 0 deletions pykoop/regressors.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,3 +426,81 @@ def _more_tags(self):
'check_fit_check_is_fitted': reason,
}
}


class DataRegressor(koopman_pipeline.KoopmanRegressor):
"""Create a :class:`KoopmanRegressor` object from a NumPy array.
This is useful for interoperability with other packages, where you may
already have a Koopman (or discrete-time state-space) matirx, but you want
to test it with ``pykoop``.
Attributes
----------
n_features_in_ : int
Number of features input, including episode feature.
n_states_in_ : int
Number of states input.
n_inputs_in_ : int
Number of inputs input.
episode_feature_ : bool
Indicates if episode feature was present during :func:`fit`.
feature_names_in_ : np.ndarray
Array of input feature name strings.
coef_ : np.ndarray
Fit coefficient matrix.
Examples
--------
Create a Koopman pipeline with the Koopman matrix forced to be::
0.0 -0.5
1.0 -0.5
0.0 1.0
You may want to do this because you have a Koopman matrix from another
library and you want to use ``pykoop`` to evaluate its performance.
>>> kp = pykoop.KoopmanPipeline(regressor=pykoop.DataRegressor(
... coef=np.array([[0, 1, 0], [-0.5, -0.5, 1]]).T)
... )
>>> kp.fit(X_msd, n_inputs=1, episode_feature=True)
KoopmanPipeline(regressor=DataRegressor(coef=array(...
"""

def __init__(self, coef: np.ndarray = None) -> None:
"""Instantiate :class:`DataRegressor`.
Parameters
----------
coef : np.ndarray
Coefficient matrix to copy to ``coef_``. If ``None``, an
appropriately-sized zero matrix is used.
"""
self.coef = coef

def _fit_regressor(self, X_unshifted: np.ndarray,
X_shifted: np.ndarray) -> np.ndarray:
required_shape = (X_unshifted.shape[1], X_shifted.shape[1])
if self.coef is None:
coef = np.zeros(required_shape)
else:
if self.coef.shape != required_shape:
raise ValueError(
f'Parameter `coef` has shape {self.coef.shape} data `X` '
f'requires shape {required_shape}.')
coef = np.copy(self.coef)
return coef

def _validate_parameters(self) -> None:
# No parameters to validate
pass

def _more_tags(self):
return {
'multioutput': True,
'multioutput_only': True,
# Allow a bad score since the ``coef_`` matrix will be filled with
# zeros, and we just care to test ``scikit-learn`` API compliance.
'poor_score': True,
}
59 changes: 54 additions & 5 deletions tests/test_regressors.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,51 @@ def test_predict(
)


class TestDataRegressorExact:
"""Test :class:`DataRegressor` with exact solutions."""

def test_fit(self, request):
"""Test fit accuracy by comparing Koopman matrix."""
# Get fixture from name
msd = request.getfixturevalue('mass_spring_damper_sine_input')
regressor = pykoop.DataRegressor(coef=msd['U_valid'].T)
# Fit regressor
regressor.fit(
msd['X_train'],
msd['Xp_train'],
n_inputs=msd['n_inputs'],
episode_feature=msd['episode_feature'],
)
# Test value of Koopman operator
np.testing.assert_allclose(
regressor.coef_.T,
msd['U_valid'],
atol=1e-5,
rtol=0,
)

def test_predict(self, request):
"""Test fit accuracy by comparing prediction."""
# Get fixture from name
msd = request.getfixturevalue('mass_spring_damper_sine_input')
regressor = pykoop.DataRegressor(coef=msd['U_valid'].T)
# Fit regressor
regressor.fit(
msd['X_train'],
msd['Xp_train'],
n_inputs=msd['n_inputs'],
episode_feature=msd['episode_feature'],
)
# Test prediction
prediction = regressor.predict(msd['X_valid'])
np.testing.assert_allclose(
prediction,
msd['Xp_valid'],
atol=1e-3,
rtol=0,
)


@pytest.mark.parametrize(
'regressor, mass_spring_damper',
[
Expand Down Expand Up @@ -210,6 +255,7 @@ class TestSkLearn:
pykoop.EdmdMeta(regressor=sklearn.linear_model.Ridge(alpha=1)),
pykoop.Dmdc(),
pykoop.Dmd(),
pykoop.DataRegressor(),
])
def test_compatible_estimator(self, estimator, check):
"""Test ``scikit-learn`` compatibility of estimators."""
Expand All @@ -224,11 +270,14 @@ class TestExceptions:
[4, 3, 2, 1],
])

@pytest.mark.parametrize('estimator', [
pykoop.Edmd(alpha=-1),
pykoop.Dmdc(mode_type='blah'),
pykoop.Dmd(mode_type='blah'),
])
@pytest.mark.parametrize(
'estimator',
[
pykoop.Edmd(alpha=-1),
pykoop.Dmdc(mode_type='blah'),
pykoop.Dmd(mode_type='blah'),
pykoop.DataRegressor(coef=np.eye(2)), # Wrong dimensions for data
])
def test_invalid_params(self, estimator):
"""Test a selection of invalid estimator parameter."""
with pytest.raises(ValueError):
Expand Down

0 comments on commit 2a7c24a

Please sign in to comment.