From 0bfec91e05f896b436b80a27e0217e98edc3ee02 Mon Sep 17 00:00:00 2001 From: Gui-FernandesBR Date: Mon, 10 Feb 2025 05:19:20 -0300 Subject: [PATCH] Squashed commit of the following: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commit 4e0ef923378347ff2944a76b678a55060d02062d Merge: 0009c24d 78645903 Author: Gui-FernandesBR Date: Mon Feb 10 05:05:23 2025 -0300 Merge branch 'develop' into enh/parallel_montecarlo commit 0009c24d13a54100f0f4451cd7e38a3e996d5881 Author: Lucas de Oliveira Prates Date: Wed Dec 18 09:39:40 2024 -0300 BUG: fixing random number generator bug in StochasticRocket and issues inside methods of Components commit ebf6bd06a4234708f3799d2859fc557d4cb05cd8 Merge: 00d9d021 2218f0f3 Author: Pedro Bressan Date: Mon Dec 16 22:35:18 2024 +0100 Merge remote-tracking branch 'origin/develop' into enh/parallel_montecarlo commit 00d9d021d2796cb6818065711873db0c1ac98c14 Author: Pedro Bressan Date: Mon Dec 16 22:11:16 2024 +0100 MNT: Simplify Monte Carlo parallel export structure. commit 2218f0f38f842cc5bfae684b46f673af824480e7 Author: Yogiraj Gutte <53410698+yogirajgutte@users.noreply.github.com> Date: Mon Dec 16 08:22:33 2024 +0530 MNT: move piecewise functions to separate file (#746) * MNT: move piecewise functions to separate file closes #667 * improved import for linting * MNT: applying code formaters * ENH: simplifying and optimizing the function, implementing tests. * MNT: update changelog and apply changes suggested in review --------- Co-authored-by: Lucas Prates <57069366+Lucas-Prates@users.noreply.github.com> Co-authored-by: Lucas de Oliveira Prates Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> commit 7a122ad773e5d664202d9f9353d236f3630404a3 Author: Lucas Gonçalves Date: Mon Dec 16 02:49:34 2024 +0000 DOCS: Erebus11 - BME Suborbitals - 2022 flight simulation (#757) * DOCS: add data for bme suborbitals flight example * DOCS: add simulation file for bme suborbitals flight example * DOCS: Error in motor fixed * DOC: Improve flight examples documentation Update flight simulation documentation with improved markdown headers and replace matplotlib with Plotly for enhanced visualizations * DEV: update changelog * DOC: Update simulation and flight data * DOC: Add weather file for simulations * DOC: Updates to erebus flight sim * DOC: Update changelog * DOC: Fix title in camoes simul * DOC:Update flight data * DOC: Comparison plots * MNT: Delete unnecessary file * DOC: Update index * DOC: run Black * DEV: update changelog * DOC: small fix index --------- Co-authored-by: Gui-FernandesBR Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> commit ed6af689193666d59d8e7c9e3f6d1f300f4ebde8 Author: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Date: Sun Dec 15 23:43:31 2024 -0300 DOC: flight comparison improvements (#755) * DOC: Improve flight examples documentation Update flight simulation documentation with improved markdown headers and replace matplotlib with Plotly for enhanced visualizations * DEV: update changelog * DOC: Fix title in camoes simul * DOC: Update docs/requirements.txt to include new dependencies --------- Co-authored-by: LUCKIN13 commit c7f1623f86ab4e56504c4364ac8d4b79105bdae6 Author: Gui-FernandesBR Date: Sat Dec 14 23:55:09 2024 -0300 DEV: move CITATION file back to the root commit f4075591447ad4f5bffb6f3b43dcc13b3ac6cdee Author: Lucas Gonçalves Date: Sun Dec 15 02:30:57 2024 +0000 DOC: Lince (Team STAR) 2023 flight sim (#752) * DOC: Add data for lince example * DOC: create simulation file * DOC: Update simulation file * DOC: Add euroc_2023 weather data * DOC: Add flight data * DOC: Update flight sim * DOC: Changelog update & Run black * DOC: Small fixes to fligh sim * DOC : Run black * DOC: Simulation fixes and index sim add * DOC: Update environments in flight documentation * DOC: fix Lince values in the rst file --------- Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Co-authored-by: Gui-FernandesBR commit 5d2fdef861544eeacfe38136d4e3ea6b6d05a03d Author: Lucas Gonçalves Date: Sun Dec 15 01:55:16 2024 +0000 DOCS : Andromeda 2022 flight simulation (#754) * DOC : Add weather file for simulation * DOC : Add data for simulation example * DOC: Add flight simulation example * DOC: Update index * DOC: Update Changelog * Update docs/examples/index.rst --------- Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> commit 17207ca8f9b4f1e5548883e6f07067d083528254 Author: Caio Souza <99217921+caioessouza@users.noreply.github.com> Date: Sun Dec 8 03:15:00 2024 +0100 ENH: create a dataset of pre-registered motors. See #664 (#744) * ENH: Create a dataset of pre-registered motors. See #664 I followed the recommendation "Download and save several .eng files in the repo so we can install it along with the rocketpy package itself". The website thrustcurve.org was very useful to search for some .eng files. I mainly focused in some of the main brands on the market: Cesaroni, Aero Tech, Animal Motors and Loki. And also focused on classes K to M, because this is the main range of total impulse that I'm used to seeing in rocketry. I tried to pick motors with a difference of about 300~600Ns in total impulse. Some more improvements than can also be made following this issue are expanding the dataset for whole SolidMotor objects, more than only thrust curves. I think this would be what the recommendation "Save .json files with all the information we may find available on internet" could mean. I decided to go for the simple for now, but having the thrust curves is a good first step to implementing that in the future, which I would totally be able to do! * Update CHANGELOG.md * MNT: git rename motor eng files in data folder * ENH: Create a dataset of pre-registered motors. See #664 I followed the recommendation "Download and save several .eng files in the repo so we can install it along with the rocketpy package itself". The website thrustcurve.org was very useful to search for some .eng files. I mainly focused in some of the main brands on the market: Cesaroni, Aero Tech, Animal Motors and Loki. And also focused on classes K to M, because this is the main range of total impulse that I'm used to seeing in rocketry. I tried to pick motors with a difference of about 300~600Ns in total impulse. Some more improvements than can also be made following this issue are expanding the dataset for whole SolidMotor objects, more than only thrust curves. I think this would be what the recommendation "Save .json files with all the information we may find available on internet" could mean. I decided to go for the simple for now, but having the thrust curves is a good first step to implementing that in the future, which I would totally be able to do! Update CHANGELOG.md MNT: git rename motor eng files in data folder --------- Co-authored-by: Gui-FernandesBR commit f1b57eff9022ff774527b0154708b1a76365346c Author: ArthurJWH <167456467+ArthurJWH@users.noreply.github.com> Date: Sat Dec 7 20:58:13 2024 -0500 DOC: add Defiance flight example (#742) * BLD: add a flight example to rocketpy "Defiance" rocket flight example was added to doc\examples as my (Arthur Hwang) challenge submission for the Team Recruitment * DOC: Add Defiance launch to flight examples graph. * Updates CHANGELOG --------- Co-authored-by: Pedro Bressan Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> DOC: add Defiance flight example (#742) * BLD: add a flight example to rocketpy "Defiance" rocket flight example was added to doc\examples as my (Arthur Hwang) challenge submission for the Team Recruitment * DOC: Add Defiance launch to flight examples graph. * Updates CHANGELOG --------- Co-authored-by: Pedro Bressan Co-authored-by: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> commit fec6bf0cbd4212be1addbad344f40374c6cbe6fe Author: Pedro Bressan Date: Sun Nov 24 18:53:58 2024 +0100 ENH: Allow for Alternative and Custom ODE Solvers. TST: Add slow testing for different ode solvers. MNT: Move ode solver validation to separate method. commit 80827109ec62fc46b82e7e228cb469c557b61a32 Author: Pedro Bressan Date: Thu Sep 5 16:17:26 2024 -0300 MNT: fix pylint messages on file handling. commit df07955612a80265eafbb5c6f9f62f94d615e31a Author: Pedro Bressan Date: Thu Sep 5 16:10:41 2024 -0300 MNT: add number of workers in parallel mode to prints. commit 1baedf66c4b4f08f7d7efb44bd37d2fa3f8e7af6 Author: Pedro Bressan Date: Thu Sep 5 16:10:07 2024 -0300 MNT: simplify process start up syntax. commit 9f7325c7cf741aa954b3914a26e4ee2dd200390b Author: Pedro Bressan Date: Thu Sep 5 16:01:41 2024 -0300 MNT: avoid unnecessary reseedings on parallel monte carlo. commit 4246809d0b64ba6707d64c72c834445094276725 Author: Pedro Bressan Date: Thu Sep 5 16:00:03 2024 -0300 MNT: add index to outputs of monte carlo. commit e40a8711b5f805423765f6115c4bab34dd1aecb1 Author: Pedro Bressan Date: Fri Aug 23 12:12:43 2024 -0300 DOC: improve docstrings regarding number of workers. commit 6fa90b7ba9449ecc94410940a03435de2b271b53 Merge: d07fcc2c 44beadeb Author: Pedro Bressan Date: Fri Aug 23 12:04:10 2024 -0300 Merge remote-tracking branch 'origin/develop' into enh/parallel_montecarlo commit d07fcc2c8f58b2e003c039cf987b043ac0f0a281 Author: Pedro Bressan Date: Fri Aug 23 12:04:00 2024 -0300 MNT: solve review comments on docstrings and code comments. commit 6dab002d95058969112b3e18c5ece5b984d6cc62 Author: Pedro Bressan Date: Fri Aug 23 11:57:25 2024 -0300 DOC: run 1000 MonteCarlo simulations for better documentatiion example. commit d22c95741474be9a20f44bf6050df92d783625ba Author: Pedro Bressan Date: Fri Aug 23 10:17:59 2024 -0300 MNT: improve process ordering for spawned workers. commit 1e2464309bf14c4a1d826006798994f5b9d5b4df Author: Pedro Bressan Date: Wed Aug 21 19:18:51 2024 -0300 FIX: return to multiprocess library for spawned process support. commit 0e4d2434c39b336fa10dd9defef274ef0e706760 Author: Pedro Bressan Date: Wed Aug 21 19:09:18 2024 -0300 MNT: solve number of processes issue on Windows. commit 5141791357c75a744c9bf4a9b680942fe58e770d Author: Pedro Bressan Date: Mon Aug 19 11:35:04 2024 -0300 MNT: improve object encoding and file handling. commit 8671e5253c47568027846eded6aaefec04f8ceae Author: Pedro Bressan Date: Sun Aug 18 19:46:43 2024 -0300 MNT: soft stop on parallel errors or interrupt. commit 671579162d87a5f2e34acda03f5d756190d8f606 Merge: b7499790 8b4c14a6 Author: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Date: Sun Aug 18 09:44:03 2024 -0300 Merge branch 'develop' into enh/parallel_montecarlo commit b7499790d20c7c03e715261726606e3da6c60bc7 Author: Pedro Henrique Marinho Bressan <87212571+phmbressan@users.noreply.github.com> Date: Fri Aug 16 22:59:06 2024 -0300 Update rocketpy/simulation/monte_carlo.py Co-authored-by: MateusStano <69485049+MateusStano@users.noreply.github.com> commit 6061d3a40663754209b9bda19bb5142803c9f902 Author: Pedro Bressan Date: Fri Aug 16 22:37:41 2024 -0300 MNT: use standard multiprocessing with instance methods. Co-authored-by: MateusStano commit c3c6c3d3c9d68ee5ad68bea5e4ce24fa254ae2ee Merge: cb88e694 3b61784c Author: Gui-FernandesBR <63590233+Gui-FernandesBR@users.noreply.github.com> Date: Tue Aug 13 09:58:50 2024 -0300 Merge branch 'develop' into enh/parallel_montecarlo commit cb88e6946e24a46fb0409b16d16ecf077acd7413 Merge: 3a08f485 4531ed50 Author: Pedro Bressan Date: Tue Aug 6 08:33:54 2024 -0300 Merge remote-tracking branch 'origin/develop' into enh/parallel_montecarlo commit 3a08f48508da3e3bf9a470472e6d04db5b0e1a2b Merge: 049276d4 00487377 Author: Pedro Henrique Marinho Bressan <87212571+phmbressan@users.noreply.github.com> Date: Mon Aug 5 17:45:55 2024 -0300 Merge pull request #649 from RocketPy-Team/mnt/parallel-refactor MNT: Refactor Parallel MonteCarlo and Stochastic Seeding commit 0048737787b626e36f823b5b9731a95edfcf2af7 Author: Pedro Bressan Date: Sat Aug 3 22:39:38 2024 -0300 MNT: improve docstrings according to code reviews. commit fe7bad3f0ec74927083290a3436b8ac5aaf0afef Author: Pedro Bressan Date: Sat Aug 3 22:27:26 2024 -0300 MNT: correct outdated docstrings and improve function naming. commit abe574774fa7bc8e16b5fcc62ab4c9e4dfc40afa Author: Pedro Bressan Date: Sat Aug 3 22:21:18 2024 -0300 FIX: stochastic model seed input not being used. commit d18408eb0679f06f89b42a2999b6a3d8c5c5698f Author: Pedro Bressan Date: Sat Aug 3 22:18:40 2024 -0300 MNT: improve random number generator naming. commit 26f692e6a8c2e9b357c2ed23d3a9bfdc77745e3e Author: Pedro Bressan Date: Sat Aug 3 14:25:52 2024 -0300 MNT: fix printing and formatting issues. commit 004bf23afca766cb137f287f52b983baca805a12 Author: Pedro Bressan Date: Sat Aug 3 13:16:28 2024 -0300 MNT: improve docstrings for parallel MonteCarlo. commit 8316993324e0552a418a889604a7303ca7eb7c27 Author: Pedro Bressan Date: Fri Aug 2 23:13:25 2024 -0300 FIX: parallel random value generation not being independent. commit 049276d4c9bad0ccd0d9aabc065ea33a3e29fbe7 Author: Pedro Bressan Date: Mon Jul 29 11:11:18 2024 -0300 FIX: optional import handling of multiprocess module. commit 2169db1bffa7af02c447c73ab5873b933e2963f2 Author: Pedro Bressan Date: Fri Jul 26 18:11:13 2024 -0300 MNT: update optional dependencies for multiprocess. commit 2cdc95e32ddf5d5c81dc9b97b0d027804edf5ed9 Author: Pedro Bressan Date: Fri Jul 26 17:59:57 2024 -0300 TST: fix testing for file Paths. commit d3a9004ee46b2ed46ad1c26533e0dffc6f314135 Merge: 25a2fed2 a901b459 Author: Pedro Bressan Date: Fri Jul 26 17:55:45 2024 -0300 Merge remote-tracking branch 'origin/develop' into enh/parallel_montecarlo commit 25a2fed250190d9d7ae15c5df70d93c03f3c3a99 Author: Pedro Bressan Date: Fri Jul 26 17:54:46 2024 -0300 MNT: remove light mode and refactor I/O file handling. commit 8008aa71c63038eb6d76cfa77eed351151551056 Author: Pedro Bressan Date: Fri Jul 26 11:09:14 2024 -0300 MNT: remove post processing scripts. commit 2a42b268ca9abe5f4b2b37bcd05b61f0757e461e Author: Pedro Bressan Date: Fri Jul 19 18:12:37 2024 -0300 FIX: small post merge corrections. commit c5634720aec79603db32e849f237b88beff418c4 Merge: 615a9070 d977fbe1 Author: Pedro Bressan Date: Fri Jul 19 17:28:41 2024 -0300 Merge remote-tracking branch 'origin/develop' into enh/parallel_montecarlo commit 615a9070ea0697be6d42e4bf7f9693474cee7bed Author: Pedro Bressan Date: Fri Jul 19 16:56:10 2024 -0300 MNT: run formatters and apply simple review suggestions. commit 342860846b6b45a0cfe3055fad5bfe6826bf7d4b Author: Bruno Sorban Date: Wed Jun 26 13:52:32 2024 +0200 Added time back to exported functions commit 01d77fa7821311a1088ef5aed139f774ce49d288 Author: Bruno Sorban Date: Wed Jun 26 10:07:49 2024 +0200 Encapsulated methods and reduced buffer size commit 2e56977ec5e15f87e46b34288a8a42ad8e037a9b Author: Bruno Sorban Date: Wed Jun 19 17:27:01 2024 +0200 added input export to light mode commit cb276dec5fb8f6a7f4754fb6c957bf6f59c79668 Author: Bruno Sorban Date: Wed Jun 19 16:20:43 2024 +0200 Update sim counter for append mode commit 3114f8171d8cbad74c2e3dca102f60ac5bbd3aaa Author: Bruno Sorban Date: Wed Jun 19 16:12:35 2024 +0200 Removed alpha serializer commit 4fe53144abf742849c00cafbd701ae2105e65127 Author: Bruno Sorban Date: Wed Jun 19 12:21:21 2024 +0200 Updated writer to write unpickled data commit d7ed4a177a07e7efc56782f79e9b8341ba660ae7 Author: Bruno Sorban Date: Wed Jun 19 11:50:54 2024 +0200 Working shared memory with big buffer commit 1999c6d5472e1cfbfd58907ebca121e3f347730d Author: Bruno Sorban Date: Tue Jun 18 17:39:59 2024 +0200 not deserializing data commit ceb18320563e10ce54a364e2fa1a37cf6515b862 Author: Bruno Sorban Date: Wed Jun 12 21:41:33 2024 +0200 Added cpu limit commit d421a83afdbe98bd1ec46efa3ed6dec8c739b4e6 Author: Bruno Sorban Date: Wed Jun 12 18:58:33 2024 +0200 Working 2 way semaphore commit 2b8dc4bf205247541454354e487fca620109ea16 Author: Bruno Sorban Date: Tue Jun 11 14:45:32 2024 +0200 Updated start time commit 98ce6baac251aed08a2a6523cdce2c83d3f00301 Author: Bruno Sorban Date: Tue Jun 11 13:37:38 2024 +0200 Centralized simulation control in SimCounter commit 38a29b19a75935d18d90c0b2b7acde3e9e5aa744 Author: Bruno Sorban Date: Sun Jun 9 15:21:36 2024 +0200 Added documentation commit ee06b9d0040ac4d65808efe11622b0648d01b742 Author: Bruno Sorban Date: Sun Jun 9 15:04:11 2024 +0200 removed unsused file commit b3dcfc612665451838641504db4eb32c8b5c8748 Author: Bruno Sorban Date: Sun Jun 9 15:01:54 2024 +0200 Updated append mode commit 918cbe07d6558e98af3c22b5ac8524b43b8a670b Author: Bruno Sorban Date: Sun Jun 9 14:25:52 2024 +0200 Removed dev files commit 75bc96b0f937e0f8e70aaaf50027b4beee84c590 Author: Bruno Sorban Date: Sun Jun 9 13:31:29 2024 +0200 removed test file commit 5a6547d43925f0f56fc46beb4102ae0b0531708d Author: Bruno Sorban Date: Sun Jun 9 13:27:23 2024 +0200 Updated example notebook commit 1fe04e16e138a372383f268da30a9e6e8bbdfa22 Author: Bruno Sorban Date: Sun Jun 9 13:27:12 2024 +0200 Added central post-processing script commit d57e43620bf6d20cb2ea35534478bc8a5fe40271 Author: Bruno Sorban Date: Sun Jun 9 11:32:37 2024 +0200 Enabled number of workers control commit 9cef6362245cf83f96a6fb509c70fb1356f8909f Author: Bruno Sorban Date: Sun Jun 9 10:26:06 2024 +0200 Added append logic to h5 file commit 175a0250f5a20a953d11c84c63c9b671c61193bb Author: Bruno Sorban Date: Wed Jun 5 18:19:21 2024 +0200 one lock per file commit 1146e20efbdfe823cd1a6e864b7830b59505903f Author: Bruno Sorban Date: Wed Jun 5 18:09:06 2024 +0200 using queue to manage simulations commit be32a758cabac3b9166942c95317fc53b8ff2902 Author: Bruno Sorban Date: Tue May 21 21:15:33 2024 +0200 Added post-processing scripts commit 6ea6ef86784915cc656033afcd64c2f4078a452f Author: Bruno Sorban Date: Tue May 21 17:18:58 2024 +0200 Style changes commit 46f5f007669100ec68c08b5f78d3536582e4ae3a Author: Bruno Sorban Date: Tue May 21 17:09:14 2024 +0200 Enable both export modes for serial and parallel commit 1b50e94b1cf579fef99e16590343a52e73772ba0 Author: Bruno Sorban Date: Thu May 9 16:36:50 2024 +0200 Write mode added commit 2927448c9bc8c60cb37b7ce1797c5432606d3213 Author: Bruno Sorban Date: Thu May 9 12:17:22 2024 +0200 Working version with shared objects commit 6fbe0f77100e5e2f382f503518ccc0a9f32ef3b3 Author: Bruno Sorban Date: Thu May 9 11:17:46 2024 +0200 added counter commit 2d5ff8df42194b3f13f445dc3bd081ca8e249dbb Author: Bruno Sorban Date: Sat May 4 14:17:38 2024 +0200 Basic paralllel structure added --- .vscode/settings.json | 1 + pyproject.toml | 3 +- requirements-optional.txt | 1 + rocketpy/rocket/components.py | 6 + rocketpy/simulation/monte_carlo.py | 597 ++++++++++++++++------- rocketpy/stochastic/stochastic_model.py | 44 +- rocketpy/stochastic/stochastic_rocket.py | 60 ++- rocketpy/tools.py | 31 +- tests/integration/test_flight.py | 36 ++ tests/integration/test_monte_carlo.py | 17 +- 10 files changed, 585 insertions(+), 211 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 6337fe44b..ebf6bf7dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -206,6 +206,7 @@ "Metrum", "modindex", "mult", + "multiprocess", "Mumma", "NASADEM", "nbformat", diff --git a/pyproject.toml b/pyproject.toml index a6787bd58..8d1071b04 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,8 @@ env-analysis = [ ] monte-carlo = [ - "imageio", + "imageio", + "multiprocess>=0.70", "statsmodels", "prettytable", ] diff --git a/requirements-optional.txt b/requirements-optional.txt index 31c37c91b..58ed1030b 100644 --- a/requirements-optional.txt +++ b/requirements-optional.txt @@ -4,5 +4,6 @@ ipywidgets>=7.6.3 jsonpickle timezonefinder imageio +multiprocess>=0.70 statsmodels prettytable \ No newline at end of file diff --git a/rocketpy/rocket/components.py b/rocketpy/rocket/components.py index 43d32b074..66448eb69 100644 --- a/rocketpy/rocket/components.py +++ b/rocketpy/rocket/components.py @@ -148,6 +148,8 @@ def remove(self, component): """ for index, comp in enumerate(self._components): if comp.component == component: + self.__component_list.pop(index) + self.__position_list.pop(index) self._components.pop(index) break else: @@ -168,6 +170,8 @@ def pop(self, index=-1): component : Any The component removed from the list of components. """ + self.__component_list.pop(index) + self.__position_list.pop(index) return self._components.pop(index) def clear(self): @@ -177,6 +181,8 @@ def clear(self): ------- None """ + self.__component_list.clear() + self.__position_list.clear() self._components.clear() def sort_by_position(self, reverse=False): diff --git a/rocketpy/simulation/monte_carlo.py b/rocketpy/simulation/monte_carlo.py index f92fe3f32..886fee1e2 100644 --- a/rocketpy/simulation/monte_carlo.py +++ b/rocketpy/simulation/monte_carlo.py @@ -14,8 +14,11 @@ """ import json +import os +import traceback import warnings -from time import process_time, time +from pathlib import Path +from time import time import numpy as np import simplekml @@ -27,6 +30,7 @@ from rocketpy.tools import ( generate_monte_carlo_ellipses, generate_monte_carlo_ellipses_coordinates, + import_optional_dependency, ) # TODO: Create evolution plots to analyze convergence @@ -136,7 +140,7 @@ def __init__( UserWarning, ) - self.filename = filename + self.filename = Path(filename) self.environment = environment self.rocket = rocket self.flight = flight @@ -149,32 +153,23 @@ def __init__( self.processed_results = {} self.prints = _MonteCarloPrints(self) self.plots = _MonteCarloPlots(self) - self._inputs_dict = {} - self._last_print_len = 0 # used to print on the same line self.export_list = self.__check_export_list(export_list) self._check_data_collector(data_collector) self.data_collector = data_collector - try: - self.import_inputs() - except FileNotFoundError: - self._input_file = f"{filename}.inputs.txt" + self.import_inputs(self.filename.with_suffix(".inputs.txt")) + self.import_outputs(self.filename.with_suffix(".outputs.txt")) + self.import_errors(self.filename.with_suffix(".errors.txt")) - try: - self.import_outputs() - except FileNotFoundError: - self._output_file = f"{filename}.outputs.txt" - - try: - self.import_errors() - except FileNotFoundError: - self._error_file = f"{filename}.errors.txt" - - # pylint: disable=consider-using-with def simulate( - self, number_of_simulations, append=False, **kwargs - ): # pylint: disable=too-many-statements + self, + number_of_simulations, + append=False, + parallel=False, + n_workers=None, + **kwargs + ): # pylint: disable=too-many-statements """ Runs the Monte Carlo simulation and saves all data. @@ -185,6 +180,13 @@ def simulate( append : bool, optional If True, the results will be appended to the existing files. If False, the files will be overwritten. Default is False. + parallel : bool, optional + If True, the simulations will be run in parallel. Default is False. + n_workers : int, optional + Number of workers to be used if ``parallel=True``. If None, the + number of workers will be equal to the number of CPUs available. + A minimum of 2 workers is required for parallel mode. + Default is None. kwargs : dict Custom arguments for simulation export of the ``inputs`` file. Options are: @@ -224,13 +226,11 @@ def simulate( # initialize counters self.number_of_simulations = number_of_simulations - self.__iteration_count = self.num_of_loaded_sims if append else 0 - self.__start_time = time() - self.__start_cpu_time = process_time() + self._initial_sim_idx = self.num_of_loaded_sims if append else 0 - # Begin display - print("Starting Monte Carlo analysis", end="\r") + _SimMonitor.reprint("Starting Monte Carlo analysis") + self.__setup_files(append) try: while self.__iteration_count < self.number_of_simulations: self.__run_single_simulation(input_file, output_file) @@ -254,155 +254,285 @@ def simulate( self.__close_files(input_file, output_file, error_file) raise error finally: - self.total_cpu_time = process_time() - self.__start_cpu_time self.total_wall_time = time() - self.__start_time - self.__terminate_simulation(input_file, output_file, error_file) + if parallel: + self.__run_in_parallel(n_workers) + else: + self.__run_in_serial() - # Auxiliary methods + self.__terminate_simulation() - def __run_single_simulation(self, input_file, output_file): + def __setup_files(self, append): """ - Runs a single simulation and saves the inputs and outputs to the - respective files. + Sets up the files for the simulation, creating them if necessary. Parameters ---------- - input_file : str - The file object to write the inputs. - output_file : str - The file object to write the outputs. + append : bool + If ``True``, the results will be appended to the existing files. If + ``False``, the files will be overwritten. Returns ------- None """ - self.__iteration_count += 1 + # Create data files for inputs, outputs and error logging + open_mode = "r+" if append else "w+" - monte_carlo_flight = Flight( - rocket=self.rocket.create_object(), - environment=self.environment.create_object(), - rail_length=self.flight._randomize_rail_length(), - inclination=self.flight._randomize_inclination(), - heading=self.flight._randomize_heading(), - initial_solution=self.flight.initial_solution, - terminate_on_apogee=self.flight.terminate_on_apogee, - ) + try: + with open(self._input_file, open_mode, encoding="utf-8") as input_file: + idx_i = len(input_file.readlines()) + with open(self._output_file, open_mode, encoding="utf-8") as output_file: + idx_o = len(output_file.readlines()) + with open(self._error_file, open_mode, encoding="utf-8"): + pass + + if idx_i != idx_o and not append: + warnings.warn( + "Input and output files are not synchronized", UserWarning + ) - self._inputs_dict = dict( - item - for d in [ - self.environment.last_rnd_dict, - self.rocket.last_rnd_dict, - self.flight.last_rnd_dict, - ] - for item in d.items() - ) + except OSError as error: + raise OSError(f"Error creating files: {error}") from error - self.__export_flight_data( - flight=monte_carlo_flight, - inputs_dict=self._inputs_dict, - input_file=input_file, - output_file=output_file, - ) + def __run_in_serial(self): + """ + Runs the monte carlo simulation in serial mode. - average_time = (process_time() - self.__start_cpu_time) / self.__iteration_count - estimated_time = int( - (self.number_of_simulations - self.__iteration_count) * average_time - ) - self.__reprint( - f"Current iteration: {self.__iteration_count:06d} | " - f"Average Time per Iteration: {average_time:.3f} s | " - f"Estimated time left: {estimated_time} s", - end="\r", - flush=True, + Returns + ------- + None + """ + sim_monitor = _SimMonitor( + initial_count=self._initial_sim_idx, + n_simulations=self.number_of_simulations, + start_time=time(), ) + try: + while sim_monitor.keep_simulating(): + sim_monitor.increment() + + flight = self.__run_single_simulation() + 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) + + sim_monitor.print_update_status(sim_monitor.count) + + sim_monitor.print_final_status() + + except KeyboardInterrupt: + _SimMonitor.reprint("Keyboard Interrupt, files saved.") + with open(self._error_file, "a", encoding="utf-8") as f: + f.write(inputs_json) + + except Exception as error: + _SimMonitor.reprint(f"Error on iteration {sim_monitor.count}: {error}") + with open(self._error_file, "a", encoding="utf-8") as f: + f.write(inputs_json) + raise error - def __close_files(self, input_file, output_file, error_file): + # pylint: disable=too-many-statements + def __run_in_parallel(self, n_workers=None): """ - Closes all the files. + Runs the monte carlo simulation in parallel. Parameters ---------- - input_file : str - The file object to write the inputs. - output_file : str - The file object to write the outputs. - error_file : str - The file object to write the errors. + n_workers: int, optional + Number of workers to be used. If None, the number of workers + will be equal to the number of CPUs available. Default is None. Returns ------- None """ - input_file.close() - output_file.close() - error_file.close() + if n_workers is None or n_workers > os.cpu_count(): + n_workers = os.cpu_count() - def __terminate_simulation(self, input_file, output_file, error_file): - """ - Terminates the simulation, closes the files and prints the results. + if n_workers < 2: + raise ValueError("Number of workers must be at least 2 for parallel mode.") + + _SimMonitor.reprint(f"Running Monte Carlo simulation with {n_workers} workers.") + + multiprocess, managers = _import_multiprocess() + + with _create_multiprocess_manager(multiprocess, managers) as manager: + mutex = manager.Lock() + simulation_error_event = manager.Event() + sim_monitor = manager._SimMonitor( + initial_count=self._initial_sim_idx, + n_simulations=self.number_of_simulations, + start_time=time(), + ) + + processes = [] + seeds = np.random.SeedSequence().spawn(n_workers) + + for seed in seeds: + sim_producer = multiprocess.Process( + target=self.__sim_producer, + args=( + seed, + sim_monitor, + mutex, + simulation_error_event, + ), + ) + processes.append(sim_producer) + sim_producer.start() + + try: + for sim_producer in processes: + sim_producer.join() + + # Handle error from the child processes + if simulation_error_event.is_set(): + raise RuntimeError( + "An error occurred during the simulation. \n" + f"Check the logs and error file {self.error_file} " + "for more information." + ) + + sim_monitor.print_final_status() + + # Handle error from the main process + # pylint: disable=broad-except + except (Exception, KeyboardInterrupt) as error: + simulation_error_event.set() + + for sim_producer in processes: + sim_producer.join() + + if not isinstance(error, KeyboardInterrupt): + raise error + + def __sim_producer(self, seed, sim_monitor, mutex, error_event): + """Simulation producer to be used in parallel by multiprocessing. Parameters ---------- - input_file : str - The file object to write the inputs. - output_file : str - The file object to write the outputs. - error_file : str - The file object to write the errors. + seed : int + The seed to set the random number generator. + sim_monitor : _SimMonitor + The simulation monitor object to keep track of the simulations. + mutex : multiprocess.Lock + The mutex to lock access to critical regions. + error_event : multiprocess.Event + Event signaling an error occurred during the simulation. + """ + try: + # Ensure Processes generate different random numbers + self.environment._set_stochastic(seed) + self.rocket._set_stochastic(seed) + self.flight._set_stochastic(seed) + + while sim_monitor.keep_simulating(): + sim_idx = sim_monitor.increment() - 1 + + flight = self.__run_single_simulation() + inputs_json = self.__evaluate_flight_inputs(sim_idx) + outputs_json = self.__evaluate_flight_outputs(flight, sim_idx) + + try: + mutex.acquire() + if error_event.is_set(): + sim_monitor.reprint( + "Simulation Interrupt, files from simulation " + f"{sim_idx} saved." + ) + with open(self.error_file, "a", encoding="utf-8") as f: + f.write(inputs_json) + + 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) + + sim_monitor.print_update_status(sim_idx) + finally: + mutex.release() + + except Exception: # pylint: disable=broad-except + mutex.acquire() + with open(self.error_file, "a", encoding="utf-8") as f: + f.write(inputs_json) + + sim_monitor.reprint(f"Error on iteration {sim_idx}:") + sim_monitor.reprint(traceback.format_exc()) + error_event.set() + mutex.release() + + def __run_single_simulation(self): + """Runs a single simulation and returns the inputs and outputs. Returns ------- - None + Flight + The flight object of the simulation. """ - final_string = ( - f"Completed {self.__iteration_count} iterations. Total CPU time: " - f"{process_time() - self.__start_cpu_time:.1f} s. Total wall time: " - f"{time() - self.__start_time:.1f} s\n" + return Flight( + rocket=self.rocket.create_object(), + environment=self.environment.create_object(), + rail_length=self.flight._randomize_rail_length(), + inclination=self.flight._randomize_inclination(), + heading=self.flight._randomize_heading(), + initial_solution=self.flight.initial_solution, + terminate_on_apogee=self.flight.terminate_on_apogee, ) - self.__reprint(final_string + "Saving results.", flush=True) - - # close files to guarantee saving - self.__close_files(input_file, output_file, error_file) - - # resave the files on self and calculate post simulation attributes - self.input_file = f"{self.filename}.inputs.txt" - self.output_file = f"{self.filename}.outputs.txt" - self.error_file = f"{self.filename}.errors.txt" + def __evaluate_flight_inputs(self, sim_idx): + """Evaluates the inputs of a single flight simulation. - print(f"Results saved to {self._output_file}") + Parameters + ---------- + sim_idx : int + The index of the simulation. - def __export_flight_data( - self, - flight, - inputs_dict, - input_file, - output_file, - ): + Returns + ------- + str + A JSON compatible dictionary with the inputs of the simulation. """ - Exports the flight data to the respective files. + inputs_dict = dict( + item + for d in [ + self.environment.last_rnd_dict, + self.rocket.last_rnd_dict, + self.flight.last_rnd_dict, + ] + for item in d.items() + ) + inputs_dict["index"] = sim_idx + return json.dumps(inputs_dict, cls=RocketPyEncoder) + "\n" + + def __evaluate_flight_outputs(self, flight, sim_idx): + """Evaluates the outputs of a single flight simulation. Parameters ---------- flight : Flight - The Flight object containing the flight data. - inputs_dict : dict - Dictionary containing the inputs used in the simulation. - input_file : str - The file object to write the inputs. - output_file : str - The file object to write the outputs. + The flight object to be evaluated. + sim_idx : int + The index of the simulation. Returns ------- - None + str + A JSON compatible dictionary with the outputs of the simulation. """ - results = { + outputs_dict = { export_item: getattr(flight, export_item) for export_item in self.export_list } + outputs_dict["index"] = sim_idx if self.data_collector is not None: additional_exports = {} @@ -413,13 +543,29 @@ def __export_flight_data( raise ValueError( f"An error was encountered running 'data_collector' callback {key}. " ) from e - results = results | additional_exports + outputs_dict = outputs_dict | additional_exports - input_file.write( - json.dumps(inputs_dict, cls=RocketPyEncoder, **self._export_config) + "\n" + return json.dumps(outputs_dict, cls=RocketPyEncoder) + "\n" + + def __terminate_simulation(self): + """ + Terminates the simulation, closes the files and prints the results. + + Returns + ------- + None + """ + # resave the files on self and calculate post simulation attributes + self.input_file = self._input_file + self.output_file = self._output_file + self.error_file = self._error_file + + _SimMonitor.reprint(f"Results saved to {self._output_file}") + self.input_file.write( + json.dumps(self.inputs_dict, cls=RocketPyEncoder, **self._export_config) + "\n" ) - output_file.write( - json.dumps(results, cls=RocketPyEncoder, **self._export_config) + "\n" + self.output_file.write( + json.dumps(self.results, cls=RocketPyEncoder, **self._export_config) + "\n" ) def __check_export_list(self, export_list): @@ -557,35 +703,6 @@ def _check_data_collector(self, data_collector): "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 - previous message with the new one, deleting the extra characters from - the previous message. - - Parameters - ---------- - msg : str - Message to be printed. - end : str, optional - String appended after the message. Default is a new line. - flush : bool, optional - If True, the output is flushed. Default is False. - - Returns - ------- - None - """ - len_msg = len(msg) - if len_msg < self._last_print_len: - msg += " " * (self._last_print_len - len_msg) - else: - self._last_print_len = len_msg - - print(msg, end=end, flush=flush) - - # Properties and setters - @property def input_file(self): """String representing the filepath of the input file""" @@ -782,16 +899,16 @@ def import_outputs(self, filename=None): file without the need to run simulations. You can use previously saved files to process analyze the results or to continue a simulation. """ - filepath = filename if filename else self.filename + filepath = filename if filename else self.filename.with_suffix(".outputs.txt") try: - with open(f"{filepath}.outputs.txt", "r+", encoding="utf-8"): - self.output_file = f"{filepath}.outputs.txt" - except FileNotFoundError: with open(filepath, "r+", encoding="utf-8"): self.output_file = filepath + except FileNotFoundError: + with open(filepath, "w+", encoding="utf-8"): + self.output_file = filepath - print( + _SimMonitor.reprint( f"A total of {self.num_of_loaded_sims} simulations results were " f"loaded from the following output file: {self.output_file}\n" ) @@ -810,16 +927,16 @@ def import_inputs(self, filename=None): ------- None """ - filepath = filename if filename else self.filename + filepath = filename if filename else self.filename.with_suffix(".inputs.txt") try: - with open(f"{filepath}.inputs.txt", "r+", encoding="utf-8"): - self.input_file = f"{filepath}.inputs.txt" - except FileNotFoundError: with open(filepath, "r+", encoding="utf-8"): self.input_file = filepath + except FileNotFoundError: + with open(filepath, "w+", encoding="utf-8"): + self.input_file = filepath - print(f"The following input file was imported: {self.input_file}") + _SimMonitor.reprint(f"The following input file was imported: {self.input_file}") def import_errors(self, filename=None): """ @@ -835,15 +952,16 @@ def import_errors(self, filename=None): ------- None """ - filepath = filename if filename else self.filename + filepath = filename if filename else self.filename.with_suffix(".errors.txt") try: - with open(f"{filepath}.errors.txt", "r+", encoding="utf-8"): - self.error_file = f"{filepath}.errors.txt" - except FileNotFoundError: with open(filepath, "r+", encoding="utf-8"): self.error_file = filepath - print(f"The following error file was imported: {self.error_file}") + except FileNotFoundError: + with open(filepath, "w+", encoding="utf-8"): + self.error_file = filepath + + _SimMonitor.reprint(f"The following error file was imported: {self.error_file}") def import_results(self, filename=None): """ @@ -852,18 +970,16 @@ def import_results(self, filename=None): Parameters ---------- filename : str, optional - Name or directory path to the file to be imported. If none, + Name or directory path to the file to be imported. If ``None``, self.filename will be used. Returns ------- None """ - filepath = filename if filename else self.filename - - self.import_outputs(filename=filepath) - self.import_inputs(filename=filepath) - self.import_errors(filename=filepath) + self.import_outputs(filename=filename) + self.import_inputs(filename=filename) + self.import_errors(filename=filename) # Export methods @@ -1021,3 +1137,128 @@ def all_info(self): self.info() self.plots.ellipses() self.plots.all() + + +def _import_multiprocess(): + """Import the necessary modules and submodules for the + multiprocess library. + + Returns + ------- + tuple + Tuple containing the imported modules. + """ + multiprocess = import_optional_dependency("multiprocess") + managers = import_optional_dependency("multiprocess.managers") + + return multiprocess, managers + + +def _create_multiprocess_manager(multiprocess, managers): + """Creates a manager for the multiprocess control of the + Monte Carlo simulation. + + Parameters + ---------- + multiprocess : module + Multiprocess module. + managers : module + Managing submodules of the multiprocess module. + + Returns + ------- + MonteCarloManager + Subclass of BaseManager with the necessary classes registered. + """ + + class MonteCarloManager(managers.BaseManager): + """Custom manager for shared objects in the Monte Carlo simulation.""" + + def __init__(self): + super().__init__() + self.register('Lock', multiprocess.Lock) + self.register('Queue', multiprocess.Queue) + self.register('Event', multiprocess.Event) + self.register('_SimMonitor', _SimMonitor) + + return MonteCarloManager() + + +class _SimMonitor: + """Class to monitor the simulation progress and display the status.""" + + _last_print_len = 0 + + def __init__(self, initial_count, n_simulations, start_time): + self.initial_count = initial_count + self.count = initial_count + self.n_simulations = n_simulations + self.start_time = start_time + + def keep_simulating(self): + return self.count < self.n_simulations + + def increment(self): + self.count += 1 + return self.count + + def print_update_status(self, sim_idx): + """Prints a message on the same line as the previous one and replaces + the previous message with the new one, deleting the extra characters + from the previous message. + + Parameters + ---------- + sim_idx : int + Index of the current simulation. + + Returns + ------- + None + """ + average_time = (time() - self.start_time) / (self.count - self.initial_count) + estimated_time = int((self.n_simulations - self.count) * average_time) + + msg = f"Current iteration: {sim_idx:06d}" + msg += f" | Average Time per Iteration: {average_time:.3f} s" + msg += f" | Estimated time left: {estimated_time} s" + + _SimMonitor.reprint(msg, end="\r", flush=True) + + def print_final_status(self): + """Prints the final status of the simulation.""" + print() + msg = f"Completed {self.count - self.initial_count} iterations." + msg += f" In total, {self.count} simulations are exported.\n" + msg += f"Total wall time: {time() - self.start_time:.1f} s" + + _SimMonitor.reprint(msg, end="\n", flush=True) + + @staticmethod + def reprint(msg, end="\n", flush=True): + """ + Prints a message on the same line as the previous one and replaces the + previous message with the new one, deleting the extra characters from + the previous message. + + Parameters + ---------- + msg : str + Message to be printed. + end : str, optional + String appended after the message. Default is a new line. + flush : bool, optional + If True, the output is flushed. Default is True. + + Returns + ------- + None + """ + padding = "" + + if len(msg) < _SimMonitor._last_print_len: + padding = " " * (_SimMonitor._last_print_len - len(msg)) + + print(msg + padding, end=end, flush=flush) + + _SimMonitor._last_print_len = len(msg) diff --git a/rocketpy/stochastic/stochastic_model.py b/rocketpy/stochastic/stochastic_model.py index 02341a11d..e8eabf833 100644 --- a/rocketpy/stochastic/stochastic_model.py +++ b/rocketpy/stochastic/stochastic_model.py @@ -40,7 +40,7 @@ class StochasticModel: "ensemble_member", ] - def __init__(self, obj, **kwargs): + def __init__(self, obj, seed=None, **kwargs): """ Initialize the StochasticModel class with validated input arguments. @@ -48,6 +48,9 @@ def __init__(self, obj, **kwargs): ---------- obj : object The main object of the class. + seed : int, optional + Seed for the random number generator. The default is None so that + a new ``numpy.random.Generator`` object is created. **kwargs : dict Dictionary of input arguments for the class. Valid argument types include tuples, lists, ints, floats, or None. Arguments will be @@ -63,9 +66,24 @@ def __init__(self, obj, **kwargs): self.obj = obj self.last_rnd_dict = {} + self.__stochastic_dict = kwargs + self._set_stochastic(seed) + + def _set_stochastic(self, seed=None): + """Set the stochastic attributes from the input dictionary. + This method is useful to reset or reseed the attributes of the instance. + + Parameters + ---------- + seed : int, optional + Seed for the random number generator. + """ + self.__random_number_generator = np.random.default_rng(seed) + self.last_rnd_dict = {} # TODO: This code block is too complex. Refactor it. - for input_name, input_value in kwargs.items(): + # TODO: Resetting a instance should not require re-validation. + for input_name, input_value in self.__stochastic_dict.items(): if input_name not in self.exception_list: attr_value = None if input_value is not None: @@ -163,14 +181,18 @@ def _validate_tuple_length_two( # is the standard deviation, and the second item is the distribution # function. In this case, the nominal value will be taken from the # object passed. - dist_func = get_distribution(input_value[1]) + dist_func = get_distribution(input_value[1], self.__random_number_generator) return (getattr(self.obj, input_name), input_value[0], dist_func) else: # if second item is an int or float, then it is assumed that the # first item is the nominal value and the second item is the # standard deviation. The distribution function will be set to # "normal". - return (input_value[0], input_value[1], get_distribution("normal")) + return ( + input_value[0], + input_value[1], + get_distribution("normal", self.__random_number_generator), + ) def _validate_tuple_length_three( self, input_name, input_value, getattr=getattr @@ -206,7 +228,7 @@ def _validate_tuple_length_three( f"'{input_name}': Third item of tuple must be a string containing the " "name of a valid numpy.random distribution function." ) - dist_func = get_distribution(input_value[2]) + dist_func = get_distribution(input_value[2], self.__random_number_generator) return (input_value[0], input_value[1], dist_func) def _validate_list( @@ -265,7 +287,7 @@ def _validate_scalar( return ( getattr(self.obj, input_name), input_value, - get_distribution("normal"), + get_distribution("normal", self.__random_number_generator), ) def _validate_factors(self, input_name, input_value): @@ -330,13 +352,19 @@ def _validate_tuple_factor(self, input_name, factor_tuple): ) if len(factor_tuple) == 2: - return (factor_tuple[0], factor_tuple[1], get_distribution("normal")) + return ( + factor_tuple[0], + factor_tuple[1], + get_distribution("normal", self.__random_number_generator), + ) elif len(factor_tuple) == 3: assert isinstance(factor_tuple[2], str), ( f"'{input_name}`: Third item of tuple must be a string containing " "the name of a valid numpy.random distribution function" ) - dist_func = get_distribution(factor_tuple[2]) + dist_func = get_distribution( + factor_tuple[2], self.__random_number_generator + ) return (factor_tuple[0], factor_tuple[1], dist_func) def _validate_list_factor(self, input_name, factor_list): diff --git a/rocketpy/stochastic/stochastic_rocket.py b/rocketpy/stochastic/stochastic_rocket.py index 01e6c66a5..2df822ea7 100644 --- a/rocketpy/stochastic/stochastic_rocket.py +++ b/rocketpy/stochastic/stochastic_rocket.py @@ -144,6 +144,11 @@ def __init__( # TODO: mention that these factors are validated differently self._validate_1d_array_like("power_off_drag", power_off_drag) self._validate_1d_array_like("power_on_drag", power_on_drag) + self.motors = Components() + self.aerodynamic_surfaces = Components() + self.rail_buttons = Components() + self.parachutes = [] + self.__components_map = {} super().__init__( obj=rocket, radius=radius, @@ -161,10 +166,53 @@ def __init__( center_of_mass_without_motor=center_of_mass_without_motor, coordinate_system_orientation=None, ) - self.motors = Components() - self.aerodynamic_surfaces = Components() - self.rail_buttons = Components() - self.parachutes = [] + + def _set_stochastic(self, seed=None): + """Set the stochastic attributes for Components, positions and + inputs. + + Parameters + ---------- + seed : int, optional + Seed for the random number generator. + """ + super()._set_stochastic(seed) + self.aerodynamic_surfaces = self.__reset_components( + self.aerodynamic_surfaces, seed + ) + self.motors = self.__reset_components(self.motors, seed) + self.rail_buttons = self.__reset_components(self.rail_buttons, seed) + for parachute in self.parachutes: + parachute._set_stochastic(seed) + + def __reset_components(self, components, seed): + """Creates a new Components whose stochastic structures + and their positions are reset. + + Parameters + ---------- + components : Components + The components which contains the stochastic structure that + will be used to create the new components. + seed : int, optional + Seed for the random number generator. + + Returns + ------- + new_components : Components + A components whose stochastic structure and position match the + input component but are reset. Ideally, it should replace the + input component. + """ + new_components = Components() + for stochastic_obj, _ in components: + stochastic_obj_position_info = self.__components_map[stochastic_obj] + stochastic_obj._set_stochastic(seed) + new_components.add( + stochastic_obj, + self._validate_position(stochastic_obj, stochastic_obj_position_info), + ) + return new_components def add_motor(self, motor, position=None): """Adds a stochastic motor to the stochastic rocket. If a motor is @@ -194,6 +242,7 @@ def add_motor(self, motor, position=None): motor = StochasticSolidMotor(solid_motor=motor) elif isinstance(motor, GenericMotor): motor = StochasticGenericMotor(generic_motor=motor) + self.__components_map[motor] = position self.motors.add(motor, self._validate_position(motor, position)) def _add_surfaces(self, surfaces, positions, type_, stochastic_type, error_message): @@ -220,6 +269,7 @@ def _add_surfaces(self, surfaces, positions, type_, stochastic_type, error_messa raise AssertionError(error_message) if isinstance(surfaces, type_): surfaces = stochastic_type(component=surfaces) + self.__components_map[surfaces] = positions self.aerodynamic_surfaces.add( surfaces, self._validate_position(surfaces, positions) ) @@ -333,6 +383,7 @@ def set_rail_buttons( ) if isinstance(rail_buttons, RailButtons): rail_buttons = StochasticRailButtons(rail_buttons=rail_buttons) + self.__components_map[rail_buttons] = lower_button_position self.rail_buttons.add( rail_buttons, self._validate_position(rail_buttons, lower_button_position) ) @@ -357,7 +408,6 @@ def _validate_position(self, validated_object, position): ValueError If the position argument does not conform to the specified formats. """ - if isinstance(position, tuple): return self._validate_tuple( "position", diff --git a/rocketpy/tools.py b/rocketpy/tools.py index 9962e9442..91428d50b 100644 --- a/rocketpy/tools.py +++ b/rocketpy/tools.py @@ -235,7 +235,7 @@ def bilinear_interpolation(x, y, x1, x2, y1, y2, z11, z12, z21, z22): ) / ((x2 - x1) * (y2 - y1)) -def get_distribution(distribution_function_name): +def get_distribution(distribution_function_name, random_number_generator=None): """Sets the distribution function to be used in the monte carlo analysis. Parameters @@ -243,24 +243,31 @@ def get_distribution(distribution_function_name): distribution_function_name : string The type of distribution to be used in the analysis. It can be 'uniform', 'normal', 'lognormal', etc. + random_number_generator : np.random.Generator, optional + The random number generator to be used. If None, the default generator + ``numpy.random.default_rng`` is used. Returns ------- np.random distribution function The distribution function to be used in the analysis. """ + if random_number_generator is None: + random_number_generator = np.random.default_rng() + + # Dictionary mapping distribution names to RNG methods distributions = { - "normal": np.random.normal, - "binomial": np.random.binomial, - "chisquare": np.random.chisquare, - "exponential": np.random.exponential, - "gamma": np.random.gamma, - "gumbel": np.random.gumbel, - "laplace": np.random.laplace, - "logistic": np.random.logistic, - "poisson": np.random.poisson, - "uniform": np.random.uniform, - "wald": np.random.wald, + "normal": random_number_generator.normal, + "binomial": random_number_generator.binomial, + "chisquare": random_number_generator.chisquare, + "exponential": random_number_generator.exponential, + "gamma": random_number_generator.gamma, + "gumbel": random_number_generator.gumbel, + "laplace": random_number_generator.laplace, + "logistic": random_number_generator.logistic, + "poisson": random_number_generator.poisson, + "uniform": random_number_generator.uniform, + "wald": random_number_generator.wald, } try: return distributions[distribution_function_name] diff --git a/tests/integration/test_flight.py b/tests/integration/test_flight.py index 3efc0c285..8e5d518e5 100644 --- a/tests/integration/test_flight.py +++ b/tests/integration/test_flight.py @@ -69,6 +69,42 @@ def test_all_info_different_solvers( assert test_flight.all_info() is None +@pytest.mark.slow +@patch("matplotlib.pyplot.show") +@pytest.mark.parametrize("solver_method", ["RK45", "DOP853", "Radau", "BDF"]) +# RK23 is unstable and requires a very low tolerance to work +# pylint: disable=unused-argument +def test_all_info_different_solvers( + mock_show, calisto_robust, example_spaceport_env, solver_method +): + """Test that the flight class is working as intended with different solver + methods. This basically calls the all_info() method and checks if it returns + None. It is not testing if the values are correct, but whether the method is + working without errors. + + Parameters + ---------- + mock_show : unittest.mock.MagicMock + Mock object to replace matplotlib.pyplot.show + calisto_robust : rocketpy.Rocket + Rocket to be simulated. See the conftest.py file for more info. + example_spaceport_env : rocketpy.Environment + Environment to be simulated. See the conftest.py file for more info. + solver_method : str + The solver method to be used in the simulation. + """ + test_flight = Flight( + environment=example_spaceport_env, + rocket=calisto_robust, + rail_length=5.2, + inclination=85, + heading=0, + terminate_on_apogee=False, + ode_solver=solver_method, + ) + assert test_flight.all_info() is None + + class TestExportData: """Tests the export_data method of the Flight class.""" diff --git a/tests/integration/test_monte_carlo.py b/tests/integration/test_monte_carlo.py index 51d8bfae9..48e47a6e2 100644 --- a/tests/integration/test_monte_carlo.py +++ b/tests/integration/test_monte_carlo.py @@ -10,7 +10,8 @@ @pytest.mark.slow -def test_monte_carlo_simulate(monte_carlo_calisto): +@pytest.mark.parametrize("parallel", [False, True]) +def test_monte_carlo_simulate(monte_carlo_calisto, parallel): """Tests the simulate method of the MonteCarlo class. Parameters @@ -19,20 +20,22 @@ def test_monte_carlo_simulate(monte_carlo_calisto): The MonteCarlo object, this is a pytest fixture. """ # NOTE: this is really slow, it runs 10 flight simulations - monte_carlo_calisto.simulate(number_of_simulations=10, append=False) + monte_carlo_calisto.simulate( + number_of_simulations=10, append=False, parallel=parallel + ) assert monte_carlo_calisto.num_of_loaded_sims == 10 assert monte_carlo_calisto.number_of_simulations == 10 - assert monte_carlo_calisto.filename == "monte_carlo_test" - assert monte_carlo_calisto.error_file == "monte_carlo_test.errors.txt" - assert monte_carlo_calisto.output_file == "monte_carlo_test.outputs.txt" + assert str(monte_carlo_calisto.filename.name) == "monte_carlo_test" + assert str(monte_carlo_calisto.error_file.name) == "monte_carlo_test.errors.txt" + assert str(monte_carlo_calisto.output_file.name) == "monte_carlo_test.outputs.txt" assert np.isclose( - monte_carlo_calisto.processed_results["apogee"][0], 4711, rtol=0.15 + monte_carlo_calisto.processed_results["apogee"][0], 4711, rtol=0.2 ) assert np.isclose( monte_carlo_calisto.processed_results["impact_velocity"][0], -5.234, - rtol=0.15, + rtol=0.2, ) os.remove("monte_carlo_test.errors.txt") os.remove("monte_carlo_test.outputs.txt")