-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmultiscale_joint_y_1_y_2_cause.m
More file actions
1823 lines (1633 loc) · 90.6 KB
/
Copy pathmultiscale_joint_y_1_y_2_cause.m
File metadata and controls
1823 lines (1633 loc) · 90.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
% Code corresponding to the multiscale atmospheric-variability model in Section
% 3.2 - "Impact of Resolving Non-Target Variables in the CIR Metrics of a
% Multiscale Model for Atmospheric Variability" of the paper "Bridging
% Prediction and Attribution: Identifying Forward and Backward Causal Influence
% Ranges Using Assimilative Causal Inference".
%
% Authors: Marios Andreou, Nan Chen.
%
% Code Information: Application of unconditional Assimilative Causal Inference
% (ACI) and of the associated forward and backward Causal Influence Range (CIR)
% metrics to a stochastic multiscale conceptual model for studying low-frequency
% atmospheric variability:
%
% dx_1/dt = a_1·x_1 - c_1·x_1^3 - x_2·(M + M_1·x_1 + M_2·x_2)
% + I_11·x_1·y_1 + I_12·x_1·y_2 + L_11·y_1 + L_12·y_2 + f_1^x(t)
% + σ_x1·dot(W_x1) + Σ_{k=1,2} (σ_yk/γ_k)·(L_1k - I_1k·x_1)·dot(W_yk),
% dx_2/dt = -c_2·x_2 + x_1·(M + M_1·x_1 + M_2·x_2)
% + I_21·x_2·y_1 + I_22·x_2·y_2 + L_21·y_1 + L_22·y_2 + f_2^x(t)
% + σ_x2·dot(W_x2) + Σ_{k=1,2} (σ_yk/γ_k)·(L_2k - I_2k·x_2)·dot(W_yk),
% dy_1/dt = -(γ_1/ε)·y_1 - L_11·x_1 - L_21·x_2
% + N·y_2 - I_11·x_1^2 - I_21·x_2^2 + f_1^y(t)
% + (σ_y1/sqrt(ε))·dot(W_y1),
% dy_2/dt = -(γ_2/ε)·y_2 - L_12·x_1 - L_22·x_2
% - N·y_1 - I_12·x_1^2 - I_22·x_2^2 + f_2^y(t)
% + (σ_y2/sqrt(ε))·dot(W_y2).
%
% This is a reduced-order model for large-scale geophysical flow, often derived
% via a stochastic-mode reduction procedure on general circulation models; see
% works on the Majda--Timofeyev--Vanden-Eijnden (MTV) method and this paper on
% normal forms for reduced stochastic climate models:
% ➤ https://doi.org/10.1073/pnas.0900173106
% It is also a conditional Gaussian nonlinear system (CGNS) for the posterior
% (y_1,y_2) | (x_1,x_2); see Appendix G of the paper. It governs the nonlinear,
% coupled evolution between the resolved slow atmospheric/climate/environmental
% variables x = (x_1,x_2)ᵀ and the unresolved fast weather components
% y = (y_1,y_2)ᵀ. Quadratic nonlinearities preserve energy and produce
% intermittent instabilities and non-Gaussian marginal/joint PDFs, while the
% linear-operator dynamics in x encode multiscale structures typical of
% turbulent flows (skew-symmetric: Coriolis effects, topographic Rossby wave
% propagation; negative-definite: Stochastic stability via dissipative processes
% like surface drag), as well as oscillatory feedbacks in y. The small parameter
% 0 < ε <= 1 separates the slow and fast timescales. The system also includes
% scale-dependent time-varying forcings, with large-scale feedbacks representing
% slow external inputs such as decadal oscillations, while small-scale forcings
% capture faster intraseasonal variabilities and/or interannual phase-locking.
% Finally, correlated additive and multiplicative (CAM) noise is added to the
% system; the multiplicative noise stems from the nonlinear advection of the
% resolved scales by the unresolved scales, whereas the additive noise part
% stems from the linear operator obtained by linearising the equations of motion
% around the climatological background state. This CAM noise is a common feature
% added to conceptual models for efficient and effective description of
% non-Gaussian low-frequency climate variability in the presence of unresolved
% processes (e.g. heavy-tailed PDFs, skewness, and intermittency). This code
% uses the same model parameter values as those cited in the paper.
%
% In this script, ACI and its forward and backward CIR metrics are employed to
% study the unconditional, joint causal relationship (y_1(t),y_2(t)) → (x_1,x_2)
% over time t∈[0,T]. Therefore, the weather vector y is treated as the latent
% candidate cause, while the atmospheric vector x is the observed effect. Unlike
% the other scripts in this codebase, which employ conditional ACI, no
% observational channel is resolved or masked in this implementation, with both
% x_1 and x_2 contributing to the Bayesian update of y(t). This is the
% unconditional ACI baseline used in the paper before conditioning on the
% ancillary/non-target variables to resolve and study the marginal links or
% contributions in the ACI and CIRs, such as the relationships
% y_1 → x_1 | (x_2,y_2) and y_2 → x_2 | (x_1,y_1).
%
% In the paper, the bivariate link (y_1(t),y_2(t)) → (x_1,x_2) is used to
% quantify the aggregate or joint weather-to-atmosphere causal influence before
% "decomposing" it to its marginal constituents through the conditional ACI and
% CIR metrics of:
% y_1 → x_1 | (x_2,y_2),
% y_2 → x_2 | (x_1,y_1).
% Note that, based on the chosen model parameter values from the paper, y_2 and
% x_1 do not causally/dynamically interact with each other explicitly in the
% system, but only do so through confounding/spurious associations via the other
% variables (and likewise for y_1 and x_2); therefore, the causal properties of
% the joint link (ACI and CIRs) are roughly decomposable to those of the two
% aforementioned conditional marginal relationships. Forward CIR analysis of
% this joint relationship assesses how long the current fast weather state
% remains predictively informative for future atmospheric evolution, while the
% backward CIR attributes current atmospheric excursions or bursts to past
% weather states. This script produces the analyses associated with Panels
% (e)-(f) of Figure 7 of the paper.
%
% This script also implements additional studies that do not appear in the paper
% for brevity, but are included in the public repository for transparency and
% reproducibility:
%
% (i) Optional calculation of the exact objective forward CIR using the
% ε-average definition of the subjective forward CIR, together with
% an exact-vs-approximate objective forward CIR comparison plot.
% (ii) Optional calculation of the exact objective backward CIR using the
% ε-average definition of the subjective backward CIR, together with
% an exact-vs-approximate objective backward CIR comparison plot.
% (iii) Optional calculation of the normalised forward CIR metric δ^f(T';t)
% and its heatmap over natural time t and lagged observational time
% after t, T'-t.
% (iv) Optional calculation of the normalised complete backward CIR metric,
% lim_{T'→T^-} δ^b(t;T'), using the discrete-time adaptive-lag online
% smoother approximation from Appendix G of the paper, and its heatmap
% over observational time T and backward lag T-t. Here T denotes the
% current online-smoother observation time, not the terminal
% simulation time.
%
% The exact objective forward and backward CIRs are defined in Eqs. (10) and
% (18) of the paper, respectively, as ε-average integrals of the corresponding
% subjective CIRs, while their computationally efficient approximations are
% correspondingly defined in Eqs. (12) and (20), as time integrals of the
% corresponding CIR relative-entropy-based metrics (specifically, their L1-to-L∞
% ratio). These approximations become exact under the monotonicity conditions
% described in the paper for the respective CIR metrics, which hold to a
% satisfactory degree for general complex dynamical systems. In general, they
% act as lower- and upper-bounding approximations to the exact CIRs,
% respectively.
%
% Written and tested in MATLAB R2024b.
%
% MATLAB Toolbox and M-file Requirements:
%
% Code used to obtain the required m-file scripts and MATLAB toolboxes:
% [fList, pList] = matlab.codetools.requiredFilesAndProducts('multiscale_joint_y_1_y_2_cause.m');
%
% M-file Scripts:
% ➤ multiscale_joint_y_1_y_2_cause.m
% ➤ progress_bar.m
% ➤ simps.m (https://www.mathworks.com/matlabcentral/fileexchange/25754-simpson-s-rule-for-numerical-integration)
% ➤ shared_helper.m
%
% Data:
% ➤ N/A - The script utilises synthetic observations simulated by the model
%
% Toolboxes:
% ➤ Statistics and Machine Learning Toolbox
%
% GitHub Repository: https://github.com/marandmath/FBCIR_code
% MIT License Information: https://github.com/marandmath/FBCIR_code/blob/main/LICENSE
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% USER CONTROLS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Fixing the random seed for reproducibility across each simulation.
rng(444)
% Total number of time steps within the given time interval; not to be confused
% with the feedback parameter N in the model equations, corresponding to the
% N_cross variable in the code, that controls the oscillatory cross-coupling
% between the fast weather variables.
N = 20000;
% Numerical integration time step.
dt = 0.005;
% Total simulation time.
T = N*dt;
% Time grid (in natural time) for the time-dependent forcings.
time = (0:N)*dt;
% Main timescale separation parameter. The value used in the paper is ε = 0.1.
epsilon = 0.1;
% Plotting time window for the figures.
time_start_plot = 50;
time_end_plot = T;
% This threshold is used to treat small relative-entropy-based CIR-metric maxima
% at each time as non-causal so as to avoid operationally inflated objective
% forward and backward CIR lengths (both exact and approximate), as well as
% optional normalised CIR-metric heatmaps, caused by normalising floating-point
% noise when dividing by the L∞ norm of the respective relative-entropy-based
% CIR metric.
RE_metric_threshold = 1e-8;
% Optional repository diagnostics; see the script's header for details. Set any
% flag to false to skip only that extra calculation/plot. The main simulation,
% ACI metric calculation, approximate objective forward and backward CIR
% calculations, and plotting of the ACI and CIR figures from the paper remain
% intact.
calculate_and_plot_exact_objective_forward_CIR = true;
calculate_and_plot_exact_objective_backward_CIR = true;
calculate_and_plot_normalised_forward_CIR_metric = true;
calculate_and_plot_normalised_backward_CIR_metric = true;
% Optional normalised CIR metric (forward and complete backward CIR metrics)
% heatmap display-lag windows for optimal plotting. Defining them before the CIR
% metric loops so that we retain only the necessary displayed metric rows for
% this optionally plotted heatmap instead of a full O(N²) cache, without
% changing the actual mathematical logic or displayed metric values.
% The smaller current-time-centred lagged observational time window for optimal
% plotting (avoiding O(N²) memory-heavy plots) of the normalised forward CIR
% metric heatmap,
% δ^f(T';t)/max{δ^f(T';t): T'∈[t,T]},
% over the natural time t in the plotting window [time_start_plot,time_end_plot]
% and lagged observational time after t, T'-t ∈ [0,lag_obs_time_end_plot],
% where:
% lag_obs_time_end_plot = T'-t ∈ [0,time_end_plot-time_start_plot].
% Note that, by definition of the forward CIR metric, T'∈[t,T] at each t.
lag_obs_time_end_plot = 5;
% The smaller natural-time-centred, current-observational-time-based backward
% lag window for optimal plotting (avoiding O(N²) memory-heavy plots) of the
% normalised complete backward CIR metric heatmap,
% lim_{T'→T^-} δ^b(t;T')/max_t{lim_{T'→T^-} δ^b(t;T')},
% over the observational time T in the plotting window [time_start_plot,
% time_end_plot] and backward lag before T, T-t ∈ [0,back_lag_time_end_plot],
% where:
% back_lag_time_end_plot = T-t ∈ [0,time_end_plot-time_start_plot].
% Note that the t-maximum in the normalised complete backward CIR metric is
% taken over the retained natural-time window (shortened by lag_bound; see
% "BACKWARD CIR" section of the script below), while still considering the
% plotting window of [time_start_plot,time_end_plot], i.e.
% t ∈ [T-lag_bound,T]
% = [min{time_start_plot, time_end_plot-lag_bound},time_end_plot]
% = [{earliest retained t closer to t=0},time_end_plot] ⊆ [time_start_plot,time_end_plot].
% Note that, by definition of the (complete) backward CIR metric through a fixed
% but arbitrary observational time T, T-t is the backward lag before the current
% observational time T.
back_lag_time_end_plot = 5;
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% MODEL SETUP %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Dimension of the observed effect and unobserved candidate-cause vectors.
obs_dim = 2;
state_dim = 2;
% state_dim-dimensional identity matrix for quick reference in the algorithms.
eye_state = eye(state_dim);
% Observed slow atmospheric/effect variables: x = (x_1,x_2)ᵀ.
x = zeros(obs_dim, N+1);
% Unobserved fast weather/candidate-cause variables: y = (y_1,y_2)ᵀ.
y = zeros(state_dim, N+1);
% Slow atmospheric self-interaction and energy-conserving feedback parameters.
a_1 = 1;
c_1 = 1/3;
M = 0.5;
M_1 = 0.5;
c_2 = 0.4;
M_2 = -1.5;
% Fast weather damping and oscillatory cross-coupling parameters.
gamma_1 = 0.5;
N_cross = 4;
gamma_2 = 1.2;
% Quadratic interactions-operator feedback matrix I.
I = zeros(2, 2);
I(1, 1) = 0.6;
I(1, 2) = 0;
I(2, 1) = 0;
I(2, 2) = 2;
% Linear-operator feedback matrix L.
L = zeros(2, 2);
L(1, 1) = 1;
L(1, 2) = 0;
L(2, 1) = 0;
L(2, 2) = 1.5;
% Large-scale forcing in x_1.
fx_1 = 0;
% Large-scale forcing in x_2.
fx2_mean = 4;
fx2_amplitude = 0.5;
fx2_period = 36;
fx_2 = zeros(1, N+1);
fx_2(1) = fx2_mean + fx2_amplitude*sin(2*pi*time(1)/fx2_period);
% Small-scale weather forcings in y_1 and y_2.
fy_1 = 1;
fy_2 = -1;
% Additive/state-independent noise/diffusion amplitudes in x and y.
sigma_x1 = 0.15;
sigma_x2 = 0.3;
sigma_y1 = 1;
sigma_y2 = 2;
% CGNS representation with y latent and x observed; the "^x" and "^y"
% superscripts used in the comments denote the observable and unobservable
% processes, respectively, and are used solely for consistency with the notation
% in Appendix G of the paper.
% dx = [L^x(t,x)·y + f^x(t,x)]dt + S_1^x(t,x)·dW_x + S_2^x(t,x)·dW_y,
% dy = [L^y(t,x)·y + f^y(t,x)]dt + S_1^y(t,x)·dW_x + S_2^y(t,x)·dW_y.
% Observable coefficient matrix: Feedback of y in x.
L_x = zeros(obs_dim, state_dim, N+1);
% Forcing in the observable process.
f_x = zeros(obs_dim, N+1);
% Noise feedback matrix corresponding to the independent x-noise channel in the
% observable process x, i.e. the (1,1)-diagonal block of the full diffusion
% matrix of the joint process (x,y).
Sx_1 = [
sigma_x1, 0;
0, sigma_x2
];
% Noise feedback matrix corresponding to the y-noise channel in the observable
% process x, producing the CAM noise in the (1,2) and (2,1) off-diagonal blocks
% of the full diffusion matrix of the joint process (x,y).
Sx_2 = zeros(obs_dim, state_dim, N+1);
% Unobservable coefficient matrix: Feedback of y in y.
L_y = [
-gamma_1/epsilon, N_cross;
-N_cross, -gamma_2/epsilon
];
% Forcing in the unobservable process.
f_y = zeros(state_dim, N+1);
% Noise feedback matrix corresponding to the x-noise channel in the unobservable
% process y, producing the CAM noise in the (1,2) and (2,1) off-diagonal blocks
% of the full diffusion matrix of the joint process (x,y).
Sy_1 = zeros(state_dim, obs_dim);
% Noise feedback matrix corresponding to the independent y-noise channel in the
% unobservable process y, i.e. the (2,2)-diagonal block of the full diffusion
% matrix of the joint process (x,y).
Sy_2 = [
sigma_y1/sqrt(epsilon), 0;
0, sigma_y2/sqrt(epsilon)
];
% Initial conditions for x = (x_1,x_2)ᵀ and y = (y_1,y_2)ᵀ; zero initial
% conditions are also a valid choice for this model.
x(:, 1) = [
0;
fx_2(1)/c_2
];
y(:, 1) = [
fy_1/(gamma_1/epsilon);
fy_2/(gamma_2/epsilon)
];
% Initiating the time-dependent model components.
L_x(:, :, 1) = [
I(1, 1)*x(1, 1) + L(1, 1), I(1, 2)*x(1, 1) + L(1, 2);
I(2, 1)*x(2, 1) + L(2, 1), I(2, 2)*x(2, 1) + L(2, 2)
];
f_x(:, 1) = [
a_1*x(1, 1) - c_1*x(1, 1)^3 - x(2, 1)*(M + M_1*x(1, 1) + M_2*x(2, 1)) + fx_1;
-c_2*x(2, 1) + x(1, 1)*(M + M_1*x(1, 1) + M_2*x(2, 1)) + fx_2(1)
];
Sx_2(:, :, 1) = [
sigma_y1*(L(1, 1) - I(1, 1)*x(1, 1))/gamma_1, sigma_y2*(L(1, 2) - I(1, 2)*x(1, 1))/gamma_2;
sigma_y1*(L(2, 1) - I(2, 1)*x(2, 1))/gamma_1, sigma_y2*(L(2, 2) - I(2, 2)*x(2, 1))/gamma_2
];
f_y(:, 1) = [
-L(1, 1)*x(1, 1) - L(2, 1)*x(2, 1) - I(1, 1)*x(1, 1)^2 - I(2, 1)*x(2, 1)^2 + fy_1;
-L(1, 2)*x(1, 1) - L(2, 2)*x(2, 1) - I(1, 2)*x(1, 1)^2 - I(2, 2)*x(2, 1)^2 + fy_2
];
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%% GENERATING THE TRUE SIGNALS %%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Used for the text-based progress bar.
start_time = tic;
for j = 2:N+1
% Text-based progress bar.
progress_bar('Simulation of the Multiscale Model', j-1, N, start_time);
% Wiener increments. For consistency with the other multiscale scripts in
% terms of the random Gaussian noise sampling during simulation, note the
% sampling order of x_1 -> x_2 -> y_1 -> y_2, which we conform to
% throughout. This is important for a consistent comparison of the ACI and
% CIR metrics on the same set of simulated true signals and across the
% multiscale assimilative causal relationships studied in the paper.
dW_x = sqrt(dt)*randn(obs_dim, 1);
dW_y = sqrt(dt)*randn(state_dim, 1);
% Euler-Maruyama update of the state variables based on the dynamical model.
x(:, j) = x(:, j-1) + (L_x(:, :, j-1)*y(:, j-1) + f_x(:, j-1))*dt ...
+ Sx_1*dW_x + Sx_2(:, :, j-1)*dW_y;
y(:, j) = y(:, j-1) + (L_y*y(:, j-1) + f_y(:, j-1))*dt ...
+ Sy_1*dW_x + Sy_2*dW_y;
% Updating the time-dependent model components.
L_x(:, :, j) = [
I(1, 1)*x(1, j) + L(1, 1), I(1, 2)*x(1, j) + L(1, 2);
I(2, 1)*x(2, j) + L(2, 1), I(2, 2)*x(2, j) + L(2, 2)
];
fx_2(j) = fx2_mean + fx2_amplitude*sin(2*pi*time(j)/fx2_period);
f_x(:, j) = [
a_1*x(1, j) - c_1*x(1, j)^3 - x(2, j)*(M + M_1*x(1, j) + M_2*x(2, j)) + fx_1;
-c_2*x(2, j) + x(1, j)*(M + M_1*x(1, j) + M_2*x(2, j)) + fx_2(j)
];
Sx_2(:, :, j) = [
sigma_y1*(L(1, 1) - I(1, 1)*x(1, j))/gamma_1, sigma_y2*(L(1, 2) - I(1, 2)*x(1, j))/gamma_2;
sigma_y1*(L(2, 1) - I(2, 1)*x(2, j))/gamma_1, sigma_y2*(L(2, 2) - I(2, 2)*x(2, j))/gamma_2
];
f_y(:, j) = [
-L(1, 1)*x(1, j) - L(2, 1)*x(2, j) - I(1, 1)*x(1, j)^2 - I(2, 1)*x(2, j)^2 + fy_1;
-L(1, 2)*x(1, j) - L(2, 2)*x(2, j) - I(1, 2)*x(1, j)^2 - I(2, 2)*x(2, j)^2 + fy_2
];
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%% PLOTTING MODEL DIAGNOSTICS %%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
plot_idx = round(time_start_plot/dt)+1:round(time_end_plot/dt)+1;
plot_time = (plot_idx - 1)*dt;
% (x,y)-phase-space 2D projection plots; these plots are indicators of how the
% multiscale, nonlinear interactions between x and y produce non-Gaussian
% marginal and joint PDFs.
figure('WindowState', 'maximized');
subplot(3, 2, 1)
plot(x(1, plot_idx), y(1, plot_idx), 'k', LineWidth=1)
xlabel('x_1')
ylabel('y_1')
title('Phase Plot (x_1,y_1)')
box on
grid on
fontsize(16, 'points')
subplot(3, 2, 2)
plot(x(2, plot_idx), y(1, plot_idx), 'k', LineWidth=1)
xlabel('x_2')
ylabel('y_1')
title('Phase Plot (x_2,y_1)')
box on
grid on
fontsize(16, 'points')
subplot(3, 2, 3)
plot(x(1, plot_idx), y(2, plot_idx), 'k', LineWidth=1)
xlabel('x_1')
ylabel('y_2')
title('Phase Plot (x_1,y_2)')
box on
grid on
fontsize(16, 'points')
subplot(3, 2, 4)
plot(x(2, plot_idx), y(2, plot_idx), 'k', LineWidth=1)
xlabel('x_2')
ylabel('y_2')
title('Phase Plot (x_2,y_2)')
box on
grid on
fontsize(16, 'points')
subplot(3, 2, 5)
plot(x(1, plot_idx), x(2, plot_idx), 'k', LineWidth=1)
xlabel('x_1')
ylabel('x_2')
title('Phase Plot (x_1,x_2)')
box on
grid on
fontsize(16, 'points')
subplot(3, 2, 6)
plot(y(1, plot_idx), y(2, plot_idx), 'k', LineWidth=1)
xlabel('y_1')
ylabel('y_2')
title('Phase Plot (y_1,y_2)')
box on
grid on
fontsize(16, 'points')
% Time series and time-averaged marginal PDFs of x_1, x_2, y_1, and y_2.
figure('WindowState', 'maximized');
subplot(4, 3, [1, 2])
plot(plot_time, x(1, plot_idx), 'k', LineWidth=1.5)
ylabel('x_1')
title('Time Series of x_1')
box on
grid on
fontsize(16, 'points')
subplot(4, 3, 3)
xx1 = linspace(min(x(1, plot_idx)), max(x(1, plot_idx)), 250);
tavg_pdf_x1 = ksdensity(x(1, plot_idx), xx1);
plot(xx1, tavg_pdf_x1, 'k', LineWidth=1.5)
hold on
mean_x1 = mean(x(1, plot_idx));
std_x1 = std(x(1, plot_idx));
plot(xx1, normpdf(xx1, mean_x1, std_x1), 'm--', LineWidth=1.5)
xlabel('x_1')
ylabel('p(x_1)')
title('PDF of x_1')
legend('Empirical', 'Gaussian Fit', Location='best')
box on
grid on
fontsize(16, 'points')
subplot(4, 3, [4, 5])
plot(plot_time, x(2, plot_idx), 'k', LineWidth=1.5)
ylabel('x_2')
title('Time Series of x_2')
box on
grid on
fontsize(16, 'points')
subplot(4, 3, 6)
xx2 = linspace(min(x(2, plot_idx)), max(x(2, plot_idx)), 250);
tavg_pdf_x2 = ksdensity(x(2, plot_idx), xx2);
plot(xx2, tavg_pdf_x2, 'k', LineWidth=1.5)
hold on
mean_x2 = mean(x(2, plot_idx));
std_x2 = std(x(2, plot_idx));
plot(xx2, normpdf(xx2, mean_x2, std_x2), 'm--', LineWidth=1.5)
xlabel('x_2')
ylabel('p(x_2)')
title('PDF of x_2')
box on
grid on
fontsize(16, 'points')
subplot(4, 3, [7, 8])
plot(plot_time, y(1, plot_idx), 'b', LineWidth=1.5)
ylabel('y_1')
title('Time Series of y_1')
box on
grid on
fontsize(16, 'points')
subplot(4, 3, 9)
yy1 = linspace(min(y(1, plot_idx)), max(y(1, plot_idx)), 250);
tavg_pdf_y1 = ksdensity(y(1, plot_idx), yy1);
plot(yy1, tavg_pdf_y1, 'b', LineWidth=1.5)
hold on
mean_y1 = mean(y(1, plot_idx));
std_y1 = std(y(1, plot_idx));
plot(yy1, normpdf(yy1, mean_y1, std_y1), 'm--', LineWidth=1.5)
xlabel('y_1')
ylabel('p(y_1)')
title('PDF of y_1')
box on
grid on
fontsize(16, 'points')
subplot(4, 3, [10, 11])
plot(plot_time, y(2, plot_idx), 'b', LineWidth=1.5)
xlabel('t')
ylabel('y_2')
title('Time Series of y_2')
box on
grid on
fontsize(16, 'points')
subplot(4, 3, 12)
yy2 = linspace(min(y(2, plot_idx)), max(y(2, plot_idx)), 250);
tavg_pdf_y2 = ksdensity(y(2, plot_idx), yy2);
plot(yy2, tavg_pdf_y2, 'b', LineWidth=1.5)
hold on
mean_y2 = mean(y(2, plot_idx));
std_y2 = std(y(2, plot_idx));
plot(yy2, normpdf(yy2, mean_y2, std_y2), 'm--', LineWidth=1.5)
xlabel('y_2')
ylabel('p(y_2)')
title('PDF of y_2')
box on
grid on
fontsize(16, 'points')
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% FILTERING %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Inverse of the observational noise Grammian matrix defining the weight of the
% observational influence on the Bayesian update of the unobserved vector y(t).
% Unlike the conditional ACI and CIR scripts, this unconditional joint-cause
% analysis uses the full precision matrix, so both x_1 and x_2 directly reduce
% the posterior uncertainty of y(t).
S_xoS_x_inv = zeros(obs_dim, obs_dim, N+1);
S_xoS_x_inv(:, :, 1) = inv(Sx_1*Sx_1' + Sx_2(:, :, 1)*Sx_2(:, :, 1)');
% Cross-Grammian (noise cross-interaction) between the latent and observed
% process noise feedbacks.
S_yoS_x = zeros(state_dim, obs_dim, N+1);
S_yoS_x(:, :, 1) = Sy_1*Sx_1' + Sy_2*Sx_2(:, :, 1)';
% Grammian of the unobservable process noise feedback.
S_yoS_y = Sy_1*Sy_1' + Sy_2*Sy_2';
% Posterior filter mean of the latent vector y.
filter_mean = zeros(state_dim, N+1);
% Initial value of the posterior filter mean.
filter_mean(:, 1) = y(:, 1); mu0 = filter_mean(:, 1);
% Posterior filter covariance matrix of the latent vector y.
filter_cov = zeros(state_dim, state_dim, N+1);
% Initial value of the posterior filter covariance. Choosing a positive definite
% matrix to preserve the positive-definiteness of the posterior covariance
% matrices over time.
filter_cov(:, :, 1) = 0.1*eye_state; R0 = filter_cov(:, :, 1);
% Used for the text-based progress bar.
start_time = tic;
for j = 2:N+1
% Text-based progress bar.
progress_bar('Filter Algorithm', j-1, N, start_time);
dx = x(:, j) - x(:, j-1);
filter_gain_aux = S_yoS_x(:, :, j-1) + R0*L_x(:, :, j-1)';
% Update the posterior filter mean and posterior filter covariance using the
% optimal nonlinear filter state estimation equations for CGNSs; see Theorem
% 2.1 in Section 2.1.2 of the Supplementary Information in the original ACI
% paper, https://doi.org/10.1038/s41467-026-68568-0.
mu = mu0 + (L_y*mu0 + f_y(:, j-1))*dt ...
+ filter_gain_aux*S_xoS_x_inv(:, :, j-1)*(dx - (L_x(:, :, j-1)*mu0 + f_x(:, j-1))*dt);
R = R0 + (L_y*R0 + R0*L_y' + S_yoS_y)*dt ...
- (filter_gain_aux*S_xoS_x_inv(:, :, j-1)*filter_gain_aux')*dt;
filter_mean(:, j) = mu; mu0 = filter_mean(:, j);
filter_cov(:, :, j) = R; R0 = filter_cov(:, :, j);
S_xoS_x_inv(:, :, j) = inv(Sx_1*Sx_1' + Sx_2(:, :, j)*Sx_2(:, :, j)');
S_yoS_x(:, :, j) = Sy_1*Sx_1' + Sy_2*Sx_2(:, :, j)';
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% SMOOTHING %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Posterior smoother mean of the latent vector y.
smoother_mean = zeros(state_dim, N+1);
% Posterior smoother covariance matrix of the latent vector y.
smoother_cov = zeros(state_dim, state_dim, N+1);
% Smoother runs backwards: "Initial" values of the smoother statistics (i.e. at
% the last time instant T) are the corresponding posterior filter statistics.
smoother_mean(:, N+1) = filter_mean(:, N+1); muT = smoother_mean(:, N+1);
smoother_cov(:, :, N+1) = filter_cov(:, :, N+1); RT = smoother_cov(:, :, N+1);
% Auxiliary matrices used for the calculation of the online smoother for this
% CGNS. The online smoother is required for the calculation of the subjective
% and objective forward and backward CIR lengths of (y_1(t),y_2(t)) → (x_1,x_2)
% at each time t∈[0,T]. Notation used is consistent with that of the original
% CGNS online smoother work:
% ➤ https://doi.org/10.1007/s00332-026-10271-x
% and the accompanying martingale-free introduction to CGNSs paper:
% ➤ https://doi.org/10.3390/e27010002
E_j_matrices = zeros(state_dim, state_dim, N+1);
F_j_matrices = zeros(state_dim, obs_dim, N+1);
G_x_j = L_x(:, :, N+1) + S_yoS_x(:, :, N+1)'/filter_cov(:, :, N+1);
G_y_j = L_y + S_yoS_y/filter_cov(:, :, N+1);
C_jj = eye_state - G_y_j*dt;
H_j = filter_cov(:, :, N+1)\(L_y*filter_cov(:, :, N+1) + filter_cov(:, :, N+1)*L_y' + S_yoS_y);
K_j = S_xoS_x_inv(:, :, N+1)*G_x_j;
E_j_matrices(:, :, N+1) = C_jj + S_yoS_x(:, :, N+1)*K_j*dt;
F_j_matrices(:, :, N+1) = -filter_cov(:, :, N+1) ...
*(K_j' + (G_x_j'*K_j*filter_cov(:, :, N+1)*K_j' - filter_cov(:, :, N+1)\H_j'*filter_cov(:, :, N+1)*K_j' + L_y'*K_j')*dt ...
- L_x(:, :, N+1)'*(S_xoS_x_inv(:, :, N+1) + K_j*filter_cov(:, :, N+1)*K_j'*dt));
% Used for the text-based progress bar.
start_time = tic;
for j = N:-1:1
% Text-based progress bar.
progress_bar('Smoother Algorithm', N-j+1, N, start_time);
% Calculation of the online smoother auxiliary matrices.
G_x_j = L_x(:, :, j) + S_yoS_x(:, :, j)'/filter_cov(:, :, j);
G_y_j = L_y + S_yoS_y/filter_cov(:, :, j);
C_jj = eye_state - G_y_j*dt;
H_j = filter_cov(:, :, j)\(L_y*filter_cov(:, :, j) + filter_cov(:, :, j)*L_y' + S_yoS_y);
K_j = S_xoS_x_inv(:, :, j)*G_x_j;
E_j_matrices(:, :, j) = C_jj + S_yoS_x(:, :, j)*K_j*dt;
F_j_matrices(:, :, j) = -filter_cov(:, :, j) ...
*(K_j' + (G_x_j'*K_j*filter_cov(:, :, j)*K_j' - filter_cov(:, :, j)\H_j'*filter_cov(:, :, j)*K_j' + L_y'*K_j')*dt ...
- L_x(:, :, j)'*(S_xoS_x_inv(:, :, j) + K_j*filter_cov(:, :, j)*K_j'*dt));
% Auxiliary matrices for the posterior smoother mean and covariance updates.
A_j = L_y - S_yoS_x(:, :, j)*S_xoS_x_inv(:, :, j)*L_x(:, :, j);
B_j = S_yoS_y - S_yoS_x(:, :, j)*S_xoS_x_inv(:, :, j)*S_yoS_x(:, :, j)';
dx = x(:, j+1) - x(:, j);
% Update the posterior smoother mean and posterior smoother covariance using
% the optimal nonlinear smoother state estimation backward equations for
% CGNSs; see Theorem 2.2 in Section 2.1.2 of the Supplementary Information
% in the original ACI paper, https://doi.org/10.1038/s41467-026-68568-0.
mu = muT - (L_y*muT + f_y(:, j) - B_j/filter_cov(:, :, j)*(filter_mean(:, j) - muT))*dt ...
+ S_yoS_x(:, :, j)*S_xoS_x_inv(:, :, j)*(-dx + (L_x(:, :, j)*muT + f_x(:, j))*dt);
R = RT - ((A_j + B_j/filter_cov(:, :, j))*RT + RT*(A_j + B_j/filter_cov(:, :, j))' - B_j)*dt;
% Alternative and equivalent discrete-time smoother update equations for
% CGNSs using the online smoother auxiliary matrices. See the
% martingale-free introduction to CGNSs paper for more details:
% ➤ https://doi.org/10.3390/e27010002
% dx = x(:, j+1) - x(:, j);
% E_j = E_j_matrices(:, :, j);
% F_j = F_j_matrices(:, :, j);
% mu = filter_mean(:, j) ...
% + E_j*(muT - (eye_state + L_y*dt)*filter_mean(:, j) - f_y(:, j)*dt) ...
% + F_j*(dx - (L_x(:, :, j)*filter_mean(:, j) + f_x(:, j))*dt);
% R = filter_cov(:, :, j) ...
% + E_j*(RT*E_j' - (eye_state + L_y*dt)*filter_cov(:, :, j)) ...
% - F_j*L_x(:, :, j)*filter_cov(:, :, j)*dt;
smoother_mean(:, j) = mu; muT = smoother_mean(:, j);
smoother_cov(:, :, j) = R; RT = smoother_cov(:, :, j);
end
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%% PLOTTING FILTER AND SMOOTHER RESULTS %%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Time series of the posterior filter and smoother (unconditional) Gaussian
% statistics of y_1 and y_2.
figure('WindowState', 'maximized');
subplot(2, 1, 1)
hold on
plot(plot_time, y(1, plot_idx), 'b', LineWidth=1.5, DisplayName='Truth')
plot(plot_time, filter_mean(1, plot_idx), 'g', LineWidth=1.5, DisplayName='Filter')
plot(plot_time, smoother_mean(1, plot_idx), 'r', LineWidth=1.5, DisplayName='Smoother')
filter_upper = filter_mean(1, plot_idx) + 2*real(sqrt(squeeze(filter_cov(1, 1, plot_idx))))';
filter_lower = filter_mean(1, plot_idx) - 2*real(sqrt(squeeze(filter_cov(1, 1, plot_idx))))';
smoother_upper = smoother_mean(1, plot_idx) + 2*real(sqrt(squeeze(smoother_cov(1, 1, plot_idx))))';
smoother_lower = smoother_mean(1, plot_idx) - 2*real(sqrt(squeeze(smoother_cov(1, 1, plot_idx))))';
patch([plot_time, fliplr(plot_time)], [filter_lower, fliplr(filter_upper)], ...
'g', FaceAlpha=0.2, LineStyle='none', DisplayName='±2Std Filter')
patch([plot_time, fliplr(plot_time)], [smoother_lower, fliplr(smoother_upper)], ...
'r', FaceAlpha=0.2, LineStyle='none', DisplayName='±2Std Smoother')
xlabel('t')
ylabel('y_1')
title('Time Series of y_1 and of its Filter and Smoother Posterior Statistics ((y_1(t),y_2(t)) \rightarrow (x_1,x_2))')
legend(Location='best')
box on
grid on
fontsize(16, 'points')
subplot(2, 1, 2)
hold on
plot(plot_time, y(2, plot_idx), 'b', LineWidth=1.5, DisplayName='Truth')
plot(plot_time, filter_mean(2, plot_idx), 'g', LineWidth=1.5, DisplayName='Filter')
plot(plot_time, smoother_mean(2, plot_idx), 'r', LineWidth=1.5, DisplayName='Smoother')
filter_upper = filter_mean(2, plot_idx) + 2*real(sqrt(squeeze(filter_cov(2, 2, plot_idx))))';
filter_lower = filter_mean(2, plot_idx) - 2*real(sqrt(squeeze(filter_cov(2, 2, plot_idx))))';
smoother_upper = smoother_mean(2, plot_idx) + 2*real(sqrt(squeeze(smoother_cov(2, 2, plot_idx))))';
smoother_lower = smoother_mean(2, plot_idx) - 2*real(sqrt(squeeze(smoother_cov(2, 2, plot_idx))))';
patch([plot_time, fliplr(plot_time)], [filter_lower, fliplr(filter_upper)], ...
'g', FaceAlpha=0.2, LineStyle='none', DisplayName='±2Std Filter')
patch([plot_time, fliplr(plot_time)], [smoother_lower, fliplr(smoother_upper)], ...
'r', FaceAlpha=0.2, LineStyle='none', DisplayName='±2Std Smoother')
xlabel('t')
ylabel('y_2')
title('Time Series of y_2 and of its Filter and Smoother Posterior Statistics ((y_1(t),y_2(t)) \rightarrow (x_1,x_2))')
legend(Location='best')
box on
grid on
fontsize(16, 'points')
% Comparison of the time-averaged low-order statistical diagnostics for the
% posterior state estimates of y_1 and y_2 (unconditional filter and smoother);
% normalised RMSE (NRMSE) and Pearson correlation coefficient.
nrmse_filter_y1 = rmse(y(1, plot_idx), filter_mean(1, plot_idx))/std(y(1, plot_idx));
nrmse_smoother_y1 = rmse(y(1, plot_idx), smoother_mean(1, plot_idx))/std(y(1, plot_idx));
relative_nrmse_change_y1 = 100*(nrmse_filter_y1 - nrmse_smoother_y1)/nrmse_filter_y1;
fprintf('Relative percentage change in smoother NRMSE from filter NRMSE for y_1 in (y_1(t),y_2(t)) \rightarrow (x_1,x_2) = %0.2f%% (+ = decreased, better)\n', relative_nrmse_change_y1);
nrmse_filter_y2 = rmse(y(2, plot_idx), filter_mean(2, plot_idx))/std(y(2, plot_idx));
nrmse_smoother_y2 = rmse(y(2, plot_idx), smoother_mean(2, plot_idx))/std(y(2, plot_idx));
relative_nrmse_change_y2 = 100*(nrmse_filter_y2 - nrmse_smoother_y2)/nrmse_filter_y2;
fprintf('Relative percentage change in smoother NRMSE from filter NRMSE for y_2 in (y_1(t),y_2(t)) \rightarrow (x_1,x_2) = %0.2f%% (+ = decreased, better)\n', relative_nrmse_change_y2);
corr_filter_y1_matrix = corrcoef(y(1, plot_idx), filter_mean(1, plot_idx));
corr_smoother_y1_matrix = corrcoef(y(1, plot_idx), smoother_mean(1, plot_idx));
corr_filter_y1 = corr_filter_y1_matrix(1, 2);
corr_smoother_y1 = corr_smoother_y1_matrix(1, 2);
relative_corr_change_y1 = corr_smoother_y1 - corr_filter_y1;
fprintf('Difference/Δ in smoother correlation from filter correlation for y_1 in (y_1(t),y_2(t)) \rightarrow (x_1,x_2) = %0.2f (+ = increased, better)\n', relative_corr_change_y1);
corr_filter_y2_matrix = corrcoef(y(2, plot_idx), filter_mean(2, plot_idx));
corr_smoother_y2_matrix = corrcoef(y(2, plot_idx), smoother_mean(2, plot_idx));
corr_filter_y2 = corr_filter_y2_matrix(1, 2);
corr_smoother_y2 = corr_smoother_y2_matrix(1, 2);
relative_corr_change_y2 = corr_smoother_y2 - corr_filter_y2;
fprintf('Difference/Δ in smoother correlation from filter correlation for y_2 in (y_1(t),y_2(t)) \rightarrow (x_1,x_2) = %0.2f (+ = increased, better)\n', relative_corr_change_y2);
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ACI ANALYSIS %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Theoretically, since the partially-observed stochastic dynamical system
% studied in this script is a real-valued CGNS, then the posterior filter and
% smoother covariances should remain, almost surely over the space of
% observable sample paths, finite, real-valued, symmetric, and positive
% semi-definite (PSD) at all times (or positive definite (PD) at all times if
% the filter covariance at the initial time t=0 is positive definite as well):
% ➤ https://doi.org/10.3390/e27010002
% ➤ https://doi.org/10.1007/978-3-662-10028-8
% However, due to finite numerical floating-point precision and/or because of
% discretisation errors (since we solve the continuous-time CGNS posterior
% filter/smoother state estimation equations in discrete time using the
% Euler-Maruyama method and a discretisation time step of dt=Δt), then the
% produced posterior filter/smoother covariances may violate these mathematical
% properties at some time instants. This is especially true for stiff update
% equations which might require very small time steps dt=Δt and for the smoother
% covariance, which is calculated backwards in time using the inverse of the
% filter covariance, and is therefore more prone to numerical instabilities.
% Still, these violations are extremely rare and usually occur only at one or
% two time instants, while also being very negligible. Still, to avoid script
% errors of this type to terminate the execution of the code, we regularise the
% posterior filter and/or smoother covariances only when such a finite numerical
% instability makes them non-real, non-symmetric, or non-PSD, and before any of
% the relative-entropy-based ACI and CIR calculations. This is achieved through
% a finite real-symmetric PD projection via a machine-scale eigenvalue or
% variance lift on the posterior covariance only whenever it is necessary. Valid
% posterior covariances over time remain unchanged. Also, for transparency and
% as a diagnostic summary, a brief report of the instances of these corrections
% and of any non-finite values is printed to the console for the user's
% information; a non-finite covariance naturally raises a clear error because it
% cannot be regularised without an intrusive intervention to the underlying
% mathematical, theoretical, and operational logic of the ACI and CIR framework.
filter_cov = shared_helper('regularise_posterior_covariance_history', filter_cov, dt, 'Filter');
smoother_cov = shared_helper('regularise_posterior_covariance_history', smoother_cov, dt, 'Smoother');
% Calculating the ACI metric for (y_1(t),y_2(t)) → (x_1,x_2) at each time in the
% plotting window, i.e. the relative entropy from the posterior smoother to the
% filter of y(t) = (y_1(t),y_2(t))ᵀ, P(p_t^s(y|x), p_t^f(y|x)), which, due to
% the Gaussianity of these posterior distributions, it is given by the
% signal-dispersion formula; see Appendix G of the paper.
signal_smoother_filter = zeros(1, length(plot_idx));
dispersion_smoother_filter = zeros(1, length(plot_idx));
for idx = 1:length(plot_idx)
j = plot_idx(idx);
mean_diff = smoother_mean(:, j) - filter_mean(:, j);
signal_smoother_filter(idx) = 0.5*mean_diff'*(filter_cov(:, :, j) \ mean_diff);
cov_ratio_smoother_filter = smoother_cov(:, :, j)/filter_cov(:, :, j);
% Alternative numerical safeguards due to the multidimensional latent causal
% space: If covariance roundoff makes the log-det term unstable use
% log(abs(det(...))) and max(real(...), 0) clamping on the final
% signal-dispersion value. These are numerical stabilisers only, not part
% of the mathematical Gaussian relative entropy formula used here.
dispersion_smoother_filter(idx) = 0.5*(trace(cov_ratio_smoother_filter) - log(det(cov_ratio_smoother_filter)) - state_dim);
end
ACI_metric = signal_smoother_filter + dispersion_smoother_filter;
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% FORWARD CIR %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Implementation of the fixed-lag online smoother for CGNSs; see Appendix G of
% the paper and original CGNS online smoother paper for the theoretical and
% implementation details:
% ➤ https://doi.org/10.1007/s00332-026-10271-x
% The fixed-lag online smoother distributions, p(y(t_j)|x(s<=t_n)), are needed
% to evaluate the forward CIR metric (and by extension the subjective and
% objective forward CIRs) for (y_1(t),y_2(t)) → (x_1,x_2) as a function of the
% lagged observational time t_n<=T at each fixed natural time t_j. In the
% following implementation, the relative entropy from the complete smoother
% distribution p(y(t_j)|x(s<=t_N)) to the lagged smoother distribution
% p(y(t_j)|x(s<=t_n)) is calculated at each natural time t_j for all lagged
% observational times t_j<=t_n<=t_N=T,
% P^j_n := P(p(y(t_j)|x(s<=t_N)), p(y(t_j)|x(s<=t_n))),
% where both posterior distributions are Gaussian for this CGNS. See Appendix G
% of the paper for the background, notations, and details.
% The fixed-lag parameter is set equal to the total number of observations,
% N = ⌈T/Δt⌉, such that at each time instant the full backward algorithm is
% carried out. This is because each unconditional online smoother distribution,
% p(y(t_j)|x(s<=t_n)), is needed for the calculation of the subjective and
% objective forward CIRs; see Appendix G of the paper. (N+1 due to the initial
% condition.)
fixed_lag = N+1;
% Saving the online smoother mean, online smoother covariance matrices, and
% update matrices in a cell array where each row is another cell array with as
% many columns as the cardinal number of the current row. Using such nested cell
% arrays efficiently simulates staggered arrays in MATLAB. This approach
% efficiently preserves space in memory without defining unnecessarily large
% high-order tensors to store the online smoother estimations and update
% matrices. In these nested cell arrays, the first/parent index corresponds to
% n∈{j,j+1,...,N}, for the current effect observation x^n = x(t_n), while the
% second/child index corresponds to j∈{0,1,...,N}, for the time instant t_j at
% which we carry out the online smoother state estimation for y^j = y(t_j).
online_fixed_mean = cell(N+1, 1);
online_fixed_cov = cell(N+1, 1);
update_matrices_fixed = cell(N-1, 1);
for n = 1:(N-1)
update_matrices_fixed{n} = zeros(state_dim, state_dim, n+1);
online_fixed_mean{n} = zeros(state_dim, n);
online_fixed_cov{n} = zeros(state_dim, state_dim, n);
end
for n = N:N+1
online_fixed_mean{n} = zeros(state_dim, n);
online_fixed_cov{n} = zeros(state_dim, state_dim, n);
end
% Need to do the first two observations manually.
% A single observation (n=1).
online_fixed_mean{1}(:, 1) = filter_mean(:, 1);
online_fixed_cov{1}(:, :, 1) = filter_cov(:, :, 1);
% Two observations (n=2).
online_fixed_mean{2}(:, 2) = filter_mean(:, 2);
online_fixed_cov{2}(:, :, 2) = filter_cov(:, :, 2);
if fixed_lag == 0
online_fixed_mean{2}(:, 1) = online_fixed_mean{1}(:, 1);
online_fixed_cov{2}(:, :, 1) = online_fixed_cov{1}(:, :, 1);
else
dx = x(:, 2) - x(:, 1);
aux_vec = filter_mean(:, 1) ...
- E_j_matrices(:, :, 1)*((eye_state + L_y*dt)*filter_mean(:, 1) + f_y(:, 1)*dt) ...
+ F_j_matrices(:, :, 1)*(dx - (L_x(:, :, 1)*filter_mean(:, 1) + f_x(:, 1))*dt);
online_fixed_mean{2}(:, 1) = E_j_matrices(:, :, 1)*filter_mean(:, 2) + aux_vec;
aux_mat = filter_cov(:, :, 1) ...
- E_j_matrices(:, :, 1)*(eye_state + L_y*dt)*filter_cov(:, :, 1) ...
- F_j_matrices(:, :, 1)*L_x(:, :, 1)*filter_cov(:, :, 1)*dt;
online_fixed_cov{2}(:, :, 1) = E_j_matrices(:, :, 1)*filter_cov(:, :, 2)*E_j_matrices(:, :, 1)' + aux_mat;
end
% Used for the text-based progress bar.
start_time = tic;
for n = 3:N+1
% Text-based progress bar.
progress_bar('Fixed-Lag Online Smoother Algorithm for Forward CIR', n-2, N-1, start_time);
online_fixed_mean{n}(:, n) = filter_mean(:, n);
online_fixed_cov{n}(:, :, n) = filter_cov(:, :, n);
if fixed_lag == 0
online_fixed_mean{n}(:, n-1) = online_fixed_mean{n-1}(:, n-1);
online_fixed_cov{n}(:, :, n-1) = online_fixed_cov{n-1}(:, :, n-1);
else
dx = x(:, n) - x(:, n-1);
aux_vec = filter_mean(:, n-1) ...
- E_j_matrices(:, :, n-1)*((eye_state + L_y*dt)*filter_mean(:, n-1) + f_y(:, n-1)*dt) ...
+ F_j_matrices(:, :, n-1)*(dx - (L_x(:, :, n-1)*filter_mean(:, n-1) + f_x(:, n-1))*dt);
online_fixed_mean{n}(:, n-1) = E_j_matrices(:, :, n-1)*filter_mean(:, n) + aux_vec;
aux_mat = filter_cov(:, :, n-1) ...
- E_j_matrices(:, :, n-1)*(eye_state + L_y*dt)*filter_cov(:, :, n-1) ...
- F_j_matrices(:, :, n-1)*L_x(:, :, n-1)*filter_cov(:, :, n-1)*dt;
online_fixed_cov{n}(:, :, n-1) = E_j_matrices(:, :, n-1)*filter_cov(:, :, n)*E_j_matrices(:, :, n-1)' + aux_mat;
end
for j = (n-1):-1:1
if (1 <= j) && (j <= n-1-fixed_lag)
online_fixed_mean{n}(:, j) = online_fixed_mean{n-1}(:, j);
online_fixed_cov{n}(:, :, j) = online_fixed_cov{n-1}(:, :, j);
elseif (n-fixed_lag <= j) && (j <= n-1)
if j == n-1
update_matrices_fixed{n-2}(:, :, n-1) = eye_state;
elseif j == n-2
update_matrices_fixed{n-2}(:, :, n-2) = E_j_matrices(:, :, n-2);
else
update_matrices_fixed{n-2}(:, :, j) = update_matrices_fixed{n-3}(:, :, j)*E_j_matrices(:, :, n-2);
end
online_mean_innovation = online_fixed_mean{n}(:, n-1) - filter_mean(:, n-1);
online_cov_innovation = online_fixed_cov{n}(:, :, n-1) - filter_cov(:, :, n-1);
online_fixed_mean{n}(:, j) = online_fixed_mean{n-1}(:, j) + update_matrices_fixed{n-2}(:, :, j)*online_mean_innovation;
online_fixed_cov{n}(:, :, j) = online_fixed_cov{n-1}(:, :, j) + update_matrices_fixed{n-2}(:, :, j)*online_cov_innovation*update_matrices_fixed{n-2}(:, :, j)';
end
end
end
% Letting 10⁻⁶ <= ε <= 10¹ with a resolution of 513 points.
epsilon_resolution = 513;
lowest_order = -6;
highest_order = 1;
eps_ord_values = flip(linspace(lowest_order, highest_order, epsilon_resolution));
% Threshold values used when evaluating the subjective CIR lengths.
CIR_epsilon_values = 10.^eps_ord_values;
epsilon_axis_tick_values = unique([lowest_order:1:floor(highest_order), highest_order]);
% The following snippet uses a more adaptive mesh of ε instead of the purely
% logarithmic one used in the above implementation, for a more realistic
% integration of the subjective CIRs over ε for obtaining the exact
% corresponding objective CIR length at each time.
% adaptive_point = -2;
% half_resolution = 250;
% eps_ord_values = unique([
% flip(log10(linspace(10^adaptive_point, 10^highest_order, half_resolution))), ...
% flip(linspace(lowest_order, adaptive_point, half_resolution))
% ], 'stable');
% Calculating the subjective and objective CIRs over the plotting time interval
% of choice. We add a lookahead tolerance for the lagged observational time:
% T'∈[t,time_end_plot+lookahead_tolerance],
% to avoid observational saturation as t approaches time_end_plot. If the
% plotting interval reaches the final simulation time, the available lookahead
% is set automatically to zero.
lookahead_tolerance = 0.6;