Skip to content

Commit 8914789

Browse files
authored
Merge branch 'develop' into feat/flight-comparison
2 parents 1b80c34 + 793e5f6 commit 8914789

10 files changed

Lines changed: 112 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Attention: The newest changes should be on top -->
4444

4545
### Fixed
4646

47+
- BUG: Fix parallel Monte Carlo simulation showing incorrect iteration count [#806](https://github.com/RocketPy-Team/RocketPy/pull/806)
4748
- BUG: Fix CSV column header spacing in FlightDataExporter [#864](https://github.com/RocketPy-Team/RocketPy/issues/864)
4849

4950

docs/user/environment/1-atm-models/standard_atmosphere.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ Other profiles can be derived from it, however, winds are automatically set to
2222

2323
env.plots.atmospheric_model()
2424

25-
.. skip one line with |
26-
|
25+
|
2726
2827
The International Standard Atmosphere can also be reset at any time by using the
2928
:meth:`rocketpy.Environment.set_atmospheric_model` method. For example:

docs/user/first_simulation.rst

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ regarding the launch site:
9090

9191
| This roughly corresponds to the location of Spaceport America, New Mexico.
9292
93+
9394
Next, we need to specify the atmospheric model to be used. In this example,
9495
we will get GFS forecasts data from tomorrow.
9596

@@ -110,6 +111,7 @@ of tomorrow:
110111

111112
| Now we set the atmospheric model to be used:
112113
114+
113115
.. jupyter-execute::
114116

115117
env.set_atmospheric_model(type="Forecast", file="GFS")
@@ -189,6 +191,7 @@ to specify several parameters:
189191

190192
| We can see its characteristics by calling the info method:
191193
194+
192195
.. jupyter-execute::
193196

194197
Pro75M1670.info()
@@ -637,8 +640,9 @@ and the rocket Mach number (see :meth:`rocketpy.Flight.mach_number`) to the file
637640
)
638641

639642
| As you can see, the first argument is the file name to be created. The following
640-
arguments are the attributes to be exported. We can check the file by reading it
641-
with :func:`pandas.read_csv`:
643+
arguments are the attributes to be exported. We can check the file by reading it
644+
with :func:`pandas.read_csv`:
645+
642646

643647
.. jupyter-execute::
644648

@@ -647,8 +651,9 @@ with :func:`pandas.read_csv`:
647651
pd.read_csv("calisto_flight_data.csv")
648652

649653
| The file header specifies the meaning of each column. The time samples are
650-
obtained from the simulation solver steps. To export the data at a different
651-
sampling rate, use the ``time_step`` argument:
654+
obtained from the simulation solver steps. To export the data at a different
655+
sampling rate, use the ``time_step`` argument:
656+
652657

653658
.. jupyter-execute::
654659

docs/user/three_dof_simulation.rst

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ The environment setup is identical to standard simulations:
6767
elevation=1400
6868
)
6969

70-
env.set_atmospheric_model(type="StandardAtmosphere")
70+
env.set_atmospheric_model(type="standard_atmosphere")
7171

7272
Step 2: Create a PointMassMotor
7373
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -123,7 +123,7 @@ Or use a custom thrust function:
123123
.. seealso::
124124

125125
For detailed information about :class:`rocketpy.PointMassMotor` parameters,
126-
see the :ref:`API reference <api_reference>`.
126+
see the :class:`rocketpy.PointMassMotor` class documentation.
127127

128128
Step 3: Create a PointMassRocket
129129
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -279,6 +279,14 @@ Export trajectory data to CSV:
279279
"vz",
280280
)
281281

282+
283+
.. jupyter-execute::
284+
:hide-code:
285+
286+
import os
287+
os.remove("trajectory_3dof.csv")
288+
289+
282290
Complete Example
283291
----------------
284292

@@ -294,7 +302,7 @@ Here's a complete 3-DOF simulation from start to finish:
294302
longitude=-8.2889,
295303
elevation=100
296304
)
297-
env.set_atmospheric_model(type="StandardAtmosphere")
305+
env.set_atmospheric_model(type="standard_atmosphere")
298306

299307
# 2. Motor
300308
motor = PointMassMotor(
@@ -412,7 +420,6 @@ See Also
412420
- :ref:`First Simulation <firstsimulation>` - Standard 6-DOF simulation tutorial
413421
- :ref:`Rocket Class Usage <rocketusage>` - Full rocket modeling capabilities
414422
- :ref:`Flight Class Usage <flightusage>` - Complete flight simulation options
415-
- :doc:`../examples/3_dof_trial_sim` - Jupyter notebook example
416423

417424
Further Reading
418425
---------------

rocketpy/plots/monte_carlo_plots.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from pathlib import Path
2+
13
import matplotlib.pyplot as plt
24
import numpy as np
35
from matplotlib.transforms import offset_copy
46

57
from ..tools import generate_monte_carlo_ellipses, import_optional_dependency
8+
from .plot_helpers import show_or_save_plot
69

710

811
class _MonteCarloPlots:
@@ -159,7 +162,7 @@ def ellipses(
159162
else:
160163
plt.show()
161164

162-
def all(self, keys=None):
165+
def all(self, keys=None, *, filename=None):
163166
"""
164167
Plot the histograms of the Monte Carlo simulation results.
165168
@@ -168,6 +171,14 @@ def all(self, keys=None):
168171
keys : str, list or tuple, optional
169172
The keys of the results to be plotted. If None, all results will be
170173
plotted. Default is None.
174+
filename : str | None, optional
175+
The path the plot should be saved to. By default None, in which case
176+
the plot will be shown instead of saved. If a filename is provided,
177+
each histogram will be saved with the key name appended to the
178+
filename (e.g., "plots/histogram_apogee.png" for key "apogee" with
179+
filename "plots/histogram.png"). Supported file endings are: eps,
180+
jpg, jpeg, pdf, pgf, png, ps, raw, rgba, svg, svgz, tif, tiff and
181+
webp (these are the formats supported by matplotlib).
171182
172183
Returns
173184
-------
@@ -207,7 +218,16 @@ def all(self, keys=None):
207218
ax1.set_xticks([])
208219

209220
plt.tight_layout()
210-
plt.show()
221+
222+
# Generate the filename for this specific key if saving
223+
if filename is not None:
224+
file_path = Path(filename)
225+
key_filename = str(
226+
file_path.parent / f"{file_path.stem}_{key}{file_path.suffix}"
227+
)
228+
show_or_save_plot(key_filename)
229+
else:
230+
show_or_save_plot(None)
211231

212232
def plot_comparison(self, other_monte_carlo):
213233
"""

rocketpy/rocket/parachute.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -25,27 +25,27 @@ class Parachute:
2525
system. It can be one of the following:
2626
2727
- A callable function that takes three arguments:
28-
1. Freestream pressure in pascals.
29-
2. Height in meters above ground level.
30-
3. The state vector of the simulation, which is defined as:
28+
1. Freestream pressure in pascals.
29+
2. Height in meters above ground level.
30+
3. The state vector of the simulation, which is defined as:
3131
32-
`[x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]`.
32+
`[x, y, z, vx, vy, vz, e0, e1, e2, e3, wx, wy, wz]`.
3333
34-
4. A list of sensors that are attached to the rocket. The most recent
35-
measurements of the sensors are provided with the
36-
``sensor.measurement`` attribute. The sensors are listed in the same
37-
order as they are added to the rocket.
34+
4. A list of sensors that are attached to the rocket. The most recent
35+
measurements of the sensors are provided with the
36+
``sensor.measurement`` attribute. The sensors are listed in the same
37+
order as they are added to the rocket.
3838
39-
The function should return ``True`` if the parachute ejection system
40-
should be triggered and False otherwise. The function will be called
41-
according to the specified sampling rate.
39+
The function should return ``True`` if the parachute ejection system
40+
should be triggered and False otherwise. The function will be called
41+
according to the specified sampling rate.
4242
4343
- A float value, representing an absolute height in meters. In this
44-
case, the parachute will be ejected when the rocket reaches this height
45-
above ground level.
44+
case, the parachute will be ejected when the rocket reaches this height
45+
above ground level.
4646
4747
- The string "apogee" which triggers the parachute at apogee, i.e.,
48-
when the rocket reaches its highest point and starts descending.
48+
when the rocket reaches its highest point and starts descending.
4949
5050
5151
Parachute.triggerfunc : function

rocketpy/rocket/rocket.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,12 +1667,11 @@ def add_air_brakes(
16671667
6. `interactive_objects` (list): A list containing the objects that
16681668
the controller function can interact with. The objects are
16691669
listed in the same order as they are provided in the
1670-
`interactive_objects`
1670+
`interactive_objects` argument.
16711671
7. `sensors` (list): A list of sensors that are attached to the
1672-
rocket. The most recent measurements of the sensors are provided
1673-
with the ``sensor.measurement`` attribute. The sensors are
1674-
listed in the same order as they are added to the rocket
1675-
``interactive_objects``
1672+
rocket. The most recent measurements of the sensors are provided
1673+
with the ``sensor.measurement`` attribute. The sensors are
1674+
listed in the same order as they are added to the rocket.
16761675
16771676
This function will be called during the simulation at the specified
16781677
sampling rate. The function should evaluate and change the observed

rocketpy/simulation/monte_carlo.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,7 @@ def __run_in_serial(self):
292292
with open(self.output_file, "a", encoding="utf-8") as f:
293293
f.write(outputs_json)
294294

295-
sim_monitor.print_update_status(sim_monitor.count)
295+
sim_monitor.print_update_status()
296296

297297
sim_monitor.print_final_status()
298298

@@ -430,7 +430,7 @@ def __sim_producer(self, seed, sim_monitor, mutex, error_event): # pylint: disa
430430
with open(self.output_file, "a", encoding="utf-8") as f:
431431
f.write(outputs_json)
432432

433-
sim_monitor.print_update_status(sim_idx)
433+
sim_monitor.print_update_status()
434434
finally:
435435
mutex.release()
436436

@@ -1208,6 +1208,7 @@ def __init__(self, initial_count, n_simulations, start_time):
12081208
self.count = initial_count
12091209
self.n_simulations = n_simulations
12101210
self.start_time = start_time
1211+
self.completed_count = 0
12111212

12121213
def keep_simulating(self):
12131214
return self.count < self.n_simulations
@@ -1216,25 +1217,24 @@ def increment(self):
12161217
self.count += 1
12171218
return self.count
12181219

1219-
def print_update_status(self, sim_idx):
1220+
def print_update_status(self):
12201221
"""Prints a message on the same line as the previous one and replaces
12211222
the previous message with the new one, deleting the extra characters
1222-
from the previous message.
1223-
1224-
Parameters
1225-
----------
1226-
sim_idx : int
1227-
Index of the current simulation.
1223+
from the previous message. This method increments the completed_count
1224+
to track how many simulations have finished (thread-safe when called
1225+
within a mutex-protected section).
12281226
12291227
Returns
12301228
-------
12311229
None
12321230
"""
1231+
self.completed_count += 1
12331232

1234-
average_time = (time() - self.start_time) / (self.count - self.initial_count)
1235-
estimated_time = int((self.n_simulations - self.count) * average_time)
1233+
average_time = (time() - self.start_time) / self.completed_count
1234+
remaining = self.n_simulations - self.initial_count - self.completed_count
1235+
estimated_time = int(remaining * average_time)
12361236

1237-
msg = f"Current iteration: {sim_idx:06d}"
1237+
msg = f"Iterations completed: {self.completed_count:06d}"
12381238
msg += f" | Average Time per Iteration: {average_time:.3f} s"
12391239
msg += f" | Estimated time left: {estimated_time} s"
12401240

rocketpy/tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1312,7 +1312,7 @@ def find_obj_from_hash(obj, hash_, depth_limit=None):
13121312
Returns
13131313
-------
13141314
object
1315-
The object whose '__rpy_hash' matches hash_, or None if not found.
1315+
The object whose '__rpy_hash' matches ``hash_``, or None if not found.
13161316
"""
13171317

13181318
stack = [(obj, 0)]

tests/integration/simulation/test_monte_carlo.py

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,18 @@
1111

1212
def _post_test_file_cleanup():
1313
"""Clean monte carlo files after test session if they exist."""
14-
if os.path.exists("monte_carlo_class_example.kml"):
15-
os.remove("monte_carlo_class_example.kml")
16-
if os.path.exists("monte_carlo_test.errors.txt"):
17-
os.remove("monte_carlo_test.errors.txt")
18-
if os.path.exists("monte_carlo_test.inputs.txt"):
19-
os.remove("monte_carlo_test.inputs.txt")
20-
if os.path.exists("monte_carlo_test.outputs.txt"):
21-
os.remove("monte_carlo_test.outputs.txt")
14+
files_to_cleanup = [
15+
"monte_carlo_class_example.kml",
16+
"monte_carlo_test.errors.txt",
17+
"monte_carlo_test.inputs.txt",
18+
"monte_carlo_test.outputs.txt",
19+
"test_histogram_apogee.png",
20+
"test_multi_apogee.png",
21+
"test_multi_x_impact.png",
22+
]
23+
for filepath in files_to_cleanup:
24+
if os.path.exists(filepath):
25+
os.remove(filepath)
2226

2327

2428
@pytest.mark.slow
@@ -134,6 +138,31 @@ def test_monte_carlo_plots(mock_show, monte_carlo_calisto_pre_loaded):
134138
_post_test_file_cleanup()
135139

136140

141+
def test_monte_carlo_plots_all_save(monte_carlo_calisto_pre_loaded):
142+
"""Tests the plots.all method with save functionality.
143+
144+
Parameters
145+
----------
146+
monte_carlo_calisto_pre_loaded : MonteCarlo
147+
The MonteCarlo object, this is a pytest fixture.
148+
"""
149+
try:
150+
# Test saving with a single key
151+
monte_carlo_calisto_pre_loaded.plots.all(
152+
keys="apogee", filename="test_histogram.png"
153+
)
154+
assert os.path.exists("test_histogram_apogee.png")
155+
156+
# Test saving with multiple keys
157+
monte_carlo_calisto_pre_loaded.plots.all(
158+
keys=["apogee", "x_impact"], filename="test_multi.png"
159+
)
160+
assert os.path.exists("test_multi_apogee.png")
161+
assert os.path.exists("test_multi_x_impact.png")
162+
finally:
163+
_post_test_file_cleanup()
164+
165+
137166
def test_monte_carlo_export_ellipses_to_kml(monte_carlo_calisto_pre_loaded):
138167
"""Tests the export_ellipses_to_kml method of the MonteCarlo class.
139168

0 commit comments

Comments
 (0)