Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

emcee move feature #897

Draft
wants to merge 1 commit into
base: release-2.5
Choose a base branch
from
Draft
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
28 changes: 21 additions & 7 deletions phoebe/frontend/bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -5610,12 +5610,11 @@ def dependencies(self, compute=None, dataset=None, solver=None):
return deps_pip, deps_other

@send_if_client
def add_feature(self, kind, component=None, dataset=None,
def add_feature(self, kind, component=None, dataset=None, solver=None,
return_changes= False, **kwargs):
"""
Add a new feature (spot, gaussian process, etc) to a component or
dataset in the system. If not
provided, `feature` (the name of the new feature) will be created
Add a new feature (spot, gaussian process, etc) to a component, dataset,
or solver. If not provided, `feature` (the name of the new feature) will be created
for you and can be accessed by the `feature` attribute of the returned
<phoebe.parameters.ParameterSet>.

Expand All @@ -5634,10 +5633,11 @@ def add_feature(self, kind, component=None, dataset=None,
* <phoebe.parameters.feature.spot>
* <phoebe.parameters.feature.gp_sklearn>
* <phoebe.parameters.feature.gp_celerite2>
* <phoebe.parameters.feature.emcee_move>

See the entries above to see the valid kinds for `component` and `dataset`
based on the type of feature. An error will be raised if the passed value
for `component` and/or `dataset` are not allowed by the type of feature
See the entries above to see the valid kinds for `component`, `dataset`,
and `solver` based on the type of feature. An error will be raised if the passed value
for `component`, `dataset`, or `solver` are not allowed by the type of feature
with kind `kind`.

Arguments
Expand All @@ -5652,6 +5652,8 @@ def add_feature(self, kind, component=None, dataset=None,
feature. Required for features that must be attached to a component.
* `dataset` (string, optional): name of the dataset to attach the feature.
Required for features that must be attached to a dataset.
* `solver` (string, optional): name of the solver to attach the feature.
Required for features that must be attached to a solver.
* `feature` (string, optional): name of the newly-created feature.
* `overwrite` (boolean, optional, default=False): whether to overwrite
an existing feature with the same `feature` tag. If False,
Expand Down Expand Up @@ -5710,11 +5712,23 @@ def add_feature(self, kind, component=None, dataset=None,
if not _feature._dataset_allowed_for_feature(func.__name__, dataset_kind):
raise ValueError("{} does not support dataset with kind {}".format(func.__name__, dataset_kind))

if solver is not None:
if solver not in self.solvers:
raise ValueError("solver '{}' not one of {}".format(solver, self.solvers))

solver_kind = self.filter(solver=solver, context='solver').kind
else:
solver_kind = None

if not _feature._solver_allowed_for_feature(func.__name__, solver_kind):
raise ValueError("{} does not support solver with kind {}".format(func.__name__, solver_kind))

params, constraints = func(**kwargs)

metawargs = {'context': 'feature',
'component': component,
'dataset': dataset,
'solver': solver,
'feature': kwargs['feature'],
'kind': func.__name__}

Expand Down
2 changes: 1 addition & 1 deletion phoebe/parameters/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ def phoebe(**kwargs):
params += _server_params(**kwargs)

params += [BoolParameter(qualifier='enabled', copy_for={'context': 'dataset', 'dataset': '*'}, dataset='_default', value=kwargs.get('enabled', True), description='Whether to create synthetics in compute/solver run')]
params += [BoolParameter(qualifier='enabled', copy_for={'context': 'feature', 'feature': '*'}, feature='_default', value=kwargs.get('enabled', True), description='Whether to enable the feature in compute/solver run')]
params += [BoolParameter(qualifier='enabled', copy_for={'context': 'feature', 'kind': ['spot', 'gp_sklearn', 'gp_celerite2'], 'feature': '*'}, feature='_default', value=kwargs.get('enabled', True), description='Whether to enable the feature in compute/solver run')]
params += [BoolParameter(visible_if='ds_has_enabled_feature:gp_*', qualifier='gp_exclude_phases_enabled', value=kwargs.get('gp_exclude_phases_enabled', True), copy_for={'kind': ['lc', 'rv', 'lp'], 'dataset': '*'}, dataset='_default', description='Whether to apply the mask in gp_exclude_phases during gaussian process fitting.')]
params += [FloatArrayParameter(visible_if='ds_has_enabled_feature:gp_*,gp_exclude_phases_enabled:True', qualifier='gp_exclude_phases', value=kwargs.get('gp_exclude_phases', []), copy_for={'kind': ['lc', 'rv', 'lp'], 'dataset': '*'}, dataset='_default', default_unit=u.dimensionless_unscaled, required_shape=[None, 2], description='List of phase-tuples. Any observations inside the range set by any of the tuples will be ignored by the gaussian process features.')]

Expand Down
97 changes: 89 additions & 8 deletions phoebe/parameters/feature.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def _component_allowed_for_feature(feature_kind, component_kind):
_allowed['gp_celerite2'] = [None]
_allowed['gaussian_process'] = [None] # deprecated: remove in 2.5

return component_kind in _allowed[feature_kind]
return component_kind in _allowed.get(feature_kind, [None])

def _dataset_allowed_for_feature(feature_kind, dataset_kind):
_allowed = {}
Expand All @@ -29,7 +29,14 @@ def _dataset_allowed_for_feature(feature_kind, dataset_kind):
_allowed['gp_celerite2'] = ['lc', 'rv', 'lp']
_allowed['gaussian_process'] = ['lc', 'rv', 'lp'] # deprecated: remove in 2.5

return dataset_kind in _allowed[feature_kind]
return dataset_kind in _allowed.get(feature_kind, [None])

def _solver_allowed_for_feature(feature_kind, solver_kind):
_allowed = {}
_allowed['emcee_move'] = ['emcee']

return solver_kind in _allowed.get(feature_kind, [None])


def spot(feature, **kwargs):
"""
Expand All @@ -45,6 +52,7 @@ def spot(feature, **kwargs):
Allowed to attach to:
* components with kind: star
* datasets: not allowed
* solver: not allowed

Arguments
----------
Expand Down Expand Up @@ -181,6 +189,7 @@ def gp_celerite2(feature, **kwargs):
Allowed to attach to:
* components: not allowed
* datasets with kind: lc
* solvers: not allowed

If `compute_times` or `compute_phases` is used: the underlying model without
gaussian_processes will be computed at the given times/phases but will then
Expand Down Expand Up @@ -244,12 +253,84 @@ def gp_celerite2(feature, **kwargs):

return ParameterSet(params), constraints

def gaussian_process(feature, **kwargs):
def emcee_move(feature, **kwargs):
"""
Deprecated (will be removed in PHOEBE 2.5)
Create a <phoebe.parameters.ParameterSet> for an emcee_move feature to attach
to an <phoebe.parameters.solver.sampler.emcee> solver.

Generally, this will be used as an input to the kind argument in
<phoebe.frontend.bundle.Bundle.add_feature>. If attaching through
<phoebe.frontend.bundle.Bundle.add_feature>, all `**kwargs` will be
passed on to set the values as described in the arguments below. Alternatively,
see <phoebe.parameters.ParameterSet.set_value> to set/change the values
after creating the Parameters.

Support for celerite gaussian processes has been removed. This is now an
alias to <phoebe.parameters.feature.gp_celerite2>.
Allowed to attach to:
* components: not allowed
* datasets: not allowed
* solvers with kind: emcee


Arguments
----------
* `move` (string, optional, default='Stretch'): Type of move
(see https://emcee.readthedocs.io/en/stable/user/moves/)
* `weight` (float, optional, default=1.0): Weighted probability to apply to
move. Weights across all enabled emcee_move features will be renormalized
to sum to 1 before passing to emcee.
* `nsplits` (int, optional, default=2):
* `randomize_split` (bool, optional, default=True):
* `a` (float, optional, default=2.0):
* `smode` (string, optional, default='auto'):
* `s` (int, optional, default=16):
* `bw_method` (string, optional, default='scott'):
* `bw_constant` (float, optional, default=1.0):
* `sigma` (float, optional, default=1e-5):
* `gamma0_mode` (string, optional, default='auto'):
* `gamma0` (float, optional, default=0.5):
* `gammas` (float, optional, default=1.7):


Returns
--------
* (<phoebe.parameters.ParameterSet>, list): ParameterSet of all newly created
<phoebe.parameters.Parameter> objects and a list of all necessary
constraints.
"""
logger.warning("gaussian_process is deprecated. Use gp_celerite2 instead.")
return gp_celerite2(feature, **kwargs)

params = []

params += [ChoiceParameter(qualifier='move', value=kwargs.get('move', 'Stretch'), choices=['Stretch', 'Walk', 'KDE', 'DE', 'DESnooker'], description='Type of move (see https://emcee.readthedocs.io/en/stable/user/moves/)')]
params += [FloatParameter(qualifier='weight', value=kwargs.get('weight', 1.0), limits=(0,None), default_unit=u.dimensionless_unscaled, description='Weighted probability to apply to move. Weights across all enabled emcee_move features will be renormalized to sum to 1 before passing to emcee.')]

# NOTE: RedBlue requires subclassing
# params += [IntParameter(visible_if='move:RedBlue', qualifier='nsplits', value=kwargs.get('nsplits', 2), limits=(1,100), description='Passed directly to emcee. The number of sub-ensembles to use. Each sub-ensemble is updated in parallel using the other sets as the complementary ensemble. The default value is 2 and you probably won’t need to change that.')]
# params += [BoolParameter(visible_if='move:RedBlue', qualifier='randomize_split', value=kwargs.get('randomize_split', True), description='Passed directly to emcee. Randomly shuffle walkers between sub-ensembles. The same number of walkers will be assigned to each sub-ensemble on each iteration.')]

params += [FloatParameter(visible_if='move:Stretch', qualifier='a', value=kwargs.get('a', 2.0), limits=(None, None), default_units=u.dimensionless_unscaled, description='Passed directly to emcee. The stretch scale parameter.')]

params += [ChoiceParameter(visible_if='move:Walk', qualifier='smode', value=kwargs.get('smode', 'auto'), choices=['auto', 'manual'], description='Whether to manually provide the s parameter (number of helper walkers) or use all walkers in the complement by passing None to emcee.')]
params += [IntParameter(visible_if='move:Walk,smode:manual', qualifier='s', value=kwargs.get('s', 16), limits=(1,None), description='Passed directly to emcee. The number of helper walkers to use.')]

params += [ChoiceParameter(visible_if='move:KDE', qualifier='bw_method', value=kwargs.get('bw_method', 'scott'), choices=['scott', 'silverman', 'constant'], description='Passed directly to emcee. The bandwidth estimation method. See https://docs.scipy.org/doc/scipy/reference/generated/scipy.stats.gaussian_kde.html')]
params += [FloatParameter(visible_if='move:KDE,bw_method:constant', qualifier='bw_constant', value=kwargs.get('bw_constant', 1.0), limits=(None, None), default_unit=u.dimensionless_unscaled, description='Bandwidth estimation kde factor. See https://docs.scipy.org/docs/scipy/reference/generated/scipy.stats.gaussian_kde.html')]

params += [FloatParameter(visible_if='move:DE', qualifier='sigma', value=kwargs.get('sigma', 1e-5), limits=(0,None), default_unit=u.dimensionless_unscaled, description='Passed directly to emcee. The standard deviation of the Gaussian used to stretch the proposal vector.')]
params += [ChoiceParameter(visible_if='move:DE', qualifier='gamma0_mode', value=kwargs.get('gamma0_mode', 'auto'), choices=['auto', 'manual'], description='Whether to manually provide gamma0 or default to 2.38/sqrt(2 * ndim)')]
params += [FloatParameter(visible_if='move:DE,gamma0_mode:manual', qualifier='gamma0', value=kwargs.get('gamma0', 0.5), limits=(0,None), default_unit=u.dimensionless_unscaled, description='Passed directly to emcee. The mean stretch factor for the proposal vector.')]

params += [FloatParameter(visible_if='move:DESnooker', qualifier='gammas', value=kwargs.get('gammas', 1.7), limits=(0,None), default_unit=u.dimensionless_unscaled, description='Passed directly to emcee. The mean stretch factor of the proposal vector.')]

# NOTE: MH not implemented as it requires a callable
# NOTE: Gaussian not implemented as it requires a covariance (as scalar, vector, or matrix)

constraints = []

return ParameterSet(params), constraints



# del deepcopy
# del _component_allowed_for_feature
# del download_passband, list_installed_passbands, list_online_passbands, list_passbands, parameter_from_json, parse_json, send_if_client, update_if_client
# del fnmatch
3 changes: 2 additions & 1 deletion phoebe/parameters/parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@
# forbid all kinds
_forbidden_labels += ['lc', 'rv', 'lp', 'sp', 'orb', 'mesh']
_forbidden_labels += ['star', 'orbit', 'envelope']
_forbidden_labels += ['spot', 'pulsation']
_forbidden_labels += ['spot', 'pulsation', 'gaussian_process', 'emcee_move']
_forbidden_labels += ['phoebe', 'legacy', 'jktebop', 'photodynam', 'ellc']


Expand Down Expand Up @@ -303,6 +303,7 @@
'sigma_0', 'constant_value_bounds', 'length_scale_bounds',
'noise_level_bounds', 'periodicity_bounds', 'alpha_bounds', 'nu_bounds',
'sigma_0_bounds', 'alg_operation',
'move', 'weight', 'a', 'smode', 's', 'bw_method', 'bw_constant'
]

# from figure:
Expand Down
2 changes: 2 additions & 0 deletions phoebe/parameters/solver/sampler.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ def emcee(**kwargs):
params = _comments_params(**kwargs)
params += _server_params(**kwargs)

params += [BoolParameter(qualifier='enabled', copy_for={'context': 'feature', 'kind': ['emcee_move'], 'feature': '*'}, feature='_default', value=kwargs.get('enabled', True), description='Whether to enable the feature in compute/solver run')]

params += [ChoiceParameter(qualifier='compute', value=kwargs.get('compute', 'None'), choices=['None'], description='compute options to use for forward model')]

params += [ChoiceParameter(qualifier='continue_from', value=kwargs.get('continue_from', 'None'), choices=['None'], description='continue the MCMC run from an existing emcee solution. Chains will be appended to existing chains (so it is safe to overwrite the existing solution). If None, will start a new run using init_from.')]
Expand Down
26 changes: 25 additions & 1 deletion phoebe/solverbackends/solverbackends.py
Original file line number Diff line number Diff line change
Expand Up @@ -1628,9 +1628,33 @@ def mpi_failed_samples_callback(result):
esargs['ndim'] = len(params_uniqueids)
esargs['log_prob_fn'] = _lnprobability
# esargs['a'] = kwargs.pop('a', None),
# esargs['moves'] = kwargs.pop('moves', None)
# esargs['args'] = None

enabled_solver_features = b.filter(qualifier='enabled', value=True, solver=solver, **_skip_filter_checks).features
enabled_moves = b.filter(feature=enabled_solver_features, kind='emcee_move', **_skip_filter_checks).features
if len(enabled_moves):
logger.info("enabled moves: {}".format(enabled_moves))
moves = []
weights_sum = np.sum([b.get_value(qualifier='weight', feature=move, **_skip_filter_checks) for move in enabled_moves])
for move_feature in enabled_moves:
move_feature_ps = b.get_feature(feature=move_feature, check_visible=True)
move_class = move_feature_ps.get_value(qualifier='move', **_skip_filter_checks)
move_weight = move_feature_ps.get_value(qualifier='weight') / weights_sum
move_kwargs = {p.qualifier: p.get_value() for p in move_feature_ps.to_list() if p.qualifier not in ['move', 'weight']}
if move_kwargs.pop('smode', None) == 'auto':
move_kwargs['s'] = None
if move_kwargs.get('bw_method', None) == 'constant':
move_kwargs['bw_method'] = move_kwargs.pop('bw_constant', 1.0)
if move_kwargs.pop('gamma0_mode', None) == 'auto':
move_kwargs['gamma0'] = None

this_move_obj = getattr(emcee.moves, '{}Move'.format(move_class))(**move_kwargs)

logger.info("creating {}Move(**{}) with weight={}".format(move_class, move_kwargs, move_weight))
moves.append((this_move_obj, move_weight))

esargs['moves'] = moves

esargs['kwargs'] = {'b': _bsolver(b, solver, compute, init_from+priors, wrap_central_values),
'params_uniqueids': params_uniqueids,
'compute': compute,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ def test_forbidden(verbose=False):
b.add_solver('optimizer.differential_corrections')
b.add_solver('optimizer.cg')
b.add_solver('optimizer.powell')
b.add_solver('sampler.emcee')
b.add_solver('sampler.emcee', solver='emcee_solv')
b.add_solver('sampler.dynesty')

b.add_server('remoteslurm')
b.add_server('localthread')

# TODO: include constraint_func? Shouldn't matter since they're not in twigs
should_be_forbidden = b.qualifiers + b.contexts + b.kinds + [c.split('@')[0] for c in b.get_parameter(qualifier='columns').choices]
Expand Down
Loading