Source code for pybnf.algorithms.optimizers.local_base
"""Shared scaffolding for the start-point local optimizers (Powell + CMA-ES, #403).
Both Powell (conjugate-direction) and CMA-ES are derivative-free, black-box local
optimizers that begin from a single point and search in *sampling space* ``u`` --
``log10`` for log-scaled parameters, linear otherwise. That is the same space the
prior and proposal arithmetic already operate in (``FreeParameter._scale``,
ADR-0003/0010), so log parameters are optimized geometrically (a multiplicative
step is an additive ``u`` step) exactly as Simplex does its log-space arithmetic.
``StartPointOptimizer`` factors out the two pieces of plumbing they share:
* **start-point resolution** -- the injected refiner start point (set by
``pybnf._refine_best_fit`` under :attr:`START_POINT_KEY`) when refining; the
single-value ``var`` / ``logvar`` specs of a standalone point-start fit (the
same start point Simplex parses, ADR-0015); or, for a ``start_from_box``
optimizer (CMA-ES) given bounded ``uniform_var`` / ``loguniform_var`` priors,
the **box center** in sampling space ``u`` -- its global-start mode (#404,
ADR-0017);
* the ``u`` <-> :class:`PSet` conversion, which maps each coordinate back to a
stored value and reflects it into the box via :meth:`FreeParameter.set_value`
(a no-op for the unbounded ``var`` / ``logvar`` of a point-start fit; active when
refining or globally searching a bounded fit's parameters).
Simplex predates this and keeps its own byte-identical start-point parsing; the
two new optimizers are the ≥2-member event (ADR-0009) that earns the shared base.
These methods plug into the run loop through ``start_run`` / ``got_result`` only
(ADR-0007); no method overrides ``run()``.
"""
from ..base import Algorithm
from ...pset import PSet
import numpy as np
[docs]
class StartPointOptimizer(Algorithm):
"""Base for the start-point local optimizers. Subclasses implement
``start_run`` / ``got_result`` and set :attr:`START_POINT_KEY`."""
#: The internal config key the refiner start point is injected under
#: (mirrors ``SimplexAlgorithm``'s ``'simplex_start_point'``). Set by each
#: subclass; ``pybnf._refine_best_fit`` writes the best fit here so refinement
#: starts from it instead of parsing the (refiner-irrelevant) variable specs.
START_POINT_KEY = None
def _resolve_start_pset(self):
"""The PSet the search starts from.
Three sources, in priority order:
* the injected refiner start point, if present (refinement);
* the **box center**, for a ``start_from_box`` optimizer given bounded
priors -- the 0.5 quantile of each ``uniform_var`` / ``loguniform_var``,
i.e. the midpoint of the box in sampling space ``u`` (#404, ADR-0017);
* else the single ``var`` / ``logvar`` / ``lnvar`` start point Simplex uses (a
single value per parameter; a log variable carries ``p1`` in its sampling
space, so ``from_sampling_space`` maps it back to a stored value -- ``10**p1``
for ``logvar``, ``exp(p1)`` for ``lnvar``, identity for ``var``).
"""
if self.START_POINT_KEY in self.config.config:
return self.config.config[self.START_POINT_KEY]
if self._is_box_start():
# Global-start mode: begin from the box center. Only start_from_box
# fit_types reach here with bounded priors -- config._load_variables
# rejects them for the point-only start optimizers (Simplex/Powell).
return PSet([v.value_from_quantile(0.5) for v in self.variables])
start_vars = [v.set_value(v.from_sampling_space(v.p1)) for v in self.variables]
return PSet(start_vars)
def _is_box_start(self):
"""True when this is a standalone fit over a bounded-prior box (the
global-start mode), rather than a point start or an injected refiner start.
It holds when no refiner start point was injected and every variable has a
bounded-support prior (``uniform_var`` / ``loguniform_var``). Whether the
fit_type is *allowed* to be here at all is enforced upstream by the
``start_from_box`` registry flag in ``config._load_variables`` (#404)."""
return (self.START_POINT_KEY not in self.config.config
and bool(self.variables)
and all(v.has_bounded_support for v in self.variables))
def _box_widths_u(self):
"""Per-coordinate box widths in sampling space ``u`` (only meaningful in
box-start mode), ordered by ``self.variables``: ``log10(p2) - log10(p1)``
for a log parameter, ``p2 - p1`` otherwise. Derived from the prior bounds
``p1`` / ``p2`` so it is independent of the reflecting-bound (``b`` / ``u``)
flag -- the same box the center is taken from."""
return np.array(
[v.to_sampling_space(v.p2) - v.to_sampling_space(v.p1)
for v in self.variables], dtype=float)
def _u_from_pset(self, pset):
"""The parameter vector of ``pset`` in sampling space ``u`` (the inverse
of :meth:`Algorithm._pset_from_u`). Delegates to the shared PSet→u bridge
:meth:`Algorithm._param_vec`; kept as a named alias because it pairs with
``_pset_from_u`` in this module's ``u`` <-> PSet vocabulary. The inverse
bridge ``_pset_from_u`` itself now lives on ``Algorithm``, next to
``_param_vec``, so the u-vector↔PSet conversion is centralized (#412)."""
return self._param_vec(pset)