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.
- kkf/DynamicalSystems.py +185 -0
- kkf/KKFsol.py +145 -0
- kkf/__init__.py +5 -0
- kkf/applyKKF.py +228 -0
- kkf/covariances.py +162 -0
- kkf/kEDMD.py +147 -0
- kkf/utils.py +0 -0
- kkf-0.11.dist-info/LICENSE +21 -0
- kkf-0.11.dist-info/METADATA +203 -0
- kkf-0.11.dist-info/RECORD +12 -0
- kkf-0.11.dist-info/WHEEL +5 -0
- kkf-0.11.dist-info/top_level.txt +1 -0
kkf/DynamicalSystems.py
ADDED
|
@@ -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,,
|
kkf-0.11.dist-info/WHEEL
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
kkf
|