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.
@@ -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))
@@ -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
@@ -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_