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/solver.py ADDED
@@ -0,0 +1,488 @@
1
+ import sys
2
+ import numpy as np
3
+
4
+ #############################
5
+ ### Euler-Maruyama scheme ###
6
+
7
+ def EM_scheme(x, f, g, dt, noise=None):
8
+ """
9
+ Euler-Maruyama scheme.
10
+
11
+ Supports scalar or vectorized inputs:
12
+ x, f, g can be scalars or numpy arrays of shape (NE,).
13
+
14
+ If noise is None, generates standard normal random noise of same shape.
15
+ If noise contains np.nan, replaces with fresh noise.
16
+
17
+ Returns:
18
+ xs : new x after one step
19
+ noise : the actual noise used
20
+ """
21
+ if noise is None:
22
+ noise = np.random.randn(*np.shape(x))
23
+ else:
24
+ # Replace any np.nan in noise with new random draws
25
+ noise = np.where(np.isnan(noise), np.random.randn(*np.shape(x)), noise)
26
+
27
+ dW = np.sqrt(dt) * noise
28
+ xs = x + f * dt + g * dW
29
+ return xs, noise
30
+
31
+
32
+ ### This module generates time-evolving RO parameters and external forcing for use in the RO solver. ###
33
+ ########################################################################################################
34
+ def par_processing(par_in, dt, NT):
35
+ num_params = len(par_in)
36
+ par_out = np.ones((NT, num_params))
37
+
38
+ ro_time = np.linspace(0, (NT - 1) * dt, NT)
39
+
40
+ for i, par in enumerate(par_in):
41
+ params = par_in[par]
42
+ if np.isscalar(params): # if 'n_T'/'n_h'/'n_g' is given as a scalar instead of list, convert it to list
43
+ params = [params]
44
+
45
+ if (par == 'n_T') or (par == 'n_h') or (par == 'n_g'):
46
+ par_out[:, i] = params[0]
47
+ else:
48
+ w = 2 * np.pi / 12
49
+ par_out[:, i] = (params[0] + params[1] * np.sin(w * ro_time + params[2]) +
50
+ params[3] * np.sin(2 * w * ro_time + params[4]))
51
+ return par_out # array of shape (NT, 16) or (NT, 2) containing time-evolving parameters
52
+ ########################################################################################################
53
+
54
+ ### RO tendency equations ###
55
+ #############################
56
+ def RO_tendency(par, T, h, xi_T, xi_h, EF):
57
+ """
58
+ Vectorized RO tendency function.
59
+
60
+ Supports scalar or array inputs (NE,).
61
+ """
62
+ # Unpack parameters
63
+ R, F1, F2, epsilon = par[0:4]
64
+ b_T, c_T, d_T, b_h = par[4:8]
65
+ sigma_T, sigma_h, B = par[8:11]
66
+ m_T, m_h = par[11:13]
67
+ n_T, n_h, n_g = map(int, par[13:16]) # Ensure integer indexing
68
+
69
+ # Unpack external forcing
70
+ E_T, E_h = EF
71
+
72
+ ### Noise factor ###
73
+ ####################
74
+ if n_g == 0:
75
+ g_T_factor = B * T
76
+ elif n_g == 1:
77
+ g_T_factor = B * np.maximum(T, 0) # Heaviside(T)
78
+ elif n_g == 2:
79
+ g_T_factor = 0.0
80
+ else:
81
+ raise ValueError("Invalid value for n_g")
82
+ ####################
83
+
84
+ ### dT/dt ###
85
+ #############
86
+ if n_T == 0: # red noise
87
+ f_T = R*T + F1*h + b_T*(T**2) - c_T*(T**3) + d_T*T*h + sigma_T*(1 + g_T_factor)*xi_T + E_T
88
+ g_T = 0.0
89
+ elif n_T == 1: # white noise
90
+ f_T = R*T + F1*h + b_T*(T**2) - c_T*(T**3) + d_T*T*h + E_T
91
+ g_T = sigma_T * (1 + g_T_factor)
92
+ else:
93
+ raise ValueError("Invalid value for n_T")
94
+
95
+ ### dh/dt ###
96
+ ##############
97
+ if n_h == 0: # red noise
98
+ f_h = -F2*T - epsilon*h - b_h*(T**2) + sigma_h*xi_h + E_h
99
+ g_h = 0.0
100
+ elif n_h == 1: # white noise
101
+ f_h = -F2*T - epsilon*h - b_h*(T**2) + E_h
102
+ g_h = sigma_h
103
+ else:
104
+ raise ValueError("Invalid value for n_h")
105
+
106
+ ### d(xi_T)/dt ###
107
+ ##################
108
+ if n_T == 0:
109
+ f_xi_T = -m_T * xi_T
110
+ g_xi_T = np.sqrt(2 * m_T)
111
+ else:
112
+ f_xi_T = np.nan
113
+ g_xi_T = np.nan
114
+
115
+ ### d(xi_h)/dt ###
116
+ ##################
117
+ if n_h == 0:
118
+ f_xi_h = -m_h * xi_h
119
+ g_xi_h = np.sqrt(2 * m_h)
120
+ else:
121
+ f_xi_h = np.nan
122
+ g_xi_h = np.nan
123
+
124
+ return f_T, g_T, f_h, g_h, f_xi_T, g_xi_T, f_xi_h, g_xi_h
125
+ #############################
126
+ #############################
127
+
128
+ ### RO integrater (Euler-Maruyama and Euler-Huen) ###
129
+ #####################################################
130
+ def RO_integral(par, EF, NM, NT, dt, T0, h0, noise_all):
131
+ """
132
+ Vectorized RO integrator over ensemble members.
133
+
134
+ Inputs:
135
+ par : (NT, 16) array
136
+ EF : (NT, 2) array
137
+ T0, h0 : initial conditions (NE,)
138
+ noise_all: (NT-1, 4, NE)
139
+ """
140
+ NE = T0.size
141
+
142
+ T = np.zeros((NT, NE))
143
+ h = np.zeros((NT, NE))
144
+ xi_T = np.zeros((NT, NE))
145
+ xi_h = np.zeros((NT, NE))
146
+
147
+ T[0,:] = T0
148
+ h[0,:] = h0
149
+
150
+ noise_out = np.full((NT-1, 4, NE), np.nan)
151
+
152
+ for i in range(NT - 1):
153
+ f_T, g_T, f_h, g_h, f_xi_T, g_xi_T, f_xi_h, g_xi_h = RO_tendency(
154
+ par[i], T[i], h[i], xi_T[i], xi_h[i], EF[i]
155
+ )
156
+
157
+ if NM == "EM":
158
+ T[i+1], noise_out[i,0] = EM_scheme(T[i], f_T, g_T, dt, noise_all[i,0])
159
+ h[i+1], noise_out[i,1] = EM_scheme(h[i], f_h, g_h, dt, noise_all[i,1])
160
+ xi_T[i+1], noise_out[i,2] = EM_scheme(xi_T[i], f_xi_T, g_xi_T, dt, noise_all[i,2])
161
+ xi_h[i+1], noise_out[i,3] = EM_scheme(xi_h[i], f_xi_h, g_xi_h, dt, noise_all[i,3])
162
+
163
+ elif NM == "EH":
164
+ T_s, noise_out[i,0] = EM_scheme(T[i], f_T, g_T, dt, noise_all[i,0])
165
+ h_s, noise_out[i,1] = EM_scheme(h[i], f_h, g_h, dt, noise_all[i,1])
166
+ xi_T_s, noise_out[i,2] = EM_scheme(xi_T[i], f_xi_T, g_xi_T, dt, noise_all[i,2])
167
+ xi_h_s, noise_out[i,3] = EM_scheme(xi_h[i], f_xi_h, g_xi_h, dt, noise_all[i,3])
168
+
169
+ f_T_s, g_T_s, f_h_s, g_h_s, f_xi_T_s, g_xi_T_s, f_xi_h_s, g_xi_h_s = RO_tendency(
170
+ par[i+1], T_s, h_s, xi_T_s, xi_h_s, EF[i+1]
171
+ )
172
+
173
+ T[i+1], _ = EM_scheme(T[i], 0.5*(f_T+f_T_s), 0.5*(g_T+g_T_s), dt, noise_out[i,0])
174
+ h[i+1], _ = EM_scheme(h[i], 0.5*(f_h+f_h_s), 0.5*(g_h+g_h_s), dt, noise_out[i,1])
175
+ xi_T[i+1], _ = EM_scheme(xi_T[i], 0.5*(f_xi_T+f_xi_T_s), 0.5*(g_xi_T+g_xi_T_s), dt, noise_out[i,2])
176
+ xi_h[i+1], _ = EM_scheme(xi_h[i], 0.5*(f_xi_h+f_xi_h_s), 0.5*(g_xi_h+g_xi_h_s), dt, noise_out[i,3])
177
+
178
+ return T, h, noise_out
179
+ #####################################################
180
+ #####################################################
181
+
182
+
183
+ ### Shapes of RO parameters and external forcing ###
184
+ ####################################################
185
+ par_shapes = ({'R': 5, 'F1': 5, 'F2': 5, 'epsilon': 5,
186
+ 'b_T': 5, 'c_T': 5, 'd_T': 5, 'b_h': 5,
187
+ 'sigma_T': 5, 'sigma_h': 5, 'B': 5,
188
+ 'm_T': 5, 'm_h': 5,
189
+ 'n_T': 1, 'n_h': 1, 'n_g': 1})
190
+ EF_shapes = {'E_T': 5, 'E_h': 5}
191
+ ####################################################
192
+ ####################################################
193
+
194
+
195
+ ### Checking input par and EF have expected shapes ###
196
+ ######################################################
197
+ def has_same_shape(other_dict, par_shapes):
198
+ for key in par_shapes:
199
+ if not isinstance(other_dict[key], list):
200
+ other_dict[key] = [other_dict[key]]
201
+ key_len = len(other_dict[key])
202
+ if not key in ['n_T', 'n_h', 'n_g']:
203
+ if key_len not in [0, 1, 3, 5]:
204
+ raise ValueError(f"Shape mismatch for key '{key}': expected length 0, 1, 3, or 5, but got {key_len}.")
205
+ elif key_len in [0, 1, 3, 5]:
206
+ other_dict[key] = other_dict[key] + [0.0] * (5 - len(other_dict[key]))
207
+ else:
208
+ if key_len != 1:
209
+ raise ValueError(f"Shape mismatch for key '{key}': expected length 1, but got {key_len}.")
210
+ return True
211
+ ######################################################
212
+ ######################################################
213
+
214
+
215
+ ### RO solver ###
216
+ #################
217
+ def RO_solver(par, IC, N, NE, NM="EH", dt=0.1, saveat=1.0, savemethod="sampling", EF=None, noise_custom=None, verbose=True):
218
+ """
219
+ Numerical solution of the Recharge Oscillator (RO) model with stochastic forcing.
220
+
221
+ This function integrates the Recharge Oscillator (RO) system numerically using
222
+ either the Euler–Maruyama (EM) or Euler–Heun (EH) scheme. It includes
223
+ deterministic dynamics, stochastic noise (white or red), and optional
224
+ external forcing. Ensemble simulations are supported, with results returned
225
+ at user-specified output intervals.
226
+
227
+ Parameters
228
+ ----------
229
+ par : dict
230
+ Dictionary of model parameters with the following keys (as 1-element arrays):
231
+
232
+ - ``'R'`` : float
233
+ Damping parameter.
234
+ - ``'F1'`` : float
235
+ Feedback parameter relating thermocline depth to SST.
236
+ - ``'epsilon'`` : float
237
+ Thermocline damping parameter.
238
+ - ``'F2'`` : float
239
+ Feedback parameter relating SST to thermocline.
240
+ - ``'sigma_T'`` : float
241
+ Noise amplitude for SST.
242
+ - ``'sigma_h'`` : float
243
+ Noise amplitude for thermocline depth.
244
+ - ``'B'`` : float
245
+ Multiplicative noise coefficient (used when ``n_g`` = 0 or 1).
246
+ - ``'n_T'`` : int
247
+ Noise type in SST equation (``1`` = white noise, ``0`` = red noise).
248
+ - ``'m_T'`` : array-like
249
+ Memory kernel for red noise in SST equation (ignored if ``n_T=1``).
250
+ - ``'n_h'`` : int
251
+ Noise type in thermocline equation (``1`` = white noise, ``0`` = red noise).
252
+ - ``'m_h'`` : array-like
253
+ Memory kernel for red noise in thermocline equation (ignored if ``n_h=1``).
254
+ - ``'n_g'`` : int
255
+ Noise structure in SST equation:
256
+ ``0`` = multiplicative, ``1`` = multiplicative + Heaviside, ``2`` = additive.
257
+ IC : tuple of float
258
+ Initial condition ``(T0, h0)``:
259
+
260
+ - ``T0`` : initial SST anomaly.
261
+ - ``h0`` : initial thermocline depth anomaly.
262
+ N : float
263
+ Total simulation length (time units).
264
+ NE : int
265
+ Number of ensemble members.
266
+ NM : {'EM', 'EH'}, optional
267
+ Numerical integration method. Default is ``'EH'`` (Euler–Heun).
268
+ dt : float, optional
269
+ Numerical integration time step. Default is ``0.1``.
270
+ saveat : float, optional
271
+ Output saving interval. Must be divisible by ``dt``. Default is ``1.0``.
272
+ savemethod : {'sampling', 'mean'}, optional
273
+ Method for saving results:
274
+
275
+ - ``'sampling'`` : store values every ``saveat`` steps (default).
276
+ - ``'mean'`` : average values within each ``saveat`` interval.
277
+ EF : dict, optional
278
+ External forcing with the following keys (as 1D arrays of length 5):
279
+
280
+ - ``'E_T'`` : SST forcing coefficients.
281
+ - ``'E_h'`` : Thermocline forcing coefficients.
282
+
283
+ If ``None`` (default), no external forcing is applied.
284
+ noise_custom : {None, int, ndarray}, optional
285
+ Specification of stochastic noise:
286
+
287
+ - ``None`` (default): generate new Gaussian noise for each ensemble.
288
+ - ``int`` : random seed for reproducible noise (same across ensembles).
289
+ - ``ndarray`` of shape (NT-1, 4, NE) : user-provided noise realizations.
290
+ verbose : bool, optional
291
+ If ``True`` (default), print detailed setup and progress messages.
292
+
293
+ Returns
294
+ -------
295
+ T_out : ndarray of shape (N_out, NE)
296
+ SST anomalies from the numerical integration, after applying the saving scheme.
297
+ h_out : ndarray of shape (N_out, NE)
298
+ Thermocline anomalies from the numerical integration, after applying the saving scheme.
299
+ noise_out : ndarray of shape (NT-1, 4, NE)
300
+ Realizations of Gaussian noise used in the simulation.
301
+
302
+ Notes
303
+ -----
304
+ - White noise forcing corresponds to ``n_T = 1`` or ``n_h = 1``.
305
+ - Red noise forcing (``n_T = 0`` or ``n_h = 0``) requires a nonzero memory kernel ``m_T`` or ``m_h``.
306
+ - For the Euler–Maruyama method (``NM = 'EM'``) with multiplicative noise,
307
+ an Ito-to-Stratonovich correction is applied automatically.
308
+
309
+ Examples
310
+ --------
311
+ >>> par = {
312
+ ... 'R':[0.5], 'F1':[1.2], 'epsilon':[0.3], 'F2':[0.8],
313
+ ... 'sigma_T':[0.2], 'sigma_h':[0.1], 'B':[0.05],
314
+ ... 'n_T':[1], 'm_T':[0], 'n_h':[1], 'm_h':[0], 'n_g':[0]
315
+ ... }
316
+ >>> IC = (0.1, -0.2)
317
+ >>> T, h, noise = RO_solver(par, IC, N=50, NE=5, dt=0.1, saveat=1.0)
318
+ >>> T.shape, h.shape
319
+ ((51, 5), (51, 5))
320
+ """
321
+
322
+ NT = int(round(N / dt))
323
+ step = int(round(saveat / dt))
324
+ ratio = saveat / dt
325
+
326
+ ### Checking simulation setups ###
327
+ if verbose:
328
+ print("---------------------------------------------------------------------------------")
329
+ print("Welcome to the CRO Solver! Your simulation setup is as follows:")
330
+ print("---------------------------------------------------------------------------------")
331
+ print(f" - Total simulation length: N = {N} months")
332
+ print(f" - Number of ensemble members: NE = {NE}")
333
+ print(f" - Numerical integration time step: dt = {dt} months (default: 0.1)")
334
+ print(f" - Data output interval: saveat = {saveat} months (default: 1.0)")
335
+
336
+ ### Check timestep settings ###
337
+ if dt > saveat:
338
+ raise ValueError(f"dt = {dt}, saveat = {saveat}: The numerical time step (dt) must be less than or equal to the data saving interval (saveat).")
339
+ if not ratio.is_integer():
340
+ raise ValueError(f"dt = {dt}, saveat = {saveat}: The numerical time step (dt) must be less than or equal to the data saving interval (saveat).")
341
+
342
+ ### Check Initial conditions ###
343
+ if np.array(IC).shape != (2,):
344
+ raise ValueError(f"Invalid 'IC' input: expected an array with shape (2,), but got {IC}.")
345
+ elif verbose:
346
+ print(f" - Initial conditions: IC = [T0, h0] = {IC}")
347
+
348
+ ### Check input parameter shapes ###
349
+ if not has_same_shape(par, par_shapes):
350
+ raise ValueError("Input parameters do not have the expected shape.")
351
+ elif verbose:
352
+ print(f" - Input parameters have the expected shapes.")
353
+
354
+ ### Check noise parameters ###
355
+ if verbose:
356
+ if np.array(par['n_T']).item() == 1:
357
+ print(" - 'n_T' = 1: White noise forcing in T; 'm_T' ignored.")
358
+ elif np.array(par['n_T']).item() == 0:
359
+ print(f" - 'n_T' = 0: Red noise forcing in T with m_T = {par['m_T']}.")
360
+ if np.all(np.array(par['m_T']) == 0):
361
+ raise ValueError(f"'m_T' needs to have at least one non-zero argument for red noise forcing.")
362
+ else:
363
+ raise ValueError(f"n_T = {np.array(par['n_T']).item()} is an invalid option: it has to be either '1' (red noise) or '0' (white noise).")
364
+
365
+ if np.array(par['n_h']).item() == 1:
366
+ print(" - 'n_h' = 1: White noise forcing in h; 'm_h' ignored.")
367
+ elif np.array(par['n_h']).item() == 0:
368
+ print(f" - 'n_h' = 0: Red noise forcing in h with m_h = {par['m_h']}.")
369
+ if np.all(np.array(par['m_h']) == 0):
370
+ raise ValueError(f"'m_h' needs to have at least one non-zero argument for red noise forcing.")
371
+ else:
372
+ raise ValueError(f"n_h = {np.array(par['n_h']).item()} is an invalid option: it has to be either '1' (red noise) or '0' (white noise).")
373
+
374
+ if np.array(par['n_g']).item() == 2:
375
+ print(" - 'n_g' = 2: Additive noise is used in the T equation; 'B' is ignored.")
376
+ elif np.array(par['n_g']).item() == 0:
377
+ print(" - 'n_g' = 0: Multiplicative noise (1 + B*T) is used in the T equation.")
378
+ elif np.array(par['n_g']).item() == 1:
379
+ print(" - 'n_g' = 1: Multiplicative noise with the heaviside function (1 + B*H*T) is used in the T equation.")
380
+ else:
381
+ raise ValueError(f"n_g = {np.array(par['n_g']).item()} is an invalid option. "
382
+ "It must be one of the following:\n"
383
+ " 0 — multiplicative\n"
384
+ " 1 — multiplicative + Heaviside\n"
385
+ " 2 — additive")
386
+
387
+ ### Check numerical integration method ###
388
+ if verbose:
389
+ if NM == "EM":
390
+ print(f" - Numerical integration method: NM = '{NM}' (Euler–Maruyama method)")
391
+ print(" The default method is NM = 'EH' (Euler–Heun method)")
392
+ elif NM == "EH":
393
+ print(f" - Numerical integration method: NM = '{NM}' (Euler–Heun method; default)")
394
+ else:
395
+ raise ValueError(
396
+ f"{NM} is an invalid NM input. Expected 'EM' (Euler–Maruyama method) or "
397
+ f"'EH' (Euler–Heun method)")
398
+
399
+ ### Check data saving method ###
400
+ if verbose:
401
+ if savemethod == "sampling":
402
+ print(f" - Data saving method: savemethod = {savemethod} (default)")
403
+ elif savemethod == "mean":
404
+ print(f" - Data saving method: savemethod = {savemethod} (default is 'sampling')")
405
+ else:
406
+ raise ValueError(f"{savemethod} is an invalid savemethod: it has to be either 'sampling' or 'mean'.")
407
+
408
+ ### Check external forcing ###
409
+ if EF is None:
410
+ if verbose:
411
+ print(" - External forcing is not given, therefore using\n EF = {'E_T': [0.0, 0.0, 0.0, 0.0, 0.0], 'E_h': [0.0, 0.0, 0.0, 0.0, 0.0]}.")
412
+ EF = {'E_T': [0.0, 0.0, 0.0, 0.0, 0.0], 'E_h': [0.0, 0.0, 0.0, 0.0, 0.0]}
413
+ elif has_same_shape(EF, EF_shapes):
414
+ if verbose:
415
+ print(f" - External forcing is given as\n EF = {EF}.")
416
+ else:
417
+ raise ValueError(f"- External forcing does not match the shape of {EF_shapes}.")
418
+
419
+ ### Check noise ###
420
+ if verbose:
421
+ if noise_custom is None:
422
+ print(f" - noise_custom = {noise_custom}: System-generated noise is used and changes at every run.")
423
+ elif np.isscalar(noise_custom):
424
+ print(f" - noise_custom = {noise_custom}: seeded same noise.")
425
+ elif noise_custom.shape == (NT-1,4,NE):
426
+ print(f" - You provided the customized noises with shapes (N/dt-1, 4) = ({NT-1}, 4).")
427
+ else:
428
+ raise ValueError(f"Invalid 'noise_custom' input: expected 'None', a scalar (int or float), "
429
+ f"or an array with shape (N/dt-1, 4) = ({NT-1}, 4).")
430
+ print("---------------------------------------------------------------------------------")
431
+
432
+ ### preprocessing parameters ###
433
+ if (NM == "EM") and (par['n_T'][0] == 1): # Do Ito to Strantonovich Conversion
434
+ # when Euler-Maruya scheme is used with white noise forcing
435
+ if par['n_g'][0] == 0: # Ito to Strantonovich Conversion for multiplicative noise
436
+ par['R'][0] = par['R'][0] + 0.5 * (par['sigma_T'][0] * par['B'][0])**2
437
+ elif par['n_g'][0] == 1: # Ito to Strantonovich Conversion for Heaviside multiplicative noise
438
+ par['R'][0] = par['R'][0] + 0.25 * (par['sigma_T'][0] * par['B'][0])**2
439
+ elif par['n_g'][0] == 2: # No correction needed for additive noise
440
+ par['R'][0] = par['R'][0]
441
+
442
+ par_in = par_processing(par, dt, NT)
443
+ EF_in = par_processing(EF, dt, NT)
444
+
445
+ ### initial conditions & noise ###
446
+ T0 = np.full(NE, IC[0])
447
+ h0 = np.full(NE, IC[1])
448
+
449
+ if noise_custom is None:
450
+ noise_all = np.random.randn(NT-1, 4, NE)
451
+ elif np.isscalar(noise_custom):
452
+ np.random.seed(noise_custom)
453
+ noise_all = np.random.randn(NT-1, 4, NE)
454
+ else:
455
+ noise_all = noise_custom
456
+
457
+ ### integration ###
458
+ if verbose:
459
+ print(f"Numerical integration starting:")
460
+ print("---------------------------------------------------------------------------------")
461
+ T_out, h_out, noise_out = RO_integral(par_in, EF_in, NM, NT, dt, T0, h0, noise_all)
462
+ if verbose:
463
+ print("---------------------------------------------------------------------------------")
464
+
465
+ ### save ###
466
+ if (savemethod == "sampling") or (savemethod is None):
467
+ T_out = T_out[::step, :]
468
+ h_out = h_out[::step, :]
469
+ elif savemethod == "mean":
470
+ T_out = T_out[int(np.ceil(step/2)):NT-int(step/2),:]
471
+ h_out = h_out[int(np.ceil(step/2)):NT-int(step/2),:]
472
+ T_out = T_out.reshape(-1, step, NE).mean(axis=1)
473
+ h_out = h_out.reshape(-1, step, NE).mean(axis=1)
474
+ if NE > 1:
475
+ T_out = np.vstack([T_out[0:1, :], T_out])
476
+ h_out = np.vstack([h_out[0:1, :], h_out])
477
+ elif NE == 1:
478
+ T_out = np.concatenate([[T_out[0]], T_out])
479
+ h_out = np.concatenate([[h_out[0]], h_out])
480
+
481
+ if verbose:
482
+ print("All steps successfully completed!")
483
+ print("---------------------------------------------------------------------------------")
484
+
485
+ return T_out, h_out, noise_out
486
+
487
+ #################
488
+ #################
@@ -0,0 +1,26 @@
1
+ seasonal_type det_type noise_color_type noise_amp_type fitting_method
2
+ constant linear white additive LR-F
3
+ constant linear white multiplicative MLE
4
+ constant linear white multiplicative-H MLE
5
+ constant linear red additive LR-F
6
+ constant linear red multiplicative LR-F-MAC
7
+ constant linear red multiplicative-H LR-F
8
+ constant nonlinear white additive LR-F
9
+ constant nonlinear white multiplicative LR-F-MAC
10
+ constant nonlinear white multiplicative-H MLE
11
+ constant nonlinear red additive LR-F
12
+ constant nonlinear red multiplicative LR-F-MAC
13
+ constant nonlinear red multiplicative-H LR-F
14
+ seasonal linear white additive LR-F
15
+ seasonal linear white multiplicative MLE
16
+ seasonal linear white multiplicative-H MLE
17
+ seasonal linear red additive LR-F
18
+ seasonal linear red multiplicative LR-F-MAC
19
+ seasonal linear red multiplicative-H LR-F
20
+ seasonal nonlinear white additive LR-F
21
+ seasonal nonlinear white multiplicative LR-F-MAC
22
+ seasonal nonlinear white multiplicative-H MLE
23
+ seasonal nonlinear red additive LR-F
24
+ seasonal nonlinear red multiplicative LR-F-MAC
25
+ seasonal nonlinear red multiplicative-H LR-F
26
+
pyCRO/utils.py ADDED
@@ -0,0 +1,91 @@
1
+ import numpy as np
2
+ import statsmodels.api as sm
3
+
4
+ def wrap_to_pi(angle):
5
+ return (angle + np.pi) % (2 * np.pi) - np.pi
6
+
7
+ def heaviside(x):
8
+ return np.where(x > 0, 1, 0)
9
+
10
+ def func_mon_std(X, dt=1.0):
11
+ X = np.array(X).flatten()
12
+ months_per_year = int(12 / dt)
13
+ num_years = int(len(X) / months_per_year)
14
+
15
+ if len(X) % months_per_year != 0:
16
+ X = X[:months_per_year * num_years] # trim excess
17
+
18
+ X_mon = X.reshape((num_years, months_per_year))
19
+ X_mon_std = np.std(X_mon, axis=0)
20
+ return X_mon_std
21
+
22
+
23
+ def RO_BWJ(par):
24
+ # Extract annual mean values
25
+ R_value = par['R'][0]
26
+ F1_value = par['F1'][0]
27
+ epsilon_value = par['epsilon'][0]
28
+ F2_value = par['F2'][0]
29
+
30
+ # Calculate growth rate and frequency
31
+ gr = (R_value - epsilon_value) / 2
32
+ w = np.sqrt(4 * F1_value * F2_value - (R_value + epsilon_value)**2) / 2
33
+
34
+ # Return complex index
35
+ return gr + 1j * w
36
+
37
+
38
+ def regress_std(y, X, alpha=0.05):
39
+ X = np.asarray(X)
40
+ y = np.asarray(y).flatten()
41
+
42
+
43
+ # Detect intercept column
44
+ intercept_idx = np.where(np.all(X == 1, axis=0))[0]
45
+ has_intercept = len(intercept_idx) > 0
46
+
47
+
48
+ # Remove intercept for standardization
49
+ X_noint = np.delete(X, intercept_idx, axis=1) if has_intercept else X
50
+
51
+ mu_X = X_noint.mean(axis=0)
52
+ sigma_X = X_noint.std(axis=0, ddof=0)
53
+
54
+ if np.isscalar(sigma_X):
55
+ sigma_X = np.array([sigma_X])
56
+ sigma_X[sigma_X == 0] = 1
57
+ X_std = (X_noint - mu_X) / sigma_X
58
+
59
+ mu_y = y.mean()
60
+ sigma_y = y.std(ddof=0)
61
+ if sigma_y == 0:
62
+ sigma_y = 1
63
+ y_std = (y - mu_y) / sigma_y
64
+
65
+ # Add intercept back
66
+ if has_intercept:
67
+ X_reg = np.column_stack([np.ones(len(X)), X_std])
68
+ else:
69
+ X_reg = X_std
70
+
71
+ # Regress
72
+ model = sm.OLS(y_std, X_reg).fit()
73
+ b_std = model.params
74
+ r_std = model.resid
75
+
76
+ b = np.zeros(b_std.shape)
77
+ if has_intercept:
78
+ non_idx = [i for i in range(X.shape[1]) if i not in intercept_idx]
79
+ b[non_idx] = b_std[1:] * (sigma_y / sigma_X)
80
+ b[intercept_idx] = mu_y - np.sum((mu_X / sigma_X) * b_std[1:]) * sigma_y
81
+ else:
82
+ b = b_std * (sigma_y / sigma_X)
83
+
84
+ if len(b) == 1:
85
+ X = X.reshape(-1, 1)
86
+ b = b.reshape(1, -1)
87
+ y_hat = X @ b
88
+ else:
89
+ y_hat = X @ b
90
+ r = y - y_hat
91
+ return b, None, r