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/__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
|