def check_convergence_and_learn_proposal(self): """ Checks the convergence of the sampling process, and, if requested, learns a new covariance matrix for the proposal distribution from the covariance of the last samples. """ # Compute Rminus1 of means self.been_waiting = 0 if more_than_one_process(): # Compute and gather means and covs use_first = int(self.n() / 2) mean = self.collection.mean(first=use_first) cov = self.collection.cov(first=use_first) acceptance_rate = self.get_acceptance_rate(use_first) Ns, means, covs, acceptance_rates = mpi.array_gather( [self.n(), mean, cov, acceptance_rate]) else: # Compute and gather means, covs and CL intervals of last m-1 chain fractions m = 1 + self.Rminus1_single_split cut = int(len(self.collection) / m) try: acceptance_rate = self.get_acceptance_rate(cut) Ns = np.ones(m - 1) * cut means = np.array([ self.collection.mean(first=i * cut, last=(i + 1) * cut - 1) for i in range(1, m) ]) covs = np.array([ self.collection.cov(first=i * cut, last=(i + 1) * cut - 1) for i in range(1, m) ]) except: self.log.info( "Not enough points in chain to check convergence. " "Waiting for next checkpoint.") return acceptance_rates = None if is_main_process(): self.progress.at[self.i_learn, "N"] = sum(Ns) self.progress.at[self.i_learn, "timestamp"] = \ datetime.datetime.now().isoformat() acceptance_rate = (np.average(acceptance_rates, weights=Ns) if acceptance_rates is not None else acceptance_rate) self.log.info( " - Acceptance rate: %.3f" + (" = avg(%r)" % list(acceptance_rates) if acceptance_rates is not None else ""), acceptance_rate) self.progress.at[self.i_learn, "acceptance_rate"] = acceptance_rate # "Within" or "W" term -- our "units" for assessing convergence # and our prospective new covariance matrix mean_of_covs = np.average(covs, weights=Ns, axis=0) # "Between" or "B" term # We don't weight with the number of samples in the chains here: # shorter chains will likely be outliers, and we want to notice them cov_of_means = np.atleast_2d(np.cov(means.T)) # , fweights=Ns) # For numerical stability, we turn mean_of_covs into correlation matrix: # rho = (diag(Sigma))^(-1/2) * Sigma * (diag(Sigma))^(-1/2) # and apply the same transformation to the mean of covs (same eigenvals!) d = np.sqrt(np.diag(cov_of_means)) corr_of_means = (cov_of_means / d).T / d norm_mean_of_covs = (mean_of_covs / d).T / d success_means = False converged_means = False # Cholesky of (normalized) mean of covs and eigvals of Linv*cov_of_means*L try: L = np.linalg.cholesky(norm_mean_of_covs) except np.linalg.LinAlgError: self.log.warning( "Negative covariance eigenvectors. " "This may mean that the covariance of the samples does not " "contain enough information at this point. " "Skipping learning a new covmat for now.") else: Linv = np.linalg.inv(L) try: eigvals = np.linalg.eigvalsh( Linv.dot(corr_of_means).dot(Linv.T)) success_means = True except np.linalg.LinAlgError: self.log.warning("Could not compute eigenvalues. " "Skipping learning a new covmat for now.") else: Rminus1 = max(np.abs(eigvals)) self.progress.at[self.i_learn, "Rminus1"] = Rminus1 # For real square matrices, a possible def of the cond number is: condition_number = Rminus1 / min(np.abs(eigvals)) self.log.debug(" - Condition number = %g", condition_number) self.log.debug(" - Eigenvalues = %r", eigvals) self.log.info( " - Convergence of means: R-1 = %f after %d accepted steps" % (Rminus1, sum(Ns)) + (" = sum(%r)" % list(Ns) if more_than_one_process() else "")) # Have we converged in means? # (criterion must be fulfilled twice in a row) converged_means = max( Rminus1, self.Rminus1_last) < self.Rminus1_stop else: mean_of_covs = None success_means = None converged_means = False Rminus1 = None success_means, converged_means = mpi.share( (success_means, converged_means)) # Check the convergence of the bounds of the confidence intervals # Same as R-1, but with the rms deviation from the mean bound # in units of the mean standard deviation of the chains if converged_means: if more_than_one_process(): mcsamples = self.collection.sampled_to_getdist_mcsamples( first=use_first) try: bound = np.array([[ mcsamples.confidence(i, limfrac=self.Rminus1_cl_level / 2., upper=which) for i in range(self.model.prior.d()) ] for which in [False, True]]).T success_bounds = True except: bound = None success_bounds = False bounds = np.array(mpi.gather(bound)) else: try: mcsamples_list = [ self.collection.sampled_to_getdist_mcsamples( first=i * cut, last=(i + 1) * cut - 1) for i in range(1, m) ] except: self.log.info( "Not enough points in chain to check c.l. convergence. " "Waiting for next checkpoint.") return try: bounds = [ np.array([[ mcs.confidence(i, limfrac=self.Rminus1_cl_level / 2., upper=which) for i in range(self.model.prior.d()) ] for which in [False, True]]).T for mcs in mcsamples_list ] success_bounds = True except: bounds = None success_bounds = False if is_main_process(): if success_bounds: Rminus1_cl = (np.std(bounds, axis=0).T / np.sqrt(np.diag(mean_of_covs))) self.log.debug(" - normalized std's of bounds = %r", Rminus1_cl) Rminus1_cl = np.max(Rminus1_cl) self.progress.at[self.i_learn, "Rminus1_cl"] = Rminus1_cl self.log.info( " - Convergence of bounds: R-1 = %f after %d " % (Rminus1_cl, (sum(Ns) if more_than_one_process() else self.n())) + "accepted steps" + (" = sum(%r)" % list(Ns) if more_than_one_process() else "")) if Rminus1_cl < self.Rminus1_cl_stop: self.converged = True self.log.info("The run has converged!") self._Ns = Ns else: self.log.info( "Computation of the bounds was not possible. " "Waiting until the next converge check.") # Broadcast and save the convergence status and the last R-1 of means if success_means: self.Rminus1_last, self.converged = mpi.share(( Rminus1, self.converged) if is_main_process() else None) # Do we want to learn a better proposal pdf? if self.learn_proposal and not self.converged: good_Rminus1 = (self.learn_proposal_Rminus1_max > self.Rminus1_last > self.learn_proposal_Rminus1_min) if not good_Rminus1: self.mpi_info( "Convergence less than requested for updates: " "waiting until the next convergence check.") return mean_of_covs = mpi.share(mean_of_covs) try: self.proposer.set_covariance(mean_of_covs) self.mpi_info( " - Updated covariance matrix of proposal pdf.") self.mpi_debug("%r", mean_of_covs) except: self.mpi_debug( "Updating covariance matrix failed unexpectedly. " "waiting until next covmat learning attempt.") # Save checkpoint info self.write_checkpoint()
def __init__(self, info_sampler, model, output=None, packages_path=None, name=None): """ Actual initialization of the class. Loads the default and input information and call the custom ``initialize`` method. [Do not modify this one.] """ self.model = model self.output = output self._updated_info = deepcopy_where_possible(info_sampler) super().__init__(info_sampler, packages_path=packages_path, name=name, initialize=False, standalone=False) # Seed, if requested if getattr(self, "seed", None) is not None: if not isinstance(self.seed, int) or not (0 <= self.seed <= 2**32 - 1): raise LoggedError( self.log, "Seeds must be a *positive integer* < 2**32 - 1, " "but got %r with type %r", self.seed, type(self.seed)) # MPI-awareness: sum the rank to the seed if more_than_one_process(): self.seed += get_mpi_rank() self.mpi_warning("This run has been SEEDED with seed %d", self.seed) # Load checkpoint info, if resuming if self.output.is_resuming() and not isinstance(self, Minimizer): try: checkpoint_info = yaml_load_file(self.checkpoint_filename()) try: for k, v in checkpoint_info[kinds.sampler][ self.get_name()].items(): setattr(self, k, v) self.mpi_info("Resuming from previous sample!") except KeyError: if is_main_process(): raise LoggedError( self.log, "Checkpoint file found at '%s' " "but it corresponds to a different sampler.", self.checkpoint_filename()) except (IOError, TypeError): pass else: try: os.remove(self.checkpoint_filename()) os.remove(self.progress_filename()) except (OSError, TypeError): pass self._set_rng() self.initialize() self._release_rng() self.model.set_cache_size(self._get_requested_cache_size()) # Add to the updated info some values which are # only available after initialisation self._updated_info[_version] = self.get_version()
def run(self): """ Runs the sampler. """ self.mpi_info("Sampling!" + ( " (NB: no accepted step will be saved until %d burn-in samples " % self.burn_in.value + "have been obtained)" if self.burn_in.value else "")) self.n_steps_raw = 0 last_output: float = 0 last_n = self.n() state_check_every = 1 with mpi.ProcessState(self) as state: while last_n < self.max_samples and not self.converged: self.get_new_sample() self.n_steps_raw += 1 if self.output_every.unit: # if output_every in sec, print some info # and dump at fixed time intervals now = datetime.datetime.now() now_sec = now.timestamp() if now_sec >= last_output + self.output_every.value: self.do_output(now) last_output = now_sec state.check_error() if self.current_point.weight == 1: # have added new point # Callback function n = self.n() if n != last_n: # and actually added last_n = n if (self.callback_function and not (max(n, 1) % self.callback_every.value) and self.current_point.weight == 1): self.callback_function_callable(self) self.last_point_callback = len(self.collection) if more_than_one_process(): # Checking convergence and (optionally) learning # the covmat of the proposal if self.check_ready() and state.set( mpi.State.READY): self.log.info(self._msg_ready + " (waiting for the rest...)") if state.all_ready(): self.mpi_info("All chains are r%s", self._msg_ready[1:]) self.check_convergence_and_learn_proposal() self.i_learn += 1 else: if self.check_ready(): self.log.debug(self._msg_ready) self.check_convergence_and_learn_proposal() self.i_learn += 1 elif self.current_point.weight % state_check_every == 0: state.check_error() # more frequent checks near beginning state_check_every = min(10, state_check_every + 1) if last_n == self.max_samples: self.log.info( "Reached maximum number of accepted steps allowed (%s). " "Stopping.", self.max_samples) # Write the last batch of samples ( < output_every (not in sec)) self.collection.out_update() ns = mpi.gather(self.n()) self.mpi_info("Sampling complete after %d accepted steps.", sum(ns))
def initialize(self): self.mpi_info("Initializing") self.max_evals = read_dnumber(self.max_evals, self.model.prior.d()) # Configure target method = self.model.loglike if self.ignore_prior else self.model.logpost kwargs = {"make_finite": True} if self.ignore_prior: kwargs["return_derived"] = False self.logp = lambda x: method(x, **kwargs) # Try to load info from previous samples. # If none, sample from reference (make sure that it has finite like/post) initial_point = None if self.output: files = self.output.find_collections() collection_in = None if files: if more_than_one_process(): if 1 + get_mpi_rank() <= len(files): collection_in = Collection(self.model, self.output, name=str(1 + get_mpi_rank()), resuming=True) else: collection_in = self.output.load_collections( self.model, concatenate=True) if collection_in: initial_point = (collection_in.bestfit() if self.ignore_prior else collection_in.MAP()) initial_point = initial_point[list( self.model.parameterization.sampled_params())].values self.log.info("Starting from %s of previous chain:", "best fit" if self.ignore_prior else "MAP") if initial_point is None: this_logp = -np.inf while not np.isfinite(this_logp): initial_point = self.model.prior.reference() this_logp = self.logp(initial_point) self.log.info("Starting from random initial point:") self.log.info( dict( zip(self.model.parameterization.sampled_params(), initial_point))) self._bounds = self.model.prior.bounds( confidence_for_unbounded=self.confidence_for_unbounded) # TODO: if ignore_prior, one should use *like* covariance (this is *post*) covmat = self._load_covmat(self.output)[0] # scale by conditional parameter widths (since not using correlation structure) scales = np.minimum(1 / np.sqrt(np.diag(np.linalg.inv(covmat))), (self._bounds[:, 1] - self._bounds[:, 0]) / 3) # Cov and affine transformation # Transform to space where initial point is at centre, and cov is normalised # Cannot do rotation, as supported minimization routines assume bounds aligned # with the parameter axes. self._affine_transform_matrix = np.diag(1 / scales) self._inv_affine_transform_matrix = np.diag(scales) self._scales = scales self._affine_transform_baseline = initial_point initial_point = self.affine_transform(initial_point) np.testing.assert_allclose(initial_point, np.zeros(initial_point.shape)) bounds = np.array( [self.affine_transform(self._bounds[:, i]) for i in range(2)]).T # Configure method if self.method.lower() == "bobyqa": self.minimizer = pybobyqa.solve self.kwargs = { "objfun": (lambda x: -self.logp_transf(x)), "x0": initial_point, "bounds": np.array(list(zip(*bounds))), "seek_global_minimum": (True if get_mpi_size() in [0, 1] else False), "maxfun": int(self.max_evals) } self.kwargs = recursive_update(deepcopy(self.kwargs), self.override_bobyqa or {}) self.log.debug( "Arguments for pybobyqa.solve:\n%r", {k: v for k, v in self.kwargs.items() if k != "objfun"}) elif self.method.lower() == "scipy": self.minimizer = scpminimize self.kwargs = { "fun": (lambda x: -self.logp_transf(x)), "x0": initial_point, "bounds": bounds, "options": { "maxiter": self.max_evals, "disp": (self.log.getEffectiveLevel() == logging.DEBUG) } } self.kwargs = recursive_update(deepcopy(self.kwargs), self.override_scipy or {}) self.log.debug( "Arguments for scipy.optimize.minimize:\n%r", {k: v for k, v in self.kwargs.items() if k != "fun"}) else: methods = ["bobyqa", "scipy"] raise LoggedError(self.log, "Method '%s' not recognized. Try one of %r.", self.method, methods)
def process_results(self): """ Determines success (or not), chooses best (if MPI) and produces output (if requested). """ evals_attr_ = evals_attr[self.method.lower()] # If something failed if not hasattr(self, "result"): return if more_than_one_process(): results = get_mpi_comm().gather(self.result, root=0) successes = get_mpi_comm().gather(self.success, root=0) _affine_transform_baselines = get_mpi_comm().gather( self._affine_transform_baseline, root=0) if is_main_process(): mins = [(getattr(r, evals_attr_) if s else np.inf) for r, s in zip(results, successes)] i_min = np.argmin(mins) self.result = results[i_min] self._affine_transform_baseline = _affine_transform_baselines[ i_min] else: successes = [self.success] if is_main_process(): if not any(successes): raise LoggedError( self.log, "Minimization failed! Here is the raw result object:\n%s", str(self.result)) elif not all(successes): self.log.warning('Some minimizations failed!') elif more_than_one_process(): if max(mins) - min(mins) > 1: self.log.warning('Big spread in minima: %r', mins) elif max(mins) - min(mins) > 0.2: self.log.warning('Modest spread in minima: %r', mins) logp_min = -np.array(getattr(self.result, evals_attr_)) x_min = self.inv_affine_transform(self.result.x) self.log.info("-log(%s) minimized to %g", "likelihood" if self.ignore_prior else "posterior", -logp_min) recomputed_post_min = self.model.logposterior(x_min, cached=False) recomputed_logp_min = (sum(recomputed_post_min.loglikes) if self.ignore_prior else recomputed_post_min.logpost) if not np.allclose(logp_min, recomputed_logp_min, atol=1e-2): raise LoggedError( self.log, "Cannot reproduce log minimum to within 0.01. Maybe your " "likelihood is stochastic or large numerical error? " "Recomputed min: %g (was %g) at %r", recomputed_logp_min, logp_min, x_min) self.minimum = OnePoint(self.model, self.output, name="", extension=get_collection_extension( self.ignore_prior)) self.minimum.add(x_min, derived=recomputed_post_min.derived, logpost=recomputed_post_min.logpost, logpriors=recomputed_post_min.logpriors, loglikes=recomputed_post_min.loglikes) self.log.info("Parameter values at minimum:\n%s", self.minimum.data.to_string()) self.minimum.out_update() self.dump_getdist()
def run(self): """ Runs the sampler. """ # Get first point, to be discarded -- not possible to determine its weight # Still, we need to compute derived parameters, since, as the proposal "blocked", # we may be saving the initial state of some block. # NB: if resuming but nothing was written (burn-in not finished): re-start self.log.info("Initial point:") if self.resuming and self.collection.n(): initial_point = (self.collection[ self.collection.sampled_params].ix[self.collection.n() - 1]).values.copy() logpost = -(self.collection[_minuslogpost].ix[self.collection.n() - 1].copy()) logpriors = -(self.collection[self.collection.prior_names].ix[ self.collection.n() - 1].copy()) loglikes = -0.5 * (self.collection[self.collection.chi2_names].ix[ self.collection.n() - 1].copy()) derived = (self.collection[self.collection.derived_params].ix[ self.collection.n() - 1].values.copy()) else: initial_point = self.model.prior.reference( max_tries=self.max_tries) logpost, logpriors, loglikes, derived = self.model.logposterior( initial_point) self.current_point.add(initial_point, derived=derived, logpost=logpost, logpriors=logpriors, loglikes=loglikes) self.log.info( "\n%s", self.current_point.data.to_string(index=False, line_width=_line_width)) # Initial dummy checkpoint (needed when 1st checkpoint not reached in prev. run) self.write_checkpoint() # Main loop! self.log.info("Sampling!" + ( " (NB: nothing will be printed until %d burn-in samples " % self.burn_in + "have been obtained)" if self.burn_in else "")) while self.n() < self.effective_max_samples and not self.converged: self.get_new_sample() # Callback function if (hasattr(self, "callback_function_callable") and not (max(self.n(), 1) % self.callback_every) and self.current_point[_weight] == 1): self.callback_function_callable(self) self.last_point_callback = self.collection.n() # Checking convergence and (optionally) learning the covmat of the proposal if self.check_all_ready(): self.check_convergence_and_learn_proposal() if self.n() == self.effective_max_samples: self.log.info( "Reached maximum number of accepted steps allowed. " "Stopping.") # Make sure the last batch of samples ( < output_every ) are written self.collection._out_update() if more_than_one_process(): Ns = (lambda x: np.array(get_mpi_comm().gather(x)))(self.n()) else: Ns = [self.n()] if am_single_or_primary_process(): self.log.info("Sampling complete after %d accepted steps.", sum(Ns))
def check_convergence_and_learn_proposal(self): """ Checks the convergence of the sampling process (MPI only), and, if requested, learns a new covariance matrix for the proposal distribution from the covariance of the last samples. """ if more_than_one_process(): # Compute and gather means, covs and CL intervals of last half of chains mean = self.collection.mean(first=int(self.n() / 2)) cov = self.collection.cov(first=int(self.n() / 2)) mcsamples = self.collection._sampled_to_getdist_mcsamples( first=int(self.n() / 2)) try: bound = np.array([[ mcsamples.confidence(i, limfrac=self.Rminus1_cl_level / 2., upper=which) for i in range(self.model.prior.d()) ] for which in [False, True]]).T success_bounds = True except: bound = None success_bounds = False Ns, means, covs, bounds = map( lambda x: np.array(get_mpi_comm().gather(x)), [self.n(), mean, cov, bound]) else: # Compute and gather means, covs and CL intervals of last m-1 chain fractions m = 1 + self.Rminus1_single_split cut = int(self.collection.n() / m) if cut <= 1: self.log.error( "Not enough points in chain to check convergence. " "Increase `check_every` or reduce `Rminus1_single_split`.") raise HandledException Ns = (m - 1) * [cut] means = np.array([ self.collection.mean(first=i * cut, last=(i + 1) * cut - 1) for i in range(1, m) ]) covs = np.array([ self.collection.cov(first=i * cut, last=(i + 1) * cut - 1) for i in range(1, m) ]) # No logging of warnings temporarily, so getdist won't complain unnecessarily logging.disable(logging.WARNING) mcsampleses = [ self.collection._sampled_to_getdist_mcsamples( first=i * cut, last=(i + 1) * cut - 1) for i in range(1, m) ] logging.disable(logging.NOTSET) try: bounds = [ np.array([[ mcs.confidence(i, limfrac=self.Rminus1_cl_level / 2., upper=which) for i in range(self.model.prior.d()) ] for which in [False, True]]).T for mcs in mcsampleses ] success_bounds = True except: bounds = None success_bounds = False # Compute convergence diagnostics if am_single_or_primary_process(): # "Within" or "W" term -- our "units" for assessing convergence # and our prospective new covariance matrix mean_of_covs = np.average(covs, weights=Ns, axis=0) # "Between" or "B" term # We don't weight with the number of samples in the chains here: # shorter chains will likely be outliers, and we want to notice them cov_of_means = np.atleast_2d(np.cov(means.T)) # , fweights=Ns) # For numerical stability, we turn mean_of_covs into correlation matrix: # rho = (diag(Sigma))^(-1/2) * Sigma * (diag(Sigma))^(-1/2) # and apply the same transformation to the mean of covs (same eigenvals!) diagSinvsqrt = np.diag(np.power(np.diag(cov_of_means), -0.5)) corr_of_means = diagSinvsqrt.dot(cov_of_means).dot(diagSinvsqrt) norm_mean_of_covs = diagSinvsqrt.dot(mean_of_covs).dot( diagSinvsqrt) # Cholesky of (normalized) mean of covs and eigvals of Linv*cov_of_means*L try: L = np.linalg.cholesky(norm_mean_of_covs) except np.linalg.LinAlgError: self.log.warning( "Negative covariance eigenvectors. " "This may mean that the covariance of the samples does not " "contain enough information at this point. " "Skipping this checkpoint") success = False else: Linv = np.linalg.inv(L) try: eigvals = np.linalg.eigvalsh( Linv.dot(corr_of_means).dot(Linv.T)) success = True except np.linalg.LinAlgError: self.log.warning("Could not compute eigenvalues. " "Skipping this checkpoint.") success = False if success: Rminus1 = max(np.abs(eigvals)) # For real square matrices, a possible def of the cond number is: condition_number = Rminus1 / min(np.abs(eigvals)) self.log.debug("Condition number = %g", condition_number) self.log.debug("Eigenvalues = %r", eigvals) self.log.info( "Convergence of means: R-1 = %f after %d accepted steps" % (Rminus1, (sum(Ns) if more_than_one_process() else self.n())) + (" = sum(%r)" % list(Ns) if more_than_one_process() else "")) # Have we converged in means? # (criterion must be fulfilled twice in a row) if max(Rminus1, self.Rminus1_last) < self.Rminus1_stop: # Check the convergence of the bounds of the confidence intervals # Same as R-1, but with the rms deviation from the mean bound # in units of the mean standard deviation of the chains if success_bounds: Rminus1_cl = (np.std(bounds, axis=0).T / np.sqrt(np.diag(mean_of_covs))) self.log.debug("normalized std's of bounds = %r", Rminus1_cl) self.log.info( "Convergence of bounds: R-1 = %f after %d " % (np.max(Rminus1_cl), (sum(Ns) if more_than_one_process() else self. n())) + "accepted steps" + (" = sum(%r)" % list(Ns) if more_than_one_process() else "")) if np.max(Rminus1_cl) < self.Rminus1_cl_stop: self.converged = True self.log.info("The run has converged!") self._Ns = Ns else: self.log.info( "Computation of the bounds was not possible. " "Waiting until the next checkpoint") if more_than_one_process(): # Broadcast and save the convergence status and the last R-1 of means success = get_mpi_comm().bcast( (success if am_single_or_primary_process() else None), root=0) if success: self.Rminus1_last = get_mpi_comm().bcast( (Rminus1 if am_single_or_primary_process() else None), root=0) self.converged = get_mpi_comm().bcast( (self.converged if am_single_or_primary_process() else None), root=0) else: if success: self.Rminus1_last = Rminus1 # Do we want to learn a better proposal pdf? if self.learn_proposal and not self.converged and success: good_Rminus1 = (self.learn_proposal_Rminus1_max > self.Rminus1_last > self.learn_proposal_Rminus1_min) if not good_Rminus1: if am_single_or_primary_process(): self.log.info("Bad convergence statistics: " "waiting until the next checkpoint.") return if more_than_one_process(): if not am_single_or_primary_process(): mean_of_covs = np.empty( (self.model.prior.d(), self.model.prior.d())) get_mpi_comm().Bcast(mean_of_covs, root=0) else: mean_of_covs = covs[0] try: self.proposer.set_covariance(mean_of_covs) except: self.log.debug( "Updating covariance matrix failed unexpectedly. " "waiting until next checkpoint.") if am_single_or_primary_process(): self.log.info("Updated covariance matrix of proposal pdf.") self.log.debug("%r", mean_of_covs) # Save checkpoint info self.write_checkpoint()
@pytest.fixture def skip_not_installed(request): return request.config.getoption("--skip-not-installed") def install_test_wrapper(skip_not_installed, func, *args, **kwargs): try: return func(*args, **kwargs) except NotInstalledError: if skip_not_installed: pytest.xfail("Missing dependencies.") raise if mpi.more_than_one_process(): @pytest.fixture(scope="session", autouse=True) def mpi_handling(request): mpi.capture_manager = request.config.pluginmanager.getplugin("capturemanager") @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield rep = outcome.get_result() if rep.when == "call" and rep.failed: rep.sections = [i for i in rep.sections if i[0] != "Captured log call"] if call.excinfo and isinstance(call.excinfo.value, mpi.OtherProcessError): # Only need very short message; stdout printed in raised-error process rep.longrepr = str(call.excinfo.value) rep.sections = []
def initialize(self): """Initializes the sampler: creates the proposal distribution and draws the initial sample.""" if not self.model.prior.d(): raise LoggedError(self.log, "No parameters being varied for sampler") self.log.debug("Initializing") # MARKED FOR DEPRECATION IN v3.0 if getattr(self, "oversample", None) is not None: self.log.warning( "*DEPRECATION*: `oversample` will be deprecated in the " "next version. Oversampling is now requested by setting " "`oversample_power` > 0.") # END OF DEPRECATION BLOCK # MARKED FOR DEPRECATION IN v3.0 if getattr(self, "check_every", None) is not None: self.log.warning( "*DEPRECATION*: `check_every` will be deprecated in the " "next version. Please use `learn_every` instead.") # BEHAVIOUR TO BE REPLACED BY ERROR: self.learn_every = getattr(self, "check_every") # END OF DEPRECATION BLOCK if self.callback_every is None: self.callback_every = self.learn_every self._quants_d_units = [] for q in ["max_tries", "learn_every", "callback_every", "burn_in"]: number = NumberWithUnits(getattr(self, q), "d", dtype=int) self._quants_d_units.append(number) setattr(self, q, number) self.output_every = NumberWithUnits(self.output_every, "s", dtype=int) if is_main_process(): if self.output.is_resuming() and (max(self.mpi_size or 0, 1) != max(get_mpi_size(), 1)): raise LoggedError( self.log, "Cannot resume a run with a different number of chains: " "was %d and now is %d.", max(self.mpi_size, 1), max(get_mpi_size(), 1)) if more_than_one_process(): if get_mpi().Get_version()[0] < 3: raise LoggedError( self.log, "MPI use requires MPI version 3.0 or " "higher to support IALLGATHER.") sync_processes() # One collection per MPI process: `name` is the MPI rank + 1 name = str(1 + (lambda r: r if r is not None else 0)(get_mpi_rank())) self.collection = Collection(self.model, self.output, name=name, resuming=self.output.is_resuming()) self.current_point = OneSamplePoint(self.model) # Use standard MH steps by default self.get_new_sample = self.get_new_sample_metropolis # Prepare callback function if self.callback_function is not None: self.callback_function_callable = (get_external_function( self.callback_function)) # Useful for getting last points added inside callback function self.last_point_callback = 0 # Monitoring/restore progress if is_main_process(): cols = [ "N", "timestamp", "acceptance_rate", "Rminus1", "Rminus1_cl" ] self.progress = DataFrame(columns=cols) self.i_learn = 1 if self.output and not self.output.is_resuming(): with open(self.progress_filename(), "w", encoding="utf-8") as progress_file: progress_file.write("# " + " ".join(self.progress.columns) + "\n") # Get first point, to be discarded -- not possible to determine its weight # Still, we need to compute derived parameters, since, as the proposal "blocked", # we may be saving the initial state of some block. # NB: if resuming but nothing was written (burn-in not finished): re-start if self.output.is_resuming() and len(self.collection): initial_point = (self.collection[ self.collection.sampled_params].iloc[len(self.collection) - 1]).values.copy() logpost = -(self.collection[_minuslogpost].iloc[ len(self.collection) - 1].copy()) logpriors = -(self.collection[self.collection.minuslogprior_names]. iloc[len(self.collection) - 1].copy()) loglikes = -0.5 * (self.collection[self.collection.chi2_names]. iloc[len(self.collection) - 1].copy()) derived = (self.collection[self.collection.derived_params].iloc[ len(self.collection) - 1].values.copy()) else: # NB: max_tries adjusted to dim instead of #cycles (blocking not computed yet) self.max_tries.set_scale(self.model.prior.d()) self.log.info( "Getting initial point... (this may take a few seconds)") initial_point, logpost, logpriors, loglikes, derived = \ self.model.get_valid_point(max_tries=self.max_tries.value) # If resuming but no existing chain, assume failed run and ignore blocking # if speeds measurement requested if self.output.is_resuming() and not len(self.collection) \ and self.measure_speeds: self.blocking = None if self.measure_speeds and self.blocking: self.log.warning( "Parameter blocking manually fixed: speeds will not be measured." ) elif self.measure_speeds: n = None if self.measure_speeds is True else int( self.measure_speeds) self.model.measure_and_set_speeds(n=n, discard=0) self.set_proposer_blocking() self.set_proposer_covmat(load=True) self.current_point.add(initial_point, derived=derived, logpost=logpost, logpriors=logpriors, loglikes=loglikes) self.log.info("Initial point: %s", self.current_point) # Max #(learn+convergence checks) to wait, # in case one process dies without sending MPI_ABORT self.been_waiting = 0 self.max_waiting = max(50, self.max_tries.unit_value) # Burning-in countdown -- the +1 accounts for the initial point (always accepted) self.burn_in_left = self.burn_in.value * self.current_point.output_thin + 1 # Initial dummy checkpoint # (needed when 1st "learn point" not reached in prev. run) self.write_checkpoint()
def initialize(self): """Imports the PolyChord sampler and prepares its arguments.""" if am_single_or_primary_process( ): # rank = 0 (MPI master) or None (no MPI) self.log.info("Initializing") # If path not given, try using general path to modules if not self.path and self.path_install: self.path = get_path(self.path_install) if self.path: if am_single_or_primary_process(): self.log.info("Importing *local* PolyChord from " + self.path) if not os.path.exists(os.path.realpath(self.path)): raise LoggedError( self.log, "The given path does not exist. " "Try installing PolyChord with " "'cobaya-install polychord -m [modules_path]") pc_build_path = get_build_path(self.path) if not pc_build_path: raise LoggedError( self.log, "Either PolyChord is not in the given folder, " "'%s', or you have not compiled it.", self.path) # Inserting the previously found path into the list of import folders sys.path.insert(0, pc_build_path) else: self.log.info("Importing *global* PolyChord.") try: import pypolychord from pypolychord.settings import PolyChordSettings self.pc = pypolychord except ImportError: raise LoggedError( self.log, "Couldn't find the PolyChord python interface. " "Make sure that you have compiled it, and that you either\n" " (a) specify a path (you didn't) or\n" " (b) install the Python interface globally with\n" " '/path/to/PolyChord/python setup.py install --user'") # Prepare arguments and settings self.nDims = self.model.prior.d() self.nDerived = (len(self.model.parameterization.derived_params()) + len(self.model.prior) + len(self.model.likelihood._likelihoods)) if self.logzero is None: self.logzero = np.nan_to_num(-np.inf) if self.max_ndead == np.inf: self.max_ndead = -1 for p in ["nlive", "nprior", "max_ndead"]: setattr(self, p, read_dnumber(getattr(self, p), self.nDims, dtype=int)) # Fill the automatic ones if getattr(self, "feedback", None) is None: values = { logging.CRITICAL: 0, logging.ERROR: 0, logging.WARNING: 0, logging.INFO: 1, logging.DEBUG: 2 } self.feedback = values[self.log.getEffectiveLevel()] try: output_folder = getattr(self.output, "folder") output_prefix = getattr(self.output, "prefix") or "" self.read_resume = self.resuming except AttributeError: # dummy output -- no resume! self.read_resume = False from tempfile import gettempdir output_folder = gettempdir() if am_single_or_primary_process(): from random import random output_prefix = hex(int(random() * 16**6))[2:] else: output_prefix = None if more_than_one_process(): output_prefix = get_mpi_comm().bcast(output_prefix, root=0) self.base_dir = os.path.join(output_folder, self.base_dir) self.file_root = output_prefix if am_single_or_primary_process(): # Creating output folder, if it does not exist (just one process) if not os.path.exists(self.base_dir): os.makedirs(self.base_dir) # Idem, a clusters folder if needed -- notice that PolyChord's default # is "True", here "None", hence the funny condition below if self.do_clustering is not False: # None here means "default" try: os.makedirs(os.path.join(self.base_dir, clusters)) except OSError: # exists! pass self.log.info("Storing raw PolyChord output in '%s'.", self.base_dir) # Exploiting the speed hierarchy if self.blocking: speeds, blocks = self.model.likelihood._check_speeds_of_params( self.blocking) else: speeds, blocks = self.model.likelihood._speeds_of_params( int_speeds=True) blocks_flat = list(chain(*blocks)) self.ordering = [ blocks_flat.index(p) for p in self.model.parameterization.sampled_params() ] self.grade_dims = np.array([len(block) for block in blocks]) # bugfix: pypolychord's C interface for Fortran does not like int numpy types self.grade_dims = [int(x) for x in self.grade_dims] # Steps per block # NB: num_repeats is ignored by PolyChord when int "grade_frac" given, # so needs to be applied by hand. # Make sure that speeds are integer, and that the slowest is 1, # for a straightforward application of num_repeats speeds = relative_to_int(speeds, 1) # In num_repeats, `d` is interpreted as dimension of each block self.grade_frac = [ int(speed * read_dnumber(self.num_repeats, dim_block)) for speed, dim_block in zip(speeds, self.grade_dims) ] # Assign settings pc_args = [ "nlive", "num_repeats", "nprior", "do_clustering", "precision_criterion", "max_ndead", "boost_posterior", "feedback", "logzero", "posteriors", "equals", "compression_factor", "cluster_posteriors", "write_resume", "read_resume", "write_stats", "write_live", "write_dead", "base_dir", "grade_frac", "grade_dims", "feedback", "read_resume", "base_dir", "file_root", "grade_frac", "grade_dims" ] # As stated above, num_repeats is ignored, so let's not pass it pc_args.pop(pc_args.index("num_repeats")) self.pc_settings = PolyChordSettings( self.nDims, self.nDerived, seed=(self.seed if self.seed is not None else -1), **{ p: getattr(self, p) for p in pc_args if getattr(self, p) is not None }) # prior conversion from the hypercube bounds = self.model.prior.bounds( confidence_for_unbounded=self.confidence_for_unbounded) # Check if priors are bounded (nan's to inf) inf = np.where(np.isinf(bounds)) if len(inf[0]): params_names = self.model.parameterization.sampled_params() params = [params_names[i] for i in sorted(list(set(inf[0])))] raise LoggedError( self.log, "PolyChord needs bounded priors, but the parameter(s) '" "', '".join(params) + "' is(are) unbounded.") locs = bounds[:, 0] scales = bounds[:, 1] - bounds[:, 0] # This function re-scales the parameters AND puts them in the right order self.pc_prior = lambda x: (locs + np.array(x)[self.ordering] * scales ).tolist() # We will need the volume of the prior domain, since PolyChord divides by it self.logvolume = np.log(np.prod(scales)) # Prepare callback function if self.callback_function is not None: self.callback_function_callable = (get_external_function( self.callback_function)) self.last_point_callback = 0 # Prepare runtime live and dead points collections self.live = Collection(self.model, None, name="live", initial_size=self.pc_settings.nlive) self.dead = Collection(self.model, self.output, name="dead") self.n_sampled = len(self.model.parameterization.sampled_params()) self.n_derived = len(self.model.parameterization.derived_params()) self.n_priors = len(self.model.prior) self.n_likes = len(self.model.likelihood._likelihoods) # Done! if am_single_or_primary_process(): self.log.info("Calling PolyChord with arguments:") for p, v in inspect.getmembers(self.pc_settings, lambda a: not (callable(a))): if not p.startswith("_"): self.log.info(" %s: %s", p, v)
def set_resuming(self, *args, **kwargs): if is_main_process(): Output.set_resuming(self, *args, **kwargs) if more_than_one_process(): self._resuming = share_mpi( self._resuming if is_main_process() else None)
def initialize(self): if self.method not in evals_attr: raise LoggedError(self.log, "Method '%s' not recognized. Try one of %r.", self.method, list(evals_attr)) self.mpi_info("Initializing") self.max_iter = int(read_dnumber(self.max_evals, self.model.prior.d())) # Configure target method = self.model.loglike if self.ignore_prior else self.model.logpost kwargs = {"make_finite": True} if self.ignore_prior: kwargs["return_derived"] = False self.logp = lambda x: method(x, **kwargs) # Try to load info from previous samples. # If none, sample from reference (make sure that it has finite like/post) self.initial_points = [] assert self.best_of > 0 num_starts = int(np.ceil(self.best_of / mpi.size())) if self.output: files = self.output.find_collections() else: files = None for start in range(num_starts): initial_point = None if files: collection_in: Optional[SampleCollection] if mpi.more_than_one_process() or num_starts > 1: index = 1 + mpi.rank() * num_starts + start if index <= len(files): collection_in = SampleCollection( self.model, self.output, name=str(index), resuming=True) else: collection_in = None else: collection_in = self.output.load_collections(self.model, concatenate=True) if collection_in: initial_point = (collection_in.bestfit() if self.ignore_prior else collection_in.MAP()) initial_point = initial_point[ list(self.model.parameterization.sampled_params())].values self.log.info("Starting %s/%s from %s of previous chain:", start + 1, num_starts, "best fit" if self.ignore_prior else "MAP") # Compute covmat if input but no .covmat file (e.g. with PolyChord) # Prefer old over `covmat` definition in yaml (same as MCMC) self.covmat = collection_in.cov(derived=False) self.covmat_params = list( self.model.parameterization.sampled_params()) if initial_point is None: for _ in range(self.max_iter // 10 + 5): initial_point = self.model.prior.reference(random_state=self._rng) if np.isfinite(self.logp(initial_point)): break else: raise LoggedError(self.log, "Could not find random starting point " "giving finite posterior") self.log.info("Starting %s/%s random initial point:", start + 1, num_starts) self.log.info( dict(zip(self.model.parameterization.sampled_params(), initial_point))) self.initial_points.append(initial_point) self._bounds = self.model.prior.bounds( confidence_for_unbounded=self.confidence_for_unbounded) # TODO: if ignore_prior, one should use *like* covariance (this is *post*) covmat = self._load_covmat(prefer_load_old=self.output)[0] # scale by conditional parameter widths (since not using correlation structure) scales = np.minimum(1 / np.sqrt(np.diag(np.linalg.inv(covmat))), (self._bounds[:, 1] - self._bounds[:, 0]) / 3) # Cov and affine transformation # Transform to space where initial point is at centre, and cov is normalised # Cannot do rotation, as supported minimization routines assume bounds aligned # with the parameter axes. self._affine_transform_matrix = np.diag(1 / scales) self._inv_affine_transform_matrix = np.diag(scales) self._scales = scales self.result = None