curvesimulator 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- curvesimulator-0.1.0/PKG-INFO +34 -0
- curvesimulator-0.1.0/README.md +20 -0
- curvesimulator-0.1.0/curvesimulator/__init__.py +2 -0
- curvesimulator-0.1.0/curvesimulator/cs_animation.py +120 -0
- curvesimulator-0.1.0/curvesimulator/cs_bodies.py +160 -0
- curvesimulator-0.1.0/curvesimulator/cs_body.py +298 -0
- curvesimulator-0.1.0/curvesimulator/cs_lightcurve.py +12 -0
- curvesimulator-0.1.0/curvesimulator/cs_parameters.py +102 -0
- curvesimulator-0.1.0/curvesimulator/cs_physics.py +92 -0
- curvesimulator-0.1.0/curvesimulator/curvesim.py +58 -0
- curvesimulator-0.1.0/curvesimulator.egg-info/PKG-INFO +34 -0
- curvesimulator-0.1.0/curvesimulator.egg-info/SOURCES.txt +15 -0
- curvesimulator-0.1.0/curvesimulator.egg-info/dependency_links.txt +1 -0
- curvesimulator-0.1.0/curvesimulator.egg-info/requires.txt +3 -0
- curvesimulator-0.1.0/curvesimulator.egg-info/top_level.txt +1 -0
- curvesimulator-0.1.0/setup.cfg +4 -0
- curvesimulator-0.1.0/setup.py +26 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: curvesimulator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Curvesimulator calculates the movements and eclipses of celestial bodies and produces a video of this and of the resulting lightcurve.
|
|
5
|
+
Home-page: https://github.com/lichtgestalter/curvesimulator
|
|
6
|
+
Author: Uli Scheuss
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: configparser
|
|
14
|
+
|
|
15
|
+
# curvesim
|
|
16
|
+
A Star System and Lightcurve Simulator<br>
|
|
17
|
+
<br>
|
|
18
|
+
Curvesim produces a video of the movements and eclipses of celestial bodies and of the resulting lightcurve.<br>
|
|
19
|
+
<br>
|
|
20
|
+
Curvesim is fast and the videos use little disk space. A video takes about the same time to produce as its playing time and uses less than 0.5 MB disc space per minute.<br>
|
|
21
|
+
<br>
|
|
22
|
+
Specify mass, radius, orbital elements and other properties of some stars and planets in a configuration file.<br>
|
|
23
|
+
Then run "curvesim.py <configfilename>" to produce the video.
|
|
24
|
+
The video shows simultanously a view of the star system from the top and from the side and
|
|
25
|
+
the lightcurve of the system's total luminosity over time.<br>
|
|
26
|
+
<br>
|
|
27
|
+
Usually you do not need to look at or even modify the python code. Instead control the program's
|
|
28
|
+
outcome with the config file. The meaning of all program parameters is documented in the config file.<br>
|
|
29
|
+
<br>
|
|
30
|
+
Curvesim uses ffmpeg to convert the data into a video. <br>
|
|
31
|
+
Download ffmpeg from https://www.ffmpeg.org/download.html. <br>
|
|
32
|
+
Extract the zip file and (on Windows) add "<yourdriveandpath>\FFmpeg\bin" to the environment variable PATH.<br>
|
|
33
|
+
<br>
|
|
34
|
+
For questions and comments just open an issue on https://github.com/lichtgestalter/curvesim/issues to get my attention :)<br>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# curvesim
|
|
2
|
+
A Star System and Lightcurve Simulator<br>
|
|
3
|
+
<br>
|
|
4
|
+
Curvesim produces a video of the movements and eclipses of celestial bodies and of the resulting lightcurve.<br>
|
|
5
|
+
<br>
|
|
6
|
+
Curvesim is fast and the videos use little disk space. A video takes about the same time to produce as its playing time and uses less than 0.5 MB disc space per minute.<br>
|
|
7
|
+
<br>
|
|
8
|
+
Specify mass, radius, orbital elements and other properties of some stars and planets in a configuration file.<br>
|
|
9
|
+
Then run "curvesim.py <configfilename>" to produce the video.
|
|
10
|
+
The video shows simultanously a view of the star system from the top and from the side and
|
|
11
|
+
the lightcurve of the system's total luminosity over time.<br>
|
|
12
|
+
<br>
|
|
13
|
+
Usually you do not need to look at or even modify the python code. Instead control the program's
|
|
14
|
+
outcome with the config file. The meaning of all program parameters is documented in the config file.<br>
|
|
15
|
+
<br>
|
|
16
|
+
Curvesim uses ffmpeg to convert the data into a video. <br>
|
|
17
|
+
Download ffmpeg from https://www.ffmpeg.org/download.html. <br>
|
|
18
|
+
Extract the zip file and (on Windows) add "<yourdriveandpath>\FFmpeg\bin" to the environment variable PATH.<br>
|
|
19
|
+
<br>
|
|
20
|
+
For questions and comments just open an issue on https://github.com/lichtgestalter/curvesim/issues to get my attention :)<br>
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
import matplotlib
|
|
4
|
+
import numpy as np
|
|
5
|
+
import matplotlib.pyplot as plt # from matplotlib import pyplot as plt
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class CurveSimAnimation:
|
|
9
|
+
|
|
10
|
+
def __init__(self, p, bodies, lightcurve):
|
|
11
|
+
sampled_lightcurve = np.take(lightcurve, range(0, p.iterations, p.sampling_rate)) # Use only some of the calculated positions for the animation because it is so slow.
|
|
12
|
+
self.fig, ax_right, ax_left, ax_lightcurve, self.red_dot = CurveSimAnimation.init_plot(p, sampled_lightcurve, lightcurve) # Adjust constants in section [Plot] of config file to fit your screen.
|
|
13
|
+
for body in bodies: # Circles represent the bodies in the animation. Set their colors and add them to the matplotlib axis.
|
|
14
|
+
body.circle_right.set_color(body.color)
|
|
15
|
+
body.circle_left.set_color(body.color)
|
|
16
|
+
ax_right.add_patch(body.circle_right)
|
|
17
|
+
ax_left.add_patch(body.circle_left)
|
|
18
|
+
self.render(p, bodies, lightcurve)
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def tic_delta(scope):
|
|
22
|
+
"""Returns a distance between two tics on an axis so that the total
|
|
23
|
+
number of tics on that axis is between 5 and 10."""
|
|
24
|
+
if scope <= 0: # no or constant values
|
|
25
|
+
return 1
|
|
26
|
+
delta = 10 ** np.floor(math.log10(scope))
|
|
27
|
+
if scope / delta < 5:
|
|
28
|
+
if scope / delta < 2:
|
|
29
|
+
return delta / 5
|
|
30
|
+
else:
|
|
31
|
+
return delta / 2
|
|
32
|
+
else:
|
|
33
|
+
return delta
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def init_plot(p, sampled_lightcurve, lightcurve):
|
|
37
|
+
"""Initialize the matplotlib figure containing 3 axis:
|
|
38
|
+
Top left: projection (x,y,z) -> (x,z), order = -y.
|
|
39
|
+
Top right: projection (x,y,z) -> (x,y), order = z.
|
|
40
|
+
Bottom: lightcurve"""
|
|
41
|
+
fig = plt.figure()
|
|
42
|
+
fig.set_figwidth(p.figure_width)
|
|
43
|
+
fig.set_figheight(p.figure_height)
|
|
44
|
+
fig.set_facecolor("black") # background color outside of ax_left and ax_lightcurve
|
|
45
|
+
buffer = 0
|
|
46
|
+
fig.subplots_adjust(left=buffer, right=1.0 - buffer, bottom=buffer, top=1 - buffer) # Positions of the subplots edges, as a fraction of the figure width.
|
|
47
|
+
|
|
48
|
+
ax_left = plt.subplot2grid(shape=(5, 2), loc=(0, 0), rowspan=4, colspan=1)
|
|
49
|
+
ax_left.set_xlim(-p.xlim, p.xlim)
|
|
50
|
+
ax_left.set_ylim(-p.ylim, p.ylim)
|
|
51
|
+
ax_left.set_aspect('equal')
|
|
52
|
+
ax_left.set_facecolor("black") # background color
|
|
53
|
+
# ax_left.get_xaxis().set_visible(False)
|
|
54
|
+
# ax_left.get_yaxis().set_visible(False)
|
|
55
|
+
|
|
56
|
+
ax_right = plt.subplot2grid(shape=(5, 2), loc=(0, 1), rowspan=4, colspan=1)
|
|
57
|
+
ax_right.set_xlim(-p.xlim, p.xlim)
|
|
58
|
+
ax_right.set_ylim(-p.ylim, p.ylim)
|
|
59
|
+
ax_right.set_aspect('equal')
|
|
60
|
+
ax_right.set_facecolor("black") # background color
|
|
61
|
+
|
|
62
|
+
ax_lightcurve = plt.subplot2grid(shape=(5, 1), loc=(4, 0), rowspan=1, colspan=1)
|
|
63
|
+
ax_lightcurve.set_facecolor("black") # background color
|
|
64
|
+
|
|
65
|
+
ax_lightcurve.tick_params(axis='x', colors='grey')
|
|
66
|
+
xmax = p.iterations * p.dt / p.x_unit_value
|
|
67
|
+
ax_lightcurve.set_xlim(0, xmax)
|
|
68
|
+
xvalues = [x * CurveSimAnimation.tic_delta(xmax) for x in range(round(xmax / CurveSimAnimation.tic_delta(xmax)))]
|
|
69
|
+
xlabels = [f'{round(x, 4)} {p.x_unit_name}' for x in xvalues]
|
|
70
|
+
ax_lightcurve.set_xticks(xvalues, labels=xlabels)
|
|
71
|
+
|
|
72
|
+
ax_lightcurve.tick_params(axis='y', colors='grey')
|
|
73
|
+
minl = lightcurve.min(initial=None)
|
|
74
|
+
maxl = lightcurve.max(initial=None)
|
|
75
|
+
if minl == maxl:
|
|
76
|
+
minl *= 0.99
|
|
77
|
+
scope = maxl - minl
|
|
78
|
+
buffer = 0.05 * scope
|
|
79
|
+
ax_lightcurve.set_ylim(minl - buffer, maxl + buffer)
|
|
80
|
+
|
|
81
|
+
ticdelta = CurveSimAnimation.tic_delta(maxl - minl)
|
|
82
|
+
yvalues = [1 - y * ticdelta for y in range(round(float((maxl - minl) / ticdelta)))]
|
|
83
|
+
ylabels = [f'{round(100 * y, 10)} %' for y in yvalues]
|
|
84
|
+
ax_lightcurve.set_yticks(yvalues, labels=ylabels)
|
|
85
|
+
|
|
86
|
+
time_axis = np.arange(0, round(p.iterations * p.dt), round(p.sampling_rate * p.dt), dtype=float)
|
|
87
|
+
time_axis /= p.x_unit_value
|
|
88
|
+
ax_lightcurve.plot(time_axis, sampled_lightcurve[0:len(time_axis)], color="white")
|
|
89
|
+
|
|
90
|
+
red_dot = matplotlib.patches.Ellipse((0, 0), p.iterations * p.dt * p.red_dot_width / p.x_unit_value, scope * p.red_dot_height) # matplotlib patch
|
|
91
|
+
red_dot.set(zorder=2) # Dot in front of lightcurve.
|
|
92
|
+
red_dot.set_color((1, 0, 0)) # red
|
|
93
|
+
ax_lightcurve.add_patch(red_dot)
|
|
94
|
+
plt.tight_layout() # Automatically adjust padding horizontally as well as vertically.
|
|
95
|
+
return fig, ax_right, ax_left, ax_lightcurve, red_dot
|
|
96
|
+
|
|
97
|
+
@staticmethod
|
|
98
|
+
def next_frame(frame, p, bodies, red_dot, lightcurve):
|
|
99
|
+
"""Update patches. Send new circle positions to animation function.
|
|
100
|
+
First parameter comes from iterator frames (a parameter of FuncAnimation).
|
|
101
|
+
The other parameters are given to this function via the parameter fargs of FuncAnimation."""
|
|
102
|
+
for body in bodies: # right view: projection (x,y,z) -> (x,y), order = z (z-axis points to viewer)
|
|
103
|
+
body.circle_right.set(zorder=body.positions[frame * p.sampling_rate][2])
|
|
104
|
+
body.circle_right.center = body.positions[frame * p.sampling_rate][0] / p.scope_right, body.positions[frame * p.sampling_rate][1] / p.scope_right
|
|
105
|
+
for body in bodies: # left view: projection (x,y,z) -> (x,z), order = -y (y-axis points away from viewer)
|
|
106
|
+
body.circle_left.set(zorder=-body.positions[frame * p.sampling_rate][1])
|
|
107
|
+
body.circle_left.center = body.positions[frame * p.sampling_rate][0] / p.scope_left, body.positions[frame * p.sampling_rate][2] / p.scope_left
|
|
108
|
+
red_dot.center = p.dt * p.sampling_rate * frame / p.x_unit_value, lightcurve[frame * p.sampling_rate]
|
|
109
|
+
if frame >= 10 and frame % int(round(p.frames / 10)) == 0: # Inform user about program's progress.
|
|
110
|
+
print(f'{round(frame / p.frames * 10) * 10:3d}% ', end="")
|
|
111
|
+
|
|
112
|
+
def render(self, p, bodies, lightcurve):
|
|
113
|
+
"""Calls next_frame() for each frame and saves the video."""
|
|
114
|
+
print(f'Animating {p.frames:8d} frames: ', end="")
|
|
115
|
+
tic = time.perf_counter()
|
|
116
|
+
anim = matplotlib.animation.FuncAnimation(self.fig, CurveSimAnimation.next_frame, fargs=(p, bodies, self.red_dot, lightcurve), interval=1000 / p.fps, frames=p.frames, blit=False)
|
|
117
|
+
anim.save(p.video_file, fps=p.fps, metadata={"title": " "}, extra_args=['-vcodec', 'libx264']) # https://www.ffmpeg.org/libavcodec.html
|
|
118
|
+
toc = time.perf_counter()
|
|
119
|
+
print(f' {toc - tic:7.2f} seconds ({p.frames / (toc - tic):.0f} frames/second)')
|
|
120
|
+
print(f'{p.video_file} saved.')
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import math
|
|
3
|
+
import time
|
|
4
|
+
import matplotlib
|
|
5
|
+
import matplotlib.animation
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from cs_body import CurveSimBody
|
|
9
|
+
from cs_lightcurve import CurveSimLightcurve
|
|
10
|
+
from cs_physics import CurveSimPhysics
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CurveSimBodies(list):
|
|
14
|
+
|
|
15
|
+
# noinspection PyUnusedLocal
|
|
16
|
+
def __init__(self, p, debug_L=-1):
|
|
17
|
+
"""Initialize instances of physical bodies.
|
|
18
|
+
Read program parameters and properties of the bodies from config file.
|
|
19
|
+
Initialize the circles in the animation (matplotlib patches)"""
|
|
20
|
+
# For ease of use of these constants in the config file they are additionally defined here without the prefix "p.".
|
|
21
|
+
g, au, r_sun, m_sun, l_sun = p.g, p.au, p.r_sun, p.m_sun, p.l_sun
|
|
22
|
+
r_jup, m_jup, r_earth, m_earth, v_earth = p.r_jup, p.m_jup, p.r_earth, p.m_earth, p.v_earth
|
|
23
|
+
config = configparser.ConfigParser(inline_comment_prefixes='#')
|
|
24
|
+
config.read(p.configfilename) # Read config file. (This time the physical objects.)
|
|
25
|
+
super().__init__() # create object by calling the constructor of class list
|
|
26
|
+
|
|
27
|
+
# Physical bodies
|
|
28
|
+
for section in config.sections():
|
|
29
|
+
if section not in p.standard_sections: # section describes a physical object
|
|
30
|
+
self.append(CurveSimBody(p=p,
|
|
31
|
+
name=section,
|
|
32
|
+
body_type=config.get(section, "body_type"),
|
|
33
|
+
mass=eval(config.get(section, "mass")),
|
|
34
|
+
radius=eval(config.get(section, "radius")),
|
|
35
|
+
luminosity=None if config.get(section, "luminosity", fallback=None) is None else eval(config.get(section, "luminosity")),
|
|
36
|
+
startposition=config.get(section, "startposition", fallback=None),
|
|
37
|
+
velocity=config.get(section, "velocity", fallback=None),
|
|
38
|
+
a=None if config.get(section, "a", fallback=None) is None else eval(config.get(section, "a")), # a bit long-winded because eval() cannot handle None
|
|
39
|
+
e=None if config.get(section, "e", fallback=None) is None else eval(config.get(section, "e")),
|
|
40
|
+
i=None if config.get(section, "i", fallback=None) is None else eval(config.get(section, "i")),
|
|
41
|
+
Ω=None if config.get(section, "longitude_of_ascending_node", fallback=None) is None else eval(config.get(section, "longitude_of_ascending_node")),
|
|
42
|
+
ω=None if config.get(section, "argument_of_periapsis", fallback=None) is None else eval(config.get(section, "argument_of_periapsis")),
|
|
43
|
+
ϖ=None if config.get(section, "longitude_of_periapsis", fallback=None) is None else eval(config.get(section, "longitude_of_periapsis")),
|
|
44
|
+
L=None if config.get(section, "L", fallback=None) is None else eval(config.get(section, "L")),
|
|
45
|
+
ma=None if config.get(section, "ma", fallback=None) is None else eval(config.get(section, "ma")),
|
|
46
|
+
ea=None if config.get(section, "ea", fallback=None) is None else eval(config.get(section, "ea")),
|
|
47
|
+
nu=None if config.get(section, "nu", fallback=None) is None else eval(config.get(section, "nu")),
|
|
48
|
+
T=None if config.get(section, "T", fallback=None) is None else eval(config.get(section, "T")),
|
|
49
|
+
t=None if config.get(section, "t", fallback=None) is None else eval(config.get(section, "t")),
|
|
50
|
+
beta=eval(config.get(section, "beta")),
|
|
51
|
+
color=tuple([eval(x) for x in config.get(section, "color").split(",")])))
|
|
52
|
+
# Checking parameters of physical bodies
|
|
53
|
+
if len(self) < 1:
|
|
54
|
+
raise Exception("No physical bodies specified.")
|
|
55
|
+
for body in self:
|
|
56
|
+
if body.radius <= 0:
|
|
57
|
+
raise Exception(f'{body.name} has invalid radius {body.radius}.')
|
|
58
|
+
if body.mass <= 0:
|
|
59
|
+
raise Exception(f'{body.name} has invalid mass {body.mass}.')
|
|
60
|
+
if body.luminosity < 0:
|
|
61
|
+
raise Exception(f'{body.name} has invalid luminosity {body.luminosity}.')
|
|
62
|
+
if body.luminosity > 0 >= body.beta: # if body.luminosity > 0 and body.beta <= 0:
|
|
63
|
+
raise Exception(f'{body.name} has invalid limb darkening parameter beta {body.beta}.')
|
|
64
|
+
for c in body.color:
|
|
65
|
+
if c < 0 or c > 1:
|
|
66
|
+
raise Exception(f'{body.name} has invalid color value {c}.')
|
|
67
|
+
if debug_L >= 0 and body.name == "Test":
|
|
68
|
+
body.L = debug_L/180.0 * math.pi
|
|
69
|
+
body.calc_state_vectors(p, self)
|
|
70
|
+
self.generate_patches(p)
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
names = "CurveSimBodies: "
|
|
74
|
+
for body in self:
|
|
75
|
+
names += body.name + ", "
|
|
76
|
+
return names[:-2]
|
|
77
|
+
|
|
78
|
+
def total_luminosity(self, stars, iteration):
|
|
79
|
+
""""Add luminosity of all stars in the system while checking for eclipses.
|
|
80
|
+
Does not yet work correctly for eclipsed eclipses (three or more bodies in line of sight at the same time)."""
|
|
81
|
+
luminosity = 0.0
|
|
82
|
+
for star in stars:
|
|
83
|
+
luminosity += star.luminosity
|
|
84
|
+
for body in self:
|
|
85
|
+
if body != star: # an object cannot eclipse itself :)
|
|
86
|
+
eclipsed_area, relative_radius = star.eclipsed_by(body, iteration)
|
|
87
|
+
if eclipsed_area != 0:
|
|
88
|
+
luminosity -= star.brightness * eclipsed_area * CurveSimPhysics.limbdarkening(relative_radius, star.beta)
|
|
89
|
+
return luminosity
|
|
90
|
+
|
|
91
|
+
def calc_positions_eclipses_luminosity(self, p):
|
|
92
|
+
"""Calculate distances, forces, accelerations, velocities of the bodies for each iteration.
|
|
93
|
+
The resulting body positions and the lightcurve are stored for later use in the animation.
|
|
94
|
+
Body motion calculations inspired by https://colab.research.google.com/drive/1YKjSs8_giaZVrUKDhWLnUAfebuLTC-A5."""
|
|
95
|
+
stars = [body for body in self if body.body_type == "star"]
|
|
96
|
+
lightcurve = CurveSimLightcurve(p.iterations) # Initialize lightcurve (essentially a np.ndarray)
|
|
97
|
+
lightcurve[0] = self.total_luminosity(stars, 0)
|
|
98
|
+
for iteration in range(1, p.iterations):
|
|
99
|
+
for body1 in self:
|
|
100
|
+
force = np.array([0.0, 0.0, 0.0])
|
|
101
|
+
for body2 in self:
|
|
102
|
+
if body1 != body2:
|
|
103
|
+
# Calculate distances between bodies:
|
|
104
|
+
distance_xyz = body2.positions[iteration - 1] - body1.positions[iteration - 1]
|
|
105
|
+
distance = math.sqrt(np.dot(distance_xyz, distance_xyz))
|
|
106
|
+
force_total = p.g * body1.mass * body2.mass / distance ** 2 # Use law of gravitation to calculate force acting on body.
|
|
107
|
+
# Compute the force of attraction in each direction:
|
|
108
|
+
x, y, z = distance_xyz[0], distance_xyz[1], distance_xyz[2]
|
|
109
|
+
polar_angle = math.acos(z / distance)
|
|
110
|
+
azimuth_angle = math.atan2(y, x)
|
|
111
|
+
force[0] += math.sin(polar_angle) * math.cos(azimuth_angle) * force_total
|
|
112
|
+
force[1] += math.sin(polar_angle) * math.sin(azimuth_angle) * force_total
|
|
113
|
+
force[2] += math.cos(polar_angle) * force_total
|
|
114
|
+
acceleration = force / body1.mass # Compute the acceleration in each direction.
|
|
115
|
+
body1.velocity += acceleration * p.dt # Compute the velocity in each direction.
|
|
116
|
+
# Update positions:
|
|
117
|
+
movement = body1.velocity * p.dt - 0.5 * acceleration * p.dt ** 2
|
|
118
|
+
body1.positions[iteration] = body1.positions[iteration - 1] + movement
|
|
119
|
+
lightcurve[iteration] = self.total_luminosity(stars, iteration) # Update lightcurve.
|
|
120
|
+
if iteration % int(round(p.iterations / 10)) == 0: # Inform user about program's progress.
|
|
121
|
+
print(f'{round(iteration / p.iterations * 10) * 10:3d}% ', end="")
|
|
122
|
+
return lightcurve, self
|
|
123
|
+
|
|
124
|
+
def calc_physics(self, p):
|
|
125
|
+
"""Calculate body positions and the resulting lightcurve."""
|
|
126
|
+
print(f'Producing {p.frames / p.fps:.0f} seconds long video, covering {p.dt * p.iterations / 60 / 60 / 24:5.2f} '
|
|
127
|
+
f'earth days. ({p.dt * p.sampling_rate * p.fps / 60 / 60 / 24:.2f} earth days per video second.)')
|
|
128
|
+
print(f'Calculating {p.iterations:6d} iterations: ', end="")
|
|
129
|
+
tic = time.perf_counter()
|
|
130
|
+
lightcurve, bodies = self.calc_positions_eclipses_luminosity(p)
|
|
131
|
+
lightcurve /= lightcurve.max(initial=None) # Normalize flux.
|
|
132
|
+
toc = time.perf_counter()
|
|
133
|
+
print(f' {toc - tic:7.2f} seconds ({p.iterations / (toc - tic):.0f} iterations/second)')
|
|
134
|
+
return lightcurve
|
|
135
|
+
|
|
136
|
+
def calc_patch_radii(self, p):
|
|
137
|
+
"""If autoscaling is on, this function calculates the radii of the circles (matplotlib patches) of the animation."""
|
|
138
|
+
logs = [math.log10(body.radius) for body in self] # log10 of all radii
|
|
139
|
+
radii_out = [(p.max_radius - p.min_radius) * (i - min(logs)) / (max(logs) - min(logs)) + p.min_radius for i in logs] # linear transformation to match the desired minmum and maximum radii
|
|
140
|
+
# print(f'patch radii:', end=" ")
|
|
141
|
+
for body, radius in zip(self, radii_out):
|
|
142
|
+
body.patch_radius = radius
|
|
143
|
+
# print(f'{body.name}: {body.patch_radius:.4f} ', end=" ")
|
|
144
|
+
# print()
|
|
145
|
+
|
|
146
|
+
def generate_patches(self, p):
|
|
147
|
+
"""Generates the circles (matplotlib patches) of the animation."""
|
|
148
|
+
if p.autoscaling:
|
|
149
|
+
self.calc_patch_radii(p)
|
|
150
|
+
for body in self:
|
|
151
|
+
body.circle_right = matplotlib.patches.Circle(xy=(0, 0), radius=body.patch_radius) # Matplotlib patch for right view
|
|
152
|
+
body.circle_left = matplotlib.patches.Circle(xy=(0, 0), radius=body.patch_radius) # Matplotlib patch for left view
|
|
153
|
+
else:
|
|
154
|
+
for body in self:
|
|
155
|
+
if body.body_type == "planet":
|
|
156
|
+
extrascale_left, extrascale_right = p.planet_scale_left, p.planet_scale_right # Scale radius in plot.
|
|
157
|
+
else:
|
|
158
|
+
extrascale_left, extrascale_right = p.star_scale_left, p.star_scale_right # It's a star. Scale radius in plot accordingly.
|
|
159
|
+
body.circle_right = matplotlib.patches.Circle((0, 0), radius=body.radius * extrascale_right / p.scope_right) # Matplotlib patch for right view
|
|
160
|
+
body.circle_left = matplotlib.patches.Circle((0, 0), radius=body.radius * extrascale_left / p.scope_left) # Matplotlib patch for left view
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
from cs_physics import CurveSimPhysics
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# noinspection NonAsciiCharacters,PyPep8Naming,PyUnusedLocal
|
|
8
|
+
class CurveSimBody:
|
|
9
|
+
|
|
10
|
+
def __init__(self, p, name, body_type, mass, radius, luminosity, startposition, velocity, a, e, i, Ω, ω, ϖ, L, ma, ea,
|
|
11
|
+
nu, T, t, beta, color):
|
|
12
|
+
"""Initialize instance of physical body."""
|
|
13
|
+
# For ease of use of constants in the config file they are additionally defined here without the prefix "p.".
|
|
14
|
+
g, au, r_sun, m_sun, l_sun = p.g, p.au, p.r_sun, p.m_sun, p.l_sun
|
|
15
|
+
r_jup, m_jup, r_earth, m_earth, v_earth = p.r_jup, p.m_jup, p.r_earth, p.m_earth, p.v_earth
|
|
16
|
+
self.name = name # name
|
|
17
|
+
self.body_type = body_type # "star" or "planet"
|
|
18
|
+
self.mass = mass # [kg]
|
|
19
|
+
self.radius = radius # [m]
|
|
20
|
+
self.area_2d = math.pi * radius ** 2 # [m**2]
|
|
21
|
+
self.luminosity = luminosity # [W]
|
|
22
|
+
self.brightness = luminosity / self.area_2d # luminosity per (apparent) area [W/m**2]
|
|
23
|
+
self.positions = np.zeros((p.iterations, 3), dtype=float) # position for each frame
|
|
24
|
+
self.color = color # (R, G, B) each between 0 and 1
|
|
25
|
+
|
|
26
|
+
if body_type == "planet":
|
|
27
|
+
self.a = a # [m] semi-major axis
|
|
28
|
+
self.e = e # [1] eccentricity
|
|
29
|
+
self.i = None if i is None else math.radians(i) # [deg] inclination
|
|
30
|
+
self.Ω = None if Ω is None else math.radians(Ω) # [deg] longitude of ascending node
|
|
31
|
+
self.ω = None if ω is None else math.radians(ω) # [deg] argument of periapsis
|
|
32
|
+
self.ϖ = None if ϖ is None else math.radians(ϖ) # [deg] longitude of periapsis
|
|
33
|
+
self.L = None if L is None else math.radians(L) # [deg] mean longitude
|
|
34
|
+
self.ma = None if ma is None else math.radians(ma) # [deg] mean anomaly
|
|
35
|
+
self.ea = None if ea is None else math.radians(ea) # [deg] eccentric anomaly
|
|
36
|
+
self.nu = None if nu is None else math.radians(nu) # [deg] true anomaly. Per definition = 270° at the time of an exoplanet's primary transit.
|
|
37
|
+
self.T = T # [s] Time of periapsis
|
|
38
|
+
self.t = t # [s] time since last time of transit
|
|
39
|
+
self.ma, self.ea, self.T = None, None, None # [rad] Only true anomaly or mean_anomaly or eccentric_anomaly or time_of_periapsis has to be provided.
|
|
40
|
+
self.mu = None # Gravitational Parameter. Depends on the masses of at least 2 bodies.
|
|
41
|
+
self.beta = None # unnecessary line of code?
|
|
42
|
+
|
|
43
|
+
if body_type == "star":
|
|
44
|
+
self.beta = beta # [1] limb darkening
|
|
45
|
+
|
|
46
|
+
if startposition is not None and velocity is not None: # State vectors are already in config file.
|
|
47
|
+
pos = []
|
|
48
|
+
for x in startposition.split(","):
|
|
49
|
+
pos.append(eval(x))
|
|
50
|
+
vel = []
|
|
51
|
+
for x in velocity.split(","):
|
|
52
|
+
vel.append(eval(x))
|
|
53
|
+
self.positions[0] = np.array(pos, dtype=float) # [m] initial position
|
|
54
|
+
self.velocity = np.array(vel, dtype=float) # [m/s]
|
|
55
|
+
else: # State vectors are not in config file. They will be calculated from Kepler orbit parameters later on after all bodies are initialized.
|
|
56
|
+
self.velocity = None
|
|
57
|
+
|
|
58
|
+
# Used for calculation of eclipsed area in function eclipsed_by.
|
|
59
|
+
self.d, self.h, self.angle, self.eclipsed_area = 0.0, 0.0, 0.0, 0.0
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
return f'CurveSimBody: {self.name}'
|
|
63
|
+
|
|
64
|
+
# noinspection NonAsciiCharacters,PyPep8Naming,PyUnusedLocal
|
|
65
|
+
def keplerian_elements_to_state_vectors_debug_new(self):
|
|
66
|
+
"""
|
|
67
|
+
Version of keplerian_elements_to_state_vectors() using alternative formulas from source [f] instead of [b] for the initial position.
|
|
68
|
+
[f]: https://www.researchgate.net/publication/232203657_Orbital_Ephemerides_of_the_Sun_Moon_and_Planets, Section 8.10
|
|
69
|
+
"""
|
|
70
|
+
a, e, i, Ω, ω, ϖ, L = self.a, self.e, self.i, self.Ω, self.ω, self.ϖ, self.L # for readability of formulas
|
|
71
|
+
ma, ea, nu, T, t, mu = self.ma, self.ea, self.nu, self.T, self.t, self.mu # for readability of formulas
|
|
72
|
+
|
|
73
|
+
ω = ϖ - Ω # [f]8-30
|
|
74
|
+
ma = L - ϖ # [f]8-30
|
|
75
|
+
|
|
76
|
+
ea = CurveSimPhysics.kepler_equation_root_debug(e, ma, ea_guess=ma) # [d], [e]. Maybe implement alternative version from [f]8-31 and [f]8.10.2???
|
|
77
|
+
|
|
78
|
+
x_ = a * (math.cos(ea) - e) # [f]8-32
|
|
79
|
+
y_ = a * math.sqrt(1 - e * e) * math.sin(ea) # [f]8-32
|
|
80
|
+
z_ = 0 # [f]8-32
|
|
81
|
+
x = x_ * (math.cos(Ω) * math.cos(ω) - math.sin(Ω) * math.sin(ω) * math.cos(i)) # [f]8-34 maybe replace ω with ω everywhere in [f]8-34?
|
|
82
|
+
x += y_ * (-math.sin(ω) * math.cos(Ω) - math.cos(ω) * math.sin(Ω) * math.cos(i)) # [f]8-34
|
|
83
|
+
y = x_ * (math.sin(Ω) * math.cos(ω) + math.cos(Ω) * math.sin(ω) * math.cos(i)) # [f]8-34
|
|
84
|
+
y += y_ * (-math.sin(ω) * math.sin(Ω) + math.cos(ω) * math.cos(Ω) * math.cos(i)) # [f]8-34
|
|
85
|
+
z = x_ * math.sin(i) * math.sin(ω) # [f]8-34
|
|
86
|
+
z += y_ * math.cos(ω) * math.sin(i) # [f]8-34
|
|
87
|
+
|
|
88
|
+
nu = 2 * math.atan(math.sqrt((1 + e) / (1 - e)) * math.tan(ea / 2)) # 3b: true anomaly (from eccentric anomaly)
|
|
89
|
+
r = a * (1 - e * math.cos(ea)) # 4b: radius r
|
|
90
|
+
h = math.sqrt(mu * a * (1 - e ** 2)) # 5b: specific angular momentum h
|
|
91
|
+
|
|
92
|
+
p = a * (1 - e ** 2) # 7b: Semi-latus rectum. Used in velocity calculation.
|
|
93
|
+
dx = (x * h * e / (r * p)) * math.sin(nu) - (h / r) * (math.cos(Ω) * math.sin(ω + nu) + math.sin(Ω) * math.cos(ω + nu) * math.cos(i)) # 7b: velocity component x
|
|
94
|
+
dy = (y * h * e / (r * p)) * math.sin(nu) - (h / r) * (math.sin(Ω) * math.sin(ω + nu) - math.cos(Ω) * math.cos(ω + nu) * math.cos(i)) # 7b: velocity component y
|
|
95
|
+
dz = (z * h * e / (r * p)) * math.sin(nu) + (h / r) * (math.cos(ω + nu) * math.sin(i)) # 7b: velocity component z
|
|
96
|
+
return np.array([x, y, z]), np.array([dx, dy, dz]), nu, ma, ea, T # state vectors
|
|
97
|
+
|
|
98
|
+
def keplerian_elements_to_state_vectors_debug(self):
|
|
99
|
+
"""
|
|
100
|
+
Shortened version of keplerian_elements_to_state_vectors()
|
|
101
|
+
for the case where L, ϖ and Ω are known.
|
|
102
|
+
"""
|
|
103
|
+
a, e, i, Ω, ω, ϖ, L = self.a, self.e, self.i, self.Ω, self.ω, self.ϖ, self.L # for readability of formulas
|
|
104
|
+
ma, ea, nu, T, t, mu = self.ma, self.ea, self.nu, self.T, self.t, self.mu # for readability of formulas
|
|
105
|
+
|
|
106
|
+
ω = ϖ - Ω
|
|
107
|
+
ma = L - ϖ
|
|
108
|
+
ea = CurveSimPhysics.kepler_equation_root(e, ma, ea_guess=ma)
|
|
109
|
+
nu = 2 * math.atan(math.sqrt((1 + e) / (1 - e)) * math.tan(ea / 2)) # 3b: true anomaly (from eccentric anomaly)
|
|
110
|
+
r = a * (1 - e * math.cos(ea)) # 4b: radius r
|
|
111
|
+
h = math.sqrt(mu * a * (1 - e ** 2)) # 5b: specific angular momentum h
|
|
112
|
+
|
|
113
|
+
x = r * (math.cos(Ω) * math.cos(ω + nu) - math.sin(Ω) * math.sin(ω + nu) * math.cos(i)) # 6b: position component x
|
|
114
|
+
y = r * (math.sin(Ω) * math.cos(ω + nu) + math.cos(Ω) * math.sin(ω + nu) * math.cos(i)) # 6b: position component y
|
|
115
|
+
z = r * (math.sin(i) * math.sin(ω + nu)) # 6b: position component z
|
|
116
|
+
|
|
117
|
+
p = a * (1 - e ** 2) # 7b: Semi-latus rectum. Used in velocity calculation.
|
|
118
|
+
dx = (x * h * e / (r * p)) * math.sin(nu) - (h / r) * (math.cos(Ω) * math.sin(ω + nu) + math.sin(Ω) * math.cos(ω + nu) * math.cos(i)) # 7b: velocity component x
|
|
119
|
+
dy = (y * h * e / (r * p)) * math.sin(nu) - (h / r) * (math.sin(Ω) * math.sin(ω + nu) - math.cos(Ω) * math.cos(ω + nu) * math.cos(i)) # 7b: velocity component y
|
|
120
|
+
dz = (z * h * e / (r * p)) * math.sin(nu) + (h / r) * (math.cos(ω + nu) * math.sin(i)) # 7b: velocity component z
|
|
121
|
+
return np.array([x, y, z]), np.array([dx, dy, dz]), nu, ma, ea, T # state vectors
|
|
122
|
+
|
|
123
|
+
def keplerian_elements_to_state_vectors_chatgpt(self):
|
|
124
|
+
"""Calculates the state vectors (position and velocity) from Keplerian Orbit Elements.
|
|
125
|
+
Returns also true anomaly, eccentric anomaly, mean anomaly and the time of periapsis."""
|
|
126
|
+
a, e, i, Ω, ω, ϖ, L = self.a, self.e, self.i, self.Ω, self.ω, self.ϖ, self.L # for readability of formulas
|
|
127
|
+
ma, ea, nu, T, t, mu = self.ma, self.ea, self.nu, self.T, self.t, self.mu # for readability of formulas
|
|
128
|
+
print("hej, chatgpt")
|
|
129
|
+
# Calculate ω if necessary
|
|
130
|
+
if ω is None and ϖ is not None and Ω is not None:
|
|
131
|
+
ω = ϖ - Ω
|
|
132
|
+
|
|
133
|
+
# Calculate mean anomaly if necessary
|
|
134
|
+
if ma is None and L is not None and ϖ is not None:
|
|
135
|
+
ma = L - ϖ
|
|
136
|
+
|
|
137
|
+
# Eccentric anomaly and true anomaly
|
|
138
|
+
if ea is not None:
|
|
139
|
+
# Calculate true anomaly from eccentric anomaly
|
|
140
|
+
nu = 2 * math.atan2(math.sqrt(1 + e) * math.sin(ea / 2), math.sqrt(1 - e) * math.cos(ea / 2))
|
|
141
|
+
# Calculate mean anomaly from eccentric anomaly
|
|
142
|
+
ma = ea - e * math.sin(ea)
|
|
143
|
+
else:
|
|
144
|
+
if nu is not None:
|
|
145
|
+
# Calculate eccentric anomaly from true anomaly
|
|
146
|
+
ea = 2 * math.atan2(math.sqrt(1 - e) * math.sin(nu / 2), math.sqrt(1 + e) * math.cos(nu / 2))
|
|
147
|
+
elif ma is not None:
|
|
148
|
+
# Solve for eccentric anomaly using Kepler's equation
|
|
149
|
+
ea = CurveSimPhysics.kepler_equation_root_chatgpt(e, ma, ea_guess=ma)
|
|
150
|
+
# Calculate true anomaly from eccentric anomaly
|
|
151
|
+
nu = 2 * math.atan2(math.sqrt(1 + e) * math.sin(ea / 2), math.sqrt(1 - e) * math.cos(ea / 2))
|
|
152
|
+
elif T is not None:
|
|
153
|
+
# Calculate mean motion
|
|
154
|
+
n = math.sqrt(mu / (a ** 3))
|
|
155
|
+
# Calculate mean anomaly at time of periapsis
|
|
156
|
+
ma = n * (t - T)
|
|
157
|
+
# Solve for eccentric anomaly using Kepler's equation
|
|
158
|
+
ea = CurveSimPhysics.kepler_equation_root_chatgpt(e, ma, ea_guess=ma)
|
|
159
|
+
# Calculate true anomaly from eccentric anomaly
|
|
160
|
+
nu = 2 * math.atan2(math.sqrt(1 + e) * math.sin(ea / 2), math.sqrt(1 - e) * math.cos(ea / 2))
|
|
161
|
+
else:
|
|
162
|
+
raise Exception("nu or ma or ea or T has to be provided to keplerian_elements_to_state_vectors()")
|
|
163
|
+
|
|
164
|
+
# Calculate mean angular motion
|
|
165
|
+
n = math.sqrt(mu / (a ** 3))
|
|
166
|
+
|
|
167
|
+
# Calculate time of periapsis passage
|
|
168
|
+
T = t - (ma / n)
|
|
169
|
+
|
|
170
|
+
# Position in orbital plane
|
|
171
|
+
r = a * (1 - e * math.cos(ea))
|
|
172
|
+
x_orb = r * math.cos(nu)
|
|
173
|
+
y_orb = r * math.sin(nu)
|
|
174
|
+
|
|
175
|
+
# Velocity in orbital plane
|
|
176
|
+
vx_orb = -math.sqrt(mu / (a * (1 - e ** 2))) * math.sin(ea)
|
|
177
|
+
vy_orb = math.sqrt(mu / (a * (1 - e ** 2))) * (math.sqrt(1 - e ** 2) * math.cos(ea))
|
|
178
|
+
|
|
179
|
+
# Rotation matrices for inclination, ascending node, and argument of periapsis
|
|
180
|
+
cos_Omega, sin_Omega = math.cos(Ω), math.sin(Ω)
|
|
181
|
+
cos_omega, sin_omega = math.cos(ω), math.sin(ω)
|
|
182
|
+
cos_i, sin_i = math.cos(i), math.sin(i)
|
|
183
|
+
|
|
184
|
+
# Transformation to 3D space
|
|
185
|
+
x = (cos_Omega * cos_omega - sin_Omega * sin_omega * cos_i) * x_orb + (-cos_Omega * sin_omega - sin_Omega * cos_omega * cos_i) * y_orb
|
|
186
|
+
y = (sin_Omega * cos_omega + cos_Omega * sin_omega * cos_i) * x_orb + (-sin_Omega * sin_omega + cos_Omega * cos_omega * cos_i) * y_orb
|
|
187
|
+
z = (sin_i * sin_omega) * x_orb + (sin_i * cos_omega) * y_orb
|
|
188
|
+
|
|
189
|
+
dx = (cos_Omega * cos_omega - sin_Omega * sin_omega * cos_i) * vx_orb + (-cos_Omega * sin_omega - sin_Omega * cos_omega * cos_i) * vy_orb
|
|
190
|
+
dy = (sin_Omega * cos_omega + cos_Omega * sin_omega * cos_i) * vx_orb + (-sin_Omega * sin_omega + cos_Omega * cos_omega * cos_i) * vy_orb
|
|
191
|
+
dz = (sin_i * sin_omega) * vx_orb + (sin_i * cos_omega) * vy_orb
|
|
192
|
+
|
|
193
|
+
return np.array([x, y, z]), np.array([dx, dy, dz]), nu, ma, ea, T # state vectors
|
|
194
|
+
|
|
195
|
+
def keplerian_elements_to_state_vectors(self):
|
|
196
|
+
"""Calculates the state vectors (position and velocity) from Keplerian Orbit Elements.
|
|
197
|
+
Returns also true anomaly, eccentric anomaly, mean anomaly and the time of periapsis.
|
|
198
|
+
[a]: https://web.archive.org/web/20160418175843/https://ccar.colorado.edu/asen5070/handouts/cart2kep2002.pdf
|
|
199
|
+
[b]: https://web.archive.org/web/20170810015111/http://ccar.colorado.edu/asen5070/handouts/kep2cart_2002.doc
|
|
200
|
+
[c]: https://space.stackexchange.com/questions/19322/converting-orbital-elements-to-cartesian-state-vectors/19335#19335
|
|
201
|
+
[d]: https://space.stackexchange.com/questions/55356/how-to-find-eccentric-anomaly-by-mean-anomaly
|
|
202
|
+
[e]: https://github.com/alfonsogonzalez/AWP/blob/main/src/python_tools/numerical_tools.py
|
|
203
|
+
Numbers in comments refer to numbered formulas in [a] and [b].
|
|
204
|
+
Code based on [c]. Added calculation of eccentric anomaly based on the explanations
|
|
205
|
+
in [d] using a stripped down version of [e]."""
|
|
206
|
+
a, e, i, Ω, ω, ϖ, L = self.a, self.e, self.i, self.Ω, self.ω, self.ϖ, self.L # for readability of formulas
|
|
207
|
+
ma, ea, nu, T, t, mu = self.ma, self.ea, self.nu, self.T, self.t, self.mu # for readability of formulas
|
|
208
|
+
|
|
209
|
+
if ω is None and ϖ is not None and Ω is not None:
|
|
210
|
+
ω = ϖ - Ω
|
|
211
|
+
if ma is None and L is not None and ϖ is not None:
|
|
212
|
+
ma = L - ϖ
|
|
213
|
+
if ea is not None: # ea provided
|
|
214
|
+
nu = 2 * math.atan(math.sqrt((1 + e) / (1 - e)) * math.tan(ea / 2)) # 3b: true anomaly (from eccentric anomaly)
|
|
215
|
+
ma = ea - e * math.sin(ea) # 2b: Mean anomaly (from eccentric anomaly). Just for completeness.
|
|
216
|
+
else: # ea not provided
|
|
217
|
+
if nu is not None: # nu provided
|
|
218
|
+
ea = 2 * math.atan(math.sqrt((1 - e) / (1 + e)) * math.tan(nu / 2)) # 11a: eccentric anomaly (from true anomaly) [rad]
|
|
219
|
+
ma = ea - e * math.sin(ea) # 2b: Mean anomaly (from eccentric anomaly). Just for completeness.
|
|
220
|
+
else: # nu, ea not provided
|
|
221
|
+
if ma is not None: # ma provided
|
|
222
|
+
ea = CurveSimPhysics.kepler_equation_root(e, ma, ea_guess=ma) # A good guess is important. With guess=0 the root finder very often does not converge.
|
|
223
|
+
nu = 2 * math.atan(math.sqrt((1 + e) / (1 - e)) * math.tan(ea / 2)) # 3b: true anomaly (from eccentric anomaly)
|
|
224
|
+
else: # nu, ea, ma not provided
|
|
225
|
+
if T is not None: # T provided
|
|
226
|
+
n = math.sqrt(mu / a ** 3) # 1b: Mean angular motion. Not needed in this function. (Except for ma, which is not needed.)
|
|
227
|
+
ma = n * T # 1b: Mean anomaly at time of periapsis (from angular motion).
|
|
228
|
+
ea = CurveSimPhysics.kepler_equation_root(e, ma, ea_guess=ma) # A good guess is important. With guess=0 the root finder very often does not converge.
|
|
229
|
+
nu = 2 * math.atan(math.sqrt((1 + e) / (1 - e)) * math.tan(ea / 2)) # 3b: true anomaly (from eccentric anomaly)
|
|
230
|
+
else: # nu, ea, ma, T not provided
|
|
231
|
+
raise Exception("nu or ma or ea or T has to be provided to keplerian_elements_to_state_vectors()")
|
|
232
|
+
n = math.sqrt(mu / a ** 3) # 12a: mean angular motion
|
|
233
|
+
T = ma / n # Time of periapsis (from mean anomaly and angular motion). Just for completeness.
|
|
234
|
+
|
|
235
|
+
# Now update ma, ea and nu for a delay
|
|
236
|
+
# print(f'@Transit: {math.degrees(nu) = :4.0f} {math.degrees(ma) = :4.0f} {math.degrees(ea) = :4.0f}')
|
|
237
|
+
ma += t * n # 1b
|
|
238
|
+
ea = CurveSimPhysics.kepler_equation_root(e, ma, ea_guess=ma) # A good guess is important. With guess=0 the root finder very often does not converge.
|
|
239
|
+
nu = 2 * math.atan(math.sqrt((1 + e) / (1 - e)) * math.tan(ea / 2)) # 3b: true anomaly (from eccentric anomaly)
|
|
240
|
+
# print(f' delayed: {math.degrees(nu) = :4.0f} {math.degrees(ma) = :4.0f} {math.degrees(ea) = :4.0f}')
|
|
241
|
+
# nu = nu % (2*math.pi)
|
|
242
|
+
# print(f'@Transit: {math.degrees(nu) = :4.0f} {math.degrees(ma) = :4.0f} {math.degrees(ea) = :4.0f}')
|
|
243
|
+
r = a * (1 - e * math.cos(ea)) # 4b: radius r
|
|
244
|
+
h = math.sqrt(mu * a * (1 - e ** 2)) # 5b: specific angular momentum h
|
|
245
|
+
x = r * (math.cos(Ω) * math.cos(ω + nu) - math.sin(Ω) * math.sin(ω + nu) * math.cos(i)) # 6b: position component x
|
|
246
|
+
y = r * (math.sin(Ω) * math.cos(ω + nu) + math.cos(Ω) * math.sin(ω + nu) * math.cos(i)) # 6b: position component y
|
|
247
|
+
z = r * (math.sin(i) * math.sin(ω + nu)) # 6b: position component z
|
|
248
|
+
p = a * (1 - e ** 2) # 7b: Semi-latus rectum. Used in velocity calculation.
|
|
249
|
+
dx = (x * h * e / (r * p)) * math.sin(nu) - (h / r) * (math.cos(Ω) * math.sin(ω + nu) + math.sin(Ω) * math.cos(ω + nu) * math.cos(i)) # 7b: velocity component x
|
|
250
|
+
dy = (y * h * e / (r * p)) * math.sin(nu) - (h / r) * (math.sin(Ω) * math.sin(ω + nu) - math.cos(Ω) * math.cos(ω + nu) * math.cos(i)) # 7b: velocity component y
|
|
251
|
+
dz = (z * h * e / (r * p)) * math.sin(nu) + (h / r) * (math.cos(ω + nu) * math.sin(i)) # 7b: velocity component z
|
|
252
|
+
return np.array([x, y, z]), np.array([dx, dy, dz]), nu, ma, ea, T # state vectors
|
|
253
|
+
|
|
254
|
+
def calc_state_vectors(self, p, bodies):
|
|
255
|
+
"""Get initial position and velocity of the physical body self."""
|
|
256
|
+
self.mu = CurveSimPhysics.gravitational_parameter(bodies, p.g) # is the same for all bodies in the system, because they are orbiting a common barycenter
|
|
257
|
+
if self.velocity is None: # State vectors are not in config file. So they will be calculated from Kepler orbit parameters instead.
|
|
258
|
+
pos, vel, *_ = self.keplerian_elements_to_state_vectors_debug_new()
|
|
259
|
+
self.positions[0] = np.array(pos, dtype=float) # [m] initial position
|
|
260
|
+
self.velocity = np.array(vel, dtype=float) # [m/s] initial velocity
|
|
261
|
+
|
|
262
|
+
def eclipsed_by(self, body, iteration):
|
|
263
|
+
"""Returns area, relative_radius
|
|
264
|
+
area: Area of self which is eclipsed by body.
|
|
265
|
+
relative_radius: The distance of the approximated center of the eclipsed area from the center of self as a percentage of self.radius (used for limb darkening)."""
|
|
266
|
+
if body.positions[iteration][1] < self.positions[iteration][1]: # Is body nearer to viewpoint than self? (i.e. its position has a smaller y-coordinate)
|
|
267
|
+
d = CurveSimPhysics.distance_2d_ecl(body, self, iteration)
|
|
268
|
+
if d < self.radius + body.radius: # Does body eclipse self?
|
|
269
|
+
if d <= abs(self.radius - body.radius): # Annular (i.e. ring) eclipse or total eclipse
|
|
270
|
+
if self.radius < body.radius: # Total eclipse
|
|
271
|
+
area = self.area_2d
|
|
272
|
+
relative_radius = 0
|
|
273
|
+
# print(f' total: {iteration:7d} rel.area: {area/self.area_2d*100:6.0f}% rel.r: {relative_radius*100:6.0f}%')
|
|
274
|
+
return area, relative_radius
|
|
275
|
+
else: # Annular (i.e. ring) eclipse
|
|
276
|
+
area = body.area_2d
|
|
277
|
+
relative_radius = d / self.radius
|
|
278
|
+
# print(f' ring: {iteration:7d} rel.area: {area / self.area_2d * 100:6.0f}% rel.r: {relative_radius * 100:6.0f}%')
|
|
279
|
+
return area, relative_radius
|
|
280
|
+
else: # Partial eclipse
|
|
281
|
+
# Eclipsed area is the sum of a circle segment of self plus a circle segment of body
|
|
282
|
+
# https://de.wikipedia.org/wiki/Kreissegment https://de.wikipedia.org/wiki/Schnittpunkt#Schnittpunkte_zweier_Kreise
|
|
283
|
+
self.d = (self.radius ** 2 - body.radius ** 2 + d ** 2) / (2 * d) # Distance of center from self to radical axis
|
|
284
|
+
body.d = (body.radius ** 2 - self.radius ** 2 + d ** 2) / (2 * d) # Distance of center from body to radical axis
|
|
285
|
+
body.h = body.radius + self.d - d # Height of circle segment
|
|
286
|
+
self.h = self.radius + body.d - d # Height of circle segment
|
|
287
|
+
body.angle = 2 * math.acos(1 - body.h / body.radius) # Angle of circle segment
|
|
288
|
+
self.angle = 2 * math.acos(1 - self.h / self.radius) # Angle of circle segment
|
|
289
|
+
body.eclipsed_area = body.radius ** 2 * (body.angle - math.sin(body.angle)) / 2 # Area of circle segment
|
|
290
|
+
self.eclipsed_area = self.radius ** 2 * (self.angle - math.sin(self.angle)) / 2 # Area of circle segment
|
|
291
|
+
area = body.eclipsed_area + self.eclipsed_area # Eclipsed area is sum of two circle segments.
|
|
292
|
+
relative_radius = (self.radius + self.d - body.h) / (2 * self.radius) # Relative distance between approximated center C of eclipsed area and center of self
|
|
293
|
+
# print(f'partial: {iteration:7d} rel.area: {area/self.area_2d*100:6.0f}% rel.r: {relative_radius*100:6.0f}%')
|
|
294
|
+
return area, relative_radius
|
|
295
|
+
else: # No eclipse because, seen from viewer, the bodies are not close enough to each other
|
|
296
|
+
return 0.0, 0.0
|
|
297
|
+
else: # body cannot eclipse self, because self is nearer to viewer than body
|
|
298
|
+
return 0.0, 0.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CurveSimLightcurve(np.ndarray):
|
|
5
|
+
|
|
6
|
+
def __new__(cls, shape, dtype=float):
|
|
7
|
+
obj = np.zeros(shape, dtype=dtype).view(cls)
|
|
8
|
+
return obj
|
|
9
|
+
|
|
10
|
+
def __str__(self):
|
|
11
|
+
return f'CurveSimLightcurve: max={self.max(initial=99.0):.5f}, min={self.min(initial=-99.0):.5f}, len={len(self)}'
|
|
12
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class CurveSimParameters:
|
|
6
|
+
|
|
7
|
+
def __init__(self):
|
|
8
|
+
"""Read program parameters and properties of the physical bodies from config file."""
|
|
9
|
+
# Read config file
|
|
10
|
+
self.standard_sections = ["Astronomical Constants", "Video", "Plot", "Scale", "Debug"]
|
|
11
|
+
config = configparser.ConfigParser(inline_comment_prefixes='#')
|
|
12
|
+
self.configfilename = CurveSimParameters.find_and_check_config_file(default="curvesimulator.ini", standard_sections=self.standard_sections)
|
|
13
|
+
config.read(self.configfilename)
|
|
14
|
+
|
|
15
|
+
# [Astronomical Constants]
|
|
16
|
+
# For ease of use of these constants in the config file they are additionally defined here without the prefix "self.".
|
|
17
|
+
g = eval(config.get("Astronomical Constants", "g"))
|
|
18
|
+
au = eval(config.get("Astronomical Constants", "au"))
|
|
19
|
+
r_sun = eval(config.get("Astronomical Constants", "r_sun"))
|
|
20
|
+
m_sun = eval(config.get("Astronomical Constants", "m_sun"))
|
|
21
|
+
l_sun = eval(config.get("Astronomical Constants", "l_sun"))
|
|
22
|
+
r_jup = eval(config.get("Astronomical Constants", "r_jup"))
|
|
23
|
+
m_jup = eval(config.get("Astronomical Constants", "m_jup"))
|
|
24
|
+
r_earth = eval(config.get("Astronomical Constants", "r_earth"))
|
|
25
|
+
m_earth = eval(config.get("Astronomical Constants", "m_earth"))
|
|
26
|
+
v_earth = eval(config.get("Astronomical Constants", "v_earth"))
|
|
27
|
+
self.g, self.au, self.r_sun, self.m_sun, self.l_sun = g, au, r_sun, m_sun, l_sun,
|
|
28
|
+
self.r_jup, self.m_jup, self.r_earth, self.m_earth, self.v_earth = r_jup, m_jup, r_earth, m_earth, v_earth
|
|
29
|
+
|
|
30
|
+
# [Video]
|
|
31
|
+
self.video_file = config.get("Video", "video_file")
|
|
32
|
+
self.frames = eval(config.get("Video", "frames"))
|
|
33
|
+
self.fps = eval(config.get("Video", "fps"))
|
|
34
|
+
self.dt = eval(config.get("Video", "dt"))
|
|
35
|
+
self.sampling_rate = eval(config.get("Video", "sampling_rate"))
|
|
36
|
+
self.iterations = self.frames * self.sampling_rate
|
|
37
|
+
|
|
38
|
+
# [Scale]
|
|
39
|
+
self.scope_left = eval(config.get("Scale", "scope_left"))
|
|
40
|
+
self.star_scale_left = eval(config.get("Scale", "star_scale_left"))
|
|
41
|
+
self.planet_scale_left = eval(config.get("Scale", "planet_scale_left"))
|
|
42
|
+
self.scope_right = eval(config.get("Scale", "scope_right"))
|
|
43
|
+
self.star_scale_right = eval(config.get("Scale", "star_scale_right"))
|
|
44
|
+
self.planet_scale_right = eval(config.get("Scale", "planet_scale_right"))
|
|
45
|
+
self.autoscaling = config.get("Scale", "autoscaling") == "on"
|
|
46
|
+
self.min_radius = eval(config.get("Scale", "min_radius")) / 100.0
|
|
47
|
+
self.max_radius = eval(config.get("Scale", "max_radius")) / 100.0
|
|
48
|
+
|
|
49
|
+
# [Plot]
|
|
50
|
+
self.figure_width = eval(config.get("Plot", "figure_width"))
|
|
51
|
+
self.figure_height = eval(config.get("Plot", "figure_height"))
|
|
52
|
+
self.xlim = eval(config.get("Plot", "xlim"))
|
|
53
|
+
self.ylim = eval(config.get("Plot", "ylim"))
|
|
54
|
+
self.time_units = {"s": 1, "min": 60, "h": 3600, "d": 24 * 3600,
|
|
55
|
+
"mon": 365.25 * 24 * 3600 / 12, "y": 365.25 * 24 * 3600}
|
|
56
|
+
self.x_unit_name = config.get("Plot", "x_unit")
|
|
57
|
+
self.x_unit_value = self.time_units[self.x_unit_name]
|
|
58
|
+
self.red_dot_height = eval(config.get("Plot", "red_dot_height"))
|
|
59
|
+
self.red_dot_width = eval(config.get("Plot", "red_dot_width"))
|
|
60
|
+
|
|
61
|
+
# Checking all parameters defined so far
|
|
62
|
+
for key in vars(self):
|
|
63
|
+
if type(getattr(self, key)) not in [str, dict, bool, list]:
|
|
64
|
+
if getattr(self, key) <= 0:
|
|
65
|
+
print(f'{self=} {key=} {getattr(self, key)=} {type(getattr(self, key))=}')
|
|
66
|
+
raise Exception(f"No parameter in sections {self.standard_sections} may be zero or negative.")
|
|
67
|
+
|
|
68
|
+
# [Debug]
|
|
69
|
+
self.debug_L = list(eval(config.get("Debug", "debug_L")))
|
|
70
|
+
# print(f'{self.debug_L=}, {type(self.debug_L)=}')
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
return f'CurveSimParameters from {self.configfilename}'
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def find_and_check_config_file(default, standard_sections):
|
|
77
|
+
"""Check program parameters and extract config file name from them.
|
|
78
|
+
Check if config file can be opened and contains all standard sections."""
|
|
79
|
+
# Check program parameters and extract config file name from them.
|
|
80
|
+
if len(sys.argv) == 1:
|
|
81
|
+
configfilename = default
|
|
82
|
+
print(f'Using default config file {configfilename}. Specify config file name as program parameter if you '
|
|
83
|
+
f'want to use another config file.')
|
|
84
|
+
elif len(sys.argv) == 2:
|
|
85
|
+
configfilename = sys.argv[1]
|
|
86
|
+
print(f'Using {configfilename} as config file.')
|
|
87
|
+
else:
|
|
88
|
+
configfilename = sys.argv[1]
|
|
89
|
+
print(f'Using {configfilename} as config file. Further program parameters are ignored.')
|
|
90
|
+
config = configparser.ConfigParser(inline_comment_prefixes='#') # Read config file.
|
|
91
|
+
red = "\u001b[31m"
|
|
92
|
+
reset = "\u001b[0m"
|
|
93
|
+
if len(config.read(configfilename)) < 1: # does opening the config file fail?
|
|
94
|
+
print(red + f'Config file {configfilename} not found. ' + reset)
|
|
95
|
+
print(red + f'Provide the config file name as the program parameter if you do not want to use the config file {configfilename}. ' + reset)
|
|
96
|
+
print(red + f'If you run this script in a jupyter notebook, add this line to the script: sys.argv[1]="yourconfigfilename.ini"' + reset)
|
|
97
|
+
exit(1)
|
|
98
|
+
for section in standard_sections: # Does the config file contain all standard sections?
|
|
99
|
+
if section not in config.sections():
|
|
100
|
+
print(red + f'Section {section} missing in config file.' + reset)
|
|
101
|
+
exit(2)
|
|
102
|
+
return configfilename
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CurveSimPhysics:
|
|
5
|
+
|
|
6
|
+
@staticmethod
|
|
7
|
+
def kepler_equation(ea, e, ma):
|
|
8
|
+
"""ea: eccentric anomaly [rad], e: eccentricity, ma: mean anomaly [rad]"""
|
|
9
|
+
if not -2 * math.pi < ea < 2 * math.pi:
|
|
10
|
+
raise ValueError("eccentric anomaly ea must be in radians but is outside of the range ]-2π;2π[")
|
|
11
|
+
if not -2 * math.pi < ma < 2 * math.pi:
|
|
12
|
+
raise ValueError("mean anomaly ma must be in radians but is outside of the range ]-2π;2π[")
|
|
13
|
+
if not 0 <= e < 1:
|
|
14
|
+
raise ValueError("eccentricity e is outside of the range [0;1[")
|
|
15
|
+
return ea - e * math.sin(ea) - ma
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def kepler_equation_derivative(ea, e):
|
|
19
|
+
"""ea: eccentric anomaly [rad], e: eccentricity"""
|
|
20
|
+
return 1.0 - e * math.cos(ea)
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def kepler_equation_root(e, ma, ea_guess=0.0, tolerance=1e-10, max_steps=50):
|
|
24
|
+
"""Calculate the root of the Kepler Equation with the Newton–Raphson method.
|
|
25
|
+
e: eccentricity, ma: mean anomaly [rad], ea_guess: eccentric anomaly [rad]. ea_guess=ma is a good start."""
|
|
26
|
+
for n in range(max_steps):
|
|
27
|
+
delta = CurveSimPhysics.kepler_equation(ea_guess, e, ma) / CurveSimPhysics.kepler_equation_derivative(ea_guess, e)
|
|
28
|
+
if abs(delta) < tolerance:
|
|
29
|
+
return ea_guess - delta
|
|
30
|
+
ea_guess -= delta
|
|
31
|
+
raise RuntimeError('Newton\'s root solver did not converge.')
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def kepler_equation_root_chatgpt(e, ma, ea_guess=0.0, tolerance=1e-10, max_steps=50):
|
|
35
|
+
"""Returns the root of the Kepler Equation with the Newton–Raphson method.
|
|
36
|
+
e: eccentricity, ma: mean anomaly [rad], ea_guess: eccentric anomaly [rad]. ea_guess=ma is a good start."""
|
|
37
|
+
ea = ea_guess
|
|
38
|
+
for _ in range(max_steps):
|
|
39
|
+
f_ea = ea - e * math.sin(ea) - ma
|
|
40
|
+
f_prime_ea = 1 - e * math.cos(ea)
|
|
41
|
+
delta = -f_ea / f_prime_ea
|
|
42
|
+
ea += delta
|
|
43
|
+
if abs(delta) < tolerance:
|
|
44
|
+
return ea
|
|
45
|
+
raise RuntimeError("Kepler's equation solver did not converge.")
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def kepler_equation_root_debug(e, ma, ea_guess=0.0, tolerance=1e-10, max_steps=50):
|
|
49
|
+
"""
|
|
50
|
+
Alternative Method for calculating the root of the Kepler Equation from source
|
|
51
|
+
[f]: https://www.researchgate.net/publication/232203657_Orbital_Ephemerides_of_the_Sun_Moon_and_Planets, Section 8.10
|
|
52
|
+
e: eccentricity, ma: mean anomaly [rad], ea_guess: eccentric anomaly [rad]. ea_guess=ma is a good start.
|
|
53
|
+
"""
|
|
54
|
+
e_deg = math.degrees(e)
|
|
55
|
+
ma_deg = math.degrees(ma)
|
|
56
|
+
ea_deg = ma_deg + e_deg * math.sin(ma)
|
|
57
|
+
|
|
58
|
+
for n in range(max_steps):
|
|
59
|
+
delta_ma = ma_deg - (ea_deg - e * math.sin(ea_deg))
|
|
60
|
+
delta_ea = delta_ma / (1 - e * math.cos(ea_deg))
|
|
61
|
+
ea_deg += delta_ea
|
|
62
|
+
if abs(delta_ea) < tolerance:
|
|
63
|
+
return math.radians(ea_deg)
|
|
64
|
+
raise RuntimeError('Solution for Kepler\'s Equation did not converge.')
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def gravitational_parameter(bodies, g):
|
|
69
|
+
"""Calculate the gravitational parameter of masses orbiting a common barycenter
|
|
70
|
+
https://en.wikipedia.org/wiki/Standard_gravitational_parameter"""
|
|
71
|
+
mass = 0.0
|
|
72
|
+
for body in bodies:
|
|
73
|
+
mass += body.mass
|
|
74
|
+
return g * mass
|
|
75
|
+
|
|
76
|
+
@staticmethod
|
|
77
|
+
def distance_2d_ecl(body1, body2, i):
|
|
78
|
+
"""Return distance of the centers of 2 physical bodies as seen by a viewer (projection y->0)."""
|
|
79
|
+
dx = body1.positions[i][0] - body2.positions[i][0]
|
|
80
|
+
dz = body1.positions[i][2] - body2.positions[i][2]
|
|
81
|
+
return math.sqrt((dx ** 2 + dz ** 2))
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def limbdarkening(relative_radius, beta):
|
|
85
|
+
"""https://en.wikipedia.org/wiki/Limb_darkening
|
|
86
|
+
https://de.wikipedia.org/wiki/Photosph%C3%A4re#Mitte-Rand-Verdunkelung
|
|
87
|
+
Approximates the flux of a star at a point on the star seen from a very large distance.
|
|
88
|
+
The point's apparent distance from the star's center is relative_radius * radius.
|
|
89
|
+
Beta depends on the wavelength. Beta=2.3 is a good compromise for the spectrum of visible light."""
|
|
90
|
+
if relative_radius >= 1:
|
|
91
|
+
return 1 / (1 + beta)
|
|
92
|
+
return (1 + beta * math.sqrt(1 - relative_radius ** 2)) / (1 + beta)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# Curvesim - A Star System Lightcurve Simulator
|
|
3
|
+
# Curvesim calculates the movements and eclipses of celestial bodies and produces a video of this.
|
|
4
|
+
# Specify mass, radius and other properties of some stars and planets in a configuration file.
|
|
5
|
+
# Then run "curvesimulator.py <configfilename>" to produce the video.
|
|
6
|
+
# The video shows simultanously a view of the star system from the top and from the side and
|
|
7
|
+
# the lightcurve of the system's total luminosity over time.
|
|
8
|
+
# Usually you do not need to look at or even modify the python code. Instead control the program's
|
|
9
|
+
# outcome with the config file. The meaning of all program parameters is documented in the config file.
|
|
10
|
+
# Curvesim uses ffmpeg to convert the data into a video. Download ffmpeg from https://www.ffmpeg.org/download.html.
|
|
11
|
+
# Extract the zip file and add "<yourdriveandpath>\FFmpeg\bin" to Environment Variable PATH.
|
|
12
|
+
#
|
|
13
|
+
# Your questions and comments are welcome.
|
|
14
|
+
# Just open an issue on https://github.com/lichtgestalter/curvesim/issues to get my attention :)
|
|
15
|
+
import math
|
|
16
|
+
|
|
17
|
+
from cs_animation import CurveSimAnimation
|
|
18
|
+
from cs_bodies import CurveSimBodies
|
|
19
|
+
from cs_parameters import CurveSimParameters
|
|
20
|
+
|
|
21
|
+
# If you run this script in a jupyter notebook, uncomment the next 2 lines in order to provide the name of your config file.
|
|
22
|
+
# import sys
|
|
23
|
+
# sys.argv[1] = "provide_your_config_file_name_here.ini"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def curvesim():
|
|
27
|
+
parameters = CurveSimParameters() # Read program parameters from config file.
|
|
28
|
+
bodies = CurveSimBodies(parameters) # Read physical bodies from config file and initialize them, calculate their state vectors and generate their patches for the animation
|
|
29
|
+
lightcurve = bodies.calc_physics(parameters) # Calculate body positions and the resulting lightcurve.
|
|
30
|
+
CurveSimAnimation(parameters, bodies, lightcurve) # Create the video
|
|
31
|
+
return parameters, bodies, lightcurve
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def debug_print_points():
|
|
35
|
+
# Just for debugging purposes, because something in the initial state vector is wrong.
|
|
36
|
+
parameters = CurveSimParameters() # Read program parameters from config file.
|
|
37
|
+
for _L in parameters.debug_L:
|
|
38
|
+
bodies = CurveSimBodies(parameters, debug_L=_L) # Initialize the physical bodies, calculate their state vectors and generate their patches for the animation
|
|
39
|
+
bodies[1].positions[0] /= 2273900000.0 # bodies[0] is the sun, bodies[1] is the test planet
|
|
40
|
+
bodies[1].a /= 2273900000.0 # normalize to a=100
|
|
41
|
+
myfile = "debug_file.txt"
|
|
42
|
+
with open(myfile, "a", encoding='utf-8') as file: # encoding is important, otherwise symbols like Ω get destroyed
|
|
43
|
+
if bodies[1].L == 0: # write headline with parameters, e.g. "a=100 e=0.90 i=90 Ω=0 ϖ=0"
|
|
44
|
+
file.write(f'a={bodies[1].a:.0f} e={bodies[1].e:.2f} i={bodies[1].i / math.pi * 180:.0f} Ω={bodies[1].Ω / math.pi * 180:.0f} ϖ={bodies[1].ϖ / math.pi * 180:.0f}\n')
|
|
45
|
+
file.write(f'L{bodies[1].L / math.pi * 180:.0f},{bodies[1].positions[0][0]:.2f},{bodies[1].positions[0][1]:.2f},{bodies[1].positions[0][2]:.2f}\n') # write coordinates of starting position for this value of L
|
|
46
|
+
return parameters, bodies
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
parameters, bodies, lightcurve = curvesim()
|
|
51
|
+
# parameters, bodies = debug_print_points()
|
|
52
|
+
print(parameters)
|
|
53
|
+
print(bodies)
|
|
54
|
+
# print(lightcurve)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
if __name__ == '__main__':
|
|
58
|
+
main()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: curvesimulator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Curvesimulator calculates the movements and eclipses of celestial bodies and produces a video of this and of the resulting lightcurve.
|
|
5
|
+
Home-page: https://github.com/lichtgestalter/curvesimulator
|
|
6
|
+
Author: Uli Scheuss
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: matplotlib
|
|
13
|
+
Requires-Dist: configparser
|
|
14
|
+
|
|
15
|
+
# curvesim
|
|
16
|
+
A Star System and Lightcurve Simulator<br>
|
|
17
|
+
<br>
|
|
18
|
+
Curvesim produces a video of the movements and eclipses of celestial bodies and of the resulting lightcurve.<br>
|
|
19
|
+
<br>
|
|
20
|
+
Curvesim is fast and the videos use little disk space. A video takes about the same time to produce as its playing time and uses less than 0.5 MB disc space per minute.<br>
|
|
21
|
+
<br>
|
|
22
|
+
Specify mass, radius, orbital elements and other properties of some stars and planets in a configuration file.<br>
|
|
23
|
+
Then run "curvesim.py <configfilename>" to produce the video.
|
|
24
|
+
The video shows simultanously a view of the star system from the top and from the side and
|
|
25
|
+
the lightcurve of the system's total luminosity over time.<br>
|
|
26
|
+
<br>
|
|
27
|
+
Usually you do not need to look at or even modify the python code. Instead control the program's
|
|
28
|
+
outcome with the config file. The meaning of all program parameters is documented in the config file.<br>
|
|
29
|
+
<br>
|
|
30
|
+
Curvesim uses ffmpeg to convert the data into a video. <br>
|
|
31
|
+
Download ffmpeg from https://www.ffmpeg.org/download.html. <br>
|
|
32
|
+
Extract the zip file and (on Windows) add "<yourdriveandpath>\FFmpeg\bin" to the environment variable PATH.<br>
|
|
33
|
+
<br>
|
|
34
|
+
For questions and comments just open an issue on https://github.com/lichtgestalter/curvesim/issues to get my attention :)<br>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.py
|
|
3
|
+
curvesimulator/__init__.py
|
|
4
|
+
curvesimulator/cs_animation.py
|
|
5
|
+
curvesimulator/cs_bodies.py
|
|
6
|
+
curvesimulator/cs_body.py
|
|
7
|
+
curvesimulator/cs_lightcurve.py
|
|
8
|
+
curvesimulator/cs_parameters.py
|
|
9
|
+
curvesimulator/cs_physics.py
|
|
10
|
+
curvesimulator/curvesim.py
|
|
11
|
+
curvesimulator.egg-info/PKG-INFO
|
|
12
|
+
curvesimulator.egg-info/SOURCES.txt
|
|
13
|
+
curvesimulator.egg-info/dependency_links.txt
|
|
14
|
+
curvesimulator.egg-info/requires.txt
|
|
15
|
+
curvesimulator.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
curvesimulator
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="curvesimulator",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
install_requires=[
|
|
8
|
+
# List your dependencies here
|
|
9
|
+
"numpy",
|
|
10
|
+
"matplotlib",
|
|
11
|
+
"configparser",
|
|
12
|
+
# "math",
|
|
13
|
+
# "time",
|
|
14
|
+
# "sys",
|
|
15
|
+
],
|
|
16
|
+
author="Uli Scheuss",
|
|
17
|
+
description="Curvesimulator calculates the movements and eclipses of celestial bodies and produces a video of this and of the resulting lightcurve.",
|
|
18
|
+
long_description=open("README.md").read(),
|
|
19
|
+
long_description_content_type="text/markdown",
|
|
20
|
+
url="https://github.com/lichtgestalter/curvesimulator",
|
|
21
|
+
classifiers=[
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: OS Independent",
|
|
25
|
+
],
|
|
26
|
+
)
|