mesas 2.0.0__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.
- mesas/__init__.py +13 -0
- mesas/me/__init__.py +0 -0
- mesas/me/recursive_split.py +514 -0
- mesas/sas/__init__.py +0 -0
- mesas/sas/_solve_numba.py +1297 -0
- mesas/sas/functions.py +828 -0
- mesas/sas/model.py +1193 -0
- mesas/sas/specs.py +471 -0
- mesas/utils/__init__.py +0 -0
- mesas/utils/vis.py +361 -0
- mesas-2.0.0.dist-info/METADATA +80 -0
- mesas-2.0.0.dist-info/RECORD +14 -0
- mesas-2.0.0.dist-info/WHEEL +4 -0
- mesas-2.0.0.dist-info/licenses/LICENSE.txt +22 -0
mesas/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""MESAS - Multiscale Estimation of StorAge Selection."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
from mesas.sas.model import Model, ModelOptions, ModelResult, SoluteSpec
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
__version__ = version("mesas")
|
|
9
|
+
except PackageNotFoundError:
|
|
10
|
+
# Running from an uninstalled source tree
|
|
11
|
+
__version__ = "2.0.0+dev"
|
|
12
|
+
|
|
13
|
+
__all__ = ["Model", "ModelOptions", "ModelResult", "SoluteSpec", "__version__"]
|
mesas/me/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""Recursive splitting algorithm for SAS function estimation.
|
|
2
|
+
|
|
3
|
+
Estimates piecewise-constant SAS functions by iteratively splitting
|
|
4
|
+
segments and optimising against observed solute concentrations using
|
|
5
|
+
cross-validated least-squares.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
from scipy.optimize import least_squares
|
|
14
|
+
from scipy.stats import ttest_rel
|
|
15
|
+
|
|
16
|
+
VERBOSE = True
|
|
17
|
+
|
|
18
|
+
ITERATION = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _verbose(string, **kwargs):
|
|
22
|
+
"""prints progress updates"""
|
|
23
|
+
if VERBOSE:
|
|
24
|
+
print(string, **kwargs)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run(model, verbose=True, search_mode="leftfirst", **kwargs):
|
|
28
|
+
"""
|
|
29
|
+
Estimates a SAS function that reproduces observations using a piecewise constant pdf
|
|
30
|
+
|
|
31
|
+
:param model: a sas model instance
|
|
32
|
+
:param components_to_learn: dict of list of str
|
|
33
|
+
:param verbose: If True, prints progress updates
|
|
34
|
+
:param kwargs:
|
|
35
|
+
:return:
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# handle verbosity
|
|
39
|
+
global VERBOSE
|
|
40
|
+
VERBOSE = verbose
|
|
41
|
+
_verbose(f"Starting {inspect.stack()[0][3]}")
|
|
42
|
+
|
|
43
|
+
# make a copy of the input model
|
|
44
|
+
initial_model = model.copy_without_results()
|
|
45
|
+
|
|
46
|
+
_verbose("Initial model is:")
|
|
47
|
+
_verbose(model)
|
|
48
|
+
|
|
49
|
+
# Create a dictionary that will hold new components (sas functions) as we find them.
|
|
50
|
+
# It will also serve to keep track of which components to keep subdividing (if there is more than 1
|
|
51
|
+
# NOTE: a component is an object that packages together a sas function and a timeseries of its weight
|
|
52
|
+
# along with a label identifying it. It allows us to specify a time-varying SAS function as a
|
|
53
|
+
# weighted sum of fixed SAS functions
|
|
54
|
+
new_components = {}
|
|
55
|
+
for flux in initial_model.components_to_learn.keys():
|
|
56
|
+
new_components[flux] = dict((label, None) for label in initial_model.components_to_learn[flux])
|
|
57
|
+
|
|
58
|
+
index = initial_model.get_obs_index()
|
|
59
|
+
|
|
60
|
+
# Fit the initial model to the observed data
|
|
61
|
+
_verbose("-- Refining the initial model. ", end="")
|
|
62
|
+
better_initial_model, _ = fit_model(initial_model, index=index, **kwargs)
|
|
63
|
+
|
|
64
|
+
better_initial_rmse = cross_validation_rmse(better_initial_model, index=index, **kwargs)
|
|
65
|
+
_verbose(f"Initial rmse = {better_initial_rmse}")
|
|
66
|
+
|
|
67
|
+
if "incres_fun" in kwargs.keys():
|
|
68
|
+
kwargs["incres_fun"](better_initial_model, "initial")
|
|
69
|
+
|
|
70
|
+
# run the algorithm
|
|
71
|
+
global ITERATION
|
|
72
|
+
ITERATION = 0
|
|
73
|
+
if search_mode == "scanning":
|
|
74
|
+
return increase_resolution_scanning(
|
|
75
|
+
better_initial_model, better_initial_rmse, new_components, segment=0, index=index, **kwargs
|
|
76
|
+
)
|
|
77
|
+
elif search_mode == "leftfirst":
|
|
78
|
+
return increase_resolution_leftfirst(
|
|
79
|
+
better_initial_model, better_initial_rmse, new_components, segment=0, index=index, **kwargs
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
print("Never be here")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def lookfor_new_components(
|
|
86
|
+
initial_model, initial_rmse, new_components, segment_dict, id_str, incres_fun=None, index=None, **kwargs
|
|
87
|
+
):
|
|
88
|
+
# This keeps track of how many new components we found
|
|
89
|
+
new_components_count = 0
|
|
90
|
+
# This will keep track of convergence information provided by fit_model in case we want to use it for plotting
|
|
91
|
+
rmse_dict = {}
|
|
92
|
+
|
|
93
|
+
# These are retained in case they save us having to run the model again
|
|
94
|
+
last_accepted_model, last_accepted_rmse = None, None
|
|
95
|
+
|
|
96
|
+
# loop over the components we want to improve, testing whether subdividing each individually leads to
|
|
97
|
+
# substantial change in the fit
|
|
98
|
+
for flux in new_components.keys():
|
|
99
|
+
for label in new_components[flux].keys():
|
|
100
|
+
if isinstance(segment_dict, dict):
|
|
101
|
+
segment = segment_dict[flux][label]
|
|
102
|
+
else:
|
|
103
|
+
segment = segment_dict
|
|
104
|
+
|
|
105
|
+
nsegment = initial_model.sas_specs[flux].components[label].sas_fun[0].nsegment
|
|
106
|
+
if segment < nsegment:
|
|
107
|
+
_verbose(f"Subdividing {flux} {label} segment {segment}")
|
|
108
|
+
|
|
109
|
+
largest_observed_ST = (initial_model.result["sT"].sum(axis=0) * initial_model.options["dt"]).max()
|
|
110
|
+
smallest_ST_in_subdivided_segment = (
|
|
111
|
+
initial_model.sas_specs[flux].components[label].sas_fun[0].ST[segment]
|
|
112
|
+
)
|
|
113
|
+
if smallest_ST_in_subdivided_segment > largest_observed_ST:
|
|
114
|
+
_verbose("Subdivision is beyond observed ST. Shouldn't make a difference.")
|
|
115
|
+
subdivision_accepted = False
|
|
116
|
+
new_rmse = None
|
|
117
|
+
|
|
118
|
+
else:
|
|
119
|
+
new_model = initial_model.subdivided_copy(flux, label, segment)
|
|
120
|
+
segment_list = new_model.get_parameter_list()
|
|
121
|
+
if np.any(segment_list[1:] < new_model.options["ST_smallest_segment"]):
|
|
122
|
+
_verbose("New segment too small! Try reducing model.options['ST_smallest_segment']")
|
|
123
|
+
subdivision_accepted = False
|
|
124
|
+
new_rmse = None
|
|
125
|
+
else:
|
|
126
|
+
_verbose("-- Initial fit of the candidate model ", end="")
|
|
127
|
+
new_model, new_rmse = fit_model(new_model, index=index, **kwargs)
|
|
128
|
+
|
|
129
|
+
# The current criteria used for deciding whether to accept the subdivision is simple:
|
|
130
|
+
# Just check whether the model improvement is greater than some threshold
|
|
131
|
+
# This can definitely be improved
|
|
132
|
+
# subdivision_accepted = (1 - new_rmse / initial_rmse) > split_tolerance
|
|
133
|
+
|
|
134
|
+
# Use k-fold cross validation to see if the split leads to an improvement in the model
|
|
135
|
+
new_rmse = cross_validation_rmse(new_model, index=index, **kwargs)
|
|
136
|
+
_verbose("Candidate model = ")
|
|
137
|
+
_verbose(new_model)
|
|
138
|
+
rmse_change = np.mean(new_rmse - initial_rmse)
|
|
139
|
+
rmse_pvalue = ttest_rel(new_rmse, initial_rmse).pvalue
|
|
140
|
+
subdivision_accepted = rmse_pvalue < 0.95 and rmse_change < 0
|
|
141
|
+
|
|
142
|
+
_verbose(f"Initial rmse = {initial_rmse}")
|
|
143
|
+
_verbose(f"Candidate rmse = {new_rmse}")
|
|
144
|
+
_verbose(f"Mean change in rmse = {rmse_change}")
|
|
145
|
+
_verbose(f"t-test p value = {rmse_pvalue}")
|
|
146
|
+
# Optionally, make some plots
|
|
147
|
+
if incres_fun is not None:
|
|
148
|
+
incres_fun(
|
|
149
|
+
new_model, f"{id_str}_{flux}_{label}_{nsegment}_{segment}_{subdivision_accepted}"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# accept or reject the subdivision
|
|
153
|
+
if subdivision_accepted:
|
|
154
|
+
_verbose(f"Subdivision accepted for {flux}, {label} segment {segment}")
|
|
155
|
+
|
|
156
|
+
# Add to the record of new components, and increment the counter
|
|
157
|
+
new_components[flux][label] = new_model.sas_specs[flux].components[label]
|
|
158
|
+
new_components_count += 1
|
|
159
|
+
last_accepted_model, last_accepted_rmse = new_model, new_rmse
|
|
160
|
+
|
|
161
|
+
else:
|
|
162
|
+
_verbose(f"Subdivision rejected for {flux}, {label} segment {segment}")
|
|
163
|
+
|
|
164
|
+
# Record the rejection by setting the dict entry to None
|
|
165
|
+
new_components[flux][label] = None
|
|
166
|
+
|
|
167
|
+
# Keep a record of the rmse for plotting
|
|
168
|
+
rmse_dict[f"{flux}, {label}"] = new_rmse
|
|
169
|
+
|
|
170
|
+
return last_accepted_model, last_accepted_rmse, new_components, new_components_count, rmse_dict
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def incorporate_new_components(initial_model, new_components):
|
|
174
|
+
"""
|
|
175
|
+
Adds components into a model from a dictionary
|
|
176
|
+
|
|
177
|
+
:param initial_model:
|
|
178
|
+
:param new_components:
|
|
179
|
+
:param index:
|
|
180
|
+
:param kwargs:
|
|
181
|
+
:return:
|
|
182
|
+
"""
|
|
183
|
+
new_model = initial_model.copy_without_results()
|
|
184
|
+
|
|
185
|
+
for flux in new_components.keys():
|
|
186
|
+
for label in new_components[flux].keys():
|
|
187
|
+
if new_components[flux][label] is not None:
|
|
188
|
+
new_model.set_component(flux, new_components[flux][label])
|
|
189
|
+
|
|
190
|
+
return new_model
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def increase_resolution_scanning(
|
|
194
|
+
initial_model, initial_rmse, new_components, maxscan=100, index=None, incres_fun=None, **kwargs
|
|
195
|
+
):
|
|
196
|
+
"""Increases sas function resolution by splitting a piecewise linear segment (scanning method)
|
|
197
|
+
|
|
198
|
+
:param initial_model: The model we want to try increasing the resolution of
|
|
199
|
+
:param initial_rmse: How well it currently fits the data
|
|
200
|
+
:param new_components: A dict that keeps track of which components we are trying to refine
|
|
201
|
+
:param segment: The index of the segment we are currently refining
|
|
202
|
+
:param kwargs: Additional arguments to be passed to the fit_model function
|
|
203
|
+
:return:
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
_verbose(f"\n***********************\nStarting {inspect.stack()[0][3]}")
|
|
207
|
+
|
|
208
|
+
any_segments_subdivided = False
|
|
209
|
+
new_model = None
|
|
210
|
+
|
|
211
|
+
for scan in range(maxscan):
|
|
212
|
+
_verbose(f"\nStarting scan #{scan + 1}")
|
|
213
|
+
|
|
214
|
+
# How many segments do we have to scan?
|
|
215
|
+
max_segment = 0
|
|
216
|
+
check_segment_dict = {}
|
|
217
|
+
for flux in new_components.keys():
|
|
218
|
+
check_segment_dict[flux] = {}
|
|
219
|
+
for label in new_components[flux].keys():
|
|
220
|
+
if max_segment < initial_model.sas_specs[flux].components[label].sas_fun[0].nsegment:
|
|
221
|
+
max_segment = initial_model.sas_specs[flux].components[label].sas_fun[0].nsegment
|
|
222
|
+
# start with segment 0
|
|
223
|
+
check_segment_dict[flux][label] = 0
|
|
224
|
+
|
|
225
|
+
for segment in range(max_segment):
|
|
226
|
+
_verbose(f"Testing increased SAS resolution in segment {segment}")
|
|
227
|
+
|
|
228
|
+
last_accepted_model, last_accepted_rmse, new_components, new_components_count, rmse_dict = (
|
|
229
|
+
lookfor_new_components(
|
|
230
|
+
initial_model,
|
|
231
|
+
initial_rmse,
|
|
232
|
+
new_components,
|
|
233
|
+
check_segment_dict,
|
|
234
|
+
id_str=f"scan_{scan}",
|
|
235
|
+
incres_fun=incres_fun,
|
|
236
|
+
index=index,
|
|
237
|
+
**kwargs,
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# if we accepted refined components we want to add them into the model
|
|
242
|
+
# before moving on to the next segment or scan
|
|
243
|
+
if new_components_count > 0:
|
|
244
|
+
any_segments_subdivided = True
|
|
245
|
+
|
|
246
|
+
# If we found more than one new component, we need to include all of them, then re-run
|
|
247
|
+
# fit_model so that their interactions can be accounted for
|
|
248
|
+
if new_components_count > 1:
|
|
249
|
+
_verbose("-- Combining and fitting ", end="")
|
|
250
|
+
new_model, _ = fit_model(
|
|
251
|
+
incorporate_new_components(initial_model, new_components), index=index, **kwargs
|
|
252
|
+
)
|
|
253
|
+
new_rmse = cross_validation_rmse(new_model, index=index, **kwargs)
|
|
254
|
+
|
|
255
|
+
else:
|
|
256
|
+
new_model, new_rmse = last_accepted_model, last_accepted_rmse
|
|
257
|
+
|
|
258
|
+
# Optionally, make some plots
|
|
259
|
+
if incres_fun is not None:
|
|
260
|
+
incres_fun(new_model, f"scan_{scan}_all_components_{max_segment}_{segment}")
|
|
261
|
+
|
|
262
|
+
_verbose("New model is:")
|
|
263
|
+
_verbose(new_model)
|
|
264
|
+
|
|
265
|
+
initial_model = new_model
|
|
266
|
+
initial_rmse = new_rmse
|
|
267
|
+
|
|
268
|
+
# Increment the segment counter
|
|
269
|
+
for flux in check_segment_dict.keys():
|
|
270
|
+
for label in check_segment_dict[flux].keys():
|
|
271
|
+
if new_components[flux][label] is not None:
|
|
272
|
+
check_segment_dict[flux][label] += 1
|
|
273
|
+
if (
|
|
274
|
+
check_segment_dict[flux][label]
|
|
275
|
+
< initial_model.sas_specs[flux].components[label].sas_fun[0].nsegment
|
|
276
|
+
):
|
|
277
|
+
check_segment_dict[flux][label] += 1
|
|
278
|
+
|
|
279
|
+
if any_segments_subdivided:
|
|
280
|
+
any_segments_subdivided = False
|
|
281
|
+
_verbose(f"Scan {scan + 1} complete")
|
|
282
|
+
else:
|
|
283
|
+
# If not, we are done. Return the model we've found,
|
|
284
|
+
_verbose("Finished increasing model resolution")
|
|
285
|
+
return new_model
|
|
286
|
+
|
|
287
|
+
if new_model is not None:
|
|
288
|
+
_verbose(f"Maximum number of scans reached ({maxscan})")
|
|
289
|
+
return new_model
|
|
290
|
+
else:
|
|
291
|
+
return initial_model
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def increase_resolution_leftfirst(
|
|
295
|
+
initial_model, initial_rmse, new_components, segment, incres_fun=None, index=None, **kwargs
|
|
296
|
+
):
|
|
297
|
+
"""Increases sas function resolution by splitting a piecewise linear segment (left-first method)
|
|
298
|
+
|
|
299
|
+
:param initial_model: The model we want to try increasing the resolution of
|
|
300
|
+
:param initial_rmse: How well it currently fits the data
|
|
301
|
+
:param new_components: A dict that keeps track of which components we are trying to refine
|
|
302
|
+
:param segment: The index of the segment we are currently refining
|
|
303
|
+
:param incres_fun: An optional function that can produce some plots along the way
|
|
304
|
+
:param kwargs: Additional arguments to be passed to the fit_model function
|
|
305
|
+
:return:
|
|
306
|
+
"""
|
|
307
|
+
|
|
308
|
+
global ITERATION
|
|
309
|
+
ITERATION += 1
|
|
310
|
+
|
|
311
|
+
_verbose(f"\n***********************\nStarting {inspect.stack()[0][3]} iteration {ITERATION}")
|
|
312
|
+
_verbose(f"Testing increased SAS resolution in segment {segment}")
|
|
313
|
+
|
|
314
|
+
# total number of segments. Used for plotting (actually for naming the saved figure files)
|
|
315
|
+
_Ns = len(initial_model.get_parameter_list()) # noqa: F841
|
|
316
|
+
|
|
317
|
+
last_accepted_model, last_accepted_rmse, new_components, new_components_count, rmse_dict = lookfor_new_components(
|
|
318
|
+
initial_model,
|
|
319
|
+
initial_rmse,
|
|
320
|
+
new_components,
|
|
321
|
+
segment,
|
|
322
|
+
id_str=f"iteration_{ITERATION}",
|
|
323
|
+
incres_fun=incres_fun,
|
|
324
|
+
index=index,
|
|
325
|
+
**kwargs,
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# if we accepted refined components we want to add them into the model
|
|
329
|
+
# and then recursively try subdividing them too
|
|
330
|
+
if new_components_count > 0:
|
|
331
|
+
# If we found more than one new component, we need to include all of them, then re-run
|
|
332
|
+
# fit_model so that their interactions can be accounted for
|
|
333
|
+
if new_components_count > 1:
|
|
334
|
+
_verbose("-- Combining and fitting ", end="")
|
|
335
|
+
new_model, _ = fit_model(incorporate_new_components(initial_model, new_components), index=index, **kwargs)
|
|
336
|
+
new_rmse = cross_validation_rmse(new_model, index=index, **kwargs)
|
|
337
|
+
|
|
338
|
+
else:
|
|
339
|
+
new_model, new_rmse = last_accepted_model, last_accepted_rmse
|
|
340
|
+
|
|
341
|
+
# Optionally, make some plots
|
|
342
|
+
if incres_fun is not None:
|
|
343
|
+
incres_fun(new_model, f"iteration_{ITERATION}_all_components")
|
|
344
|
+
|
|
345
|
+
_verbose("New model is:")
|
|
346
|
+
_verbose(new_model)
|
|
347
|
+
|
|
348
|
+
# Recursively try subdividing the first of the two new segments
|
|
349
|
+
return increase_resolution_leftfirst(
|
|
350
|
+
new_model, new_rmse, new_components, segment, incres_fun=incres_fun, index=index, **kwargs
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# If we didn't accept any new subdivisions, we need to move on
|
|
354
|
+
else:
|
|
355
|
+
# First, we check whether each component even has more segments to subdivide
|
|
356
|
+
more_segments = False
|
|
357
|
+
for flux in list(new_components.keys()).copy():
|
|
358
|
+
for label in list(new_components[flux].keys()).copy():
|
|
359
|
+
if segment == initial_model.sas_specs[flux].components[label].sas_fun[0].nsegment - 1:
|
|
360
|
+
# no more segments in this component, so delete it from the dict
|
|
361
|
+
_verbose(f"No more refinement for {flux}, {label}")
|
|
362
|
+
del new_components[flux][label]
|
|
363
|
+
|
|
364
|
+
else:
|
|
365
|
+
more_segments = True
|
|
366
|
+
|
|
367
|
+
# did any?
|
|
368
|
+
if more_segments:
|
|
369
|
+
# If so, recursively call this function on the next segment
|
|
370
|
+
_verbose(f"Moving to segment {segment + 1}")
|
|
371
|
+
return increase_resolution_leftfirst(
|
|
372
|
+
initial_model, initial_rmse, new_components, segment + 1, incres_fun=incres_fun, index=index, **kwargs
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
# If not, we are done. Return the model we've found,
|
|
376
|
+
_verbose("Finished increasing model resolution")
|
|
377
|
+
return initial_model
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def fit_model(model, include_C_old=True, learn_fun=None, index=None, jacobian_mode="numerical", **kwargs):
|
|
381
|
+
"""
|
|
382
|
+
Fit the sas function to the data using a least-squares regression optimization
|
|
383
|
+
|
|
384
|
+
:param model:
|
|
385
|
+
:param include_C_old:
|
|
386
|
+
:param learn_fun:
|
|
387
|
+
:param index:
|
|
388
|
+
:param jacobian_mode: 'numerical' (default). 'analytical' is not currently
|
|
389
|
+
supported: the Numba solver does not implement parameter sensitivities,
|
|
390
|
+
so analytical jacobians would be silently zero.
|
|
391
|
+
:param kwargs:
|
|
392
|
+
:return:
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
if jacobian_mode == "analytical":
|
|
396
|
+
raise NotImplementedError(
|
|
397
|
+
"jacobian_mode='analytical' is not supported: the solver does not "
|
|
398
|
+
"implement parameter sensitivities (they would be silently zero). "
|
|
399
|
+
"Use jacobian_mode='numerical'."
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
_verbose("Fitting...", end="")
|
|
403
|
+
|
|
404
|
+
if index is None:
|
|
405
|
+
index = model.get_obs_index()
|
|
406
|
+
|
|
407
|
+
new_model = model.copy_without_results()
|
|
408
|
+
|
|
409
|
+
# Use the current values as the initial estimate
|
|
410
|
+
segment_list = new_model.get_parameter_list()
|
|
411
|
+
# Assume that any segments of zero length are the first segment
|
|
412
|
+
# and exclude these from the estimate of x0
|
|
413
|
+
# TODO: check that zero_segments are indeed ST_min
|
|
414
|
+
non_zero_segments = segment_list > 0
|
|
415
|
+
x0 = np.log(segment_list[non_zero_segments])
|
|
416
|
+
xmin = np.ones_like(x0) * np.log(new_model.options["ST_smallest_segment"])
|
|
417
|
+
xmax = np.ones_like(x0) * np.log(new_model.options["ST_largest_segment"])
|
|
418
|
+
if any((x0 < xmin) | (x0 > xmax)):
|
|
419
|
+
print(f"A segment size is out of bounds: {np.exp(x0)}")
|
|
420
|
+
nseg_x0 = len(x0)
|
|
421
|
+
if include_C_old:
|
|
422
|
+
x0 = np.r_[x0, [new_model.solute_parameters[sol]["C_old"] for sol in new_model._solorder]]
|
|
423
|
+
xmin = np.r_[xmin, [-np.inf for sol in new_model._solorder]]
|
|
424
|
+
xmax = np.r_[xmax, [np.inf for sol in new_model._solorder]]
|
|
425
|
+
|
|
426
|
+
# Construct an index into the the columns returned by get_jacobian() that we wish to keep
|
|
427
|
+
keep_jac_columns = np.ones(len(segment_list) + new_model._numsol) == 1
|
|
428
|
+
keep_jac_columns[: len(segment_list)] = non_zero_segments
|
|
429
|
+
|
|
430
|
+
new_model.x_prev = None
|
|
431
|
+
|
|
432
|
+
def update_parameters(x):
|
|
433
|
+
if np.any(x > xmax) or np.any(x < xmin):
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
if new_model.x_prev is not None and np.all(x == new_model.x_prev):
|
|
437
|
+
return False
|
|
438
|
+
|
|
439
|
+
else:
|
|
440
|
+
# _verbose(x)
|
|
441
|
+
new_segment_list = np.zeros_like(segment_list)
|
|
442
|
+
new_segment_list[non_zero_segments] = np.exp(x[:nseg_x0])
|
|
443
|
+
new_model.update_from_parameter_list(new_segment_list)
|
|
444
|
+
|
|
445
|
+
# optionally update the C_old parameter
|
|
446
|
+
if include_C_old:
|
|
447
|
+
for isol, sol in enumerate(new_model._solorder):
|
|
448
|
+
new_model.solute_parameters[sol]["C_old"] = x[-new_model._numsol + isol]
|
|
449
|
+
new_model.x_prev = x
|
|
450
|
+
return True
|
|
451
|
+
|
|
452
|
+
def f(x):
|
|
453
|
+
"""return residuals given parameters x"""
|
|
454
|
+
|
|
455
|
+
update_parameters(x)
|
|
456
|
+
new_model.run()
|
|
457
|
+
|
|
458
|
+
# optionally do some plotting
|
|
459
|
+
if learn_fun is not None:
|
|
460
|
+
learn_fun(new_model)
|
|
461
|
+
|
|
462
|
+
residuals = new_model.get_residuals()[index]
|
|
463
|
+
return residuals
|
|
464
|
+
|
|
465
|
+
def jac(x):
|
|
466
|
+
"""return the jacobian given parameters x"""
|
|
467
|
+
|
|
468
|
+
if update_parameters(x):
|
|
469
|
+
new_model.run()
|
|
470
|
+
|
|
471
|
+
jacobian = new_model.get_jacobian()[index, :]
|
|
472
|
+
return jacobian[:, keep_jac_columns]
|
|
473
|
+
|
|
474
|
+
# use the scipy.optimize.least_square package
|
|
475
|
+
if jacobian_mode == "analytical":
|
|
476
|
+
OptimizeResult = least_squares(fun=f, x0=x0, jac=jac, verbose=2, bounds=(xmin, xmax))
|
|
477
|
+
elif jacobian_mode == "numerical":
|
|
478
|
+
OptimizeResult = least_squares(fun=f, x0=x0, verbose=0, bounds=(xmin, xmax))
|
|
479
|
+
|
|
480
|
+
xopt = OptimizeResult.x
|
|
481
|
+
update_parameters(xopt)
|
|
482
|
+
new_rmse = np.sqrt(np.mean(OptimizeResult.fun**2))
|
|
483
|
+
new_model.rmse = new_rmse
|
|
484
|
+
|
|
485
|
+
_verbose("done.")
|
|
486
|
+
return new_model, new_rmse
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def cross_validation_rmse(model, index=None, n_splits=3, **kwargs):
|
|
490
|
+
try:
|
|
491
|
+
from sklearn.model_selection import KFold
|
|
492
|
+
except ImportError as err:
|
|
493
|
+
raise ImportError(
|
|
494
|
+
"scikit-learn is required for k-fold cross-validation. "
|
|
495
|
+
"Install it with `pip install mesas[estimation]` (or `pip install scikit-learn`)."
|
|
496
|
+
) from err
|
|
497
|
+
if index is None:
|
|
498
|
+
index = model.get_obs_index()
|
|
499
|
+
kf = KFold(n_splits=n_splits)
|
|
500
|
+
rmse_cv_list = []
|
|
501
|
+
_verbose("Getting rmse from k-fold cross validation")
|
|
502
|
+
iter = 0
|
|
503
|
+
for train, test in kf.split(index):
|
|
504
|
+
iter += 1
|
|
505
|
+
_verbose(f"-- iteration {iter} of {n_splits} ", end="")
|
|
506
|
+
new_model_cv, _ = fit_model(model, index=index[train], **kwargs)
|
|
507
|
+
rmse_cv_list.append(np.sqrt(np.mean(new_model_cv.get_residuals()[index[test]] ** 2)))
|
|
508
|
+
# _verbose(f' rmse_cv = {rmse_cv_list}')
|
|
509
|
+
# rmse_cv_list.remove(max(rmse_cv_list))
|
|
510
|
+
# rmse_cv_list.remove(min(rmse_cv_list))
|
|
511
|
+
# rmse_cv = np.sqrt(np.mean(np.sqrt(rmse_cv_list))**2)
|
|
512
|
+
rmse_cv = np.array(rmse_cv_list)
|
|
513
|
+
model.rmse_cv = rmse_cv
|
|
514
|
+
return rmse_cv
|
mesas/sas/__init__.py
ADDED
|
File without changes
|