# Copyright (C) 2019 Collin Capano
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""Jump proposals that use a normal distribution."""
import numpy
from epsie import proposals as epsie_proposals
from epsie.proposals import Boundaries
from pycbc import VARARGS_DELIM
[docs]
class EpsieNormal(epsie_proposals.Normal):
"""Adds ``from_config`` method to epsie's normal proposal."""
[docs]
@classmethod
def from_config(cls, cp, section, tag):
"""Loads a proposal from a config file.
This calls :py:func:`epsie_from_config` with ``cls`` set to
:py:class:`epsie.proposals.Normal` and ``with_boundaries`` set to
False. See that function for details on options that can be read.
Example::
[jump_proposal-mchrip+q]
name = normal
var-q = 0.1
Parameters
----------
cp : WorkflowConfigParser instance
Config file to read from.
section : str
The name of the section to look in.
tag : str
:py:const:`pycbc.VARARGS_DELIM` separated list of parameter names
to create proposals for.
Returns
-------
:py:class:`epsie.proposals.Normal`:
A normal proposal for use with ``epsie`` samplers.
"""
return epsie_from_config(cls, cp, section, tag, with_boundaries=False)
[docs]
class EpsieAdaptiveNormal(epsie_proposals.AdaptiveNormal):
"""Adds ``from_config`` method to epsie's adaptive normal proposal."""
[docs]
@classmethod
def from_config(cls, cp, section, tag):
"""Loads a proposal from a config file.
This calls :py:func:`epsie_adaptive_from_config` with ``cls`` set to
:py:class:`epsie.proposals.AdaptiveNormal`. See that function for
details on options that can be read.
Example::
[jump_proposal-mchirp+q]
name = adaptive_normal
adaptation-duration = 1000
min-q = 1
max-q = 8
min-mchirp = 20
max-mchirp = 80
Parameters
----------
cp : WorkflowConfigParser instance
Config file to read from.
section : str
The name of the section to look in.
tag : str
:py:const:`pycbc.VARARGS_DELIM` separated list of parameter names
to create proposals for.
Returns
-------
:py:class:`epsie.proposals.AdaptiveNormal`:
An adaptive normal proposal for use with ``epsie`` samplers.
"""
return epsie_adaptive_from_config(cls, cp, section, tag,
boundary_arg_name='prior_widths')
[docs]
class EpsieATAdaptiveNormal(epsie_proposals.ATAdaptiveNormal):
"""Adds ``from_config`` method to epsie's ATAdaptiveProposal."""
[docs]
@classmethod
def from_config(cls, cp, section, tag):
"""Loads a proposal from a config file.
This calls :py:func:`epsie_from_config` with ``cls`` set to
:py:class:`epsie.proposals.AdaptiveProposal` and ``with_boundaries``
set to False. See that function for details on options that can be
read.
Example::
[jump_proposal-mchrip+q]
name = adaptive_proposal
diagonal =
Parameters
----------
cp : WorkflowConfigParser instance
Config file to read from.
section : str
The name of the section to look in.
tag : str
:py:const:`pycbc.VARARGS_DELIM` separated list of parameter names
to create proposals for.
Returns
-------
:py:class:`epsie.proposals.AdaptiveProposal`:
An adaptive proposal for use with ``epsie`` samplers.
"""
return epsie_at_adaptive_from_config(cls, cp, section, tag,
with_boundaries=False)
[docs]
def epsie_from_config(cls, cp, section, tag, with_boundaries=False):
r"""Generic function for loading epsie proposals from a config file.
This should be used for proposals that are not adaptive.
The section that is read should have the format ``[{section}-{tag}]``,
where ``{tag}`` is a :py:const:`pycbc.VARARGS_DELIM` separated list
of the parameters to create the jump proposal for.
Options that are read:
* name : str
Required. Must match the name of the proposal.
* var-{param} : float
Optional. Variance to use for parameter {param}. If ``with_boundaries``
is True, then any parameter not specified will use a default variance
of :math:`(\Delta p/10)^2`, where :math:`\Delta p` is the boundary
width for that parameter. If ``with_boundaries`` is False, will use
a default value of 1.
* min-{param} : float
* max-{param} : float
The bounds on each parameter. Required if ``with_boundaries`` is set to
True, in which case bounds must be provided for every parameter.
Parameters
----------
cls : epsie.proposals.BaseProposal
The epsie proposal class to initialize.
cp : WorkflowConfigParser instance
Config file to read from.
section : str
The name of the section to look in.
tag : str
:py:const:`pycbc.VARARGS_DELIM` separated list of parameter names
to create proposals for.
with_boundaries : bool, optional
Try to load boundaries from the section and pass a ``boundaries``
argument to the class's initialization. This should be set to true
for bounded proposals. Default is False.
Returns
-------
cls :
The class initialized with the options read from the config file.
"""
# check that the name matches
assert cp.get_opt_tag(section, "name", tag) == cls.name, (
"name in specified section must match mine")
params, opts = load_opts(cp, section, tag, skip=['name'])
args = {'parameters': params}
if with_boundaries:
boundaries = get_param_boundaries(params, opts)
args['boundaries'] = boundaries
if 'discrete' in cls.name.split('_'):
args.update({'successive':
get_epsie_discrete_successive_settings(params, opts)})
# if there are any options left, assume they are for setting the variance
if opts:
cov = get_variance(params, opts)
elif with_boundaries:
cov = numpy.array([abs(boundaries[p])/10. for p in params])**2.
else:
cov = None
args['cov'] = cov
# no other options should remain
if opts:
raise ValueError("unrecognized options {}"
.format(', '.join(opts.keys())))
return cls(**args)
[docs]
def epsie_adaptive_from_config(cls, cp, section, tag, with_boundaries=True,
boundary_arg_name='boundaries'):
"""Generic function for loading adaptive epsie proposals from a config
file.
The section that is read should have the format ``[{section}-{tag}]``,
where ``{tag}`` is a :py:const:`pycbc.VARARGS_DELIM` separated list
of the parameters to create the jump proposal for.
Options that are read:
* name : str
Required. Must match the name of the proposal.
* adaptation-duration : int
Required. Sets the ``adaptation_duration``.
* min-{param} : float
* max-{param} : float
The bounds on each parameter. Required if ``with_boundaries`` is set to
True, in which case bounds must be provided for every parameter.
* var-{param} : float
Optional. Initial variance to use. If not provided, will use a
default based on the bounds (see
:py:class:`epsie.proposals.AdaptiveSupport` for details).
* adaptation-decay : int
Optional. Sets the ``adaptation_decay``. If not provided, will use
the class's default.
* start-iteration : int
Optional. Sets the ``start_iteration``.If not provided, will use
the class's default.
* target-rate : float
Optional. Sets the ``target_rate``. If not provided, will use
the class's default.
Parameters
----------
cls : epsie.proposals.BaseProposal
The epsie proposal class to initialize. The class should have
:py:class:`epsie.proposals.normal.AdaptiveSupport`.
cp : WorkflowConfigParser instance
Config file to read from.
section : str
The name of the section to look in.
tag : str
:py:const:`pycbc.VARARGS_DELIM` separated list of parameter names
to create proposals for.
with_boundaries : bool, optional
Try to load boundaries from the section and pass a ``boundaries``
argument to the class's initialization. Default is True.
boundary_arg_name : str, optional
The name of the argument for the boundaries (only used if
``with_boundaries`` is True). Provided because some adaptive proposals
that only need the boundary widths call this ``prior_widths``. Default
is ``'boundaries'``.
Returns
-------
cls :
The class initialized with the options read from the config file.
"""
# check that the name matches
assert cp.get_opt_tag(section, "name", tag) == cls.name, (
"name in specified section must match mine")
params, opts = load_opts(cp, section, tag, skip=['name'])
args = {'parameters': params}
# get the bounds
if with_boundaries:
args[boundary_arg_name] = get_param_boundaries(params, opts)
if 'discrete' in cls.name.split('_'):
args.update({'successive':
get_epsie_discrete_successive_settings(params, opts)})
# get the adaptation parameters
args.update(get_epsie_adaptation_settings(opts))
# if there are any other options, assume they are for setting the
# initial standard deviation
if opts:
var = get_variance(params, opts)
args['initial_std'] = var**0.5
# at this point, there should be no options left
if opts:
raise ValueError('unrecognized options {} in section {}'
.format(', '.join(opts.keys()),
'-'.join([section, tag])))
return cls(**args)
[docs]
def epsie_at_adaptive_from_config(cls, cp, section, tag,
with_boundaries=False):
"""Generic function for loading AT Adaptive Normal proposals from a config
file.
The section that is read should have the format ``[{section}-{tag}]``,
where ``{tag}`` is a :py:const:`pycbc.VARARGS_DELIM` separated list
of the parameters to create the jump proposal for.
Options that are read:
* name : str
Required. Must match the name of the proposal.
* adaptation-duration : int
Sets the ``adaptation_duration``. If not provided will use the class's
default.
* diagonal : bool, optional
Determines whether only to adapt the variance. If True will only train
the diagonal elements.
* componentwise : bool, optional
Whether to include a componentwise scaling of the parameters.
By default set to False. Componentwise scaling `ndim` times more
expensive than global scaling.
* min-{param} : float
* max-{param} : float
The bounds on each parameter. Required if ``with_boundaries`` is set to
True, in which case bounds must be provided for every parameter.
* start-iteration : int
Optional. Sets the ``start_iteration``. If not provided, will use
the class's default.
* target-rate : float
Optional. Sets the ``target_rate``. If not provided, will use
the class's default.
Parameters
----------
cls : epsie.proposals.BaseProposal
The epsie proposal class to initialize. The class should have
:py:class:`epsie.proposals.normal.AdaptiveSupport`.
cp : WorkflowConfigParser instance
Config file to read from.
section : str
The name of the section to look in.
tag : str
:py:const:`pycbc.VARARGS_DELIM` separated list of parameter names
to create proposals for.
with_boundaries : bool, optional
Try to load boundaries from the section and pass a ``boundaries``
argument to the class's initialization. Default is True.
Returns
-------
cls :
The class initialized with the options read from the config file.
"""
# check that the name matches
assert cp.get_opt_tag(section, "name", tag) == cls.name, (
"name in specified section must match mine")
params, opts = load_opts(cp, section, tag, skip=['name'])
args = {'parameters': params}
# get the bounds
if with_boundaries:
args['boundaries'] = get_param_boundaries(params, opts)
if 'discrete' in cls.name.split('_'):
args.update({'successive':
get_epsie_discrete_successive_settings(params, opts)})
# get the adaptation parameters
args.update(get_epsie_adaptation_settings(opts, cls.name))
# bounded and angular adaptive proposals support diagonal-only
diagonal = opts.pop('diagonal', None)
if not any(p in cls.name.split('_') for p in ['bounded', 'angular']):
args.update({'diagonal': diagonal is not None})
componentwise = opts.pop('componentwise', None)
if componentwise is not None:
args.update({'componentwise': True})
if opts:
raise ValueError("unrecognized options {}"
.format(', '.join(opts.keys())))
return cls(**args)
[docs]
def load_opts(cp, section, tag, skip=None):
"""Loads config options for jump proposals.
All `-` in option names are converted to `_` before returning.
Parameters
----------
cp : WorkflowConfigParser instance
Config file to read from.
section : str
The name of the section to look in.
tag : str
:py:const:`pycbc.VARARGS_DELIM` separated list of parameter names
to create proposals for.
skip : list, optional
List of option names to skip loading.
Returns
-------
params : list
List of parameter names the jump proposal is for.
opts : dict
Dictionary of option names -> values, where all values are strings.
"""
if skip is None:
skip = []
params = tag.split(VARARGS_DELIM)
# get options
readsection = '-'.join([section, tag])
opts = {opt.replace('-', '_'): cp.get(readsection, opt)
for opt in cp.options(readsection) if opt not in skip}
return params, opts
[docs]
def get_variance(params, opts, default=1.):
"""Gets variance for jump proposals from the dictionary of options.
This looks for ``var_{param}`` for every parameter listed in ``params``.
If found, the argument is popped from the given ``opts`` dictionary. If not
found, ``default`` will be used.
Parameters
----------
params : list of str
List of parameter names to look for.
opts : dict
Dictionary of option -> value that was loaded from a config file
section.
default : float, optional
Default value to use for parameters that do not have variances
provided. Default is 1.
Returns
-------
numpy.array
Array of variances to use. Order is the same as the parameter names
given in ``params``.
"""
varfmt = 'var_{}'
cov = numpy.array([float(opts.pop(varfmt.format(param), default))
for param in params])
return cov
[docs]
def get_param_boundaries(params, opts):
"""Gets parameter boundaries for jump proposals.
The syntax for the options should be ``(min|max)_{param} = value``. Both
a minimum and maximum should be provided for every parameter in ``params``.
If the opts are created using ``load_opts``, then the options can be
formatted as ``(min|max)-{param}``, since that function will turn all ``-``
to ``_`` in option names.
Arguments will be popped from the given ``opts`` dictionary.
Parameters
----------
params : list of str
List of parameter names to get boundaries for.
opts : dict
Dictionary of option -> value that was loaded from a config file
section.
Returns
-------
dict :
Dictionary of parameter names -> :py:class:`epsie.proposals.Boundaries`
"""
boundaries = {}
for param in params:
minbound = opts.pop('min_{}'.format(param), None)
if minbound is None:
raise ValueError("Must provide a minimum bound for {p}."
"Syntax is min_{p} = val".format(p=param))
maxbound = opts.pop('max_{}'.format(param), None)
if maxbound is None:
raise ValueError("Must provide a maximum bound for {p}."
"Syntax is max_{p} = val".format(p=param))
boundaries[param] = Boundaries((float(minbound), float(maxbound)))
return boundaries
[docs]
def get_epsie_adaptation_settings(opts, name=None):
"""Get settings for Epsie adaptive proposals from a config file.
This requires that ``adaptation_duration`` is in the given dictionary.
It will also look for ``adaptation_decay``, ``start_iteration``, and
``target_rate``, but these are optional. Arguments will be popped from the
given dictionary.
Parameters
----------
opts : dict
Dictionary of option -> value that was loaded from a config file
section.
name : str (optional)
Proposal name
Returns
-------
dict :
Dictionary of argument name -> values.
"""
args = {}
adaptation_duration = opts.pop('adaptation_duration', None)
if adaptation_duration is None:
if name is not None:
if all(p in name.split('_') for p in ['at', 'adaptive']):
args.update({'adaptation_duration': None})
else:
raise ValueError("No adaptation_duration specified")
else:
args.update({'adaptation_duration': int(adaptation_duration)})
# optional args
adaptation_decay = opts.pop('adaptation_decay', None)
if adaptation_decay is not None:
args.update({'adaptation_decay': int(adaptation_decay)})
start_iteration = opts.pop('start_iteration', None)
if start_iteration is not None:
args.update({'start_iteration': int(start_iteration)})
target_rate = opts.pop('target_rate', None)
if target_rate is not None:
args.update({'target_rate': float(target_rate)})
return args
[docs]
def get_epsie_discrete_successive_settings(params, opts):
"""Get settings for Epsie successive discrete proposal successive jumps
from a config file.
If ``successive`` is not defined for a parameter then assumes successive
jumps are not allowed (i.e. jumps from an integer to the same integer).
Arguments will be popped from the given dictionary.
Example::
[jump_proposal-k+n]
name = discrete
successive-k =
This example sets successive jumps for ``k`` but does not do so for ``n``.
Parameters
----------
params : list of str
List of parameter names to get the successive option for.
opts : dict
Dictionary of option -> value that was loaded from a config file
section.
Returns
-------
dict :
Dictionary of parameter names -> bools
"""
successive = {}
for param in params:
successive.update(
{param: opts.pop('successive_{}'.format(param), None) is not None})
return successive