pythonCRO 0.1.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyCRO/fit_MLE.py ADDED
@@ -0,0 +1,735 @@
1
+ import sys
2
+ import numpy as np
3
+
4
+ from scipy.optimize import fsolve
5
+ from scipy.optimize import least_squares
6
+ from scipy.interpolate import interp1d
7
+ from scipy.linalg import solve, inv
8
+ import warnings
9
+
10
+ from .utils import wrap_to_pi
11
+
12
+ ########################################################################################################
13
+ def fit_MLE_white(T, h, par_option_T, par_option_h, par_option_noise, dt, raw_output=False):
14
+ T = np.array(T).flatten()
15
+ h = np.array(h).flatten()
16
+ n = len(T)
17
+
18
+ par_option_T = np.array(par_option_T)
19
+ par_option_h = np.array(par_option_h)
20
+ par_option_noise = np.array(par_option_noise)
21
+ n_T, n_h, n_g = par_option_noise
22
+
23
+ par_option_T_onoff = (par_option_T >= 1).astype(int)
24
+ par_option_h_onoff = (par_option_h >= 1).astype(int)
25
+ par_option_T_h_onoff = np.concatenate([par_option_T_onoff, par_option_h_onoff])
26
+
27
+ par_option_T_season = np.where(par_option_T > 1)[0]
28
+ par_option_h_season = np.where(par_option_h > 1)[0]
29
+ par_option_T_h_season = np.where(np.concatenate([par_option_T, par_option_h]) > 1)[0]
30
+ par_option_T_h = np.concatenate([par_option_T, par_option_h])
31
+
32
+ # Construct MT and Mh
33
+ MT = np.column_stack([T, h, T**2, -T**3, T * h, np.zeros((n, 3))]) * dt * np.concatenate([par_option_T_onoff, np.zeros(3)])
34
+ Mh = np.column_stack([np.zeros((n, 5)), -T, -h, -T**2]) * dt * np.concatenate([np.zeros(5), par_option_h_onoff])
35
+
36
+ w = 2 * np.pi / 12
37
+ t = np.arange(n) * dt
38
+ t_shift = t + 0.5*dt
39
+
40
+ for idx in par_option_T_season:
41
+ base = MT[:, idx]
42
+ if par_option_T[idx] == 3:
43
+ MT_add = np.column_stack([base * np.sin(w * t_shift), base * np.cos(w * t_shift)])
44
+ elif par_option_T[idx] == 5:
45
+ MT_add = np.column_stack([base * np.sin(w * t_shift), base * np.cos(w * t_shift),
46
+ base * np.sin(2 * w * t_shift), base * np.cos(2 * w * t_shift)])
47
+ else:
48
+ continue
49
+ MT = np.hstack([MT, MT_add])
50
+ Mh = np.hstack([Mh, np.zeros_like(MT_add)])
51
+
52
+ for idx in par_option_h_season:
53
+ base = Mh[:, 5 + idx]
54
+ if par_option_h[idx] == 3:
55
+ Mh_add = np.column_stack([base * np.sin(w * t_shift), base * np.cos(w * t_shift)])
56
+ elif par_option_h[idx] == 5:
57
+ Mh_add = np.column_stack([base * np.sin(w * t_shift), base * np.cos(w * t_shift),
58
+ base * np.sin(2 * w * t_shift), base * np.cos(2 * w * t_shift)])
59
+ else:
60
+ continue
61
+ Mh = np.hstack([Mh, Mh_add])
62
+ MT = np.hstack([MT, np.zeros_like(Mh_add)])
63
+
64
+ # Assume MT and Mh are shape (1200, 8)
65
+ M = np.stack([MT.T, Mh.T], axis=0)
66
+ M = M[:, :, :-1]
67
+ Mtr = np.transpose(M, (1, 0, 2))
68
+
69
+ on_T = np.concatenate([par_option_T_onoff, np.zeros(3)])
70
+ on_h = np.concatenate([np.zeros(5), par_option_h_onoff])
71
+
72
+ ind = np.where((on_T == 0) & (on_h == 0))[0]
73
+
74
+ M = np.delete(M, ind, axis=1)
75
+ Mtr = np.delete(Mtr, ind, axis=0)
76
+
77
+ x = np.stack([np.diff(T), np.diff(h)], axis=0)
78
+ x = x[:, np.newaxis, :]
79
+
80
+ sigma_T = 1.0
81
+ sigma_h = 1.0
82
+ sigma_B = 0.0
83
+
84
+ if n_g == 0:
85
+ noise_g = np.ones_like(T)
86
+ elif n_g == 1:
87
+ noise_g = np.heaviside(T, 0)
88
+ elif n_g == 2:
89
+ noise_g = np.zeros_like(T)
90
+
91
+
92
+ if n_g in [0, 1]: # multiplicative
93
+ n_iter = 100
94
+ elif n_g == 2: # additive
95
+ n_iter = 1
96
+
97
+ for my_iter in range(n_iter):
98
+ # Setting R Matrix
99
+ RT = np.column_stack([((sigma_T + sigma_B * noise_g * T)**2), np.zeros(n)]) * dt
100
+ Rh = np.column_stack([np.zeros(n), np.ones(n) * sigma_h**2]) * dt
101
+ R = np.stack([RT, Rh], axis=2)
102
+ R = np.transpose(R, (2, 1, 0))
103
+
104
+ with np.errstate(divide='ignore', invalid='ignore'):
105
+ Rinv = 1 / R
106
+ Rinv[np.isinf(Rinv)] = 0
107
+
108
+ R = R[:, :, :-1]
109
+ Rinv = Rinv[:, :, :-1]
110
+
111
+ thetao_A = np.einsum('ikt,klt,ljt->ij', Mtr, Rinv, M)
112
+ thetao_B = np.einsum('ikt,klt,ljt->ij', Mtr, Rinv, x)
113
+ thetao = np.linalg.solve(thetao_A, thetao_B)
114
+
115
+
116
+ # Calculate R Matrix
117
+ R_A = x - np.einsum('ijk,jl->ilk', M, thetao)
118
+ R_B = np.transpose(R_A, (1, 0, 2))
119
+ # Calculate Noise for Additive Noise RO
120
+ R = np.sum(R_A * R_B, axis=2) / (n - 1)
121
+
122
+ # Calculate Noise for Multiplicative Noise RO
123
+ if my_iter == 0:
124
+ sigma_T = np.sqrt(R[0, 0] / dt) # % Initial Guess for sigma_T using Additive Noise
125
+ sigma_B = 0.0 # % Initial Guess for sigma_B
126
+
127
+ def func(sigma_x):
128
+ #print(sigma_x[0], sigma_x[1])
129
+ r0 = (R_A[0, 0, :] * R_B[0, 0, :])
130
+ r1 = -dt * (sigma_x[0] + sigma_x[1] * noise_g[:-1] * T[:-1])**2
131
+ r2 = (sigma_x[0] + sigma_x[1] * noise_g[:-1] * T[:-1])**(-3) * T[:-1]
132
+ r3 = (sigma_x[0] + sigma_x[1] * noise_g[:-1] * T[:-1])**(-3)
133
+ r_val1 = np.sum((r0 + r1) * r2) / len(T)
134
+ r_val2 = np.sum((r0 + r1) * r3) / len(T)
135
+ val1 = r_val1
136
+ val2 = r_val2
137
+
138
+ return np.array([val1, val2])
139
+
140
+ res = least_squares(func, x0=[sigma_T, sigma_B], bounds=([1e-6, -1], [np.inf, np.inf]))
141
+ sigma_T, sigma_B = res.x
142
+ sigma_h = np.sqrt(R[1, 1] / dt)
143
+
144
+ # Post-Processing Output
145
+ on_indices = np.where(par_option_T_h_onoff == 1)[0]
146
+ thetao_ann = thetao[:len(on_indices)]
147
+ thetao_season = thetao[len(on_indices):]
148
+
149
+ par_T_h = [[0.0] * 5 for _ in range(8)]
150
+ for idx, param_idx in enumerate(on_indices):
151
+ par_T_h[param_idx][0] = float(thetao_ann[idx])
152
+
153
+ if len(par_option_T_h_season) > 0:
154
+ ind_base = 0
155
+ for i in range(len(par_option_T_h_season)):
156
+ idx = par_option_T_h_season[i]
157
+ if par_option_T_h[idx] == 3:
158
+ ind_sin = ind_base
159
+ ind_cos = ind_base + 1
160
+
161
+ A = np.sqrt(thetao_season[ind_sin]**2 + thetao_season[ind_cos]**2).item()
162
+ phi = np.mod(np.arctan2(thetao_season[ind_cos], thetao_season[ind_sin]), 2 * np.pi).item()
163
+
164
+ if np.pi < phi:
165
+ phi = phi - 2*np.pi
166
+
167
+ par_T_h[idx][1:3] = [A, phi]
168
+
169
+ ind_base = ind_cos + 1
170
+
171
+ elif par_option_T_h[idx] == 5:
172
+ ind_sin_a = ind_base
173
+ ind_cos_a = ind_base + 1
174
+ ind_sin_sa = ind_base + 2
175
+ ind_cos_sa = ind_base + 3
176
+
177
+ A_a = np.sqrt(thetao_season[ind_sin_a]**2 + thetao_season[ind_cos_a]**2).item()
178
+ phi_a = np.mod(np.arctan2(thetao_season[ind_cos_a], thetao_season[ind_sin_a]), 2 * np.pi).item()
179
+ A_sa = np.sqrt(thetao_season[ind_sin_sa]**2 + thetao_season[ind_cos_sa]**2).item()
180
+ phi_sa = np.mod(np.arctan2(thetao_season[ind_cos_sa], thetao_season[ind_sin_sa]), 2 * np.pi).item()
181
+
182
+ if np.pi < phi_a:
183
+ phi_a = phi_a - 2*np.pi
184
+ if np.pi < phi_sa:
185
+ phi_sa = phi_sa - 2*np.pi
186
+
187
+ par_T_h[idx][1:5] = [A_a, phi_a, A_sa, phi_sa]
188
+
189
+ ind_base = ind_cos_sa + 1
190
+
191
+ par_T_h = np.array(par_T_h)
192
+ par_noise = np.array([[sigma_T, 0, 0, 0, 0],
193
+ [sigma_h, 0, 0, 0, 0],
194
+ [sigma_B/sigma_T, 0, 0, 0, 0],
195
+ [0, 0, 0, 0, 0],
196
+ [0, 0, 0, 0, 0],
197
+ [n_T, 0, 0, 0, 0],
198
+ [n_h, 0, 0, 0, 0],
199
+ [n_g, 0, 0, 0, 0]])
200
+ par_combined = np.vstack([par_T_h, par_noise])
201
+ return par_combined
202
+
203
+
204
+
205
+ def fit_MLE_red(T, h, par_option_T, par_option_h, par_option_noise, dt):
206
+
207
+ par_option_T = np.array(par_option_T)
208
+ par_option_h = np.array(par_option_h)
209
+ par_option_noise = np.array(par_option_noise)
210
+
211
+
212
+ n_T, n_h, n_g = par_option_noise.astype(int)
213
+
214
+
215
+ # Setting Option
216
+ par_option_T_onoff = (par_option_T >= 1).astype(int)
217
+ par_option_h_onoff = (par_option_h >= 1).astype(int)
218
+ par_option_T_h_onoff = np.concatenate([par_option_T_onoff, par_option_h_onoff])
219
+
220
+ par_option_T_season = np.where(par_option_T > 1)[0]
221
+ par_option_h_season = np.where(par_option_h > 1)[0]
222
+ par_option_T_h_season = np.where(np.concatenate([par_option_T, par_option_h]) > 1)[0]
223
+
224
+
225
+ par_option_T_h = np.concatenate([par_option_T, par_option_h])
226
+
227
+
228
+ # Initial Guess for White Noise Fitting Value
229
+ par_white_in = fit_MLE_white(T, h, par_option_T, par_option_h, [1, 1, n_g], dt)
230
+
231
+
232
+ # only select annual mean value
233
+ R, F1, b_T, c_T, d_T, F2, epsilon, b_h = par_white_in[0:8, 0]
234
+ sigma_T, sigma_h, sigma_B = par_white_in[8:11, 0]
235
+ sigma_B = sigma_B*sigma_T
236
+ m_T = 1.0
237
+ m_h = 1.0
238
+
239
+ # Setting Seasonal Parameters
240
+ par_season_T = par_white_in[0:5, 1:5]
241
+ par_season_h = par_white_in[5:8, 1:5]
242
+
243
+
244
+
245
+ sigma_T2 = 0.01
246
+ sigma_h2 = 0.01
247
+
248
+ # Interpolation of T and h
249
+ dt_interp = 0.01 # empirically dt_interp should be 0.01 or smaller
250
+ t = np.arange(len(T)) * dt
251
+ if dt > dt_interp:
252
+ time_interp = np.arange(0, t[-1] + dt_interp, dt_interp)
253
+ T = interp1d(t, T, kind='linear')(time_interp)
254
+ h = interp1d(t, h, kind='linear')(time_interp)
255
+ dt = dt_interp
256
+ t = time_interp
257
+
258
+
259
+ w = 2 * np.pi / 12
260
+ t_shift = t + 0.5*dt
261
+ n = len(T)
262
+
263
+ # Setting x diff
264
+ x = np.stack([np.diff(T), np.diff(h)])
265
+ x = np.expand_dims(x, axis=1)
266
+
267
+
268
+ if n_g == 0: # Linear (B*T)
269
+ noise_g = np.ones_like(T)
270
+ elif n_g == 1: # Heavisdie (B*H(T)*T)
271
+ noise_g = np.heaviside(T, 0)
272
+ elif n_g == 2: # Additive (B=0)
273
+ noise_g = np.zeros_like(T)
274
+
275
+ # --- Iterative estimation ---
276
+ print("-------------------------------------------------------------------")
277
+ print("Hang tight — red noise MLE fitting can take a bit!")
278
+ print("-------------------------------------------------------------------")
279
+ niter = 10 #100
280
+ for _ in range(niter):
281
+ print(f"{_ + 1} out of {niter} iterations")
282
+ # Setting CGNS Matrix
283
+ A0_T = R*T + F1*h + b_T*T**2 - c_T*T**3 + d_T*T*h
284
+ A0_h = -F2*T - epsilon*h - b_h*T**2
285
+
286
+
287
+ Tv = np.array([T, h, T**2, -T**3, T*h]).T
288
+ hv = np.array([-T, -h, -T**2]).T
289
+
290
+
291
+
292
+ A0_T_add = ( par_season_T[0,0] * Tv[:,0] * np.sin(w*t_shift) + par_season_T[0,1] * Tv[:,0] * np.cos(w*t_shift)
293
+ + par_season_T[0,2] * Tv[:,0]*np.sin(2*w*t_shift) + par_season_T[0,3] * Tv[:,0]*np.cos(2*w*t_shift)
294
+ + par_season_T[1,0] * Tv[:,1] * np.sin(w*t_shift) + par_season_T[1,1] * Tv[:,1] * np.cos(w*t_shift)
295
+ + par_season_T[1,2] * Tv[:,1]*np.sin(2*w*t_shift) + par_season_T[1,3] * Tv[:,1]*np.cos(2*w*t_shift)
296
+ + par_season_T[2,0] * Tv[:,2] * np.sin(w*t_shift) + par_season_T[2,1] * Tv[:,2] * np.cos(w*t_shift)
297
+ + par_season_T[2,2] * Tv[:,2]*np.sin(2*w*t_shift) + par_season_T[2,3] * Tv[:,2]*np.cos(2*w*t_shift)
298
+ + par_season_T[3,0] * Tv[:,3] * np.sin(w*t_shift) + par_season_T[3,1] * Tv[:,3] * np.cos(w*t_shift)
299
+ + par_season_T[3,2] * Tv[:,3]*np.sin(2*w*t_shift) + par_season_T[3,3] * Tv[:,3]*np.cos(2*w*t_shift)
300
+ + par_season_T[4,0] * Tv[:,4] * np.sin(w*t_shift) + par_season_T[4,1] * Tv[:,4] * np.cos(w*t_shift)
301
+ + par_season_T[4,2] * Tv[:,4]*np.sin(2*w*t_shift) + par_season_T[4,3] * Tv[:,4]*np.cos(2*w*t_shift))
302
+ A0_T = A0_T + A0_T_add
303
+
304
+ A0_h_add = ( par_season_h[0,0] * hv[:,0] * np.sin(w*t_shift) + par_season_h[0,1] * hv[:,0] * np.cos(w*t_shift)
305
+ + par_season_h[0,2] * hv[:,0]*np.sin(2*w*t_shift) + par_season_h[0,3] * hv[:,0]*np.cos(2*w*t_shift)
306
+ + par_season_h[1,0] * hv[:,1] * np.sin(w*t_shift) + par_season_h[1,1] * hv[:,1] * np.cos(w*t_shift)
307
+ + par_season_h[1,2] * hv[:,1]*np.sin(2*w*t_shift) + par_season_h[1,3] * hv[:,1]*np.cos(2*w*t_shift)
308
+ + par_season_h[2,0] * hv[:,2] * np.sin(w*t_shift) + par_season_h[2,1] * hv[:,2] * np.cos(w*t_shift)
309
+ + par_season_h[2,2] * hv[:,2]*np.sin(2*w*t_shift) + par_season_h[2,3] * hv[:,2]*np.cos(2*w*t_shift))
310
+ A0_h = A0_h + A0_h_add
311
+
312
+
313
+ A0 = np.stack([A0_T, A0_h])
314
+
315
+ A1_T = np.column_stack([
316
+ sigma_T + sigma_B * noise_g * T,
317
+ np.zeros_like(T)])
318
+
319
+ A1_h = np.column_stack([
320
+ np.zeros_like(T),
321
+ sigma_h * np.ones_like(T)])
322
+
323
+ A1 = np.stack([A1_T, A1_h], axis=2)
324
+ A1 = np.transpose(A1, (2, 1, 0))
325
+
326
+ # Define noise model
327
+ B1 = np.diag([sigma_T2, sigma_h2])
328
+ a0 = np.zeros((2, 1))
329
+ a1 = np.diag([-m_T, -m_h])
330
+ b2 = np.identity(2)
331
+
332
+
333
+
334
+ mu_f = np.zeros((2, n))
335
+ R_f = np.zeros((2, 2, n))
336
+ R_f[:, :, 0] = 0.01 * np.eye(2)
337
+
338
+
339
+ # Forward filter
340
+ for i in range(n - 1):
341
+ A0_sel = A0[:, i]
342
+ A0_sel = A0_sel[:, np.newaxis]
343
+
344
+ A1_sel = A1[:, :, i]
345
+
346
+ R_f_sel = R_f[:, :, i]
347
+
348
+ mu_f_sel = mu_f[:, i]
349
+ mu_f_sel = mu_f_sel[:, np.newaxis]
350
+
351
+ x_sel = x[:, :, i]
352
+
353
+ dummy1 = a0 + a1 @ mu_f[:, [i]]
354
+ dummy2 = R_f_sel @ A1_sel.T
355
+ dummy3 = np.linalg.inv(B1 @ B1.T)
356
+ dummy4 = x_sel / dt - (A0_sel + A1_sel @ mu_f_sel)
357
+ mu_f_tendency = dummy1 + dummy2 * dummy3 @ dummy4
358
+
359
+ R_f_tendency = (a1 @ R_f_sel
360
+ + R_f_sel @ a1.T
361
+ + b2 @ b2.T
362
+ - (R_f_sel @ A1_sel.T) @ np.linalg.inv(B1 @ B1.T) @ (A1_sel @ R_f_sel))
363
+
364
+ mu_f[:, i+1] = mu_f[:, i] + (mu_f_tendency[:, 0] * dt)
365
+ R_f[:, :, i+1] = R_f[:, :, i] + (R_f_tendency * dt)
366
+
367
+ # Backward smoother
368
+ mu_s = np.zeros((2, n))
369
+ R_s = np.zeros((2, 2, n))
370
+
371
+
372
+ mu_s[:, -1] = mu_f[:, -1]
373
+ R_s[:, :, -1] = R_f[:, :, -1]
374
+
375
+
376
+ for i in range(n - 2, -1, -1):
377
+ #print(i)
378
+ R_f_sel = R_f[:, :, i+1]
379
+ mu_f_sel = mu_f[:, i+1]
380
+ mu_f_sel = mu_f_sel[:, np.newaxis]
381
+ R_s_sel = R_s[:, :, i+1]
382
+ mu_s_sel = mu_s[:, i+1]
383
+ mu_s_sel = mu_s_sel[:, np.newaxis]
384
+
385
+
386
+ mu_s_tendency = -a0 - a1 @ mu_s_sel + (b2 @ b2.T) @ np.linalg.inv(R_f_sel) @ (mu_f_sel - mu_s_sel)
387
+ R_s_tendency = -(a1 + (b2 @ b2.T) @ np.linalg.inv(R_f_sel)) @ R_s_sel \
388
+ - R_s_sel @ (a1.T + (b2 @ b2.T) @ R_f_sel) \
389
+ + (b2 @ b2.T)
390
+
391
+
392
+ mu_s[:, i] = mu_s[:, i+1] + (mu_s_tendency * dt).flatten()
393
+ R_s[:, :, i] = R_s[:, :, i+1] + R_s_tendency * dt
394
+
395
+
396
+ # Calculate C matrix and y moments
397
+ C = np.zeros_like(R_f)
398
+ I = np.eye(2)
399
+ A1_dt = I + a1 * dt
400
+ B2 = b2 @ b2.T
401
+
402
+ for i in range(R_f.shape[2]):
403
+ R_f_sel = R_f[:, :, i]
404
+ denom = B2 * dt + A1_dt @ R_f_sel @ A1_dt.T
405
+ C[:, :, i] = R_f_sel @ A1_dt.T @ np.linalg.inv(denom)
406
+
407
+
408
+ yi = mu_s.copy()
409
+ yiyi = np.zeros_like(R_s)
410
+ for i in range(R_s.shape[2]):
411
+ R_s_sel = R_s[:, :, i]
412
+ mu_s_sel = mu_s[:, i].reshape(-1, 1)
413
+ yiyi[:, :, i] = R_s_sel + mu_s_sel @ mu_s_sel.T
414
+
415
+
416
+ yi1yi = np.zeros((2, 2, R_s.shape[2] - 1))
417
+ for i in range(R_s.shape[2] - 1):
418
+ C_i = C[:, :, i]
419
+ mu_next = mu_s[:, i + 1].reshape(-1, 1)
420
+ mu_now = mu_s[:, i].reshape(-1, 1)
421
+ yi1yi[:, :, i] = R_s[:, :, i + 1] @ C_i.T + mu_next @ mu_now.T
422
+
423
+
424
+ # Setting M and Rinv
425
+ Rinv_single = np.diag([1 / sigma_T2**2, 1 / sigma_h2**2, 1, 1]) * (1 / dt)
426
+ Rinv = np.repeat(Rinv_single[:, :, np.newaxis], n, axis=2)
427
+
428
+ xi_T = yi[0, :].reshape(-1, 1)
429
+ xi_h = yi[1, :].reshape(-1, 1)
430
+
431
+ T_col = T.reshape(-1, 1)
432
+ h_col = h.reshape(-1, 1)
433
+ xi_T_col = xi_T.reshape(-1, 1)
434
+ xi_h_col = xi_h.reshape(-1, 1)
435
+ noise_g_col = noise_g.reshape(-1, 1)
436
+
437
+ MT_raw = np.hstack([T_col, h_col, T_col**2, -T_col**3, T_col * h_col, xi_T_col, xi_T_col * noise_g_col * T_col, np.zeros((T.shape[0], 6))])
438
+ MT_weights = np.concatenate([par_option_T_onoff, [1, 1], np.zeros(6)])
439
+ MT = MT_raw * dt * MT_weights
440
+
441
+ Mh_raw = np.hstack([np.zeros((T.shape[0], 7)), -T_col, -h_col, -T_col**2, xi_h_col, np.zeros((T.shape[0], 2))])
442
+ Mh_weights = np.concatenate([np.zeros(7), par_option_h_onoff, [1], np.zeros(2)])
443
+ Mh = Mh_raw * dt * Mh_weights
444
+
445
+ MxiT_raw = np.hstack([np.zeros((T.shape[0], 11)), -xi_T_col, np.zeros((T.shape[0], 1))])
446
+ MxiT_weights = np.concatenate([np.zeros(11), [1], [0]])
447
+ MxiT = MxiT_raw * dt * MxiT_weights
448
+
449
+ Mxih_raw = np.hstack([np.zeros((T.shape[0], 12)),-xi_h_col])
450
+ Mxih_weights = np.concatenate([np.zeros(12), [1]])
451
+ Mxih = Mxih_raw * dt * Mxih_weights
452
+
453
+
454
+ if len(par_option_T_season) > 0:
455
+ for i in range(len(par_option_T_season)):
456
+ idx = par_option_T_season[i] # column index to modulate
457
+
458
+ MT_col = MT[:, idx] # column to modulate, shape (n,)
459
+
460
+ if par_option_T[idx] == 3: # annual only
461
+ MT_add = np.column_stack([MT_col * np.sin(w * t_shift), MT_col * np.cos(w * t_shift)])
462
+ elif par_option_T[idx] == 5: # annual + semiannual
463
+ MT_add = np.column_stack([
464
+ MT_col * np.sin(w * t_shift),
465
+ MT_col * np.cos(w * t_shift),
466
+ MT_col * np.sin(2 * w * t_shift),
467
+ MT_col * np.cos(2 * w * t_shift)])
468
+ else:
469
+ continue # skip unsupported codes
470
+
471
+ # Stack MT_add and corresponding zero-padding to other matrices
472
+ MT = np.hstack([MT, MT_add])
473
+ zeros_like_MT_add = np.zeros_like(MT_add)
474
+ Mh = np.hstack([Mh, zeros_like_MT_add])
475
+ MxiT = np.hstack([MxiT, zeros_like_MT_add])
476
+ Mxih = np.hstack([Mxih, zeros_like_MT_add])
477
+
478
+ if len(par_option_h_season) > 0:
479
+ for i in range(len(par_option_h_season)):
480
+ idx = par_option_h_season[i] # Index within par_option_h (0-based)
481
+ mh_col_idx = 7 + idx # Adjusted index in Mh to find the correct feature column
482
+
483
+ Mh_col = Mh[:, mh_col_idx] # shape (n,)
484
+
485
+ if par_option_h[idx] == 3: # annual only
486
+ Mh_add = np.column_stack([Mh_col * np.sin(w * t_shift), Mh_col * np.cos(w * t_shift)])
487
+ elif par_option_h[idx] == 5: # annual + semiannual
488
+ Mh_add = np.column_stack([Mh_col * np.sin(w * t_shift),
489
+ Mh_col * np.cos(w * t_shift),
490
+ Mh_col * np.sin(2 * w * t_shift),
491
+ Mh_col * np.cos(2 * w * t_shift)])
492
+ else:
493
+ continue # skip unsupported cases
494
+
495
+ # Stack Mh_add and corresponding zero-padding to other matrices
496
+ Mh = np.hstack([Mh, Mh_add])
497
+ zeros_like_Mh_add = np.zeros_like(Mh_add)
498
+ MT = np.hstack([MT, zeros_like_Mh_add])
499
+ MxiT = np.hstack([MxiT, zeros_like_Mh_add])
500
+ Mxih = np.hstack([Mxih, zeros_like_Mh_add])
501
+
502
+ M = np.stack([MT, Mh, MxiT, Mxih], axis=2)
503
+ M = np.transpose(M, (2, 1, 0))
504
+ Mtr = np.transpose(M, (1, 0, 2))
505
+
506
+ if n_g == 0 or n_g == 1: # multiplicative
507
+ par_option_matrix = np.array([
508
+ np.concatenate([par_option_T_onoff, [1], [1], np.zeros(6)]), # MT
509
+ np.concatenate([np.zeros(7), par_option_h_onoff, [1], np.zeros(2)]), # Mh
510
+ np.concatenate([np.zeros(11), [1], [0]]), # MxiT
511
+ np.concatenate([np.zeros(11), [0], [1]]) # Mxih
512
+ ])
513
+ elif n_g == 2: # additive
514
+ par_option_matrix = np.array([
515
+ np.concatenate([par_option_T_onoff, [1], [0], np.zeros(6)]),
516
+ np.concatenate([np.zeros(7), par_option_h_onoff, [1], np.zeros(2)]),
517
+ np.concatenate([np.zeros(11), [1], [0]]),
518
+ np.concatenate([np.zeros(11), [0], [1]])
519
+ ])
520
+
521
+
522
+
523
+ total_T = np.sum(np.where(par_option_T > 1, par_option_T - 1, 0))
524
+ total_h = np.sum(np.where(par_option_h > 1, par_option_h - 1, 0))
525
+
526
+ par_option_matrix = np.array([
527
+ np.concatenate([par_option_matrix[0], np.ones(total_T), np.zeros(total_h)]),
528
+ np.concatenate([par_option_matrix[1], np.zeros(total_T), np.ones(total_h)]),
529
+ np.concatenate([par_option_matrix[2], np.zeros(total_T + total_h)]),
530
+ np.concatenate([par_option_matrix[3], np.zeros(total_T + total_h)])
531
+ ])
532
+
533
+ zero_col_inds = np.where(np.sum(par_option_matrix, axis=0) == 0)[0]
534
+ M = np.delete(M, zero_col_inds, axis=1)
535
+ Mtr = np.delete(Mtr, zero_col_inds, axis=0)
536
+
537
+ # Calculate thetao_A
538
+ temp = np.einsum('ijk,jlk->ilk', Mtr, Rinv)
539
+ thetao_A = np.einsum('ijk,jlk->ilk', temp, M)
540
+
541
+ nM = thetao_A.shape[0] - total_T - total_h
542
+ nT = np.sum(par_option_T_onoff == 1)
543
+
544
+
545
+ thetao_A[nT, nT, :] = dt * yiyi[0, 0, :] / (sigma_T2**2)
546
+ thetao_A[nM - 3, nM - 3, :] = dt * yiyi[1, 1, :] / (sigma_h2**2)
547
+ thetao_A[nM - 2, nM - 2, :] = dt * yiyi[0, 0, :] / 1
548
+ thetao_A[nM - 1, nM - 1, :] = dt * yiyi[1, 1, :] / 1
549
+
550
+ if n_g == 0 or n_g == 1:
551
+ thetao_A[nT + 1, nT + 1, :] = dt * (noise_g * T**2) * yiyi[0, 0, :] / (sigma_T2**2)
552
+
553
+ thetao_A = thetao_A[:, :, :-1]
554
+
555
+ # Calculate thetao_B
556
+ x_NaN = np.zeros_like(x)
557
+ x_all = np.concatenate([x, x_NaN], axis=0)
558
+
559
+ Mtr_sub = Mtr[:, :, :-1]
560
+ Rinv_sub = Rinv[:, :, :-1]
561
+ x_sub = x_all[:, :, :]
562
+
563
+ temp = np.einsum('ijk,jlk->ilk', Mtr_sub, Rinv_sub)
564
+
565
+ thetao_B = np.einsum('ijk,jlk->ilk', temp, x_sub)
566
+ thetao_B[nM - 2, 0, :] = -yi1yi[0, 0, :] + yiyi[0, 0, :-1]
567
+ thetao_B[nM - 1, 0, :] = -yi1yi[1, 1, :] + yiyi[1, 1, :-1]
568
+
569
+
570
+ # Sum across the time dimension (axis=2), from ind_res to end-ind_res+1
571
+ thetao_A_sum = np.sum(thetao_A[:, :, :], axis=2)
572
+ thetao_B_sum = np.sum(thetao_B[:, :, :], axis=2)
573
+ thetao_B_sum = thetao_B_sum.squeeze()
574
+
575
+ thetao = np.linalg.solve(thetao_A_sum, thetao_B_sum)
576
+
577
+ M_sliced = M[:, :, :-1]
578
+ thetao_squeezed = np.squeeze(thetao)
579
+ M_thetao = np.matmul(M_sliced.transpose(2, 0, 1), thetao_squeezed).transpose(1, 0)
580
+ x_sliced = x_all[:, :, :] if x_all.ndim == 3 else x_all[:, :]
581
+ R_A = x_sliced - M_thetao[:, np.newaxis, :]
582
+ R_B = np.transpose(R_A, (1, 0, 2))
583
+ R_AB = np.einsum('ijk,jlk->ilk', R_A, R_B)
584
+
585
+
586
+ adjust1 = ((sigma_T * xi_T[:-1, 0]) + sigma_B * noise_g[:-1] * xi_T[:-1, 0] * T[:-1]) * dt
587
+ adjust1 = adjust1 ** 2
588
+ R_AB[0, 0, :] = R_AB[0, 0, :] - np.squeeze(adjust1)
589
+
590
+ adjust2 = ((sigma_h * xi_h[:-1, 0]) * dt)
591
+ adjust2 = adjust2 ** 2
592
+ R_AB[1, 1, :] = R_AB[1, 1, :] - np.squeeze(adjust2)
593
+
594
+ R_AB[0, 0, :] += (
595
+ np.squeeze(yiyi[0, 0, :-1]) *
596
+ (dt * (sigma_T + sigma_B * noise_g[:-1] * T[:-1])) ** 2)
597
+
598
+ R_AB[1, 1, :] += (
599
+ np.squeeze(yiyi[1, 1, :-1]) *
600
+ (dt * sigma_h) ** 2)
601
+
602
+
603
+ # Compute the thetao term (same index used in both the square and linear terms)
604
+ theta_idx_1 = len(thetao) - 2 - total_T - total_h
605
+ theta_term_1 = thetao[theta_idx_1] * dt - 1
606
+ theta_idx_2 = len(thetao) - 1 - total_T - total_h
607
+ theta_term_2 = thetao[theta_idx_2] * dt - 1
608
+
609
+ R_AB[2, 2, :] = (
610
+ np.squeeze(yiyi[0, 0, 1:]) +
611
+ np.squeeze(yiyi[0, 0, :-1]) * (theta_term_1 ** 2) +
612
+ 2 * np.squeeze(yi1yi[0, 0, :]) * theta_term_1)
613
+
614
+ R_AB[3, 3, :] = (
615
+ np.squeeze(yiyi[1, 1, 1:]) +
616
+ np.squeeze(yiyi[1, 1, :-1]) * (theta_term_2 ** 2) +
617
+ 2 * np.squeeze(yi1yi[1, 1, :]) * theta_term_2)
618
+
619
+ # Sum over time (axis=2), then divide by the total number of time steps in R_AB
620
+ R_M = np.sum(R_AB[:, :, : ], axis=2) / R_AB.shape[2]
621
+
622
+
623
+ # Save to Parameter
624
+ thetao_out = np.zeros((13 + total_T + total_h, 1))
625
+
626
+ selected_indices = np.where(np.sum(par_option_matrix, axis=0) == 1)[0]
627
+
628
+ thetao_out[selected_indices, 0] = thetao.flatten()
629
+
630
+ R = thetao_out[0, 0]
631
+ F1 = thetao_out[1, 0]
632
+ b_T = thetao_out[2, 0]
633
+ c_T = thetao_out[3, 0]
634
+ d_T = thetao_out[4, 0]
635
+ sigma_T = thetao_out[5, 0]
636
+ sigma_B = thetao_out[6, 0]
637
+ B = sigma_B/sigma_T
638
+ F2 = thetao_out[7, 0]
639
+ epsilon = thetao_out[8, 0]
640
+ b_h = thetao_out[9, 0]
641
+ sigma_h = thetao_out[10, 0]
642
+ m_T = thetao_out[11, 0]
643
+ m_h = thetao_out[12, 0]
644
+ thetao_out = list(thetao_out.flatten())
645
+
646
+ par_T_h = []
647
+ par_order_array=[0, 1, 2, 3, 4, 7, 8, 9]
648
+ j = 0
649
+ for i in range(0, len(par_option_T_h)):
650
+ seasonal_list = []
651
+ if (par_option_T_h[i] == 0) or (par_option_T_h[i] == 1):
652
+ seasonal_list.extend([0, 0, 0, 0])
653
+ elif(par_option_T_h[i] == 3):
654
+ seasonal_list = thetao_out[8+j:8+j+2]
655
+ seasonal_list.extend([0, 0])
656
+ j += 2
657
+ elif(par_option_T_h[i] == 5):
658
+ seasonal_list = thetao_out[8+j:8+j+4]
659
+ j += 4
660
+ sin_a, cos_a, sin_sa, cos_sa = seasonal_list
661
+ A_a = np.sqrt(sin_a**2 + cos_a**2)
662
+ phi_a = np.mod(np.arctan2(cos_a, sin_a), 2 * np.pi)
663
+ A_sa = np.sqrt(sin_sa**2 + cos_sa**2)
664
+ phi_sa = np.mod(np.arctan2(cos_sa, sin_sa), 2 * np.pi)
665
+
666
+ par_T_h.append([thetao_out[par_order_array[i]], A_a, phi_a, A_sa, phi_sa])
667
+ par_T_h = np.array(par_T_h)
668
+ sigma_T2 = 0.01
669
+ sigma_h2 = 0.01
670
+
671
+ par_noise = np.array([[sigma_T / np.sqrt(2 * m_T), 0, 0, 0, 0],
672
+ [sigma_h / np.sqrt(2 * m_h), 0, 0, 0, 0],
673
+ [sigma_B/sigma_T, 0, 0, 0, 0],
674
+ [m_T, 0, 0, 0, 0],
675
+ [m_h, 0, 0, 0, 0],
676
+ [n_T, 0, 0, 0, 0],
677
+ [n_h, 0, 0, 0, 0],
678
+ [n_g, 0, 0, 0, 0]])
679
+ par_combined = np.vstack([par_T_h, par_noise])
680
+
681
+ return par_combined
682
+
683
+
684
+
685
+ def fit_MLE(T, h, par_option_T, par_option_h, par_option_noise, dt):
686
+ """
687
+ Estimate recharge oscillator (RO) parameters using **Maximum Likelihood Estimation (MLE)**
688
+
689
+ Parameters
690
+ ----------
691
+ T : array_like
692
+ Time series of SST anomalies (1D).
693
+ h : array_like
694
+ Time series of thermocline depth anomalies (1D).
695
+ par_option_T : array_like
696
+ Switches for which SST-related parameters to include
697
+ (0=off, 1=on, 3=annual, 5=annual+semiannual).
698
+ par_option_h : array_like
699
+ Switches for which h-related parameters to include (same coding).
700
+ par_option_noise : array_like of length 3
701
+ Noise structure options `[n_T, n_h, n_g]`:
702
+ - n_T : noise option for T-equation
703
+ - n_h : noise option for h-equation
704
+ - n_g : type of multiplicative noise term
705
+ * 0 = linear multiplicative (`B*T`)
706
+ * 1 = Heaviside multiplicative (`B*H(T)*T`)
707
+ * 2 = additive (`B=0`)
708
+ dt : float
709
+ Time step (months).
710
+
711
+ Returns
712
+ -------
713
+ par : ndarray
714
+ Final estimated parameter set including deterministic RO parameters,
715
+ seasonal amplitudes/phases, and red noise variances.
716
+
717
+ Notes
718
+ -----
719
+ - Implements iterative forward filtering and backward smoothing
720
+ to estimate hidden noise states.
721
+ - This function may take a long time to converge (default 10 iterations,
722
+ could be increased for accuracy).
723
+ """
724
+
725
+ n_T = par_option_noise[0]
726
+ n_h = par_option_noise[1]
727
+
728
+ if n_T == 1 and n_h == 1:
729
+ par = fit_MLE_white(T, h, par_option_T, par_option_h, par_option_noise, dt)
730
+ elif n_T == 0 and n_h == 0:
731
+ par = fit_MLE_red(T, h, par_option_T, par_option_h, par_option_noise, dt)
732
+ else:
733
+ raise ValueError("Error: Mixed noise types not supported by MLE")
734
+
735
+ return par