Skip to content
Open
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ coverage.xml
cover/
.pylint-report.txt
.ruff-report.txt
# Ignore temporary MonteCarlo export test artifacts
temp_test_output.json
monte_carlo_test.inputs.txt
monte_carlo_test.outputs.txt
monte_carlo_test.errors.txt


# Translations
*.mo
Expand Down
124 changes: 124 additions & 0 deletions rocketpy/simulation/monte_carlo.py
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether we should create a "MonteCarloDataExporter" similarly to what we have recelty done with the Flight class.

Needs to think more about it.

Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""

import json
import csv
import os
import traceback
import warnings
Expand Down Expand Up @@ -1150,6 +1151,129 @@ def export_ellipses_to_kml( # pylint: disable=too-many-statements

kml.save(filename)

def export_json(self, filename, indent_size=4):
"""
Export Monte Carlo results into a JSON file.

The exported data reflects exactly what is stored inside ``self.results``.
Depending on how the MonteCarlo object was populated, values may be either
lists (when results come from running Monte Carlo simulations) or scalars
(when results are imported from previously saved output summaries).

Parameters
----------
filename : str
Path to the JSON file that will be created. If the file already exists,
it will be overwritten.
indent_size : int, optional
Number of spaces used for indentation in the generated JSON file.
Defaults to 4.

Notes
-----
This function only exports the data already contained inside
``self.results``. No computations are performed during export. Users
should call ``simulate()`` or ``import_results()`` before exporting.

Examples
--------
Run new Monte Carlo simulations and export results::

mc = MonteCarlo(environment=env, rocket=rocket, flight=flight)
mc.simulate(20)
mc.export_json("results.json")

Export results previously loaded from file::

mc = MonteCarlo(environment=env, rocket=rocket, flight=flight)
mc.import_results("sample.outputs.txt")
mc.export_json("summary.json")
"""
if filename is None:
raise ValueError("A valid filename must be provided")

if not filename.lower().endswith(".json"):
raise ValueError("filename must end with .json")

if not self.results or len(self.results) == 0:
raise RuntimeError(
"No reesults found run simulation() or import results first"
)

export_dictionary = {}

for key, value_list in self.results.items():
converted_values = []

for value in value_list:
if isinstance(value, np.generic):
converted_values.append(float(value))

else:
converted_values.append(value)

export_dictionary[key] = converted_values

with open(filename, "w", encoding="utf-8") as file:
json.dump(export_dictionary, file, indent=indent_size, cls=RocketPyEncoder)

def export_csv(self, filename: str):
"""
Export Monte Carlo results into a CSV file.

The CSV rows correspond to individual simulation iterations whenever
the stored values are lists. Scalar values (from imported summaries)
are repeated across all rows.

Parameters
----------
filename : str
Output CSV file path. Must end in ".csv".
"""
if filename is None:
raise ValueError("A Filename must be provided")

if not filename.lower().endswith(".csv"):
raise ValueError("The filename must end with .csv")

if not self.results or len(self.results) == 0:
raise RuntimeError(
"No results found run simulations() or import results first"
)

# collection of keys and length
headers = list(self.results.keys())

# Determine number of rows
# If lists exist → number of simulations
# If all scalars → only 1 row
max_len = 1

for _ in self.results.values():
if isinstance(_, (list, np.ndarray)):
max_len = len(_)
break

with open(filename, "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)

# write headers
writer.writerow(headers)

# write rows
for i in range(max_len):
row = []
for key in headers:
value = self.results[key]

if isinstance(value, np.ndarray):
row.append(value[i])

else:
row.append(value)

writer.writerow(row)

def info(self):
"""
Print information about the Monte Carlo simulation.
Expand Down
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you dont need to create the ‎tests/integration/simulation/test_monte_carlo_export_csv_create_valid_file.py‎ and ‎tests/integration/simulation/test_monte_carlo_export_json_creates_valid_file.py‎ files, there are only 2 est functions, which could be easily placed at the tests/unit/simulation/flight.py file (or similar)

Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import os
import csv


def test_csv_export(monte_carlo_calisto):
mc = monte_carlo_calisto
mc.simulate(3)

filename = "temp_mc_export.csv"
"""
tests that results of monte carlo are exported to a CSV file
"""
mc.export_csv(filename)

assert os.path.exists(filename)

with open(filename, newline="") as f:
reader = list(csv.reader(f))

# Header exists
assert len(reader[0]) > 0

# Should have 3 rows of data after header
assert len(reader) == 4

os.remove(filename)
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import os
import json


def test_json_export(monte_carlo_calisto):
mc = monte_carlo_calisto
mc.simulate(3)

filename = "temp_test_output.json"
"""
tests weather the results of monte carlo are exported to a JSON file.
"""
mc.export_json(filename)
Comment on lines +5 to +13
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def test_json_export(monte_carlo_calisto):
mc = monte_carlo_calisto
mc.simulate(3)
filename = "temp_test_output.json"
"""
tests weather the results of monte carlo are exported to a JSON file.
"""
mc.export_json(filename)
def test_json_export(monte_carlo_calisto):
"""
tests weather the results of monte carlo are exported to a JSON file.
"""
mc = monte_carlo_calisto
mc.simulate(3)
filename = "temp_test_output.json"
mc.export_json(filename)


assert os.path.exists(filename)

try:
mc.export_json(filename)
assert os.path.exists(filename)
with open(filename, "r") as f:
data = json.load(f)
# Assert dictionary keys exist
assert len(data.keys()) > 0
# Check that at least one key corresponds to a list of simulation results
list_keys = [k for k, v in data.items() if isinstance(v, list)]
# There must be at least 1 Monte Carlo-dependent field
assert len(list_keys) > 0
first_list_key = list_keys[0]
assert len(data[first_list_key]) == 3
finally:
if os.path.exists(filename):
os.remove(filename)
Comment on lines +30 to +32
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be more pythonic...

    finally:
        if os.path.exists(filename):
            os.remove(filename)