Skip to content

Commit 8cb812a

Browse files
committed
use class-structure for built-in features
1 parent 9337bc3 commit 8cb812a

6 files changed

Lines changed: 127 additions & 127 deletions

File tree

phoebe/features/common.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ class BaseFeature:
55
def __init__(self, **kwargs):
66
self.kwargs = kwargs
77

8+
@classmethod
9+
def create_feature_parameters(self, feature, **kwargs):
10+
raise NotImplementedError("create_feature_parameters must be implemented in the feature subclass")
11+
812
@classmethod
913
def parse_from_feature_ps(cls, b, feature_ps, param_list):
1014
_skip_filter_checks = {'check_default': False,
@@ -28,8 +32,3 @@ def parse_bundle(cls, b, feature_ps):
2832
@classmethod
2933
def from_bundle(cls, b, feature_ps):
3034
return cls(**cls.parse_bundle(b, feature_ps))
31-
32-
@classmethod
33-
def get_parameters(self, **kwargs):
34-
raise NotImplementedError("get_parameters must be implemented in the feature subclass")
35-

phoebe/features/component_features.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import astropy.units as u
33

44
import phoebe.parameters.feature as _parameters_feature
5+
from phoebe.parameters import FloatParameter, ParameterSet, constraint
56
from phoebe.features.common import BaseFeature
67

78
import logging
@@ -115,6 +116,46 @@ class Spot(ComponentFeature):
115116
remeshing_required = False
116117
proto_coords = True
117118

119+
@classmethod
120+
def create_feature_parameters(cls, feature, **kwargs):
121+
"""
122+
Create a <phoebe.parameters.ParameterSet> for a spot feature.
123+
124+
Generally, this will be used as an input to the kind argument in
125+
<phoebe.frontend.bundle.Bundle.add_feature>. If attaching through
126+
<phoebe.frontend.bundle.Bundle.add_feature>, all `**kwargs` will be
127+
passed on to set the values as described in the arguments below. Alternatively,
128+
see <phoebe.parameters.ParameterSet.set_value> to set/change the values
129+
after creating the Parameters.
130+
131+
Allowed to attach to:
132+
* components with kind: star
133+
* datasets: not allowed
134+
135+
Arguments
136+
----------
137+
* `colat` (float/quantity, optional): colatitude of the center of the spot
138+
wrt spin axis.
139+
* `long` (float/quantity, optional): longitude of the center of the spot wrt
140+
spin axis.
141+
* `radius` (float/quantity, optional): angular radius of the spot.
142+
* `relteff` (float/quantity, optional): temperature of the spot relative
143+
to the intrinsic temperature.
144+
145+
Returns
146+
--------
147+
* (<phoebe.parameters.ParameterSet>, list): ParameterSet of all newly created
148+
<phoebe.parameters.Parameter> objects and a list of all necessary
149+
constraints.
150+
"""
151+
params = []
152+
params += [FloatParameter(qualifier="colat", value=kwargs.get('colat', 0.0), default_unit=u.deg, description='Colatitude of the center of the spot wrt spin axis')]
153+
params += [FloatParameter(qualifier="long", value=kwargs.get('long', 0.0), default_unit=u.deg, description='Longitude of the center of the spot wrt spin axis')]
154+
params += [FloatParameter(qualifier='radius', value=kwargs.get('radius', 1.0), default_unit=u.deg, description='Angular radius of the spot')]
155+
params += [FloatParameter(qualifier='relteff', value=kwargs.get('relteff', 1.0), limits=(0.,None), default_unit=u.dimensionless_unscaled, description='Temperature of the spot relative to the intrinsic temperature')]
156+
157+
return ParameterSet(params), []
158+
118159
@classmethod
119160
def parse_bundle(cls, b, feature_ps):
120161
"""
@@ -188,9 +229,9 @@ def pointing_vector(self, s, time):
188229
exp = np.cross(eyp, ezp)
189230

190231
# now we can express the pointing vector in terms of the primed basis
191-
pv = (np.sin(self._colat)*np.cos(longitude)*exp +
192-
np.sin(self._colat)*np.sin(longitude)*eyp +
193-
np.cos(self._colat)*ezp)
232+
pv = (np.sin(colat)*np.cos(longitude)*exp +
233+
np.sin(colat)*np.sin(longitude)*eyp +
234+
np.cos(colat)*ezp)
194235

195236
# renormalize and return pointing vector
196237
return pv / np.linalg.norm(pv)

phoebe/features/dataset_features.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,41 @@ def __repr__(self):
3131
def get_parameters(self, **kwargs):
3232
raise NotImplementedError("get_parameters must be implemented in the feature subclass")
3333

34-
def modify_data_for_estimators(self, b, feature_ps, data_ps, **data_arrays):
34+
def modify_data_for_estimators(self, b, data_ps, **data_arrays):
3535
"""
3636
Modify the data parameters for the estimators.
3737
This is called before the data is passed to the estimators.
3838
"""
3939
return {}
4040

41-
def modify_model(self, b, feature_ps, model_ps):
41+
def modify_model(self, b, model_ps):
4242
raise NotImplementedError("modify_model must be implemented in the feature subclass")
4343

4444

45-
class Rv_Offset(DatasetFeature):
45+
class RVOffset(DatasetFeature):
4646
allowed_component_kinds = ['star']
4747
allowed_dataset_kinds = ['rv']
4848

49+
@classmethod
50+
def create_feature_parameters(self, feature, **kwargs):
51+
"""
52+
Create a <phoebe.parameters.ParameterSet> for an rvoffset feature.
53+
54+
Generally, this will be used as an input to the kind argument in
55+
<phoebe.frontend.bundle.Bundle.add_feature>. If attaching through
56+
<phoebe.frontend.bundle.Bundle.add_feature>, all `**kwargs` will be
57+
passed on to set the values as described in the arguments below. Alternatively,
58+
see <phoebe.parameters.ParameterSet.set_value> to set/change the values
59+
after creating the Parameters.
60+
61+
Allowed to attach to:
62+
* datasets: rv
63+
"""
64+
params = []
65+
params += [FloatParameter(qualifier='rv_offset', copy_for={'kind': ['star'], 'component': '*'}, component='_default', value=kwargs.get('rv_offset', 0.0), default_unit=u.km/u.s, description='Per-component offset to add to synthetic RVs (i.e. for hot stars)')]
66+
67+
return ParameterSet(params), []
68+
4969
@classmethod
5070
def parse_bundle(cls, b, feature_ps):
5171
"""
@@ -54,7 +74,7 @@ def parse_bundle(cls, b, feature_ps):
5474
rv_offsets = feature_ps.filter(qualifier='rv_offset', **_skip_filter_checks)
5575
return {param.component: param.get_quantity(**_skip_filter_checks) for param in rv_offsets.to_list()}
5676

57-
def modify_model(self, b, feature_ps, model_ps):
77+
def modify_model(self, b, model_ps):
5878
for rv_param in model_ps.filter(qualifier='rvs', kind=['rv', 'mesh'], **_skip_filter_checks).to_list():
5979
rv_param.set_value(rv_param.get_value() + self.kwargs.get(rv_param.component).to_value(rv_param.default_unit), ignore_readonly=True, **_skip_filter_checks)
6080

phoebe/frontend/bundle.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from packaging.version import parse
1818
from copy import deepcopy as _deepcopy
1919
import pickle as _pickle
20-
from inspect import getsource as _getsource
20+
from inspect import getsource as _getsource, isclass
2121

2222
from scipy.optimize import curve_fit as cfit
2323
from tqdm import tqdm as _tqdm
@@ -42,6 +42,7 @@
4242
from phoebe.solverbackends import solverbackends as _solverbackends
4343
from phoebe.distortions import roche
4444
from phoebe.frontend import io
45+
from phoebe.features.common import BaseFeature
4546
from phoebe.features import dataset_features, component_features
4647
from phoebe.features.gaussian_processes import handle_gaussian_processes, _use_celerite2, _use_sklearn
4748
from phoebe.atmospheres.passbands import list_installed_passbands, list_online_passbands, get_passband, update_passband, _timestamp_to_dt
@@ -5599,8 +5600,8 @@ def add_feature(self, kind, component=None, dataset=None,
55995600
* ValueError: if `dataset` is required but it not provided or is of the
56005601
wrong kind.
56015602
"""
5602-
if getattr(kind, '_phoebe_custom_feature', False):
5603-
func = kind.get_parameters
5603+
if isclass(kind) and issubclass(kind, BaseFeature):
5604+
func = kind.create_feature_parameters
56045605
kind_name = kind.__name__
56055606
custom_feature = True
56065607
else:
@@ -5645,6 +5646,7 @@ def add_feature(self, kind, component=None, dataset=None,
56455646
if not _feature._dataset_allowed_for_feature(kind, dataset_kind):
56465647
raise ValueError("{} does not support dataset with kind {}".format(kind_name, dataset_kind))
56475648

5649+
# NOTE: kwargs is guaranteed to include feature, which is a required argument to func
56485650
params, constraints = func(**kwargs)
56495651

56505652
if custom_feature:
@@ -5709,6 +5711,28 @@ def get_feature(self, feature=None, **kwargs):
57095711
kwargs['context'] = 'feature'
57105712
return self.filter(**kwargs)
57115713

5714+
def get_feature_code(self, feature=None, **kwargs):
5715+
"""
5716+
Get the instantiated object containing the logic to run a feature.
5717+
5718+
See also:
5719+
* <phoebe.frontend.bundle.Bundle.get_feature>
5720+
5721+
Arguments
5722+
---------
5723+
* `feature`: (string, optional, default=None): the name of the feature
5724+
* `**kwargs`: any other tags to do the filtering (excluding feature and context)
5725+
5726+
Returns:
5727+
* (obj): the object that implements the feature logic
5728+
"""
5729+
feature_ps = self.get_feature(feature=feature, **kwargs)
5730+
if 'custom_code' in feature_ps.qualifiers:
5731+
cls = feature_ps.get_value(qualifier='custom_code', check_visible=False, check_default=False)
5732+
else:
5733+
cls = _feature._feature_classes.get(feature_ps.kind)
5734+
return cls.from_bundle(self, feature_ps)
5735+
57125736
@send_if_client
57135737
def remove_feature(self, feature=None, return_changes=False, **kwargs):
57145738
"""
@@ -12000,15 +12024,10 @@ def restore_conf():
1200012024
# handle all dataset-features (except for GPs, which need to all be handled simultaneously
1200112025
# and AFTER - instead of BEFORE - dataset-scaling from pblum_mode='dataset-scaled')
1200212026
enabled_features = self.filter(qualifier='enabled', compute=compute, context='compute', value=True, **_skip_filter_checks).features
12003-
for feature in enabled_features:
12004-
feature_ps = self.get_feature(feature=feature, **_skip_filter_checks)
12005-
if feature_ps.get_value(qualifier='feature_type', **_skip_filter_checks) != 'dataset':
12006-
continue
12007-
if 'custom_code' in feature_ps.qualifiers:
12008-
feature_cls = feature_ps.get_value(qualifier='custom_code', **_skip_filter_checks)
12009-
else:
12010-
feature_cls = getattr(dataset_features, feature_ps.kind.title(), None)
12011-
feature_cls.from_bundle(self, feature_ps).modify_model(self, feature_ps, ml_params)
12027+
enabled_dataset_features = self.filter(qualifier='feature_type', feature=enabled_features, context='feature', value='dataset', **_skip_filter_checks).features
12028+
for feature in enabled_dataset_features:
12029+
feature_obj = self.get_feature_code(feature=feature)
12030+
feature_obj.modify_model(self, ml_params)
1201212031

1201312032
# handle flux scaling for any pblum_mode == 'dataset-scaled'
1201412033
# or for any dataset in which pblum_mode == 'dataset-coupled' and pblum_dataset points to a 'dataset-scaled' dataset

phoebe/parameters/feature.py

Lines changed: 13 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
from phoebe.parameters import *
44
from phoebe.parameters import constraint
5-
from phoebe.features.dataset_features import *
5+
from phoebe.features.component_features import Spot
6+
from phoebe.features.dataset_features import RVOffset
67
from phoebe import u
78
from phoebe import conf
89

@@ -12,96 +13,31 @@
1213

1314
### NOTE: if creating new parameters, add to the _forbidden_labels list in parameters.py
1415

15-
_allowed_components = {'spot': ['star', 'envelope'],
16-
'rv_offset': [None],
17-
'pulsation': ['star', 'envelope'],
16+
_allowed_components = {'pulsation': ['star', 'envelope'],
1817
'gp_sklearn': [None],
19-
'gp_celerite2': [None],
20-
'gaussian_process': [None]}
18+
'gp_celerite2': [None]}
2119

22-
_allowed_datasets = {'spot': [None],
23-
'rv_offset': ['rv'],
24-
'pulsation': [None],
20+
_allowed_datasets = {'pulsation': [None],
2521
'gp_sklearn': ['lc', 'rv', 'lp'],
26-
'gp_celerite2': ['lc', 'rv', 'lp'],
27-
'gaussian_process': ['lc', 'rv', 'lp']}
22+
'gp_celerite2': ['lc', 'rv', 'lp']}
23+
24+
_feature_classes = {}
2825

2926
def _component_allowed_for_feature(feature_kind, component_kind):
3027
return component_kind in getattr(feature_kind, 'allowed_component_kinds', _allowed_components.get(feature_kind, []))
3128

3229
def _dataset_allowed_for_feature(feature_kind, dataset_kind):
3330
return dataset_kind in getattr(feature_kind, 'allowed_dataset_kinds', _allowed_datasets.get(feature_kind, []))
3431

35-
def _register(feature_cls, name):
36-
globals()[name] = feature_cls.get_parameters
32+
def _register_feature(feature_cls, name):
33+
globals()[name] = feature_cls.create_feature_parameters
3734
_allowed_components[name] = getattr(feature_cls, 'allowed_component_kinds', [])
3835
_allowed_datasets[name] = getattr(feature_cls, 'allowed_dataset_kinds', [])
36+
_feature_classes[name] = feature_cls
3937

40-
def spot(feature, **kwargs):
41-
"""
42-
Create a <phoebe.parameters.ParameterSet> for a spot feature.
43-
44-
Generally, this will be used as an input to the kind argument in
45-
<phoebe.frontend.bundle.Bundle.add_feature>. If attaching through
46-
<phoebe.frontend.bundle.Bundle.add_feature>, all `**kwargs` will be
47-
passed on to set the values as described in the arguments below. Alternatively,
48-
see <phoebe.parameters.ParameterSet.set_value> to set/change the values
49-
after creating the Parameters.
50-
51-
Allowed to attach to:
52-
* components with kind: star
53-
* datasets: not allowed
54-
55-
Arguments
56-
----------
57-
* `colat` (float/quantity, optional): colatitude of the center of the spot
58-
wrt spin axis.
59-
* `long` (float/quantity, optional): longitude of the center of the spot wrt
60-
spin axis.
61-
* `radius` (float/quantity, optional): angular radius of the spot.
62-
* `relteff` (float/quantity, optional): temperature of the spot relative
63-
to the intrinsic temperature.
64-
65-
Returns
66-
--------
67-
* (<phoebe.parameters.ParameterSet>, list): ParameterSet of all newly created
68-
<phoebe.parameters.Parameter> objects and a list of all necessary
69-
constraints.
70-
"""
7138

72-
params = []
73-
74-
params += [FloatParameter(qualifier="colat", value=kwargs.get('colat', 0.0), default_unit=u.deg, description='Colatitude of the center of the spot wrt spin axis')]
75-
params += [FloatParameter(qualifier="long", value=kwargs.get('long', 0.0), default_unit=u.deg, description='Longitude of the center of the spot wrt spin axis')]
76-
params += [FloatParameter(qualifier='radius', value=kwargs.get('radius', 1.0), default_unit=u.deg, description='Angular radius of the spot')]
77-
# params += [FloatParameter(qualifier='area', value=kwargs.get('area', 1.0), default_unit=u.solRad, description='Surface area of the spot')]
78-
79-
params += [FloatParameter(qualifier='relteff', value=kwargs.get('relteff', 1.0), limits=(0.,None), default_unit=u.dimensionless_unscaled, description='Temperature of the spot relative to the intrinsic temperature')]
80-
# params += [FloatParameter(qualifier='teff', value=kwargs.get('teff', 10000), default_unit=u.K, description='Temperature of the spot')]
81-
82-
constraints = []
83-
84-
return ParameterSet(params), constraints
85-
86-
def rv_offset(feature, **kwargs):
87-
"""
88-
Create a <phoebe.parameters.ParameterSet> for an rvoffset feature.
89-
90-
Generally, this will be used as an input to the kind argument in
91-
<phoebe.frontend.bundle.Bundle.add_feature>. If attaching through
92-
<phoebe.frontend.bundle.Bundle.add_feature>, all `**kwargs` will be
93-
passed on to set the values as described in the arguments below. Alternatively,
94-
see <phoebe.parameters.ParameterSet.set_value> to set/change the values
95-
after creating the Parameters.
96-
97-
Allowed to attach to:
98-
* datasets: rv
99-
"""
100-
params = []
101-
102-
params += [FloatParameter(qualifier='rv_offset', copy_for={'kind': ['star'], 'component': '*'}, component='_default', value=kwargs.get('rv_offset', 0.0), default_unit=u.km/u.s, description='Per-component offset to add to synthetic RVs (i.e. for hot stars)')]
103-
104-
return ParameterSet(params), []
39+
_register_feature(Spot, 'spot')
40+
_register_feature(RVOffset, 'rv_offset')
10541

10642

10743
def gp_sklearn(feature, **kwargs):
@@ -270,13 +206,3 @@ def gp_celerite2(feature, **kwargs):
270206
constraints = []
271207

272208
return ParameterSet(params), constraints
273-
274-
def gaussian_process(feature, **kwargs):
275-
"""
276-
Deprecated (will be removed in PHOEBE 2.5)
277-
278-
Support for celerite gaussian processes has been removed. This is now an
279-
alias to <phoebe.parameters.feature.gp_celerite2>.
280-
"""
281-
logger.warning("gaussian_process is deprecated. Use gp_celerite2 instead.")
282-
return gp_celerite2(feature, **kwargs)

phoebe/solverbackends/solverbackends.py

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -246,22 +246,17 @@ def _get_combined_lc(b, datasets, combine, phase_component=None, mask=True, norm
246246
ds_sigmas = np.full_like(ds_fluxes, fill_value=0.001*ds_fluxes.mean())
247247

248248
# run dataset features attached to this dataset
249-
for feature in b.filter(context='feature', dataset=dataset, **_skip_filter_checks).features:
250-
feature_ps = b.get_feature(feature=feature, **_skip_filter_checks)
251-
if feature_ps.get_value(qualifier='feature_type', **_skip_filter_checks) != 'dataset':
252-
continue
253-
if 'custom_code' in feature_ps.qualifiers:
254-
feature_cls = feature_ps.get_value(qualifier='custom_code', **_skip_filter_checks)
255-
else:
256-
# TODO: import
257-
feature_cls = getattr(dataset_features, feature_ps.kind.title(), None)
258-
if feature_cls is None:
259-
raise ValueError("dataset feature '{}' not found".format(feature_ps.kind))
260-
modified_params = feature_cls.from_bundle(b, feature).modify_data_for_estimators(b, feature_ps,
261-
lc_ps,
262-
times=ds_times,
263-
fluxes=ds_fluxes,
264-
sigmas=ds_sigmas)
249+
# NOTE: enabled features are per-compute but estimators don't even have a compute
250+
# so instead we just include any that are enabled in ANY compute
251+
# TODO: copy enabled@feature for all estimators (but not other solvers)
252+
enabled_features = b.filter(qualifier='enabled', value=True, **_skip_filter_checks).features
253+
for feature in b.filter(context='feature', feature=enabled_features, dataset=dataset, **_skip_filter_checks).features:
254+
feature_obj = b.get_feature_code(feature=feature)
255+
modified_params = feature_obj.modify_data_for_estimators(b,
256+
lc_ps,
257+
times=ds_times,
258+
fluxes=ds_fluxes,
259+
sigmas=ds_sigmas)
265260
ds_times = modified_params.get('times', ds_times)
266261
ds_fluxes = modified_params.get('fluxes', ds_fluxes)
267262
ds_sigmas = modified_params.get('sigmas', ds_sigmas)

0 commit comments

Comments
 (0)