class minimize(Sampler):
    def initialize(self):
        """Prepares the arguments for `scipy.minimize`."""
        if not get_mpi_rank():
            self.log.info("Initializing")
        self.logp = ((lambda x: self.model.logposterior(x, make_finite=True)[0])
                     if not self.ignore_prior else
                     (lambda x: sum(self.model.loglikes(x, return_derived=True)[0])))
        # Initial point: sample from reference and make sure that it has finite lik/post
        this_logp = -np.inf
        while not np.isfinite(this_logp):
            initial_point = self.model.prior.reference()
            this_logp = self.logp(initial_point)
        self.kwargs = {
            "fun": (lambda x: -self.logp(x)),
            "x0": initial_point,
            "bounds": self.model.prior.bounds(confidence_for_unbounded=0.999),
            "tol": self.tol,
            "options": {
                "maxiter": self.maxiter,
                "disp": (self.log.getEffectiveLevel() == logging.DEBUG)}}
        self.kwargs.update(self.override or {})
        self.log.debug("Arguments for scipy.optimize.minimize:\n%r", self.kwargs)

    def run(self):
        """
        Runs `scipy.minimize`
        """
        self.log.info("Starting minimization.")
        self.result = scpminimize(**self.kwargs)
        if self.result.success:
            self.log.info("Finished succesfully.")
        else:
            self.log.error("Finished Unsuccesfully.")

    def close(self, *args):
        """
        Determines success (or not), chooses best (if MPI)
        and produces output (if requested).
        """
        # If something failed
        if not hasattr(self, "result"):
            return
        if get_mpi_size():
            results = get_mpi_comm().gather(self.result, root=0)
            if not get_mpi_rank():
                self.result = results[np.argmin([r.fun for r in results])]
        if not get_mpi_rank():
            if not self.result.success:
                self.log.error("Maximization failed! Here is the `scipy` raw result:\n%r",
                               self.result)
                raise HandledException
            self.log.info("log%s maximized at %g",
                          "likelihood" if self.ignore_prior else "posterior",
                          -self.result.fun)
            post = self.model.logposterior(self.result.x)
            recomputed_max = sum(post.loglikes) if self.ignore_prior else post.logpost
            if not np.allclose(-self.result.fun, recomputed_max):
                self.log.error("Cannot reproduce result. Something bad happened. "
                               "Recomputed max: %g at %r", recomputed_max, self.result.x)
                raise HandledException
            self.maximum = OnePoint(
                self.model, self.output, name="maximum",
                extension=("likelihood" if self.ignore_prior else "posterior"))
            self.maximum.add(self.result.x, derived=post.derived, logpost=post.logpost,
                             logpriors=post.logpriors, loglikes=post.loglikes)
            self.log.info("Parameter values at maximum:\n%s"%self.maximum.data.to_string())
            self.maximum._out_update()

    def products(self):
        """
        Auxiliary function to define what should be returned in a scripted call.

        Returns:
           The :class:`OnePoint` that maximizes the posterior or likelihood (depending on
           ``ignore_prior``), and the `scipy.optimize.OptimizeResult
           <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.OptimizeResult.html>`_
           instance.
        """
        if not get_mpi_rank():
            return {"maximum": self.maximum, "OptimizeResult": self.result}
Exemple #2
0
class minimize(Sampler):
    def initialize(self):
        """Prepares the arguments for `scipy.minimize`."""
        if am_single_or_primary_process():
            self.log.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.update({"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
        covmat = None
        if self.output:
            collection_in = self.output.load_collections(self.model,
                                                         skip=0,
                                                         thin=1,
                                                         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")
                # TODO: if ignore_prior, one should use *like* covariance (this is *post*)
                covmat = collection_in.cov()
        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)))
        # Cov and affine transformation
        self._affine_transform_matrix = None
        self._inv_affine_transform_matrix = None
        self._affine_transform_baseline = None
        if covmat is None:
            # Use as much info as we have from ref & prior
            covmat = self.model.prior.reference_covmat()
        # Transform to space where initial point is at centre, and cov is normalised
        sigmas_diag, L = choleskyL(covmat, return_scale_free=True)
        self._affine_transform_matrix = np.linalg.inv(sigmas_diag)
        self._inv_affine_transform_matrix = sigmas_diag
        self._affine_transform_baseline = initial_point
        self.affine_transform = lambda x: (self._affine_transform_matrix.dot(
            x - self._affine_transform_baseline))
        self.inv_affine_transform = lambda x: (
            self._inv_affine_transform_matrix.dot(
                x) + self._affine_transform_baseline)
        bounds = self.model.prior.bounds(
            confidence_for_unbounded=self.confidence_for_unbounded)
        # Re-scale
        self.logp_transf = lambda x: self.logp(self.inv_affine_transform(x))
        initial_point = self.affine_transform(initial_point)
        bounds = np.array(
            [self.affine_transform(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 run(self):
        """
        Runs `scipy.minimize`
        """
        self.log.info("Starting minimization.")
        try:
            self.result = self.minimizer(**self.kwargs)
        except:
            self.log.error("Minimizer '%s' raised an unexpected error:",
                           self.method)
            raise
        self.success = (self.result.success if self.method.lower() == "scipy"
                        else self.result.flag == self.result.EXIT_SUCCESS)
        if self.success:
            self.log.info("Finished successfully!")
        else:
            if self.method.lower() == "bobyqa":
                reason = {
                    self.result.EXIT_MAXFUN_WARNING:
                    "Maximum allowed objective evaluations reached. "
                    "This is the most likely return value when using multiple restarts.",
                    self.result.EXIT_SLOW_WARNING:
                    "Maximum number of slow iterations reached.",
                    self.result.EXIT_FALSE_SUCCESS_WARNING:
                    "Py-BOBYQA reached the maximum number of restarts which decreased the"
                    " objective, but to a worse value than was found in a previous run.",
                    self.result.EXIT_INPUT_ERROR:
                    "Error in the inputs.",
                    self.result.EXIT_TR_INCREASE_ERROR:
                    "Error occurred when solving the trust region subproblem.",
                    self.result.EXIT_LINALG_ERROR:
                    "Linear algebra error, e.g. the interpolation points produced a "
                    "singular linear system."
                }[self.result.flag]
            else:
                reason = ""
            self.log.error("Finished unsuccessfully." +
                           (" Reason: " + reason if reason else ""))

    def close(self, *args):
        """
        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 get_mpi_size():
            results = get_mpi_comm().gather(self.result, root=0)
            _inv_affine_transform_matrices = get_mpi_comm().gather(
                self._inv_affine_transform_matrix, root=0)
            _affine_transform_baselines = get_mpi_comm().gather(
                self._affine_transform_baseline, root=0)
            if am_single_or_primary_process():
                i_min = np.argmin([getattr(r, evals_attr_) for r in results])
                self.result = results[i_min]
                self._inv_affine_transform_matrix = _inv_affine_transform_matrices[
                    i_min]
                self._affine_transform_baseline = _affine_transform_baselines[
                    i_min]
        if am_single_or_primary_process():
            if not self.success:
                raise LoggedError(
                    self.log,
                    "Minimization failed! Here is the raw result object:\n%s",
                    str(self.result))
            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):
                raise LoggedError(
                    self.log,
                    "Cannot reproduce result. Maybe yout likelihood is stochastic? "
                    "Recomputed min: %g (was %g) at %r", recomputed_logp_min,
                    logp_min, x_min)
            self.minimum = OnePoint(
                self.model,
                self.output,
                name="",
                extension=("bestfit.txt"
                           if self.ignore_prior else "minimum.txt"))
            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 products(self):
        r"""
        Returns a dictionary containing:

        - ``minimum``: :class:`OnePoint` that maximizes the posterior or likelihood
          (depending on ``ignore_prior``).

        - ``result_object``: instance of results class of
          `scipy <https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.OptimizeResult.html>`_
          or `pyBOBYQA
          <https://numericalalgorithmsgroup.github.io/pybobyqa/build/html/userguide.html>`_.

        - ``M``: inverse of the affine transform matrix (see below).
          ``None`` if no transformation applied.

        - ``X0``: offset of the affine transform matrix (see below)
          ``None`` if no transformation applied.

        If non-trivial ``M`` and ``X0`` are returned, this means that the minimizer has
        been working on an affine-transformed parameter space :math:`x^\prime`, from which
        the real space points can be obtained as :math:`x = M x^\prime + X_0`. This inverse
        transformation needs to be applied to the coordinates appearing inside the
        ``result_object``.
        """
        if am_single_or_primary_process():
            return {
                "minimum": self.minimum,
                "result_object": self.result,
                "M": self._inv_affine_transform_matrix,
                "X0": self._affine_transform_baseline
            }

    def getdist_point_text(self, params, weight=None, minuslogpost=None):
        lines = []
        if weight is not None:
            lines.append('  weight    = %s' % weight)
        if minuslogpost is not None:
            lines.append(' -log(Like) = %s' % minuslogpost)
            lines.append('  chi-sq    = %s' % (2 * minuslogpost))
        lines.append('')
        labels = self.model.parameterization.labels()
        label_list = list(labels.keys())
        if hasattr(params, 'chi2_names'): label_list += params.chi2_names
        width = max([len(lab) for lab in label_list]) + 2

        def add_section(pars):
            for p, val in pars:
                lab = labels.get(p, p)
                num = label_list.index(p) + 1
                if isinstance(val,
                              (float, np.floating)) and len(str(val)) > 10:
                    lines.append("%5d  %-17.9e %-*s %s" %
                                 (num, val, width, p, lab))
                else:
                    lines.append("%5d  %-17s %-*s %s" %
                                 (num, val, width, p, lab))

        num_sampled = len(self.model.parameterization.sampled_params())
        num_derived = len(self.model.parameterization.derived_params())
        add_section([[p, params[p]]
                     for p in self.model.parameterization.sampled_params()])
        lines.append('')
        add_section([[p, value] for p, value in
                     self.model.parameterization.constant_params().items()])
        lines.append('')
        add_section([[p, params[p]]
                     for p in self.model.parameterization.derived_params()])
        if hasattr(params, 'chi2_names'):
            from cobaya.conventions import _chi2, _separator
            labels.update(
                odict([[
                    p,
                    r'\chi^2_{\rm %s}' %
                    (p.replace(_chi2 + _separator, '').replace("_", r"\ "))
                ] for p in params.chi2_names]))
            add_section([[chi2, params[chi2]] for chi2 in params.chi2_names])
        return "\n".join(lines)

    def dump_getdist(self):
        if not self.output:
            return
        getdist_bf = self.getdist_point_text(
            self.minimum, minuslogpost=self.minimum['minuslogpost'])
        out_filename = os.path.join(
            self.output.folder,
            self.output.prefix + getdist_ext_ignore_prior[self.ignore_prior])
        with open(out_filename, 'w') as f:
            f.write(getdist_bf)