{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Center of Mass Interactive\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Experiment with this system to see what happens when you adjust the mass of the two stars. \n", "\n", "1. Adjust the masses of the two stars so they aren't equal.\n", " - Which star is closer to the center of mass? How does its mass compare to that of the farther star?\n", " - Press the 'Play' button and look at how the two stars move around the center of mass. Does the name 'center of mass' make some sense here?\n", "\n", "2. Set the mass of star 1 to 1 $M_\\odot$ and the mass of star 2 to 2 $M_\\odot$ and make their separation 20 $R_\\odot$ (*NOTE:* a $M_\\odot$ is 1 solar mass and $R_\\odot$ is 1 solar radius). \n", " - What is the ratio of the mass of star 1 to star 2? In other words, calculate $\\frac{M_1}{M_2}$.\n", " - How do their distances from the center of mass compare to each other? What is the ratio of the distance of star 1 from the center of mass versus star 2? In other words, calculate $\\frac{r_1}{r_2}$.\n", " \n", "3. Set the mass of star 1 to 0.5 $M_\\odot$ and the mass of star 2 to 2 $M_\\odot$. Keep their separation at 20 $R_\\odot$.\n", " - What is $\\frac{M_1}{M_2}$ now? How about $\\frac{r_1}{r_2}$?\n", "\n", "3. Set the mass of star 1 to 10 $M_\\odot$ and the mass of star 2 to 2 $M_\\odot$. Keep their separation at 20 $R_\\odot$.\n", " - Is there anything different about the location of the center of mass in this case versus the last two cases?\n", " - What is $\\frac{M_1}{M_2}$ now? How about $\\frac{r_1}{r_2}$?\n", " \n", "4. Physicists say that the relationship between the masses and center of mass distances should be $M_1\\times r_1 = M_2\\times r_2$ which means ${M_2}{M_1} = {r_1}{r_2}$. Notice this is the inverse of the mass ratio you found above. Does this equation work for all the cases above?\n", "\n", "5. If star 2 is 10 times the mass of star 1, which star should be closer to the center of mass? Does this match what you expected after answering the first question?\n", "\n", "6. If our telescope can separate the stars optically, we could measure their distances from the center of mass, $r_1$ and $r_2$. Does knowing both $r_1$ and $r_2$ allow you to determine the masses of both stars? What can you learn about the two stars if you know $r_1$ and $r_2$?" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Originally developed using bqplot by Sam Holen in late May 2018.\n", "# pythreejs version developed by Sam Holen in early June 2018, refined by Juan Cabanela after that." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from IPython.display import display, HTML\n", "import numpy as np\n", "import ipywidgets as widgets\n", "import pythreejs as p3j\n", "import tempNcolor as tc\n", "import starlib as star" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## FUNCTIONS ##\n", "\n", "def x1_x2_update_V2(m1,m2,x1,x2):\n", " '''\n", " Takes the masses, m1 and m2, and 1-D positions, x1, and x2, of 2 stars.\n", " Uses these values to compute the center of mass of these objects.\n", " Then, with the intention is that the center of mass is held constant (at (0,0)),\n", " it computes updated positions x1 and x2 and returns these.\n", " '''\n", " new_CM = (m1*x1+m2*x2)/(m1+m2)\n", " x1 -= new_CM\n", " x2 -= new_CM\n", " return [x1,x2]\n", "\n", "def ConfigBothStars(mass1, mass2):\n", " '''\n", " Determines the radii (in solar radii), temperature (in K), and hexcolor of the two stars assuming \n", " they are main sequence stars and returns that information. Does this by calling the ConfigStar \n", " function for both stars.\n", " '''\n", " \n", " (radius1, temp1, hexcolor1) = star.ConfigStar(mass1)\n", " (radius2, temp2, hexcolor2) = star.ConfigStar(mass2)\n", " \n", " return (radius1, temp1, hexcolor1, radius2, temp2, hexcolor2)\n", " \n", "def star_property_change(change=None):\n", " '''\n", " This function updates the colors and radii of the stars (based on their temperatures).\n", " \n", " ##Updated##\n", " This function, along with the later widgetname.observe(h, names=['value']) allow the .value \n", " commands to update each time the widget is adjusted without having to rerun the code. This\n", " makes function calls such as Rad_calc easier to implement.\n", " '''\n", " global star1, star2\n", " \n", " # Set of separation based on separation slider\n", " init_sep = separation_slider.value\n", " \n", " # intial x positions of each star.\n", " x1_init = -init_sep/2\n", " x2_init = init_sep/2\n", " \n", " # updates the radial position of each star as the slider is adjusted\n", " r_star1, r_star2 = x1_x2_update_V2(mass1_slider.value, mass2_slider.value,x1_init,x2_init)\n", " \n", " # Get previous orbital phase angle\n", " theta0 = np.arctan2(star1.position, star1.position)\n", " \n", " # Get current orbital phase angle\n", " alpha = theta_slider.value*np.pi/180\n", " dtheta = alpha - theta0\n", " \n", " # Update the positions in orbit\n", " beta = alpha + np.pi\n", " star1.position = [np.abs(r_star1)*np.cos(alpha), np.abs(r_star1)*np.sin(alpha), 0]\n", " star2.position = [np.abs(r_star2)*np.cos(beta), np.abs(r_star2)*np.sin(beta), 0]\n", " # Rotate the stars (so they stay \"tidally locked\")\n", " star1.rotateZ(dtheta)\n", " star2.rotateZ(dtheta)\n", "\n", " # changes the value of the textbox that outputs the distance of each star from\n", " # the center of mass\n", " star1_output.value = '{:.2f}'.format(abs(r_star1))\n", " star2_output.value = '{:.2f}'.format(abs(r_star2))\n", " \n", " # determine parameters of the two stars\n", " (radius1, temp1, hexcolor1, radius2, temp2, hexcolor2) = ConfigBothStars(mass1_slider.value, mass2_slider.value)\n", " \n", " # updates the radii and color of each star (assuming initial radius was 1 solar radius)\n", " scale1 = (radius1/init_r1, radius1/init_r1, radius1/init_r1)\n", " scale2 = (radius2/init_r2, radius2/init_r2, radius2/init_r2)\n", " star1.scale = scale1\n", " star2.scale = scale2\n", " star.StarMeshColor(star1, hexcolor1)\n", " star.StarMeshColor(star2, hexcolor2)\n", " \n", " # If either star covers the origin, adjust the center of mass marker so it doesn't get covered up.\n", " markerscale = 1.25\n", " if (np.abs(r_star1) < radius1):\n", " angle = np.arccos(r_star1/radius1)\n", " clearance = radius1*np.sin(angle)\n", " adjust=markerscale*clearance\n", " elif (np.abs(r_star2) < radius2):\n", " angle = np.arccos(r_star2/radius2)\n", " clearance = radius2*np.sin(angle)\n", " adjust=markerscale*clearance\n", " else:\n", " # Not overlapping origin\n", " angle = 0\n", " adjust=1 \n", " Xaxis.scale = (1, adjust, 1)\n", " Yaxis.scale = (1, adjust, 1)\n", " Zaxis.scale = (1, adjust, 1)\n", "\n", " \n", "def OverheadView(change):\n", " \"\"\"\n", " Resets the view to default view of scene (aka overhead view)\n", " \"\"\"\n", " global controller\n", " \n", " controller.exec_three_obj_method('reset')\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "## INTERACTIVE/DISPLAY WIDGETS ##\n", "\n", "# Define constants\n", "min_mass = 0.2 # Maximum stellar mass in solar masses\n", "max_mass = 24 # Maximum stellar mass in solar masses\n", "mass_step = 0.1 # Step size for mass sliders in solar masses\n", "init_mass = 1 # Initial mass of both stars in solar masses\n", "\n", "min_sep = 15 # Minimum separation of stars in solar radii\n", "max_sep = 40 # Maximum separation of stars in solar radii\n", "sep_step = 1 # Step size for separation slider in solar radii\n", "init_sep = min_sep # Start off with the two stars close together\n", "grid_step = 5 # Step size of grid to draw in solar radii\n", "\n", "# Creates sliders for the mass of star 1 and star 2 respectively\n", "ControlColWidth = '450px'\n", "slider_width = '300px'\n", "slider_width = '300px'\n", "readout_width = '70px'\n", "\n", "mass1_slider = widgets.FloatSlider(value=init_mass,\n", " min=min_mass,\n", " max=max_mass+(mass_step/2),\n", " step=mass_step,\n", " disabled=False,\n", " continuous_update=True,\n", " style = {'description_width': 'initial'},\n", " description = 'Star 1 Mass:',\n", " orientation='horizontal',\n", " readout=False,\n", " readout_format='.1f',\n", " layout=widgets.Layout(width=slider_width,\n", " overflow_x='visible', overflow_y='visible') )\n", "\n", "mass2_slider = widgets.FloatSlider(value=init_mass,\n", " min=min_mass,\n", " max=max_mass+(mass_step/2),\n", " step=mass_step,\n", " disabled=False,\n", " continuous_update=True,\n", " style = {'description_width': 'initial'},\n", " description = 'Star 2 Mass:',\n", " orientation='horizontal',\n", " readout=False,\n", " readout_format='.1f',\n", " layout=widgets.Layout(width=slider_width,\n", " overflow_x='visible', overflow_y='visible') )\n", "\n", "# Define text boxes for readout\n", "mass1_readout = widgets.BoundedFloatText(min=mass1_slider.min, max=mass1_slider.max, \n", " value=mass1_slider.value, \n", " layout=widgets.Layout(width=readout_width, \n", " overflow_x='visible', \n", " overflow_y='visible'))\n", "\n", "mass2_readout = widgets.BoundedFloatText(min=mass2_slider.min, max=mass2_slider.max, \n", " value=mass2_slider.value, \n", " layout=widgets.Layout(width=readout_width,\n", " overflow_x='visible', \n", " overflow_y='visible'))\n", "\n", "# Link slider and textboxes\n", "widgets.jslink((mass1_readout, 'value'), (mass1_slider, 'value'))\n", "widgets.jslink((mass2_readout, 'value'), (mass2_slider, 'value'))\n", "\n", "# Create the individual controls for stellar masses\n", "solar_mass = widgets.HTML('M')\n", "mass1_cntl = widgets.HBox([mass1_slider, mass1_readout, solar_mass], \n", " layout=widgets.Layout(width=ControlColWidth, \n", " overflow_x='visible', \n", " overflow_y='visible'))\n", "\n", "mass2_cntl = widgets.HBox([mass2_slider, mass2_readout, solar_mass], \n", " layout=widgets.Layout(width=ControlColWidth, \n", " overflow_x='visible', \n", " overflow_y='visible'))\n", "\n", "\n", "separation_slider = widgets.FloatSlider(value=init_sep,\n", " min=min_sep,\n", " max=max_sep,\n", " step=sep_step,\n", " description=\"Separation of Stars\",\n", " style = {'description_width': 'initial'},\n", " disabled=False,\n", " continuous_update=True,\n", " orientation='horizontal',\n", " readout=False,\n", " readout_format='.0f',\n", " layout=widgets.Layout(width=slider_width,\n", " overflow_x='visible', overflow_y='visible') )\n", "\n", "separation_readout = widgets.BoundedFloatText(min=separation_slider.min, max=separation_slider.max, \n", " value=separation_slider.value, \n", " layout=widgets.Layout(width=readout_width, \n", " overflow_x='visible', \n", " overflow_y='visible'))\n", "widgets.jslink((separation_readout, 'value'), (separation_slider, 'value'))\n", "Solar_radius = widgets.HTML('R', layout=widgets.Layout(overflow_x='visible', \n", " overflow_y='visible'))\n", "separation_cntl = widgets.HBox([separation_slider, separation_readout, Solar_radius], \n", " layout=widgets.Layout(width=ControlColWidth,\n", " overflow_x='visible',\n", " overflow_y='visible'))\n", "\n", "\n", "theta_slider = widgets.FloatSlider(value=0.0,\n", " min=0.0,\n", " max=360,\n", " step=0.1,\n", " description=\"Phase\",\n", " style = {'description_width': 'initial'},\n", " disabled=False,\n", " continuous_update=True,\n", " orientation='horizontal',\n", " readout=False,\n", " readout_format='.1f',\n", " layout=widgets.Layout(width=slider_width, \n", " overflow_x='visible', overflow_y='visible') )\n", "theta_play = widgets.Play(interval = 1, \n", " value = theta_slider.min, \n", " min=theta_slider.min, \n", " max=theta_slider.max, \n", " step=1, \n", " description=\"Press play\", \n", " disabled=False, \n", " _repeat=True, show_repeat=False,\n", " layout=widgets.Layout(overflow_x='visible', overflow_y='visible') )\n", "\n", "widgets.jslink((theta_play, 'value'), (theta_slider, 'value'))\n", "theta_cntl = widgets.HBox([theta_slider, theta_play], \n", " layout=widgets.Layout(width=ControlColWidth, \n", " overflow_x='visible', overflow_y='visible'))\n", "\n", "\n", "# Creates textbox widgets to display the distances of each star from the center of mass.\n", "# These are noninteactable so that students may only read the output.\n", "star1_output = widgets.Text(value = str(separation_slider.value/2),\n", " style = {'description_width': 'initial'},\n", " description = 'Star 1 Distance from center of mass',\n", " disabled = True, \n", " layout=widgets.Layout(width='300px') )\n", "\n", "star2_output = widgets.Text(value = str(separation_slider.value/2),\n", " style = {'description_width': 'initial'},\n", " description = 'Star 2 Distance from center of mass',\n", " disabled = True, \n", " layout=widgets.Layout(width='300px') )" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Set viewer size\n", "view_width = 500\n", "view_height = 500\n", "\n", "# Generate a flat surface to represent orbital plane\n", "xmax = int(np.ceil(max_sep/grid_step))*grid_step\n", "# Generate flat surface and grid for perspective\n", "surf, surfgrid = star.xyplane(xmax, grid_step)\n", "# Generate origin marker to display\n", "Xaxis, Yaxis, Zaxis = star.origin_marker(grid_step/2)\n", "\n", "# Define initial position\n", "separation_slider.value = init_sep\n", "init_position = [0, 0, 3*xmax]\n", "\n", "# Define initial masses\n", "mass1_slider.value = init_mass\n", "mass2_slider.value = init_mass\n", "# Set initial parameters based on stellar parameters\n", "(radius1, temp1, hexcolor1, radius2, temp2, hexcolor2) = ConfigBothStars(mass1_slider.value, mass2_slider.value)\n", "r1 = radius1\n", "r2 = radius2\n", "# Save initial radius to scale all other radii to this\n", "init_r1 = r1\n", "init_r2 = r2\n", "scale1 = (r1/init_r1, r1/init_r1, r1/init_r1)\n", "scale2 = (r2/init_r2, r1/init_r2, r1/init_r2)\n", "\n", "# Create stars at the appropriate positions with appropriate characteristics\n", "# NOTE: This assumes BOTH stars are the same mass initially to avoid computing their positions in detail.\n", "# It also assumes stars are small enough that their don't cover the center of mass.\n", "\n", "star1 = star.StarMesh(temp1, r1, scale1, [init_sep/2, 0, 0])\n", "alpha = theta_slider.value*(np.pi/180)\n", "star1.rotateZ(alpha) # Rotates by this many radians, NOT a rotation from initial position\n", "\n", "star2 = star.StarMesh(temp2, r2, scale2, [-init_sep/2, 0, 0])\n", "beta = alpha + np.pi/2\n", "star2.rotateZ(beta) # Rotates by this many radians, NOT a rotation from initial position\n", "\n", "# Makes the scene environment, not sure how the background works yet\n", "scene2 = p3j.Scene(children=[star1, star2, surf, surfgrid, Xaxis, Yaxis, Zaxis], background='black')\n", "\n", "# Creates the camera so you can see stuff (on z-axis looking down on system)\n", "starcam = p3j.PerspectiveCamera(position=init_position, up=[0, 1, 0], aspect=view_width/view_height)\n", "\n", "# Makes a controller to use for the \n", "controller = p3j.OrbitControls(controlling=starcam, enablePan=False, enableRotate=True, enableZoom=False, \n", " minPolarAngle=0, maxPolarAngle=np.pi, enableKeys=True,\n", " target = [0, 0, 0])\n", "\n", "# creates the object that gets displayed to the screen\n", "renderer2 = p3j.Renderer(camera=starcam, \n", " scene=scene2, \n", " controls=[controller],\n", " width=view_width, height=view_height)\n", "\n", "# Include label for grid size\n", "star_display = widgets.VBox([renderer2])\n", "\n", "## SCREEN DISPLAY ##\n", "\n", "# Creates slider controls for various variables\n", "spacer = widgets.HTML('

')\n", "star_title = widgets.HTML('Model Controls:')\n", "grid_note = widgets.HTML('NOTE: Grid Spacing is {0:.0f} solar radii.'.format(grid_step))\n", "\n", "star_controls = widgets.VBox([star_title, mass1_cntl, mass2_cntl, spacer],\n", " layout=widgets.Layout(width=ControlColWidth,\n", " overflow_x='visible',\n", " overflow_y='visible') )\n", "\n", "separation_controls = widgets.VBox([separation_cntl, grid_note, star1_output, star2_output, spacer],\n", " layout=widgets.Layout(width=ControlColWidth,\n", " overflow_x='visible',\n", " overflow_y='visible') )\n", "\n", "# Create play button to control theta value automatically\n", "theta_title = widgets.HTML('Controls for Orbit Angle:')\n", "orbit_controls = widgets.VBox([theta_title, theta_cntl],\n", " layout=widgets.Layout(width=ControlColWidth,\n", " overflow_x='visible',\n", " overflow_y='visible') )\n", "\n", "# Create view reset button\n", "ViewReset = widgets.Button(description='View from Overhead', disabled=False, button_style='',\n", " tooltip='Click me to reset view')\n", "\n", "# Creates a box for the output of each star's distance from the CM.\n", "controls = widgets.VBox([star_controls, separation_controls, orbit_controls, ViewReset])\n", "\n", "# Places the figure, sliders, and output into a Vbox. The figure is \n", "# alone in the top, while the sliders and output are in a Hbox\n", "# inside the bottom of the Vbox.\n", "BOX = widgets.HBox([star_display, controls])\n", "\n", "# Sets the dimensions of the box. Sets the entire width and the height of \n", "# just the top.\n", "BOX.layout.width = '970px'\n", "BOX.layout.overflow_x = 'visible'\n", "\n", "# Displays everything to the screen.\n", "display(BOX)\n", "\n", "# Makes the function respond to changes in the slider values for each star.\n", "mass1_slider.observe(star_property_change, names=['value'])\n", "mass2_slider.observe(star_property_change, names=['value'])\n", "separation_slider.observe(star_property_change, names=['value'])\n", "theta_slider.observe(star_property_change, names=['value'])\n", "ViewReset.on_click(OverheadView)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 4 }