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.
@@ -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,2 @@
1
+ # __init__.py
2
+ from .curvesim import curvesim
@@ -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,3 @@
1
+ numpy
2
+ matplotlib
3
+ configparser
@@ -0,0 +1 @@
1
+ curvesimulator
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ )