6666theta_0 = np .pi / 4
6767
6868
69+ def rotation_matrix (theta ):
70+ return np .array ([[np .cos (theta ), - np .sin (theta )], [np .sin (theta ), np .cos (theta )]])
71+
72+
6973def generate_data (time , tau , freq , theta ):
7074 t_ = np .sin (2 * np .pi * freq [None , :] * time [:, None ]) * np .exp (
7175 - tau [None , :] * time [:, None ]
7276 )
7377 t_ = t_ .sum (axis = 1 )
7478 traj_0 = np .zeros ((t_ .shape [0 ], 2 ))
7579 traj_0 [:, 0 ] = t_
76- rotation_matrix = np .array (
77- [[np .cos (theta ), - np .sin (theta )], [np .sin (theta ), np .cos (theta )]]
78- )
79- traj_0 = traj_0 @ rotation_matrix .T
80+ R_ = rotation_matrix (theta )
81+ traj_0 = traj_0 @ R_ .T
8082 return traj_0
8183
8284
8385traj_0 = generate_data (time , tau_0 , freq_0 , theta_0 )
86+ traj_0_proj = traj_0 @ rotation_matrix (theta_0 )[:, 0 ]
8487
8588
8689# plot the observed signal components and their sum
8790plt .figure (figsize = (10 , 4 ))
88- plt .plot (time , traj_0 , label = "base trajectory" , linewidth = 2 )
91+ plt .plot (time , traj_0_proj , label = "projected trajectory" , linewidth = 2 )
8992plt .xlabel ("time" )
9093plt .ylabel ("amplitude" )
9194plt .legend ()
9295plt .title (r"Observed scalar signal along $\vec{e}(\theta)$" )
9396plt .show ()
9497
95-
9698# %%
9799# 2. Interpret the signal as coming from a continuous linear dynamical system
98100# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -274,12 +276,8 @@ def augment(traj, window_length=2):
274276# Processing Systems, 35, pp.4017-4031.
275277
276278
277- def estimator (X , Y , rank = 4 ):
278- # X: (n_samples, n_features)
279- # Y: (n_samples, n_features)
280-
281- # estimate operator
282- cxx = X .T @ X
279+ def estimator (X , Y , rank = 4 , eps = 1e-8 ):
280+ cxx = X .T @ X + eps * np .eye (X .shape [1 ])
283281 U , S , Vt = np .linalg .svd (cxx )
284282 S_inv = np .divide (1 , S , out = np .zeros_like (S ), where = S != 0 )
285283 cxx_inv_half = Vt .T @ np .diag (np .sqrt (S_inv )) @ U .T
@@ -416,6 +414,24 @@ def estimator(X, Y, rank=4):
416414# spectral atoms, taking into account both the location of eigenvalues and the
417415# relative geometry of their eigenspaces.
418416
417+ # %%
418+ # A wider delay window for the SGOT experiments below
419+ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
420+ #
421+ # The window of length 4 used above is enough to identify a single reference
422+ # operator, but the experiments below also probe signals whose two modes
423+ # nearly coincide in frequency (e.g. :math:`\omega_2'\to\omega_1`). Telling
424+ # such near-degenerate modes apart requires the delay embedding to span
425+ # enough time to "see" their differing decay, so we re-embed the reference
426+ # signal with a longer window before running the sweeps.
427+
428+ sgot_window = 10
429+ Z = augment (traj_0 , sgot_window )
430+ _ , B_0_spec_sgot = estimator (Z [:- 1 ], Z [1 :])
431+ D_0_sgot = np .log (B_0_spec_sgot ["eig_val" ]) * fs
432+ L_0_sgot = B_0_spec_sgot ["eig_vec_left" ]
433+ R_0_sgot = B_0_spec_sgot ["eig_vec_right" ]
434+
419435# %%
420436# SGOT distance versus rotation angle
421437# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -434,52 +450,66 @@ def estimator(X, Y, rank=4):
434450# this experiment isolates the effect of rotating the underlying one-dimensional
435451# subspace in the observation plane.
436452
437- thetas = np .linspace (0 , np .pi / 2 , 50 )
438- lst = []
439- for i , theta in enumerate (thetas ):
440- traj = generate_data (time , tau_0 , freq_0 , theta )
441- Z = augment (traj , 4 )
442- X = Z [:- 1 ]
443- Y = Z [1 :]
444- B , B_spec = estimator (X , Y , rank = 4 )
445- D , R , L = B_spec ["eig_val" ], B_spec ["eig_vec_right" ], B_spec ["eig_vec_left" ]
446- D = np .log (D ) * fs
447- lst .append (sgot_metric (D_0 , R_0 , L_0 , D , R , L , eta = 0.01 ))
448-
449- plt .figure (figsize = (8 , 5 ))
450- plt .plot (thetas , lst )
451- plt .xlabel ("theta" )
452- plt .ylabel ("SGOT distance" )
453- plt .title ("SGOT distance vs rotation angle" )
453+ thetas = np .linspace (0 , np .pi / 2 , 51 )
454+ rotation_scores = []
455+
456+ for theta in thetas :
457+ Z = augment (generate_data (time , tau_0 , freq_0 , theta ), sgot_window )
458+ B , B_spec = estimator (Z [:- 1 ], Z [1 :])
459+ D = np .log (B_spec ["eig_val" ]) * fs
460+ L = B_spec ["eig_vec_left" ]
461+ R = B_spec ["eig_vec_right" ]
462+ rotation_scores .append (
463+ sgot_metric (
464+ D_0_sgot , R_0_sgot , L_0_sgot , D , R , L , eta = 0.9 , grassmann_metric = "chordal"
465+ )
466+ )
467+
468+ fig , ax = plt .subplots (figsize = (7 , 4 ))
469+ ax .plot (thetas , rotation_scores , linewidth = 1.8 )
470+ ax .axvline (theta_0 , color = "gray" , linestyle = "--" , linewidth = 0.8 )
471+ ax .set_xlabel (r"Rotation angle $\theta$ (rad)" )
472+ ax .set_ylabel (r"$d_S$" )
473+ ax .set_title ("SGOT distance vs. rotation angle" )
474+ fig .tight_layout ()
454475plt .show ()
455476
456477# %%
457478# Comparison across Grassmannian metrics for SGOT distance versus rotation angle
458479# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
459480
460- thetas = np .linspace (0 , np .pi / 2 , 50 )
461- lst = []
462- for i , theta in enumerate (thetas ):
463- traj = generate_data (time , tau_0 , freq_0 , theta )
464- Z = augment (traj , 4 )
465- X = Z [:- 1 ]
466- Y = Z [1 :]
467- B , B_spec = estimator (X , Y , rank = 4 )
468- D , R , L = B_spec ["eig_val" ], B_spec ["eig_vec_right" ], B_spec ["eig_vec_left" ]
469- D = np .log (D ) * fs
470- lst1 = []
471- for name in ["chordal" , "martin" , "geodesic" , "procrustes" ]:
472- lst1 .append (sgot_metric (D_0 , R_0 , L_0 , D , R , L , eta = 0.9 , grassmann_metric = name ))
473- lst .append (lst1 )
474- lst2 = np .array (lst )
475- plt .figure (figsize = (8 , 5 ))
476- for i , name in enumerate (["chordal" , "martin" , "geodesic" , "procrustes" ]):
477- plt .plot (thetas , lst2 [:, i ], label = name )
478-
479- plt .xlabel ("theta" )
480- plt .ylabel ("SGOT distance" )
481- plt .title ("SGOT distance vs rotation angle" )
482- plt .legend ()
481+ metrics = ["chordal" , "geodesic" , "procrustes" , "martin" ]
482+ styles = {"chordal" : "-" , "geodesic" : "--" , "procrustes" : "-." , "martin" : ":" }
483+ rotation_results = {m : [] for m in metrics }
484+
485+ for theta in thetas :
486+ Z = augment (generate_data (time , tau_0 , freq_0 , theta ), sgot_window )
487+ B , B_spec = estimator (Z [:- 1 ], Z [1 :])
488+ D = np .log (B_spec ["eig_val" ]) * fs
489+ L = B_spec ["eig_vec_left" ]
490+ R = B_spec ["eig_vec_right" ]
491+ for m in metrics :
492+ rotation_results [m ].append (
493+ sgot_metric (
494+ D_0_sgot , R_0_sgot , L_0_sgot , D , R , L , eta = 0.9 , grassmann_metric = m
495+ )
496+ )
497+
498+ fig , ax = plt .subplots (figsize = (7 , 4 ))
499+ for m in metrics :
500+ ax .plot (thetas , rotation_results [m ], styles [m ], label = m , linewidth = 1.8 )
501+ ax .axvline (
502+ theta_0 ,
503+ color = "gray" ,
504+ linestyle = "--" ,
505+ linewidth = 0.8 ,
506+ label = r"$\theta_0 = \pi/4$ (reference)" ,
507+ )
508+ ax .set_xlabel (r"Rotation angle $\theta$ (rad)" )
509+ ax .set_ylabel (r"$d_S$" )
510+ ax .set_title ("SGOT distance vs. rotation angle across Grassmannian metrics" )
511+ ax .legend ()
512+ fig .tight_layout ()
483513plt .show ()
484514
485515# %%
@@ -501,38 +531,38 @@ def estimator(X, Y, rank=4):
501531# distance changes as a function of the perturbed frequency :math:`\omega_2'`.
502532
503533omegas = np .linspace (0.5 , 3.0 , 21 )
504- methods = ["chordal" , "martin" , "geodesic" , "procrustes" ]
505- scores_omega = []
506- theta = theta_0
534+ frequency_scores = {m : [] for m in metrics }
507535
508- eta_fixed = 0.9
509536for omega in omegas :
510- freq_1 = np .array ([freq_0 [0 ], omega ])
511- traj = generate_data (time , tau_0 , freq_1 , theta )
512- Z = augment (traj , 4 )
513- X = Z [:- 1 ]
514- Y = Z [1 :]
515-
516- B , B_spec = estimator (X , Y , rank = 4 )
517- D , R , L = B_spec ["eig_val" ], B_spec ["eig_vec_right" ], B_spec ["eig_vec_left" ]
518- D = np .log (D ) * fs
519-
520- row = []
521- for name in methods :
522- row .append (
523- sgot_metric (D_0 , R_0 , L_0 , D , R , L , eta = eta_fixed , grassmann_metric = name )
537+ Z = augment (
538+ generate_data (time , tau_0 , np .array ([freq_0 [0 ], omega ]), theta_0 ), sgot_window
539+ )
540+ B , B_spec = estimator (Z [:- 1 ], Z [1 :])
541+ D = np .log (B_spec ["eig_val" ]) * fs
542+ L = B_spec ["eig_vec_left" ]
543+ R = B_spec ["eig_vec_right" ]
544+ for m in metrics :
545+ frequency_scores [m ].append (
546+ sgot_metric (
547+ D_0_sgot , R_0_sgot , L_0_sgot , D , R , L , eta = 0.9 , grassmann_metric = m
548+ )
524549 )
525- scores_omega .append (row )
526-
527- scores_omega = np .array (scores_omega )
528- plt .figure (figsize = (8 , 5 ))
529- for i , name in enumerate (methods ):
530- plt .plot (omegas , scores_omega [:, i ], label = name )
531550
532- plt .xlabel ("omega" )
533- plt .ylabel ("SGOT distance" )
534- plt .title ("SGOT distance vs omega" )
535- plt .legend ()
551+ fig , ax = plt .subplots (figsize = (7 , 4 ))
552+ for m in metrics :
553+ ax .plot (omegas , frequency_scores [m ], styles [m ], label = m , linewidth = 1.8 )
554+ ax .axvline (
555+ freq_0 [1 ],
556+ color = "gray" ,
557+ linestyle = "--" ,
558+ linewidth = 0.8 ,
559+ label = r"$\omega_2 = 2.0$ Hz (reference)" ,
560+ )
561+ ax .set_xlabel (r"Frequency $\omega_2'$ (Hz)" )
562+ ax .set_ylabel (r"$d_S$" )
563+ ax .set_title ("SGOT distance vs. frequency across Grassmannian metrics" )
564+ ax .legend ()
565+ fig .tight_layout ()
536566plt .show ()
537567
538568# %%
@@ -553,47 +583,28 @@ def estimator(X, Y, rank=4):
553583# In this way, both modes share the same modified decay parameter
554584# :math:`\tau`, allowing us to isolate the influence of dissipation on the SGOT
555585# distance.
556- decays = np .linspace (0.1 , 3.0 , 20 ) # adjust range as needed
557- methods = ["chordal" , "martin" , "geodesic" , "procrustes" ]
558- scores_decay = []
559- theta = theta_0
560-
561- for tau in decays :
562- freq_1 = np .array ([freq_0 [0 ], recovered_freqs [1 ]])
563- tau_1 = np .array ([tau , tau ]) # or whatever structure your generator expects
564-
565- traj = generate_data (time , tau_1 , freq_1 , theta )
566- Z = augment (traj , 4 )
567- X = Z [:- 1 ]
568- Y = Z [1 :]
569-
570- B , B_spec = estimator (X , Y , rank = 4 )
571- D , R , L = B_spec ["eig_val" ], B_spec ["eig_vec_right" ], B_spec ["eig_vec_left" ]
572- D = np .log (D ) * fs
573-
574- row = []
575- for name in methods :
576- row .append (
586+ taus = np .linspace (0.1 , 3.0 , 21 )
587+ decay_scores = {m : [] for m in metrics }
588+
589+ for tau in taus :
590+ Z = augment (generate_data (time , np .array ([tau , tau ]), freq_0 , theta_0 ), sgot_window )
591+ B , B_spec = estimator (Z [:- 1 ], Z [1 :])
592+ D = np .log (B_spec ["eig_val" ]) * fs
593+ L = B_spec ["eig_vec_left" ]
594+ R = B_spec ["eig_vec_right" ]
595+ for m in metrics :
596+ decay_scores [m ].append (
577597 sgot_metric (
578- D_0 ,
579- R_0 ,
580- L_0 ,
581- D ,
582- R ,
583- L ,
584- eta = 0.9 , # keep eta fixed here
585- grassmann_metric = name ,
598+ D_0_sgot , R_0_sgot , L_0_sgot , D , R , L , eta = 0.9 , grassmann_metric = m
586599 )
587600 )
588- scores_decay .append (row )
589601
590- scores_decay = np .array (scores_decay )
591- plt .figure (figsize = (8 , 5 ))
592- for i , name in enumerate (methods ):
593- plt .plot (decays , scores_decay [:, i ], label = name )
594-
595- plt .xlabel ("decay" )
596- plt .ylabel ("SGOT distance" )
597- plt .title ("SGOT distance vs decay" )
598- plt .legend ()
602+ fig , ax = plt .subplots (figsize = (7 , 4 ))
603+ for m in metrics :
604+ ax .plot (taus , decay_scores [m ], styles [m ], label = m , linewidth = 1.8 )
605+ ax .set_xlabel (r"Decay rate $\tau$" )
606+ ax .set_ylabel (r"$d_S$" )
607+ ax .set_title ("SGOT distance vs. decay across Grassmannian metrics" )
608+ ax .legend ()
609+ fig .tight_layout ()
599610plt .show ()
0 commit comments