This interactive application simulates the orbit of two stars around each other, taking into account
to accurately model the orbit as well as the observable radial velocites and light curve of the binary star system as observed from Earth. Note: This code takes an approach for modelling binary stars outlined by Carroll and Ostlie in their TwoStars code, but it has been extensively optimized for the Python programming language.
Remember that the model shown on the left hand side of the two stars and their orbit is supposed to be just that, a model. It is not something we actually observe, but rather an explanation for what we observe. For example: We might observe a particular point of light in the sky varying in brightness in a regular manner. We try to explain why this point of light varies in brightness using a model that assumes it is due to two stars orbiting each other resulting in one star eclipsing the other. We may never have a telescope powerful enough to resolve that point of light into two stars, but we can explain its variation in brightness over time using the model.
# Developed by Juan Cabanela starting June 19, 2018 and proceeding through the Summer of 2018
#
# This simulation is meant to allow students to discover how adjusting the parameters of a model of a stellar system
# can allow use to model either radial velocity curves or light curves.
#
# This code started life as an extension of the Center of Mass interactive (by Sam Holen) but required the development
# of a Python implementation of a binary star model (based on approach used in the TwoStars code from
# Carrolll and Ostlie).
from IPython.display import display, HTML
import numpy as np
import ipywidgets as widgets
import traitlets
import pythreejs as p3j
import bqplot as bq
import tempNcolor as tc
import starlib as star
import number_formatting as nf
## FUNCTIONS ##
def ConfigBothStars(mass1, mass2):
'''
Determines the radii (in solar radii), temperature (in K), and hexcolor of the two stars assuming
they are main sequence stars and returns that information. Does this by calling the ConfigStar
function for both stars.
'''
(radius1, temp1, hexcolor1) = star.ConfigStar(mass1)
(radius2, temp2, hexcolor2) = star.ConfigStar(mass2)
return (radius1, temp1, hexcolor1, radius2, temp2, hexcolor2)
def MS_update(change=None):
'''
Did the user switch back and forth between main sequence stars and not main sequence stars?
'''
global mass1, mass2, radius1, radius2, temp1, temp2
MS_choice = change['new']
if (MS_choice == MS_yes):
radius1_slider.disabled = True
radius2_slider.disabled = True
temp1_slider.disabled = True
temp2_slider.disabled = True
radius1_readout.disabled = True
radius2_readout.disabled = True
temp1_readout.disabled = True
temp2_readout.disabled = True
else:
radius1_slider.disabled = False
radius2_slider.disabled = False
temp1_slider.disabled = False
temp2_slider.disabled = False
radius1_readout.disabled = False
radius2_readout.disabled = False
temp1_readout.disabled = False
temp2_readout.disabled = False
# Set the values of the radius and temperature sliders based on mass using
# a main sequence assumption (OK to do at transition between the two assumptions)
(radius1_slider.value, temp1_slider.value, hexcolor1,
radius2_slider.value, temp2_slider.value, hexcolor2) = ConfigBothStars(mass1_slider.value, mass2_slider.value)
# Update the orbital parameters if necessary
property_update()
def property_update(change=None):
'''
This function updates the stellar properties (and orbital properties) and is meant to be used
when there are changes to stellar properties as controled by various ipywidgets on screen.
'''
global bsm, mass1, mass2, radius1, radius2, temp1, temp2
# First determine if this is a main sequence star or not and adjust behavior
if (MS_selector.value == MS_yes):
bsm.continuous_update = False # Turn off continuous updating to allow quick changing of several variables
# determine radius and temperature of stars assuming main sequence realtionship
# to mass (changes should automatically propogate to the binary star model object)
(radius1_slider.value, temp1_slider.value, hexcolor1,
radius2_slider.value, temp2_slider.value, hexcolor2) = ConfigBothStars(mass1_slider.value, mass2_slider.value)
bsm.continuous_update = True # Turn on continuous updating
# Force a binary star model update (which should update view as well)
bsm.force_update()
# If there is a collision, stop the phase changes, otherwise allow access to changing phase
if (bsm.collision):
phase_title.value = phase_title_collision
phase_play.step = 0
phase_play.disabled = True
phase_slider.disabled = True
else:
# Make sure orbital phase can be adjusted
phase_title.value = phase_title_default
phase_play.step = 1
phase_play.disabled = False
phase_slider.disabled = False
# Set luminosities
L1_output.value = str(nf.SigFig((temp1_slider.value/star.Te_Sun)**4 * radius1_slider.value**2, 3))
L2_output.value = str(nf.SigFig((temp2_slider.value/star.Te_Sun)**4 * radius2_slider.value**2, 3))
# Revise orbital readouts
P_output.value = "{0:.1f}".format(bsm.P)
ap_output.value = "{0:.1f}".format(bsm.ap*AU2RSun)
aa_output.value = "{0:.1f}".format(bsm.aa*AU2RSun)
# Update the on screen label to indicate unreal scaling if necessary
if (binary_view.multiplier > 1):
sys_title.value = sys_title_scaling
else:
sys_title.value = sys_title_default
# Perform the inclination update to force an update of the display
inclination_update()
def inclination_update(change=None):
'''
This function updates the star system's inclination to the plane of the sky and updates the graph shown
'''
global bsm
# Retrieve updated radial velocity or light curves and then update figure
# (in OO version of this code, the bsm should have updated automatically following the inclination angle change)
if (fig_selector.value == rv_val):
update_radvel_curve(bsm.radvel_info, bsm.orbit_info)
else:
update_light_curve(bsm.lc_info)
def position_update(change=None):
'''
This function updates the two stars' phase line on whatever graph is shown. The viewer changes are handled
automatically by linking the slider to the t_idx value in the binary_view object.
'''
global binary_view
# Update the phase line in radial velocity or light curve
if (fig_selector.value == rv_val):
rv_phase_line.x = [bsm.radvel_info['phase'][phase_slider.value], bsm.radvel_info['phase'][phase_slider.value]]
else:
lc_phase_line.x = [bsm.lc_info['phase'][phase_slider.value], bsm.lc_info['phase'][phase_slider.value]]
def scene_update(change=None):
'''
This function updates the notificaiton of the grid/orbit drawing if necessary.
'''
binary_view.reset_grid_and_orbit()
def graph_update(change=None):
'''
This function switches which graph to plot
'''
global bsm, graph_fig
# Check if graph exists, if it doesn't, create it.
try:
graph_fig
except NameError:
graph_fig = None
if (fig_selector.value == rv_val):
bsm.wipe_lc_info()
bsm.set_radvel_info()
new_fig = create_radvel_curve(bsm.radvel_info, bsm.orbit_info)
else:
bsm.wipe_radvel_info()
bsm.set_lc_info()
new_fig = create_light_curve(bsm.lc_info)
if (graph_fig is None):
graph_fig = new_fig
else:
graph_fig.marks = new_fig.marks
graph_fig.axes = new_fig.axes
graph_fig.title = new_fig.title
graph_fig.layout = new_fig.layout
def create_light_curve(light_curve):
'''
Initialize the entire light curve
'''
global lc_line, lc_phase_line
# Set scales
sc_x = bq.LinearScale()
sc_y = bq.LinearScale()
# Build the light curve
lc_line = bq.Lines(x=light_curve['phase'], y=light_curve['F_norm'], scales={'x': sc_x, 'y': sc_y},
colors=['Black'])
# Indicate the current phase
x_phase = [light_curve['phase'][phase_slider.value], light_curve['phase'][phase_slider.value]]
y_phase = [0, 1]
lc_phase_line = bq.Lines(x=x_phase, y=y_phase, scales={'x': sc_x, 'y': sc_y},
colors=['Red'])
# Setup axes and return figure
ax_x = bq.Axis(scale=sc_x, label='Phase')
ax_y = bq.Axis(scale=sc_y, orientation='vertical', label='Fraction of Maximum Flux')
return bq.Figure(marks=[lc_line, lc_phase_line], axes=[ax_x, ax_y], title='Light Curve',
layout=widgets.Layout(width=graph_width, height=graph_height, margin='5px 5px 5px 5px'))
def update_light_curve(lc_info):
'''
Update the light curve
'''
global lc_line
lc_line.x=lc_info['phase']
lc_line.y=lc_info['F_norm']
def create_radvel_curve(radvel_info, orbit_info):
'''
Initialize the entire radial velocity curve
'''
global star1_line, star2_line, rv_phase_line
# Set up the scales
sc_x = bq.LinearScale()
sc_y = bq.LinearScale()
# Indicate the current phase
x_phase = [radvel_info['phase'][phase_slider.value], radvel_info['phase'][phase_slider.value]]
# Scale to min/max velocity at all inclinations (assume no systemic radial velocity)
maxval = max(-np.min(orbit_info['vx1']), -np.min(orbit_info['vx2']))
minval = min(-np.max(orbit_info['vx1']), -np.max(orbit_info['vx2']))
y_phase = [minval, maxval]
rv_phase_line = bq.Lines(x=x_phase, y=y_phase, scales={'x': sc_x, 'y': sc_y},
colors=['Red'])
# Draw the radial velocity curves
star1_line = bq.Lines(x=radvel_info['phase'], y=radvel_info['v1r'], scales={'x': sc_x, 'y': sc_y},
colors=['DarkOrange'], labels=['Star 1'], display_legend=True)
star2_line = bq.Lines(x=radvel_info['phase'], y=radvel_info['v2r'], scales={'x': sc_x, 'y': sc_y},
colors=['Blue'], labels=['Star 2'], display_legend=True)
# Setup axes and return (initially invisible) figure
ax_x = bq.Axis(scale=sc_x, label='Phase')
ax_y = bq.Axis(scale=sc_y, orientation='vertical', label='Radial velocity (km/s)')
ax_y.label_offset = '3.5em'
return bq.Figure(marks=[star1_line, star2_line, rv_phase_line], axes=[ax_x, ax_y], title='Radial Velocity Curve',
layout=widgets.Layout(width=graph_width, height=graph_height, margin='5px 5px 5px 5px'))
def update_radvel_curve(radvel_info, orbit_info):
'''
Update the radial velocity curve
'''
global star1_line, star2_line, rv_phase_line
star1_line.x=radvel_info['phase']
star1_line.y=radvel_info['v1r']
star2_line.x=radvel_info['phase']
star2_line.y=radvel_info['v2r']
# Rescale phase line limits if orbit changes
maxval = max(-np.min(orbit_info['vx1']), -np.min(orbit_info['vx2']))
minval = min(-np.max(orbit_info['vx1']), -np.max(orbit_info['vx2']))
rv_phase_line.y = [minval, maxval]
## INTERACTIVE/DISPLAY WIDGETS ##
# Conversion constants
RSun2AU = star.R_Sun/star.AU
AU2RSun = 1/RSun2AU
# Define constants
min_mass = 0.2 # Minimum stellar mass in solar masses
max_mass = 24 # Maximum stellar mass in solar masses
mass_step = 0.1 # Step size for mass sliders in solar masses
init_mass = 1 # Initial mass of both stars in solar masses
min_radius = 0.2 # Minimum stellar radius in solar radii
max_radius = 100 # Maximum stellar radius in solar radii
radius_step = 0.1 # Step size for radius sliders in solar radii
init_radius = 1 # Initial radius of both stars in solar radii
min_temp = 3100 # Minimum stellar temperature in K
max_temp = 40000 # Maximum stellar temperature in K
temp_step = 10 # Step size for temperature sliders in K
init_temp = int(star.Te_Sun/10)*10 # Initial temperature of both stars in K
min_a = 0.005 # Minimum semimajor axis of stars in AU
max_a = 1.5 # Maximum semimajor axis of stars in AU
step_a = 0.005 # Step size for separation slider in AU
init_a = 0.05 # Start off with the two stars close together
init_incl = 0.0 # Initial inclination value (non-zero value avoids odd orientation of FOV at start)
init_phi = 0 # Initial semimajor axis phase angle
init_ecc = 0.2 # Initial orbital eccentricity
view_factor = 1.25 # How many times the maximum distance to place the viewer
N = 1000 # Number of time steps to use for orbit
Na = 50 # Number of annuli to break up stars into for computing eclipse fraction
Ntheta = 180 # Number of angular steps to break up stars into for computing eclipse fraction
#
# Define some widths to use throughout for layout of controls
#
# Set simulation size
view_width = 350
view_height = view_width
# Initialize slider sizes
EntireWidth = '1000px'
SimWidth = '{0:.0f}px'.format(view_width)
ControlColWidth = '450px'
slider_width = '300px'
slider_minwidth = '250px'
readout_width = '70px'
lum_width = '120px'
inform_width = '200px'
graph_width = '450px'
graph_height = '300px'
##
## Create control for selecting Figure
##
rv_val = 'Radial Velocity Curve'
lc_val = 'Light Curve'
fig_selector = widgets.ToggleButtons(options=[rv_val, lc_val],
value=rv_val,
#description='Plot to Display:',
#style = {'description_width': 'initial'},
disabled=False,
orientation='horizontal',
layout=widgets.Layout(width=ControlColWidth,
height='70px',
overflow='visible')
)
##
## Create control for selecting main sequence or not
##
MS_yes = 'yes'
MS_yes_descript = 'Model stars as main sequence stars described solely by mass'
MS_no = 'no'
MS_no_descript = 'Allow non-main sequence stars to be modelled'
MS_selector = widgets.ToggleButtons(options=[MS_yes, MS_no],
value=MS_yes,
description='Main Sequence?',
style = {'description_width': 'initial'},
disabled=False,
button_style='',
tooltips=[MS_yes_descript, MS_no_descript],
layout=widgets.Layout(width=ControlColWidth,
height='70px',
flex_flow='row',
align_items='center',
align_contents='center',
overflow='visible')
)
##
##Create controls for stellar parameters
##
## Mass
mass1_slider = widgets.FloatSlider(
value=init_mass,
min=min_mass,
max=max_mass+(mass_step/2),
step=mass_step,
description="Star 1 mass",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible')
)
mass2_slider = widgets.FloatSlider(
value=init_mass,
min=min_mass,
max=max_mass+(mass_step/2),
step=mass_step,
description="Star 2 mass",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible')
)
# Define text boxes for readout
mass1_readout = widgets.BoundedFloatText(min=mass1_slider.min, max=mass1_slider.max,
value=mass1_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
mass2_readout = widgets.BoundedFloatText(min=mass2_slider.min, max=mass2_slider.max,
value=mass2_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
# Link slider and textboxes
widgets.jslink((mass1_readout, 'value'), (mass1_slider, 'value'))
widgets.jslink((mass2_readout, 'value'), (mass2_slider, 'value'))
# Create the individual controls for stellar masses
solar_mass = widgets.HTML('M<sub>☉</sub>')
mass1_cntl = widgets.HBox([mass1_slider, mass1_readout, solar_mass],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
mass2_cntl = widgets.HBox([mass2_slider, mass2_readout, solar_mass],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
## Radius
radius1_slider = widgets.FloatSlider(
value=init_radius,
min=min_radius,
max=max_radius+(radius_step/2),
step=radius_step,
description="Radius",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible')
)
radius2_slider = widgets.FloatSlider(
value=init_radius,
min=min_radius,
max=max_radius+(radius_step/2),
step=radius_step,
description="Radius",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible')
)
# Define text boxes for readout
radius1_readout = widgets.BoundedFloatText(min=radius1_slider.min, max=radius1_slider.max,
value=radius1_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
radius2_readout = widgets.BoundedFloatText(min=radius2_slider.min, max=radius2_slider.max,
value=radius2_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
# Link slider and textboxes
widgets.jslink((radius1_readout, 'value'), (radius1_slider, 'value'))
widgets.jslink((radius2_readout, 'value'), (radius2_slider, 'value'))
# Create the individual controls for stellar masses
solar_radius = widgets.HTML('R<sub>☉</sub>')
radius1_cntl = widgets.HBox([radius1_slider, radius1_readout, solar_radius],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
radius2_cntl = widgets.HBox([radius2_slider, radius2_readout, solar_radius],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
## Temperature
temp1_slider = widgets.IntSlider(
value=init_temp,
min=min_temp,
max=max_temp,
step=temp_step,
description="Temperature",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible')
)
temp2_slider = widgets.IntSlider(
value=init_temp,
min=min_temp,
max=max_temp,
step=temp_step,
description="Temperature",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible')
)
# Define text boxes for readout
temp1_readout = widgets.BoundedIntText(min=temp1_slider.min, max=temp1_slider.max,
value=temp1_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
temp2_readout = widgets.BoundedIntText(min=temp2_slider.min, max=temp2_slider.max,
value=temp2_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
# Link slider and textboxes
widgets.jslink((temp1_readout, 'value'), (temp1_slider, 'value'))
widgets.jslink((temp2_readout, 'value'), (temp2_slider, 'value'))
# Create the individual controls for stellar masses
Kelvin = widgets.Label('K')
temp1_cntl = widgets.HBox([temp1_slider, temp1_readout, Kelvin],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
temp2_cntl = widgets.HBox([temp2_slider, temp2_readout, Kelvin],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
##
##Create controls for system properties
##
# These sliders change entire orbital model and should NOT be continously updated
semimajor_slider = widgets.FloatSlider(value=init_a,
min=min_a,
max=max_a,
step=step_a,
description="Semimajor Axis",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.0f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible') )
ecc_slider = widgets.FloatSlider(value=init_ecc,
min=0,
max=0.8,
step=0.02,
description="Eccentricity",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.2f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible') )
phi_slider = widgets.FloatSlider(value=init_phi,
min=0,
max=180,
step=1,
description="Major Axis Longitude",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.0f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible') )
incl_slider = widgets.FloatSlider(value=init_incl,
min=0,
max=90,
step=1,
description="Inclination",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=False,
orientation='horizontal',
readout=False,
readout_format='.0f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible') )
# Define text boxes for readout
semimajor_readout = widgets.BoundedFloatText(min=semimajor_slider.min, max=semimajor_slider.max,
value=semimajor_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
ecc_readout = widgets.BoundedFloatText(min=ecc_slider.min, max=ecc_slider.max,
value=ecc_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
incl_readout = widgets.BoundedFloatText(min=incl_slider.min, max=incl_slider.max,
value=incl_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
phi_readout = widgets.BoundedFloatText(min=phi_slider.min, max=phi_slider.max,
value=phi_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
# Link slider and textboxes
widgets.jslink((semimajor_readout, 'value'), (semimajor_slider, 'value'))
widgets.jslink((ecc_readout, 'value'), (ecc_slider, 'value'))
widgets.jslink((incl_readout, 'value'), (incl_slider, 'value'))
widgets.jslink((phi_readout, 'value'), (phi_slider, 'value'))
# Create the individual controls for system properties
Solar_radius = widgets.HTML('R<sub>☉</sub>', layout=widgets.Layout(overflow='visible'))
AU_label = widgets.HTML('AU', layout=widgets.Layout(overflow='visible'))
deg_label = widgets.HTML('°', layout=widgets.Layout(overflow='visible'))
semimajor_cntl = widgets.HBox([semimajor_slider, semimajor_readout, AU_label],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
ecc_cntl = widgets.HBox([ecc_slider, ecc_readout],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
incl_cntl = widgets.HBox([incl_slider, incl_readout, deg_label],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
phi_cntl = widgets.HBox([phi_slider, phi_readout, deg_label],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
##
## Orbital playback controls
##
phase_slider = widgets.IntSlider(value=0,
min=0,
max=N,
step=1,
description="Phase",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=True,
orientation='horizontal',
readout=False,
readout_format='.0f',
layout=widgets.Layout(width=slider_width, min_width=slider_minwidth,
overflow='visible') )
phase_play = widgets.Play(interval = 1,
value = phase_slider.min,
min=phase_slider.min,
max=phase_slider.max,
step=2,
description="Press play",
disabled=False,
show_repeat=True,
layout=widgets.Layout(overflow='visible'))
widgets.jslink((phase_play, 'value'), (phase_slider, 'value'))
phase_cntl = widgets.HBox([phase_slider, phase_play],
layout=widgets.Layout(width=SimWidth,
overflow='visible'))
draw_grid = widgets.Checkbox(value=True,
description='Display orbital plane and orbital path',
disabled=False,
layout=widgets.Layout(width=ControlColWidth,
overflow='visible') )
##
## Create text boxes for reporting certain system parameters
##
P_output = widgets.Text(value = str(0),
description = 'Orbital Period (Days)',
style = {'description_width': 'initial'},
disabled = True,
layout=widgets.Layout(width=inform_width, height='40px',
overflow='visible'))
ap_output = widgets.Text(value = str(0),
description = 'Periastron (R<sub>☉</sub>)',
style = {'description_width': 'initial'},
disabled = True,
layout=widgets.Layout(width=inform_width,
overflow='visible'))
aa_output = widgets.Text(value = str(0),
description = 'Apastron ($R_\odot$)',
style = {'description_width': 'initial'},
disabled = True,
layout=widgets.Layout(width=inform_width,
overflow='visible'))
gridsep_output = widgets.Text(value = str(0),
description = 'Grid Spacing ($R_\odot$)',
style = {'description_width': 'initial'},
disabled = True,
layout=widgets.Layout(width=inform_width,
overflow='visible'))
luminosity = widgets.HTML('Luminosity (L<sub>☉</sub>) ')
L1_output = widgets.Text(value = str(0),
description = 'Star 1: ',
style = {'description_width': 'initial'},
disabled = True,
layout=widgets.Layout(width=lum_width,
overflow='visible'))
L2_output = widgets.Text(value = str(0),
description = 'Star 2: ',
style = {'description_width': 'initial'},
disabled = True,
layout=widgets.Layout(width=lum_width,
overflow='visible'))
lum_info = widgets.HBox([luminosity, L1_output, L2_output],
layout=widgets.Layout(height='40px', overflow='visible'))
# Initialize orbit control sliders
semimajor_slider.value = init_a
ecc_slider.value = init_ecc
incl_slider.value = init_incl
phi_slider.value = init_phi
# Initialize stellar mass sliders
mass1_slider.value = init_mass
mass2_slider.value = init_mass
# Set initial parameters based on stellar mass assuming main sequence stars
(radius1_slider.value, temp1_slider.value, hexcolor1,
radius2_slider.value, temp2_slider.value, hexcolor2) = ConfigBothStars(mass1_slider.value, mass2_slider.value)
# Set luminosities
L1_output.value = str(nf.SigFig((temp1_slider.value/star.Te_Sun)**4 * radius1_slider.value**2, 3))
L2_output.value = str(nf.SigFig((temp2_slider.value/star.Te_Sun)**4 * radius2_slider.value**2, 3))
# Initialize time index
t_idx = 0
# Build the initial Binary Star Model with radial velocity and light curves turned off
# and semimajor axis in solar radii
bsm = star.BinaryStarModel(mass1=mass1_slider.value,
mass2=mass2_slider.value,
rad1=radius1_slider.value,
rad2=radius2_slider.value,
temp1=temp1_slider.value,
temp2=temp2_slider.value,
a=semimajor_slider.value,
e=ecc_slider.value,
phi=phi_slider.value,
N=N,
Na=Na,
Ntheta=Ntheta,
rv_init=False,
lc_init=False,
a_in_AU=True)
# Convert units
P_output.value = "{0:.1f}".format(bsm.P)
ap_output.value = "{0:.1f}".format(bsm.ap*AU2RSun)
aa_output.value = "{0:.1f}".format(bsm.aa*AU2RSun)
##
## Set Up 3D Simulation and controls for left side
##
# creates the object that gets displayed to the screen (defaulting to showing orbital paths)
binary_view = star.BinaryStarViewer(bsm=bsm, t_idx0=t_idx, view_width=view_width,
view_height=view_height,
draw_grid=draw_grid.value,
draw_orbits=draw_grid.value)
binary_renderer = binary_view.renderer
#
# Construct left half controls
#
# Spacer widget
spacer = widgets.HTML('<p>', layout=widgets.Layout(width='10px', overflow='visible'))
# Create play button to control phase value automatically
phase_title_default = '<b>Controls for Orbital Motion</b>:'
phase_title_collision = '<B style="color:red">COLLISION DETECTED! CONTROLS DISABLED!</B>'
phase_title = widgets.HTML(phase_title_default)
phase_controls = widgets.VBox([phase_title, phase_cntl],
layout=widgets.Layout(width=SimWidth,
overflow='visible'))
# Creates System Parameter Controls (e.g. orbital property controls)
sys_title_default = '<b>System Parameters</b>:'
sys_title_scaling = '<b>System Parameters</b> (<B style="color:red">Stars\' sizes are NOT TO SCALE!</B>):'
if (binary_view.multiplier > 1):
sys_title = widgets.HTML(value=sys_title_scaling, layout=widgets.Layout(overflow='visible'))
else:
sys_title = widgets.HTML(value=sys_title_default, layout=widgets.Layout(overflow='visible'))
starorbit_controls = widgets.VBox([sys_title,
semimajor_cntl,
widgets.HBox([spacer, widgets.VBox([P_output])]),
ecc_cntl,
incl_cntl,
phi_cntl,
spacer],
layout=widgets.Layout(overflow='visible'))
# Assemble items in left column
sim_view = widgets.HBox([widgets.HTML('<h4>Model View</h4>'), binary_renderer],
layout=widgets.Layout(width=ControlColWidth,
flex_flow='column',
align_items='center',
align_contents='center',
overflow='visible'))
left_column = widgets.VBox([sim_view, draw_grid, phase_controls, starorbit_controls],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
##
## Build Controls on right side
##
# Compute light curve or radial velocity curve and select which to display initially
graph_update()
# Creates Stellar Mass Controls
star_title = widgets.HTML('<b>Stellar Properties</b>:', layout=widgets.Layout(overflow='hidden'))
mass1_title = widgets.HTML(value="<b>Star 1 Parameters</b>", layout=widgets.Layout(overflow='hidden'))
mass2_title = widgets.HTML(value="<b>Star 2 Parameters</b>", layout=widgets.Layout(overflow='hidden'))
star_controls = widgets.VBox([star_title, MS_selector,
mass1_title, mass1_cntl, radius1_cntl, temp1_cntl,
mass2_title, mass2_cntl, radius2_cntl, temp2_cntl,
widgets.HBox([spacer, lum_info])],
layout=widgets.Layout(overflow='visible'))
# Decide which controls to initially display depending on Main Sequence setting
if (MS_selector.value == MS_yes):
radius1_slider.disabled = True
radius2_slider.disabled = True
temp1_slider.disabled = True
temp2_slider.disabled = True
radius1_readout.disabled = True
radius2_readout.disabled = True
temp1_readout.disabled = True
temp2_readout.disabled = True
else:
radius1_slider.disabled = False
radius2_slider.disabled = False
temp1_slider.disabled = False
temp2_slider.disabled = False
radius1_readout.disabled = False
radius2_readout.disabled = False
temp1_readout.disabled = False
temp2_readout.disabled = False
# Creates a vertical box for the right control panel
graph_box = widgets.VBox([fig_selector, graph_fig],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible') )
right_column = widgets.VBox([graph_box, star_controls],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible') )
# Places the figure, sliders, and output into a Vbox. The figure is
# alone in the top, while the sliders and output are in a Hbox
# inside the bottom of the Vbox.
MainDisplay = widgets.HBox([left_column, spacer, right_column])
# Sets the dimensions of the box. Sets the entire width and the height of
# just the top.
MainDisplay.layout.width = EntireWidth
MainDisplay.layout.overflow = 'hidden'
display(MainDisplay)
##
## Turn on interactivity by linking sliders to functions changing the simulation.
##
# This control determines which graph to display
fig_selector.observe(graph_update, names=['value'])
# This control determines if we eassume main sequence or not
MS_selector.observe(MS_update, names=['value'])
# If mass changes, we also need to check for radius and temperature changes
mass1_link = traitlets.directional_link((mass1_slider, 'value'), (bsm, 'mass1'))
mass2_link = traitlets.directional_link((mass2_slider, 'value'), (bsm, 'mass2'))
# These sliders adjust stellar properties and make calls to binary star model and viewer to automatically
# adjust model (if needed) and view
radius1_link = traitlets.directional_link((radius1_slider, 'value'), (bsm, 'rad1'))
radius2_link = traitlets.directional_link((radius2_slider, 'value'), (bsm, 'rad2'))
temp1_link = traitlets.directional_link((temp1_slider, 'value'), (bsm, 'temp1'))
temp2_link = traitlets.directional_link((temp2_slider, 'value'), (bsm, 'temp2'))
# Handle changes in orbit directly in binary star model object
a_link = traitlets.directional_link((semimajor_slider, 'value'), (bsm, 'a'))
ecc_link = traitlets.directional_link((ecc_slider, 'value'), (bsm, 'e'))
phi_link = traitlets.directional_link((phi_slider, 'value'), (bsm, 'phi'))
# Call property_update if anything that potentially changes the orbit or luminosity is changed
mass1_slider.observe(property_update, names=['value'])
mass2_slider.observe(property_update, names=['value'])
radius1_slider.observe(property_update, names=['value'])
radius2_slider.observe(property_update, names=['value'])
temp1_slider.observe(property_update, names=['value'])
temp2_slider.observe(property_update, names=['value'])
semimajor_slider.observe(property_update, names=['value'])
ecc_slider.observe(property_update, names=['value'])
phi_slider.observe(property_update, names=['value'])
# Handle changes in inclination directly in binary star model and binary star viewer
incl_link = traitlets.directional_link((incl_slider, 'value'), (bsm, 'incl'))
incl_viewlink = traitlets.directional_link((incl_slider, 'value'), (binary_view, 'incl'))
incl_slider.observe(inclination_update, names=['value'])
# This slider just changes what phase of the orbit to display
phase_link = traitlets.directional_link((phase_slider, 'value'), (binary_view, 't_idx'))
phase_slider.observe(position_update, names=['value'])
# if Binary Star Model orbit model changes, trigger the Binary Star Viewer to change
orbit_change_link = traitlets.directional_link((bsm, 'mdl_counter'), (binary_view, 'mdl_counter'))
orbit_change_link = traitlets.directional_link((draw_grid, 'value'), (binary_view, 'draw_grid'))
orbit_change_link = traitlets.directional_link((draw_grid, 'value'), (binary_view, 'draw_orbits'))
draw_grid.observe(scene_update, names=['value'])