{ "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": "code", "execution_count": 1, "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": 2, "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": 3, "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[1], star1.position[0])\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": 6, "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='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='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='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='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='visible'))\n", "\n", "mass2_cntl = widgets.HBox([mass2_slider, mass2_readout, solar_mass], \n", " layout=widgets.Layout(width=ControlColWidth, \n", " overflow='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='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='visible'))\n", "widgets.jslink((separation_readout, 'value'), (separation_slider, 'value'))\n", "Solar_radius = widgets.HTML('R', layout=widgets.Layout(overflow='visible'))\n", "separation_cntl = widgets.HBox([separation_slider, separation_readout, Solar_radius], \n", " layout=widgets.Layout(width=ControlColWidth,\n", " overflow='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='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", " show_repeat=True,\n", " layout=widgets.Layout(overflow='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='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": 7, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { "model_id": "dc9c37d0624947fd8bc981feb8479d6f", "version_major": 2, "version_minor": 0 }, "text/plain": [ "HBox(children=(VBox(children=(Renderer(camera=PerspectiveCamera(position=(0.0, 0.0, 120.0), quaternion=(0.0, 0…" ] }, "metadata": {}, "output_type": "display_data" } ], "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='visible') )\n", "\n", "separation_controls = widgets.VBox([separation_cntl, grid_note, star1_output, star2_output, spacer],\n", " layout=widgets.Layout(width=ControlColWidth,\n", " overflow='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='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 = '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)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.11.0" } }, "nbformat": 4, "nbformat_minor": 4 }