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
43 changes: 37 additions & 6 deletions autofit/non_linear/plot/mle_plotters.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,29 @@
from autofit.non_linear.plot.plot_util import skip_in_test_mode, output_figure
from autofit.non_linear.plot.plot_util import (
skip_in_test_mode,
output_figure,
checked_kwargs,
)


def _line_kwargs(kwargs, defaults):
"""
Merge caller ``kwargs`` into the trace-line defaults these plots draw with.

The traces are plain matplotlib lines, so a caller's ``**kwargs`` are
``Line2D`` properties. Validating against the artist's own setters (aliases
included) means a mistyped property is named here rather than surfacing as a
``Line2D.set()`` error from deep inside matplotlib.
"""
from matplotlib.artist import ArtistInspector
from matplotlib.lines import Line2D

inspector = ArtistInspector(Line2D)
accepts = set(inspector.get_setters())
accepts |= {alias for aliases in inspector.aliasd.values() for alias in aliases}

settings = dict(defaults)
settings.update(checked_kwargs(kwargs, accepts=accepts, target="matplotlib"))
return settings


@skip_in_test_mode
Expand All @@ -13,6 +38,8 @@ def subplot_parameters(
):
import matplotlib.pyplot as plt

line_kwargs = _line_kwargs(kwargs, {"c": "k"})

model = samples.model
parameter_lists = samples.parameters_extract

Expand All @@ -28,9 +55,9 @@ def subplot_parameters(
parameters = parameters[int(len(parameters) / 2) :]

if use_log_y:
plt.semilogy(iteration_list, parameters, c="k")
plt.semilogy(iteration_list, parameters, **line_kwargs)
else:
plt.plot(iteration_list, parameters, c="k")
plt.plot(iteration_list, parameters, **line_kwargs)

plt.xlabel("Iteration", fontsize=16)
plt.ylabel(model.parameter_labels_with_superscripts_latex[i], fontsize=16)
Expand Down Expand Up @@ -58,6 +85,8 @@ def log_likelihood_vs_iteration(
):
import matplotlib.pyplot as plt

line_kwargs = _line_kwargs(kwargs, {"c": "k"})

log_likelihood_list = samples.log_likelihood_list
iteration_list = range(len(log_likelihood_list))

Expand All @@ -68,9 +97,9 @@ def log_likelihood_vs_iteration(
plt.figure(figsize=(12, 12))

if use_log_y:
plt.semilogy(iteration_list, log_likelihood_list, c="k")
plt.semilogy(iteration_list, log_likelihood_list, **line_kwargs)
else:
plt.plot(iteration_list, log_likelihood_list, c="k")
plt.plot(iteration_list, log_likelihood_list, **line_kwargs)

plt.xlabel("Iteration", fontsize=16)
plt.ylabel("Log Likelihood", fontsize=16)
Expand Down Expand Up @@ -118,10 +147,12 @@ def figure_of_merit_vs_iteration(

import matplotlib.pyplot as plt

line_kwargs = _line_kwargs(kwargs, {"c": "k"})

iteration_list = range(len(fom_history))

plt.figure(figsize=(12, 12))
plt.plot(iteration_list, fom_history, c="k")
plt.plot(iteration_list, fom_history, **line_kwargs)

plt.xlabel("Step", fontsize=16)
plt.ylabel("Global-Best Figure of Merit (-2 ln posterior)", fontsize=16)
Expand Down
40 changes: 36 additions & 4 deletions autofit/non_linear/plot/nest_plotters.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
skip_in_test_mode,
log_plot_exception,
output_figure,
accepted_kwarg_names,
checked_kwargs,
)


Expand Down Expand Up @@ -43,19 +45,49 @@ def corner_anesthetic(samples, path=None, filename="corner_anesthetic", format="
if SettingWithCopyWarning is not None:
warnings.filterwarnings("ignore", category=SettingWithCopyWarning)

# ``kwargs`` splits by destination: what shapes the figure goes to
# ``make_2d_axes``, everything else styles the plot via ``plot_2d``, which
# forwards what it does not name straight to matplotlib — matplotlib raises
# on an unknown property, so unlike ``corner`` there is no silent sink here
# to guard against and the only check needed is that the caller is not
# overriding the sample array.
#
# ``figsize`` / ``facecolor`` / ``dpi`` are named explicitly because
# ``make_2d_axes`` takes them through its own ``**fig_kw`` rather than
# declaring them — including the two this function computes below, which a
# caller must be able to override.
axes_names = (accepted_kwarg_names(make_2d_axes) - {"params"}) | {
"figsize",
"facecolor",
"dpi",
}
kwargs = checked_kwargs(
kwargs,
reserved=("data", "weights", "columns"),
target="anesthetic",
)

axes_settings = dict(figsize=figsize, facecolor=config_dict["facecolor"])
axes_settings.update(
{key: value for key, value in kwargs.items() if key in axes_names}
)

plot_settings = dict(alpha=config_dict["alpha"], label="posterior")
plot_settings.update(
{key: value for key, value in kwargs.items() if key not in axes_names}
)

fig, axes = make_2d_axes(
model.parameter_labels_with_superscripts_latex,
figsize=figsize,
facecolor=config_dict["facecolor"],
**axes_settings,
)

if SettingWithCopyWarning is not None:
warnings.filterwarnings("default", category=SettingWithCopyWarning)

nested_samples.plot_2d(
axes,
alpha=config_dict["alpha"],
label="posterior",
**plot_settings,
)
axes.iloc[-1, 0].legend(
bbox_to_anchor=(len(axes) / 2, len(axes)),
Expand Down
98 changes: 98 additions & 0 deletions autofit/non_linear/plot/plot_util.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import difflib
import inspect
import logging
import os
from functools import wraps
Expand All @@ -10,6 +12,98 @@
logger = logging.getLogger(__name__)


class PlotKwargsError(TypeError):
"""
Raised when a plot function is handed a ``**kwargs`` entry its underlying
plotting library cannot honour.

A ``TypeError`` subclass so it reads like the error Python raises for an
unexpected keyword argument, while staying distinguishable from the
``TypeError``s ``log_plot_exception`` swallows.
"""


def accepted_kwarg_names(*funcs):
"""
The keyword-argument names ``funcs`` genuinely honour.

Only *named* parameters count. A function's own ``**kwargs`` is deliberately
not read as "accepts anything": ``corner.corner`` funnels everything it does
not name into ``corner.core.hist2d``, whose body reads only ``extent`` and
silently discards the rest, so treating that sink as permissive would leave
the very failure this guard exists to catch.
"""
names = set()
for func in funcs:
for name, parameter in inspect.signature(func).parameters.items():
if parameter.kind in (
inspect.Parameter.VAR_POSITIONAL,
inspect.Parameter.VAR_KEYWORD,
):
continue
names.add(name)
return names


def checked_kwargs(kwargs, *, accepts=None, reserved=(), target):
"""
Validate caller ``kwargs`` against what ``target`` accepts, before forwarding.

The plot functions pass their ``**kwargs`` on to ``corner`` / ``anesthetic``
/ ``matplotlib``. Anything those libraries would drop on the floor is raised
here instead, so a mis-typed or wrong-library argument fails loudly rather
than producing a figure that quietly ignores it.

Parameters
----------
kwargs
The caller's keyword arguments.
accepts
Names ``target`` honours — see ``accepted_kwarg_names``. ``None`` when
``target`` has a genuine open pass-through that raises on what it cannot
use (``anesthetic`` hands unknown names to matplotlib, which rejects
them), so only ``reserved`` is enforced.
reserved
Names this wrapper sets itself and the caller may not override (e.g. the
sample array ``corner`` is being asked to plot).
target
Human-readable name of the receiving function, for the error message.

Raises
------
PlotKwargsError
If any name is reserved or is not accepted by ``target``. Unknown names
get a "did you mean" hint where a close match exists.
"""
reserved = set(reserved)

overridden = sorted(name for name in kwargs if name in reserved)
if overridden:
raise PlotKwargsError(
f"{', '.join(overridden)} is set by PyAutoFit and cannot be "
f"overridden when forwarding to {target}."
if len(overridden) == 1
else f"{', '.join(overridden)} are set by PyAutoFit and cannot be "
f"overridden when forwarding to {target}."
)

if accepts is None:
return dict(kwargs)

unknown = sorted(name for name in kwargs if name not in accepts)
if unknown:
hints = []
for name in unknown:
close = difflib.get_close_matches(name, sorted(accepts), n=1)
hints.append(f"{name!r}" + (f" (did you mean {close[0]!r}?)" if close else ""))
raise PlotKwargsError(
f"{target} does not accept {', '.join(hints)}. These would be "
f"silently ignored, so they are rejected instead."
)

return dict(kwargs)


def skip_in_test_mode(func):
@wraps(func)
def wrapper(*args, **kwargs):
Expand All @@ -25,6 +119,10 @@ def log_plot_exception(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except PlotKwargsError:
# A rejected kwarg is the caller's mistake, not an unconverged
# posterior — never downgrade it to the info log below.
raise
except (
ValueError,
KeyError,
Expand Down
29 changes: 25 additions & 4 deletions autofit/non_linear/plot/samples_plotters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@

from autonerves import conf

from autofit.non_linear.plot.plot_util import skip_in_test_mode, output_figure
from autofit.non_linear.plot.plot_util import (
skip_in_test_mode,
output_figure,
accepted_kwarg_names,
checked_kwargs,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -50,12 +55,28 @@ def corner_cornerpy(samples, path=None, filename="corner", format="show", **kwar
pylab.rcParams.update(params)

import corner
from corner.core import hist2d as corner_hist2d

corner.corner(
data=data,
weight_list=samples.weight_list,
settings = dict(
weights=samples.weight_list,
labels=samples.model.parameter_labels_with_superscripts_latex,
range=_corner_range_from(data),
)

# ``hist2d``'s named parameters are a documented corner pass-through, so they
# stay valid; ``x`` / ``y`` are its positional sample arrays, not options.
accepts = accepted_kwarg_names(corner.corner, corner_hist2d) - {"x", "y"}

for key, value in checked_kwargs(
kwargs, accepts=accepts, reserved=("data",), target="corner.corner"
).items():
# ``None`` against a value PyAutoFit computes means "use the default" —
# notably for ``range``, where blanking it would hand ``corner`` back the
# degenerate columns ``_corner_range_from`` exists to widen.
if value is None and key in settings:
continue
settings[key] = value

corner.corner(data=data, **settings)

output_figure(path=path, filename=filename, format=format)
2 changes: 1 addition & 1 deletion autofit/plot/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from autofit.non_linear.plot.samples_plotters import corner_cornerpy
from autofit.non_linear.plot.nest_plotters import corner_anesthetic
from autofit.non_linear.plot.mle_plotters import subplot_parameters, log_likelihood_vs_iteration
from autofit.non_linear.plot.plot_util import output_figure
from autofit.non_linear.plot.plot_util import output_figure, PlotKwargsError
12 changes: 12 additions & 0 deletions docs/api/plot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,18 @@ The plotting API is **functional**: import ``autofit.plot`` and call a plot func

aplt.corner_cornerpy(samples=result.samples)

Every plot function forwards its ``**kwargs`` to the library it wraps (``corner.py``, ``anesthetic``,
``matplotlib``), so the wrapped library's own options are available directly:

.. code-block:: python

aplt.corner_cornerpy(samples=result.samples, bins=5, show_titles=True)

Values **PyAutoFit** computes — the parameter labels, the sample weights, and the plot range that keeps
``corner.py`` from raising "no dynamic range" on a converged parameter — act as defaults a keyword argument
overrides. A name the wrapped library does not accept raises a ``TypeError`` naming it, rather than being
silently ignored.

**Examples / Tutorials:**

- `readthedocs: non-linear search example <https://pyautofit.readthedocs.io/en/latest/cookbooks/search.html>`_
Expand Down
4 changes: 3 additions & 1 deletion docs/cookbooks/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ search = af.Emcee(number_of_cores=4)
The results of a model-fit are plotted and inspected via the plot functions of `autofit.plot`, which wrap the
in-built visualization libraries of the searches. These are fully described in the `plot` package of the workspace.

For example, `corner_cornerpy` wraps `corner.py` and is used as follows.
For example, `corner_cornerpy` wraps `corner.py` and is used as follows. Any keyword argument `corner.py`
accepts can be passed straight through — they are forwarded to `corner.corner`, and a name `corner.py` does not
recognise raises a `TypeError` naming it rather than being silently ignored.

Checkout the `plot` package for a complete description of the plots that can be made for a given search.

Expand Down
Loading
Loading