julien-c HF staff commited on
Commit
6b6806c
1 Parent(s): 88ec14b

initial import

Browse files
Files changed (5) hide show
  1. Dockerfile +22 -0
  2. app.py +229 -0
  3. requirements.txt +1 -0
  4. simulation.py +203 -0
  5. www/coords.png +0 -0
Dockerfile CHANGED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ubuntu:kinetic
2
+
3
+ # Doesn't usually have an "upgrade"
4
+ RUN apt-get update \
5
+ && DEBIAN_FRONTEND=noninteractive \
6
+ apt-get install --no-install-recommends --assume-yes \
7
+ build-essential \
8
+ python3 \
9
+ python3-dev \
10
+ python3-pip
11
+
12
+ COPY requirements.txt .
13
+
14
+ RUN pip install -r requirements.txt
15
+
16
+ COPY . .
17
+
18
+ ENTRYPOINT ["/bin/sh", "-c"]
19
+
20
+ EXPOSE 7860
21
+
22
+ CMD ["shiny run --port 7860 --host 0.0.0.0 app.py"]
app.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pathlib import Path
2
+ from simulation import Body, Simulation, nbody_solve, spherical_to_cartesian
3
+ import matplotlib.pyplot as plt
4
+ import astropy.units as u
5
+ import numpy as np
6
+
7
+ from shiny import App, reactive, render, ui
8
+
9
+ # This application adapted from RK4 Orbit Integrator tutorial in Python for Astronomers
10
+ # https://prappleizer.github.io/
11
+
12
+
13
+ def panel_box(*args, **kwargs):
14
+ return ui.div(
15
+ ui.div(*args, class_="card-body"),
16
+ **kwargs,
17
+ class_="card mb-3",
18
+ )
19
+
20
+
21
+ app_ui = ui.page_fluid(
22
+ {"class": "p-4"},
23
+ ui.row(
24
+ ui.column(
25
+ 4,
26
+ panel_box(
27
+ ui.input_slider("days", "Simulation duration (days)", 0, 200, value=60),
28
+ ui.input_slider(
29
+ "step_size",
30
+ "Simulation time step (hours)",
31
+ 0,
32
+ 24,
33
+ value=4,
34
+ step=0.5,
35
+ ),
36
+ ui.input_action_button(
37
+ "run", "Run simulation", class_="btn-primary w-100"
38
+ ),
39
+ ),
40
+ ui.navset_tab_card(
41
+ ui.nav(
42
+ "Earth",
43
+ ui.input_checkbox("earth", "Enable", True),
44
+ ui.panel_conditional(
45
+ "input.earth",
46
+ ui.input_numeric(
47
+ "earth_mass",
48
+ "Mass (10^22 kg)",
49
+ 597.216,
50
+ ),
51
+ ui.input_slider(
52
+ "earth_speed",
53
+ "Speed (km/s)",
54
+ 0,
55
+ 1,
56
+ value=0.0126,
57
+ step=0.001,
58
+ ),
59
+ ui.input_slider("earth_theta", "Angle (5)", 0, 360, value=270),
60
+ ui.input_slider("earth_phi", "5", 0, 180, value=90),
61
+ ),
62
+ ),
63
+ ui.nav(
64
+ "Moon",
65
+ ui.input_checkbox("moon", "Enable", True),
66
+ ui.panel_conditional(
67
+ "input.moon",
68
+ ui.input_numeric("moon_mass", "Mass (10^22 kg)", 7.347),
69
+ ui.input_slider(
70
+ "moon_speed", "Speed (km/s)", 0, 2, value=1.022, step=0.001
71
+ ),
72
+ ui.input_slider("moon_theta", "Angle (5)", 0, 360, value=90),
73
+ ui.input_slider("moon_phi", "5", 0, 180, value=90),
74
+ ),
75
+ ),
76
+ ui.nav(
77
+ "Planet X",
78
+ ui.input_checkbox("planetx", "Enable", False),
79
+ ui.output_ui("planetx_controls"),
80
+ ui.panel_conditional(
81
+ "input.planetx",
82
+ ui.input_numeric("planetx_mass", "Mass (10^22 kg)", 7.347),
83
+ ui.input_slider(
84
+ "planetx_speed",
85
+ "Speed (km/s)",
86
+ 0,
87
+ 2,
88
+ value=1.022,
89
+ step=0.001,
90
+ ),
91
+ ui.input_slider("planetx_theta", "Angle (5)", 0, 360, 270),
92
+ ui.input_slider("planetx_phi", "5", 0, 180, 90),
93
+ ),
94
+ ),
95
+ ),
96
+ ),
97
+ ui.column(
98
+ 8,
99
+ ui.output_plot("orbits", width="500px", height="500px"),
100
+ ui.img(src="coords.png", style="width: 100%; max-width: 250px;"),
101
+ ),
102
+ ),
103
+ )
104
+
105
+
106
+ def server(input, output, session):
107
+ def earth_body():
108
+ v = spherical_to_cartesian(
109
+ input.earth_theta(), input.earth_phi(), input.earth_speed()
110
+ )
111
+
112
+ return Body(
113
+ mass=input.earth_mass() * 10e21 * u.kg,
114
+ x_vec=np.array([0, 0, 0]) * u.km,
115
+ v_vec=np.array(v) * u.km / u.s,
116
+ name="Earth",
117
+ )
118
+
119
+ def moon_body():
120
+ v = spherical_to_cartesian(
121
+ input.moon_theta(), input.moon_phi(), input.moon_speed()
122
+ )
123
+
124
+ return Body(
125
+ mass=input.moon_mass() * 10e21 * u.kg,
126
+ x_vec=np.array([3.84e5, 0, 0]) * u.km,
127
+ v_vec=np.array(v) * u.km / u.s,
128
+ name="Moon",
129
+ )
130
+
131
+ def planetx_body():
132
+ v = spherical_to_cartesian(
133
+ input.planetx_theta(), input.planetx_phi(), input.planetx_speed()
134
+ )
135
+
136
+ return Body(
137
+ mass=input.planetx_mass() * 10e21 * u.kg,
138
+ x_vec=np.array([-3.84e5, 0, 0]) * u.km,
139
+ v_vec=np.array(v) * u.km / u.s,
140
+ name="Planet X",
141
+ )
142
+
143
+ def simulation():
144
+ bodies = []
145
+ if input.earth():
146
+ bodies.append(earth_body())
147
+ if input.moon():
148
+ bodies.append(moon_body())
149
+ if input.planetx():
150
+ bodies.append(planetx_body())
151
+
152
+ simulation_ = Simulation(bodies)
153
+ simulation_.set_diff_eq(nbody_solve)
154
+
155
+ return simulation_
156
+
157
+ has_run = False
158
+
159
+ @output
160
+ @render.plot
161
+ @reactive.event(input.run, ignore_none=False)
162
+ def orbits():
163
+ return make_orbit_plot()
164
+
165
+ def make_orbit_plot():
166
+ sim = simulation()
167
+ n_steps = input.days() * 24 / input.step_size()
168
+ with ui.Progress(min=1, max=n_steps) as p:
169
+ sim.run(input.days() * u.day, input.step_size() * u.hr, progress=p)
170
+
171
+ sim_hist = sim.history
172
+ end_idx = len(sim_hist) - 1
173
+
174
+ fig = plt.figure()
175
+
176
+ ax = plt.axes(projection="3d")
177
+
178
+ n_bodies = int(sim_hist.shape[1] / 6)
179
+ for i in range(0, n_bodies):
180
+ ax.scatter3D(
181
+ sim_hist[end_idx, i * 6],
182
+ sim_hist[end_idx, i * 6 + 1],
183
+ sim_hist[end_idx, i * 6 + 2],
184
+ s=50,
185
+ )
186
+ ax.plot3D(
187
+ sim_hist[:, i * 6],
188
+ sim_hist[:, i * 6 + 1],
189
+ sim_hist[:, i * 6 + 2],
190
+ )
191
+
192
+ ax.view_init(30, 20)
193
+ set_axes_equal(ax)
194
+
195
+ return fig
196
+
197
+
198
+ www_dir = Path(__file__).parent / "www"
199
+ app = App(app_ui, server, static_assets=www_dir)
200
+
201
+
202
+ # https://stackoverflow.com/a/31364297/412655
203
+ def set_axes_equal(ax):
204
+ """Make axes of 3D plot have equal scale so that spheres appear as spheres,
205
+ cubes as cubes, etc.. This is one possible solution to Matplotlib's
206
+ ax.set_aspect('equal') and ax.axis('equal') not working for 3D.
207
+
208
+ Input
209
+ ax: a matplotlib axis, e.g., as output from plt.gca().
210
+ """
211
+
212
+ x_limits = ax.get_xlim3d()
213
+ y_limits = ax.get_ylim3d()
214
+ z_limits = ax.get_zlim3d()
215
+
216
+ x_range = abs(x_limits[1] - x_limits[0])
217
+ x_middle = np.mean(x_limits)
218
+ y_range = abs(y_limits[1] - y_limits[0])
219
+ y_middle = np.mean(y_limits)
220
+ z_range = abs(z_limits[1] - z_limits[0])
221
+ z_middle = np.mean(z_limits)
222
+
223
+ # The plot bounding box is a sphere in the sense of the infinity
224
+ # norm, hence I call half the max range the plot radius.
225
+ plot_radius = 0.5 * max([x_range, y_range, z_range])
226
+
227
+ ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
228
+ ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
229
+ ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ astropy==5.1
simulation.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Any
2
+ import numpy as np
3
+ import astropy.constants as c
4
+ import time
5
+
6
+ # Adapted from Python for Astronomers: An Introduction to Scientific Computing
7
+ # by Imad Pasha & Christopher Agostino
8
+ # https://prappleizer.github.io/Tutorials/RK4/RK4_Tutorial.html
9
+
10
+ # Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
11
+ # http://creativecommons.org/licenses/by-nc-sa/4.0/
12
+
13
+
14
+ class Body:
15
+ def __init__(self, mass, x_vec, v_vec, name=None, has_units=True):
16
+ """
17
+ spawn instance of the Body class, which is used in Simulations.
18
+
19
+ :param: mass | mass of particle. if has_units=True, an Astropy Quantity, otherwise a float
20
+ :param: x_vec | a vector len(3) containing the x, y, z initial positions of the body.
21
+ the array can be unitless if has_units=False, or be of the form np.array([0,0,0])*u.km
22
+ :param: v_vec | vector len(3) containing the v_x, v_y, v_z initial velocities of the body.
23
+ :param: name | string containing a name, used for plotting later
24
+ :param: has_units | defines how the code treats the problem, as unit-ed, or unitless.
25
+ """
26
+ self.name = name
27
+ self.has_units = has_units
28
+ if self.has_units:
29
+ self.mass = mass.cgs
30
+ self.x_vec = x_vec.cgs.value
31
+ self.v_vec = v_vec.cgs.value
32
+ else:
33
+ self.mass = mass
34
+ self.x_vec = x_vec
35
+ self.v_vec = v_vec
36
+
37
+ def return_vec(self):
38
+ """
39
+ Concatenates the x and v vector into 1 vector 'y' used in RK formalism.
40
+ """
41
+ return np.concatenate((self.x_vec, self.v_vec))
42
+
43
+ def return_mass(self):
44
+ """
45
+ handler to strip the mass units if present (after converting to cgs) or return float
46
+ """
47
+ if self.has_units:
48
+ return self.mass.cgs.value
49
+ else:
50
+ return self.mass
51
+
52
+ def return_name(self):
53
+ return self.name
54
+
55
+
56
+ class Simulation:
57
+ def __init__(self, bodies, has_units=True):
58
+ """
59
+ Initializes instance of Simulation object.
60
+ -------------------------------------------
61
+ Params:
62
+ bodies (list): a list of Body() objects
63
+ has_units (bool): set whether bodies entered have units or not.
64
+ """
65
+ self.has_units = has_units
66
+ self.bodies = bodies
67
+ self.N_bodies = len(self.bodies)
68
+ self.nDim = 6.0
69
+ self.quant_vec = np.concatenate(np.array([i.return_vec() for i in self.bodies]))
70
+ self.mass_vec = np.array([i.return_mass() for i in self.bodies])
71
+ self.name_vec = [i.return_name() for i in self.bodies]
72
+
73
+ def set_diff_eq(self, calc_diff_eqs, **kwargs):
74
+ """
75
+ Method which assigns an external solver function as the diff-eq solver for RK4.
76
+ For N-body or gravitational setups, this is the function which calculates accelerations.
77
+ ---------------------------------
78
+ Params:
79
+ calc_diff_eqs: A function which returns a [y] vector for RK4
80
+ **kwargs: Any additional inputs/hyperparameters the external function requires
81
+ """
82
+ self.diff_eq_kwargs = kwargs
83
+ self.calc_diff_eqs = calc_diff_eqs
84
+
85
+ def rk4(self, t, dt):
86
+ """
87
+ RK4 integrator. Calculates the K values and returns a new y vector
88
+ --------------------------------
89
+ Params:
90
+ t: a time. Only used if the diff eq depends on time (gravity doesn't).
91
+ dt: timestep. Non adaptive in this case
92
+ """
93
+ k1 = dt * self.calc_diff_eqs(
94
+ t, self.quant_vec, self.mass_vec, **self.diff_eq_kwargs
95
+ )
96
+ k2 = dt * self.calc_diff_eqs(
97
+ t + 0.5 * dt,
98
+ self.quant_vec + 0.5 * k1,
99
+ self.mass_vec,
100
+ **self.diff_eq_kwargs,
101
+ )
102
+ k3 = dt * self.calc_diff_eqs(
103
+ t + 0.5 * dt,
104
+ self.quant_vec + 0.5 * k2,
105
+ self.mass_vec,
106
+ **self.diff_eq_kwargs,
107
+ )
108
+ k4 = dt * self.calc_diff_eqs(
109
+ t + dt, self.quant_vec + k2, self.mass_vec, **self.diff_eq_kwargs
110
+ )
111
+
112
+ y_new = self.quant_vec + ((k1 + 2 * k2 + 2 * k3 + k4) / 6.0)
113
+
114
+ return y_new
115
+
116
+ def run(self, T, dt, t0=0, progress=None):
117
+ """
118
+ Method which runs the simulation on a given set of bodies.
119
+ ---------------------
120
+ Params:
121
+ T: total time (in simulation units) to run the simulation. Can have units or not, just set has_units appropriately.
122
+ dt: timestep (in simulation units) to advance the simulation. Same as above
123
+ t0 (optional): set a non-zero start time to the simulation.
124
+ progress (optional): A shiny.ui.Progress object which will be used to send progress updates.
125
+
126
+ Returns:
127
+ None, but leaves an attribute history accessed via
128
+ 'simulation.history' which contains all y vectors for the simulation.
129
+ These are of shape (Nstep,Nbodies * 6), so the x and y positions of particle 1 are
130
+ simulation.history[:,0], simulation.history[:,1], while the same for particle 2 are
131
+ simulation.history[:,6], simulation.history[:,7]. Velocities are also extractable.
132
+ """
133
+ if not hasattr(self, "calc_diff_eqs"):
134
+ raise AttributeError("You must set a diff eq solver first.")
135
+ if self.has_units:
136
+ try:
137
+ _ = t0.unit
138
+ except:
139
+ t0 = (t0 * T.unit).cgs.value
140
+ T = T.cgs.value
141
+ dt = dt.cgs.value
142
+
143
+ self.history: Any = [self.quant_vec]
144
+ clock_time = t0
145
+ nsteps = int((T - t0) / dt)
146
+ start_time = time.time()
147
+ for step in range(nsteps):
148
+ if progress is not None and step % 5 == 0:
149
+ progress.set(
150
+ step,
151
+ message=f"Integrating step = {step} / {nsteps}",
152
+ detail=f"Elapsed time = {round(clock_time/1e6, 1)}",
153
+ )
154
+ y_new = self.rk4(0, dt)
155
+ self.history.append(y_new)
156
+ self.quant_vec = y_new
157
+ clock_time += dt
158
+ runtime = time.time() - start_time
159
+ self.history = np.array(self.history)
160
+
161
+
162
+ def nbody_solve(t, y, masses):
163
+ N_bodies = int(len(y) / 6)
164
+ solved_vector = np.zeros(y.size)
165
+ for i in range(N_bodies):
166
+ ioffset = i * 6
167
+ for j in range(N_bodies):
168
+ joffset = j * 6
169
+ solved_vector[ioffset] = y[ioffset + 3]
170
+ solved_vector[ioffset + 1] = y[ioffset + 4]
171
+ solved_vector[ioffset + 2] = y[ioffset + 5]
172
+ if i != j:
173
+ dx = y[ioffset] - y[joffset]
174
+ dy = y[ioffset + 1] - y[joffset + 1]
175
+ dz = y[ioffset + 2] - y[joffset + 2]
176
+ r = (dx**2 + dy**2 + dz**2) ** 0.5
177
+ ax = (-c.G.cgs * masses[j] / r**3) * dx
178
+ ay = (-c.G.cgs * masses[j] / r**3) * dy
179
+ az = (-c.G.cgs * masses[j] / r**3) * dz
180
+ ax = ax.value
181
+ ay = ay.value
182
+ az = az.value
183
+ solved_vector[ioffset + 3] += ax
184
+ solved_vector[ioffset + 4] += ay
185
+ solved_vector[ioffset + 5] += az
186
+ return solved_vector
187
+
188
+
189
+ def spherical_to_cartesian(
190
+ theta: float, phi: float, rho: float
191
+ ) -> tuple[float, float, float]:
192
+ x = rho * sind(phi) * cosd(theta)
193
+ y = rho * sind(phi) * sind(theta)
194
+ z = rho * cosd(phi)
195
+ return (x, y, z)
196
+
197
+
198
+ def cosd(x):
199
+ return np.cos(x / 180 * np.pi)
200
+
201
+
202
+ def sind(x):
203
+ return np.sin(x / 180 * np.pi)
www/coords.png ADDED