kkf 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kkf-0.3.0/KKF/__init__.py +48 -0
- kkf-0.3.0/KKF/covariances.py +159 -0
- kkf-0.3.0/KKF/filter.py +250 -0
- kkf-0.3.0/KKF/koopman.py +155 -0
- kkf-0.3.0/KKF/solution.py +145 -0
- kkf-0.3.0/KKF/systems.py +200 -0
- kkf-0.3.0/KKF/utils.py +0 -0
- kkf-0.3.0/LICENSE +21 -0
- kkf-0.3.0/PKG-INFO +367 -0
- kkf-0.3.0/README.md +317 -0
- kkf-0.3.0/kkf.egg-info/PKG-INFO +367 -0
- kkf-0.3.0/kkf.egg-info/SOURCES.txt +18 -0
- kkf-0.3.0/kkf.egg-info/dependency_links.txt +1 -0
- kkf-0.3.0/kkf.egg-info/requires.txt +25 -0
- kkf-0.3.0/kkf.egg-info/top_level.txt +1 -0
- kkf-0.3.0/pyproject.toml +160 -0
- kkf-0.3.0/setup.cfg +4 -0
- kkf-0.3.0/setup.py +8 -0
- kkf-0.3.0/tests/test_core.py +357 -0
- kkf-0.3.0/tests/test_utils.py +263 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Kernel Koopman Kalman Filter (KKF) - A library for nonlinear state estimation.
|
|
2
|
+
|
|
3
|
+
This package provides a complete implementation of the Koopman-Kalman Filter,
|
|
4
|
+
combining Koopman operator theory with Kalman filtering for accurate state
|
|
5
|
+
estimation in nonlinear dynamical systems.
|
|
6
|
+
|
|
7
|
+
Main Components
|
|
8
|
+
---------------
|
|
9
|
+
- systems : Dynamical system definitions
|
|
10
|
+
- koopman : Koopman operator approximation via kEDMD
|
|
11
|
+
- covariances : Covariance matrix computations
|
|
12
|
+
- solution : Filter solution data structure
|
|
13
|
+
- filter : Main filtering algorithm
|
|
14
|
+
|
|
15
|
+
Quick Start
|
|
16
|
+
-----------
|
|
17
|
+
>>> from KKF import DynamicalSystem, KoopmanOperator, apply_koopman_kalman_filter
|
|
18
|
+
>>> # or
|
|
19
|
+
>>> from KKF.filter import apply_koopman_kalman_filter
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Core classes and utilities
|
|
23
|
+
from .covariances import (
|
|
24
|
+
compute_dynamics_covariance,
|
|
25
|
+
compute_initial_covariance,
|
|
26
|
+
compute_observation_covariance,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Main filtering function
|
|
30
|
+
from .filter import apply_koopman_kalman_filter
|
|
31
|
+
from .koopman import KoopmanOperator
|
|
32
|
+
from .solution import KoopmanKalmanFilterSolution
|
|
33
|
+
from .systems import DynamicalSystem, create_additive_system
|
|
34
|
+
|
|
35
|
+
__all__ = [
|
|
36
|
+
# Classes
|
|
37
|
+
"DynamicalSystem",
|
|
38
|
+
"KoopmanOperator",
|
|
39
|
+
"KoopmanKalmanFilterSolution",
|
|
40
|
+
# Functions
|
|
41
|
+
"create_additive_system",
|
|
42
|
+
"apply_koopman_kalman_filter",
|
|
43
|
+
"compute_initial_covariance",
|
|
44
|
+
"compute_dynamics_covariance",
|
|
45
|
+
"compute_observation_covariance",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Any, Callable, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from scipy.stats import rv_continuous
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_initial_covariance(
|
|
9
|
+
x: NDArray[np.float64],
|
|
10
|
+
n_features: int,
|
|
11
|
+
initial_distribution: rv_continuous,
|
|
12
|
+
koopman_operator: Any,
|
|
13
|
+
n_samples: int,
|
|
14
|
+
) -> NDArray[np.float64]:
|
|
15
|
+
"""
|
|
16
|
+
Compute the covariance matrix for the initial distribution in feature space.
|
|
17
|
+
|
|
18
|
+
This function samples from an initial distribution and computes the covariance
|
|
19
|
+
matrix of the transformed samples using a Koopman operator's feature map.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
x : np.ndarray
|
|
24
|
+
Current state vector or reference point.
|
|
25
|
+
n_features : int
|
|
26
|
+
Number of features in the transformed space.
|
|
27
|
+
initial_distribution : scipy.stats.rv_continuous
|
|
28
|
+
Initial probability distribution to sample from.
|
|
29
|
+
koopman_operator : object
|
|
30
|
+
Object containing the feature map phi method for state transformation.
|
|
31
|
+
Must have a method phi(x) that maps states to feature space.
|
|
32
|
+
n_samples : int
|
|
33
|
+
Number of samples to use for covariance estimation.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
np.ndarray
|
|
38
|
+
Covariance matrix of size (n_features, n_features) in the transformed space.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
The function performs the following steps:
|
|
43
|
+
1. Samples from the initial distribution
|
|
44
|
+
2. Applies the Koopman operator's feature map to each sample
|
|
45
|
+
3. Computes the covariance matrix of the transformed samples
|
|
46
|
+
"""
|
|
47
|
+
# Sample from initial distribution
|
|
48
|
+
samples = initial_distribution.rvs(size=n_samples).reshape((n_samples, len(x)))
|
|
49
|
+
|
|
50
|
+
# Initialize array for transformed samples
|
|
51
|
+
transformed_samples = np.zeros((n_samples, n_features))
|
|
52
|
+
|
|
53
|
+
# Transform each sample using the feature map
|
|
54
|
+
for i in range(n_samples):
|
|
55
|
+
transformed_samples[i, :] = koopman_operator.phi(samples[i, :])
|
|
56
|
+
|
|
57
|
+
# Compute and return covariance matrix
|
|
58
|
+
return np.cov(transformed_samples, rowvar=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def compute_dynamics_covariance(
|
|
62
|
+
x: NDArray[np.float64], n_features: int, dynamics: Any, koopman_operator: Any, n_samples: int
|
|
63
|
+
) -> NDArray[np.float64]:
|
|
64
|
+
"""
|
|
65
|
+
Compute the covariance matrix for the system dynamics in feature space.
|
|
66
|
+
|
|
67
|
+
This function samples from the dynamics distribution, applies the system
|
|
68
|
+
dynamics, and computes the covariance matrix of the transformed results.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
x : np.ndarray
|
|
73
|
+
Current state vector or reference point.
|
|
74
|
+
n_features : int
|
|
75
|
+
Number of features in the transformed space.
|
|
76
|
+
dynamics : object
|
|
77
|
+
Dynamical system object containing:
|
|
78
|
+
- dist_dyn: scipy.stats.rv_continuous distribution for dynamics noise
|
|
79
|
+
- dynamics(x, w): method implementing the system dynamics
|
|
80
|
+
koopman_operator : object
|
|
81
|
+
Object containing the feature map phi method for state transformation.
|
|
82
|
+
Must have a method phi(x) that maps states to feature space.
|
|
83
|
+
n_samples : int
|
|
84
|
+
Number of samples to use for covariance estimation.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
np.ndarray
|
|
89
|
+
Covariance matrix of size (n_features, n_features) in the transformed space.
|
|
90
|
+
|
|
91
|
+
Notes
|
|
92
|
+
-----
|
|
93
|
+
The function performs the following steps:
|
|
94
|
+
1. Samples from the dynamics noise distribution
|
|
95
|
+
2. Applies the system dynamics to each sample
|
|
96
|
+
3. Transforms the results using the Koopman operator's feature map
|
|
97
|
+
4. Computes the covariance matrix of the transformed samples
|
|
98
|
+
"""
|
|
99
|
+
# Sample from dynamics distribution
|
|
100
|
+
noise_samples = dynamics.dist_dyn.rvs(size=n_samples).reshape((n_samples, len(x)))
|
|
101
|
+
|
|
102
|
+
# Initialize array for transformed samples
|
|
103
|
+
transformed_samples = np.zeros((n_samples, n_features))
|
|
104
|
+
|
|
105
|
+
# Apply dynamics and transform each sample
|
|
106
|
+
for i in range(n_samples):
|
|
107
|
+
state_evolution = dynamics.dynamics(x, noise_samples[i, :])
|
|
108
|
+
transformed_samples[i, :] = koopman_operator.phi(state_evolution)
|
|
109
|
+
|
|
110
|
+
# Compute and return covariance matrix
|
|
111
|
+
return np.cov(transformed_samples, rowvar=False)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def compute_observation_covariance(
|
|
115
|
+
x: NDArray[np.float64], n_outputs: int, dynamics: Any, n_samples: int
|
|
116
|
+
) -> NDArray[np.float64]:
|
|
117
|
+
"""
|
|
118
|
+
Compute the covariance matrix for the observation/measurement process.
|
|
119
|
+
|
|
120
|
+
This function samples from the measurement noise distribution and computes
|
|
121
|
+
the covariance matrix of the measurement process.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
x : np.ndarray
|
|
126
|
+
Current state vector or reference point.
|
|
127
|
+
n_outputs : int
|
|
128
|
+
Number of measurement outputs.
|
|
129
|
+
dynamics : object
|
|
130
|
+
Dynamical system object containing:
|
|
131
|
+
- dist_obs: scipy.stats.rv_continuous distribution for measurement noise
|
|
132
|
+
- measurements(x, w): method implementing the measurement process
|
|
133
|
+
n_samples : int
|
|
134
|
+
Number of samples to use for covariance estimation.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
np.ndarray
|
|
139
|
+
Covariance matrix of size (n_outputs, n_outputs) for the measurement process.
|
|
140
|
+
|
|
141
|
+
Notes
|
|
142
|
+
-----
|
|
143
|
+
The function performs the following steps:
|
|
144
|
+
1. Samples from the measurement noise distribution
|
|
145
|
+
2. Applies the measurement function to each sample
|
|
146
|
+
3. Computes the covariance matrix of the measurements
|
|
147
|
+
"""
|
|
148
|
+
# Sample from measurement distribution
|
|
149
|
+
noise_samples = dynamics.dist_obs.rvs(size=n_samples).reshape((n_samples, n_outputs))
|
|
150
|
+
|
|
151
|
+
# Initialize array for measurements
|
|
152
|
+
measurement_samples = np.zeros((n_samples, n_outputs))
|
|
153
|
+
|
|
154
|
+
# Apply measurement function to each sample
|
|
155
|
+
for i in range(n_samples):
|
|
156
|
+
measurement_samples[i, :] = dynamics.measurements(x, noise_samples[i, :])
|
|
157
|
+
|
|
158
|
+
# Compute and return covariance matrix (ensure 2D even for single output)
|
|
159
|
+
return np.atleast_2d(np.cov(measurement_samples, rowvar=False))
|
kkf-0.3.0/KKF/filter.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Koopman Kalman Filter algorithm implementation.
|
|
2
|
+
|
|
3
|
+
Combines Koopman operator theory with Kalman filtering for nonlinear
|
|
4
|
+
state estimation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.linalg import pinv
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
|
|
13
|
+
from .covariances import (
|
|
14
|
+
compute_dynamics_covariance,
|
|
15
|
+
compute_initial_covariance,
|
|
16
|
+
compute_observation_covariance,
|
|
17
|
+
)
|
|
18
|
+
from .solution import KoopmanKalmanFilterSolution
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def apply_koopman_kalman_filter(
|
|
22
|
+
koopman_operator: Any,
|
|
23
|
+
observations: NDArray[np.float64],
|
|
24
|
+
initial_distribution: Any,
|
|
25
|
+
n_features: int,
|
|
26
|
+
optimize: bool = True,
|
|
27
|
+
n_restarts_optimizer: int = 10,
|
|
28
|
+
noise_samples: int = 100,
|
|
29
|
+
) -> KoopmanKalmanFilterSolution:
|
|
30
|
+
"""
|
|
31
|
+
Implementation of the Koopman-Kalman Filter algorithm.
|
|
32
|
+
|
|
33
|
+
This function combines Koopman operator theory with Kalman filtering to perform
|
|
34
|
+
state estimation for nonlinear dynamical systems. It operates by lifting the
|
|
35
|
+
state space to a higher-dimensional feature space where the dynamics are
|
|
36
|
+
approximately linear.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
koopman_operator : object
|
|
41
|
+
Object containing Koopman operator methods and attributes:
|
|
42
|
+
- compute_edmd(n_features): Extended Dynamic Mode Decomposition method
|
|
43
|
+
- dynamical_system: Dynamical system object
|
|
44
|
+
- U, B, C: Matrices for Koopman approximation
|
|
45
|
+
- phi: Feature map function
|
|
46
|
+
observations : np.ndarray
|
|
47
|
+
Array of shape (n_timesteps, n_outputs) containing system measurements.
|
|
48
|
+
initial_distribution : object
|
|
49
|
+
Distribution object for initial state with methods:
|
|
50
|
+
- mean: Returns mean of initial state
|
|
51
|
+
- rvs: Random sampling method
|
|
52
|
+
n_features : int
|
|
53
|
+
Number of features in the lifted space.
|
|
54
|
+
optimize : bool, optional
|
|
55
|
+
Whether to optimize the kernel parameters. Default is True.
|
|
56
|
+
n_restarts_optimizer : int, optional
|
|
57
|
+
Number of restarts for kernel optimization. Default is 10.
|
|
58
|
+
noise_samples : int, optional
|
|
59
|
+
Number of samples for noise covariance estimation. Default is 100.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
KoopmanKalmanFilterSolution
|
|
64
|
+
Object containing all filter estimates and covariances.
|
|
65
|
+
|
|
66
|
+
Notes
|
|
67
|
+
-----
|
|
68
|
+
The algorithm consists of three main phases:
|
|
69
|
+
1. Initialization:
|
|
70
|
+
- Sets up initial states and covariances
|
|
71
|
+
- Computes Koopman approximation
|
|
72
|
+
|
|
73
|
+
2. Prediction Step:
|
|
74
|
+
- Projects state forward using system dynamics
|
|
75
|
+
- Updates covariances using Koopman operator
|
|
76
|
+
|
|
77
|
+
3. Update Step:
|
|
78
|
+
- Incorporates new measurements
|
|
79
|
+
- Updates state and covariance estimates
|
|
80
|
+
|
|
81
|
+
The filter operates in both the original state space (x) and the lifted
|
|
82
|
+
feature space (z), maintaining estimates and covariances in both spaces.
|
|
83
|
+
"""
|
|
84
|
+
# Compute Koopman approximation
|
|
85
|
+
koopman_operator.compute_edmd(n_features, optimize, n_restarts_optimizer)
|
|
86
|
+
dynamical_system = koopman_operator.dynamical_system
|
|
87
|
+
|
|
88
|
+
# Extract system and Koopman components
|
|
89
|
+
state_dynamics = dynamical_system.f
|
|
90
|
+
measurement_model = dynamical_system.g
|
|
91
|
+
U, B, C = koopman_operator.U, koopman_operator.B, koopman_operator.C
|
|
92
|
+
phi = koopman_operator.phi
|
|
93
|
+
|
|
94
|
+
### Initialization ###
|
|
95
|
+
mean_val = initial_distribution.mean # Initial state estimate
|
|
96
|
+
x0 = np.atleast_1d(np.asarray(mean_val() if callable(mean_val) else mean_val))
|
|
97
|
+
z0 = phi(x0) # Initial state in feature space
|
|
98
|
+
|
|
99
|
+
# Get problem dimensions
|
|
100
|
+
n_timesteps, n_states = observations.shape[0], len(x0)
|
|
101
|
+
n_outputs = observations.shape[1]
|
|
102
|
+
|
|
103
|
+
# Initialize state arrays
|
|
104
|
+
x_minus, x_plus = _initialize_state_arrays(n_timesteps, n_states)
|
|
105
|
+
z_minus, z_plus = _initialize_state_arrays(n_timesteps, n_features)
|
|
106
|
+
|
|
107
|
+
# Initialize covariance arrays
|
|
108
|
+
Px_minus, Px_plus = _initialize_covariance_arrays(n_timesteps, n_states)
|
|
109
|
+
Pz_minus, Pz_plus = _initialize_covariance_arrays(n_timesteps, n_features)
|
|
110
|
+
|
|
111
|
+
# Initialize filter matrices
|
|
112
|
+
S = np.zeros((n_timesteps, n_outputs, n_outputs)) # Innovation covariance
|
|
113
|
+
K = np.zeros((n_timesteps, n_features, n_outputs)) # Kalman gain
|
|
114
|
+
|
|
115
|
+
# Set initial conditions
|
|
116
|
+
x_minus[0, :], x_plus[0, :] = x0, x0
|
|
117
|
+
z_minus[0, :], z_plus[0, :] = z0, z0
|
|
118
|
+
|
|
119
|
+
# Initialize covariances
|
|
120
|
+
initial_covariance = compute_initial_covariance(
|
|
121
|
+
x_minus[0, :], n_features, initial_distribution, koopman_operator, noise_samples
|
|
122
|
+
)
|
|
123
|
+
Pz_minus[0, :, :], Pz_plus[0, :, :] = initial_covariance, initial_covariance
|
|
124
|
+
|
|
125
|
+
### Main Filter Loop ###
|
|
126
|
+
for t in range(1, n_timesteps):
|
|
127
|
+
# Prediction Step
|
|
128
|
+
x_minus[t], z_minus[t], Pz_minus[t] = _prediction_step(
|
|
129
|
+
x_plus[t - 1],
|
|
130
|
+
z_plus[t - 1],
|
|
131
|
+
Pz_plus[t - 1],
|
|
132
|
+
state_dynamics,
|
|
133
|
+
phi,
|
|
134
|
+
U,
|
|
135
|
+
dynamical_system,
|
|
136
|
+
koopman_operator,
|
|
137
|
+
n_features,
|
|
138
|
+
noise_samples,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Update Step
|
|
142
|
+
x_plus[t], z_plus[t], Pz_plus[t], S[t], K[t] = _update_step(
|
|
143
|
+
x_minus[t],
|
|
144
|
+
z_minus[t],
|
|
145
|
+
Pz_minus[t],
|
|
146
|
+
observations[t],
|
|
147
|
+
measurement_model,
|
|
148
|
+
C,
|
|
149
|
+
B,
|
|
150
|
+
dynamical_system,
|
|
151
|
+
noise_samples,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Update state-space covariances
|
|
155
|
+
Px_minus[t], Px_plus[t] = _update_state_covariances(Pz_minus[t], Pz_plus[t], B)
|
|
156
|
+
|
|
157
|
+
return KoopmanKalmanFilterSolution(x_plus, x_minus, Pz_plus, Pz_minus, Px_plus, Px_minus, S, K)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _initialize_state_arrays(
|
|
161
|
+
n_timesteps: int, n_dim: int
|
|
162
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
163
|
+
"""Initialize arrays for state estimates."""
|
|
164
|
+
return (np.zeros((n_timesteps, n_dim)), np.zeros((n_timesteps, n_dim)))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _initialize_covariance_arrays(
|
|
168
|
+
n_timesteps: int, n_dim: int
|
|
169
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
170
|
+
"""Initialize arrays for covariance matrices."""
|
|
171
|
+
return (np.zeros((n_timesteps, n_dim, n_dim)), np.zeros((n_timesteps, n_dim, n_dim)))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _prediction_step(
|
|
175
|
+
x_prev: NDArray[np.float64],
|
|
176
|
+
z_prev: NDArray[np.float64],
|
|
177
|
+
Pz_prev: NDArray[np.float64],
|
|
178
|
+
state_dynamics: Callable,
|
|
179
|
+
phi: Callable,
|
|
180
|
+
U: NDArray[np.float64],
|
|
181
|
+
dynamical_system: Any,
|
|
182
|
+
koopman_operator: Any,
|
|
183
|
+
n_features: int,
|
|
184
|
+
noise_samples: int,
|
|
185
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
186
|
+
"""Perform the prediction step of the filter."""
|
|
187
|
+
# Compute dynamics covariance
|
|
188
|
+
Qz = compute_dynamics_covariance(
|
|
189
|
+
x_prev, n_features, dynamical_system, koopman_operator, noise_samples
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Predict states
|
|
193
|
+
x_pred = state_dynamics(
|
|
194
|
+
x_prev,
|
|
195
|
+
)
|
|
196
|
+
z_pred = phi(x_pred)
|
|
197
|
+
|
|
198
|
+
# Predict covariance
|
|
199
|
+
Pz_pred = U @ Pz_prev @ U.T + Qz
|
|
200
|
+
|
|
201
|
+
return x_pred, z_pred, Pz_pred
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _update_step(
|
|
205
|
+
x_minus: NDArray[np.float64],
|
|
206
|
+
z_minus: NDArray[np.float64],
|
|
207
|
+
Pz_minus: NDArray[np.float64],
|
|
208
|
+
observation: NDArray[np.float64],
|
|
209
|
+
measurement_model: Callable,
|
|
210
|
+
C: NDArray[np.float64],
|
|
211
|
+
B: NDArray[np.float64],
|
|
212
|
+
dynamical_system: Any,
|
|
213
|
+
noise_samples: int,
|
|
214
|
+
) -> Tuple[
|
|
215
|
+
NDArray[np.float64],
|
|
216
|
+
NDArray[np.float64],
|
|
217
|
+
NDArray[np.float64],
|
|
218
|
+
NDArray[np.float64],
|
|
219
|
+
NDArray[np.float64],
|
|
220
|
+
]:
|
|
221
|
+
"""Perform the update step of the filter."""
|
|
222
|
+
# Compute measurement covariance
|
|
223
|
+
Rz = compute_observation_covariance(x_minus, len(observation), dynamical_system, noise_samples)
|
|
224
|
+
|
|
225
|
+
# Compute innovation
|
|
226
|
+
innovation = observation - measurement_model(x_minus)
|
|
227
|
+
|
|
228
|
+
# Compute innovation covariance
|
|
229
|
+
S = C @ Pz_minus @ C.T + Rz
|
|
230
|
+
|
|
231
|
+
# Compute Kalman gain
|
|
232
|
+
K = Pz_minus @ C.T @ pinv(S)
|
|
233
|
+
|
|
234
|
+
# Update states
|
|
235
|
+
z_plus = z_minus + K @ innovation
|
|
236
|
+
x_plus = B @ z_plus
|
|
237
|
+
|
|
238
|
+
# Update covariance
|
|
239
|
+
Pz_plus = (np.eye(len(z_minus)) - K @ C) @ Pz_minus
|
|
240
|
+
|
|
241
|
+
return x_plus, z_plus, Pz_plus, S, K
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _update_state_covariances(
|
|
245
|
+
Pz_minus: NDArray[np.float64], Pz_plus: NDArray[np.float64], B: NDArray[np.float64]
|
|
246
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
247
|
+
"""Update covariances in the state space."""
|
|
248
|
+
Px_minus = B @ Pz_minus @ B.T
|
|
249
|
+
Px_plus = B @ Pz_plus @ B.T
|
|
250
|
+
return Px_minus, Px_plus
|
kkf-0.3.0/KKF/koopman.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Koopman operator approximation module.
|
|
2
|
+
|
|
3
|
+
Implements kernel-based Extended Dynamic Mode Decomposition (kEDMD)
|
|
4
|
+
for computing Koopman operator approximations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
12
|
+
from sklearn.gaussian_process.kernels import Kernel
|
|
13
|
+
|
|
14
|
+
from .systems import DynamicalSystem, create_additive_system
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KoopmanOperator:
|
|
18
|
+
"""
|
|
19
|
+
Implementation of Koopman operator approximation using kernel-based Extended
|
|
20
|
+
Dynamic Mode Decomposition (kEDMD).
|
|
21
|
+
|
|
22
|
+
This class provides methods to compute finite-dimensional approximations of the
|
|
23
|
+
Koopman operator for nonlinear dynamical systems.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
kernel_function : Callable
|
|
28
|
+
Kernel function for computing feature space mappings.
|
|
29
|
+
dynamical_system : DynamicalSystem
|
|
30
|
+
The underlying dynamical system.
|
|
31
|
+
X : Optional[np.ndarray]
|
|
32
|
+
Dictionary of states used for kernel computations.
|
|
33
|
+
phi : Optional[Callable]
|
|
34
|
+
Feature map function.
|
|
35
|
+
U : Optional[np.ndarray]
|
|
36
|
+
Koopman operator matrix.
|
|
37
|
+
G : Optional[np.ndarray]
|
|
38
|
+
Gram matrix.
|
|
39
|
+
C : Optional[np.ndarray]
|
|
40
|
+
Output matrix.
|
|
41
|
+
B : Optional[np.ndarray]
|
|
42
|
+
State-to-feature space transformation matrix.
|
|
43
|
+
|
|
44
|
+
Notes
|
|
45
|
+
-----
|
|
46
|
+
The Koopman operator framework lifts nonlinear dynamics to a linear setting
|
|
47
|
+
in a higher-dimensional feature space. This implementation uses kernel methods
|
|
48
|
+
to compute the necessary feature spaces and operators.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
kernel_function: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
|
54
|
+
dynamical_system: DynamicalSystem,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.kernel_function = kernel_function
|
|
57
|
+
self.dynamical_system = dynamical_system
|
|
58
|
+
self.X: Optional[NDArray[np.float64]] = None
|
|
59
|
+
self.phi: Optional[Callable] = None
|
|
60
|
+
self.U: Optional[NDArray[np.float64]] = None
|
|
61
|
+
self.G: Optional[NDArray[np.float64]] = None
|
|
62
|
+
self.C: Optional[NDArray[np.float64]] = None
|
|
63
|
+
self.B: Optional[NDArray[np.float64]] = None
|
|
64
|
+
|
|
65
|
+
def compute_edmd(
|
|
66
|
+
self, n_features: int, optimize: bool = True, n_restarts_optimizer: int = 10
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Compute the kernel-based Extended Dynamic Mode Decomposition (kEDMD).
|
|
70
|
+
|
|
71
|
+
This method constructs finite-dimensional approximations of the Koopman
|
|
72
|
+
operator and associated matrices using kernel methods.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
n_features : int
|
|
77
|
+
Number of features to use in the approximation.
|
|
78
|
+
optimize : bool
|
|
79
|
+
Whether to optimize the kernel function. If True, the method will
|
|
80
|
+
optimize the kernel function using Gaussian Process Regression. If False,
|
|
81
|
+
the provided kernel function will be used without optimization. Default is True.
|
|
82
|
+
n_restarts_optimizer : int
|
|
83
|
+
Number of restarts for the optimizer. If optimize is False, will be ignored. Default is 10.
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
-----
|
|
87
|
+
The method performs the following steps:
|
|
88
|
+
1. Generates dictionary points using the state distribution
|
|
89
|
+
2. Constructs the feature map using the kernel function
|
|
90
|
+
3. Computes the Gram matrix and its inverse
|
|
91
|
+
4. Constructs the Koopman operator approximation
|
|
92
|
+
5. Computes output and state transformation matrices
|
|
93
|
+
"""
|
|
94
|
+
# Extract system components
|
|
95
|
+
f, g = self.dynamical_system.f, self.dynamical_system.g
|
|
96
|
+
|
|
97
|
+
# Generate dictionary points
|
|
98
|
+
self.X = self.dynamical_system.sample_state(n_features)
|
|
99
|
+
|
|
100
|
+
# Optimize kernel function
|
|
101
|
+
if optimize:
|
|
102
|
+
self.opt_kernel(X=self.X, n_restarts_optimizer=n_restarts_optimizer)
|
|
103
|
+
|
|
104
|
+
# Define feature map
|
|
105
|
+
self.phi = lambda x: self.kernel_function(x, self.X)[0]
|
|
106
|
+
|
|
107
|
+
# Compute Gram matrix
|
|
108
|
+
self.G = self.kernel_function(self.X, self.X)
|
|
109
|
+
G_inv = np.linalg.inv(self.G)
|
|
110
|
+
|
|
111
|
+
# Compute Koopman operator approximation
|
|
112
|
+
next_states = f(self.X.T).T
|
|
113
|
+
self.U = self.kernel_function(self.X, next_states) @ G_inv
|
|
114
|
+
|
|
115
|
+
# Compute output and state transformation matrices
|
|
116
|
+
self.C = g(self.X.T) @ G_inv
|
|
117
|
+
self.B = self.X.T @ G_inv
|
|
118
|
+
|
|
119
|
+
def get_feature_dimension(self) -> Optional[int]:
|
|
120
|
+
"""
|
|
121
|
+
Get the dimension of the feature space.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
Optional[int]
|
|
126
|
+
Dimension of the feature space, or None if EDMD hasn't been computed.
|
|
127
|
+
"""
|
|
128
|
+
return self.X.shape[0] if self.X is not None else None
|
|
129
|
+
|
|
130
|
+
def opt_kernel(self, X: NDArray[np.float64], n_restarts_optimizer: int = 10) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Optimize the kernel function for the Koopman operator.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
X : np.ndarray
|
|
137
|
+
Set of points.
|
|
138
|
+
n_restarts_optimizer : int
|
|
139
|
+
Number of restarts for the optimizer. Default is 10.
|
|
140
|
+
|
|
141
|
+
Notes
|
|
142
|
+
-----
|
|
143
|
+
This method uses Gaussian Process Regression to optimize the kernel function.
|
|
144
|
+
"""
|
|
145
|
+
# Compute the output of the dynamical system
|
|
146
|
+
y = self.dynamical_system.f(X.T).T
|
|
147
|
+
|
|
148
|
+
# Fit the Gaussian process
|
|
149
|
+
gp = GaussianProcessRegressor(
|
|
150
|
+
kernel=self.kernel_function, n_restarts_optimizer=n_restarts_optimizer
|
|
151
|
+
)
|
|
152
|
+
gp.fit(X.T, y.T)
|
|
153
|
+
|
|
154
|
+
# Update with optimal kernel
|
|
155
|
+
self.kernel_function = gp.kernel_
|