dsa-metric 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dsa_metric-1.0.2/DSA/__init__.py +5 -0
- dsa_metric-1.0.2/DSA/dmd.py +596 -0
- dsa_metric-1.0.2/DSA/dsa.py +336 -0
- dsa_metric-1.0.2/DSA/excess.py +17 -0
- dsa_metric-1.0.2/DSA/kerneldmd.py +180 -0
- dsa_metric-1.0.2/DSA/simdist.py +400 -0
- dsa_metric-1.0.2/DSA/stats.py +348 -0
- dsa_metric-1.0.2/LICENSE +21 -0
- dsa_metric-1.0.2/PKG-INFO +94 -0
- dsa_metric-1.0.2/README.md +75 -0
- dsa_metric-1.0.2/dsa_metric.egg-info/PKG-INFO +94 -0
- dsa_metric-1.0.2/dsa_metric.egg-info/SOURCES.txt +15 -0
- dsa_metric-1.0.2/dsa_metric.egg-info/dependency_links.txt +1 -0
- dsa_metric-1.0.2/dsa_metric.egg-info/top_level.txt +1 -0
- dsa_metric-1.0.2/pyproject.toml +19 -0
- dsa_metric-1.0.2/setup.cfg +4 -0
- dsa_metric-1.0.2/setup.py +25 -0
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
"""This module computes the Havok DMD model for a given dataset."""
|
|
2
|
+
import numpy as np
|
|
3
|
+
import torch
|
|
4
|
+
|
|
5
|
+
def embed_signal_torch(data, n_delays, delay_interval=1):
|
|
6
|
+
"""
|
|
7
|
+
Create a delay embedding from the provided tensor data.
|
|
8
|
+
|
|
9
|
+
Parameters
|
|
10
|
+
----------
|
|
11
|
+
data : torch.tensor
|
|
12
|
+
The data from which to create the delay embedding. Must be either: (1) a
|
|
13
|
+
2-dimensional array/tensor of shape T x N where T is the number
|
|
14
|
+
of time points and N is the number of observed dimensions
|
|
15
|
+
at each time point, or (2) a 3-dimensional array/tensor of shape
|
|
16
|
+
K x T x N where K is the number of "trials" and T and N are
|
|
17
|
+
as defined above.
|
|
18
|
+
|
|
19
|
+
n_delays : int
|
|
20
|
+
Parameter that controls the size of the delay embedding. Explicitly,
|
|
21
|
+
the number of delays to include.
|
|
22
|
+
|
|
23
|
+
delay_interval : int
|
|
24
|
+
The number of time steps between each delay in the delay embedding. Defaults
|
|
25
|
+
to 1 time step.
|
|
26
|
+
"""
|
|
27
|
+
if isinstance(data, np.ndarray):
|
|
28
|
+
data = torch.from_numpy(data)
|
|
29
|
+
device = data.device
|
|
30
|
+
|
|
31
|
+
if data.shape[int(data.ndim==3)] - (n_delays - 1)*delay_interval < 1:
|
|
32
|
+
raise ValueError("The number of delays is too large for the number of time points in the data!")
|
|
33
|
+
|
|
34
|
+
# initialize the embedding
|
|
35
|
+
if data.ndim == 3:
|
|
36
|
+
embedding = torch.zeros((data.shape[0], data.shape[1] - (n_delays - 1)*delay_interval, data.shape[2]*n_delays)).to(device)
|
|
37
|
+
else:
|
|
38
|
+
embedding = torch.zeros((data.shape[0] - (n_delays - 1)*delay_interval, data.shape[1]*n_delays)).to(device)
|
|
39
|
+
|
|
40
|
+
for d in range(n_delays):
|
|
41
|
+
index = (n_delays - 1 - d)*delay_interval
|
|
42
|
+
ddelay = d*delay_interval
|
|
43
|
+
|
|
44
|
+
if data.ndim == 3:
|
|
45
|
+
ddata = d*data.shape[2]
|
|
46
|
+
embedding[:,:, ddata: ddata + data.shape[2]] = data[:,index:data.shape[1] - ddelay]
|
|
47
|
+
else:
|
|
48
|
+
ddata = d*data.shape[1]
|
|
49
|
+
embedding[:, ddata:ddata + data.shape[1]] = data[index:data.shape[0] - ddelay]
|
|
50
|
+
|
|
51
|
+
return embedding
|
|
52
|
+
|
|
53
|
+
class DMD:
|
|
54
|
+
"""DMD class for computing and predicting with DMD models.
|
|
55
|
+
"""
|
|
56
|
+
def __init__(
|
|
57
|
+
self,
|
|
58
|
+
data,
|
|
59
|
+
n_delays,
|
|
60
|
+
delay_interval=1,
|
|
61
|
+
rank=None,
|
|
62
|
+
rank_thresh=None,
|
|
63
|
+
rank_explained_variance=None,
|
|
64
|
+
reduced_rank_reg=False,
|
|
65
|
+
lamb=0,
|
|
66
|
+
device='cpu',
|
|
67
|
+
verbose=False,
|
|
68
|
+
send_to_cpu=False,
|
|
69
|
+
steps_ahead=1
|
|
70
|
+
):
|
|
71
|
+
"""
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
data : np.ndarray or torch.tensor
|
|
75
|
+
The data to fit the DMD model to. Must be either: (1) a
|
|
76
|
+
2-dimensional array/tensor of shape T x N where T is the number
|
|
77
|
+
of time points and N is the number of observed dimensions
|
|
78
|
+
at each time point, or (2) a 3-dimensional array/tensor of shape
|
|
79
|
+
K x T x N where K is the number of "trials" and T and N are
|
|
80
|
+
as defined above.
|
|
81
|
+
|
|
82
|
+
n_delays : int
|
|
83
|
+
Parameter that controls the size of the delay embedding. Explicitly,
|
|
84
|
+
the number of delays to include.
|
|
85
|
+
|
|
86
|
+
delay_interval : int
|
|
87
|
+
The number of time steps between each delay in the delay embedding. Defaults
|
|
88
|
+
to 1 time step.
|
|
89
|
+
|
|
90
|
+
rank : int
|
|
91
|
+
The rank of V in fitting HAVOK DMD - i.e., the number of columns of V to
|
|
92
|
+
use to fit the DMD model. Defaults to None, in which case all columns of V
|
|
93
|
+
will be used.
|
|
94
|
+
|
|
95
|
+
rank_thresh : float
|
|
96
|
+
Parameter that controls the rank of V in fitting HAVOK DMD by dictating a threshold
|
|
97
|
+
of singular values to use. Explicitly, the rank of V will be the number of singular
|
|
98
|
+
values greater than rank_thresh. Defaults to None.
|
|
99
|
+
|
|
100
|
+
rank_explained_variance : float
|
|
101
|
+
Parameter that controls the rank of V in fitting HAVOK DMD by indicating the percentage of
|
|
102
|
+
cumulative explained variance that should be explained by the columns of V. Defaults to None.
|
|
103
|
+
|
|
104
|
+
reduced_rank_reg : bool
|
|
105
|
+
Determines whether to use reduced rank regression (True) or principal component regression (False)
|
|
106
|
+
|
|
107
|
+
lamb : float
|
|
108
|
+
Regularization parameter for ridge regression. Defaults to 0.
|
|
109
|
+
|
|
110
|
+
device: string, int, or torch.device
|
|
111
|
+
A string, int or torch.device object to indicate the device to torch.
|
|
112
|
+
|
|
113
|
+
verbose: bool
|
|
114
|
+
If True, print statements will be provided about the progress of the fitting procedure.
|
|
115
|
+
|
|
116
|
+
send_to_cpu: bool
|
|
117
|
+
If True, will send all tensors in the object back to the cpu after everything is computed.
|
|
118
|
+
This is implemented to prevent gpu memory overload when computing multiple DMDs.
|
|
119
|
+
|
|
120
|
+
steps_ahead: int
|
|
121
|
+
The number of time steps ahead to predict. Defaults to 1.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
self.device = device
|
|
125
|
+
self._init_data(data)
|
|
126
|
+
|
|
127
|
+
self.n_delays = n_delays
|
|
128
|
+
self.delay_interval = delay_interval
|
|
129
|
+
self.rank = rank
|
|
130
|
+
self.rank_thresh = rank_thresh
|
|
131
|
+
self.rank_explained_variance = rank_explained_variance
|
|
132
|
+
self.reduced_rank_reg = reduced_rank_reg
|
|
133
|
+
self.lamb = lamb
|
|
134
|
+
self.verbose = verbose
|
|
135
|
+
self.send_to_cpu = send_to_cpu
|
|
136
|
+
self.steps_ahead = steps_ahead
|
|
137
|
+
|
|
138
|
+
# Hankel matrix
|
|
139
|
+
self.H = None
|
|
140
|
+
|
|
141
|
+
# SVD attributes
|
|
142
|
+
self.U = None
|
|
143
|
+
self.S = None
|
|
144
|
+
self.V = None
|
|
145
|
+
self.S_mat = None
|
|
146
|
+
self.S_mat_inv = None
|
|
147
|
+
|
|
148
|
+
# DMD attributes
|
|
149
|
+
self.A_v = None
|
|
150
|
+
self.A_havok_dmd = None
|
|
151
|
+
|
|
152
|
+
def _init_data(self, data):
|
|
153
|
+
# check if the data is an np.ndarry - if so, convert it to Torch
|
|
154
|
+
if isinstance(data, np.ndarray):
|
|
155
|
+
data = torch.from_numpy(data)
|
|
156
|
+
self.data = data
|
|
157
|
+
if self.data.ndim == 2:
|
|
158
|
+
self.data = self.data.unsqueeze(0)
|
|
159
|
+
# create attributes for the data dimensions
|
|
160
|
+
if self.data.ndim == 3:
|
|
161
|
+
self.ntrials = self.data.shape[0]
|
|
162
|
+
self.window = self.data.shape[1]
|
|
163
|
+
self.n = self.data.shape[2]
|
|
164
|
+
else:
|
|
165
|
+
self.window = self.data.shape[0]
|
|
166
|
+
self.n = self.data.shape[1]
|
|
167
|
+
self.ntrials = 1
|
|
168
|
+
|
|
169
|
+
return data
|
|
170
|
+
|
|
171
|
+
def compute_hankel(
|
|
172
|
+
self,
|
|
173
|
+
data=None,
|
|
174
|
+
n_delays=None,
|
|
175
|
+
delay_interval=None,
|
|
176
|
+
):
|
|
177
|
+
"""
|
|
178
|
+
Computes the Hankel matrix from the provided data.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
data : np.ndarray or torch.tensor
|
|
183
|
+
The data to fit the DMD model to. Must be either: (1) a
|
|
184
|
+
2-dimensional array/tensor of shape T x N where T is the number
|
|
185
|
+
of time points and N is the number of observed dimensions
|
|
186
|
+
at each time point, or (2) a 3-dimensional array/tensor of shape
|
|
187
|
+
K x T x N where K is the number of "trials" and T and N are
|
|
188
|
+
as defined above.
|
|
189
|
+
|
|
190
|
+
n_delays : int
|
|
191
|
+
Parameter that controls the size of the delay embedding. Explicitly,
|
|
192
|
+
the number of delays to include. Defaults to None - provide only if you want
|
|
193
|
+
to override the value of n_delays from the init.
|
|
194
|
+
|
|
195
|
+
delay_interval : int
|
|
196
|
+
The number of time steps between each delay in the delay embedding. Defaults
|
|
197
|
+
to 1 time step. Defaults to None - provide only if you want
|
|
198
|
+
to override the value of n_delays from the init.
|
|
199
|
+
"""
|
|
200
|
+
if self.verbose:
|
|
201
|
+
print("Computing Hankel matrix ...")
|
|
202
|
+
|
|
203
|
+
# if parameters are provided, overwrite them from the init
|
|
204
|
+
self.data = self.data if data is None else self._init_data(data)
|
|
205
|
+
self.n_delays = self.n_delays if n_delays is None else n_delays
|
|
206
|
+
self.delay_interval = self.delay_interval if delay_interval is None else delay_interval
|
|
207
|
+
self.data = self.data.to(self.device)
|
|
208
|
+
|
|
209
|
+
self.H = embed_signal_torch(self.data, self.n_delays, self.delay_interval)
|
|
210
|
+
|
|
211
|
+
if self.verbose:
|
|
212
|
+
print("Hankel matrix computed!")
|
|
213
|
+
|
|
214
|
+
def compute_svd(self):
|
|
215
|
+
"""
|
|
216
|
+
Computes the SVD of the Hankel matrix.
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
if self.verbose:
|
|
220
|
+
print("Computing SVD on Hankel matrix ...")
|
|
221
|
+
if self.H.ndim == 3: #flatten across trials for 3d
|
|
222
|
+
H = self.H.reshape(self.H.shape[0] * self.H.shape[1], self.H.shape[2])
|
|
223
|
+
else:
|
|
224
|
+
H = self.H
|
|
225
|
+
# compute the SVD
|
|
226
|
+
U, S, Vh = torch.linalg.svd(H.T, full_matrices=False)
|
|
227
|
+
|
|
228
|
+
# update attributes
|
|
229
|
+
V = Vh.T
|
|
230
|
+
self.U = U
|
|
231
|
+
self.S = S
|
|
232
|
+
self.V = V
|
|
233
|
+
|
|
234
|
+
# construct the singuar value matrix and its inverse
|
|
235
|
+
# dim = self.n_delays * self.n
|
|
236
|
+
# s = len(S)
|
|
237
|
+
# self.S_mat = torch.zeros(dim, dim,dtype=torch.float32).to(self.device)
|
|
238
|
+
# self.S_mat_inv = torch.zeros(dim, dim,dtype=torch.float32).to(self.device)
|
|
239
|
+
self.S_mat = torch.diag(S).to(self.device)
|
|
240
|
+
self.S_mat_inv= torch.diag(1 / S).to(self.device)
|
|
241
|
+
|
|
242
|
+
# compute explained variance
|
|
243
|
+
exp_variance_inds = self.S**2 / ((self.S**2).sum())
|
|
244
|
+
cumulative_explained = torch.cumsum(exp_variance_inds, 0)
|
|
245
|
+
self.cumulative_explained_variance = cumulative_explained
|
|
246
|
+
|
|
247
|
+
#make the X and Y components of the regression by staggering the hankel eigen-time delay coordinates by time
|
|
248
|
+
if self.reduced_rank_reg:
|
|
249
|
+
V = self.V
|
|
250
|
+
else:
|
|
251
|
+
V = self.V
|
|
252
|
+
|
|
253
|
+
if self.ntrials > 1:
|
|
254
|
+
if V.numel() < self.H.numel():
|
|
255
|
+
raise ValueError("The dimension of the SVD of the Hankel matrix is smaller than the dimension of the Hankel matrix itself. \n \
|
|
256
|
+
This is likely due to the number of time points being smaller than the number of dimensions. \n \
|
|
257
|
+
Please reduce the number of delays.")
|
|
258
|
+
|
|
259
|
+
V = V.reshape(self.H.shape)
|
|
260
|
+
|
|
261
|
+
#first reshape back into Hankel shape, separated by trials
|
|
262
|
+
newshape = (self.H.shape[0]*(self.H.shape[1]-self.steps_ahead),self.H.shape[2])
|
|
263
|
+
self.Vt_minus = V[:,:-self.steps_ahead].reshape(newshape)
|
|
264
|
+
self.Vt_plus = V[:,self.steps_ahead:].reshape(newshape)
|
|
265
|
+
else:
|
|
266
|
+
self.Vt_minus = V[:-self.steps_ahead]
|
|
267
|
+
self.Vt_plus = V[self.steps_ahead:]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
if self.verbose:
|
|
271
|
+
print("SVD complete!")
|
|
272
|
+
|
|
273
|
+
def recalc_rank(self,rank,rank_thresh,rank_explained_variance):
|
|
274
|
+
'''
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
rank : int
|
|
278
|
+
The rank of V in fitting HAVOK DMD - i.e., the number of columns of V to
|
|
279
|
+
use to fit the DMD model. Defaults to None, in which case all columns of V
|
|
280
|
+
will be used. Provide only if you want to override the value from the init.
|
|
281
|
+
|
|
282
|
+
rank_thresh : float
|
|
283
|
+
Parameter that controls the rank of V in fitting HAVOK DMD by dictating a threshold
|
|
284
|
+
of singular values to use. Explicitly, the rank of V will be the number of singular
|
|
285
|
+
values greater than rank_thresh. Defaults to None - provide only if you want
|
|
286
|
+
to override the value from the init.
|
|
287
|
+
|
|
288
|
+
rank_explained_variance : float
|
|
289
|
+
Parameter that controls the rank of V in fitting HAVOK DMD by indicating the percentage of
|
|
290
|
+
cumulative explained variance that should be explained by the columns of V. Defaults to None -
|
|
291
|
+
provide only if you want to overried the value from the init.
|
|
292
|
+
'''
|
|
293
|
+
# if an argument was provided, overwrite the stored rank information
|
|
294
|
+
none_vars = (rank is None) + (rank_thresh is None) + (rank_explained_variance is None)
|
|
295
|
+
if none_vars != 3:
|
|
296
|
+
self.rank = None
|
|
297
|
+
self.rank_thresh = None
|
|
298
|
+
self.rank_explained_variance = None
|
|
299
|
+
|
|
300
|
+
self.rank = self.rank if rank is None else rank
|
|
301
|
+
self.rank_thresh = self.rank_thresh if rank_thresh is None else rank_thresh
|
|
302
|
+
self.rank_explained_variance = self.rank_explained_variance if rank_explained_variance is None else rank_explained_variance
|
|
303
|
+
|
|
304
|
+
none_vars = (self.rank is None) + (self.rank_thresh is None) + (self.rank_explained_variance is None)
|
|
305
|
+
if none_vars < 2:
|
|
306
|
+
raise ValueError("More than one value was provided between rank, rank_thresh, and rank_explained_variance. Please provide only one of these, and ensure the others are None!")
|
|
307
|
+
elif none_vars == 3:
|
|
308
|
+
self.rank = len(self.S)
|
|
309
|
+
|
|
310
|
+
if self.reduced_rank_reg:
|
|
311
|
+
S = self.proj_mat_S
|
|
312
|
+
else:
|
|
313
|
+
S = self.S
|
|
314
|
+
|
|
315
|
+
if rank_thresh is not None:
|
|
316
|
+
if S[-1] > rank_thresh:
|
|
317
|
+
self.rank = len(S)
|
|
318
|
+
else:
|
|
319
|
+
self.rank = torch.argmax(torch.arange(len(S), 0, -1).to(self.device)*(S < rank_thresh))
|
|
320
|
+
|
|
321
|
+
if rank_explained_variance is not None:
|
|
322
|
+
self.rank = int(torch.argmax((self.cumulative_explained_variance > rank_explained_variance).type(torch.int)).cpu().numpy())
|
|
323
|
+
|
|
324
|
+
if self.rank > self.H.shape[-1]:
|
|
325
|
+
self.rank = self.H.shape[-1]
|
|
326
|
+
|
|
327
|
+
if self.rank is None:
|
|
328
|
+
if S[-1] > self.rank_thresh:
|
|
329
|
+
self.rank = len(S)
|
|
330
|
+
else:
|
|
331
|
+
self.rank = torch.argmax(torch.arange(len(S), 0, -1).to(self.device)*(S < self.rank_thresh))
|
|
332
|
+
|
|
333
|
+
def compute_havok_dmd(self,lamb=None):
|
|
334
|
+
"""
|
|
335
|
+
Computes the Havok DMD matrix (Principal Component Regression)
|
|
336
|
+
|
|
337
|
+
Parameters
|
|
338
|
+
----------
|
|
339
|
+
lamb : float
|
|
340
|
+
Regularization parameter for ridge regression. Defaults to 0 - provide only if you want
|
|
341
|
+
to override the value of n_delays from the init.
|
|
342
|
+
|
|
343
|
+
"""
|
|
344
|
+
if self.verbose:
|
|
345
|
+
print("Computing least squares fits to HAVOK DMD ...")
|
|
346
|
+
|
|
347
|
+
self.lamb = self.lamb if lamb is None else lamb
|
|
348
|
+
|
|
349
|
+
A_v = (torch.linalg.inv(self.Vt_minus[:, :self.rank].T @ self.Vt_minus[:, :self.rank] + self.lamb*torch.eye(self.rank).to(self.device)) \
|
|
350
|
+
@ self.Vt_minus[:, :self.rank].T @ self.Vt_plus[:, :self.rank]).T
|
|
351
|
+
self.A_v = A_v
|
|
352
|
+
self.A_havok_dmd = self.U @ self.S_mat[:self.U.shape[1], :self.rank] @ self.A_v @ self.S_mat_inv[:self.rank, :self.U.shape[1]] @ self.U.T
|
|
353
|
+
|
|
354
|
+
if self.verbose:
|
|
355
|
+
print("Least squares complete! \n")
|
|
356
|
+
|
|
357
|
+
def get_projections_onto_modes(self):
|
|
358
|
+
"""
|
|
359
|
+
Returns the projection of each time point onto each mode
|
|
360
|
+
"""
|
|
361
|
+
assert self.A_v is not None, "DMD must be fit before projecting onto modes"
|
|
362
|
+
eigvals, eigvecs = torch.linalg.eigh(self.A_v)
|
|
363
|
+
#project Vt_minus onto the eigenvectors
|
|
364
|
+
projections = self.V[:,:self.rank] @ eigvecs
|
|
365
|
+
projections = projections.reshape(self.data.shape[0],self.data.shape[1]-self.n_delays+1,-1)
|
|
366
|
+
|
|
367
|
+
#get the data that matches the shape of the original data
|
|
368
|
+
return projections, self.data[:,:self.n_delays-1]
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def compute_proj_mat(self,lamb=None):
|
|
372
|
+
if self.verbose:
|
|
373
|
+
print("Computing Projector Matrix for Reduced Rank Regression")
|
|
374
|
+
|
|
375
|
+
self.lamb = self.lamb if lamb is None else lamb
|
|
376
|
+
|
|
377
|
+
self.proj_mat = self.Vt_plus.T @ self.Vt_minus @ torch.linalg.inv(self.Vt_minus.T @ self.Vt_minus +
|
|
378
|
+
self.lamb*torch.eye(self.Vt_minus.shape[1]).to(self.device)) @ \
|
|
379
|
+
self.Vt_minus.T @ self.Vt_plus
|
|
380
|
+
|
|
381
|
+
self.proj_mat_S, self.proj_mat_V = torch.linalg.eigh(self.proj_mat)
|
|
382
|
+
#todo: more efficient to flip ranks (negative index) in compute_reduced_rank_regression but also less interpretable
|
|
383
|
+
self.proj_mat_S = torch.flip(self.proj_mat_S, dims=(0,))
|
|
384
|
+
self.proj_mat_V = torch.flip(self.proj_mat_V, dims=(1,))
|
|
385
|
+
|
|
386
|
+
if self.verbose:
|
|
387
|
+
print("Projector Matrix computed! \n")
|
|
388
|
+
|
|
389
|
+
def compute_reduced_rank_regression(self,lamb=None):
|
|
390
|
+
if self.verbose:
|
|
391
|
+
print("Computing Reduced Rank Regression ...")
|
|
392
|
+
|
|
393
|
+
self.lamb = self.lamb if lamb is None else lamb
|
|
394
|
+
proj_mat = self.proj_mat_V[:,:self.rank] @ self.proj_mat_V[:,:self.rank].T
|
|
395
|
+
B_ols = torch.linalg.inv(self.Vt_minus.T @ self.Vt_minus + self.lamb*torch.eye(self.Vt_minus.shape[1]).to(self.device)) @ self.Vt_minus.T @ self.Vt_plus
|
|
396
|
+
|
|
397
|
+
self.A_v = B_ols @ proj_mat
|
|
398
|
+
self.A_havok_dmd = self.U @ self.S_mat[:self.U.shape[1],:self.A_v.shape[1]] @ self.A_v.T @ self.S_mat_inv[:self.A_v.shape[0], :self.U.shape[1]] @ self.U.T
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if self.verbose:
|
|
402
|
+
print("Reduced Rank Regression complete! \n")
|
|
403
|
+
|
|
404
|
+
def substitute_shift_operator(self):
|
|
405
|
+
'''
|
|
406
|
+
the shift operator is a rectangular matrix of shape [dim*(n_delays-nshift),dim*n_delays]
|
|
407
|
+
where the first dim*(n_delays-nshift) rows are the identity matrix
|
|
408
|
+
and the last dim*nshift rows are zeros
|
|
409
|
+
this can be substituted for the bottom of the Havok matrix to predict nshift steps ahead
|
|
410
|
+
why? it can reduce noise
|
|
411
|
+
'''
|
|
412
|
+
if self.A_havok_dmd is None:
|
|
413
|
+
if self.verbose:
|
|
414
|
+
print("Havok DMD must be computed before substituting the shift operator")
|
|
415
|
+
return
|
|
416
|
+
if self.steps_ahead // self.delay_interval != self.steps_ahead / self.delay_interval:
|
|
417
|
+
if self.verbose:
|
|
418
|
+
print("steps_ahead / delay_interval must be an integer to substitute the shift operator")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
nshift = self.steps_ahead // self.delay_interval
|
|
422
|
+
|
|
423
|
+
if self.n*(self.n_delays - nshift) <= 0 :
|
|
424
|
+
if self.verbose:
|
|
425
|
+
print("n*(n_delays - nshift) must be greater than 0 to substitute the shift operator")
|
|
426
|
+
return
|
|
427
|
+
# create the shift operator
|
|
428
|
+
|
|
429
|
+
shift_operator = torch.eye(self.n*(self.n_delays - nshift)).to(self.device)
|
|
430
|
+
shift_operator = torch.vstack([shift_operator, torch.zeros(self.n*nshift,self.n*(self.n_delays - nshift)).to(self.device)]).T
|
|
431
|
+
|
|
432
|
+
self.A_havok_dmd[self.n*nshift:,:] = shift_operator
|
|
433
|
+
|
|
434
|
+
def fit(
|
|
435
|
+
self,
|
|
436
|
+
data=None,
|
|
437
|
+
n_delays=None,
|
|
438
|
+
delay_interval=None,
|
|
439
|
+
rank=None,
|
|
440
|
+
rank_thresh=None,
|
|
441
|
+
rank_explained_variance=None,
|
|
442
|
+
lamb=None,
|
|
443
|
+
device=None,
|
|
444
|
+
verbose=None,
|
|
445
|
+
steps_ahead=None,
|
|
446
|
+
substitute_shift_operator=False
|
|
447
|
+
):
|
|
448
|
+
"""
|
|
449
|
+
Parameters
|
|
450
|
+
----------
|
|
451
|
+
data : np.ndarray or torch.tensor
|
|
452
|
+
The data to fit the DMD model to. Must be either: (1) a
|
|
453
|
+
2-dimensional array/tensor of shape T x N where T is the number
|
|
454
|
+
of time points and N is the number of observed dimensions
|
|
455
|
+
at each time point, or (2) a 3-dimensional array/tensor of shape
|
|
456
|
+
K x T x N where K is the number of "trials" and T and N are
|
|
457
|
+
as defined above. Defaults to None - provide only if you want to
|
|
458
|
+
override the value from the init.
|
|
459
|
+
|
|
460
|
+
n_delays : int
|
|
461
|
+
Parameter that controls the size of the delay embedding. Explicitly,
|
|
462
|
+
the number of delays to include. Defaults to None - provide only if you want to
|
|
463
|
+
override the value from the init.
|
|
464
|
+
|
|
465
|
+
delay_interval : int
|
|
466
|
+
The number of time steps between each delay in the delay embedding. Defaults to None -
|
|
467
|
+
provide only if you want to override the value from the init.
|
|
468
|
+
|
|
469
|
+
rank : int
|
|
470
|
+
The rank of V in fitting HAVOK DMD - i.e., the number of columns of V to
|
|
471
|
+
use to fit the DMD model. Defaults to None, in which case all columns of V
|
|
472
|
+
will be used - provide only if you want to
|
|
473
|
+
override the value from the init.
|
|
474
|
+
|
|
475
|
+
rank_thresh : int
|
|
476
|
+
Parameter that controls the rank of V in fitting HAVOK DMD by dictating a threshold
|
|
477
|
+
of singular values to use. Explicitly, the rank of V will be the number of singular
|
|
478
|
+
values greater than rank_thresh. Defaults to None - provide only if you want to
|
|
479
|
+
override the value from the init.
|
|
480
|
+
|
|
481
|
+
rank_explained_variance : float
|
|
482
|
+
Parameter that controls the rank of V in fitting HAVOK DMD by indicating the percentage of
|
|
483
|
+
cumulative explained variance that should be explained by the columns of V. Defaults to None -
|
|
484
|
+
provide only if you want to overried the value from the init.
|
|
485
|
+
|
|
486
|
+
lamb : float
|
|
487
|
+
Regularization parameter for ridge regression. Defaults to None - provide only if you want to
|
|
488
|
+
override the value from the init.
|
|
489
|
+
|
|
490
|
+
device: string or int
|
|
491
|
+
A string or int to indicate the device to torch. For example, can be 'cpu' or 'cuda',
|
|
492
|
+
or alternatively 0 if the intenion is to use GPU device 0. Defaults to None - provide only
|
|
493
|
+
if you want to override the value from the init.
|
|
494
|
+
|
|
495
|
+
verbose: bool
|
|
496
|
+
If True, print statements will be provided about the progress of the fitting procedure.
|
|
497
|
+
Defaults to None - provide only if you want to override the value from the init.
|
|
498
|
+
|
|
499
|
+
steps_ahead: int
|
|
500
|
+
The number of time steps ahead to predict. Defaults to 1.
|
|
501
|
+
|
|
502
|
+
substitute_shift_operator: bool
|
|
503
|
+
If true, will substitute the bottom of the Havok matrix with the shift operator
|
|
504
|
+
Note that this will only work if steps_ahead / delay_interval is an integer
|
|
505
|
+
|
|
506
|
+
"""
|
|
507
|
+
# if parameters are provided, overwrite them from the init
|
|
508
|
+
self.steps_ahead = self.steps_ahead if steps_ahead is None else steps_ahead
|
|
509
|
+
self.device = self.device if device is None else device
|
|
510
|
+
self.verbose = self.verbose if verbose is None else verbose
|
|
511
|
+
rank = self.rank if rank is None else rank
|
|
512
|
+
rank_thresh = self.rank_thresh if rank_thresh is None else rank_thresh
|
|
513
|
+
rank_explained_variance = self.rank_explained_variance if rank_explained_variance is None else rank_explained_variance
|
|
514
|
+
|
|
515
|
+
self.compute_hankel(data, n_delays, delay_interval)
|
|
516
|
+
self.compute_svd()
|
|
517
|
+
|
|
518
|
+
if self.reduced_rank_reg:
|
|
519
|
+
self.compute_proj_mat(lamb)
|
|
520
|
+
self.recalc_rank(rank,rank_thresh,rank_explained_variance)
|
|
521
|
+
self.compute_reduced_rank_regression(lamb)
|
|
522
|
+
else:
|
|
523
|
+
self.recalc_rank(rank,rank_thresh,rank_explained_variance)
|
|
524
|
+
self.compute_havok_dmd(lamb)
|
|
525
|
+
if substitute_shift_operator:
|
|
526
|
+
self.substitute_shift_operator()
|
|
527
|
+
|
|
528
|
+
if self.send_to_cpu:
|
|
529
|
+
self.all_to_device('cpu') #send back to the cpu to save memory
|
|
530
|
+
|
|
531
|
+
def predict(
|
|
532
|
+
self,
|
|
533
|
+
test_data=None,
|
|
534
|
+
reseed=None,
|
|
535
|
+
full_return=False
|
|
536
|
+
):
|
|
537
|
+
"""
|
|
538
|
+
Returns
|
|
539
|
+
-------
|
|
540
|
+
pred_data : torch.tensor
|
|
541
|
+
The predictions generated by the HAVOK model. Of the same shape as test_data. Note that the first
|
|
542
|
+
(self.n_delays - 1)*self.delay_interval + 1 time steps of the generated predictions are by construction
|
|
543
|
+
identical to the test_data.
|
|
544
|
+
|
|
545
|
+
H_test_havok_dmd : torch.tensor (Optional)
|
|
546
|
+
Returned if full_return=True. The predicted Hankel matrix generated by the HAVOK model.
|
|
547
|
+
H_test : torch.tensor (Optional)
|
|
548
|
+
Returned if full_return=True. The true Hankel matrix
|
|
549
|
+
"""
|
|
550
|
+
# initialize test_data
|
|
551
|
+
if test_data is None:
|
|
552
|
+
test_data = self.data
|
|
553
|
+
if isinstance(test_data, np.ndarray):
|
|
554
|
+
test_data = torch.from_numpy(test_data).to(self.device)
|
|
555
|
+
ndim = test_data.ndim
|
|
556
|
+
if ndim == 2:
|
|
557
|
+
test_data = test_data.unsqueeze(0)
|
|
558
|
+
H_test = embed_signal_torch(test_data, self.n_delays, self.delay_interval)
|
|
559
|
+
steps_ahead = self.steps_ahead if self.steps_ahead is not None else 1
|
|
560
|
+
|
|
561
|
+
if reseed is None:
|
|
562
|
+
reseed = 1
|
|
563
|
+
|
|
564
|
+
H_test_havok_dmd = torch.zeros(H_test.shape).to(self.device)
|
|
565
|
+
H_test_havok_dmd[:, :steps_ahead] = H_test[:, :steps_ahead]
|
|
566
|
+
|
|
567
|
+
A = self.A_havok_dmd.unsqueeze(0)
|
|
568
|
+
for t in range(steps_ahead, H_test.shape[1]):
|
|
569
|
+
if t % reseed == 0:
|
|
570
|
+
H_test_havok_dmd[:, t] = (A @ H_test[:, t - steps_ahead].transpose(-2, -1)).transpose(-2, -1)
|
|
571
|
+
else:
|
|
572
|
+
H_test_havok_dmd[:, t] = (A @ H_test_havok_dmd[:, t - steps_ahead].transpose(-2, -1)).transpose(-2, -1)
|
|
573
|
+
pred_data = torch.hstack([test_data[:, :(self.n_delays - 1)*self.delay_interval + steps_ahead], H_test_havok_dmd[:, steps_ahead:, :self.n]])
|
|
574
|
+
|
|
575
|
+
if ndim == 2:
|
|
576
|
+
pred_data = pred_data[0]
|
|
577
|
+
|
|
578
|
+
if full_return:
|
|
579
|
+
return pred_data, H_test_havok_dmd, H_test
|
|
580
|
+
else:
|
|
581
|
+
return pred_data
|
|
582
|
+
|
|
583
|
+
def all_to_device(self,device='cpu'):
|
|
584
|
+
for k,v in self.__dict__.items():
|
|
585
|
+
if isinstance(v, torch.Tensor):
|
|
586
|
+
self.__dict__[k] = v.to(device)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def project_onto_modes(self):
|
|
590
|
+
eigvals, eigvecs = torch.linalg.eigh(self.A_v)
|
|
591
|
+
#project Vt_minus onto the eigenvectors
|
|
592
|
+
projections = self.V[:,:self.rank] @ eigvecs
|
|
593
|
+
projections = projections.reshape(self.data.shape[0],self.data.shape[1]-self.n_delays+1,-1)
|
|
594
|
+
|
|
595
|
+
#get the data that matches the shape of the original data
|
|
596
|
+
return projections, self.data[:,:-self.n_delays+1]
|