Constraints

Setup

Let's first make sure we have the latest version of PHOEBE 2.1 installed. (You can comment out this line if you don't use pip for your installation or don't want to update to the latest release).


In [ ]:
!pip install -I "phoebe>=2.1,<2.2"

As always, let's do imports and initialize a logger and a new Bundle. See Building a System for more details.


In [1]:
import phoebe
from phoebe import u # units
import numpy as np
import matplotlib.pyplot as plt

logger = phoebe.logger()

b = phoebe.default_binary()

What are Constraints?

Constraints live in their own context of the Bundle, and many are created by default - either when you add a component or when you set the system hierarchy.

Let's look at all the existing constraints for our binary system.


In [2]:
b.filter(context='constraint')


Out[2]:
<ParameterSet: 23 parameters | kinds: star, orbit>

To see what all of these constraints do, see the 'Built-in Constraints' section below.

For now let's look at a single constraint by accessing a ConstraintParameter.


In [3]:
b['constraint']['primary']['mass']


Out[3]:
<ConstraintParameter: {mass@primary@component} = (39.478418 * ({sma@binary@component} ** 3.000000)) / ((({period@binary@component} ** 2.000000) * (1.000000 + {q@binary@component})) * 0.000000000066740799999999990427) => 0.998813135806 solMass>

Here we see the equation used to derive the mass of the primary star from its orbit, as well as the current value

If we access the Parameter that it is constraining we can see that it is automatically kept up-to-date.


In [4]:
print b.get_value('mass@primary@component')


0.998813135806

The parameter is aware that it's being constrained and all the relevant linking parameters.


In [5]:
print b['mass@primary@component']


Parameter: mass@primary@component
                       Qualifier: mass
                     Description: Mass
                           Value: 0.998813135806 solMass
                  Constrained by: sma@binary@component, period@binary@component, q@binary@component
                      Constrains: None
                      Related to: sma@binary@component, period@binary@component, q@binary@component

If you change the hierarchy, built-in cross-object constraints (like mass that depends on its parent orbit), will be adjusted to reflect the new hierarchy. See the 'Changing Hierarchies' section below for more details.

Built-in Constraints

There are a number of built-in constraints that can be applied to our system. Those added by default are all listed below:

asini

This constraint handles computing the projected semi-major axis along the line of sight and can be automatically inverted to solve for either 'asini', 'sma', or 'incl'.


In [6]:
b['asini@constraint']


Out[6]:
<ConstraintParameter: {asini@binary@component} = {sma@binary@component} * (sin({incl@binary@component})) => 5.3 solRad>

esinw, ecosw

These constraints handle computing the projected eccentricity which can be helpful in that they are better representations of the geometry of a light curve and result in symmetric posteriors for near-circular orbits.

Both can be inverted to also automatically solve for 'ecc' or 'per0'.


In [7]:
b['esinw@constraint']


Out[7]:
<ConstraintParameter: {esinw@binary@component} = {ecc@binary@component} * (sin({per0@binary@component})) => 0.0>

In [8]:
b['ecosw@constraint']


Out[8]:
<ConstraintParameter: {ecosw@binary@component} = {ecc@binary@component} * (cos({per0@binary@component})) => 0.0>

t0

This constraint handles converting between different t0 conventions - namely providing a reference time at periastron passage (t0_perpass) and at superior conjunction (t0_supconj).

Currently, this constraint only supports inverting to be solved for 't0_supconj' (ie you cannot automatically invert this constraint to constraint phshift or per0).


In [9]:
b['t0_perpass@constraint']


Out[9]:
<ConstraintParameter: {t0_perpass@binary@component} = t0_supconj_to_perpass({t0_supconj@binary@component}, {period@binary@component}, {ecc@binary@component}, {per0@binary@component}) => -0.25 d>

freq

This constraint handles the simple conversion to frequency from period - whether that be rotational or orbital - and does support inversion to solve for 'period'.


In [10]:
b['freq@constraint']


Out[10]:
<ParameterSet: 3 parameters | kinds: star, orbit>

In [11]:
b['freq@binary@constraint']


Out[11]:
<ConstraintParameter: {freq@binary@component} = 6.283185 / {period@binary@component} => 6.283185 rad / d>

In [12]:
b['freq@primary@constraint']


Out[12]:
<ConstraintParameter: {freq@primary@component} = 6.283185 / {period@primary@component} => 6.283185 rad / d>

mass

This constraint handles solving for the mass of a component by obeying Kepler's third law within the parent orbit.

It can be inverted to solve for 'sma' or 'period' (in addition to 'mass'), but not 'q'.


In [13]:
b['mass@constraint']


Out[13]:
<ParameterSet: 2 parameters | components: primary, secondary>

In [14]:
b['mass@primary@constraint']


Out[14]:
<ConstraintParameter: {mass@primary@component} = (39.478418 * ({sma@binary@component} ** 3.000000)) / ((({period@binary@component} ** 2.000000) * (1.000000 + {q@binary@component})) * 0.000000000066740799999999990427) => 0.998813135806 solMass>

component sma

This constraint handles computing the semi-major axis of a component about the center of mass of its parent orbit. Note that this is not the same as the semi-major axis of the parent orbit.

This currently can be inverted to solve for 'sma' of the parent orbit, but not 'q'.


In [15]:
b['sma@constraint']


Out[15]:
<ParameterSet: 2 parameters | components: primary, secondary>

In [16]:
b['sma@primary@constraint']


Out[16]:
<ConstraintParameter: {sma@primary@component} = {sma@binary@component} / (1.000000 + (1.000000 / {q@binary@component})) => 2.65 solRad>

requiv_max

NEW IN PHOEBE 2.1

This constraint handles solving for the maxium equivalent radius (for a detached system).


In [17]:
b['requiv_max@constraint']


Out[17]:
<ParameterSet: 2 parameters | components: primary, secondary>

In [18]:
b['requiv_max@primary@constraint']


Out[18]:
<ConstraintParameter: {requiv_max@primary@component} = requiv_L1({q@binary@component}, {syncpar@primary@component}, {ecc@binary@component}, {sma@binary@component}, {incl@primary@component}, {long_an@primary@component}, {incl@binary@component}, {long_an@binary@component}, 1) => 2.01327517654 solRad>

rotation period

This constraint handles computing the rotation period of a star given its synchronicity parameter (syncpar).

It can be inverted to solve for any of the three parameters 'period' (both rotational and orbital) and 'syncpar'.


In [19]:
b['period@constraint']


Out[19]:
<ParameterSet: 2 parameters | components: primary, secondary>

In [20]:
b['period@primary@constraint']


Out[20]:
<ConstraintParameter: {period@primary@component} = {period@binary@component} / {syncpar@primary@component} => 1.0 d>

pitch/yaw (incl/long_an)

NEW IN PHOEBE 2.1

pitch constrains the relation between the orbital and rotational inclination whereas yaw constrains the relation between the orbital and rotational long_an. When pitch and yaw are set to 0, the system is aligned.


In [23]:
b['incl@constraint']


Out[23]:
<ParameterSet: 2 parameters | components: primary, secondary>

In [24]:
b['incl@primary@constraint']


Out[24]:
<ConstraintParameter: {incl@primary@component} = {incl@binary@component} + {pitch@primary@component} => 90.0 deg>

In [25]:
b['long_an@constraint']


Out[25]:
<ParameterSet: 2 parameters | components: primary, secondary>

In [26]:
b['long_an@primary@constraint']


Out[26]:
<ConstraintParameter: {long_an@primary@component} = {long_an@binary@component} + {yaw@primary@component} => 0.0 deg>

Re-Parameterizing

NOTE: this is an experimental feature. When re-parameterizing, please be careful and make sure all results and parameters make sense.

As we've just seen, the mass is a constrained (ie derived) parameter. But let's say that you would rather provide masses for some reason (perhaps that was what was provided in a paper). You can choose to provide mass and instead have one of its related parameters constrained by calling flip_constraint.


In [27]:
print b['mass@primary@component'].constrained_by


[<Parameter: sma=5.3 solRad | keys: description, value, quantity, default_unit, limits, visible_if, copy_for>, <Parameter: period=1.0 d | keys: description, value, quantity, default_unit, limits, visible_if, copy_for>, <Parameter: q=1.0 | keys: description, value, quantity, default_unit, limits, visible_if, copy_for>]

In [28]:
print b['value@mass@primary@component'], b['value@mass@secondary@component'], b['value@period@orbit@component']


0.998813135806 0.998813135806 1.0

In [29]:
b.flip_constraint('mass@primary', 'period')


Out[29]:
<ConstraintParameter: {period@binary@component} = ((39.478418 * ({sma@binary@component} ** 3.000000)) / (({mass@primary@component} * (1.000000 + {q@binary@component})) * 0.000000000066740799999999990427)) ** 0.500000 => 1.0 d>

In [30]:
b['mass@primary@component'] = 1.0

In [31]:
print b['value@mass@primary@component'], b['value@mass@secondary@component'], b['value@period@orbit@component']


1.0 1.0 0.999406391718

You'll see that when we set the primary mass, the secondary mass has also changed (because the masses are related through q) and the period has changed (based on resolving the Kepler's third law constraint).

Note that the tags for the constraint are based on those of the constrained parameter, so to switch the parameterization back, we'll have to use a slightly different twig.


In [32]:
print b['constraint']


ParameterSet: 23 parameters
         freq@primary@constraint: 6.283185 / {period@primary@component}
  irrad_frac_lost_bol@primary...: 1.000000 - {irrad_frac_refl_bol@primary@component}
       freq@secondary@constraint: 6.283185 / {period@secondary@component}
  irrad_frac_lost_bol@seconda...: 1.000000 - {irrad_frac_refl_bol@secondary@component}
         asini@binary@constraint: {sma@binary@component} * (sin({incl@binary@component}))
    t0_perpass@binary@constraint: t0_supconj_to_perpass({t0_supconj@binary@component}, {period@binary@component}, {ecc@binary@component}, {per0@binary@component})
        t0_ref@binary@constraint: t0_supconj_to_ref({t0_supconj@binary@component}, {period@binary@component}, {ecc@binary@component}, {per0@binary@component})
     mean_anom@binary@constraint: (6.283185 * ({t0@system} - {t0_perpass@binary@component})) / {period@binary@component}
         ecosw@binary@constraint: {ecc@binary@component} * (cos({per0@binary@component}))
         esinw@binary@constraint: {ecc@binary@component} * (sin({per0@binary@component}))
          freq@binary@constraint: 6.283185 / {period@binary@component}
        period@binary@constraint: ((39.478418 * ({sma@binary@component} ** 3.000000)) / (({mass@primary@component} * (1.000000 + {q@binary@component})) * 0.000000000066740799999999990427)) ** 0.500000
          sma@primary@constraint: {sma@binary@component} / (1.000000 + (1.000000 / {q@binary@component}))
       period@primary@constraint: {period@binary@component} / {syncpar@primary@component}
   requiv_max@primary@constraint: requiv_L1({q@binary@component}, {syncpar@primary@component}, {ecc@binary@component}, {sma@binary@component}, {incl@primary@component}, {long_an@primary@component}, {incl@binary@component}, {long_an@binary@component}, 1)
         incl@primary@constraint: {incl@binary@component} + {pitch@primary@component}
      long_an@primary@constraint: {long_an@binary@component} + {yaw@primary@component}
       mass@secondary@constraint: (39.478418 * ({sma@binary@component} ** 3.000000)) / ((({period@binary@component} ** 2.000000) * (1.000000 + (1.000000 / {q@binary@component}))) * 0.000000000066740799999999990427)
        sma@secondary@constraint: {sma@binary@component} / (1.000000 + {q@binary@component})
     period@secondary@constraint: {period@binary@component} / {syncpar@secondary@component}
  requiv_max@secondary@constr...: requiv_L1({q@binary@component}, {syncpar@secondary@component}, {ecc@binary@component}, {sma@binary@component}, {incl@secondary@component}, {long_an@secondary@component}, {incl@binary@component}, {long_an@binary@component}, 2)
       incl@secondary@constraint: {incl@binary@component} + {pitch@secondary@component}
    long_an@secondary@constraint: {long_an@binary@component} + {yaw@secondary@component}

In [33]:
b['period@constraint@binary']


Out[33]:
<ConstraintParameter: {period@binary@component} = ((39.478418 * ({sma@binary@component} ** 3.000000)) / (({mass@primary@component} * (1.000000 + {q@binary@component})) * 0.000000000066740799999999990427)) ** 0.500000 => 0.999406391718 d>

In [34]:
b['period@constraint@binary'].meta


Out[34]:
OrderedDict([('time', None),
             ('qualifier', 'period'),
             ('history', None),
             ('feature', None),
             ('component', 'binary'),
             ('dataset', None),
             ('constraint', None),
             ('compute', None),
             ('model', None),
             ('fitting', None),
             ('feedback', None),
             ('plugin', None),
             ('kind', 'orbit'),
             ('context', 'constraint'),
             ('twig', 'period@binary@orbit@constraint'),
             ('uniquetwig', 'period@binary@constraint')])

Notice that the qualifier tag has changed from 'mass' to 'period' and the 'component' tag has changed from 'primary' to 'binary' (since sma is in the binary).


In [35]:
b.flip_constraint('period@binary', 'mass')


Out[35]:
<ConstraintParameter: {mass@primary@component} = (39.478418 * ({sma@binary@component} ** 3.000000)) / ((({period@binary@component} ** 2.000000) * (1.000000 + {q@binary@component})) * 0.000000000066740799999999990427) => 1.0 solMass>

Changing Hierarchies

Some of the built-in constraints depend on the system hierarchy, and will automatically adjust to reflect changes to the hierarchy.

For example, the masses depend on the period and semi-major axis of the parent orbit but also depend on the mass-ratio (q) which is defined as the primary mass over secondary mass. For this reason, changing the roles of the primary and secondary components should be reflected in the masses (so long as q remains fixed).

In order to show this example, let's set the mass-ratio to be non-unity.


In [36]:
b.set_value('q', 0.8)

Here the star with component tag 'primary' is actually the primary component in the hierarchy, so should have the LARGER mass (for a q < 1.0).


In [37]:
print "M1: {}, M2: {}".format(b.get_value('mass@primary@component'),
                              b.get_value('mass@secondary@component'))


M1: 1.11111111111, M2: 0.888888888889

Now let's flip the hierarchy so that the star with the 'primary' component tag is actually the secondary component in the system (and so takes the role of numerator in q = M2/M1).

For more information on the syntax for setting hierarchies, see the Building a System Tutorial.


In [38]:
b.set_hierarchy('orbit:binary(star:secondary, star:primary)')

In [39]:
print b.get_value('q')


0.8

In [40]:
print "M1: {}, M2: {}".format(b.get_value('mass@primary@component'),
                              b.get_value('mass@secondary@component'))


M1: 0.888888888889, M2: 1.11111111111

Even though under-the-hood the constraints are being rebuilt from scratch, they will remember if you have flipped them to solve for some other parameter.

To show this, let's flip the constraint for the secondary mass to solve for 'period' and then change the hierarchy back to its original value.


In [41]:
print "M1: {}, M2: {}, period: {}, q: {}".format(b.get_value('mass@primary@component'),
                                                 b.get_value('mass@secondary@component'),
                                                 b.get_value('period@binary@component'),
                                                 b.get_value('q@binary@component'))


M1: 0.888888888889, M2: 1.11111111111, period: 0.999406391718, q: 0.8

In [42]:
b.flip_constraint('mass@secondary@constraint', 'period')


Out[42]:
<ConstraintParameter: {period@binary@component} = ((39.478418 * ({sma@binary@component} ** 3.000000)) / (({mass@secondary@component} * (1.000000 + {q@binary@component})) * 0.000000000066740799999999990427)) ** 0.500000 => 0.999406391718 d>

In [43]:
print "M1: {}, M2: {}, period: {}, q: {}".format(b.get_value('mass@primary@component'),
                                                 b.get_value('mass@secondary@component'),
                                                 b.get_value('period@binary@component'),
                                                 b.get_value('q@binary@component'))


M1: 0.888888888889, M2: 1.11111111111, period: 0.999406391718, q: 0.8

In [44]:
b.set_value('mass@secondary@component', 1.0)

In [45]:
print "M1: {}, M2: {}, period: {}, q: {}".format(b.get_value('mass@primary@component'),
                                                 b.get_value('mass@secondary@component'),
                                                 b.get_value('period@binary@component'),
                                                 b.get_value('q@binary@component'))


M1: 0.8, M2: 1.0, period: 1.05346683532, q: 0.8

Next

Next up: let's add a dataset to our Bundle.


In [ ]: