Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions autofit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@
from .non_linear.clipper import AbstractClipper
from .non_linear.clipper import ClipperNone
from .non_linear.clipper import ClipperPriorBox
from .non_linear.scaler import AbstractScaler
from .non_linear.scaler import ScalerNone
from .non_linear.scaler import ScalerPriorWidth
from .non_linear.initializer import InitializerBall
from .non_linear.initializer import InitializerPrior
from .non_linear.initializer import InitializerParamBounds
Expand Down
35 changes: 29 additions & 6 deletions autofit/non_linear/clipper.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,30 @@ def bounds_from_model(self, model) -> Tuple[np.ndarray, np.ndarray]:
"""

@abstractmethod
def project(self, vector, model, xp=np):
def project(self, vector, model, xp=np, scale=None):
"""
Project ``vector`` onto the prior support.

Parameters
----------
vector
A physical parameter vector, either a single ``(n_params,)`` vector or
a batched ``(n_starts, n_params)`` array of them. Broadcasting handles
both, so no ``vmap`` is required of the caller.
A parameter vector, either a single ``(n_params,)`` vector or a
batched ``(n_starts, n_params)`` array of them. Broadcasting handles
both, so no ``vmap`` is required of the caller. Physical unless
``scale`` is given, in which case it is in scaled coordinates.
model
The model whose priors define the support.
xp
The array module, ``numpy`` or ``jax.numpy``.
scale
The per-parameter step scale from a
:mod:`~autofit.non_linear.scaler`, when the caller is stepping in
``phi = theta / scale`` rather than in physical parameters. The
**bounds** are divided by it, so the projection happens in the
caller's own coordinates and no round-trip through physical space is
needed. Scales are strictly positive, so dividing preserves the
ordering of each ``(lower, upper)`` pair and ``+/-inf`` stay
``+/-inf``.

Returns
-------
Expand All @@ -134,7 +144,7 @@ def bounds_from_model(self, model) -> Tuple[np.ndarray, np.ndarray]:
n = model.prior_count
return np.full(n, -np.inf), np.full(n, np.inf)

def project(self, vector, model, xp=np):
def project(self, vector, model, xp=np, scale=None):
return vector, xp.zeros_like(vector, dtype=bool)


Expand Down Expand Up @@ -264,9 +274,22 @@ def bounds_from_model(self, model) -> Tuple[np.ndarray, np.ndarray]:

return lower_inset, upper_inset

def project(self, vector, model, xp=np):
def project(self, vector, model, xp=np, scale=None):
lower, upper = self.bounds_from_model(model)

if scale is not None:
# Divided AFTER the insets are applied, not before. The inset is
# relative to the box width, so scaling the raw limits first and
# insetting afterwards gives the identical box -- but the half-open
# `strict_epsilon` is ABSOLUTE, and dividing it by the scale would
# shrink the nudge for a large-scale coordinate until it no longer
# lands strictly inside a support that excludes its limit. Insetting
# in physical space and then mapping the finished bounds keeps the
# inset meaning what it says in the space the prior is declared in.
scale = np.asarray(scale, dtype=float)
lower = lower / scale
upper = upper / scale

dtype = getattr(vector, "dtype", None)
lower = xp.asarray(lower, dtype=dtype)
upper = xp.asarray(upper, dtype=dtype)
Expand Down
25 changes: 24 additions & 1 deletion autofit/non_linear/paths/directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,9 +483,32 @@ def _save_metadata(self, search_name):
def _save_model_info(self, model):
"""
Save the model.info file, which summarises every parameter and prior.

A search that applies a per-parameter step scale
(:mod:`autofit.non_linear.scaler`) appends its scale vector to the bottom
of the same file. It goes in the FILE rather than into ``model.info`` the
property because the scale is a property of the *search*, which the model
knows nothing about — a search-dependent block cannot be rendered by a
model-only property, and splitting it into a second file would leave the
canonical "what is this model" artefact silently omitting the fact that
the search does not step in the units the file lists.

Guarded the same way ``_save_model_start_point``'s source is: a search
with no scaler, an older pickled search, or a scaler that declines to
describe itself all fall through to writing ``model.info`` alone.
"""
info = model.info

try:
scaler_info = self.search.scaler.info_from_model(model=model)
except (NotImplementedError, AttributeError):
scaler_info = ""

if scaler_info:
info += f"\n\n{scaler_info}"

with open_(self.output_path / "model.info", "w+") as f:
f.write(model.info)
f.write(info)

if should_output("model_graph") and hasattr(model, "graph_info"):
with open_(self.output_path / "model.graph", "w+") as f:
Expand Down
Loading
Loading