Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 35 additions & 8 deletions rocketpy/simulation/monte_carlo.py
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,39 @@ def __setup_files(self, append):
except OSError as error:
raise OSError(f"Error creating files: {error}") from error

def _append_simulation_record(self, inputs_json, outputs_json):
"""Append one simulation's inputs and outputs as a paired record.

Writes the inputs row first, then the outputs row. If the outputs write
fails, the inputs file is truncated back to its size before this call so
the two files do not drift out of alignment.

Parameters
----------
inputs_json : str
Serialized inputs row, including its trailing newline.
outputs_json : str
Serialized outputs row, including its trailing newline.
"""
input_path = self.input_file
output_path = self.output_file

try:
previous_input_size = os.path.getsize(input_path)
except OSError:
previous_input_size = 0

with open(input_path, "a", encoding="utf-8") as f:
f.write(inputs_json)

try:
with open(output_path, "a", encoding="utf-8") as f:
f.write(outputs_json)
except Exception:
with open(input_path, "rb+") as f:
f.truncate(previous_input_size)
raise

def __run_in_serial(self):
"""
Runs the monte carlo simulation in serial mode.
Expand All @@ -289,10 +322,7 @@ def __run_in_serial(self):
inputs_json = self.__evaluate_flight_inputs(sim_monitor.count)
outputs_json = self.__evaluate_flight_outputs(flight, sim_monitor.count)

with open(self.input_file, "a", encoding="utf-8") as f:
f.write(inputs_json)
with open(self.output_file, "a", encoding="utf-8") as f:
f.write(outputs_json)
self._append_simulation_record(inputs_json, outputs_json)

sim_monitor.print_update_status()

Expand Down Expand Up @@ -431,10 +461,7 @@ def __sim_producer(self, seed, sim_monitor, mutex, error_event): # pylint: disa

break

with open(self.input_file, "a", encoding="utf-8") as f:
f.write(inputs_json)
with open(self.output_file, "a", encoding="utf-8") as f:
f.write(outputs_json)
self._append_simulation_record(inputs_json, outputs_json)

sim_monitor.print_update_status()
finally:
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/simulation/test_monte_carlo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import builtins
import csv
import json
import os
import pathlib
from collections import namedtuple
from unittest.mock import patch

import matplotlib as plt
import numpy as np
Expand Down Expand Up @@ -86,6 +89,37 @@ def __init__(self):
}


def test_append_simulation_record_rolls_back_inputs_on_output_failure(tmp_path):
"""If the outputs append fails, the inputs row must not remain on disk."""
mc = MockMonteCarlo()
input_file = tmp_path / "inputs.json"
output_file = tmp_path / "outputs.json"
input_file.write_text('{"index": 0}\n', encoding="utf-8")
output_file.write_text('{"index": 0}\n', encoding="utf-8")
mc._input_file = str(input_file)
mc._output_file = str(output_file)

mc._append_simulation_record('{"index": 1}\n', '{"index": 1}\n')

original_open = builtins.open
output_path = os.fspath(output_file)

def failing_output_open(*args, **kwargs):
# Match builtins.open call shapes without keyword-before-vararg (W1113).
file = args[0] if args else kwargs["file"]
mode = args[1] if len(args) > 1 else kwargs.get("mode", "r")
if os.fspath(file) == output_path and "a" in mode:
raise OSError("no space left on device")
return original_open(*args, **kwargs)

with pytest.raises(OSError, match="no space left on device"):
with patch("builtins.open", side_effect=failing_output_open):
mc._append_simulation_record('{"index": 2}\n', '{"index": 2}\n')

assert input_file.read_text(encoding="utf-8") == '{"index": 0}\n{"index": 1}\n'
assert output_file.read_text(encoding="utf-8") == '{"index": 0}\n{"index": 1}\n'


def test_estimate_confidence_interval_contains_known_mean():
"""Checks that the confidence interval contains the known mean."""
mc = MockMonteCarlo()
Expand Down