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/__init__.py +16 -0
- pyCRO/analytic.py +244 -0
- pyCRO/fit_LR.py +341 -0
- pyCRO/fit_MAC.py +90 -0
- pyCRO/fit_MLE.py +735 -0
- pyCRO/fitting.py +259 -0
- pyCRO/par_load.py +185 -0
- pyCRO/solver.py +488 -0
- pyCRO/table_default_fitting_method.txt +26 -0
- pyCRO/utils.py +91 -0
- pyCRO/visual.py +296 -0
- pythoncro-0.1.8.data/data/pyCRO_data/CESM1_LENS_ENSO_timeseries.nc +0 -0
- pythoncro-0.1.8.data/data/pyCRO_data/CRO_logo.png +0 -0
- pythoncro-0.1.8.data/data/pyCRO_data/CRO_parlib_v0.0.mat +0 -0
- pythoncro-0.1.8.data/data/pyCRO_data/CROdata_timeseries_CMIP6.nc +0 -0
- pythoncro-0.1.8.data/data/pyCRO_data/CROdata_timeseries_oras5.nc +0 -0
- pythoncro-0.1.8.data/data/pyCRO_data/XRO_indices_oras5.nc +0 -0
- pythoncro-0.1.8.dist-info/METADATA +88 -0
- pythoncro-0.1.8.dist-info/RECORD +22 -0
- pythoncro-0.1.8.dist-info/WHEEL +5 -0
- pythoncro-0.1.8.dist-info/licenses/LICENSE +201 -0
- pythoncro-0.1.8.dist-info/top_level.txt +1 -0
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
|