Distributions

Distributions are mostly useful when using samplers (which we'll see in the next tutorial on solving the inverse problem) - but can also be useful to propagate any set of distributions (whether those be uncertainties in the literature, etc) through the forward model.

Setup

Let's first make sure we have the latest version of PHOEBE 2.3 installed (uncomment this line if running in an online notebook session such as colab).


In [ ]:
#!pip install -I "phoebe>=2.3,<2.4"

In [1]:
%matplotlib inline

In [4]:
import phoebe

logger = phoebe.logger()

b = phoebe.default_binary()
b.add_dataset('lc', compute_phases=phoebe.linspace(0,1,101))


Out[4]:
<ParameterSet: 42 parameters | contexts: compute, figure, dataset, constraint>

Adding Distributions

Distributions can be attached to most any FloatParameter in the Bundle. To see a list of these available parameters, we can call b.get_adjustable_parameters. Note the exclude_constrained option which defaults to True: we can set distributions on constrained parameters (for priors, for example), but those will not be able to be sampled from in the forward model or while fitting. We'll come back to this in the next tutorial when looking at priors.


In [5]:
b.get_adjustable_parameters()


Out[5]:
<ParameterSet: 36 parameters | contexts: component, dataset, system>

add_distribution is quite flexible and accepts several different syntaxes to add multiple distributions in one line. Here we'll just attach a distribution to a single parameter at a time. Just like when calling add_dataset or add_compute, add_distribution optionally takes a distribution tag -- and in the cases of distributions, we can attach distributions to multiple parameters with the same distribution tag.

The values of the DistributionParameters are distl distribution objects -- the most common of which are conveniently available at the top-level of PHOEBE:

Now let's attach a gaussian distribution on the temperature of the primary star.


In [6]:
b.add_distribution(qualifier='teff', component='primary', 
                   value=phoebe.gaussian(6000,100), 
                   distribution='mydist')


Out[6]:
<ParameterSet: 1 parameters>

As you probably can expect by now, we also have methods to:


In [7]:
print(b.get_distribution(distribution='mydist'))


ParameterSet: 1 parameters
         teff@mydist@distribution: <distl.gaussian loc=6000.0 scale=100.0 unit=K>

Now let's add another distribution, with the same distribution tag, to the inclination of the binary.


In [8]:
b.add_distribution(qualifier='incl', component='binary',
                   value=phoebe.uniform(80,90),
                   distribution='mydist')


Out[8]:
<ParameterSet: 1 parameters>

In [9]:
print(b.get_distribution(distribution='mydist'))


ParameterSet: 2 parameters
         teff@mydist@distribution: <distl.gaussian loc=6000.0 scale=100.0 unit=K>
         incl@mydist@distribution: <distl.uniform low=80.0 high=90.0 unit=deg>

Accessing & Plotting Distributions

The parameters we've created and attached are DistributionParameters and live in context='distribution', with all other tags matching the parameter they're referencing. For example, let's filter and look at the distributions we've added.


In [10]:
print(b.filter(context='distribution'))


ParameterSet: 2 parameters
         teff@mydist@distribution: <distl.gaussian loc=6000.0 scale=100.0 unit=K>
         incl@mydist@distribution: <distl.uniform low=80.0 high=90.0 unit=deg>

In [11]:
print(b.get_parameter(context='distribution', qualifier='incl'))


Parameter: incl@mydist@distribution
                       Qualifier: incl
                     Description: distribution for the referenced parameter
                           Value: <distl.uniform low=80.0 high=90.0 unit=deg>
                  Constrained by: 
                      Constrains: None
                      Related to: None


In [12]:
print(b.get_parameter(context='distribution', qualifier='incl').tags)


OrderedDict([('time', None), ('qualifier', 'incl'), ('feature', None), ('component', 'binary'), ('dataset', None), ('constraint', None), ('distribution', 'mydist'), ('compute', None), ('model', None), ('solver', None), ('solution', None), ('figure', None), ('kind', None), ('context', 'distribution')])

The "value" of the parameter, is the distl distributon object itself.


In [14]:
b.get_value(context='distribution', qualifier='incl')


Out[14]:
<distl.uniform low=80.0 high=90.0 unit=deg>

And because of that, we can call any method on the distl object, including plotting the distribution.


In [17]:
_ = b.get_value(context='distribution', qualifier='incl').plot(show=True)


If we want to see how multiple individual distributions interact and are correlated with each other via a corner plot, we can access the combined "distribution collection" from any number of distribution tags. This may not be terribly useful now, but is very useful when trying to create multivariate priors.


In [18]:
_ = b.plot_distribution_collection(distribution='mydist', show=True)


Sampling Distributions

We can also sample from these distributions - either manually by calling sample on the distl or in bulk by respecting any covariances in the "distributon collection" via:


In [19]:
b.sample_distribution_collection(distribution='mydist')


Out[19]:
{'teff@primary@star@component': 5920.785917792791,
 'incl@binary@orbit@component': 80.37969107107588}

By default this just returns a dictionary with the twigs and sampled values. But if we wanted, we could have these applied immediately to the face-values by passing set_value=True, in which case a ParameterSet of changed parameters (including those via constraints) is returned instead.


In [21]:
changed_params = b.sample_distribution_collection(distribution='mydist', set_value=True)

In [22]:
print(changed_params)


ParameterSet: 9 parameters
           teff@primary@component: 5914.066714070324 K
            incl@binary@component: 81.8035460310878 deg
C          asini@binary@component: 5.245860798417423 solRad
C          incl@primary@component: 81.8035460310878 deg
C    requiv_max@primary@component: 2.013275176537638 solRad
C        incl@secondary@component: 81.8035460310878 deg
C  requiv_max@secondary@component: 2.013275176537638 solRad
C    requiv_max@primary@component: 2.013275176537638 solRad
C  requiv_max@secondary@component: 2.013275176537638 solRad

Propagating Distributions through Forward Model

Lastly, we can have PHOEBE automatically draw from a "distribution collection" multiple times and expose the distribution of the model itself.


In [24]:
print(b.get_parameter(qualifier='sample_from', context='compute'))


Parameter: sample_from@phoebe01@compute
                       Qualifier: sample_from
                     Description: distributions or solutions to use for sampling.  If pointing to a solution, adopt_solution(as_distributions=True, **kwargs) will be called to create a temporary distribution which will be removed automatically.  If all distributions are delta functions (face-values), sample_mode and sample_num will be ignored with a warning.
                           Value: []
                         Choices: mydist
                  Constrained by: 
                      Constrains: None
                      Related to: None

Once sample_from is set, sample_num and sample_mode are exposed as visible parameters


In [25]:
b.set_value('sample_from', value='mydist')

In [26]:
print(b.filter(qualifier='sample*'))


ParameterSet: 4 parameters
     sample_from@phoebe01@compute: ['mydist']
   sample_from_combine@phoebe0...: first
      sample_num@phoebe01@compute: 10
     sample_mode@phoebe01@compute: 1-sigma

Now when we call run_compute, 10 different instances of the forward model will be computed from 10 random draws from the "distribution collection" but only the median and 1-sigma uncertainties will be exposed in the model.


In [27]:
b.run_compute(irrad_method='none')


Out[27]:
<ParameterSet: 8 parameters | qualifiers: samples, sampled_twigs, failed_samples, comments, sample_mode, sampled_uniqueids, times, fluxes>

In [28]:
_ = b.plot(show=True)


Next

Next up: let's learn about solving the inverse problem