From a25c2ee8b3d526223856da228b82495f9ae4074f Mon Sep 17 00:00:00 2001 From: Morgan Thomas Date: Sun, 6 Oct 2024 21:02:53 +0100 Subject: [PATCH 01/13] Inital implementation of callback function with some quick fixes of existing functions for compatability --- rocketpy/simulation/monte_carlo.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index cbe7b6734..ef4818d2b 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -80,7 +80,8 @@ class MonteCarlo: """ def __init__( - self, filename, environment, rocket, flight, export_list=None + self, filename, environment, rocket, flight, export_list=None, + export_function=None ): # pylint: disable=too-many-statements """ Initialize a MonteCarlo object. @@ -104,6 +105,11 @@ def __init__( `out_of_rail_stability_margin`, `out_of_rail_time`, `out_of_rail_velocity`, `max_mach_number`, `frontal_surface_wind`, `lateral_surface_wind`. Default is None. + export_function : callable, optional + A function which gets called at the end of a simulation to collect + additional data to be exported that isn't pre-defined. Takes the + Flight object as an argument and returns a dictionary. Default is None. + Returns ------- @@ -132,6 +138,7 @@ def __init__( self._last_print_len = 0 # used to print on the same line self.export_list = self.__check_export_list(export_list) + self.export_function = export_function try: self.import_inputs() @@ -359,6 +366,13 @@ def __export_flight_data( for export_item in self.export_list } + if self.export_function is not None: + additional_exports = self.export_function(flight) + for key in additional_exports.keys(): + if key in self.export_list: + raise ValueError(f"Invalid export function, returns dict which overwrites key, '{key}'") + results = results | additional_exports + input_file.write(json.dumps(inputs_dict, cls=RocketPyEncoder) + "\n") output_file.write(json.dumps(results, cls=RocketPyEncoder) + "\n") @@ -654,9 +668,12 @@ def set_processed_results(self): """ self.processed_results = {} for result, values in self.results.items(): - mean = np.mean(values) - stdev = np.std(values) - self.processed_results[result] = (mean, stdev) + try: + mean = np.mean(values) + stdev = np.std(values) + self.processed_results[result] = (mean, stdev) + except TypeError: + self.processed_results[result] = (None, None) # Import methods From 59296567703a6d90b0601dd46189cc08eeca4893 Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Mon, 11 Nov 2024 10:42:56 -0300 Subject: [PATCH 02/13] bug: fix print error when summary contains None results --- rocketpy/prints/monte_carlo_prints.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rocketpy/prints/monte_carlo_prints.py b/rocketpy/prints/monte_carlo_prints.py index 6249626ce..dc7cc1265 100644 --- a/rocketpy/prints/monte_carlo_prints.py +++ b/rocketpy/prints/monte_carlo_prints.py @@ -24,4 +24,7 @@ def all(self): print(f"{'Parameter':>25} {'Mean':>15} {'Std. Dev.':>15}") print("-" * 60) for key, value in self.monte_carlo.processed_results.items(): - print(f"{key:>25} {value[0]:>15.3f} {value[1]:>15.3f}") + try: + print(f"{key:>25} {value[0]:>15.3f} {value[1]:>15.3f}") + except TypeError: + print(f"{key:>25} {str(value[0]):>15} {str(value[1]):>15}") From 62b3edbd7afec61229c0d4da1f2057368457bdb3 Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Mon, 11 Nov 2024 10:50:12 -0300 Subject: [PATCH 03/13] ENH: avoid computing mean and std when it might not make sense (e.g dates/impact_state) --- rocketpy/simulation/monte_carlo.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index ea864ee8a..7c7545e8c 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -80,8 +80,13 @@ class MonteCarlo: """ def __init__( - self, filename, environment, rocket, flight, export_list=None, - export_function=None + self, + filename, + environment, + rocket, + flight, + export_list=None, + export_function=None, ): # pylint: disable=too-many-statements """ Initialize a MonteCarlo object. @@ -370,7 +375,9 @@ def __export_flight_data( additional_exports = self.export_function(flight) for key in additional_exports.keys(): if key in self.export_list: - raise ValueError(f"Invalid export function, returns dict which overwrites key, '{key}'") + raise ValueError( + f"Invalid export function, returns dict which overwrites key, '{key}'" + ) results = results | additional_exports input_file.write(json.dumps(inputs_dict, cls=RocketPyEncoder) + "\n") @@ -669,9 +676,12 @@ def set_processed_results(self): self.processed_results = {} for result, values in self.results.items(): try: - mean = np.mean(values) - stdev = np.std(values) - self.processed_results[result] = (mean, stdev) + if isinstance(values[0], float): + mean = np.mean(values) + stdev = np.std(values) + self.processed_results[result] = (mean, stdev) + else: + self.processed_results[result] = (None, None) except TypeError: self.processed_results[result] = (None, None) From b60294980302525025d6fc53b0c9c806e1e288fb Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Mon, 11 Nov 2024 11:12:08 -0300 Subject: [PATCH 04/13] ENH: raises an error when user 'export_function' contains errors from any kind (attribute, logical, etc) --- rocketpy/simulation/monte_carlo.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index 7c7545e8c..9ad83f1bd 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -372,7 +372,14 @@ def __export_flight_data( } if self.export_function is not None: - additional_exports = self.export_function(flight) + try: + additional_exports = self.export_function(flight) + except Exception as e: + raise ValueError( + "An error was encountered running your custom export function. " + "Check for errors in 'export_function' definition." + ) from e + for key in additional_exports.keys(): if key in self.export_list: raise ValueError( From b0e0a08d69702ab61ade074c570d528ea0fee668 Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Mon, 11 Nov 2024 16:22:09 -0300 Subject: [PATCH 05/13] TST: providing tests to monte carlo callbacks 'export_function' argument --- tests/integration/test_monte_carlo.py | 53 +++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/integration/test_monte_carlo.py b/tests/integration/test_monte_carlo.py index b5caddbc8..764a64338 100644 --- a/tests/integration/test_monte_carlo.py +++ b/tests/integration/test_monte_carlo.py @@ -111,3 +111,56 @@ def test_monte_carlo_export_ellipses_to_kml(monte_carlo_calisto_pre_loaded): ) os.remove("monte_carlo_class_example.kml") + + +@pytest.mark.slow +def test_monte_carlo_callback(monte_carlo_calisto): + """Tests the export_function argument of the MonteCarlo class. + + Parameters + ---------- + monte_carlo_calisto : MonteCarlo + The MonteCarlo object, this is a pytest fixture. + """ + + def valid_export_function(flight): + custom_export_dict = { + "name": flight.name, + "density_t0": flight.env.density(0), + } + return custom_export_dict + + monte_carlo_calisto.export_function = valid_export_function + # NOTE: this is really slow, it runs 10 flight simulations + monte_carlo_calisto.simulate(number_of_simulations=10, append=False) + + # tests if print works when we have None in summary + monte_carlo_calisto.info() + + # tests if logical errors in export functions raise errors + def export_function_with_logical_error(flight): + custom_export_dict = { + "date": flight.env.date, + "density_t0": flight.env.density(0) / "0", + } + return custom_export_dict + + monte_carlo_calisto.export_function = export_function_with_logical_error + # NOTE: this is really slow, it runs 10 flight simulations + with pytest.raises(ValueError): + monte_carlo_calisto.simulate(number_of_simulations=10, append=False) + + # tests if overwriting default exports raises errors + def export_function_with_overwriting_error(flight): + custom_export_dict = { + "apogee": flight.apogee, + } + return custom_export_dict + + monte_carlo_calisto.export_function = export_function_with_overwriting_error + with pytest.raises(ValueError): + monte_carlo_calisto.simulate(number_of_simulations=10, append=False) + + os.remove("monte_carlo_test.errors.txt") + os.remove("monte_carlo_test.outputs.txt") + os.remove("monte_carlo_test.inputs.txt") From d2232e0c6b85ee59e0776be344dc5200a473caea Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Wed, 13 Nov 2024 15:27:09 -0300 Subject: [PATCH 06/13] DOC: add simple example on how to use the callback export function --- .../monte_carlo_class_usage.ipynb | 88 ++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb index 3886f72c4..10be08133 100644 --- a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb +++ b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb @@ -1115,6 +1115,92 @@ " type=\"impact\",\n", ")" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Custom exports using callback functions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We have shown, so far, how to perform to use the `MonteCarlo` class and visualize its results. By default, some variables exported to the output files, such as *apogee* and *x_impact*. The `export_list` argument provides a simplified way for the user to export additional variables listed in the documentation, such as *inclination* and *heading*. \n", + "\n", + "There are applications in which you might need to extract more information in the results than the `export_list` argument can handle. To that end, the `MonteCarlo` class has a `export_function` argument which allows you customize further the output of the simulation.\n", + "\n", + "To exemplify its use, we show how to export the *date* of the environment used in the simulation together with the *average reynolds number* along with the default variables." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We will use the `stochastic_env`, `stochastic_rocket` and `stochastic_flight` objects previously defined, and only change the `MonteCarlo` object. First, we need to define our customized export function." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "def custom_export_function(flight):\n", + " reynold_number_list = flight.reynolds_number(flight.time)\n", + " average_reynolds_number = np.mean(reynold_number_list)\n", + " custom_exports = {\n", + " \"average_reynolds_number\": average_reynolds_number,\n", + " \"date\": flight.env.date,\n", + " }\n", + " return custom_exports" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + "The `export_function` must be a function which takes a `Flight` object and outputs a dictionary whose keys are variables names to export and their values. Notice how we can compute complex expressions in this function and just export the result. For instance, the *average_reynolds_number* calls the `flight.reynolds_number` method for each value in `flight.time` list and computes the average value using numpy's `mean`. The *date* variable is straightforward.\n", + "\n", + "After we define the export function, we pass it as an argument to the `MonteCarlo` class." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_dispersion = MonteCarlo(\n", + " filename=\"monte_carlo_analysis_outputs/monte_carlo_class_example_customized\",\n", + " environment=stochastic_env,\n", + " rocket=stochastic_rocket,\n", + " flight=stochastic_flight,\n", + " export_function=custom_export_function,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_dispersion.simulate(number_of_simulations=10, append=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "test_dispersion.prints.all()" + ] } ], "metadata": { @@ -1134,7 +1220,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.11" + "version": "3.11.2" } }, "nbformat": 4, From afb4e3fc657ec47f133e9bd16079232eaa189c6e Mon Sep 17 00:00:00 2001 From: Lucas Prates Date: Wed, 13 Nov 2024 15:28:04 -0300 Subject: [PATCH 07/13] MNT: make black to notebook example --- .../notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb index 10be08133..42d0ae424 100644 --- a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb +++ b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb @@ -1149,6 +1149,7 @@ "source": [ "import numpy as np\n", "\n", + "\n", "def custom_export_function(flight):\n", " reynold_number_list = flight.reynolds_number(flight.time)\n", " average_reynolds_number = np.mean(reynold_number_list)\n", From f19cb815877bf9061e7bdc2648367f91ec2f02bd Mon Sep 17 00:00:00 2001 From: Lucas de Oliveira Prates Date: Fri, 22 Nov 2024 15:53:38 -0300 Subject: [PATCH 08/13] MNT: refactoring 'export_list' to 'data_collectors' dict of callbacks --- rocketpy/simulation/monte_carlo.py | 66 +++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index 9ad83f1bd..e240f88ea 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -86,7 +86,7 @@ def __init__( rocket, flight, export_list=None, - export_function=None, + data_collector=None, ): # pylint: disable=too-many-statements """ Initialize a MonteCarlo object. @@ -110,11 +110,10 @@ def __init__( `out_of_rail_stability_margin`, `out_of_rail_time`, `out_of_rail_velocity`, `max_mach_number`, `frontal_surface_wind`, `lateral_surface_wind`. Default is None. - export_function : callable, optional - A function which gets called at the end of a simulation to collect - additional data to be exported that isn't pre-defined. Takes the - Flight object as an argument and returns a dictionary. Default is None. - + data_collector : dict, optional + A dictionary whose keys are the names of the exported variables + and the values are callback functions. The callback functions receive + a Flight object and returns a value of that variable. Returns ------- @@ -143,7 +142,8 @@ def __init__( self._last_print_len = 0 # used to print on the same line self.export_list = self.__check_export_list(export_list) - self.export_function = export_function + self._check_data_collector(data_collector) + self.data_collector = data_collector try: self.import_inputs() @@ -371,20 +371,15 @@ def __export_flight_data( for export_item in self.export_list } - if self.export_function is not None: - try: - additional_exports = self.export_function(flight) - except Exception as e: - raise ValueError( - "An error was encountered running your custom export function. " - "Check for errors in 'export_function' definition." - ) from e - - for key in additional_exports.keys(): - if key in self.export_list: + if self.data_collector is not None: + additional_exports = {} + for key, callback in self.data_collector.items(): + try: + additional_exports[key] = callback(flight) + except Exception as e: raise ValueError( - f"Invalid export function, returns dict which overwrites key, '{key}'" - ) + f"An error was encountered running 'data_collector' callback {key}. " + ) from e results = results | additional_exports input_file.write(json.dumps(inputs_dict, cls=RocketPyEncoder) + "\n") @@ -494,6 +489,37 @@ def __check_export_list(self, export_list): return export_list + def _check_data_collector(self, data_collector): + """Check if data collector provided is a valid + + Parameters + ---------- + data_collector : dict + A dictionary whose keys are the names of the exported variables + and the values are callback functions that receive a Flight object + and returns a value of that variable + """ + + if data_collector is not None: + + if not isinstance(data_collector, dict): + raise ValueError( + "Invalid 'data_collector' argument! " + "It must be a dict of callback functions." + ) + + for key, callback in data_collector.items(): + if key in self.export_list: + raise ValueError( + "Invalid 'data_collector' key! " + f"Variable names overwrites standard key '{key}'." + ) + if not callable(callback): + raise ValueError( + f"Invalid value in 'data_collector' for key '{key}'! " + "Values must be python callables (callback functions)." + ) + def __reprint(self, msg, end="\n", flush=False): """ Prints a message on the same line as the previous one and replaces the From f10cad0ea69de459f53fc8faeee7c15ec76f5166 Mon Sep 17 00:00:00 2001 From: Lucas de Oliveira Prates Date: Fri, 22 Nov 2024 15:54:19 -0300 Subject: [PATCH 09/13] TST: refactoring tests to new 'data_collector' input format (dict of callbacks) --- tests/integration/test_monte_carlo.py | 52 ++++++++++++++------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/tests/integration/test_monte_carlo.py b/tests/integration/test_monte_carlo.py index 764a64338..51d8bfae9 100644 --- a/tests/integration/test_monte_carlo.py +++ b/tests/integration/test_monte_carlo.py @@ -115,7 +115,7 @@ def test_monte_carlo_export_ellipses_to_kml(monte_carlo_calisto_pre_loaded): @pytest.mark.slow def test_monte_carlo_callback(monte_carlo_calisto): - """Tests the export_function argument of the MonteCarlo class. + """Tests the data_collector argument of the MonteCarlo class. Parameters ---------- @@ -123,41 +123,43 @@ def test_monte_carlo_callback(monte_carlo_calisto): The MonteCarlo object, this is a pytest fixture. """ - def valid_export_function(flight): - custom_export_dict = { - "name": flight.name, - "density_t0": flight.env.density(0), - } - return custom_export_dict + # define valid data collector + valid_data_collector = { + "name": lambda flight: flight.name, + "density_t0": lambda flight: flight.env.density(0), + } - monte_carlo_calisto.export_function = valid_export_function + monte_carlo_calisto.data_collector = valid_data_collector # NOTE: this is really slow, it runs 10 flight simulations monte_carlo_calisto.simulate(number_of_simulations=10, append=False) # tests if print works when we have None in summary monte_carlo_calisto.info() - # tests if logical errors in export functions raise errors - def export_function_with_logical_error(flight): - custom_export_dict = { - "date": flight.env.date, - "density_t0": flight.env.density(0) / "0", - } - return custom_export_dict + ## tests if an error is raised for invalid data_collector definitions + # invalid type + def invalid_data_collector(flight): + return flight.name - monte_carlo_calisto.export_function = export_function_with_logical_error - # NOTE: this is really slow, it runs 10 flight simulations with pytest.raises(ValueError): - monte_carlo_calisto.simulate(number_of_simulations=10, append=False) + monte_carlo_calisto._check_data_collector(invalid_data_collector) + + # invalid key overwrite + invalid_data_collector = {"apogee": lambda flight: flight.apogee} + with pytest.raises(ValueError): + monte_carlo_calisto._check_data_collector(invalid_data_collector) - # tests if overwriting default exports raises errors - def export_function_with_overwriting_error(flight): - custom_export_dict = { - "apogee": flight.apogee, - } - return custom_export_dict + # invalid callback definition + invalid_data_collector = {"name": "Calisto"} # callbacks must be callables! + with pytest.raises(ValueError): + monte_carlo_calisto._check_data_collector(invalid_data_collector) - monte_carlo_calisto.export_function = export_function_with_overwriting_error + # invalid logic (division by zero) + invalid_data_collector = { + "density_t0": lambda flight: flight.env.density(0) / "0", + } + monte_carlo_calisto.data_collector = invalid_data_collector + # NOTE: this is really slow, it runs 10 flight simulations with pytest.raises(ValueError): monte_carlo_calisto.simulate(number_of_simulations=10, append=False) From 1a09345998b22172d88db506a154190f42cf6549 Mon Sep 17 00:00:00 2001 From: Lucas de Oliveira Prates Date: Fri, 22 Nov 2024 16:01:07 -0300 Subject: [PATCH 10/13] MNT: removing float check on 'set_processed_results' --- rocketpy/simulation/monte_carlo.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index e240f88ea..566d4247f 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -512,7 +512,7 @@ def _check_data_collector(self, data_collector): if key in self.export_list: raise ValueError( "Invalid 'data_collector' key! " - f"Variable names overwrites standard key '{key}'." + f"Variable names overwrites 'export_list' key '{key}'." ) if not callable(callback): raise ValueError( @@ -709,12 +709,9 @@ def set_processed_results(self): self.processed_results = {} for result, values in self.results.items(): try: - if isinstance(values[0], float): - mean = np.mean(values) - stdev = np.std(values) - self.processed_results[result] = (mean, stdev) - else: - self.processed_results[result] = (None, None) + mean = np.mean(values) + stdev = np.std(values) + self.processed_results[result] = (mean, stdev) except TypeError: self.processed_results[result] = (None, None) From 1a51082b382ed262ac1b148193769275c85eb467 Mon Sep 17 00:00:00 2001 From: Lucas de Oliveira Prates Date: Fri, 22 Nov 2024 16:25:29 -0300 Subject: [PATCH 11/13] DOC: updating notebook example and class documentation after callback refactoring --- .../monte_carlo_class_usage.ipynb | 32 +++++++++++-------- rocketpy/simulation/monte_carlo.py | 14 ++++++-- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb index 42d0ae424..cfd31aa66 100644 --- a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb +++ b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb @@ -1129,7 +1129,7 @@ "source": [ "We have shown, so far, how to perform to use the `MonteCarlo` class and visualize its results. By default, some variables exported to the output files, such as *apogee* and *x_impact*. The `export_list` argument provides a simplified way for the user to export additional variables listed in the documentation, such as *inclination* and *heading*. \n", "\n", - "There are applications in which you might need to extract more information in the results than the `export_list` argument can handle. To that end, the `MonteCarlo` class has a `export_function` argument which allows you customize further the output of the simulation.\n", + "There are applications in which you might need to extract more information in the results than the `export_list` argument can handle. To that end, the `MonteCarlo` class has a `data_collector` argument which allows you customize further the output of the simulation.\n", "\n", "To exemplify its use, we show how to export the *date* of the environment used in the simulation together with the *average reynolds number* along with the default variables." ] @@ -1138,36 +1138,39 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We will use the `stochastic_env`, `stochastic_rocket` and `stochastic_flight` objects previously defined, and only change the `MonteCarlo` object. First, we need to define our customized export function." + "We will use the `stochastic_env`, `stochastic_rocket` and `stochastic_flight` objects previously defined, and only change the `MonteCarlo` object. First, we need to define our customized data collector." ] }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", - "\n", - "def custom_export_function(flight):\n", + "# Defining custom callback functions\n", + "def get_average_reynolds_number(flight):\n", " reynold_number_list = flight.reynolds_number(flight.time)\n", " average_reynolds_number = np.mean(reynold_number_list)\n", - " custom_exports = {\n", - " \"average_reynolds_number\": average_reynolds_number,\n", - " \"date\": flight.env.date,\n", - " }\n", - " return custom_exports" + " return average_reynolds_number\n", + "\n", + "def get_date(flight):\n", + " return flight.env.date\n", + "\n", + "custom_data_collector = {\n", + " \"average_reynolds_number\": get_average_reynolds_number,\n", + " \"date\": get_date,\n", + "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "The `data_collector` must be a dictionary whose keys are the names of the variables we want to export and the values are callback functions (python callables) that compute these variable values. Notice how we can compute complex expressions in this function and just export the result. For instance, the *get_average_reynolds_number* calls the `flight.reynolds_number` method for each value in `flight.time` list and computes the average value using numpy's `mean`. The *date* variable is straightforward.\n", "\n", - "The `export_function` must be a function which takes a `Flight` object and outputs a dictionary whose keys are variables names to export and their values. Notice how we can compute complex expressions in this function and just export the result. For instance, the *average_reynolds_number* calls the `flight.reynolds_number` method for each value in `flight.time` list and computes the average value using numpy's `mean`. The *date* variable is straightforward.\n", - "\n", - "After we define the export function, we pass it as an argument to the `MonteCarlo` class." + "After we define the data collector, we pass it as an argument to the `MonteCarlo` class." ] }, { @@ -1181,7 +1184,8 @@ " environment=stochastic_env,\n", " rocket=stochastic_rocket,\n", " flight=stochastic_flight,\n", - " export_function=custom_export_function,\n", + " export_list=[\"apogee\", \"apogee_time\", \"x_impact\"],\n", + " data_collector=custom_data_collector,\n", ")" ] }, diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index 566d4247f..70584838a 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -48,6 +48,9 @@ class MonteCarlo: The stochastic flight object to be iterated over. export_list : list The list of variables to export at each simulation. + data_collector : dict + A dictionary whose keys are the names of the additional + exported variables and the values are callback functions. inputs_log : list List of dictionaries with the inputs used in each simulation. outputs_log : list @@ -112,8 +115,15 @@ def __init__( `lateral_surface_wind`. Default is None. data_collector : dict, optional A dictionary whose keys are the names of the exported variables - and the values are callback functions. The callback functions receive - a Flight object and returns a value of that variable. + and the values are callback functions. The keys (variable names) must not + overwrite the default names on 'export_list'. The callback functions receive + a Flight object and returns a value of that variable. For instance + + .. code-block:: python + custom_data_collector = { + "max_acceleration": lambda flight: max(flight.acceleration(flight.time)), + "date": lambda flight: flight.env.date, + } Returns ------- From e4aead78b1014824f394b8679958e719967c9482 Mon Sep 17 00:00:00 2001 From: Lucas de Oliveira Prates Date: Fri, 22 Nov 2024 16:31:56 -0300 Subject: [PATCH 12/13] MNT: make black for notebook --- .../monte_carlo_analysis/monte_carlo_class_usage.ipynb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb index cfd31aa66..2f94a0b16 100644 --- a/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb +++ b/docs/notebooks/monte_carlo_analysis/monte_carlo_class_usage.ipynb @@ -1149,15 +1149,18 @@ "source": [ "import numpy as np\n", "\n", + "\n", "# Defining custom callback functions\n", "def get_average_reynolds_number(flight):\n", " reynold_number_list = flight.reynolds_number(flight.time)\n", " average_reynolds_number = np.mean(reynold_number_list)\n", " return average_reynolds_number\n", "\n", + "\n", "def get_date(flight):\n", " return flight.env.date\n", "\n", + "\n", "custom_data_collector = {\n", " \"average_reynolds_number\": get_average_reynolds_number,\n", " \"date\": get_date,\n", From 6c477e3f2f3fef3f1a81dcab25a048453ebbee59 Mon Sep 17 00:00:00 2001 From: Lucas de Oliveira Prates Date: Fri, 22 Nov 2024 18:16:21 -0300 Subject: [PATCH 13/13] DOC: update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d083a77..90d73549b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Attention: The newest changes should be on top --> ### Added +- ENH: Callback function for collecting additional data from Monte Carlo sims [#702](https://github.com/RocketPy-Team/RocketPy/pull/702) - ENH: Implement optional plot saving [#597](https://github.com/RocketPy-Team/RocketPy/pull/597) ### Changed