from ._global_imports import *
from ._backends import NestedBackend
from ._runs import Runs, ParameterError
from ._run import Run # for Sphinx docstring cross-references
class Params(object):
""" Information about parameters shared by runs for plotting. """
def __init__(self, names):
self._names = names
@property
def names(self):
return self._names
@property
def labels(self):
return self._labels
def __len__(self):
return len(self._names)
[docs]class PostProcessor(object):
""" Post-process samples for inference and posterior checking.
Base class that functions as a container for nested sampling run objects
representing some set of posterior distributions that share some subset of
parameters of interest that are to be plotted. The parameters can be
shared in the sense that their relationship is the identity relation, or
multiple physical objects in a population context each have an instance
of a parameter type. An example of the former is the mass of a star, for
which there are two or more distinct posterior distributions, due, e.g.:
* to sequential inference conditional on independent data sets, where
each posterior is an updated of the previous in the sequence;
* inference conditional on independent data sets, where the different
analyses invoked same [different] prior models and one simply wants
to overlay the posteriors e.g., to gauge the synergy of different
experiments [and argue the priors are weakly informative/
diffuse in the context of the likelihood functions];
* to analysis of a single data set but the effective existence of
discrete hyperparameter resulting in posteriors conditional on
the discrete label over population-level prior distributions that
has not been marginalized over (i.e., the problem of choosing an
appropriate the hierarchical prior model);
* to competing models, where models can differ in the likelihood
function, prior density, or both.
An example of the latter is two more (neutron) stars, each with a mass;
these masses collectively encode interesting population-level information.
Subclasses will calculate inferences, usually in the form of approximate
integrals over the posterior distribution. They will also provide tools
for visualisation of posterior information and derived quantities, which
is useful for basic posterior predictive checking.
:param iterable posteriors:
An iterable of instances of :class:`~.Runs`.
"""
def __init__(self, posteriors):
self.posteriors = posteriors
self.val_cred=[]
self.samples={}
@property
def posteriors(self):
""" Get the posteriors. """
return self._posteriors
@posteriors.setter
def posteriors(self, obj):
""" Set the posteriors attribute. """
if isinstance(obj, (list, tuple)):
if not obj:
raise TypeError('No objects of type ``Runs`` supplied.')
IDs = []
for o in obj:
if not isinstance(o, Runs):
raise TypeError('Objects must be instances of the '
'``Runs`` class.')
if o.ID not in IDs:
IDs.append(o.ID)
else:
raise AmbiguityError('Use distinct IDs for distinct '
'posteriors (run sets).')
self._posteriors = obj
elif isinstance(obj, Runs):
self._posteriors = [obj]
else:
raise TypeError('Supply an instance or instances of the '
'``Runs`` class.')
[docs] @make_verbose('Curating set of runs for posterior plotting',
'Run set curated')
def set_subset(self, IDs=None,
combine=False, combine_all=False,
force_combine=True, only_combined=False,
overwrite=False):
""" Set a current list of :class:`~.Runs` instances.
Helper function to get and notify which runs will be plotted.
:param OrderedDict IDs:
A dictionary of lists, where keys match :class:`~.Runs` instance
IDs, and list elements match :class:`~.Run` instance IDs. If no
key matches a :class:`~.Runs` instance ID, it is assumed that
all associated :class:`~.Run` instances are to be selected as the
current subset for plotting applications.
:param bool combine:
Additionally combine the runs into a single run for overplotting?
The overriding setting if there is more than one underlying
posterior to be plotted, is to attempt to combine runs on each
posterior if multiple such runs are available, in order to
reduce information density. If there is a single underlying
posterior, the user-specified value is respected.
:param bool combine_all:
Combine all runs in each :class:`Runs` instance or only those
for which IDs are provided? Ignored if ``combine`` is ``False``.
:param bool force_combine:
Force recombination of elligible run sets, even if a
combined run is already cached?
:param bool only_combined:
Only plot the combined run? Only heeded if a single posterior
is selected for plotting, and in that case is ignored if
``combine`` is ``False``.
:param bool overwrite:
Overwrite combined-sample files on disk with the same filename?
"""
if IDs is None:
self._subset = self._posteriors
IDs = {}
else:
if isinstance(IDs, OrderedDict):
self._subset = [self[ID] for ID in IDs]
else:
raise TypeError('IDs must be supplied in a '
'collections.OrderedDict container.')
if len(self._subset) > 1:
if len(self._subset) > 9:
print('Warning: Only the first nine positional posteriors '
'will be plotted, with IDs to prevent the plot from overcrowding'
+ ', '.join(str(p.ID) for p in self._subset[:9]))
for posterior in self._subset[:9]:
posterior.set_subset(IDs.get(posterior.ID, None),
combine = True,
combine_all = combine_all,
force_combine = True,
only_combined = True,
only_principal = True,
overwrite = overwrite)
else:
posterior = self._subset[0]
ids = IDs.get(posterior.ID, None)
if only_combined:
pass
elif len(ids) > 9 and not combine:
print('Warning: Only the first nine positional runs '
'will be plotted individually, with IDs to prevent the plot from overcrowding'
+ ', '.join(ids[:9]))
ids = ids[:9]
elif len(ids) == 9 and combine:
print('Warning: Only the first eight positional runs will be '
'plotted individually, with IDs to prevent the plot from overcrowding '
+ ', '.join(ids[:8]))
ids = ids[:8]
posterior.set_subset(ids,
combine = combine,
combine_all = combine_all,
force_combine = force_combine,
only_combined = only_combined,
only_principal = False,
overwrite = overwrite)
@property
def subset(self):
""" Get the current subset of posteriors for plotting. """
return self._subset
@property
def subset_to_plot(self):
""" Get the current subset of runs for plotting. """
if len(self._subset) > 1:
runs = []
for posterior in self._subset:
runs += posterior.subset_to_plot
# reorder so nestcheck-compatible last
reordered_runs = []
temp_runs = []
reordered_posteriors = []
temp_posteriors = []
for i, run in enumerate(runs):
if run.use_nestcheck:
reordered_runs.append(run)
reordered_posteriors.append(self._subset[i])
else:
temp_runs.append(run)
temp_posteriors.append(self._subset[i])
reordered_runs += temp_runs
reordered_posteriors += temp_posteriors
self._subset = reordered_posteriors
else:
posterior = self._subset[0]
runs = posterior.subset_to_plot
# reorder so nestcheck-compatible first
reordered_runs = []
temp_runs = []
for run in runs:
if run.use_nestcheck:
reordered_runs.append(run)
else:
temp_runs.append(run)
reordered_runs += temp_runs
return reordered_runs
def __getitem__(self, ID):
""" Get a :class:`~.Runs` instance using the associated ID. """
def search(ID):
if isinstance(ID, _six.string_types):
for posterior in self._posteriors:
if ID == posterior.ID:
return posterior
raise KeyError('No posterior with ID matching key.')
if isinstance(ID, _six.string_types):
return search(ID)
elif isinstance(ID, tuple):
if len(ID) == 2:
return search(ID[0])[ID[1]]
else:
raise TypeError('Invalid run ID specification.')
@property
def params(self):
""" Get the current parameter information. """
return self._params
[docs] def set_params(self, names):
""" Set current parameters for plotting, which must be shared. """
self._params = Params(names)
for name in names:
for posterior in self._subset:
if name not in posterior.names:
self._params = None
raise ParameterError('No parameter name matching %s in run '
'with ID %s.' % (name, posterior.ID))
try:
self._check_params('labels')
except Exception:
self._params = None
raise
def _check_params(self, attrs):
""" Check consistency of parameter information across posteriors. """
_attrs = '_' + attrs
setattr(self._params, _attrs, [None] * len(self._params._names))
try:
for i, param in enumerate(self._params._names):
for posterior in self._subset[1:]:
if (getattr(self._subset[0], attrs)[param] \
!= getattr(posterior, attrs)[param]):
raise ValueError('Inconsistent %s for parameter'
' %s between posteriors %s and %s.' %
(attrs, param,
self._subset[0].ID, posterior.ID))
getattr(self._params, _attrs)[i] = \
getattr(self._subset[0], attrs)[param]
except (AttributeError, KeyError):
print('Parameter %s not specified correctly.' % attrs)
raise
[docs] def get_attr(self, attribute):
""" Get a list of attributes of the :class:`~.Runs` instances stored as
the current subset. """
return [getattr(run, attribute) for run in self.subset_to_plot]
def _filter_nestcheck_compatible(self):
""" Return only runs to plot that are compatible with nestcheck. """
try:
for run in self.subset_to_plot:
if not isinstance(run, NestedBackend):
raise TypeError('Nested sampling backends are required.')
except AttributeError:
print('Nested sampling runs are required.')
raise
else:
runs = [] # space to cache refs to only nestcheck-compatible runs
nestcheck_bcknds = []
# we only want the runs that are nestcheck-compatible
for run in self.subset_to_plot:
if run.use_nestcheck:
nestcheck_bcknds.append(run.nestcheck_backend)
runs.append(run)
return nestcheck_bcknds, runs