kkf 0.3.0__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/__init__.py +48 -0
- KKF/covariances.py +159 -0
- KKF/filter.py +250 -0
- KKF/koopman.py +155 -0
- KKF/solution.py +145 -0
- KKF/systems.py +200 -0
- KKF/utils.py +0 -0
- kkf-0.3.0.dist-info/LICENSE +21 -0
- kkf-0.3.0.dist-info/METADATA +367 -0
- kkf-0.3.0.dist-info/RECORD +12 -0
- kkf-0.3.0.dist-info/WHEEL +5 -0
- kkf-0.3.0.dist-info/top_level.txt +1 -0
KKF/__init__.py
ADDED
|
@@ -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"
|
KKF/covariances.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from typing import Any, Callable, Union
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from numpy.typing import NDArray
|
|
5
|
+
from scipy.stats import rv_continuous
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def compute_initial_covariance(
|
|
9
|
+
x: NDArray[np.float64],
|
|
10
|
+
n_features: int,
|
|
11
|
+
initial_distribution: rv_continuous,
|
|
12
|
+
koopman_operator: Any,
|
|
13
|
+
n_samples: int,
|
|
14
|
+
) -> NDArray[np.float64]:
|
|
15
|
+
"""
|
|
16
|
+
Compute the covariance matrix for the initial distribution in feature space.
|
|
17
|
+
|
|
18
|
+
This function samples from an initial distribution and computes the covariance
|
|
19
|
+
matrix of the transformed samples using a Koopman operator's feature map.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
x : np.ndarray
|
|
24
|
+
Current state vector or reference point.
|
|
25
|
+
n_features : int
|
|
26
|
+
Number of features in the transformed space.
|
|
27
|
+
initial_distribution : scipy.stats.rv_continuous
|
|
28
|
+
Initial probability distribution to sample from.
|
|
29
|
+
koopman_operator : object
|
|
30
|
+
Object containing the feature map phi method for state transformation.
|
|
31
|
+
Must have a method phi(x) that maps states to feature space.
|
|
32
|
+
n_samples : int
|
|
33
|
+
Number of samples to use for covariance estimation.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
np.ndarray
|
|
38
|
+
Covariance matrix of size (n_features, n_features) in the transformed space.
|
|
39
|
+
|
|
40
|
+
Notes
|
|
41
|
+
-----
|
|
42
|
+
The function performs the following steps:
|
|
43
|
+
1. Samples from the initial distribution
|
|
44
|
+
2. Applies the Koopman operator's feature map to each sample
|
|
45
|
+
3. Computes the covariance matrix of the transformed samples
|
|
46
|
+
"""
|
|
47
|
+
# Sample from initial distribution
|
|
48
|
+
samples = initial_distribution.rvs(size=n_samples).reshape((n_samples, len(x)))
|
|
49
|
+
|
|
50
|
+
# Initialize array for transformed samples
|
|
51
|
+
transformed_samples = np.zeros((n_samples, n_features))
|
|
52
|
+
|
|
53
|
+
# Transform each sample using the feature map
|
|
54
|
+
for i in range(n_samples):
|
|
55
|
+
transformed_samples[i, :] = koopman_operator.phi(samples[i, :])
|
|
56
|
+
|
|
57
|
+
# Compute and return covariance matrix
|
|
58
|
+
return np.cov(transformed_samples, rowvar=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def compute_dynamics_covariance(
|
|
62
|
+
x: NDArray[np.float64], n_features: int, dynamics: Any, koopman_operator: Any, n_samples: int
|
|
63
|
+
) -> NDArray[np.float64]:
|
|
64
|
+
"""
|
|
65
|
+
Compute the covariance matrix for the system dynamics in feature space.
|
|
66
|
+
|
|
67
|
+
This function samples from the dynamics distribution, applies the system
|
|
68
|
+
dynamics, and computes the covariance matrix of the transformed results.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
x : np.ndarray
|
|
73
|
+
Current state vector or reference point.
|
|
74
|
+
n_features : int
|
|
75
|
+
Number of features in the transformed space.
|
|
76
|
+
dynamics : object
|
|
77
|
+
Dynamical system object containing:
|
|
78
|
+
- dist_dyn: scipy.stats.rv_continuous distribution for dynamics noise
|
|
79
|
+
- dynamics(x, w): method implementing the system dynamics
|
|
80
|
+
koopman_operator : object
|
|
81
|
+
Object containing the feature map phi method for state transformation.
|
|
82
|
+
Must have a method phi(x) that maps states to feature space.
|
|
83
|
+
n_samples : int
|
|
84
|
+
Number of samples to use for covariance estimation.
|
|
85
|
+
|
|
86
|
+
Returns
|
|
87
|
+
-------
|
|
88
|
+
np.ndarray
|
|
89
|
+
Covariance matrix of size (n_features, n_features) in the transformed space.
|
|
90
|
+
|
|
91
|
+
Notes
|
|
92
|
+
-----
|
|
93
|
+
The function performs the following steps:
|
|
94
|
+
1. Samples from the dynamics noise distribution
|
|
95
|
+
2. Applies the system dynamics to each sample
|
|
96
|
+
3. Transforms the results using the Koopman operator's feature map
|
|
97
|
+
4. Computes the covariance matrix of the transformed samples
|
|
98
|
+
"""
|
|
99
|
+
# Sample from dynamics distribution
|
|
100
|
+
noise_samples = dynamics.dist_dyn.rvs(size=n_samples).reshape((n_samples, len(x)))
|
|
101
|
+
|
|
102
|
+
# Initialize array for transformed samples
|
|
103
|
+
transformed_samples = np.zeros((n_samples, n_features))
|
|
104
|
+
|
|
105
|
+
# Apply dynamics and transform each sample
|
|
106
|
+
for i in range(n_samples):
|
|
107
|
+
state_evolution = dynamics.dynamics(x, noise_samples[i, :])
|
|
108
|
+
transformed_samples[i, :] = koopman_operator.phi(state_evolution)
|
|
109
|
+
|
|
110
|
+
# Compute and return covariance matrix
|
|
111
|
+
return np.cov(transformed_samples, rowvar=False)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def compute_observation_covariance(
|
|
115
|
+
x: NDArray[np.float64], n_outputs: int, dynamics: Any, n_samples: int
|
|
116
|
+
) -> NDArray[np.float64]:
|
|
117
|
+
"""
|
|
118
|
+
Compute the covariance matrix for the observation/measurement process.
|
|
119
|
+
|
|
120
|
+
This function samples from the measurement noise distribution and computes
|
|
121
|
+
the covariance matrix of the measurement process.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
x : np.ndarray
|
|
126
|
+
Current state vector or reference point.
|
|
127
|
+
n_outputs : int
|
|
128
|
+
Number of measurement outputs.
|
|
129
|
+
dynamics : object
|
|
130
|
+
Dynamical system object containing:
|
|
131
|
+
- dist_obs: scipy.stats.rv_continuous distribution for measurement noise
|
|
132
|
+
- measurements(x, w): method implementing the measurement process
|
|
133
|
+
n_samples : int
|
|
134
|
+
Number of samples to use for covariance estimation.
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
np.ndarray
|
|
139
|
+
Covariance matrix of size (n_outputs, n_outputs) for the measurement process.
|
|
140
|
+
|
|
141
|
+
Notes
|
|
142
|
+
-----
|
|
143
|
+
The function performs the following steps:
|
|
144
|
+
1. Samples from the measurement noise distribution
|
|
145
|
+
2. Applies the measurement function to each sample
|
|
146
|
+
3. Computes the covariance matrix of the measurements
|
|
147
|
+
"""
|
|
148
|
+
# Sample from measurement distribution
|
|
149
|
+
noise_samples = dynamics.dist_obs.rvs(size=n_samples).reshape((n_samples, n_outputs))
|
|
150
|
+
|
|
151
|
+
# Initialize array for measurements
|
|
152
|
+
measurement_samples = np.zeros((n_samples, n_outputs))
|
|
153
|
+
|
|
154
|
+
# Apply measurement function to each sample
|
|
155
|
+
for i in range(n_samples):
|
|
156
|
+
measurement_samples[i, :] = dynamics.measurements(x, noise_samples[i, :])
|
|
157
|
+
|
|
158
|
+
# Compute and return covariance matrix (ensure 2D even for single output)
|
|
159
|
+
return np.atleast_2d(np.cov(measurement_samples, rowvar=False))
|
KKF/filter.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Koopman Kalman Filter algorithm implementation.
|
|
2
|
+
|
|
3
|
+
Combines Koopman operator theory with Kalman filtering for nonlinear
|
|
4
|
+
state estimation.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Tuple
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.linalg import pinv
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
|
|
13
|
+
from .covariances import (
|
|
14
|
+
compute_dynamics_covariance,
|
|
15
|
+
compute_initial_covariance,
|
|
16
|
+
compute_observation_covariance,
|
|
17
|
+
)
|
|
18
|
+
from .solution import KoopmanKalmanFilterSolution
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def apply_koopman_kalman_filter(
|
|
22
|
+
koopman_operator: Any,
|
|
23
|
+
observations: NDArray[np.float64],
|
|
24
|
+
initial_distribution: Any,
|
|
25
|
+
n_features: int,
|
|
26
|
+
optimize: bool = True,
|
|
27
|
+
n_restarts_optimizer: int = 10,
|
|
28
|
+
noise_samples: int = 100,
|
|
29
|
+
) -> KoopmanKalmanFilterSolution:
|
|
30
|
+
"""
|
|
31
|
+
Implementation of the Koopman-Kalman Filter algorithm.
|
|
32
|
+
|
|
33
|
+
This function combines Koopman operator theory with Kalman filtering to perform
|
|
34
|
+
state estimation for nonlinear dynamical systems. It operates by lifting the
|
|
35
|
+
state space to a higher-dimensional feature space where the dynamics are
|
|
36
|
+
approximately linear.
|
|
37
|
+
|
|
38
|
+
Parameters
|
|
39
|
+
----------
|
|
40
|
+
koopman_operator : object
|
|
41
|
+
Object containing Koopman operator methods and attributes:
|
|
42
|
+
- compute_edmd(n_features): Extended Dynamic Mode Decomposition method
|
|
43
|
+
- dynamical_system: Dynamical system object
|
|
44
|
+
- U, B, C: Matrices for Koopman approximation
|
|
45
|
+
- phi: Feature map function
|
|
46
|
+
observations : np.ndarray
|
|
47
|
+
Array of shape (n_timesteps, n_outputs) containing system measurements.
|
|
48
|
+
initial_distribution : object
|
|
49
|
+
Distribution object for initial state with methods:
|
|
50
|
+
- mean: Returns mean of initial state
|
|
51
|
+
- rvs: Random sampling method
|
|
52
|
+
n_features : int
|
|
53
|
+
Number of features in the lifted space.
|
|
54
|
+
optimize : bool, optional
|
|
55
|
+
Whether to optimize the kernel parameters. Default is True.
|
|
56
|
+
n_restarts_optimizer : int, optional
|
|
57
|
+
Number of restarts for kernel optimization. Default is 10.
|
|
58
|
+
noise_samples : int, optional
|
|
59
|
+
Number of samples for noise covariance estimation. Default is 100.
|
|
60
|
+
|
|
61
|
+
Returns
|
|
62
|
+
-------
|
|
63
|
+
KoopmanKalmanFilterSolution
|
|
64
|
+
Object containing all filter estimates and covariances.
|
|
65
|
+
|
|
66
|
+
Notes
|
|
67
|
+
-----
|
|
68
|
+
The algorithm consists of three main phases:
|
|
69
|
+
1. Initialization:
|
|
70
|
+
- Sets up initial states and covariances
|
|
71
|
+
- Computes Koopman approximation
|
|
72
|
+
|
|
73
|
+
2. Prediction Step:
|
|
74
|
+
- Projects state forward using system dynamics
|
|
75
|
+
- Updates covariances using Koopman operator
|
|
76
|
+
|
|
77
|
+
3. Update Step:
|
|
78
|
+
- Incorporates new measurements
|
|
79
|
+
- Updates state and covariance estimates
|
|
80
|
+
|
|
81
|
+
The filter operates in both the original state space (x) and the lifted
|
|
82
|
+
feature space (z), maintaining estimates and covariances in both spaces.
|
|
83
|
+
"""
|
|
84
|
+
# Compute Koopman approximation
|
|
85
|
+
koopman_operator.compute_edmd(n_features, optimize, n_restarts_optimizer)
|
|
86
|
+
dynamical_system = koopman_operator.dynamical_system
|
|
87
|
+
|
|
88
|
+
# Extract system and Koopman components
|
|
89
|
+
state_dynamics = dynamical_system.f
|
|
90
|
+
measurement_model = dynamical_system.g
|
|
91
|
+
U, B, C = koopman_operator.U, koopman_operator.B, koopman_operator.C
|
|
92
|
+
phi = koopman_operator.phi
|
|
93
|
+
|
|
94
|
+
### Initialization ###
|
|
95
|
+
mean_val = initial_distribution.mean # Initial state estimate
|
|
96
|
+
x0 = np.atleast_1d(np.asarray(mean_val() if callable(mean_val) else mean_val))
|
|
97
|
+
z0 = phi(x0) # Initial state in feature space
|
|
98
|
+
|
|
99
|
+
# Get problem dimensions
|
|
100
|
+
n_timesteps, n_states = observations.shape[0], len(x0)
|
|
101
|
+
n_outputs = observations.shape[1]
|
|
102
|
+
|
|
103
|
+
# Initialize state arrays
|
|
104
|
+
x_minus, x_plus = _initialize_state_arrays(n_timesteps, n_states)
|
|
105
|
+
z_minus, z_plus = _initialize_state_arrays(n_timesteps, n_features)
|
|
106
|
+
|
|
107
|
+
# Initialize covariance arrays
|
|
108
|
+
Px_minus, Px_plus = _initialize_covariance_arrays(n_timesteps, n_states)
|
|
109
|
+
Pz_minus, Pz_plus = _initialize_covariance_arrays(n_timesteps, n_features)
|
|
110
|
+
|
|
111
|
+
# Initialize filter matrices
|
|
112
|
+
S = np.zeros((n_timesteps, n_outputs, n_outputs)) # Innovation covariance
|
|
113
|
+
K = np.zeros((n_timesteps, n_features, n_outputs)) # Kalman gain
|
|
114
|
+
|
|
115
|
+
# Set initial conditions
|
|
116
|
+
x_minus[0, :], x_plus[0, :] = x0, x0
|
|
117
|
+
z_minus[0, :], z_plus[0, :] = z0, z0
|
|
118
|
+
|
|
119
|
+
# Initialize covariances
|
|
120
|
+
initial_covariance = compute_initial_covariance(
|
|
121
|
+
x_minus[0, :], n_features, initial_distribution, koopman_operator, noise_samples
|
|
122
|
+
)
|
|
123
|
+
Pz_minus[0, :, :], Pz_plus[0, :, :] = initial_covariance, initial_covariance
|
|
124
|
+
|
|
125
|
+
### Main Filter Loop ###
|
|
126
|
+
for t in range(1, n_timesteps):
|
|
127
|
+
# Prediction Step
|
|
128
|
+
x_minus[t], z_minus[t], Pz_minus[t] = _prediction_step(
|
|
129
|
+
x_plus[t - 1],
|
|
130
|
+
z_plus[t - 1],
|
|
131
|
+
Pz_plus[t - 1],
|
|
132
|
+
state_dynamics,
|
|
133
|
+
phi,
|
|
134
|
+
U,
|
|
135
|
+
dynamical_system,
|
|
136
|
+
koopman_operator,
|
|
137
|
+
n_features,
|
|
138
|
+
noise_samples,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
# Update Step
|
|
142
|
+
x_plus[t], z_plus[t], Pz_plus[t], S[t], K[t] = _update_step(
|
|
143
|
+
x_minus[t],
|
|
144
|
+
z_minus[t],
|
|
145
|
+
Pz_minus[t],
|
|
146
|
+
observations[t],
|
|
147
|
+
measurement_model,
|
|
148
|
+
C,
|
|
149
|
+
B,
|
|
150
|
+
dynamical_system,
|
|
151
|
+
noise_samples,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Update state-space covariances
|
|
155
|
+
Px_minus[t], Px_plus[t] = _update_state_covariances(Pz_minus[t], Pz_plus[t], B)
|
|
156
|
+
|
|
157
|
+
return KoopmanKalmanFilterSolution(x_plus, x_minus, Pz_plus, Pz_minus, Px_plus, Px_minus, S, K)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _initialize_state_arrays(
|
|
161
|
+
n_timesteps: int, n_dim: int
|
|
162
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
163
|
+
"""Initialize arrays for state estimates."""
|
|
164
|
+
return (np.zeros((n_timesteps, n_dim)), np.zeros((n_timesteps, n_dim)))
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _initialize_covariance_arrays(
|
|
168
|
+
n_timesteps: int, n_dim: int
|
|
169
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
170
|
+
"""Initialize arrays for covariance matrices."""
|
|
171
|
+
return (np.zeros((n_timesteps, n_dim, n_dim)), np.zeros((n_timesteps, n_dim, n_dim)))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _prediction_step(
|
|
175
|
+
x_prev: NDArray[np.float64],
|
|
176
|
+
z_prev: NDArray[np.float64],
|
|
177
|
+
Pz_prev: NDArray[np.float64],
|
|
178
|
+
state_dynamics: Callable,
|
|
179
|
+
phi: Callable,
|
|
180
|
+
U: NDArray[np.float64],
|
|
181
|
+
dynamical_system: Any,
|
|
182
|
+
koopman_operator: Any,
|
|
183
|
+
n_features: int,
|
|
184
|
+
noise_samples: int,
|
|
185
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]:
|
|
186
|
+
"""Perform the prediction step of the filter."""
|
|
187
|
+
# Compute dynamics covariance
|
|
188
|
+
Qz = compute_dynamics_covariance(
|
|
189
|
+
x_prev, n_features, dynamical_system, koopman_operator, noise_samples
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# Predict states
|
|
193
|
+
x_pred = state_dynamics(
|
|
194
|
+
x_prev,
|
|
195
|
+
)
|
|
196
|
+
z_pred = phi(x_pred)
|
|
197
|
+
|
|
198
|
+
# Predict covariance
|
|
199
|
+
Pz_pred = U @ Pz_prev @ U.T + Qz
|
|
200
|
+
|
|
201
|
+
return x_pred, z_pred, Pz_pred
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _update_step(
|
|
205
|
+
x_minus: NDArray[np.float64],
|
|
206
|
+
z_minus: NDArray[np.float64],
|
|
207
|
+
Pz_minus: NDArray[np.float64],
|
|
208
|
+
observation: NDArray[np.float64],
|
|
209
|
+
measurement_model: Callable,
|
|
210
|
+
C: NDArray[np.float64],
|
|
211
|
+
B: NDArray[np.float64],
|
|
212
|
+
dynamical_system: Any,
|
|
213
|
+
noise_samples: int,
|
|
214
|
+
) -> Tuple[
|
|
215
|
+
NDArray[np.float64],
|
|
216
|
+
NDArray[np.float64],
|
|
217
|
+
NDArray[np.float64],
|
|
218
|
+
NDArray[np.float64],
|
|
219
|
+
NDArray[np.float64],
|
|
220
|
+
]:
|
|
221
|
+
"""Perform the update step of the filter."""
|
|
222
|
+
# Compute measurement covariance
|
|
223
|
+
Rz = compute_observation_covariance(x_minus, len(observation), dynamical_system, noise_samples)
|
|
224
|
+
|
|
225
|
+
# Compute innovation
|
|
226
|
+
innovation = observation - measurement_model(x_minus)
|
|
227
|
+
|
|
228
|
+
# Compute innovation covariance
|
|
229
|
+
S = C @ Pz_minus @ C.T + Rz
|
|
230
|
+
|
|
231
|
+
# Compute Kalman gain
|
|
232
|
+
K = Pz_minus @ C.T @ pinv(S)
|
|
233
|
+
|
|
234
|
+
# Update states
|
|
235
|
+
z_plus = z_minus + K @ innovation
|
|
236
|
+
x_plus = B @ z_plus
|
|
237
|
+
|
|
238
|
+
# Update covariance
|
|
239
|
+
Pz_plus = (np.eye(len(z_minus)) - K @ C) @ Pz_minus
|
|
240
|
+
|
|
241
|
+
return x_plus, z_plus, Pz_plus, S, K
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _update_state_covariances(
|
|
245
|
+
Pz_minus: NDArray[np.float64], Pz_plus: NDArray[np.float64], B: NDArray[np.float64]
|
|
246
|
+
) -> Tuple[NDArray[np.float64], NDArray[np.float64]]:
|
|
247
|
+
"""Update covariances in the state space."""
|
|
248
|
+
Px_minus = B @ Pz_minus @ B.T
|
|
249
|
+
Px_plus = B @ Pz_plus @ B.T
|
|
250
|
+
return Px_minus, Px_plus
|
KKF/koopman.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Koopman operator approximation module.
|
|
2
|
+
|
|
3
|
+
Implements kernel-based Extended Dynamic Mode Decomposition (kEDMD)
|
|
4
|
+
for computing Koopman operator approximations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
from sklearn.gaussian_process import GaussianProcessRegressor
|
|
12
|
+
from sklearn.gaussian_process.kernels import Kernel
|
|
13
|
+
|
|
14
|
+
from .systems import DynamicalSystem, create_additive_system
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class KoopmanOperator:
|
|
18
|
+
"""
|
|
19
|
+
Implementation of Koopman operator approximation using kernel-based Extended
|
|
20
|
+
Dynamic Mode Decomposition (kEDMD).
|
|
21
|
+
|
|
22
|
+
This class provides methods to compute finite-dimensional approximations of the
|
|
23
|
+
Koopman operator for nonlinear dynamical systems.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
kernel_function : Callable
|
|
28
|
+
Kernel function for computing feature space mappings.
|
|
29
|
+
dynamical_system : DynamicalSystem
|
|
30
|
+
The underlying dynamical system.
|
|
31
|
+
X : Optional[np.ndarray]
|
|
32
|
+
Dictionary of states used for kernel computations.
|
|
33
|
+
phi : Optional[Callable]
|
|
34
|
+
Feature map function.
|
|
35
|
+
U : Optional[np.ndarray]
|
|
36
|
+
Koopman operator matrix.
|
|
37
|
+
G : Optional[np.ndarray]
|
|
38
|
+
Gram matrix.
|
|
39
|
+
C : Optional[np.ndarray]
|
|
40
|
+
Output matrix.
|
|
41
|
+
B : Optional[np.ndarray]
|
|
42
|
+
State-to-feature space transformation matrix.
|
|
43
|
+
|
|
44
|
+
Notes
|
|
45
|
+
-----
|
|
46
|
+
The Koopman operator framework lifts nonlinear dynamics to a linear setting
|
|
47
|
+
in a higher-dimensional feature space. This implementation uses kernel methods
|
|
48
|
+
to compute the necessary feature spaces and operators.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
kernel_function: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
|
54
|
+
dynamical_system: DynamicalSystem,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.kernel_function = kernel_function
|
|
57
|
+
self.dynamical_system = dynamical_system
|
|
58
|
+
self.X: Optional[NDArray[np.float64]] = None
|
|
59
|
+
self.phi: Optional[Callable] = None
|
|
60
|
+
self.U: Optional[NDArray[np.float64]] = None
|
|
61
|
+
self.G: Optional[NDArray[np.float64]] = None
|
|
62
|
+
self.C: Optional[NDArray[np.float64]] = None
|
|
63
|
+
self.B: Optional[NDArray[np.float64]] = None
|
|
64
|
+
|
|
65
|
+
def compute_edmd(
|
|
66
|
+
self, n_features: int, optimize: bool = True, n_restarts_optimizer: int = 10
|
|
67
|
+
) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Compute the kernel-based Extended Dynamic Mode Decomposition (kEDMD).
|
|
70
|
+
|
|
71
|
+
This method constructs finite-dimensional approximations of the Koopman
|
|
72
|
+
operator and associated matrices using kernel methods.
|
|
73
|
+
|
|
74
|
+
Parameters
|
|
75
|
+
----------
|
|
76
|
+
n_features : int
|
|
77
|
+
Number of features to use in the approximation.
|
|
78
|
+
optimize : bool
|
|
79
|
+
Whether to optimize the kernel function. If True, the method will
|
|
80
|
+
optimize the kernel function using Gaussian Process Regression. If False,
|
|
81
|
+
the provided kernel function will be used without optimization. Default is True.
|
|
82
|
+
n_restarts_optimizer : int
|
|
83
|
+
Number of restarts for the optimizer. If optimize is False, will be ignored. Default is 10.
|
|
84
|
+
|
|
85
|
+
Notes
|
|
86
|
+
-----
|
|
87
|
+
The method performs the following steps:
|
|
88
|
+
1. Generates dictionary points using the state distribution
|
|
89
|
+
2. Constructs the feature map using the kernel function
|
|
90
|
+
3. Computes the Gram matrix and its inverse
|
|
91
|
+
4. Constructs the Koopman operator approximation
|
|
92
|
+
5. Computes output and state transformation matrices
|
|
93
|
+
"""
|
|
94
|
+
# Extract system components
|
|
95
|
+
f, g = self.dynamical_system.f, self.dynamical_system.g
|
|
96
|
+
|
|
97
|
+
# Generate dictionary points
|
|
98
|
+
self.X = self.dynamical_system.sample_state(n_features)
|
|
99
|
+
|
|
100
|
+
# Optimize kernel function
|
|
101
|
+
if optimize:
|
|
102
|
+
self.opt_kernel(X=self.X, n_restarts_optimizer=n_restarts_optimizer)
|
|
103
|
+
|
|
104
|
+
# Define feature map
|
|
105
|
+
self.phi = lambda x: self.kernel_function(x, self.X)[0]
|
|
106
|
+
|
|
107
|
+
# Compute Gram matrix
|
|
108
|
+
self.G = self.kernel_function(self.X, self.X)
|
|
109
|
+
G_inv = np.linalg.inv(self.G)
|
|
110
|
+
|
|
111
|
+
# Compute Koopman operator approximation
|
|
112
|
+
next_states = f(self.X.T).T
|
|
113
|
+
self.U = self.kernel_function(self.X, next_states) @ G_inv
|
|
114
|
+
|
|
115
|
+
# Compute output and state transformation matrices
|
|
116
|
+
self.C = g(self.X.T) @ G_inv
|
|
117
|
+
self.B = self.X.T @ G_inv
|
|
118
|
+
|
|
119
|
+
def get_feature_dimension(self) -> Optional[int]:
|
|
120
|
+
"""
|
|
121
|
+
Get the dimension of the feature space.
|
|
122
|
+
|
|
123
|
+
Returns
|
|
124
|
+
-------
|
|
125
|
+
Optional[int]
|
|
126
|
+
Dimension of the feature space, or None if EDMD hasn't been computed.
|
|
127
|
+
"""
|
|
128
|
+
return self.X.shape[0] if self.X is not None else None
|
|
129
|
+
|
|
130
|
+
def opt_kernel(self, X: NDArray[np.float64], n_restarts_optimizer: int = 10) -> None:
|
|
131
|
+
"""
|
|
132
|
+
Optimize the kernel function for the Koopman operator.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
X : np.ndarray
|
|
137
|
+
Set of points.
|
|
138
|
+
n_restarts_optimizer : int
|
|
139
|
+
Number of restarts for the optimizer. Default is 10.
|
|
140
|
+
|
|
141
|
+
Notes
|
|
142
|
+
-----
|
|
143
|
+
This method uses Gaussian Process Regression to optimize the kernel function.
|
|
144
|
+
"""
|
|
145
|
+
# Compute the output of the dynamical system
|
|
146
|
+
y = self.dynamical_system.f(X.T).T
|
|
147
|
+
|
|
148
|
+
# Fit the Gaussian process
|
|
149
|
+
gp = GaussianProcessRegressor(
|
|
150
|
+
kernel=self.kernel_function, n_restarts_optimizer=n_restarts_optimizer
|
|
151
|
+
)
|
|
152
|
+
gp.fit(X.T, y.T)
|
|
153
|
+
|
|
154
|
+
# Update with optimal kernel
|
|
155
|
+
self.kernel_function = gp.kernel_
|
KKF/solution.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Solution data structure for Koopman Kalman Filter results.
|
|
2
|
+
|
|
3
|
+
Provides the KoopmanKalmanFilterSolution dataclass for storing and accessing
|
|
4
|
+
filter outputs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
from numpy.typing import NDArray
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class KoopmanKalmanFilterSolution:
|
|
16
|
+
"""
|
|
17
|
+
A class to store the solution of a Koopman-Kalman Filter iteration.
|
|
18
|
+
|
|
19
|
+
This class maintains the state estimates, covariance matrices, and filter gains
|
|
20
|
+
for both the prior (minus) and posterior (plus) estimates in both state and
|
|
21
|
+
feature spaces.
|
|
22
|
+
|
|
23
|
+
Attributes
|
|
24
|
+
----------
|
|
25
|
+
x_plus : np.ndarray
|
|
26
|
+
Posterior state estimate after measurement update.
|
|
27
|
+
x_minus : np.ndarray
|
|
28
|
+
Prior state estimate from prediction step.
|
|
29
|
+
Pz_plus : np.ndarray
|
|
30
|
+
Posterior covariance matrix in feature space after measurement update.
|
|
31
|
+
Pz_minus : np.ndarray
|
|
32
|
+
Prior covariance matrix in feature space from prediction step.
|
|
33
|
+
Px_plus : np.ndarray
|
|
34
|
+
Posterior covariance matrix in state space after measurement update.
|
|
35
|
+
Px_minus : np.ndarray
|
|
36
|
+
Prior covariance matrix in state space from prediction step.
|
|
37
|
+
S : np.ndarray
|
|
38
|
+
Innovation (residual) covariance matrix.
|
|
39
|
+
K : np.ndarray
|
|
40
|
+
Kalman gain matrix.
|
|
41
|
+
|
|
42
|
+
Notes
|
|
43
|
+
-----
|
|
44
|
+
The class uses the common Kalman filter notation where:
|
|
45
|
+
- (-) denotes prior estimates before measurement update
|
|
46
|
+
- (+) denotes posterior estimates after measurement update
|
|
47
|
+
- Pz refers to covariances in the feature/transformed space
|
|
48
|
+
- Px refers to covariances in the original state space
|
|
49
|
+
|
|
50
|
+
Examples
|
|
51
|
+
--------
|
|
52
|
+
>>> solution = KoopmanKalmanFilterSolution(
|
|
53
|
+
... x_plus=np.array([1.0, 2.0]),
|
|
54
|
+
... x_minus=np.array([0.9, 1.9]),
|
|
55
|
+
... Pz_plus=np.eye(2),
|
|
56
|
+
... Pz_minus=np.eye(2) * 1.1,
|
|
57
|
+
... Px_plus=np.eye(2) * 0.9,
|
|
58
|
+
... Px_minus=np.eye(2),
|
|
59
|
+
... S=np.eye(2) * 0.5,
|
|
60
|
+
... K=np.array([[0.1, 0], [0, 0.1]])
|
|
61
|
+
... )
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
x_plus: NDArray[np.float64]
|
|
65
|
+
x_minus: NDArray[np.float64]
|
|
66
|
+
Pz_plus: NDArray[np.float64]
|
|
67
|
+
Pz_minus: NDArray[np.float64]
|
|
68
|
+
Px_plus: NDArray[np.float64]
|
|
69
|
+
Px_minus: NDArray[np.float64]
|
|
70
|
+
S: NDArray[np.float64]
|
|
71
|
+
K: NDArray[np.float64]
|
|
72
|
+
|
|
73
|
+
def __post_init__(self) -> None:
|
|
74
|
+
"""Validate the dimensions of the input arrays."""
|
|
75
|
+
# Ensure all inputs are numpy arrays
|
|
76
|
+
for attr in ["x_plus", "x_minus", "Pz_plus", "Pz_minus", "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 self.x_plus.shape[-1]
|
|
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[-1]
|
|
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/systems.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""Dynamical systems module.
|
|
2
|
+
|
|
3
|
+
Defines classes and utilities for representing general dynamical systems
|
|
4
|
+
with state transitions and measurement functions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable, Optional
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
from numpy.typing import NDArray
|
|
11
|
+
from scipy.stats import rv_continuous
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DynamicalSystem:
|
|
15
|
+
"""
|
|
16
|
+
A class representing a general dynamical system with state and measurement equations.
|
|
17
|
+
|
|
18
|
+
This class encapsulates the dynamics, measurements, and associated probability
|
|
19
|
+
distributions for both state and measurement noise. Can be considered as:
|
|
20
|
+
|
|
21
|
+
Discrete time:
|
|
22
|
+
Dynamics: x_{k+1} = f(x_{k}) + w_{k}
|
|
23
|
+
Measurements: y_{k} = g(x_{k}) + v_{k}
|
|
24
|
+
|
|
25
|
+
Continous time:
|
|
26
|
+
Dynamics: x'(t) = f(x(t)) + w(t)
|
|
27
|
+
Measurements: y(t) = g(x(t)) + v(t)
|
|
28
|
+
|
|
29
|
+
Attributes
|
|
30
|
+
----------
|
|
31
|
+
nx : int
|
|
32
|
+
Dimension of the state space.
|
|
33
|
+
ny : int
|
|
34
|
+
Dimension of the measurement/output space.
|
|
35
|
+
f : Callable
|
|
36
|
+
State transition function (dynamics).
|
|
37
|
+
g : Callable
|
|
38
|
+
Measurement/output function.
|
|
39
|
+
dist_X : rv_continuous
|
|
40
|
+
Probability distribution for initial states.
|
|
41
|
+
dist_dyn : rv_continuous
|
|
42
|
+
Probability distribution for dynamics noise.
|
|
43
|
+
dist_obs : rv_continuous
|
|
44
|
+
Probability distribution for measurement noise.
|
|
45
|
+
discrete_time : bool
|
|
46
|
+
Indicates if the system is in discrete time, if False the system is in continous time.
|
|
47
|
+
|
|
48
|
+
Notes
|
|
49
|
+
-----
|
|
50
|
+
The functions f and g should have the following signatures:
|
|
51
|
+
- f(x: ndarray, w: ndarray) -> ndarray
|
|
52
|
+
- g(x: ndarray, v: ndarray) -> ndarray
|
|
53
|
+
where:
|
|
54
|
+
- x is the state vector
|
|
55
|
+
- w is the process noise
|
|
56
|
+
- v is the measurement noise
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
nx: int,
|
|
62
|
+
ny: int,
|
|
63
|
+
f: Callable[[NDArray[np.float64]], NDArray[np.float64]],
|
|
64
|
+
g: Callable[[NDArray[np.float64]], NDArray[np.float64]],
|
|
65
|
+
dist_X: rv_continuous,
|
|
66
|
+
dist_dyn: rv_continuous,
|
|
67
|
+
dist_obs: rv_continuous,
|
|
68
|
+
discrete_time: bool,
|
|
69
|
+
) -> None:
|
|
70
|
+
if nx <= 0:
|
|
71
|
+
raise ValueError(f"State dimension nx must be positive, got {nx}")
|
|
72
|
+
self.nx = nx
|
|
73
|
+
self.ny = ny
|
|
74
|
+
self.f = f
|
|
75
|
+
self.g = g
|
|
76
|
+
self.dist_X = dist_X
|
|
77
|
+
self.dist_dyn = dist_dyn
|
|
78
|
+
self.dist_obs = dist_obs
|
|
79
|
+
self.discrete_time = discrete_time
|
|
80
|
+
|
|
81
|
+
def dynamics(self, x: NDArray[np.float64], w: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
82
|
+
"""
|
|
83
|
+
Apply the state transition function.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
x : np.ndarray
|
|
88
|
+
Current state vector.
|
|
89
|
+
w : np.ndarray
|
|
90
|
+
Process noise vector.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
np.ndarray
|
|
95
|
+
Next state vector.
|
|
96
|
+
"""
|
|
97
|
+
return self.f(x) + w
|
|
98
|
+
|
|
99
|
+
def measurements(self, x: NDArray[np.float64], v: NDArray[np.float64]) -> NDArray[np.float64]:
|
|
100
|
+
"""
|
|
101
|
+
Apply the measurement function.
|
|
102
|
+
|
|
103
|
+
Parameters
|
|
104
|
+
----------
|
|
105
|
+
x : np.ndarray
|
|
106
|
+
Current state vector.
|
|
107
|
+
v : np.ndarray
|
|
108
|
+
Measurement noise vector.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
np.ndarray
|
|
113
|
+
Measurement/output vector.
|
|
114
|
+
"""
|
|
115
|
+
return self.g(x) + v
|
|
116
|
+
|
|
117
|
+
def sample_state(self, size: int = 1) -> NDArray[np.float64]:
|
|
118
|
+
"""
|
|
119
|
+
Sample from the state distribution.
|
|
120
|
+
|
|
121
|
+
Parameters
|
|
122
|
+
----------
|
|
123
|
+
size : int, optional
|
|
124
|
+
Number of samples to draw. Default is 1.
|
|
125
|
+
|
|
126
|
+
Returns
|
|
127
|
+
-------
|
|
128
|
+
np.ndarray
|
|
129
|
+
Sampled state(s).
|
|
130
|
+
"""
|
|
131
|
+
return self.dist_X.rvs(size=size).reshape((size, self.nx))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def create_additive_system(
|
|
135
|
+
nx: int,
|
|
136
|
+
ny: int,
|
|
137
|
+
f: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
|
138
|
+
g: Callable[[NDArray[np.float64], NDArray[np.float64]], NDArray[np.float64]],
|
|
139
|
+
dist_X: rv_continuous,
|
|
140
|
+
dist_dyn: rv_continuous,
|
|
141
|
+
dist_obs: rv_continuous,
|
|
142
|
+
N_samples: int,
|
|
143
|
+
discrete_time: bool = True,
|
|
144
|
+
) -> DynamicalSystem:
|
|
145
|
+
"""
|
|
146
|
+
Create an additive dynamical system where noise is added to the state and observation functions.
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
nx : int
|
|
151
|
+
Dimension of the state space.
|
|
152
|
+
ny : int
|
|
153
|
+
Dimension of the measurement space.
|
|
154
|
+
f : Callable
|
|
155
|
+
State transition function (with noise).
|
|
156
|
+
g : Callable
|
|
157
|
+
Measurement function (with noise).
|
|
158
|
+
dist_X : rv_continuous
|
|
159
|
+
Initial state distribution.
|
|
160
|
+
dist_dyn : rv_continuous
|
|
161
|
+
Dynamics noise distribution.
|
|
162
|
+
dist_obs : rv_continuous
|
|
163
|
+
Measurement noise distribution.
|
|
164
|
+
N_samples: int
|
|
165
|
+
Number of samples to generate empirical mean of dynamics and observation.
|
|
166
|
+
discrete_time : bool, optional
|
|
167
|
+
Indicates if the system is in discrete time. Default is True.
|
|
168
|
+
|
|
169
|
+
Returns
|
|
170
|
+
-------
|
|
171
|
+
DynamicalSystem
|
|
172
|
+
New dynamical system instance with additive noise.
|
|
173
|
+
|
|
174
|
+
Notes
|
|
175
|
+
-----
|
|
176
|
+
The resulting system has the form:
|
|
177
|
+
x[k+1] = f(x[k]) + w[k]
|
|
178
|
+
y[k] = g(x[k]) + v[k]
|
|
179
|
+
where w and v are noise terms.
|
|
180
|
+
"""
|
|
181
|
+
new_f = lambda x: np.mean([f(x.reshape((len(x), 1)), dist_dyn.rvs(N_samples))], axis=1)
|
|
182
|
+
new_g = lambda x: np.mean([g(x.reshape((len(x), 1)), dist_obs.rvs(N_samples))], axis=1)
|
|
183
|
+
|
|
184
|
+
class DynDist:
|
|
185
|
+
def __init__(self) -> None:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
def rvs(self, x: NDArray[np.float64], size: int = 1) -> NDArray[np.float64]:
|
|
189
|
+
return f(x, dist_dyn.rvs(size=size)) - new_f(x)
|
|
190
|
+
|
|
191
|
+
class ObsDist:
|
|
192
|
+
def __init__(self) -> None:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
def rvs(self, x: NDArray[np.float64], size: int = 1) -> NDArray[np.float64]:
|
|
196
|
+
return g(x, dist_obs.rvs(size=size)) - new_g(x)
|
|
197
|
+
|
|
198
|
+
return DynamicalSystem(
|
|
199
|
+
nx, ny, new_f, new_g, dist_X, DynDist(), ObsDist(), discrete_time=discrete_time
|
|
200
|
+
)
|
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,367 @@
|
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
|
+
Name: kkf
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: KKF: Kernel Koopman Kalman Filter - A library for non-linear state estimation using kernel methods and Koopman operator theory
|
|
5
|
+
Author-email: Diego Olguin-Wende <dolguin@dim.uchile.cl>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/diegoolguinw/kkf
|
|
8
|
+
Project-URL: Documentation, https://kkf.readthedocs.io
|
|
9
|
+
Project-URL: Repository, https://github.com/diegoolguinw/kkf.git
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/diegoolguinw/kkf/issues
|
|
11
|
+
Keywords: kalman-filter,koopman-operator,kernel-methods,edmd,nonlinear-dynamics,state-estimation,filtering
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
25
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: numpy>=1.20.0
|
|
30
|
+
Requires-Dist: scipy>=1.7.0
|
|
31
|
+
Requires-Dist: scikit-learn>=1.0.0
|
|
32
|
+
Provides-Extra: viz
|
|
33
|
+
Requires-Dist: matplotlib>=3.5.0; extra == "viz"
|
|
34
|
+
Provides-Extra: dev
|
|
35
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
36
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
37
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
38
|
+
Requires-Dist: flake8>=4.0.0; extra == "dev"
|
|
39
|
+
Requires-Dist: mypy>=0.990; extra == "dev"
|
|
40
|
+
Requires-Dist: isort>=5.10.0; extra == "dev"
|
|
41
|
+
Requires-Dist: pylint>=2.13.0; extra == "dev"
|
|
42
|
+
Provides-Extra: docs
|
|
43
|
+
Requires-Dist: sphinx>=4.5.0; extra == "docs"
|
|
44
|
+
Requires-Dist: sphinx-rtd-theme>=1.0.0; extra == "docs"
|
|
45
|
+
Requires-Dist: nbsphinx>=0.8.8; extra == "docs"
|
|
46
|
+
Requires-Dist: ipython>=7.0.0; extra == "docs"
|
|
47
|
+
Requires-Dist: jupyter>=1.0.0; extra == "docs"
|
|
48
|
+
Provides-Extra: all
|
|
49
|
+
Requires-Dist: kkf[dev,docs,viz]; extra == "all"
|
|
50
|
+
|
|
51
|
+
# Kernel Koopman Kalman Filter (KKF)
|
|
52
|
+
|
|
53
|
+
[](https://github.com/diegoolguinw/kkf/actions)
|
|
54
|
+
[](https://badge.fury.io/py/kkf)
|
|
55
|
+
[](https://opensource.org/licenses/MIT)
|
|
56
|
+
[](https://www.python.org/downloads/)
|
|
57
|
+
|
|
58
|
+
A Python library for non-linear state estimation using Kernel-based Extended Dynamic Mode Decomposition (kEDMD) and Koopman operator theory.
|
|
59
|
+
|
|
60
|
+
## What is KKF?
|
|
61
|
+
|
|
62
|
+
KKF is a sophisticated filtering library that combines:
|
|
63
|
+
|
|
64
|
+
- **Koopman Operator Theory**: Lifts nonlinear dynamics to a linear setting in feature space
|
|
65
|
+
- **Kernel Methods**: Uses kernel functions for feature space approximation
|
|
66
|
+
- **Kalman Filtering**: Applies optimal filtering in the lifted feature space
|
|
67
|
+
- **Extended Dynamic Mode Decomposition**: Learns accurate linear approximations
|
|
68
|
+
|
|
69
|
+
This enables accurate state estimation for highly nonlinear dynamical systems where traditional Kalman filters struggle.
|
|
70
|
+
|
|
71
|
+
## Features
|
|
72
|
+
|
|
73
|
+
✨ **Core Capabilities**
|
|
74
|
+
- Kernel Extended Dynamic Mode Decomposition (kEDMD)
|
|
75
|
+
- Non-linear Kalman Filter implementation
|
|
76
|
+
- Support for arbitrary dynamical systems
|
|
77
|
+
- Multiple kernel function options (RBF, Matérn, etc.)
|
|
78
|
+
- Robust state estimation with uncertainty quantification
|
|
79
|
+
- Covariance propagation in both state and feature spaces
|
|
80
|
+
|
|
81
|
+
📊 **High-Quality Implementation**
|
|
82
|
+
- Type hints throughout the codebase
|
|
83
|
+
- Comprehensive error handling
|
|
84
|
+
- Numerical stability optimizations
|
|
85
|
+
- Efficient NumPy/SciPy operations
|
|
86
|
+
|
|
87
|
+
🧪 **Well-Tested**
|
|
88
|
+
- 95+ unit and integration tests
|
|
89
|
+
- Edge case coverage
|
|
90
|
+
- Multi-platform CI/CD
|
|
91
|
+
- Coverage reporting
|
|
92
|
+
|
|
93
|
+
📚 **Comprehensive Documentation**
|
|
94
|
+
- Detailed docstrings
|
|
95
|
+
- 3 complete working examples
|
|
96
|
+
- Parameter optimization guide
|
|
97
|
+
- API documentation
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
|
|
101
|
+
### Current Release
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install kkf
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Development Version
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
git clone https://github.com/diegoolguinw/kkf.git
|
|
111
|
+
cd kkf
|
|
112
|
+
pip install -e ".[dev]"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Optional Dependencies
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
# For visualization examples
|
|
119
|
+
pip install kkf[viz]
|
|
120
|
+
|
|
121
|
+
# For development and testing
|
|
122
|
+
pip install kkf[dev]
|
|
123
|
+
|
|
124
|
+
# For building documentation
|
|
125
|
+
pip install kkf[docs]
|
|
126
|
+
|
|
127
|
+
# Install everything
|
|
128
|
+
pip install kkf[all]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Quick Start
|
|
132
|
+
|
|
133
|
+
Here's a complete example of using KKF to estimate states in a SIR (Susceptible-Infected-Recovered) epidemiological model (see also [`examples/02_sir_epidemic_model.py`](examples/02_sir_epidemic_model.py)):
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
import numpy as np
|
|
137
|
+
from scipy import stats
|
|
138
|
+
from sklearn.gaussian_process.kernels import Matern
|
|
139
|
+
|
|
140
|
+
from KKF import systems, koopman, filter
|
|
141
|
+
|
|
142
|
+
# Define SIR dynamics
|
|
143
|
+
beta, gamma = 0.12, 0.04
|
|
144
|
+
|
|
145
|
+
def f(x): # State transition
|
|
146
|
+
return x + np.array([
|
|
147
|
+
-beta * x[0] * x[1],
|
|
148
|
+
beta * x[0] * x[1] - gamma * x[1],
|
|
149
|
+
gamma * x[1]
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
def g(x): # Observation function (infected population)
|
|
153
|
+
return np.array([x[1]])
|
|
154
|
+
|
|
155
|
+
# Setup system
|
|
156
|
+
nx, ny, n_features = 3, 1, 50
|
|
157
|
+
X_dist = stats.dirichlet(alpha=np.ones(nx))
|
|
158
|
+
dyn_dist = stats.multivariate_normal(mean=np.zeros(nx), cov=1e-5 * np.eye(nx))
|
|
159
|
+
obs_dist = stats.multivariate_normal(mean=np.zeros(ny), cov=1e-3 * np.eye(ny))
|
|
160
|
+
|
|
161
|
+
system = DynamicalSystem(nx, ny, f, g, X_dist, dyn_dist, obs_dist, discrete_time=True)
|
|
162
|
+
|
|
163
|
+
# Generate synthetic observations
|
|
164
|
+
np.random.seed(42)
|
|
165
|
+
n_timesteps = 100
|
|
166
|
+
y_obs = np.random.randn(n_timesteps, ny) * 0.1 + 0.1
|
|
167
|
+
|
|
168
|
+
# Apply Koopman Kalman Filter
|
|
169
|
+
kernel = Matern(length_scale=1.0, nu=0.5)
|
|
170
|
+
koopman_op = KoopmanOperator(kernel, system)
|
|
171
|
+
|
|
172
|
+
x0_prior = np.array([0.8, 0.15, 0.05])
|
|
173
|
+
initial_prior = stats.multivariate_normal(mean=x0_prior, cov=0.1 * np.eye(nx))
|
|
174
|
+
|
|
175
|
+
solution = apply_koopman_kalman_filter(
|
|
176
|
+
koopman_operator=koopman_op,
|
|
177
|
+
observations=y_obs,
|
|
178
|
+
initial_distribution=initial_prior,
|
|
179
|
+
n_features=n_features,
|
|
180
|
+
optimize=False,
|
|
181
|
+
noise_samples=100
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Access results
|
|
185
|
+
print(f"State estimates shape: {solution.x_plus.shape}") # (n_timesteps, nx)
|
|
186
|
+
print(f"State covariances shape: {solution.Px_plus.shape}") # (n_timesteps, nx, nx)
|
|
187
|
+
print(f"Mean estimation error: {solution.get_estimation_error().mean():.6f}")
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Examples
|
|
191
|
+
|
|
192
|
+
The library includes three comprehensive examples:
|
|
193
|
+
|
|
194
|
+
### 1. Basic Linear System (`examples/01_basic_linear_system.py`)
|
|
195
|
+
|
|
196
|
+
Introduces core concepts with a simple linear system:
|
|
197
|
+
- System definition
|
|
198
|
+
- Synthetic data generation
|
|
199
|
+
- Filter application
|
|
200
|
+
- Confidence interval visualization
|
|
201
|
+
|
|
202
|
+
**Run:** `python examples/01_basic_linear_system.py`
|
|
203
|
+
|
|
204
|
+
### 2. SIR Epidemic Model (`examples/02_sir_epidemic_model.py`)
|
|
205
|
+
|
|
206
|
+
Real-world application to disease modeling:
|
|
207
|
+
- Nonlinear dynamics
|
|
208
|
+
- Partial state observation
|
|
209
|
+
- Population dynamics estimation
|
|
210
|
+
- Epidemic curve prediction
|
|
211
|
+
|
|
212
|
+
**Run:** `python examples/02_sir_epidemic_model.py`
|
|
213
|
+
|
|
214
|
+
### 3. Parameter Exploration (`examples/03_advanced_parameter_exploration.py`)
|
|
215
|
+
|
|
216
|
+
Advanced hyperparameter tuning:
|
|
217
|
+
- Multiple kernel comparison
|
|
218
|
+
- Feature dimension optimization
|
|
219
|
+
- Performance analysis
|
|
220
|
+
- Systematic search strategy
|
|
221
|
+
|
|
222
|
+
**Run:** `python examples/03_advanced_parameter_exploration.py`
|
|
223
|
+
|
|
224
|
+
For more details, see [examples/README.md](examples/README.md)
|
|
225
|
+
|
|
226
|
+
## Documentation
|
|
227
|
+
|
|
228
|
+
### API Reference
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
# Core classes
|
|
232
|
+
from KKF import DynamicalSystem, KoopmanOperator, KoopmanKalmanFilterSolution
|
|
233
|
+
from KKF.filter import apply_koopman_kalman_filter
|
|
234
|
+
|
|
235
|
+
# Utility functions
|
|
236
|
+
from KKF import (
|
|
237
|
+
compute_initial_covariance,
|
|
238
|
+
compute_dynamics_covariance,
|
|
239
|
+
compute_observation_covariance
|
|
240
|
+
)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Theory
|
|
244
|
+
|
|
245
|
+
The KKF algorithm works in three main steps:
|
|
246
|
+
|
|
247
|
+
1. **Lifting**: Maps nonlinear states to a higher-dimensional feature space using kernel methods
|
|
248
|
+
2. **Approximation**: Constructs a linear Koopman operator approximation via kEDMD
|
|
249
|
+
3. **Filtering**: Applies Kalman filtering in the lifted space, then maps back to original coordinates
|
|
250
|
+
|
|
251
|
+
### Key Parameters
|
|
252
|
+
|
|
253
|
+
| Parameter | Description | Default |
|
|
254
|
+
|-----------|-------------|---------|
|
|
255
|
+
| `n_features` | Dimension of lifted feature space | 50 |
|
|
256
|
+
| `kernel` | Kernel function (RBF, Matérn, etc.) | RBF(1.0) |
|
|
257
|
+
| `optimize` | Whether to optimize kernel parameters | False |
|
|
258
|
+
| `noise_samples` | Monte Carlo samples for covariance | 100 |
|
|
259
|
+
|
|
260
|
+
## System Requirements
|
|
261
|
+
|
|
262
|
+
- Python 3.8 or higher
|
|
263
|
+
- NumPy ≥ 1.20.0
|
|
264
|
+
- SciPy ≥ 1.7.0
|
|
265
|
+
- scikit-learn ≥ 1.0.0
|
|
266
|
+
- Matplotlib ≥ 3.5.0 (optional, for visualization)
|
|
267
|
+
|
|
268
|
+
## Advanced Usage
|
|
269
|
+
|
|
270
|
+
### Custom Kernel Functions
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
from sklearn.gaussian_process.kernels import (
|
|
274
|
+
RBF, Matern, ExpSineSquared, DotProduct, ConstantKernel
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Different kernel choices
|
|
278
|
+
kernel1 = RBF(length_scale=1.0)
|
|
279
|
+
kernel2 = Matern(length_scale=1.0, nu=2.5)
|
|
280
|
+
kernel3 = ExpSineSquared(length_scale=1.0, periodicity=1.0)
|
|
281
|
+
kernel4 = ConstantKernel(constant_value=1.0) * Matern(nu=1.5)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Manual Kernel Optimization
|
|
285
|
+
|
|
286
|
+
```python
|
|
287
|
+
solution = apply_koopman_kalman_filter(
|
|
288
|
+
koopman_operator=koopman_op,
|
|
289
|
+
observations=y_obs,
|
|
290
|
+
initial_distribution=initial_prior,
|
|
291
|
+
n_features=50,
|
|
292
|
+
optimize=True, # Enable kernel optimization
|
|
293
|
+
n_restarts_optimizer=20 # Number of optimization restarts
|
|
294
|
+
)
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### Extracting Uncertainty Estimates
|
|
298
|
+
|
|
299
|
+
```python
|
|
300
|
+
# Posterior state covariance
|
|
301
|
+
Px_plus = solution.Px_plus # (n_timesteps, nx, nx)
|
|
302
|
+
|
|
303
|
+
# Compute confidence intervals
|
|
304
|
+
std_devs = np.sqrt(np.diagonal(Px_plus, axis1=1, axis2=2))
|
|
305
|
+
ci_lower = solution.x_plus - 1.96 * std_devs
|
|
306
|
+
ci_upper = solution.x_plus + 1.96 * std_devs
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
## Testing
|
|
310
|
+
|
|
311
|
+
Run the comprehensive test suite:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
# All tests
|
|
315
|
+
pytest tests/
|
|
316
|
+
|
|
317
|
+
# Verbose output
|
|
318
|
+
pytest tests/ -v
|
|
319
|
+
|
|
320
|
+
# Specific test file
|
|
321
|
+
pytest tests/test_core.py
|
|
322
|
+
|
|
323
|
+
# Skip slow tests
|
|
324
|
+
pytest tests/ -m "not slow"
|
|
325
|
+
|
|
326
|
+
# With coverage report
|
|
327
|
+
pytest tests/ --cov=KKF --cov-report=html
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Contributing
|
|
331
|
+
|
|
332
|
+
We welcome contributions! See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines on how to:
|
|
333
|
+
- Report bugs
|
|
334
|
+
- Suggest features
|
|
335
|
+
- Submit pull requests
|
|
336
|
+
- Improve documentation
|
|
337
|
+
|
|
338
|
+
## Code of Conduct
|
|
339
|
+
|
|
340
|
+
Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating you agree to abide by its terms.
|
|
341
|
+
|
|
342
|
+
## Citation
|
|
343
|
+
|
|
344
|
+
If you use KKF in your research, please cite:
|
|
345
|
+
|
|
346
|
+
```bibtex
|
|
347
|
+
@software{kkf2024,
|
|
348
|
+
title={KKF: Kernel Koopman Kalman Filter},
|
|
349
|
+
author={Olguin-Wende, Diego},
|
|
350
|
+
year={2024},
|
|
351
|
+
url={https://github.com/diegoolguinw/kkf}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## License
|
|
356
|
+
|
|
357
|
+
This project is licensed under the MIT License - see [LICENSE](LICENSE) file for details.
|
|
358
|
+
|
|
359
|
+
## Changelog
|
|
360
|
+
|
|
361
|
+
See [CHANGELOG.md](CHANGELOG.md) for version history and release notes.
|
|
362
|
+
|
|
363
|
+
## Support & Contact
|
|
364
|
+
|
|
365
|
+
- **Issues & Bug Reports**: [GitHub Issues](https://github.com/diegoolguinw/kkf/issues)
|
|
366
|
+
- **Discussions**: [GitHub Discussions](https://github.com/diegoolguinw/kkf/discussions)
|
|
367
|
+
- **Email**: dolguin@dim.uchile.cl
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
KKF/__init__.py,sha256=3W3pPuvwh-uVF9eHZ6o6xzSRLRHQEwkfXQdv_4B4fI4,1412
|
|
2
|
+
KKF/covariances.py,sha256=7ET1-wvbDnYBIc8v_i_aO7k06wZwHXI7Cxj_HGBbIkQ,5735
|
|
3
|
+
KKF/filter.py,sha256=evtaZFj9mbOKie6wUKesC50rX1-Dpvkk-Pf4uwOd3m0,8095
|
|
4
|
+
KKF/koopman.py,sha256=KobfN1WPW-kxiCl_4-9v5NoGX4TTfChw8s8ngM0Evp0,5480
|
|
5
|
+
KKF/solution.py,sha256=AqglUKkr6XiwEXnfA8YCX4edTaF1cw3ceLGlDOAGEOk,4425
|
|
6
|
+
KKF/systems.py,sha256=FmKc5BuRVEPxKF94y1FAi_-uvmCteNFYuF3ps7NRL00,5798
|
|
7
|
+
KKF/utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
kkf-0.3.0.dist-info/LICENSE,sha256=6WnhMjAMnLiDcClR0tHyWuaX8lEs0MSn5R51JqXL7Ts,1069
|
|
9
|
+
kkf-0.3.0.dist-info/METADATA,sha256=t_ZymB92I2o0Iz22L7WcOq0JaXAV3gy_xc1Hk7wBBVI,10888
|
|
10
|
+
kkf-0.3.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
11
|
+
kkf-0.3.0.dist-info/top_level.txt,sha256=yCY9tBJ5DJeTknZPyFujr9XvomHsm-7oP4ABMz348pw,4
|
|
12
|
+
kkf-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
KKF
|