11import numpy as np
22from pytest import raises
33from scipy .integrate import solve_ivp
4-
54from numpy .testing import assert_equal
65
6+ from pydmd import DMD
7+ from pydmd import PiDMD
78from pydmd import HAVOK
89
910
@@ -12,8 +13,9 @@ def generate_lorenz_data(t_eval):
1213 Given a time vector t_eval = t1, t2, ..., evaluates and returns
1314 the snapshots of the Lorenz system as columns of the matrix X.
1415 """
16+
1517 def lorenz_system (t , state ):
16- sigma , rho , beta = 10 , 28 , 8 / 3 # chaotic parameters
18+ sigma , rho , beta = 10 , 28 , 8 / 3 # chaotic parameters
1719 x , y , z = state
1820 x_dot = sigma * (y - x )
1921 y_dot = (x * (rho - z )) - y
@@ -38,7 +40,7 @@ def lorenz_system(t, state):
3840
3941
4042# Generate Lorenz system data.
41- dt = 0.001 # time step
43+ dt = 0.001 # time step
4244m = 50000 # number of data samples
4345t = np .arange (m ) * dt
4446X = generate_lorenz_data (t )
@@ -47,17 +49,11 @@ def lorenz_system(t, state):
4749
4850def test_error_fitted ():
4951 """
50- Ensure that attempting to get attributes results
52+ Ensure that attempting to get HAVOK attributes results
5153 in an error if fit() has not yet been called.
5254 """
5355 havok = HAVOK ()
5456
55- with raises (ValueError ):
56- _ = havok .snapshots
57- with raises (ValueError ):
58- _ = havok .ho_snapshots
59- with raises (ValueError ):
60- _ = havok .time
6157 with raises (ValueError ):
6258 _ = havok .modes
6359 with raises (ValueError ):
@@ -82,12 +78,20 @@ def test_error_fitted():
8278
8379def test_hankel_1 ():
8480 """
85- Test
81+ Test that the hankel and dehankel functions work as intended.
82+ Use 1-D data, lag = 1, and various delay values.
8683 """
8784 dummy_data = np .array ([[1 , 2 , 3 , 4 ]])
8885
8986 havok = HAVOK (delays = 1 )
90- assert_equal (havok .hankel (dummy_data ), np .array ([[1 , 2 , 3 , 4 ],]))
87+ assert_equal (
88+ havok .hankel (dummy_data ),
89+ np .array (
90+ [
91+ [1 , 2 , 3 , 4 ],
92+ ]
93+ ),
94+ )
9195 assert_equal (havok .dehankel (havok .hankel (dummy_data )), dummy_data )
9296
9397 havok = HAVOK (delays = 2 )
@@ -99,7 +103,25 @@ def test_hankel_1():
99103 assert_equal (havok .dehankel (havok .hankel (dummy_data )), dummy_data )
100104
101105 havok = HAVOK (delays = 4 )
102- assert_equal (havok .hankel (dummy_data ), np .array ([[1 ,], [2 ,], [3 ,], [4 ,]]))
106+ assert_equal (
107+ havok .hankel (dummy_data ),
108+ np .array (
109+ [
110+ [
111+ 1 ,
112+ ],
113+ [
114+ 2 ,
115+ ],
116+ [
117+ 3 ,
118+ ],
119+ [
120+ 4 ,
121+ ],
122+ ]
123+ ),
124+ )
103125 assert_equal (havok .dehankel (havok .hankel (dummy_data )), dummy_data )
104126
105127 with raises (ValueError ):
@@ -109,7 +131,8 @@ def test_hankel_1():
109131
110132def test_hankel_2 ():
111133 """
112- Test
134+ Test that the hankel and dehankel functions work as intended.
135+ Use 2-D data, lag = 1, and various delay values.
113136 """
114137 dummy_data = np .array ([[1 , 2 , 3 ], [4 , 5 , 6 ]])
115138 H2 = np .array ([[1 , 2 ], [4 , 5 ], [2 , 3 ], [5 , 6 ]])
@@ -134,7 +157,8 @@ def test_hankel_2():
134157
135158def test_hankel_3 ():
136159 """
137- Test
160+ Test that the hankel and dehankel functions work as intended.
161+ Use 1-D data, lag = 2, and various delay values.
138162 """
139163 dummy_data = np .array ([[1 , 2 , 3 , 4 , 5 , 6 ]])
140164 H2 = np .array ([[1 , 2 , 3 , 4 ], [3 , 4 , 5 , 6 ]])
@@ -149,12 +173,12 @@ def test_hankel_3():
149173 assert_equal (havok .hankel (dummy_data ), H2 )
150174 assert_equal (havok .dehankel (havok .hankel (dummy_data )), dummy_data )
151175
152- havok = HAVOK (delays = 3 , lag = 3 )
176+ havok = HAVOK (delays = 3 , lag = 2 )
153177 assert_equal (havok .hankel (dummy_data ), H3 )
154178 assert_equal (havok .dehankel (havok .hankel (dummy_data )), dummy_data )
155179
156180 with raises (ValueError ):
157- havok = HAVOK (delays = 2 , lag = 2 )
181+ havok = HAVOK (delays = 4 , lag = 2 )
158182 havok .hankel (dummy_data )
159183
160184
@@ -186,43 +210,215 @@ def test_shape_2():
186210 assert havok .B .shape == (havok .r - 2 , 2 )
187211
188212
189- def test_r ():
213+ def test_snapshots_1 ():
190214 """
191- Ensure the accuracy of the r property in the following situations:
215+ Test the stored snapshots and ho_snapshots.
216+ Ensure that they are accurate in the default case with 1-D data.
192217 """
193- # If no svd truncation, r is the min of the dimensions of the hankel matrix
194- havok = HAVOK (svd_rank = - 1 )
218+ havok = HAVOK ()
219+ with raises (ValueError ):
220+ _ = havok .snapshots
221+ with raises (ValueError ):
222+ _ = havok .ho_snapshots
195223 havok .fit (x , t )
196- assert havok .r == min (havok .delays , len (t ) - havok .delays + 1 )
224+ assert_equal (havok .snapshots , x )
225+ assert_equal (havok .ho_snapshots , havok .hankel (x ))
226+
227+
228+ def test_snapshots_2 ():
229+ """
230+ Test the stored snapshots and ho_snapshots.
231+ Ensure that they are accurate in the default case with 2-D data.
232+ """
233+ havok = HAVOK ().fit (X , t )
234+ assert_equal (havok .snapshots , X )
235+ assert_equal (havok .ho_snapshots , havok .hankel (X ))
236+
197237
198- # Test the above case, but for a larger d value
199- havok = HAVOK (svd_rank = - 1 , delays = 500 )
238+ def test_time_1 ():
239+ """
240+ Test the stored time attribute.
241+ Ensure that it is accurate in the default case.
242+ """
243+ havok = HAVOK ()
244+ with raises (ValueError ):
245+ _ = havok .time
200246 havok .fit (x , t )
201- assert havok .r == min (havok .delays , len (t ) - havok .delays + 1 )
247+ assert_equal (havok .time , t )
248+
249+
250+ def test_time_2 ():
251+ """
252+ Test that a HAVOK model fitted with a time vector is essentially
253+ the same as a HAVOK model fitted with the time-step dt. Check the
254+ stored time vector and the computed HAVOK operator.
255+ """
256+ havok_1 = HAVOK ().fit (x , t )
257+ havok_2 = HAVOK ().fit (x , dt )
258+ assert_equal (havok_1 .time , havok_2 .time )
259+ assert_equal (havok_1 .operator , havok_2 .operator )
260+
202261
203- # Test the above case, but for an even larger d value
204- havok = HAVOK (svd_rank = - 1 , delays = len (t ) - 20 )
262+ def test_plot_summary_1 ():
263+ """
264+ Test that everything is fine if we fit a HAVOK
265+ model and ask for the default summary plot.
266+ """
267+ havok = HAVOK (svd_rank = 16 , delays = 100 )
205268 havok .fit (x , t )
206- assert havok .r == min ( havok . delays , len ( t ) - havok . delays + 1 )
269+ havok .plot_summary ( )
207270
208271
209- def test_reconstruction ():
272+ def test_plot_summary_2 ():
210273 """
211- Test the accuracy of the HAVOK reconstruction.
274+ Test that everything is fine if we fit a HAVOK model and
275+ ask for various (but still valid) plot modifications.
212276 """
213277 havok = HAVOK (svd_rank = 16 , delays = 100 )
214278 havok .fit (x , t )
215- error = x - havok .reconstructed_data .real
216- assert np .linalg .norm (error ) / np .linalg .norm (x ) < 0.2
279+ havok .plot_summary (
280+ num_plot = 15000 ,
281+ index_linear = (0 , 1 ),
282+ forcing_threshold = 0.005 ,
283+ min_jump_dist = 200 ,
284+ figsize = (15 , 4 ),
285+ dpi = 100 ,
286+ )
287+
288+
289+ def test_plot_summary_3 ():
290+ """
291+ Test that an error is thrown if we ask for
292+ a summary from an unfitted HAVOK model.
293+ """
294+ havok = HAVOK ()
295+ with raises (ValueError ):
296+ havok .plot_summary ()
297+
298+
299+ def test_reconstruction_1 ():
300+ """
301+ Test the accuracy of the HAVOK reconstruction.
302+ """
303+ havok = HAVOK (svd_rank = 16 , delays = 100 ).fit (x , t )
304+ error = x - havok .reconstructed_data
305+ assert np .linalg .norm (error ) / np .linalg .norm (x ) < 0.05
306+
307+
308+ def test_reconstruction_2 ():
309+ """
310+ Test the accuracy of the sHAVOK reconstruction.
311+ """
312+ havok = HAVOK (svd_rank = 4 , delays = 100 , structured = True ).fit (x , t )
313+ error = x [:- 1 ] - havok .reconstructed_data
314+ assert np .linalg .norm (error ) / np .linalg .norm (x [:- 1 ]) < 0.07
315+
316+
317+ def test_predict_1 ():
318+ """
319+ Test the accuracy of the HAVOK prediction.
320+ Ensure that prediction with the HAVOK forcing term and the times
321+ of fitting simply yields the computed data reconstruction.
322+ """
323+ havok = HAVOK (svd_rank = 16 , delays = 100 ).fit (x , t )
324+ assert_equal (
325+ havok .predict (havok .forcing , havok .time ),
326+ havok .reconstructed_data ,
327+ )
328+
329+
330+ def test_predict_2 ():
331+ """
332+ Test the accuracy of the HAVOK prediction.
333+ Test that predicting beyond the training set isn't absurdly inaccurate.
334+ """
335+ havok = HAVOK (svd_rank = 16 , delays = 100 ).fit (x , t )
336+
337+ # Build a longer data set and fit a HAVOK model to it.
338+ t_long = np .arange (2 * m ) * dt
339+ x_long = generate_lorenz_data (t_long )[0 ]
340+ havok_long = HAVOK (svd_rank = 16 , delays = 100 ).fit (x_long , t_long )
341+
342+ # We only use the long HAVOK model to obtain a long forcing signal.
343+ forcing_long = havok_long .forcing
344+ time_long = t_long [: len (forcing_long )]
217345
346+ # Get the error of the full prediction.
347+ error = x_long - havok .predict (forcing_long , time_long )
348+ assert np .linalg .norm (error ) / np .linalg .norm (x_long ) < 0.45
218349
219- def test_predict ():
350+
351+ def test_predict_3 ():
220352 """
221353 Test the accuracy of the HAVOK prediction.
354+ Test that predicting with V0 indices is functionally
355+ the same as predicting with array-valued V0 inputs.
356+ """
357+ havok = HAVOK (svd_rank = 16 , delays = 100 ).fit (x , t )
358+ assert_equal (
359+ havok .predict (havok .forcing , havok .time , V0 = 0 ),
360+ havok .predict (havok .forcing , havok .time , V0 = havok .linear_dynamics [0 ]),
361+ )
362+ assert_equal (
363+ havok .predict (havok .forcing , havok .time , V0 = 1 ),
364+ havok .predict (havok .forcing , havok .time , V0 = havok .linear_dynamics [1 ]),
365+ )
366+ assert_equal (
367+ havok .predict (havok .forcing , havok .time , V0 = - 1 ),
368+ havok .predict (havok .forcing , havok .time , V0 = havok .linear_dynamics [- 1 ]),
369+ )
370+
371+
372+ def test_threshold_1 ():
222373 """
374+ Test compute_threshold function.
375+ Test that threshold computation works as expected, whether you plot or not.
376+ """
377+ havok = HAVOK (svd_rank = 16 , delays = 100 ).fit (x , t )
378+ thres_1 = havok .compute_threshold (p = 0.1 , bins = 100 , plot = False )
379+ thres_2 = havok .compute_threshold (p = 0.1 , bins = 100 , plot = True )
380+ assert thres_1 == thres_2
381+
382+
383+ def test_threshold_2 ():
384+ """
385+ Test compute_threshold function.
386+ Test that threshold computation works as expected, whether you index
387+ the stored forcing term, or provide a custom array-valued forcing term.
388+ """
389+ havok = HAVOK (svd_rank = 16 , delays = 100 ).fit (x , t )
390+ vr = havok .forcing .flatten ()
391+ thres_1 = havok .compute_threshold (p = 0.1 , bins = 100 )
392+ thres_2 = havok .compute_threshold (forcing = vr , p = 0.1 , bins = 100 )
393+ assert thres_1 == thres_2
223394
224395
225- def test_time ():
396+ def test_dmd_1 ():
226397 """
227- Test that
398+ Test that HAVOK works when used with an externally-defined DMD model.
399+ Test the basic exact DMD model - ensure that plot_summary works just
400+ fine and that the data reconstruction is still accurate.
228401 """
402+ dmd = DMD (svd_rank = - 1 )
403+ havok = HAVOK (svd_rank = 16 , delays = 100 , dmd = dmd ).fit (x , t )
404+ havok .plot_summary ()
405+ error = x - havok .reconstructed_data
406+ assert np .linalg .norm (error ) / np .linalg .norm (x ) < 0.05
407+
408+
409+ def test_dmd_2 ():
410+ """
411+ Test that HAVOK works when used with an externally-defined DMD model.
412+ Test a physics-informed DMD model - ensure that plot_summary works just
413+ fine and that the data reconstruction is still accurate.
414+ """
415+ pidmd = PiDMD (
416+ svd_rank = - 1 ,
417+ manifold = "diagonal" ,
418+ manifold_opt = 2 ,
419+ compute_A = True ,
420+ )
421+ havok = HAVOK (svd_rank = 4 , delays = 100 , dmd = pidmd ).fit (x , t )
422+ havok .plot_summary ()
423+ error = x - havok .reconstructed_data
424+ assert np .linalg .norm (error ) / np .linalg .norm (x ) < 0.1
0 commit comments