mc3 3.3.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.
mc3/fit_driver.py ADDED
@@ -0,0 +1,270 @@
1
+ # Copyright (c) 2015-2026 Patricio Cubillos and contributors.
2
+ # mc3 is open-source software under the MIT license (see LICENSE).
3
+
4
+ __all__ = [
5
+ 'fit',
6
+ ]
7
+
8
+ import numpy as np
9
+ import scipy.optimize as so
10
+
11
+ from . import stats as ms
12
+ from . import utils as mu
13
+
14
+
15
+ def fit(
16
+ data, uncert, func, params, indparams=[], indparams_dict={},
17
+ pstep=None, pmin=None, pmax=None,
18
+ prior=None, priorlow=None, priorup=None, leastsq='lm',
19
+ ):
20
+ r"""
21
+ Find the best-fitting params values to the dataset by performing a
22
+ Maximum-A-Posteriori optimization.
23
+
24
+ This is achieved by minimizing the negative log posterior, with:
25
+ log_post = log(posterior)
26
+ = log(likelihood) + log(prior)
27
+ = -0.5*chi-squared + log_prior
28
+ = sum_i -0.5*((data[i] - model[i])/uncert[i])**2 + log_prior
29
+
30
+ where log_prior is defined as:
31
+ log_prior = sum -0.5*((params - prior)/prior_uncert)**2
32
+ for each parameter with a Gaussian prior; parameters with
33
+ uniform priors do not contribute to log_prior.
34
+
35
+ Constant terms have been neglected since they don't affect the
36
+ optimization.
37
+
38
+ Parameters
39
+ ----------
40
+ data: 1D ndarray
41
+ Data fitted by func.
42
+ uncert: 1D ndarray
43
+ 1-sigma uncertainties of data.
44
+ func: callable
45
+ The fitting function to model the data. It must be callable as:
46
+ model = func(params, *indparams, **indparams_dict)
47
+ params: 1D ndarray
48
+ The model parameters.
49
+ indparams: tuple
50
+ Additional arguments required by func (if required).
51
+ indparams_dict: dict
52
+ Additional keyword arguments required by func (if required).
53
+ pstep: 1D ndarray
54
+ Parameters fitting behavior.
55
+ If pstep is positive, the parameter is free for fitting.
56
+ If pstep is zero, keep the parameter value fixed.
57
+ If pstep is a negative integer, copy the value from
58
+ params[np.abs(pstep)+1].
59
+ pmin: 1D ndarray
60
+ Model parameters' lower boundaries. Default -np.inf.
61
+ Only for leastsq='trf', since 'lm' does not handle bounds.
62
+ pmax: 1D ndarray
63
+ Model parameters' upper boundaries. Default +np.inf.
64
+ Only for leastsq='trf', since 'lm' does not handle bounds.
65
+ prior: 1D ndarray
66
+ Parameters priors. The type of prior is determined by priorlow
67
+ and priorup:
68
+ Gaussian: if both priorlow>0 and priorup>0
69
+ Uniform: else
70
+ priorlow: 1D ndarray
71
+ Parameters' lower 1-sigma Gaussian prior.
72
+ priorup: 1D ndarray
73
+ Paraneters' upper 1-sigma Gaussian prior.
74
+ leastsq: String
75
+ Optimization algorithm:
76
+ If 'lm': use the Levenberg-Marquardt algorithm
77
+ If 'trf': use the Trust Region Reflective algorithm
78
+
79
+ Returns
80
+ -------
81
+ mc3_output: Dict
82
+ A dictionary containing the fit outputs, including:
83
+ - best_log_post: optimal log of the posterior (as defined above).
84
+ - best_chisq: chi-square for the found best_log_post.
85
+ - best_model: model evaluated at bestp.
86
+ - bestp: Model parameters for the optimal best_log_post.
87
+ - optimizer_res: the output from the scipy optimizer.
88
+
89
+ Examples
90
+ --------
91
+ >>> import mc3
92
+ >>> import numpy as np
93
+
94
+ >>> def quad(p, x):
95
+ >>> '''Quadratic polynomial: y(x) = p0 + p1*x + p2*x^2'''
96
+ >>> return p[0] + p[1]*x + p[2]*x**2.0
97
+
98
+ >>> # Preamble, create a noisy synthetic dataset:
99
+ >>> np.random.seed(10)
100
+ >>> x = np.linspace(0, 10, 100)
101
+ >>> p_true = [4.5, -2.4, 0.5]
102
+ >>> y = quad(p_true, x)
103
+ >>> uncert = np.sqrt(np.abs(y))
104
+ >>> data = y + np.random.normal(0, uncert)
105
+
106
+ >>> # Initial guess for fitting parameters:
107
+ >>> params = np.array([ 3.0, -2.0, 0.1])
108
+
109
+ >>> # Fit data:
110
+ >>> output = mc3.fit(data, uncert, quad, params, indparams=[x])
111
+ >>> print(output['bestp'], output['best_chisq'], -2*output['best_log_post'], sep='\n')
112
+ [ 4.57471072 -2.28357843 0.48341911]
113
+ 92.79923183159411
114
+ 92.79923183159411
115
+
116
+ >>> # Fit with priors (Gaussian, uniform, uniform):
117
+ >>> prior = np.array([4.0, 0.0, 0.0])
118
+ >>> priorlow = np.array([0.1, 0.0, 0.0])
119
+ >>> priorup = np.array([0.1, 0.0, 0.0])
120
+ >>> output = mc3.fit(data, uncert, quad, params, indparams=[x],
121
+ prior=prior, priorlow=priorlow, priorup=priorup)
122
+ >>> print(output['bestp'], output['best_chisq'], -2*output['best_log_post'], sep='\n')
123
+ [ 4.01743461 -2.00989433 0.45686521]
124
+ 93.77082119449915
125
+ 93.80121777303248
126
+ """
127
+ with mu.Log() as log:
128
+ if leastsq not in [None, 'lm', 'trf']:
129
+ log.error(
130
+ f"Invalid 'leastsq' input ({leastsq}). Must select "
131
+ "from ['lm', 'trf']"
132
+ )
133
+
134
+ # Total number of model parameters:
135
+ npars = len(params)
136
+ # Default pstep:
137
+ if pstep is None:
138
+ pstep = np.ones(npars, np.double)
139
+ # Default boundaries (all parameter space):
140
+ if pmin is None:
141
+ pmin = np.tile(-np.inf, npars)
142
+ if pmax is None:
143
+ pmax = np.tile(np.inf, npars)
144
+ # Default priors, must set all or no one:
145
+ if prior is None or priorlow is None or priorup is None:
146
+ prior = priorup = priorlow = np.zeros(npars)
147
+
148
+ # Cast to ndarrays:
149
+ params = np.asarray(params)
150
+ pstep = np.asarray(pstep)
151
+ pmin = np.asarray(pmin)
152
+ pmax = np.asarray(pmax)
153
+ prior = np.asarray(prior)
154
+ priorlow = np.asarray(priorlow)
155
+ priorup = np.asarray(priorup)
156
+
157
+ # Get indices:
158
+ ifree = np.where(pstep > 0)[0]
159
+ ishare = np.where(pstep < 0)[0]
160
+
161
+ fitparams = params[ifree]
162
+
163
+ args = (
164
+ params, func, data, uncert, indparams, indparams_dict,
165
+ pstep, prior, priorlow, priorup, ifree, ishare,
166
+ )
167
+ # Levenberg-Marquardt optimization:
168
+ if leastsq == 'lm':
169
+ lsfit = so.leastsq(
170
+ residuals, fitparams, args=args,
171
+ ftol=3e-16, xtol=3e-16, gtol=3e-16,
172
+ full_output=True,
173
+ )
174
+ output, cov_x, infodict, mesg, err = lsfit
175
+ params[ifree] = lsfit[0]
176
+ resid = lsfit[2]["fvec"]
177
+
178
+ # Bounded optimization:
179
+ elif leastsq == 'trf':
180
+ lsfit = so.least_squares(
181
+ residuals, fitparams,
182
+ bounds=(pmin[ifree], pmax[ifree]), args=args,
183
+ ftol=3e-16, xtol=3e-16, gtol=3e-16,
184
+ method='trf',
185
+ )
186
+ params[ifree] = lsfit["x"]
187
+ resid = lsfit["fun"]
188
+
189
+ # Update shared parameters:
190
+ for s in ishare:
191
+ params[s] = params[-int(pstep[s])-1]
192
+
193
+ # Compute best-fit model:
194
+ best_model = func(params, *indparams, **indparams_dict)
195
+ # Calculate chi-squared for best-fitting values:
196
+ best_log_post = -0.5*np.sum(resid**2.0)
197
+ log_prior = ms.log_prior(params[ifree], prior, priorlow, priorup, pstep)
198
+ best_chisq = -2*(best_log_post - log_prior)
199
+
200
+ return {
201
+ 'bestp':params,
202
+ 'best_log_post':best_log_post,
203
+ 'best_chisq':best_chisq,
204
+ 'best_model':best_model,
205
+ 'optimizer_res':lsfit,
206
+ }
207
+
208
+
209
+ def residuals(
210
+ fitparams, params, func, data, uncert, indparams, indparams_dict,
211
+ pstep, prior, priorlow, priorup, ifree, ishare,
212
+ ):
213
+ """
214
+ Calculate the weighted residuals between data and a model, accounting
215
+ also for parameter priors.
216
+
217
+ Parameters
218
+ ----------
219
+ fitparams: 1D ndarray
220
+ The model free parameters.
221
+ params: 1D ndarray
222
+ The model parameters (including fixed and shared parameters).
223
+ func: Callable
224
+ The fitting function to model the data, called as:
225
+ model = func(params, *indparams, **indparams_dict)
226
+ data: 1D ndarray
227
+ Dependent data fitted by func.
228
+ uncert: 1D ndarray
229
+ 1-sigma uncertainty of data.
230
+ indparams: tuple
231
+ Additional arguments required by func (if required).
232
+ indparams_dict: dict
233
+ Additional keyword arguments required by func (if required).
234
+ pstep: 1D ndarray
235
+ Parameters' jump scale (same size as params).
236
+ If the pstep is positive, the parameter is free for fitting.
237
+ If the pstep is 0, keep the parameter value fixed.
238
+ If the pstep is a negative integer, copy (share) the parameter value
239
+ from params[np.abs(pstep)+1], which can be free or fixed.
240
+ prior: 1D ndarray
241
+ Model parameters' (Gaussian) prior values (same size as params).
242
+ Considered only when priolow != 0. priorlow and priorup are the
243
+ lower and upper 1-sigma width of the Gaussian prior, respectively.
244
+ priorlow: 1D ndarray
245
+ Parameters' lower 1-sigma Gaussian prior (same size as params).
246
+ priorup: 1D ndarray
247
+ Paraneters' upper 1-sigma Gaussian prior (same size as params).
248
+ ifree: 1D bool ndarray
249
+ Indices of the free parameters in params.
250
+ ishare: 1D bool ndarray
251
+ Indices of the shared parameters in params.
252
+
253
+ Returns
254
+ -------
255
+ Array of weighted data-model and prior-params residuals.
256
+ """
257
+ # Update params with fitparams:
258
+ params[ifree] = fitparams
259
+
260
+ # Update shared parameters:
261
+ for s in ishare:
262
+ params[s] = params[-int(pstep[s])-1]
263
+
264
+ # Compute model:
265
+ model = func(params, *indparams, **indparams_dict)
266
+ # Calculate residuals:
267
+ residuals = ms.residuals(
268
+ model, data, uncert, params, prior, priorlow, priorup,
269
+ )
270
+ return residuals
mc3/mcmc_driver.py ADDED
@@ -0,0 +1,378 @@
1
+ # Copyright (c) 2015-2026 Patricio Cubillos and contributors.
2
+ # mc3 is open-source software under the MIT license (see LICENSE).
3
+
4
+ __all__ = [
5
+ 'mcmc',
6
+ ]
7
+
8
+ import time
9
+ import ctypes
10
+ import multiprocessing as mpr
11
+
12
+ import numpy as np
13
+
14
+ from . import chain as ch
15
+ from . import stats as ms
16
+
17
+
18
+ def mcmc(
19
+ data, uncert, func, params, indparams, indparams_dict,
20
+ pmin, pmax, pstep,
21
+ prior, priorlow, priorup,
22
+ nchains, ncpu, nsamples, sampler,
23
+ wlike, fit_output, grtest, grbreak, grnmin, burnin, thinning,
24
+ fgamma, fepsilon, hsize, kickoff, savefile, resume, log,
25
+ pnames, texnames,
26
+ ):
27
+ """
28
+ Mid-level routine called by mc3.sample() to execute Markov-chain Monte
29
+ Carlo run.
30
+
31
+ Parameters
32
+ ----------
33
+ data: 1D float ndarray
34
+ Data to be fit by func.
35
+ uncert: 1D float ndarray
36
+ Uncertainties of data.
37
+ func: Callable or string-iterable
38
+ The callable function that models data as:
39
+ model = func(params, *indparams, **indparams_dict)
40
+ params: 1D float ndarray
41
+ Set of initial fitting parameters for func.
42
+ indparams: tuple
43
+ Additional arguments required by func.
44
+ indparams_dict: dict
45
+ Additional keyword arguments required by func (if required).
46
+ pmin: 1D ndarray
47
+ Lower boundaries for the posterior exploration.
48
+ pmax: 1D ndarray
49
+ Upper boundaries for the posterior exploration.
50
+ pstep: 1D ndarray
51
+ Parameter stepping.
52
+ prior: 1D ndarray
53
+ Parameter prior distribution means.
54
+ priorlow: 1D ndarray
55
+ Lower prior uncertainty values.
56
+ priorup: 1D ndarray
57
+ Upper prior uncertainty values.
58
+ nchains: Scalar
59
+ Number of simultaneous chains to run.
60
+ ncpu: Integer
61
+ Number of processors for the MCMC chains.
62
+ nsamples: Scalar
63
+ Total number of samples.
64
+ sampler: String
65
+ MCMC sampling algorithm select from [mrw, demc, snooker]
66
+ wlike: Bool
67
+ If True, calculate the likelihood in a wavelet-base.
68
+ grtest: Boolean
69
+ Run Gelman & Rubin test.
70
+ grbreak: Float
71
+ Gelman-Rubin convergence threshold to stop the MCMC.
72
+ grnmin: Float
73
+ Minimum number of samples required for grbreak to stop the MCMC.
74
+ burnin: Integer
75
+ Number of burned-in (discarded) iterations.
76
+ thinning: Integer
77
+ Thinning factor of the chains (use every thinning-th iteration).
78
+ fgamma: Float
79
+ Proposals jump scale factor for DEMC's gamma.
80
+ fepsilon: Float
81
+ Jump scale factor for DEMC's support distribution.
82
+ hsize: Integer
83
+ Number of initial samples per chain.
84
+ kickoff: String
85
+ How to start the chains:
86
+ 'normal' for normal distribution around initial guess, or
87
+ 'uniform' for uniform distribution withing the given boundaries.
88
+ savefile: String
89
+ If not None, filename to store allparams and other MCMC results.
90
+ resume: Boolean
91
+ If True resume a previous run.
92
+ log: mc3.utils.Log instance
93
+ Logging object.
94
+
95
+ Returns
96
+ -------
97
+ output: Dict
98
+ A Dictionary containing the MCMC posterior distribution and related
99
+ stats, including:
100
+ - posterior: thinned posterior distribution of shape [nsamples, nfree].
101
+ - zchain: chain indices for each sample in Z.
102
+ - zmask: indices that turn Z into the desired posterior (remove burn-in)
103
+ - chisq: chi^2 value for each sample in Z.
104
+ - log_posterior: log(posterior) for the samples in Z.
105
+ - burnin: number of burned-in samples per chain.
106
+ - bestp: model parameters for the optimal log(posterior) sample.
107
+ - best_model: model evaluated at bestp.
108
+ - best_chisq: chi^2 for the optimal log(posterior) in the sample.
109
+ - best_log_post: optimal log(posterior) in posterior.
110
+ - acceptance_rate: sample's acceptance rate.
111
+
112
+ Examples
113
+ --------
114
+ >>> # See https://mc3.readthedocs.io/en/latest/mcmc_tutorial.html
115
+ """
116
+ nfree = int(np.sum(pstep > 0))
117
+ ifree = np.where(pstep > 0)[0]
118
+ ishare = np.where(pstep < 0)[0]
119
+
120
+ if resume:
121
+ oldrun = np.load(savefile)
122
+ zold = oldrun["posterior"]
123
+ zchain_old = oldrun["zchain"]
124
+ # Size of posterior (prior to this MCMC sample):
125
+ pre_zsize = M0 = np.shape(zold)[0]
126
+ else:
127
+ pre_zsize = M0 = hsize*nchains
128
+
129
+ # Number of Z samples per chain:
130
+ nzchain = int(np.ceil(nsamples/nchains/thinning))
131
+ # Number of iterations per chain:
132
+ niter = nzchain * thinning
133
+ # Total number of Z samples (initial + chains):
134
+ zlen = pre_zsize + nzchain*nchains
135
+
136
+ burnin = int(burnin)
137
+ if not resume and niter < burnin:
138
+ log.error(
139
+ f"The number of burned-in samples ({burnin}) is greater than "
140
+ f"the number of iterations per chain ({niter})"
141
+ )
142
+
143
+ # Initialize shared-memory variables:
144
+ sm_freepars = mpr.Array(ctypes.c_double, nchains*nfree)
145
+ freepars = np.ctypeslib.as_array(sm_freepars.get_obj())
146
+ freepars = freepars.reshape((nchains, nfree))
147
+
148
+ best_log_post = mpr.Value(ctypes.c_double, np.inf)
149
+ sm_bestp = mpr.Array(ctypes.c_double, np.copy(params))
150
+ bestp = np.ctypeslib.as_array(sm_bestp.get_obj())
151
+ # There seems to be a strange behavior with np.ctypeslib.as_array()
152
+ # when the argument is a single-element array. In this case, the
153
+ # returned value is a two-dimensional array, instead of 1D. The
154
+ # following line fixes(?) that behavior:
155
+ if np.ndim(bestp) > 1:
156
+ bestp = bestp.flatten()
157
+
158
+ numaccept = mpr.Value(ctypes.c_int, 0)
159
+ outbounds = mpr.Array(ctypes.c_int, nfree)
160
+
161
+ # Z array with the chains history:
162
+ sm_Z = mpr.Array(ctypes.c_double, zlen*nfree)
163
+ Z = np.ctypeslib.as_array(sm_Z.get_obj())
164
+ Z = Z.reshape((zlen, nfree))
165
+
166
+ # Chi-square value of Z:
167
+ sm_log_post = mpr.Array(ctypes.c_double, zlen)
168
+ log_post = np.ctypeslib.as_array(sm_log_post.get_obj())
169
+ # Chain index for given state in the Z array:
170
+ sm_zchain = mpr.Array(ctypes.c_int, -np.ones(zlen, int))
171
+ zchain = np.ctypeslib.as_array(sm_zchain.get_obj())
172
+ # Current number of samples in the Z array:
173
+ zsize = mpr.Value(ctypes.c_int, M0)
174
+ # Burned samples in the Z array per chain:
175
+ zburn = int(burnin/thinning)
176
+
177
+ # Include values from previous run:
178
+ if resume:
179
+ Z[0:pre_zsize,:] = zold
180
+ zchain[0:pre_zsize] = oldrun["zchain"]
181
+ log_post[0:pre_zsize] = oldrun["log_post"]
182
+ # Redefine zsize:
183
+ zsize.value = pre_zsize
184
+ numaccept.value = int(oldrun["acceptance_rate"] / 100. * pre_zsize)
185
+
186
+ # Set GR N-min as number of thinned samples:
187
+ if grnmin >= 1:
188
+ grnmin = int(grnmin/thinning)
189
+ elif grnmin > 0:
190
+ grnmin = int(grnmin*nchains*(nzchain-zburn))
191
+ elif grnmin < 0:
192
+ log.error(
193
+ "Invalid 'grnmin' argument (minimum number of samples to "
194
+ "stop the MCMC under GR convergence), must either be grnmin > 1"
195
+ "to set the minimum number of samples, or 0 < grnmin < 1"
196
+ "to set the fraction of samples required to evaluate.")
197
+ # Add these to compare grnmin to zsize (which also include them):
198
+ grnmin += int(M0 + zburn*nchains)
199
+
200
+ # Current length of each chain:
201
+ sm_chainsize = mpr.Array(ctypes.c_int, np.tile(hsize, nchains))
202
+ chainsize = np.ctypeslib.as_array(sm_chainsize.get_obj())
203
+
204
+ # Number of chains per processor:
205
+ ncpp = np.tile(int(nchains/ncpu), ncpu)
206
+ ncpp[0:nchains % ncpu] += 1
207
+
208
+ # Launch Chains:
209
+ mp_context = mpr.get_context('fork')
210
+ pipes = []
211
+ chains = []
212
+ for i in range(ncpu):
213
+ p = mp_context.Pipe()
214
+ pipes.append(p[0])
215
+ chains.append(
216
+ ch.Chain(func, indparams, indparams_dict, p[1], data, uncert,
217
+ params, freepars, pstep, pmin, pmax,
218
+ sampler, wlike, prior, priorlow, priorup, thinning,
219
+ fgamma, fepsilon, Z, zsize, log_post, zchain, M0,
220
+ numaccept, outbounds, ncpp[i],
221
+ chainsize, bestp, best_log_post, i, ncpu))
222
+
223
+ if resume:
224
+ bestp[:] = oldrun['bestp']
225
+ best_log_post.value = oldrun['best_log_post']
226
+ for c in range(nchains):
227
+ chainsize[c] = np.sum(zchain_old==c)
228
+ else:
229
+ def random_pick(kickoff):
230
+ x0 = np.copy(params[ifree])
231
+ sigma = np.copy(pstep[ifree])
232
+ x_min = np.copy(pmin[ifree])
233
+ x_max = np.copy(pmax[ifree])
234
+ while True:
235
+ if kickoff == 'normal':
236
+ yield np.random.normal(x0, sigma)
237
+ elif kickoff == 'uniform':
238
+ yield np.random.uniform(x_min, x_max)
239
+
240
+ # Evaluate models for initial sample of Z:
241
+ values = np.asarray(params)
242
+ i = 0
243
+ j = 0
244
+ nmax_trials = 100 * M0
245
+ for trial in random_pick(kickoff):
246
+ if i == M0 or j == nmax_trials:
247
+ break
248
+
249
+ values[ifree] = trial
250
+ if np.any(values > pmax) or np.any(values < pmin):
251
+ j += 1
252
+ continue
253
+ # Update shared parameters:
254
+ for s in ishare:
255
+ values[s] = values[-int(pstep[s])-1]
256
+ chi_square = -0.5*chains[0].eval_model(values, ret='chisq')
257
+ if not np.isfinite(chi_square):
258
+ j += 1
259
+ continue
260
+ Z[i] = values[ifree]
261
+ log_post[i] = chi_square
262
+ i += 1
263
+
264
+ if i < M0-1:
265
+ log.error(
266
+ 'Cannot populate an initial sample set of parameters, try '
267
+ 'updating the parameters initial guess to avoid sampling '
268
+ 'beyond the parameter boundaries or where the model returns '
269
+ 'non-finite values.'
270
+ )
271
+
272
+ # Best-fitting values (so far):
273
+ izbest = np.argmax(log_post[0:M0])
274
+ best_log_post.value = log_post[izbest]
275
+ bestp[ifree] = np.copy(Z[izbest])
276
+ if fit_output is not None:
277
+ bestp[:] = np.copy(fit_output['bestp'])
278
+ best_log_post.value = fit_output['best_log_post']
279
+
280
+ # The output dict:
281
+ output = {
282
+ 'pnames': pnames,
283
+ 'texnames': texnames,
284
+ 'pstep': pstep,
285
+ 'ifree': ifree,
286
+ 'burnin': zburn,
287
+ }
288
+
289
+ # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
290
+ # Start loop:
291
+ print("Yippee Ki Yay Monte Carlo!")
292
+ log.msg(f"Start MCMC chains ({time.ctime()})")
293
+ for chain in chains:
294
+ chain.start()
295
+ bit = bool(1) # Dummy variable to send through pipe for DEMC
296
+ # Intermediate steps to run GR test and print progress report:
297
+ intsteps = (nzchain*nchains) / 10
298
+ report = intsteps
299
+
300
+ while True:
301
+ # Proposal jump:
302
+ if sampler == "demc":
303
+ # Send and receive bit for DEMC synchronization:
304
+ for pipe in pipes:
305
+ pipe.send(bit)
306
+ for pipe in pipes:
307
+ _ = pipe.recv()
308
+
309
+ # Print intermediate info:
310
+ if (zsize.value-pre_zsize >= report) or (zsize.value == zlen):
311
+ report += intsteps
312
+ log.progressbar((zsize.value+1.0-pre_zsize)/(nzchain*nchains))
313
+
314
+ out_of_bounds = np.asarray(outbounds[:])
315
+ chisq = -2*best_log_post.value
316
+ log.msg(
317
+ f"Out-of-bound Trials:\n{out_of_bounds}\n"
318
+ f"Best Parameters: (chisq={chisq:.4f})\n{bestp[ifree]}",
319
+ width=80)
320
+
321
+ # Save intermediate state:
322
+ if savefile is not None:
323
+ ms.update_output(output, chains[0], hsize)
324
+ np.savez(savefile, **output)
325
+
326
+ # Gelman-Rubin statistics:
327
+ if grtest and np.all(chainsize > (zburn+hsize)):
328
+ psrf = ms.gelman_rubin(Z, zchain, zburn)
329
+ log.msg(
330
+ f"Gelman-Rubin statistics for free parameters:\n{psrf}",
331
+ width=80,
332
+ )
333
+ if np.all(psrf < 1.01):
334
+ log.msg("All parameters converged to within 1% of unity.")
335
+ converged = (
336
+ grbreak > 0.0 and
337
+ np.all(psrf < grbreak) and
338
+ zsize.value > grnmin
339
+ )
340
+ if converged:
341
+ with zsize.get_lock():
342
+ zsize.value = zlen
343
+ log.msg(
344
+ "\nAll parameters satisfy the GR convergence "
345
+ f"threshold of {grbreak:g}, stopping the MCMC.")
346
+ break
347
+ if zsize.value == zlen:
348
+ break
349
+
350
+ # Make sure chains finish and release all locks
351
+ for chain in chains:
352
+ chain.join()
353
+
354
+ for chain in chains: # Make sure to terminate the subprocesses
355
+ chain.terminate()
356
+
357
+ # Evaluate model for best fitting parameters:
358
+ posterior = ms.update_output(output, chains[0], hsize)
359
+
360
+ # Print out Summary:
361
+ Z = output['posterior']
362
+ nsample = len(Z)*thinning
363
+ nzsample = len(posterior)
364
+ fmt = len(str(nsample))
365
+ chain_iter = nsample // nchains
366
+ accept_rate = output['acceptance_rate']
367
+
368
+ log.msg('\nMCMC Summary:\n-------------')
369
+ log.msg(
370
+ f"Number of evaluated samples: {nsample:{fmt}d}\n"
371
+ f"Number of parallel chains: {nchains:{fmt}d}\n"
372
+ f"Average iterations per chain: {chain_iter:{fmt}d}\n"
373
+ f"Burned-in iterations per chain: {burnin:{fmt}d}\n"
374
+ f"Thinning factor: {thinning:{fmt}d}\n"
375
+ f"MCMC sample size (thinned, burned): {nzsample:{fmt}d}\n"
376
+ f"Acceptance rate: {accept_rate:.2f}%\n", indent=2)
377
+
378
+ return output
mc3/plots/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ # Copyright (c) 2015-2026 Patricio Cubillos and contributors.
2
+ # mc3 is open-source software under the MIT license (see LICENSE).
3
+
4
+ from .plot_functions import *
5
+ from .posterior import *
6
+ from .colors import *
7
+
8
+ __all__ = (
9
+ plot_functions.__all__
10
+ + posterior.__all__
11
+ + colors.__all__
12
+ )
13
+
14
+ # Clean up top-level namespace--delete everything that isn't in __all__
15
+ # or is a magic attribute, and that isn't a submodule of this package
16
+ for varname in dir():
17
+ if not ((varname.startswith('__') and varname.endswith('__')) or
18
+ varname in __all__):
19
+ del locals()[varname]
20
+ del(varname)