osanseviero's picture
osanseviero HF staff
Duplicate from elonmuskceo/shiny-orbit-simulation
da6f810
from pathlib import Path
from simulation import Body, Simulation, nbody_solve, spherical_to_cartesian
import matplotlib.pyplot as plt
import astropy.units as u
import numpy as np
from shiny import App, reactive, render, ui
# This application adapted from RK4 Orbit Integrator tutorial in Python for Astronomers
# https://prappleizer.github.io/
def panel_box(*args, **kwargs):
return ui.div(
ui.div(*args, class_="card-body"),
**kwargs,
class_="card mb-3",
)
app_ui = ui.page_fluid(
{"class": "p-4"},
ui.row(
ui.column(
4,
panel_box(
ui.input_slider("days", "Simulation duration (days)", 0, 200, value=60),
ui.input_slider(
"step_size",
"Simulation time step (hours)",
0,
24,
value=4,
step=0.5,
),
ui.input_action_button(
"run", "Run simulation", class_="btn-primary w-100"
),
),
ui.navset_tab_card(
ui.nav(
"Earth",
ui.input_checkbox("earth", "Enable", True),
ui.panel_conditional(
"input.earth",
ui.input_numeric(
"earth_mass",
"Mass (10^22 kg)",
597.216,
),
ui.input_slider(
"earth_speed",
"Speed (km/s)",
0,
1,
value=0.0126,
step=0.001,
),
ui.input_slider("earth_theta", "Angle (5)", 0, 360, value=270),
ui.input_slider("earth_phi", "5", 0, 180, value=90),
),
),
ui.nav(
"Moon",
ui.input_checkbox("moon", "Enable", True),
ui.panel_conditional(
"input.moon",
ui.input_numeric("moon_mass", "Mass (10^22 kg)", 7.347),
ui.input_slider(
"moon_speed", "Speed (km/s)", 0, 2, value=1.022, step=0.001
),
ui.input_slider("moon_theta", "Angle (5)", 0, 360, value=90),
ui.input_slider("moon_phi", "5", 0, 180, value=90),
),
),
ui.nav(
"Planet X",
ui.input_checkbox("planetx", "Enable", False),
ui.output_ui("planetx_controls"),
ui.panel_conditional(
"input.planetx",
ui.input_numeric("planetx_mass", "Mass (10^22 kg)", 7.347),
ui.input_slider(
"planetx_speed",
"Speed (km/s)",
0,
2,
value=1.022,
step=0.001,
),
ui.input_slider("planetx_theta", "Angle (5)", 0, 360, 270),
ui.input_slider("planetx_phi", "5", 0, 180, 90),
),
),
),
),
ui.column(
8,
ui.output_plot("orbits", width="500px", height="500px"),
ui.img(src="coords.png", style="width: 100%; max-width: 250px;"),
),
),
)
def server(input, output, session):
def earth_body():
v = spherical_to_cartesian(
input.earth_theta(), input.earth_phi(), input.earth_speed()
)
return Body(
mass=input.earth_mass() * 10e21 * u.kg,
x_vec=np.array([0, 0, 0]) * u.km,
v_vec=np.array(v) * u.km / u.s,
name="Earth",
)
def moon_body():
v = spherical_to_cartesian(
input.moon_theta(), input.moon_phi(), input.moon_speed()
)
return Body(
mass=input.moon_mass() * 10e21 * u.kg,
x_vec=np.array([3.84e5, 0, 0]) * u.km,
v_vec=np.array(v) * u.km / u.s,
name="Moon",
)
def planetx_body():
v = spherical_to_cartesian(
input.planetx_theta(), input.planetx_phi(), input.planetx_speed()
)
return Body(
mass=input.planetx_mass() * 10e21 * u.kg,
x_vec=np.array([-3.84e5, 0, 0]) * u.km,
v_vec=np.array(v) * u.km / u.s,
name="Planet X",
)
def simulation():
bodies = []
if input.earth():
bodies.append(earth_body())
if input.moon():
bodies.append(moon_body())
if input.planetx():
bodies.append(planetx_body())
simulation_ = Simulation(bodies)
simulation_.set_diff_eq(nbody_solve)
return simulation_
has_run = False
@output
@render.plot
@reactive.event(input.run, ignore_none=False)
def orbits():
return make_orbit_plot()
def make_orbit_plot():
sim = simulation()
n_steps = input.days() * 24 / input.step_size()
with ui.Progress(min=1, max=n_steps) as p:
sim.run(input.days() * u.day, input.step_size() * u.hr, progress=p)
sim_hist = sim.history
end_idx = len(sim_hist) - 1
fig = plt.figure()
ax = plt.axes(projection="3d")
n_bodies = int(sim_hist.shape[1] / 6)
for i in range(0, n_bodies):
ax.scatter3D(
sim_hist[end_idx, i * 6],
sim_hist[end_idx, i * 6 + 1],
sim_hist[end_idx, i * 6 + 2],
s=50,
)
ax.plot3D(
sim_hist[:, i * 6],
sim_hist[:, i * 6 + 1],
sim_hist[:, i * 6 + 2],
)
ax.view_init(30, 20)
set_axes_equal(ax)
return fig
www_dir = Path(__file__).parent / "www"
app = App(app_ui, server, static_assets=www_dir)
# https://stackoverflow.com/a/31364297/412655
def set_axes_equal(ax):
"""Make axes of 3D plot have equal scale so that spheres appear as spheres,
cubes as cubes, etc.. This is one possible solution to Matplotlib's
ax.set_aspect('equal') and ax.axis('equal') not working for 3D.
Input
ax: a matplotlib axis, e.g., as output from plt.gca().
"""
x_limits = ax.get_xlim3d()
y_limits = ax.get_ylim3d()
z_limits = ax.get_zlim3d()
x_range = abs(x_limits[1] - x_limits[0])
x_middle = np.mean(x_limits)
y_range = abs(y_limits[1] - y_limits[0])
y_middle = np.mean(y_limits)
z_range = abs(z_limits[1] - z_limits[0])
z_middle = np.mean(z_limits)
# The plot bounding box is a sphere in the sense of the infinity
# norm, hence I call half the max range the plot radius.
plot_radius = 0.5 * max([x_range, y_range, z_range])
ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])