kkf 0.11__py3-none-any.whl

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,185 @@
1
+ from typing import Callable, Any, Optional, Union
2
+ import numpy as np
3
+ from numpy.typing import NDArray
4
+ from scipy.stats import rv_continuous
5
+
6
+ class DynamicalSystem:
7
+ """
8
+ A class representing a general dynamical system with state and measurement equations.
9
+
10
+ This class encapsulates the dynamics, measurements, and associated probability
11
+ distributions for both state and measurement noise. Can be considered as:
12
+
13
+ Discrete time:
14
+ Dynamics: x_{k+1} = f(x_{k}) + w_{k}
15
+ Measurements: y_{k} = g(x_{k}) + v_{k}
16
+
17
+ Continous time:
18
+ Dynamics: x'(t) = f(x(t)) + w(t)
19
+ Measurements: y(t) = g(x(t)) + v(t)
20
+
21
+ Attributes
22
+ ----------
23
+ nx : int
24
+ Dimension of the state space.
25
+ ny : int
26
+ Dimension of the measurement/output space.
27
+ f : Callable
28
+ State transition function (dynamics).
29
+ g : Callable
30
+ Measurement/output function.
31
+ dist_X : rv_continuous
32
+ Probability distribution for initial states.
33
+ dist_dyn : rv_continuous
34
+ Probability distribution for dynamics noise.
35
+ dist_obs : rv_continuous
36
+ Probability distribution for measurement noise.
37
+ discrete_time : bool
38
+ Indicates if the system is in discrete time, if False the system is in continous time.
39
+
40
+ Notes
41
+ -----
42
+ The functions f and g should have the following signatures:
43
+ - f(x: ndarray, w: ndarray) -> ndarray
44
+ - g(x: ndarray, v: ndarray) -> ndarray
45
+ where:
46
+ - x is the state vector
47
+ - w is the process noise
48
+ - v is the measurement noise
49
+ """
50
+
51
+ def __init__(
52
+ self,
53
+ nx: int,
54
+ ny: int,
55
+ f: Callable[[NDArray[np.float64]], NDArray[np.float64]],
56
+ g: Callable[[NDArray[np.float64]], NDArray[np.float64]],
57
+ dist_X: rv_continuous,
58
+ dist_dyn: rv_continuous,
59
+ dist_obs: rv_continuous,
60
+ discrete_time: bool
61
+ ):
62
+ self.nx = nx
63
+ self.ny = ny
64
+ self.f = f
65
+ self.g = g
66
+ self.dist_X = dist_X
67
+ self.dist_dyn = dist_dyn
68
+ self.dist_obs = dist_obs
69
+ self.discrete_time = discrete_time
70
+
71
+ def dynamics(self, x: NDArray[np.float64], w: NDArray[np.float64]) -> NDArray[np.float64]:
72
+ """
73
+ Apply the state transition function.
74
+
75
+ Parameters
76
+ ----------
77
+ x : np.ndarray
78
+ Current state vector.
79
+ w : np.ndarray
80
+ Process noise vector.
81
+
82
+ Returns
83
+ -------
84
+ np.ndarray
85
+ Next state vector.
86
+ """
87
+ return self.f(x) + w
88
+
89
+ def measurements(self, x: NDArray[np.float64], v: NDArray[np.float64]) -> NDArray[np.float64]:
90
+ """
91
+ Apply the measurement function.
92
+
93
+ Parameters
94
+ ----------
95
+ x : np.ndarray
96
+ Current state vector.
97
+ v : np.ndarray
98
+ Measurement noise vector.
99
+
100
+ Returns
101
+ -------
102
+ np.ndarray
103
+ Measurement/output vector.
104
+ """
105
+ return self.g(x) + v
106
+
107
+ def sample_state(self, size: int = 1) -> NDArray[np.float64]:
108
+ """
109
+ Sample from the state distribution.
110
+
111
+ Parameters
112
+ ----------
113
+ size : int, optional
114
+ Number of samples to draw. Default is 1.
115
+
116
+ Returns
117
+ -------
118
+ np.ndarray
119
+ Sampled state(s).
120
+ """
121
+ return self.dist_X.rvs(size=size).reshape((size, self.nx))
122
+
123
+ def create_additive_system(
124
+ nx: int,
125
+ ny: int,
126
+ f: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
127
+ g: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
128
+ dist_X: rv_continuous,
129
+ dist_dyn: rv_continuous,
130
+ dist_obs: rv_continuous,
131
+ N_samples: int,
132
+ discrete_time: bool = True
133
+ ) -> DynamicalSystem:
134
+ """
135
+ Create an additive dynamical system where noise is added to the state and observation functions.
136
+
137
+ Parameters
138
+ ----------
139
+ nx : int
140
+ Dimension of the state space.
141
+ ny : int
142
+ Dimension of the measurement space.
143
+ f : Callable
144
+ State transition function (with noise).
145
+ g : Callable
146
+ Measurement function (with noise).
147
+ dist_X : rv_continuous
148
+ Initial state distribution.
149
+ dist_dyn : rv_continuous
150
+ Dynamics noise distribution.
151
+ dist_obs : rv_continuous
152
+ Measurement noise distribution.
153
+ N_samples: int
154
+ Number of samples to generate empirical mean of dynamics and observation.
155
+ discrete_time : bool, optional
156
+ Indicates if the system is in discrete time. Default is True.
157
+
158
+ Returns
159
+ -------
160
+ DynamicalSystem
161
+ New dynamical system instance with additive noise.
162
+
163
+ Notes
164
+ -----
165
+ The resulting system has the form:
166
+ x[k+1] = f(x[k]) + w[k]
167
+ y[k] = g(x[k]) + v[k]
168
+ where w and v are noise terms.
169
+ """
170
+ new_f = lambda x: np.mean([f(x.reshape((len(x),1)), dist_dyn.rvs(N_samples))], axis=1)
171
+ new_g = lambda x: np.mean([g(x.reshape((len(x),1)), dist_obs.rvs(N_samples))], axis=1)
172
+ class DynDist:
173
+ def __init__(self):
174
+ pass
175
+
176
+ def rvs(self, x, size=1):
177
+ return f(x, dist_dyn.rvs(size=size)) - new_f(x)
178
+
179
+ class ObsDist:
180
+ def __init__(self):
181
+ pass
182
+
183
+ def rvs(self, x, size=1):
184
+ return g(x, dist_obs.rvs(size=size)) - new_g(x)
185
+ return DynamicalSystem(nx, ny, new_f, new_g, dist_X, DynDist, ObsDist, discrete_time=discrete_time)
kkf/KKFsol.py ADDED
@@ -0,0 +1,145 @@
1
+ from dataclasses import dataclass
2
+ import numpy as np
3
+ from numpy.typing import NDArray
4
+ from typing import Optional
5
+
6
+ import numpy as np
7
+ from numpy.linalg import pinv, cholesky
8
+ from typing import Any, Tuple, Optional, Callable
9
+ from numpy.typing import NDArray
10
+
11
+ from .covariances import compute_initial_covariance, compute_dynamics_covariance, compute_observation_covariance
12
+
13
+ @dataclass
14
+ class KoopmanKalmanFilterSolution:
15
+ """
16
+ A class to store the solution of a Koopman-Kalman Filter iteration.
17
+
18
+ This class maintains the state estimates, covariance matrices, and filter gains
19
+ for both the prior (minus) and posterior (plus) estimates in both state and
20
+ feature spaces.
21
+
22
+ Attributes
23
+ ----------
24
+ x_plus : np.ndarray
25
+ Posterior state estimate after measurement update.
26
+ x_minus : np.ndarray
27
+ Prior state estimate from prediction step.
28
+ Pz_plus : np.ndarray
29
+ Posterior covariance matrix in feature space after measurement update.
30
+ Pz_minus : np.ndarray
31
+ Prior covariance matrix in feature space from prediction step.
32
+ Px_plus : np.ndarray
33
+ Posterior covariance matrix in state space after measurement update.
34
+ Px_minus : np.ndarray
35
+ Prior covariance matrix in state space from prediction step.
36
+ S : np.ndarray
37
+ Innovation (residual) covariance matrix.
38
+ K : np.ndarray
39
+ Kalman gain matrix.
40
+
41
+ Notes
42
+ -----
43
+ The class uses the common Kalman filter notation where:
44
+ - (-) denotes prior estimates before measurement update
45
+ - (+) denotes posterior estimates after measurement update
46
+ - Pz refers to covariances in the feature/transformed space
47
+ - Px refers to covariances in the original state space
48
+
49
+ Examples
50
+ --------
51
+ >>> solution = KoopmanKalmanFilterSolution(
52
+ ... x_plus=np.array([1.0, 2.0]),
53
+ ... x_minus=np.array([0.9, 1.9]),
54
+ ... Pz_plus=np.eye(2),
55
+ ... Pz_minus=np.eye(2) * 1.1,
56
+ ... Px_plus=np.eye(2) * 0.9,
57
+ ... Px_minus=np.eye(2),
58
+ ... S=np.eye(2) * 0.5,
59
+ ... K=np.array([[0.1, 0], [0, 0.1]])
60
+ ... )
61
+ """
62
+
63
+ x_plus: NDArray[np.float64]
64
+ x_minus: NDArray[np.float64]
65
+ Pz_plus: NDArray[np.float64]
66
+ Pz_minus: NDArray[np.float64]
67
+ Px_plus: NDArray[np.float64]
68
+ Px_minus: NDArray[np.float64]
69
+ S: NDArray[np.float64]
70
+ K: NDArray[np.float64]
71
+
72
+ def __post_init__(self):
73
+ """Validate the dimensions of the input arrays."""
74
+ # Ensure all inputs are numpy arrays
75
+ for attr in ['x_plus', 'x_minus', 'Pz_plus', 'Pz_minus',
76
+ 'Px_plus', 'Px_minus', 'S', 'K']:
77
+ value = getattr(self, attr)
78
+ if not isinstance(value, np.ndarray):
79
+ setattr(self, attr, np.array(value))
80
+
81
+ def get_state_dimension(self) -> int:
82
+ """
83
+ Get the dimension of the state vector.
84
+
85
+ Returns
86
+ -------
87
+ int
88
+ The dimension of the state vector.
89
+ """
90
+ return len(self.x_plus)
91
+
92
+ def get_feature_dimension(self) -> int:
93
+ """
94
+ Get the dimension of the feature space.
95
+
96
+ Returns
97
+ -------
98
+ int
99
+ The dimension of the feature space.
100
+ """
101
+ return self.Pz_plus.shape[0]
102
+
103
+ def get_estimation_error(self) -> NDArray[np.float64]:
104
+ """
105
+ Calculate the difference between prior and posterior estimates.
106
+
107
+ Returns
108
+ -------
109
+ np.ndarray
110
+ The difference between posterior and prior state estimates.
111
+ """
112
+ return self.x_plus - self.x_minus
113
+
114
+ def get_trace_reduction(self) -> float:
115
+ """
116
+ Calculate the reduction in uncertainty as measured by trace of covariance.
117
+
118
+ Returns
119
+ -------
120
+ float
121
+ The relative reduction in trace of the state covariance matrix.
122
+ """
123
+ trace_minus = np.trace(self.Px_minus)
124
+ trace_plus = np.trace(self.Px_plus)
125
+ return (trace_minus - trace_plus) / trace_minus if trace_minus != 0 else 0.0
126
+
127
+ def to_dict(self) -> dict:
128
+ """
129
+ Convert the solution to a dictionary format.
130
+
131
+ Returns
132
+ -------
133
+ dict
134
+ Dictionary containing all solution components.
135
+ """
136
+ return {
137
+ 'x_plus': self.x_plus,
138
+ 'x_minus': self.x_minus,
139
+ 'Pz_plus': self.Pz_plus,
140
+ 'Pz_minus': self.Pz_minus,
141
+ 'Px_plus': self.Px_plus,
142
+ 'Px_minus': self.Px_minus,
143
+ 'S': self.S,
144
+ 'K': self.K
145
+ }
kkf/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ # __init__.py
2
+ from .covariances import compute_initial_covariance, compute_dynamics_covariance, compute_observation_covariance
3
+ from .DynamicalSystems import DynamicalSystem, create_additive_system
4
+ from .kEDMD import KoopmanOperator
5
+ from .KKFsol import KoopmanKalmanFilterSolution
kkf/applyKKF.py ADDED
@@ -0,0 +1,228 @@
1
+ from dataclasses import dataclass
2
+ import numpy as np
3
+ from numpy.linalg import pinv, cholesky
4
+ from numpy.typing import NDArray
5
+
6
+ from typing import Any, Tuple, Optional, Callable
7
+
8
+ from .covariances import compute_initial_covariance, compute_dynamics_covariance, compute_observation_covariance
9
+ from .KKFsol import KoopmanKalmanFilterSolution
10
+
11
+ def apply_koopman_kalman_filter(
12
+ koopman_operator: Any,
13
+ observations: NDArray[np.float64],
14
+ initial_distribution: Any,
15
+ n_features: int,
16
+ optimize: bool = True,
17
+ n_restarts_optimizer: int = 10,
18
+ noise_samples: int = 100
19
+ ) -> 'KoopmanKalmanFilterSolution':
20
+ """
21
+ Implementation of the Koopman-Kalman Filter algorithm.
22
+
23
+ This function combines Koopman operator theory with Kalman filtering to perform
24
+ state estimation for nonlinear dynamical systems. It operates by lifting the
25
+ state space to a higher-dimensional feature space where the dynamics are
26
+ approximately linear.
27
+
28
+ Parameters
29
+ ----------
30
+ koopman_operator : object
31
+ Object containing Koopman operator methods and attributes:
32
+ - kEDMD(n_features): Extended Dynamic Mode Decomposition method
33
+ - dyn_sys: Dynamical system object
34
+ - U, B, C: Matrices for Koopman approximation
35
+ - phi: Feature map function
36
+ observations : np.ndarray
37
+ Array of shape (n_timesteps, n_outputs) containing system measurements.
38
+ initial_distribution : object
39
+ Distribution object for initial state with methods:
40
+ - mean: Returns mean of initial state
41
+ - rvs: Random sampling method
42
+ n_features : int
43
+ Number of features in the lifted space.
44
+ optimize : bool, optional
45
+ Whether to optimize the kernel parameters. Default is True.
46
+ n_restarts_optimizer : int, optional
47
+ Number of restarts for kernel optimization. Default is 10.
48
+ noise_samples : int, optional
49
+ Number of samples for noise covariance estimation. Default is 100.
50
+
51
+ Returns
52
+ -------
53
+ KoopmanKalmanFilterSolution
54
+ Object containing all filter estimates and covariances.
55
+
56
+ Notes
57
+ -----
58
+ The algorithm consists of three main phases:
59
+ 1. Initialization:
60
+ - Sets up initial states and covariances
61
+ - Computes Koopman approximation
62
+
63
+ 2. Prediction Step:
64
+ - Projects state forward using system dynamics
65
+ - Updates covariances using Koopman operator
66
+
67
+ 3. Update Step:
68
+ - Incorporates new measurements
69
+ - Updates state and covariance estimates
70
+
71
+ The filter operates in both the original state space (x) and the lifted
72
+ feature space (z), maintaining estimates and covariances in both spaces.
73
+ """
74
+ # Compute Koopman approximation
75
+ koopman_operator.compute_edmd(n_features, optimize, n_restarts_optimizer)
76
+ dynamical_system = koopman_operator.dynamical_system
77
+
78
+ # Extract system and Koopman components
79
+ state_dynamics = dynamical_system.f
80
+ measurement_model = dynamical_system.g
81
+ U, B, C = koopman_operator.U, koopman_operator.B, koopman_operator.C
82
+ phi = koopman_operator.phi
83
+
84
+ ### Initialization ###
85
+ x0 = initial_distribution.mean # Initial state estimate
86
+ z0 = phi(x0) # Initial state in feature space
87
+
88
+ # Get problem dimensions
89
+ n_timesteps, n_states = observations.shape[0], len(x0)
90
+ n_outputs = observations.shape[1]
91
+
92
+ # Initialize state arrays
93
+ x_minus, x_plus = _initialize_state_arrays(n_timesteps, n_states)
94
+ z_minus, z_plus = _initialize_state_arrays(n_timesteps, n_features)
95
+
96
+ # Initialize covariance arrays
97
+ Px_minus, Px_plus = _initialize_covariance_arrays(n_timesteps, n_states)
98
+ Pz_minus, Pz_plus = _initialize_covariance_arrays(n_timesteps, n_features)
99
+
100
+ # Initialize filter matrices
101
+ S = np.zeros((n_timesteps, n_outputs, n_outputs)) # Innovation covariance
102
+ K = np.zeros((n_timesteps, n_features, n_outputs)) # Kalman gain
103
+
104
+ # Set initial conditions
105
+ x_minus[0, :], x_plus[0, :] = x0, x0
106
+ z_minus[0, :], z_plus[0, :] = z0, z0
107
+
108
+ # Initialize covariances
109
+ initial_covariance = compute_initial_covariance(
110
+ x_minus[0, :], n_features, initial_distribution,
111
+ koopman_operator, noise_samples
112
+ )
113
+ Pz_minus[0, :, :], Pz_plus[0, :, :] = initial_covariance, initial_covariance
114
+
115
+ ### Main Filter Loop ###
116
+ for t in range(1, n_timesteps):
117
+ # Prediction Step
118
+ x_minus[t], z_minus[t], Pz_minus[t] = _prediction_step(
119
+ x_plus[t-1], z_plus[t-1], Pz_plus[t-1],
120
+ state_dynamics, phi, U, dynamical_system,
121
+ koopman_operator, n_features, noise_samples
122
+ )
123
+
124
+ # Update Step
125
+ x_plus[t], z_plus[t], Pz_plus[t], S[t], K[t] = _update_step(
126
+ x_minus[t], z_minus[t], Pz_minus[t],
127
+ observations[t], measurement_model,
128
+ C, B, dynamical_system, noise_samples
129
+ )
130
+
131
+ # Update state-space covariances
132
+ Px_minus[t], Px_plus[t] = _update_state_covariances(
133
+ Pz_minus[t], Pz_plus[t], B
134
+ )
135
+
136
+ return KoopmanKalmanFilterSolution(
137
+ x_plus, x_minus, Pz_plus, Pz_minus, Px_plus, Px_minus, S, K
138
+ )
139
+
140
+ def _initialize_state_arrays(
141
+ n_timesteps: int,
142
+ n_dim: int
143
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
144
+ """Initialize arrays for state estimates."""
145
+ return (np.zeros((n_timesteps, n_dim)),
146
+ np.zeros((n_timesteps, n_dim)))
147
+
148
+ def _initialize_covariance_arrays(
149
+ n_timesteps: int,
150
+ n_dim: int
151
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
152
+ """Initialize arrays for covariance matrices."""
153
+ return (np.zeros((n_timesteps, n_dim, n_dim)),
154
+ np.zeros((n_timesteps, n_dim, n_dim)))
155
+
156
+ def _prediction_step(
157
+ x_prev: NDArray[np.float64],
158
+ z_prev: NDArray[np.float64],
159
+ Pz_prev: NDArray[np.float64],
160
+ state_dynamics: Callable,
161
+ phi: Callable,
162
+ U: NDArray[np.float64],
163
+ dynamical_system: Any,
164
+ koopman_operator: Any,
165
+ n_features: int,
166
+ noise_samples: int
167
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
168
+ """Perform the prediction step of the filter."""
169
+ # Compute dynamics covariance
170
+ Qz = compute_dynamics_covariance(
171
+ x_prev, n_features, dynamical_system,
172
+ koopman_operator, noise_samples
173
+ )
174
+
175
+ # Predict states
176
+ x_pred = state_dynamics(x_prev, )
177
+ z_pred = phi(x_pred)
178
+
179
+ # Predict covariance
180
+ Pz_pred = U @ Pz_prev @ U.T + Qz
181
+
182
+ return x_pred, z_pred, Pz_pred
183
+
184
+ def _update_step(
185
+ x_minus: NDArray[np.float64],
186
+ z_minus: NDArray[np.float64],
187
+ Pz_minus: NDArray[np.float64],
188
+ observation: NDArray[np.float64],
189
+ measurement_model: Callable,
190
+ C: NDArray[np.float64],
191
+ B: NDArray[np.float64],
192
+ dynamical_system: Any,
193
+ noise_samples: int
194
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64],
195
+ NDArray[np.float64], NDArray[np.float64]]:
196
+ """Perform the update step of the filter."""
197
+ # Compute measurement covariance
198
+ Rz = compute_observation_covariance(
199
+ x_minus, len(observation), dynamical_system, noise_samples
200
+ )
201
+
202
+ # Compute innovation
203
+ innovation = observation - measurement_model(x_minus)
204
+
205
+ # Compute innovation covariance
206
+ S = C @ Pz_minus @ C.T + Rz
207
+
208
+ # Compute Kalman gain
209
+ K = Pz_minus @ C.T @ pinv(cholesky(S))
210
+
211
+ # Update states
212
+ z_plus = z_minus + K @ innovation
213
+ x_plus = B @ z_plus
214
+
215
+ # Update covariance
216
+ Pz_plus = (np.eye(len(z_minus)) - K @ C) @ Pz_minus
217
+
218
+ return x_plus, z_plus, Pz_plus, S, K
219
+
220
+ def _update_state_covariances(
221
+ Pz_minus: NDArray[np.float64],
222
+ Pz_plus: NDArray[np.float64],
223
+ B: NDArray[np.float64]
224
+ ) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
225
+ """Update covariances in the state space."""
226
+ Px_minus = B @ Pz_minus @ B.T
227
+ Px_plus = B @ Pz_plus @ B.T
228
+ return Px_minus, Px_plus
kkf/covariances.py ADDED
@@ -0,0 +1,162 @@
1
+ import numpy as np
2
+ from typing import Callable, Any, Union
3
+ from scipy.stats import rv_continuous
4
+ from numpy.typing import NDArray
5
+
6
+ def compute_initial_covariance(
7
+ x: NDArray[np.float64],
8
+ n_features: int,
9
+ initial_distribution: rv_continuous,
10
+ koopman_operator: Any,
11
+ n_samples: int
12
+ ) -> NDArray[np.float64]:
13
+ """
14
+ Compute the covariance matrix for the initial distribution in feature space.
15
+
16
+ This function samples from an initial distribution and computes the covariance
17
+ matrix of the transformed samples using a Koopman operator's feature map.
18
+
19
+ Parameters
20
+ ----------
21
+ x : np.ndarray
22
+ Current state vector or reference point.
23
+ n_features : int
24
+ Number of features in the transformed space.
25
+ initial_distribution : scipy.stats.rv_continuous
26
+ Initial probability distribution to sample from.
27
+ koopman_operator : object
28
+ Object containing the feature map phi method for state transformation.
29
+ Must have a method phi(x) that maps states to feature space.
30
+ n_samples : int
31
+ Number of samples to use for covariance estimation.
32
+
33
+ Returns
34
+ -------
35
+ np.ndarray
36
+ Covariance matrix of size (n_features, n_features) in the transformed space.
37
+
38
+ Notes
39
+ -----
40
+ The function performs the following steps:
41
+ 1. Samples from the initial distribution
42
+ 2. Applies the Koopman operator's feature map to each sample
43
+ 3. Computes the covariance matrix of the transformed samples
44
+ """
45
+ # Sample from initial distribution
46
+ samples = initial_distribution.rvs(size=n_samples).reshape((n_samples, len(x)))
47
+
48
+ # Initialize array for transformed samples
49
+ transformed_samples = np.zeros((n_samples, n_features))
50
+
51
+ # Transform each sample using the feature map
52
+ for i in range(n_samples):
53
+ transformed_samples[i, :] = koopman_operator.phi(samples[i,:])
54
+
55
+ # Compute and return covariance matrix
56
+ return np.cov(transformed_samples, rowvar=False)
57
+
58
+ def compute_dynamics_covariance(
59
+ x: NDArray[np.float64],
60
+ n_features: int,
61
+ dynamics: Any,
62
+ koopman_operator: Any,
63
+ n_samples: int
64
+ ) -> NDArray[np.float64]:
65
+ """
66
+ Compute the covariance matrix for the system dynamics in feature space.
67
+
68
+ This function samples from the dynamics distribution, applies the system
69
+ dynamics, and computes the covariance matrix of the transformed results.
70
+
71
+ Parameters
72
+ ----------
73
+ x : np.ndarray
74
+ Current state vector or reference point.
75
+ n_features : int
76
+ Number of features in the transformed space.
77
+ dynamics : object
78
+ Dynamical system object containing:
79
+ - dist_dyn: scipy.stats.rv_continuous distribution for dynamics noise
80
+ - dynamics(x, w): method implementing the system dynamics
81
+ koopman_operator : object
82
+ Object containing the feature map phi method for state transformation.
83
+ Must have a method phi(x) that maps states to feature space.
84
+ n_samples : int
85
+ Number of samples to use for covariance estimation.
86
+
87
+ Returns
88
+ -------
89
+ np.ndarray
90
+ Covariance matrix of size (n_features, n_features) in the transformed space.
91
+
92
+ Notes
93
+ -----
94
+ The function performs the following steps:
95
+ 1. Samples from the dynamics noise distribution
96
+ 2. Applies the system dynamics to each sample
97
+ 3. Transforms the results using the Koopman operator's feature map
98
+ 4. Computes the covariance matrix of the transformed samples
99
+ """
100
+ # Sample from dynamics distribution
101
+ noise_samples = dynamics.dist_dyn.rvs(size=n_samples).reshape((n_samples, len(x)))
102
+
103
+ # Initialize array for transformed samples
104
+ transformed_samples = np.zeros((n_samples, n_features))
105
+
106
+ # Apply dynamics and transform each sample
107
+ for i in range(n_samples):
108
+ state_evolution = dynamics.dynamics(x, noise_samples[i,:])
109
+ transformed_samples[i, :] = koopman_operator.phi(state_evolution)
110
+
111
+ # Compute and return covariance matrix
112
+ return np.cov(transformed_samples, rowvar=False)
113
+
114
+ def compute_observation_covariance(
115
+ x: NDArray[np.float64],
116
+ n_outputs: int,
117
+ dynamics: Any,
118
+ n_samples: int
119
+ ) -> NDArray[np.float64]:
120
+ """
121
+ Compute the covariance matrix for the observation/measurement process.
122
+
123
+ This function samples from the measurement noise distribution and computes
124
+ the covariance matrix of the measurement process.
125
+
126
+ Parameters
127
+ ----------
128
+ x : np.ndarray
129
+ Current state vector or reference point.
130
+ n_outputs : int
131
+ Number of measurement outputs.
132
+ dynamics : object
133
+ Dynamical system object containing:
134
+ - dist_obs: scipy.stats.rv_continuous distribution for measurement noise
135
+ - measurements(x, w): method implementing the measurement process
136
+ n_samples : int
137
+ Number of samples to use for covariance estimation.
138
+
139
+ Returns
140
+ -------
141
+ np.ndarray
142
+ Covariance matrix of size (n_outputs, n_outputs) for the measurement process.
143
+
144
+ Notes
145
+ -----
146
+ The function performs the following steps:
147
+ 1. Samples from the measurement noise distribution
148
+ 2. Applies the measurement function to each sample
149
+ 3. Computes the covariance matrix of the measurements
150
+ """
151
+ # Sample from measurement distribution
152
+ noise_samples = dynamics.dist_obs.rvs(size=n_samples).reshape((n_samples, n_outputs))
153
+
154
+ # Initialize array for measurements
155
+ measurement_samples = np.zeros((n_samples, n_outputs))
156
+
157
+ # Apply measurement function to each sample
158
+ for i in range(n_samples):
159
+ measurement_samples[i, :] = dynamics.measurements(x, noise_samples[i,:])
160
+
161
+ # Compute and return covariance matrix
162
+ return np.cov(measurement_samples, rowvar=False)
kkf/kEDMD.py ADDED
@@ -0,0 +1,147 @@
1
+ from typing import Callable, Any, Optional, Union
2
+ import numpy as np
3
+ from numpy.typing import NDArray
4
+ from scipy.stats import rv_continuous
5
+ from sklearn.gaussian_process.kernels import Kernel
6
+ from sklearn.gaussian_process import GaussianProcessRegressor
7
+ from .DynamicalSystems import DynamicalSystem, create_additive_system
8
+
9
+ class KoopmanOperator:
10
+ """
11
+ Implementation of Koopman operator approximation using kernel-based Extended
12
+ Dynamic Mode Decomposition (kEDMD).
13
+
14
+ This class provides methods to compute finite-dimensional approximations of the
15
+ Koopman operator for nonlinear dynamical systems.
16
+
17
+ Attributes
18
+ ----------
19
+ kernel_function : Callable
20
+ Kernel function for computing feature space mappings.
21
+ dynamical_system : DynamicalSystem
22
+ The underlying dynamical system.
23
+ X : Optional[np.ndarray]
24
+ Dictionary of states used for kernel computations.
25
+ phi : Optional[Callable]
26
+ Feature map function.
27
+ U : Optional[np.ndarray]
28
+ Koopman operator matrix.
29
+ G : Optional[np.ndarray]
30
+ Gram matrix.
31
+ C : Optional[np.ndarray]
32
+ Output matrix.
33
+ B : Optional[np.ndarray]
34
+ State-to-feature space transformation matrix.
35
+
36
+ Notes
37
+ -----
38
+ The Koopman operator framework lifts nonlinear dynamics to a linear setting
39
+ in a higher-dimensional feature space. This implementation uses kernel methods
40
+ to compute the necessary feature spaces and operators.
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ kernel_function: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
46
+ dynamical_system: DynamicalSystem
47
+ ):
48
+ self.kernel_function = kernel_function
49
+ self.dynamical_system = dynamical_system
50
+ self.X: Optional[NDArray[np.float64]] = None
51
+ self.phi: Optional[Callable] = None
52
+ self.U: Optional[NDArray[np.float64]] = None
53
+ self.G: Optional[NDArray[np.float64]] = None
54
+ self.C: Optional[NDArray[np.float64]] = None
55
+ self.B: Optional[NDArray[np.float64]] = None
56
+
57
+ def compute_edmd(self, n_features: int, optimize: bool = True, n_restarts_optimizer: int = 10) -> None:
58
+ """
59
+ Compute the kernel-based Extended Dynamic Mode Decomposition (kEDMD).
60
+
61
+ This method constructs finite-dimensional approximations of the Koopman
62
+ operator and associated matrices using kernel methods.
63
+
64
+ Parameters
65
+ ----------
66
+ n_features : int
67
+ Number of features to use in the approximation.
68
+
69
+ optimize : bool
70
+ Whether to optimize the kernel function. If True, the method will
71
+ optimize the kernel function using Gaussian Process Regression. If False,
72
+ the provided kernel function will be used without optimization. Default is True.
73
+
74
+ n_restarts_optimizer : int
75
+ Number of restarts for the optimizer. If optimize is False, will be ignored. Default is 10.
76
+
77
+ Notes
78
+ -----
79
+ The method performs the following steps:
80
+ 1. Generates dictionary points using the state distribution
81
+ 2. Constructs the feature map using the kernel function
82
+ 3. Computes the Gram matrix and its inverse
83
+ 4. Constructs the Koopman operator approximation
84
+ 5. Computes output and state transformation matrices
85
+ """
86
+ # Extract system components
87
+ f, g = self.dynamical_system.f, self.dynamical_system.g
88
+
89
+ # Generate dictionary points
90
+ self.X = self.dynamical_system.sample_state(n_features)
91
+
92
+ # Optimize kernel function
93
+ if optimize:
94
+ self.opt_kernel(X=self.X, n_restarts_optimizer=n_restarts_optimizer)
95
+
96
+ # Define feature map
97
+ self.phi = lambda x: self.kernel_function(x, self.X)[0]
98
+
99
+ # Compute Gram matrix
100
+ self.G = self.kernel_function(self.X, self.X)
101
+ G_inv = np.linalg.inv(self.G)
102
+
103
+ # Compute Koopman operator approximation
104
+ next_states = f(self.X.T).T
105
+ self.U = self.kernel_function(self.X, next_states) @ G_inv
106
+
107
+ # Compute output and state transformation matrices
108
+ self.C = g(self.X.T) @ G_inv
109
+ self.B = self.X.T @ G_inv
110
+
111
+ def get_feature_dimension(self) -> Optional[int]:
112
+ """
113
+ Get the dimension of the feature space.
114
+
115
+ Returns
116
+ -------
117
+ Optional[int]
118
+ Dimension of the feature space, or None if EDMD hasn't been computed.
119
+ """
120
+ return self.X.shape[0] if self.X is not None else None
121
+
122
+ def opt_kernel(self, X: NDArray[np.float64], n_restarts_optimizer: int = 10) -> None:
123
+ """
124
+ Optimal kernel function for the Koopman operator.
125
+
126
+ Parameters
127
+ ----------
128
+ X : np.ndarray
129
+ Set of points.
130
+
131
+ n_restarts_optimizer : int
132
+ Number of restarts for the optimizer. Default is 10.
133
+
134
+ Notes
135
+ -----
136
+ This method uses Gaussian Process Regression to optimize the kernel function.
137
+ """
138
+ # Compute the output of the dynamical system
139
+ y = self.dynamical_system.f(X.T).T
140
+
141
+ # Fit the Gaussian process
142
+ gp = GaussianProcessRegressor(kernel=self.kernel_function, n_restarts_optimizer=n_restarts_optimizer)
143
+ gp.fit(X.T, y.T)
144
+
145
+ # Return the optimal kernel
146
+ self.kernel_function = gp.kernel_
147
+
kkf/utils.py ADDED
File without changes
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 diegoolguinw
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,203 @@
1
+ Metadata-Version: 2.1
2
+ Name: kkf
3
+ Version: 0.11
4
+ Summary: kkf: a library for Python implementation of Kernel-Koopman-Kalman Filter.
5
+ Home-page: https://github.com/diegoolguinw/kkf
6
+ Author: Diego Olguin-Wende
7
+ Author-email: dolguin@dim.uchile.cl
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.6
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy
15
+ Requires-Dist: scipy
16
+ Requires-Dist: scikit-learn
17
+
18
+ # Kernel Koopman Kalman Filter
19
+
20
+ KKKF is a Python library that implements kernel Extended Dynamic Mode Decomposition (EDMD) of Koopman operators and provides a non-linear variant of the Kalman Filter. This library is particularly useful for state estimation in dynamical systems with non-linear behavior.
21
+
22
+ ## Installation
23
+
24
+ You can install KKKF using pip:
25
+
26
+ ```bash
27
+ pip install KKKF
28
+ ```
29
+
30
+ ## Features
31
+
32
+ - Kernel-based Extended Dynamic Mode Decomposition (EDMD)
33
+ - Non-linear Kalman Filter implementation
34
+ - Support for general dynamical systems
35
+ - Integration with various kernel functions (e.g., Matérn kernel)
36
+ - Robust state estimation with noise handling
37
+
38
+ ## Dependencies
39
+
40
+ - NumPy
41
+ - SciPy
42
+ - scikit-learn (for kernel functions)
43
+ - Matplotlib (for visualization)
44
+
45
+ ## Quick Start
46
+
47
+ Here's a complete example of using KKKF to estimate and visualize states in a SIR (Susceptible-Infected-Recovered) model:
48
+
49
+ ```python
50
+ # Dependencies
51
+ import numpy as np
52
+ import matplotlib.pyplot as plt
53
+ from scipy import stats
54
+ from sklearn.gaussian_process.kernels import Matern
55
+ from KKKF.DynamicalSystems import DynamicalSystem
56
+ from KKKF.kEDMD import KoopmanOperator
57
+ from KKKF.applyKKKF import apply_koopman_kalman_filter
58
+
59
+ # Define system parameters
60
+ beta, gamma = 0.12, 0.04
61
+
62
+ # Define system dynamics
63
+ def f(x):
64
+ return x + np.array([-beta*x[0]*x[1], beta*x[0]*x[1] - gamma*x[1], gamma*x[1]])
65
+
66
+ # Define system observations
67
+ def g(x):
68
+ return np.array([x[1]])
69
+
70
+ # Setup system dimensions and kernel
71
+ N = 300
72
+ nx, ny = 3, 1
73
+ k = Matern(length_scale=N**(-1/nx), nu=0.5)
74
+
75
+ # Setup distributions
76
+ X_dist = stats.dirichlet(alpha=np.ones(nx))
77
+ dyn_dist = stats.multivariate_normal(mean=np.zeros(nx), cov=1e-5*np.eye(3))
78
+ obs_dist = stats.multivariate_normal(mean=np.zeros(ny), cov=1e-3*np.eye(1))
79
+
80
+ # Create dynamical system
81
+ dyn = DynamicalSystem(nx, ny, f, g, X_dist, dyn_dist, obs_dist)
82
+
83
+ # Generate synthetic data
84
+ iters = 100
85
+ x0 = np.array([0.9, 0.1, 0.0])
86
+ x = np.zeros((iters, nx))
87
+ y = np.zeros((iters, ny))
88
+
89
+ x[0] = x0
90
+ y[0] = g(x[0]) + obs_dist.rvs()
91
+
92
+ for i in range(1, iters):
93
+ x[i] = f(x[i-1]) + dyn.dist_dyn.rvs()
94
+ y[i] = g(x[i]) + obs_dist.rvs()
95
+
96
+ # Initialize and apply Koopman Kalman Filter
97
+
98
+ # Prior for the initial condition
99
+ x0_prior = np.array([0.8, 0.15, 0.05])
100
+ d0 = stats.multivariate_normal(mean=x0_prior, cov=0.1*np.eye(3))
101
+
102
+ # Koopman operator
103
+ Koop = KoopmanOperator(k, dyn)
104
+
105
+ # Solution
106
+ sol = apply_koopman_kalman_filter(Koop, y, d0, N, noise_samples=100)
107
+
108
+ # Visualization with confidence intervals
109
+ conf = np.zeros((iters, nx))
110
+ for i in range(iters):
111
+ conf[i, :] = np.sqrt(np.diag(sol.Px_plus[i,:,:]))
112
+
113
+ # 95% confidence interval
114
+ err1 = sol.x_plus - 1.96*conf
115
+ err2 = sol.x_plus + 1.96*conf
116
+
117
+ # Plot elements
118
+ labels = ["S (True)", "I (True)", "R (True)"]
119
+ colors = ["blue", "red", "green"]
120
+
121
+ plt.plot(sol.x_plus, label=["S (KKF)", "I (KKF)", "R (KKF)"])
122
+
123
+ for i in range(nx):
124
+ plt.fill_between(np.arange(iters), err1[:,i], err2[:,i], alpha=0.6)
125
+ plt.scatter(np.arange(iters), x[:,i], label=labels[i], color=colors[i], s=1.4)
126
+
127
+ plt.xlabel("Days")
128
+ plt.ylabel("Propotion of population")
129
+ plt.title("KKKF Estimation")
130
+ plt.legend()
131
+ plt.show()
132
+ ```
133
+
134
+ ## API Reference
135
+
136
+ ### DynamicalSystem
137
+
138
+ ```python
139
+ DynamicalSystem(nx, ny, f, g, X_dist, dyn_dist, obs_dist)
140
+ ```
141
+ Creates a dynamical system with:
142
+ - `nx`: State dimension
143
+ - `ny`: Observation dimension
144
+ - `f`: State transition function
145
+ - `g`: Observation function
146
+ - `X_dist`: State distribution
147
+ - `dyn_dist`: Dynamic noise distribution
148
+ - `obs_dist`: Observation noise distribution
149
+
150
+ ### KoopmanOperator
151
+
152
+ ```python
153
+ KoopmanOperator(kernel, dynamical_system)
154
+ ```
155
+ Initializes a Koopman operator with:
156
+ - `kernel`: Kernel function (e.g., Matérn kernel)
157
+ - `dynamical_system`: Instance of DynamicalSystem
158
+
159
+ ### apply_koopman_kalman_filter
160
+
161
+ ```python
162
+ apply_koopman_kalman_filter(koopman, observations, initial_distribution, N, noise_samples=100)
163
+ ```
164
+ Applies the Koopman-based Kalman filter with:
165
+ - `koopman`: KoopmanOperator instance
166
+ - `observations`: Observation data
167
+ - `initial_distribution`: Initial state distribution
168
+ - `N`: Number of samples
169
+ - `noise_samples`: Number of noise samples for uncertainty estimation
170
+
171
+ Returns a solution object containing:
172
+ - `x_plus`: State estimates
173
+ - `Px_plus`: Covariance matrices
174
+ - Additional filter statistics
175
+
176
+ ## Visualization
177
+
178
+ The library supports visualization of results with confidence intervals. The example above demonstrates how to:
179
+ - Plot state estimates
180
+ - Add confidence intervals (shaded regions)
181
+ - Compare with real data (if available)
182
+ - Customize plot appearance
183
+
184
+ ## Contributing
185
+
186
+ Contributions are welcome! Please feel free to submit a Pull Request.
187
+
188
+ ## License
189
+
190
+ This project is licensed under the MIT License - see the LICENSE file for details.
191
+
192
+ ## Citation
193
+
194
+ If you use this library in your research, please cite:
195
+
196
+ ```bibtex
197
+ @software{kkkf,
198
+ title = {KKKF: Kernel Koopman Kalman Filter},
199
+ year = {2024},
200
+ author = {Diego Olguín-Wende},
201
+ url = {https://github.com/diegoolguinw/KKKF}
202
+ }
203
+ ```
@@ -0,0 +1,12 @@
1
+ kkf/DynamicalSystems.py,sha256=rVtnyaHkAVvbc_cVF4cuLp2jH78lqlLpav2DijspsV0,5519
2
+ kkf/KKFsol.py,sha256=1OJf3ILunJavRnvHMQQF-gR4eG6F6Z-qb_6ZdrIJlyY,4613
3
+ kkf/__init__.py,sha256=NV_a6HLBOlB-0Db21t2g2ZXMQ0vcb3s-LYylO8vxebA,279
4
+ kkf/applyKKF.py,sha256=_65n-PUp3AqccrjYModDgdNY48Qgc59Lp-v75NJHzGE,7887
5
+ kkf/covariances.py,sha256=m7J10FSQj6hVSyJBUVjl4Rf_Gn87NWRqyBQvqLmHSV4,5812
6
+ kkf/kEDMD.py,sha256=ihxD8If5XxCNrZBJsEeFpeMJZHZjpPxnCZYDCHxS628,5467
7
+ kkf/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ kkf-0.11.dist-info/LICENSE,sha256=6WnhMjAMnLiDcClR0tHyWuaX8lEs0MSn5R51JqXL7Ts,1069
9
+ kkf-0.11.dist-info/METADATA,sha256=h_-BrDZrdELzzACjoI4C42qNONhw5JiF5ifMeUEBYDc,5494
10
+ kkf-0.11.dist-info/WHEEL,sha256=2wepM1nk4DS4eFpYrW1TTqPcoGNfHhhO_i5m4cOimbo,92
11
+ kkf-0.11.dist-info/top_level.txt,sha256=_KexmN1Hv2HyrRJoTbhwHIS1S9dmHS1wsXA0gIW1Bls,4
12
+ kkf-0.11.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.38.4)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ kkf