This interactive contains a simple visualization of a binary star system. It allows you to vary the masses and separations of the stars and see what effect that has on the center of mass of the binary system. The center of mass of this binary star system is marked with a small yellow marker.
# Originally developed using bqplot by Sam Holen in late May 2018.
# pythreejs version developed by Sam Holen in early June 2018, refined by Juan Cabanela after that.
from IPython.display import display, HTML
import numpy as np
import ipywidgets as widgets
import pythreejs as p3j
import tempNcolor as tc
import starlib as star
## FUNCTIONS ##
def x1_x2_update_V2(m1,m2,x1,x2):
'''
Takes the masses, m1 and m2, and 1-D positions, x1, and x2, of 2 stars.
Uses these values to compute the center of mass of these objects.
Then, with the intention is that the center of mass is held constant (at (0,0)),
it computes updated positions x1 and x2 and returns these.
'''
new_CM = (m1*x1+m2*x2)/(m1+m2)
x1 -= new_CM
x2 -= new_CM
return [x1,x2]
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 star_property_change(change=None):
'''
This function updates the colors and radii of the stars (based on their temperatures).
##Updated##
This function, along with the later widgetname.observe(h, names=['value']) allow the .value
commands to update each time the widget is adjusted without having to rerun the code. This
makes function calls such as Rad_calc easier to implement.
'''
global star1, star2
# Set of separation based on separation slider
init_sep = separation_slider.value
# intial x positions of each star.
x1_init = -init_sep/2
x2_init = init_sep/2
# updates the radial position of each star as the slider is adjusted
r_star1, r_star2 = x1_x2_update_V2(mass1_slider.value, mass2_slider.value,x1_init,x2_init)
# Get previous orbital phase angle
theta0 = np.arctan2(star1.position[1], star1.position[0])
# Get current orbital phase angle
alpha = theta_slider.value*np.pi/180
dtheta = alpha - theta0
# Update the positions in orbit
beta = alpha + np.pi
star1.position = [np.abs(r_star1)*np.cos(alpha), np.abs(r_star1)*np.sin(alpha), 0]
star2.position = [np.abs(r_star2)*np.cos(beta), np.abs(r_star2)*np.sin(beta), 0]
# Rotate the stars (so they stay "tidally locked")
star1.rotateZ(dtheta)
star2.rotateZ(dtheta)
# changes the value of the textbox that outputs the distance of each star from
# the center of mass
star1_output.value = '{:.2f}'.format(abs(r_star1))
star2_output.value = '{:.2f}'.format(abs(r_star2))
# determine parameters of the two stars
(radius1, temp1, hexcolor1, radius2, temp2, hexcolor2) = ConfigBothStars(mass1_slider.value, mass2_slider.value)
# updates the radii and color of each star (assuming initial radius was 1 solar radius)
scale1 = (radius1/init_r1, radius1/init_r1, radius1/init_r1)
scale2 = (radius2/init_r2, radius2/init_r2, radius2/init_r2)
star1.scale = scale1
star2.scale = scale2
star.StarMeshColor(star1, hexcolor1)
star.StarMeshColor(star2, hexcolor2)
# If either star covers the origin, adjust the center of mass marker so it doesn't get covered up.
markerscale = 1.25
if (np.abs(r_star1) < radius1):
angle = np.arccos(r_star1/radius1)
clearance = radius1*np.sin(angle)
adjust=markerscale*clearance
elif (np.abs(r_star2) < radius2):
angle = np.arccos(r_star2/radius2)
clearance = radius2*np.sin(angle)
adjust=markerscale*clearance
else:
# Not overlapping origin
angle = 0
adjust=1
Xaxis.scale = (1, adjust, 1)
Yaxis.scale = (1, adjust, 1)
Zaxis.scale = (1, adjust, 1)
def OverheadView(change):
"""
Resets the view to default view of scene (aka overhead view)
"""
global controller
controller.exec_three_obj_method('reset')
## INTERACTIVE/DISPLAY WIDGETS ##
# Define constants
min_mass = 0.2 # Maximum 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_sep = 15 # Minimum separation of stars in solar radii
max_sep = 40 # Maximum separation of stars in solar radii
sep_step = 1 # Step size for separation slider in solar radii
init_sep = min_sep # Start off with the two stars close together
grid_step = 5 # Step size of grid to draw in solar radii
# Creates sliders for the mass of star 1 and star 2 respectively
ControlColWidth = '450px'
slider_width = '300px'
slider_width = '300px'
readout_width = '70px'
mass1_slider = widgets.FloatSlider(value=init_mass,
min=min_mass,
max=max_mass+(mass_step/2),
step=mass_step,
disabled=False,
continuous_update=True,
style = {'description_width': 'initial'},
description = 'Star 1 Mass:',
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width,
overflow='visible') )
mass2_slider = widgets.FloatSlider(value=init_mass,
min=min_mass,
max=max_mass+(mass_step/2),
step=mass_step,
disabled=False,
continuous_update=True,
style = {'description_width': 'initial'},
description = 'Star 2 Mass:',
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width,
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'))
separation_slider = widgets.FloatSlider(value=init_sep,
min=min_sep,
max=max_sep,
step=sep_step,
description="Separation of Stars",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=True,
orientation='horizontal',
readout=False,
readout_format='.0f',
layout=widgets.Layout(width=slider_width,
overflow='visible') )
separation_readout = widgets.BoundedFloatText(min=separation_slider.min, max=separation_slider.max,
value=separation_slider.value,
layout=widgets.Layout(width=readout_width,
overflow='visible'))
widgets.jslink((separation_readout, 'value'), (separation_slider, 'value'))
Solar_radius = widgets.HTML('R<sub>☉</sub>', layout=widgets.Layout(overflow='visible'))
separation_cntl = widgets.HBox([separation_slider, separation_readout, Solar_radius],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
theta_slider = widgets.FloatSlider(value=0.0,
min=0.0,
max=360,
step=0.1,
description="Phase",
style = {'description_width': 'initial'},
disabled=False,
continuous_update=True,
orientation='horizontal',
readout=False,
readout_format='.1f',
layout=widgets.Layout(width=slider_width,
overflow='visible') )
theta_play = widgets.Play(interval = 1,
value = theta_slider.min,
min=theta_slider.min,
max=theta_slider.max,
step=1,
description="Press play",
disabled=False,
show_repeat=True,
layout=widgets.Layout(overflow='visible') )
widgets.jslink((theta_play, 'value'), (theta_slider, 'value'))
theta_cntl = widgets.HBox([theta_slider, theta_play],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible'))
# Creates textbox widgets to display the distances of each star from the center of mass.
# These are noninteactable so that students may only read the output.
star1_output = widgets.Text(value = str(separation_slider.value/2),
style = {'description_width': 'initial'},
description = 'Star 1 Distance from center of mass',
disabled = True,
layout=widgets.Layout(width='300px') )
star2_output = widgets.Text(value = str(separation_slider.value/2),
style = {'description_width': 'initial'},
description = 'Star 2 Distance from center of mass',
disabled = True,
layout=widgets.Layout(width='300px') )
# Set viewer size
view_width = 500
view_height = 500
# Generate a flat surface to represent orbital plane
xmax = int(np.ceil(max_sep/grid_step))*grid_step
# Generate flat surface and grid for perspective
surf, surfgrid = star.xyplane(xmax, grid_step)
# Generate origin marker to display
Xaxis, Yaxis, Zaxis = star.origin_marker(grid_step/2)
# Define initial position
separation_slider.value = init_sep
init_position = [0, 0, 3*xmax]
# Define initial masses
mass1_slider.value = init_mass
mass2_slider.value = init_mass
# Set initial parameters based on stellar parameters
(radius1, temp1, hexcolor1, radius2, temp2, hexcolor2) = ConfigBothStars(mass1_slider.value, mass2_slider.value)
r1 = radius1
r2 = radius2
# Save initial radius to scale all other radii to this
init_r1 = r1
init_r2 = r2
scale1 = (r1/init_r1, r1/init_r1, r1/init_r1)
scale2 = (r2/init_r2, r1/init_r2, r1/init_r2)
# Create stars at the appropriate positions with appropriate characteristics
# NOTE: This assumes BOTH stars are the same mass initially to avoid computing their positions in detail.
# It also assumes stars are small enough that their don't cover the center of mass.
star1 = star.StarMesh(temp1, r1, scale1, [init_sep/2, 0, 0])
alpha = theta_slider.value*(np.pi/180)
star1.rotateZ(alpha) # Rotates by this many radians, NOT a rotation from initial position
star2 = star.StarMesh(temp2, r2, scale2, [-init_sep/2, 0, 0])
beta = alpha + np.pi/2
star2.rotateZ(beta) # Rotates by this many radians, NOT a rotation from initial position
# Makes the scene environment, not sure how the background works yet
scene2 = p3j.Scene(children=[star1, star2, surf, surfgrid, Xaxis, Yaxis, Zaxis], background='black')
# Creates the camera so you can see stuff (on z-axis looking down on system)
starcam = p3j.PerspectiveCamera(position=init_position, up=[0, 1, 0], aspect=view_width/view_height)
# Makes a controller to use for the
controller = p3j.OrbitControls(controlling=starcam, enablePan=False, enableRotate=True, enableZoom=False,
minPolarAngle=0, maxPolarAngle=np.pi, enableKeys=True,
target = [0, 0, 0])
# creates the object that gets displayed to the screen
renderer2 = p3j.Renderer(camera=starcam,
scene=scene2,
controls=[controller],
width=view_width, height=view_height)
# Include label for grid size
star_display = widgets.VBox([renderer2])
## SCREEN DISPLAY ##
# Creates slider controls for various variables
spacer = widgets.HTML('<p>')
star_title = widgets.HTML('<b>Model Controls</b>:')
grid_note = widgets.HTML('<b>NOTE:</b> Grid Spacing is {0:.0f} solar radii.'.format(grid_step))
star_controls = widgets.VBox([star_title, mass1_cntl, mass2_cntl, spacer],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible') )
separation_controls = widgets.VBox([separation_cntl, grid_note, star1_output, star2_output, spacer],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible') )
# Create play button to control theta value automatically
theta_title = widgets.HTML('<b>Controls for Orbit Angle</b>:')
orbit_controls = widgets.VBox([theta_title, theta_cntl],
layout=widgets.Layout(width=ControlColWidth,
overflow='visible') )
# Create view reset button
ViewReset = widgets.Button(description='View from Overhead', disabled=False, button_style='',
tooltip='Click me to reset view')
# Creates a box for the output of each star's distance from the CM.
controls = widgets.VBox([star_controls, separation_controls, orbit_controls, ViewReset])
# 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.
BOX = widgets.HBox([star_display, controls])
# Sets the dimensions of the box. Sets the entire width and the height of
# just the top.
BOX.layout.width = '970px'
BOX.layout.overflow = 'visible'
# Displays everything to the screen.
display(BOX)
# Makes the function respond to changes in the slider values for each star.
mass1_slider.observe(star_property_change, names=['value'])
mass2_slider.observe(star_property_change, names=['value'])
separation_slider.observe(star_property_change, names=['value'])
theta_slider.observe(star_property_change, names=['value'])
ViewReset.on_click(OverheadView)
HBox(children=(VBox(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, 120.0), quaternion=(0.0, 0…