General Concepts: The PHOEBE Bundle

HOW TO RUN THIS FILE: if you're running this in a Jupyter notebook or Google Colab session, you can click on a cell and then shift+Enter to run the cell and automatically select the next cell. Alt+Enter will run a cell and create a new cell below it. Ctrl+Enter will run a cell but keep it selected. To restart from scratch, restart the kernel/runtime.

All of these tutorials assume basic comfort with Python in general - particularly with the concepts of lists, dictionaries, and objects as well as basic comfort with using the numpy and matplotlib packages. This tutorial introduces all the general concepts of accessing parameters within the Bundle.

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 [1]:
#!pip install -I "phoebe>=2.3,<2.4"

Let's get started with some basic imports:


In [2]:
import phoebe
from phoebe import u # units

If running in IPython notebooks, you may see a "ShimWarning" depending on the version of Jupyter you are using - this is safe to ignore.

PHOEBE 2 uses constants defined in the IAU 2015 Resolution which conflict with the constants defined in astropy. As a result, you'll see the warnings as phoebe.u and phoebe.c "hijacks" the values in astropy.units and astropy.constants.

Whenever providing units, please make sure to use phoebe.u instead of astropy.units, otherwise the conversions may be inconsistent.

Logger

Before starting any script, it is a good habit to initialize a logger and define which levels of information you want printed to the command line (clevel) and dumped to a file (flevel). A convenience function is provided at the top-level via phoebe.logger to initialize the logger with any desired level.

The levels from most to least information are:

  • DEBUG
  • INFO
  • WARNING
  • ERROR
  • CRITICAL

In [3]:
logger = phoebe.logger(clevel='WARNING')

All of these arguments are optional and will default to clevel='WARNING' if not provided. There is therefore no need to provide a filename if you don't provide a value for flevel.

So with this logger, anything with INFO, WARNING, ERROR, or CRITICAL levels will be printed to the screen. All messages of any level will be written to a file named 'tutorial.log' in the current directory.

Note: the logger messages are not included in the outputs shown below.

Overview

As a quick overview of what's to come, here is a quick preview of some of the steps used when modeling a binary system with PHOEBE. Each of these steps will be explained in more detail throughout these tutorials.

First we need to create our binary system. For the sake of most of these tutorials, we'll use the default detached binary available through the phoebe.default_binary constructor.


In [4]:
b = phoebe.default_binary()

This object holds all the parameters and their respective values. We'll see in this tutorial and the next tutorial on constraints how to search through these parameters and set their values.


In [5]:
b.set_value(qualifier='teff', component='primary', value=6500)

Next, we need to define our datasets via b.add_dataset. This will be the topic of the following tutorial on datasets.


In [6]:
b.add_dataset('lc', compute_times=phoebe.linspace(0,1,101))


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

We'll then want to run our forward model to create a synthetic model of the observables defined by these datasets using b.run_compute, which will be the topic of the computing observables tutorial.


In [7]:
b.run_compute()


Out[7]:
<ParameterSet: 3 parameters | qualifiers: fluxes, times, comments>

We can then plot the resulting model with b.plot, which will be covered in the plotting tutorial.


In [8]:
afig, mplfig = b.plot(show=True)


And then lastly, if we wanted to solve the inverse problem and "fit" parameters to observational data, we may want to add distributions to our system so that we can run estimators, optimizers, or samplers.

Default Binary Bundle

For this tutorial, let's start over and discuss this b object in more detail and how to access and change the values of the input parameters.

Everything for our system will be stored in this single Python object that we call the Bundle which we'll call b (short for bundle).


In [9]:
b = phoebe.default_binary()

The Bundle is just a collection of Parameter objects along with some callable methods. Here we can see that the default binary Bundle consists of over 100 individual parameters.


In [10]:
b


Out[10]:
<PHOEBE Bundle: 125 parameters | contexts: component, setting, figure, compute, system, constraint>

If we want to view or edit a Parameter in the Bundle, we first need to know how to access it. Each Parameter object has a number of tags which can be used to filter (similar to a database query). When filtering the Bundle, a ParameterSet is returned - this is essentially just a subset of the Parameters in the Bundle and can be further filtered until eventually accessing a single Parameter.


In [11]:
b.filter(context='compute')


Out[11]:
<ParameterSet: 16 parameters | components: primary, secondary>

Here we filtered on the context tag for all Parameters with context='compute' (i.e. the options for computing a model). If we want to see all the available options for this tag in the Bundle, we can use the plural form of the tag as a property on the Bundle or any ParameterSet.


In [12]:
b.contexts


Out[12]:
['system', 'component', 'constraint', 'compute', 'figure', 'setting']

Although there is no strict hierarchy or order to the tags, it can be helpful to think of the context tag as the top-level tag and is often very helpful to filter by the appropriate context first.

Other tags currently include:

  • kind
  • figure
  • component
  • feature
  • dataset
  • distribution
  • compute
  • model
  • solver
  • solution
  • time
  • qualifier

Accessing the plural form of the tag as an attribute also works on a filtered ParameterSet


In [13]:
b.filter(context='compute').components


Out[13]:
['primary', 'secondary']

This then tells us what can be used to filter further.


In [14]:
b.filter(context='compute').filter(component='primary')


Out[14]:
<ParameterSet: 4 parameters | qualifiers: distortion_method, mesh_method, atm, ntriangles>

The qualifier tag is the shorthand name of the Parameter itself. If you don't know what you're looking for, it is often useful to list all the qualifiers of the Bundle or a given ParameterSet.


In [15]:
b.filter(context='compute', component='primary').qualifiers


Out[15]:
['mesh_method', 'ntriangles', 'distortion_method', 'atm']

Now that we know the options for the qualifier within this filter, we can choose to filter on one of those. Let's look filter by the 'ntriangles' qualifier.


In [16]:
b.filter(context='compute', component='primary', qualifier='ntriangles')


Out[16]:
<ParameterSet: 1 parameters>

Once we filter far enough to get to a single Parameter, we can use get_parameter to return the Parameter object itself (instead of a ParameterSet).


In [17]:
b.filter(context='compute', component='primary', qualifier='ntriangles').get_parameter()


Out[17]:
<Parameter: ntriangles=1500 | keys: description, value, limits, visible_if, copy_for, readonly, advanced>

As a shortcut, get_parameter also takes filtering keywords. So the above line is also equivalent to the following:


In [18]:
b.get_parameter(context='compute', component='primary', qualifier='ntriangles')


Out[18]:
<Parameter: ntriangles=1500 | keys: description, value, limits, visible_if, copy_for, readonly, advanced>

Each Parameter object contains several keys that provide information about that Parameter. The keys "description" and "value" are always included, with additional keys available depending on the type of Parameter.


In [19]:
b.get_parameter(context='compute', component='primary', qualifier='ntriangles').get_value()


Out[19]:
1500

In [20]:
b.get_parameter(context='compute', component='primary', qualifier='ntriangles').get_description()


Out[20]:
"Requested number of triangles (won't be exact)."

Since the Parameter for ntriangles is a FloatParameter, it also includes a key for the allowable limits.


In [21]:
b.get_parameter(context='compute', component='primary', qualifier='ntriangles').get_limits()


Out[21]:
[100, None]

In this case, we're looking at the Parameter called ntriangles with the component tag set to 'primary'. This Parameter therefore defines how many triangles should be created when creating the mesh for the star named 'primary'. By default, this is set to 1500 triangles, with allowable values above 100.

If we wanted a finer mesh, we could change the value.


In [22]:
b.get_parameter(context='compute', component='primary', qualifier='ntriangles').set_value(2000)

In [23]:
b.get_parameter(context='compute', component='primary', qualifier='ntriangles')


Out[23]:
<Parameter: ntriangles=2000 | keys: description, value, limits, visible_if, copy_for, readonly, advanced>

If we choose the distortion_method qualifier from that same ParameterSet, we'll see that it has a few different keys in addition to description and value.


In [24]:
b.get_parameter(context='compute', component='primary', qualifier='distortion_method')


Out[24]:
<Parameter: distortion_method=roche | keys: description, choices, value, visible_if, copy_for, readonly, advanced>

In [25]:
b.get_parameter(context='compute', component='primary', qualifier='distortion_method').get_value()


Out[25]:
'roche'

In [26]:
b.get_parameter(context='compute', component='primary', qualifier='distortion_method').get_description()


Out[26]:
'Method to use for distorting stars'

Since the distortion_method Parameter is a ChoiceParameter, it contains a key for the allowable choices.


In [27]:
b.get_parameter(context='compute', component='primary', qualifier='distortion_method').get_choices()


Out[27]:
['roche', 'rotstar', 'sphere', 'none']

We can only set a value if it is contained within this list - if you attempt to set a non-valid value, an error will be raised.


In [28]:
try:
    b.get_parameter(context='compute', component='primary', qualifier='distortion_method').set_value('blah')
except Exception as e:
    print(e)


value for distortion_method@primary@phoebe01@compute must be one of ['roche', 'rotstar', 'sphere', 'none'], not 'blah'

In [29]:
b.get_parameter(context='compute', component='primary', qualifier='distortion_method').set_value('rotstar')

In [30]:
b.get_parameter(context='compute', component='primary', qualifier='distortion_method').get_value()


Out[30]:
'rotstar'

Twigs

As a shortcut to needing to filter by all these tags, the Bundle and ParameterSets can be filtered through what we call "twigs" (as in a Bundle of twigs). These are essentially a single string-representation of the tags, separated by @ symbols.

This is very useful as a shorthand when working in an interactive Python console, but somewhat obfuscates the names of the tags and can make it difficult if you use them in a script and make changes earlier in the script.

For example, the following lines give identical results:


In [31]:
b.filter(context='compute', component='primary')


Out[31]:
<ParameterSet: 4 parameters | qualifiers: distortion_method, mesh_method, atm, ntriangles>

In [32]:
b['primary@compute']


Out[32]:
<ParameterSet: 4 parameters | qualifiers: distortion_method, mesh_method, atm, ntriangles>

In [33]:
b['compute@primary']


Out[33]:
<ParameterSet: 4 parameters | qualifiers: distortion_method, mesh_method, atm, ntriangles>

However, this dictionary-style twig access will never return a ParameterSet with a single Parameter, instead it will return the Parameter itself. This can be seen in the different output between the following two lines:


In [34]:
b.filter(context='compute', component='primary', qualifier='distortion_method')


Out[34]:
<ParameterSet: 1 parameters>

In [35]:
b['distortion_method@primary@compute']


Out[35]:
<Parameter: distortion_method=rotstar | keys: description, choices, value, visible_if, copy_for, readonly, advanced>

Because of this, this dictionary-style twig access can also set the value directly:


In [36]:
b['distortion_method@primary@compute'] = 'roche'

In [37]:
print(b['distortion_method@primary@compute'])


Parameter: distortion_method@primary@phoebe01@compute
                       Qualifier: distortion_method
                     Description: Method to use for distorting stars
                           Value: roche
                         Choices: roche, rotstar, sphere, none
                  Constrained by: 
                      Constrains: None
                      Related to: None
                 Only visible if: mesh_method:marching,hierarchy.is_meshable:true

And can even provide direct access to the keys/attributes of the Parameter (value, description, limits, etc)


In [38]:
print(b['value@distortion_method@primary@compute'])


roche

In [39]:
print(b['description@distortion_method@primary@compute'])


Method to use for distorting stars

As with the tags, you can call .twigs on any ParameterSet to see the "smallest unique twigs" of the contained Parameters


In [40]:
b['compute'].twigs


Out[40]:
['sample_from@phoebe01@phoebe@compute',
 'comments@phoebe01@phoebe@compute',
 'dynamics_method@phoebe01@phoebe@compute',
 'ltte@phoebe01@phoebe@compute',
 'irrad_method@phoebe01@phoebe@compute',
 'boosting_method@phoebe01@phoebe@compute',
 'eclipse_method@phoebe01@phoebe@compute',
 'horizon_method@phoebe01@phoebe@compute',
 'mesh_method@primary@phoebe01@phoebe@compute',
 'mesh_method@secondary@phoebe01@phoebe@compute',
 'ntriangles@primary@phoebe01@phoebe@compute',
 'ntriangles@secondary@phoebe01@phoebe@compute',
 'distortion_method@primary@phoebe01@phoebe@compute',
 'distortion_method@secondary@phoebe01@phoebe@compute',
 'atm@primary@phoebe01@phoebe@compute',
 'atm@secondary@phoebe01@phoebe@compute']

Since the more verbose method without twigs is a bit clearer to read, most of the tutorials will show that syntax, but feel free to use twigs if they make more sense to you.