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 ADDED
@@ -0,0 +1,16 @@
1
+ # __init__.py
2
+
3
+ __version__ = "0.1.8"
4
+
5
+ from .fitting import RO_fitting
6
+ from .solver import RO_solver
7
+
8
+ from .analytic import RO_analytic_std, RO_analytic_solver
9
+
10
+ from .utils import RO_BWJ, func_mon_std
11
+ from .par_load import par_load
12
+
13
+ from .fit_LR import fit_LR
14
+ from .fit_MLE import fit_MLE
15
+
16
+ from .visual import plot_RO_par, plot_ens_RO_par
pyCRO/analytic.py ADDED
@@ -0,0 +1,244 @@
1
+ import sys
2
+ import numpy as np
3
+
4
+ def RO_analytic_std(par):
5
+ """
6
+ Compute analytical standard deviation of T and h for the Recharge Oscillator (RO) model.
7
+ ONLY for linear RO model with white noise (annual mean parameters only)
8
+
9
+ Parameters
10
+ ----------
11
+ par : dict
12
+ Dictionary containing parameter arrays:
13
+ 'R', 'F1', 'epsilon', 'F2', 'sigma_T', 'sigma_h'.
14
+
15
+ Returns
16
+ -------
17
+ T_std : float
18
+ Standard deviation of T.
19
+ h_std : float
20
+ Standard deviation of h.
21
+ """
22
+
23
+ R_value = par['R'][0]
24
+ F1_value = par['F1'][0]
25
+ epsilon_value = par['epsilon'][0]
26
+ F2_value = par['F2'][0]
27
+ sigma_T_value = par['sigma_T'][0]
28
+ sigma_h_value = par['sigma_h'][0]
29
+
30
+ # Precompute useful terms
31
+ numerator_T = ((F1_value * F2_value - epsilon_value * R_value + epsilon_value**2) * sigma_T_value**2 +
32
+ (F1_value**2) * sigma_h_value**2)
33
+ denominator = 2 * (-R_value + epsilon_value) * (F1_value * F2_value - R_value * epsilon_value)
34
+ T_std = np.sqrt(numerator_T / denominator)
35
+
36
+ numerator_h = ((F2_value**2) * sigma_T_value**2 +
37
+ (F1_value * F2_value - epsilon_value * R_value + R_value**2) * sigma_h_value**2)
38
+ h_std = np.sqrt(numerator_h / denominator)
39
+
40
+ return T_std, h_std
41
+
42
+
43
+ def RO_analytic_solver(par, IC, N, NE, dt=0.1, saveat=1.0, savemethod="sampling", noise_custom=None):
44
+ """
45
+ Analytical solution of the Recharge Oscillator (RO) model with deterministic and stochastic forcing.
46
+ ONLY for linear RO model with white noise (annual mean parameters only)
47
+
48
+ Parameters
49
+ ----------
50
+ par : dict
51
+ Dictionary of model parameters, each as at least a one-element array:
52
+
53
+ - ``'R'`` : float
54
+ Damping parameter.
55
+ - ``'F1'`` : float
56
+ Feedback parameter relating thermocline depth to SST.
57
+ - ``'epsilon'`` : float
58
+ Thermocline damping parameter.
59
+ - ``'F2'`` : float
60
+ Feedback parameter relating SST to thermocline.
61
+ - ``'sigma_T'`` : float
62
+ Noise amplitude for SST.
63
+ - ``'sigma_h'`` : float
64
+ Noise amplitude for thermocline depth.
65
+ IC : tuple of float
66
+ Initial condition ``(To, ho)``:
67
+
68
+ - ``To`` : initial SST anomaly.
69
+ - ``ho`` : initial thermocline depth anomaly.
70
+ N : float
71
+ Total simulation length (time units).
72
+ NE : int
73
+ Number of ensemble members.
74
+ dt : float, optional
75
+ Numerical integration time step. Default is ``0.1``.
76
+ saveat : float, optional
77
+ Output saving interval. Must be divisible by ``dt``. Default is ``1.0``.
78
+ savemethod : {'sampling', 'mean'}, optional
79
+ Method for saving results:
80
+
81
+ - ``'sampling'`` : take samples every ``saveat``.
82
+ - ``'mean'`` : average values over each ``saveat`` interval.
83
+ noise_custom : {None, int, ndarray}, optional
84
+ Specification of stochastic noise:
85
+
86
+ - ``None`` (default): new Gaussian noise is generated for each ensemble.
87
+ - ``int`` : seed for reproducible noise (same across ensembles).
88
+ - ``ndarray`` of shape ``(NT-1, 4)`` : user-provided noise, repeated across ensembles.
89
+ - ``ndarray`` of shape ``(NT-1, 4, NE)`` : user-provided noise for each ensemble.
90
+
91
+ Returns
92
+ -------
93
+ T_anal_out : ndarray of shape (N_out, NE)
94
+ SST anomalies including deterministic and stochastic forcing,
95
+ after applying the saving scheme. The final row is NaN-padded
96
+ for dimensional consistency.
97
+ h_anal_out : ndarray of shape (N_out, NE)
98
+ Thermocline anomalies including deterministic and stochastic forcing,
99
+ after applying the saving scheme. The final row is NaN-padded.
100
+ noise_array : ndarray of shape (NT-1, 4, NE)
101
+ Realizations of Gaussian noise used in the simulation.
102
+
103
+ Notes
104
+ -----
105
+ The analytical solution consists of:
106
+
107
+ - Deterministic part: exponential-sinusoidal functions of time.
108
+ - Stochastic part: weighted sums (discrete convolutions) of noise
109
+ with exponential and sinusoidal kernels.
110
+
111
+ Ensemble members are evaluated simultaneously using vectorized
112
+ NumPy broadcasting, avoiding explicit Python loops.
113
+
114
+ Examples
115
+ --------
116
+ >>> par = {'R':[0.5], 'F1':[1.2], 'epsilon':[0.3], 'F2':[0.8],
117
+ ... 'sigma_T':[0.2], 'sigma_h':[0.1]}
118
+ >>> IC = (0.1, -0.2)
119
+ >>> T, h, noise = RO_solver_analytic(par, IC, N=50, NE=10, dt=0.1, saveat=1.0)
120
+ >>> T.shape, h.shape
121
+ ((51, 10), (51, 10))
122
+ """
123
+
124
+ NT = int(round(N / dt))
125
+ step = int(round(saveat / dt))
126
+ ratio = saveat / dt
127
+
128
+ ### Checking simulation setups ###
129
+ ##################################
130
+ print("---------------------------------------------------------------------------------")
131
+ print("Welcome to the CRO Analytical Solver!")
132
+ print("Ensure that the same arguments are provided as for RO_solver,\n except that 'NM' and 'EF' are not required.")
133
+ print("---------------------------------------------------------------------------------")
134
+
135
+ ### Check timestep settings ###
136
+ if dt > saveat:
137
+ raise ValueError(f"dt={dt} and saveat={saveat}: The numerical time step (dt) must be smaller than or equal to the data saving interval (saveat).")
138
+ if not ratio.is_integer():
139
+ raise ValueError(f"dt = {dt}, saveat = {saveat}: saveat must be divisible by dt so that saveat/dt results in an integer.")
140
+ ###############################
141
+
142
+ ### extracting linear and noise amplitude parameters ###
143
+ ########################################################
144
+ R_value = par['R'][0]
145
+ F1_value = par['F1'][0]
146
+ epsilon_value = par['epsilon'][0]
147
+ F2_value = par['F2'][0]
148
+ sigma_T_value = par['sigma_T'][0]
149
+ sigma_h_value = par['sigma_h'][0]
150
+ ########################################################
151
+ ########################################################
152
+
153
+
154
+ ### initial setups ###
155
+ ######################
156
+ To, ho = IC
157
+ t = np.arange(NT) * dt
158
+ gr = (R_value - epsilon_value) / 2
159
+ w = np.sqrt(4 * F1_value * F2_value - (R_value + epsilon_value)**2) / 2
160
+
161
+ T_anal_out = np.zeros((NT, NE))
162
+ h_anal_out = np.zeros((NT, NE))
163
+
164
+ T_det = np.exp(gr * t) * (To * np.cos(w * t) +
165
+ To * (R_value + epsilon_value) / (2 * w) * np.sin(w * t) +
166
+ ho * (F1_value / w) * np.sin(w * t))
167
+
168
+ h_det = np.exp(gr * t) * (ho * np.cos(w * t) -
169
+ ho * (R_value + epsilon_value) / (2 * w) * np.sin(w * t) -
170
+ To * (F2_value / w) * np.sin(w * t))
171
+ ######################
172
+ ######################
173
+
174
+ ########################################################
175
+ # Noise generation (vectorized for all NE)
176
+ if noise_custom is None:
177
+ noise_array = np.random.randn(NT - 1, 4, NE)
178
+ elif np.isscalar(noise_custom):
179
+ np.random.seed(int(noise_custom))
180
+ noise_array = np.random.randn(NT - 1, 4, NE)
181
+ elif noise_custom.shape == (NT - 1, 4):
182
+ noise_array = np.repeat(noise_custom[..., None], NE, axis=2)
183
+ elif noise_custom.shape == (NT - 1, 4, NE):
184
+ noise_array = noise_custom
185
+ else:
186
+ raise ValueError(f"Invalid 'noise_custom' input: expected None, scalar, shape ({NT-1},4) or ({NT-1},4,{NE}).")
187
+
188
+ w_T = sigma_T_value * noise_array[:, 0, :] / np.sqrt(dt) # (NT-1, NE)
189
+ w_h = sigma_h_value * noise_array[:, 1, :] / np.sqrt(dt) # (NT-1, NE)
190
+
191
+ # Vectorized convolution-like integral for stochastic terms
192
+ T_sto = np.zeros((NT - 1, NE))
193
+ h_sto = np.zeros((NT - 1, NE))
194
+
195
+ ### analytical calculations
196
+ ########################################################
197
+ for i in range(1, NT - 1):
198
+ tau = t[:i+1] # (i+1,)
199
+ delta = t[i] - tau # (i+1,)
200
+ sin = np.sin(w * delta) # (i+1,)
201
+ cos = np.cos(w * delta) # (i+1,)
202
+ expgr = np.exp(gr * delta) # (i+1,)
203
+
204
+ # (i+1, NE) via broadcasting
205
+ T_kernel = (
206
+ w_T[:i+1, :] * cos[:, None] +
207
+ w_T[:i+1, :] * (R_value + epsilon_value) / (2 * w) * sin[:, None] +
208
+ w_h[:i+1, :] * (F1_value / w) * sin[:, None]
209
+ )
210
+ h_kernel = (
211
+ w_h[:i+1, :] * cos[:, None] -
212
+ w_h[:i+1, :] * (R_value + epsilon_value) / (2 * w) * sin[:, None] -
213
+ w_T[:i+1, :] * (F2_value / w) * sin[:, None]
214
+ )
215
+
216
+ T_sto[i, :] = np.sum(dt * expgr[:, None] * T_kernel, axis=0)
217
+ h_sto[i, :] = np.sum(dt * expgr[:, None] * h_kernel, axis=0)
218
+
219
+ # Add deterministic and stochastic contributions
220
+ T_anal_out = np.vstack([T_det[:-1, None] + T_sto, np.full((1, NE), np.nan)])
221
+ h_anal_out = np.vstack([h_det[:-1, None] + h_sto, np.full((1, NE), np.nan)])
222
+
223
+ ### save data ###
224
+ #################
225
+ if savemethod == "sampling":
226
+ T_anal_out = T_anal_out[::step, :]
227
+ h_anal_out = h_anal_out[::step, :]
228
+ elif savemethod == "mean":
229
+ T_anal_out = T_anal_out[int(np.ceil(step/2)):NT-int(step/2),:]
230
+ h_anal_out = h_anal_out[int(np.ceil(step/2)):NT-int(step/2),:]
231
+ T_anal_out = T_anal_out.reshape(-1, step, NE).mean(axis=1)
232
+ h_anal_out = h_anal_out.reshape(-1, step, NE).mean(axis=1)
233
+ if NE > 1:
234
+ T_anal_out = np.vstack([T_anal_out[0:1, :], T_anal_out])
235
+ h_anal_out = np.vstack([h_anal_out[0:1, :], h_anal_out])
236
+ elif NE == 1:
237
+ T_anal_out = np.concatenate([[T_anal_out[0]], T_anal_out])
238
+ h_anal_out = np.concatenate([[h_anal_out[0]], h_anal_out])
239
+
240
+ print("All steps are successfully completed!")
241
+ print("---------------------------------------------------------------------------------")
242
+ #################
243
+ #################
244
+ return T_anal_out, h_anal_out, noise_array
pyCRO/fit_LR.py ADDED
@@ -0,0 +1,341 @@
1
+ import sys
2
+ import numpy as np
3
+ from .utils import wrap_to_pi, regress_std, heaviside
4
+ from .fit_MAC import fit_MAC
5
+
6
+ def fit_LR(T, h, par_option_T, par_option_h, par_option_noise, dt,
7
+ tend_option, fitting_option_B, fitting_option_red):
8
+ """
9
+ Estimates Recharge Oscillator (RO) parameters with linear regression (LR)
10
+ given ENSO SST (T) and thermocline depth (h) anomaly time series.
11
+
12
+ Parameters
13
+ ----------
14
+ T : array_like
15
+ Time series of SST anomalies (1D).
16
+ h : array_like
17
+ Time series of thermocline depth anomalies (1D).
18
+ par_option_T : array_like
19
+ Seasonal fitting options for the T-equation terms.
20
+ Values must be one of:
21
+ - 0 : constant zero
22
+ - 1 : constant non-zero
23
+ - 3 : annual variation
24
+ - 5 : annual + semi-annual variation
25
+ par_option_h : array_like
26
+ Seasonal fitting options for the h-equation terms (same coding as above).
27
+ par_option_noise : array_like of length 3
28
+ Noise fitting options [n_T, n_h, n_g]:
29
+ - n_T : 0 = red, 1 = white
30
+ - n_h : 0 = red, 1 = white
31
+ - n_g : 0 = multiplicative (full),
32
+ 1 = multiplicative (Heaviside),
33
+ 2 = additive
34
+ dt : float
35
+ Time step (in months).
36
+ tend_option : {"F", "C"}
37
+ Option for numerical derivative:
38
+ - "F" : forward difference
39
+ - "C" : centered difference
40
+ fitting_option_B : {"MAC", "LR"}
41
+ Method for estimating multiplicative noise amplitude `B`:
42
+ - "MAC" : Moment Approximation Calibration
43
+ - "LR" : Linear regression
44
+ fitting_option_red : {"LR", "AR1", "ARn"}
45
+ Method for fitting red noise memory:
46
+ - "LR" : regression of residual derivative
47
+ - "AR1" : lag-1 autocorrelation
48
+ - "ARn" : regression using autocorrelation decay
49
+
50
+ Returns
51
+ -------
52
+ final_par : ndarray, shape (N, 5)
53
+ Stacked parameter array including:
54
+ - Deterministic T-equation parameters (seasonalized)
55
+ - Deterministic h-equation parameters (seasonalized)
56
+ - Noise parameters:
57
+ [sigma_T, sigma_h, B, m_T, m_h, n_T, n_h, n_g]
58
+
59
+ Notes
60
+ -----
61
+ - Seasonal terms are expanded into constant, annual, and semi-annual
62
+ components depending on the fitting options.
63
+ - Noise fitting can handle white/red and additive/multiplicative cases.
64
+ - If multiplicative noise is selected and `fitting_option_B="MAC"`,
65
+ the MAC method overwrites `sigma_T` and `B`.
66
+
67
+ """
68
+
69
+ T = np.ravel(T)
70
+ h = np.ravel(h)
71
+
72
+ par_option_T = np.array(par_option_T)
73
+ par_option_h = np.array(par_option_h)
74
+ par_option_noise = np.array(par_option_noise)
75
+
76
+ n_T, n_h, n_g = par_option_noise.astype(int)
77
+
78
+ par_option_T_onoff = (par_option_T >= 1).astype(int)
79
+ par_option_h_onoff = (par_option_h >= 1).astype(int)
80
+
81
+
82
+ # Derivative
83
+ if tend_option == "F":
84
+ Tdiff = np.diff(T) / dt
85
+ hdiff = np.diff(h) / dt
86
+ Ts = T[:-1]
87
+ hs = h[:-1]
88
+ elif tend_option == "C":
89
+ Tdiff = (T[2:] - T[:-2]) / (2 * dt)
90
+ hdiff = (h[2:] - h[:-2]) / (2 * dt)
91
+ Ts = T[1:-1]
92
+ hs = h[1:-1]
93
+
94
+ # Design matrices
95
+ Tv = np.column_stack([Ts, hs, Ts**2, -Ts**3, Ts * hs]) * par_option_T_onoff
96
+ hv = np.column_stack([-Ts, -hs, -Ts**2]) * par_option_h_onoff
97
+
98
+
99
+ # Seasonal features
100
+ w = 2 * np.pi / 12
101
+ t = np.arange(0, len(T)) * dt
102
+
103
+ if tend_option == "F":
104
+ t = t[:-1]
105
+ t_shift = t + 0.5 * dt
106
+ elif tend_option == "C":
107
+ t = t[1:-1]
108
+ t_shift = t
109
+
110
+
111
+
112
+ def add_seasonal(X=Tv, par_option=par_option_T, option="T"):
113
+ X_seasonal_extended = []
114
+ for i in range(0, len(par_option)):
115
+ if par_option[i] == 0:
116
+ on_and_off = np.array([0, 0, 0, 0, 0])
117
+ elif par_option[i] == 1:
118
+ on_and_off = np.array([1, 0, 0, 0, 0])
119
+ elif par_option[i] == 3:
120
+ on_and_off = np.array([1, 1, 1, 0, 0])
121
+ elif par_option[i] == 5:
122
+ on_and_off = np.array([1, 1, 1, 1, 1])
123
+ else:
124
+ raise ValueError(f"Invalid {option} fitting option {par_option}: "
125
+ "Value must be one of the following — 0 (constant zero), "
126
+ "1 (constant non-zero), 3 (annual variation), or 5 (annual + semi-annual variation).")
127
+
128
+
129
+
130
+ X_seasonal = np.column_stack([
131
+ X[:, i] * np.sin(w * t_shift),
132
+ X[:, i] * np.cos(w * t_shift),
133
+ X[:, i] * np.sin(2 * w * t_shift),
134
+ X[:, i] * np.cos(2 * w * t_shift)])
135
+ X_seasonal = np.hstack([X[:, i].reshape(-1, 1), X_seasonal])
136
+ X_seasonal = X_seasonal * on_and_off
137
+ X_seasonal_extended.append(X_seasonal)
138
+
139
+ X_seasonal_extended = np.array(X_seasonal_extended)
140
+ X_seasonal_extended = np.transpose(X_seasonal_extended, (1, 0, 2)) # shape becomes (1199, 5, 5)
141
+ if option == "T":
142
+ X_seasonal_extended = X_seasonal_extended.reshape(X.shape[0], 25)
143
+ if option == "h":
144
+ X_seasonal_extended = X_seasonal_extended.reshape(X.shape[0], 15)
145
+ return X_seasonal_extended
146
+
147
+ Tv = add_seasonal(Tv, par_option_T, option="T")
148
+ hv = add_seasonal(hv, par_option_h, option="h")
149
+
150
+ par_T, _, res_T = regress_std(Tdiff, Tv)
151
+ par_h, _, res_h = regress_std(hdiff, hv)
152
+
153
+
154
+ def extract_seasonal(params, option):
155
+ my_params = []
156
+ if option == "T":
157
+ index = 5
158
+ elif option == "h":
159
+ index = 3
160
+
161
+ params = params.reshape(index, 5)
162
+ for i in range(0, index):
163
+ A_a = np.sqrt(params[i,1]**2 + params[i,2]**2)
164
+ phi_a = np.mod(np.arctan2(params[i,2], params[i,1]), 2*np.pi)
165
+
166
+ A_sa = np.sqrt(params[i,3]**2 + params[i,4]**2)
167
+ phi_sa = np.mod(np.arctan2(params[i,4], params[i,3]), 2*np.pi)
168
+
169
+ if np.pi < phi_a:
170
+ phi_a = phi_a - 2*np.pi
171
+ if np.pi < phi_sa:
172
+ phi_sa = phi_sa - 2*np.pi
173
+ my_param = [params[i,0], A_a, phi_a, A_sa, phi_sa]
174
+ my_params.append(my_param)
175
+ my_params = np.array(my_params)
176
+
177
+ return my_params
178
+
179
+
180
+ par_T = extract_seasonal(par_T, option="T")
181
+ par_h = extract_seasonal(par_h, option="h")
182
+
183
+
184
+ # Noise fitting
185
+ res_T_norm = res_T * np.sqrt(dt)
186
+ res_h_norm = res_h * np.sqrt(dt)
187
+
188
+ if n_T == 1 and n_h == 1 and n_g == 2: # (white, white, additive)
189
+ sigma_T = np.std(res_T_norm)
190
+ sigma_h = np.std(res_h_norm)
191
+ B = m_T = m_h = 0.0 # np.nan
192
+
193
+ elif n_T == 0 and n_h == 0 and n_g == 2: # (red, red, additive)
194
+ if fitting_option_red == "LR":
195
+ res_Tdiff = np.diff(res_T) / dt
196
+ res_Ts = res_T[:-1]
197
+ m_T, _, _ = regress_std(res_Tdiff, res_Ts)
198
+
199
+ res_hdiff = np.diff(res_h) / dt
200
+ res_hs = res_h[:-1]
201
+ m_h, _, _ = regress_std(res_hdiff, res_hs)
202
+ m_T = np.abs(m_T.item())
203
+ m_h = np.abs(m_h.item())
204
+ elif fitting_option_red == "AR1":
205
+ m_T = -np.log(np.corrcoef(res_T[:-1], res_T[1:])[0, 1]) / dt
206
+ m_h = -np.log(np.corrcoef(res_h[:-1], res_h[1:])[0, 1]) / dt
207
+ elif fitting_option_red == "ARn":
208
+ # m_T part
209
+ r, lags = np.correlate(res_T - np.mean(res_T), res_T - np.mean(res_T), mode='full'), np.arange(-len(res_T)+1, len(res_T))
210
+ r = r / np.max(r)
211
+ lags_dt = lags * dt
212
+ positive_lags = lags_dt >= 0
213
+ r = r[positive_lags]
214
+ lags = lags_dt[positive_lags]
215
+
216
+ try:
217
+ cutoff = np.where(r < 0)[0][0]
218
+ r = r[:cutoff]
219
+ lags = lags[:cutoff]
220
+ except IndexError:
221
+ pass
222
+
223
+ y = -np.log(r)
224
+ X = lags.reshape(-1, 1)
225
+ m_T, _, _ = regress_std(y, X)
226
+
227
+ # m_h part
228
+ r, lags = np.correlate(res_h - np.mean(res_h), res_h - np.mean(res_h), mode='full'), np.arange(-len(res_h)+1, len(res_h))
229
+ r = r / np.max(r)
230
+ lags_dt = lags * dt
231
+ positive_lags = lags_dt >= 0
232
+ r = r[positive_lags]
233
+ lags = lags_dt[positive_lags]
234
+
235
+ try:
236
+ cutoff = np.where(r < 0)[0][0]
237
+ r = r[:cutoff]
238
+ lags = lags[:cutoff]
239
+ except IndexError:
240
+ pass
241
+
242
+ y = -np.log(r)
243
+ X = lags.reshape(-1, 1)
244
+ m_h, _, _ = regress_std(y, X)
245
+ m_T = m_T.item()
246
+ m_h = m_h.item()
247
+ else:
248
+ print(f"Invalid fitting method for red noise: {fitting_option_red}")
249
+ sys.exit()
250
+
251
+
252
+
253
+ sigma_T = np.std(res_T)
254
+ sigma_h = np.std(res_h)
255
+ B = 0 #np.nan
256
+
257
+ elif n_T == 1 and n_h == 1 and n_g in (0, 1): # (white, white, multiplicative (either full or Heaviside))
258
+ n = 10 * 12
259
+ res_T_var = np.zeros(10**5)
260
+ T_var = np.zeros(10**5)
261
+
262
+ for i in range(10**5):
263
+ ind = np.random.randint(0, len(res_T_norm), size=n)
264
+ res_T_var[i] = np.var(res_T_norm[ind], ddof=1) # ddof=1 for unbiased (same as MATLAB)
265
+
266
+ if n_g == 0.0:
267
+ T_var[i] = np.var(Ts[ind], ddof=1)
268
+ elif n_g == 1.0:
269
+ T_var[i] = np.var(np.heaviside(Ts[ind], 0) * Ts[ind], ddof=1)
270
+
271
+
272
+
273
+ X = np.column_stack([T_var, np.ones(len(T_var))])
274
+ par_T_res, _, _ = regress_std(res_T_var, X)
275
+ if par_T_res[0] < 0:
276
+ B = 0.0
277
+ else:
278
+ B = np.sqrt(par_T_res[0] / par_T_res[1])
279
+ sigma_T = np.sqrt(par_T_res[1])
280
+ sigma_h = np.std(res_h_norm)
281
+ m_T = m_h = 0.0
282
+
283
+ elif n_T == 0 and n_h == 0 and n_g in (0, 1): # (red, red, multiplicative (either full or Heaviside))
284
+ n = 10 * 12
285
+ res_T_var, T_var = [], []
286
+ for _ in range(10000):
287
+ ind = np.random.randint(0, len(res_T_norm), size=n)
288
+ res_T_var.append(np.var(res_T_norm[ind]))
289
+ if n_g == 0:
290
+ T_var.append(np.var(Ts[ind]))
291
+ else:
292
+ T_var.append(np.var(heaviside(Ts[ind]) * Ts[ind]))
293
+ X = np.column_stack([T_var, np.ones(len(T_var))])
294
+ res_T_var = np.array(res_T_var)
295
+ par_T_res, _, _ = regress_std(res_T_var, X)
296
+ sigma_T = np.sqrt(par_T_res[1])
297
+
298
+ if par_T_res[0] < 0:
299
+ B = 0.0
300
+ else:
301
+ B = np.sqrt(par_T_res[0] / par_T_res[1])
302
+ xi = res_T / (sigma_T * (1 + B * Ts))
303
+ m_T, _, _ = regress_std(np.diff(xi), xi[:-1].reshape(-1, 1))
304
+ m_T = np.abs(m_T.item())
305
+ res_hdiff = np.diff(res_h) / dt
306
+ res_hs = res_h[:-1]
307
+ m_h, _, _ = regress_std(res_hdiff, res_hs.reshape(-1, 1))
308
+ m_h = np.abs(m_h.item())
309
+ sigma_h = np.std(res_h)
310
+
311
+
312
+ #if (not np.isfinite(m_T) or m_T < 0.) or (not np.isfinite(m_h) or m_h < 0.):
313
+ # raise ValueError(f"Estimated m_T = {m_T} or m_h = {m_h} is not a valid number.\n"
314
+ # f"This error typically occurs when trying to fit red noise expressions into the time series "
315
+ # f"generated with white noise.\nTry using the fitting option for white noise instead.")
316
+
317
+ #if np.isnan(B):
318
+ # raise ValueError(f"B = np.sqrt({par_T_res[0]} / {par_T_res[1]}) is NaN.\n"
319
+ # f"This error typically occurs when trying to fit too many annual and semi-annual components "
320
+ # f"at the same time with multiplicative noise.\n"
321
+ # f"It can also occur when attempting to fit red noise expressions to a time series generated with white noise.\n"
322
+ # f"Try reducing the seasonality components and/or using the fitting option for white noise instead.")
323
+ #sys.exit(
324
+
325
+
326
+ par_noise = np.array([[sigma_T, 0, 0, 0, 0],
327
+ [sigma_h, 0, 0, 0, 0],
328
+ [B, 0, 0, 0, 0],
329
+ [m_T, 0, 0, 0, 0],
330
+ [m_h, 0, 0, 0, 0],
331
+ [n_T, 0, 0, 0, 0],
332
+ [n_h, 0, 0, 0, 0],
333
+ [n_g, 0, 0, 0, 0]])
334
+
335
+ final_par = np.vstack([par_T, par_h, par_noise])
336
+
337
+
338
+ if (n_g == 0 or n_g == 1) and fitting_option_B == "MAC":
339
+ final_par = fit_MAC(T, h, final_par) # Calculate sigma_T and B using MAC, overwrite par
340
+
341
+ return final_par
pyCRO/fit_MAC.py ADDED
@@ -0,0 +1,90 @@
1
+ import sys
2
+ import numpy as np
3
+
4
+ def fit_MAC(T, h, par):
5
+ # Extract annual mean values (first element from each parameter)
6
+ R = par[0,0]
7
+ F1 = par[1,0]
8
+ epsilon = par[5,0]
9
+ F2 = par[6,0]
10
+ b_T = par[2,0]
11
+ c_T = par[3,0]
12
+ d_T = par[4,0]
13
+ B_ref = par[10,0]
14
+ sigma_T_ref = par[8,0]
15
+ n_g = par[15,0]
16
+
17
+ T = T - np.mean(T)
18
+ h = h - np.mean(h)
19
+
20
+ if n_g == 0: # linear
21
+ T2 = np.mean(T**2)
22
+ T3 = np.mean(T**3)
23
+ T4 = np.mean(T**4)
24
+ T5 = np.mean(T**5)
25
+ T6 = np.mean(T**6)
26
+ hT = np.mean(h * T)
27
+ hT2 = np.mean(h * T**2)
28
+ hT3 = np.mean(h * T**3)
29
+ hT4 = np.mean(h * T**4)
30
+
31
+ TA = R*T2 + F1*hT + b_T*T3 - c_T*T4 + d_T*hT2
32
+ T2A = R*T3 + F1*hT2 + b_T*T4 - b_T*T2**2 - c_T*T5 + c_T*T3*T2 + d_T*hT3 - d_T*hT*T2
33
+ T3A = R*T4 + F1*hT3 + b_T*T5 - b_T*T3*T2 - c_T*T6 + c_T*T3**2 + d_T*hT4 - d_T*hT*T3
34
+
35
+ k = -(2/3)*(T3A - 1.5*T2A*T3/T2 - 3*TA*T2) / (T4 - T3**2/T2 - T2**2)
36
+ if np.isnan(np.sqrt(-2 * TA - k * T2)):
37
+ sigma_T = sigma_T_ref
38
+ B = B_ref
39
+ else:
40
+ sigma_T = np.sqrt(-2 * TA - k * T2)
41
+ B = -(T2A + k * T3) / (2 * sigma_T**2 * T2)
42
+
43
+
44
+ elif n_g == 1: # Heaviside-linear
45
+ T2 = np.mean(T**2)
46
+ T3 = np.mean(T**3)
47
+ T4 = np.mean(T**4)
48
+ T5 = np.mean(T**5)
49
+ T6 = np.mean(T**6)
50
+ hT = np.mean(h * T)
51
+ hT2 = np.mean(h * T**2)
52
+ hT3 = np.mean(h * T**3)
53
+ hT4 = np.mean(h * T**4)
54
+
55
+ TA = R*T2 + F1*hT + b_T*T3 - c_T*T4 + d_T*hT2
56
+ T2A = R*T3 + F1*hT2 + b_T*T4 - b_T*T2**2 - c_T*T5 + c_T*T3*T2 + d_T*hT3 - d_T*hT*T2
57
+ T3A = R*T4 + F1*hT3 + b_T*T5 - b_T*T3*T2 - c_T*T6 + c_T*T3**2 + d_T*hT4 - d_T*hT*T3
58
+
59
+ T1p = T
60
+ T2p = T**2
61
+ T3p = T**3
62
+ T4p = T**4
63
+
64
+ T1p = T1p[T1p >= 0]
65
+ T2p = T2p[T2p >= 0]
66
+ T3p = T3p[T3p >= 0]
67
+ T4p = T4p[T4p >= 0]
68
+
69
+ T1p = np.mean(T1p)
70
+ T2p = np.mean(T2p)
71
+ T3p = np.mean(T3p)
72
+ T4p = np.mean(T4p)
73
+
74
+ k1 = (4*TA*T2 + T2A*(2*T3p/T2p - T1p*T2/T2p) - (4/3)*T3A) / (
75
+ 2*T4p - 2*T2p*T2 - 2*(T3p**2)/T2p + T2*T3p*T1p/T2p
76
+ )
77
+ k2 = -(T2A + T3p*k1) / (2 * T2p)
78
+
79
+ B = k1 / k2
80
+ sigma_T = np.sqrt(k2 / B)
81
+
82
+ if np.isnan(B) or np.isnan(sigma_T):
83
+ sigma_T = sigma_T_ref
84
+ B = B_ref
85
+
86
+ par_out = par[:]
87
+ par_out[8, 0] = sigma_T # index 9 in MATLAB (0-based here)
88
+ par_out[10, 0] = B # index 11 in MATLAB
89
+
90
+ return par_out