From d01c7137a7af698dc32db14506ab1238208adde8 Mon Sep 17 00:00:00 2001 From: Carl Recine Date: Tue, 30 Jan 2024 11:01:28 -0800 Subject: [PATCH 1/5] Added a CLI for users to download models --- aviary/interface/cmd_entry_points.py | 32 +----- aviary/interface/download_models.py | 100 ++++++++++++++++++ aviary/interface/graphical_input.py | 27 +++++ aviary/interface/test/test_download_models.py | 68 ++++++++++++ 4 files changed, 199 insertions(+), 28 deletions(-) create mode 100644 aviary/interface/download_models.py create mode 100644 aviary/interface/test/test_download_models.py diff --git a/aviary/interface/cmd_entry_points.py b/aviary/interface/cmd_entry_points.py index 56dff8119..117e6dd7f 100644 --- a/aviary/interface/cmd_entry_points.py +++ b/aviary/interface/cmd_entry_points.py @@ -6,34 +6,8 @@ from aviary.interface.methods_for_level1 import _exec_level1, _setup_level1_parser from aviary.utils.Fortran_to_Aviary import _exec_F2A, _setup_F2A_parser from aviary.visualization.dashboard import _dashboard_setup_parser, _dashboard_cmd -from aviary.interface.graphical_input import IntegratedPlottingApp - - -def _setup_flight_profile_parser(parser): - """ - Set up the command line options for the Flight Profile plotting tool. - - Parameters - ---------- - parser : argparse.ArgumentParser - The parser instance. - """ - pass - - -def _exec_flight_profile(options, user_args): - """ - Run the Flight Profile plotting tool. - - Parameters - ---------- - options : argparse.Namespace - Command line options. - user_args : list of str - Args to be passed to the user script. - """ - app = IntegratedPlottingApp() - app.mainloop() +from aviary.interface.graphical_input import _exec_flight_profile, _setup_flight_profile_parser +from aviary.interface.download_models import _exec_hanger, _setup_hanger_parser def _load_and_exec(script_name, user_args): @@ -74,6 +48,8 @@ def _load_and_exec(script_name, user_args): "Allows users to draw a mission profile for use in Aviary."), 'dashboard': (_dashboard_setup_parser, _dashboard_cmd, "Run the Dashboard tool"), + 'hanger': (_setup_hanger_parser, _exec_hanger, + "Allows users that pip installed Aviary to download models from the Aviary Hanger"), } diff --git a/aviary/interface/download_models.py b/aviary/interface/download_models.py new file mode 100644 index 000000000..cdc9594c6 --- /dev/null +++ b/aviary/interface/download_models.py @@ -0,0 +1,100 @@ +import os +from pathlib import Path +import argparse +import pkg_resources +import shutil + + +def aviary_resource(resource_name: str) -> str: + return pkg_resources.resource_filename("aviary", resource_name) + + +def get_model(file_name: str, verbose=False) -> Path: + ''' + This function attempts to find the path to a file or folder in aviary/models + If the path cannot be found in any of the locations, a FileNotFoundError is raised. + + Parameters + ---------- + path : str or Path + The input path, either as a string or a Path object. + + Returns + ------- + aviary_path + The absolute path to the file. + + Raises + ------ + FileNotFoundError + If the path is not found. + ''' + + # Get the path to Aviary's models + path = Path('models', file_name) + aviary_path = Path(aviary_resource(str(path))) + + # If the file name was provided without a path, check in the subfolders + if not aviary_path.exists(): + sub_dirs = [x[0] for x in os.walk(aviary_resource('models'))] + for sub_dir in sub_dirs: + temp_path = Path(sub_dir, file_name) + if temp_path.exists(): + # only return the first matching file + aviary_path = temp_path + continue + + # If the path still doesn't exist, raise an error. + if not aviary_path.exists(): + raise FileNotFoundError( + f"File or Folder not found in Aviary's hanger" + ) + if verbose: + print('found', aviary_path, '\n') + return aviary_path + + +def save_file(aviary_path: Path, outdir: Path, verbose=False) -> Path: + ''' + Saves the file or folder specified into the output directory + ''' + outdir.mkdir(parents=True, exist_ok=True) + if aviary_path.is_dir(): + if verbose: + print(aviary_path, 'is a directory, getting all files') + outdir = outdir.joinpath(aviary_path.stem) + outdir.mkdir(exist_ok=True) + for file in next(os.walk(aviary_path))[-1]: + if verbose: + print('copying', str(aviary_path / file), 'to', str(outdir / file)) + shutil.copy2(aviary_path / file, outdir) + else: + if verbose: + print('copying', str(aviary_path), 'to', str(outdir / aviary_path.name)) + shutil.copy2(aviary_path, outdir) + return outdir + + +def _setup_hanger_parser(parser: argparse.ArgumentParser): + def_outdir = os.path.join(os.getcwd(), "aviary_models") + parser.add_argument( + 'input_decks', metavar='indecks', type=str, nargs='+', help='Name of vehicle input deck file' + ) + parser.add_argument( + "-o", "--outdir", default=def_outdir, help="Directory to write outputs" + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable verbose outputs", + ) + + +def _exec_hanger(args, user_args): + # check if args.input_deck is a list, if so, use the first element + input_decks = [] + for input_deck in args.input_decks: + input_decks.append(get_model(input_deck, args.verbose)) + + for input_deck in input_decks: + save_file(input_deck, Path(args.outdir), args.verbose) diff --git a/aviary/interface/graphical_input.py b/aviary/interface/graphical_input.py index eae7cea41..baeda5cf5 100644 --- a/aviary/interface/graphical_input.py +++ b/aviary/interface/graphical_input.py @@ -559,6 +559,33 @@ def update_plot(self, ax, data, label, color): ax.figure.canvas.draw() +def _setup_flight_profile_parser(parser): + """ + Set up the command line options for the Flight Profile plotting tool. + + Parameters + ---------- + parser : argparse.ArgumentParser + The parser instance. + """ + pass + + +def _exec_flight_profile(options, user_args): + """ + Run the Flight Profile plotting tool. + + Parameters + ---------- + options : argparse.Namespace + Command line options. + user_args : list of str + Args to be passed to the user script. + """ + app = IntegratedPlottingApp() + app.mainloop() + + if __name__ == "__main__": app = IntegratedPlottingApp() app.mainloop() diff --git a/aviary/interface/test/test_download_models.py b/aviary/interface/test/test_download_models.py new file mode 100644 index 000000000..1627243b5 --- /dev/null +++ b/aviary/interface/test/test_download_models.py @@ -0,0 +1,68 @@ +import subprocess +import unittest +from pathlib import Path +import shutil + +import pkg_resources +from openmdao.utils.testing_utils import use_tempdirs +from aviary.interface.download_models import get_model, save_file + + +@use_tempdirs +class CommandEntryPointsTestCases(unittest.TestCase): + + def run_and_test_hanger(self, filenames, out_dir=''): + # this only tests that a given command line tool returns a 0 return code. It doesn't + # check the expected output at all. The underlying functions that implement the + # commands should be tested seperately. + if isinstance(filenames, str): + filenames = [filenames] + cmd = ['aviary', 'hanger'] + filenames + + if out_dir: + cmd += ['-o', out_dir] + out_dir = Path(out_dir) + else: + out_dir = Path.cwd() / 'aviary_models' + + try: + output = subprocess.check_output(cmd) + for filename in filenames: + path = out_dir / filename.split('/')[-1] + self.assertTrue(path.exists()) + except subprocess.CalledProcessError as err: + self.fail(f"Command '{cmd}' failed. Return code: {err.returncode}") + + def test_single_file_without_path(self): + filename = 'turbofan_22k.deck' + self.run_and_test_hanger(filename) + + def test_single_file_with_path(self): + filename = 'engines/turbofan_22k.deck' + self.run_and_test_hanger(filename) + + def test_multiple_files(self): + filenames = ['small_single_aisle_GwGm.dat', 'small_single_aisle_GwGm.csv'] + self.run_and_test_hanger(filenames) + + def test_folder(self): + filename = 'engines' + self.run_and_test_hanger(filename) + + def test_single_file_custom_outdir(self): + filename = 'small_single_aisle_GwGm.csv' + out_dir = '~/test_hanger' + self.run_and_test_hanger(filename, out_dir) + shutil.rmtree(out_dir) + + def test_expected_path(self): + filename = f'test_aircraft/converter_configuration_test_data_GwGm.dat' + aviary_path = get_model('converter_configuration_test_data_GwGm.dat') + + expected_path = pkg_resources.resource_filename('aviary', + 'models/test_aircraft/converter_configuration_test_data_GwGm.dat') + self.assertTrue(str(aviary_path) == expected_path) + + +if __name__ == "__main__": + unittest.main() From 47ad90991bf923f8a0d35d96a006bb280d561d6e Mon Sep 17 00:00:00 2001 From: Carl Recine Date: Tue, 30 Jan 2024 11:15:53 -0800 Subject: [PATCH 2/5] Added docs --- aviary/docs/user_guide/aviary_commands.ipynb | 43 ++++++++++++++++++++ aviary/interface/download_models.py | 4 +- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/aviary/docs/user_guide/aviary_commands.ipynb b/aviary/docs/user_guide/aviary_commands.ipynb index 87d179dd8..ce025100f 100644 --- a/aviary/docs/user_guide/aviary_commands.ipynb +++ b/aviary/docs/user_guide/aviary_commands.ipynb @@ -172,6 +172,49 @@ "source": [ "!aviary fortran_to_aviary -h" ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "(aviary-hanger-command)=\n", + "### aviary hanger\n", + "\n", + "The `aviary hanger` command will download files and folders from the Aviary hanger.\n", + "This is useful for users that have pip installed Aviary, but want to experiment with the included examples.\n", + "\n", + "The only required input is the name of an input deck.\n", + "This can be specified as the name of the file, the path from aviary/models, the name of a folder in aviary/models. Multiple files and folders can be downloaded at once.\n", + "Optionally, the output directory can be specified; if it is not, the files will be downloaded into the current working directory in a folder caller aviary_models.\n", + "If specified, the output directory will be created as needed.\n", + "\n", + "Example usage:\n", + "```\n", + "`aviary hanger engines` Download all files in the engines folder\n", + "`aviary hanger turbofan_22k.txt` Download the 22k turbofan deck\n", + "`aviary hanger N3CC/N3CC_data.py` Download the N3CC data\n", + "`aviary hanger small_single_aisle_GwGm.dat small_single_aisle_GwGm.csv` Download the Fortran and Aviary input decks for the small single aisle\n", + "`aviary hanger turbofan_22k.txt ~/example_files` Download the engine model into ~/example_files\n", + "```\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```\n", + "aviary hanger -h\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!aviary hanger -h" + ] } ], "metadata": { diff --git a/aviary/interface/download_models.py b/aviary/interface/download_models.py index cdc9594c6..6debb219e 100644 --- a/aviary/interface/download_models.py +++ b/aviary/interface/download_models.py @@ -56,7 +56,7 @@ def get_model(file_name: str, verbose=False) -> Path: def save_file(aviary_path: Path, outdir: Path, verbose=False) -> Path: ''' - Saves the file or folder specified into the output directory + Saves the file or folder specified into the output directory, creating directories as needed. ''' outdir.mkdir(parents=True, exist_ok=True) if aviary_path.is_dir(): @@ -81,7 +81,7 @@ def _setup_hanger_parser(parser: argparse.ArgumentParser): 'input_decks', metavar='indecks', type=str, nargs='+', help='Name of vehicle input deck file' ) parser.add_argument( - "-o", "--outdir", default=def_outdir, help="Directory to write outputs" + "-o", "--outdir", default=def_outdir, help="Directory to write outputs. Defaults to aviary_models in the current directory." ) parser.add_argument( "-v", "--verbose", From db8639266cd70a382c244442ebc82328cfd5ad3d Mon Sep 17 00:00:00 2001 From: Carl Recine Date: Tue, 30 Jan 2024 13:11:51 -0800 Subject: [PATCH 3/5] Incorporating Feedback --- aviary/docs/user_guide/aviary_commands.ipynb | 22 +++++++++---------- aviary/interface/download_models.py | 8 +++---- aviary/interface/test/test_download_models.py | 20 ++++++++--------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/aviary/docs/user_guide/aviary_commands.ipynb b/aviary/docs/user_guide/aviary_commands.ipynb index ce025100f..d5adabdb4 100644 --- a/aviary/docs/user_guide/aviary_commands.ipynb +++ b/aviary/docs/user_guide/aviary_commands.ipynb @@ -177,24 +177,24 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "(aviary-hanger-command)=\n", - "### aviary hanger\n", + "(aviary-hangar-command)=\n", + "### aviary hangar\n", "\n", - "The `aviary hanger` command will download files and folders from the Aviary hanger.\n", + "The `aviary hangar` command will download files and folders from the Aviary hangar.\n", "This is useful for users that have pip installed Aviary, but want to experiment with the included examples.\n", "\n", "The only required input is the name of an input deck.\n", - "This can be specified as the name of the file, the path from aviary/models, the name of a folder in aviary/models. Multiple files and folders can be downloaded at once.\n", + "This can be specified as the name of the file, the path from [aviary/models](https://github.com/OpenMDAO/Aviary/tree/main/aviary/models), the name of a folder in aviary/models. Multiple files and folders can be downloaded at once.\n", "Optionally, the output directory can be specified; if it is not, the files will be downloaded into the current working directory in a folder caller aviary_models.\n", "If specified, the output directory will be created as needed.\n", "\n", "Example usage:\n", "```\n", - "`aviary hanger engines` Download all files in the engines folder\n", - "`aviary hanger turbofan_22k.txt` Download the 22k turbofan deck\n", - "`aviary hanger N3CC/N3CC_data.py` Download the N3CC data\n", - "`aviary hanger small_single_aisle_GwGm.dat small_single_aisle_GwGm.csv` Download the Fortran and Aviary input decks for the small single aisle\n", - "`aviary hanger turbofan_22k.txt ~/example_files` Download the engine model into ~/example_files\n", + "`aviary hangar engines` Download all files in the engines folder\n", + "`aviary hangar turbofan_22k.txt` Download the 22k turbofan deck\n", + "`aviary hangar N3CC/N3CC_data.py` Download the N3CC data\n", + "`aviary hangar small_single_aisle_GwGm.dat small_single_aisle_GwGm.csv` Download the Fortran and Aviary input decks for the small single aisle\n", + "`aviary hangar turbofan_22k.txt ~/example_files` Download the engine model into ~/example_files\n", "```\n" ] }, @@ -203,7 +203,7 @@ "metadata": {}, "source": [ "```\n", - "aviary hanger -h\n", + "aviary hangar -h\n", "```" ] }, @@ -213,7 +213,7 @@ "metadata": {}, "outputs": [], "source": [ - "!aviary hanger -h" + "!aviary hangar -h" ] } ], diff --git a/aviary/interface/download_models.py b/aviary/interface/download_models.py index 6debb219e..e58c3dea1 100644 --- a/aviary/interface/download_models.py +++ b/aviary/interface/download_models.py @@ -47,7 +47,7 @@ def get_model(file_name: str, verbose=False) -> Path: # If the path still doesn't exist, raise an error. if not aviary_path.exists(): raise FileNotFoundError( - f"File or Folder not found in Aviary's hanger" + f"File or Folder not found in Aviary's hangar" ) if verbose: print('found', aviary_path, '\n') @@ -75,10 +75,10 @@ def save_file(aviary_path: Path, outdir: Path, verbose=False) -> Path: return outdir -def _setup_hanger_parser(parser: argparse.ArgumentParser): +def _setup_hangar_parser(parser: argparse.ArgumentParser): def_outdir = os.path.join(os.getcwd(), "aviary_models") parser.add_argument( - 'input_decks', metavar='indecks', type=str, nargs='+', help='Name of vehicle input deck file' + 'input_decks', metavar='indecks', type=str, nargs='+', help='Name of file or folder to download from Aviary/models' ) parser.add_argument( "-o", "--outdir", default=def_outdir, help="Directory to write outputs. Defaults to aviary_models in the current directory." @@ -90,7 +90,7 @@ def _setup_hanger_parser(parser: argparse.ArgumentParser): ) -def _exec_hanger(args, user_args): +def _exec_hangar(args, user_args): # check if args.input_deck is a list, if so, use the first element input_decks = [] for input_deck in args.input_decks: diff --git a/aviary/interface/test/test_download_models.py b/aviary/interface/test/test_download_models.py index 1627243b5..c8fa4d036 100644 --- a/aviary/interface/test/test_download_models.py +++ b/aviary/interface/test/test_download_models.py @@ -11,13 +11,11 @@ @use_tempdirs class CommandEntryPointsTestCases(unittest.TestCase): - def run_and_test_hanger(self, filenames, out_dir=''): - # this only tests that a given command line tool returns a 0 return code. It doesn't - # check the expected output at all. The underlying functions that implement the - # commands should be tested seperately. + def run_and_test_hangar(self, filenames, out_dir=''): + # tests that the commands return an exit code of 0 and that the files are generated if isinstance(filenames, str): filenames = [filenames] - cmd = ['aviary', 'hanger'] + filenames + cmd = ['aviary', 'hangar'] + filenames if out_dir: cmd += ['-o', out_dir] @@ -35,24 +33,24 @@ def run_and_test_hanger(self, filenames, out_dir=''): def test_single_file_without_path(self): filename = 'turbofan_22k.deck' - self.run_and_test_hanger(filename) + self.run_and_test_hangar(filename) def test_single_file_with_path(self): filename = 'engines/turbofan_22k.deck' - self.run_and_test_hanger(filename) + self.run_and_test_hangar(filename) def test_multiple_files(self): filenames = ['small_single_aisle_GwGm.dat', 'small_single_aisle_GwGm.csv'] - self.run_and_test_hanger(filenames) + self.run_and_test_hangar(filenames) def test_folder(self): filename = 'engines' - self.run_and_test_hanger(filename) + self.run_and_test_hangar(filename) def test_single_file_custom_outdir(self): filename = 'small_single_aisle_GwGm.csv' - out_dir = '~/test_hanger' - self.run_and_test_hanger(filename, out_dir) + out_dir = '~/test_hangar' + self.run_and_test_hangar(filename, out_dir) shutil.rmtree(out_dir) def test_expected_path(self): From e200219bf2624096815263ac166d27b504a16e0d Mon Sep 17 00:00:00 2001 From: Carl Recine Date: Tue, 30 Jan 2024 13:20:09 -0800 Subject: [PATCH 4/5] fixed typo --- aviary/interface/cmd_entry_points.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aviary/interface/cmd_entry_points.py b/aviary/interface/cmd_entry_points.py index 117e6dd7f..3652a4d60 100644 --- a/aviary/interface/cmd_entry_points.py +++ b/aviary/interface/cmd_entry_points.py @@ -7,7 +7,7 @@ from aviary.utils.Fortran_to_Aviary import _exec_F2A, _setup_F2A_parser from aviary.visualization.dashboard import _dashboard_setup_parser, _dashboard_cmd from aviary.interface.graphical_input import _exec_flight_profile, _setup_flight_profile_parser -from aviary.interface.download_models import _exec_hanger, _setup_hanger_parser +from aviary.interface.download_models import _exec_hangar, _setup_hangar_parser def _load_and_exec(script_name, user_args): @@ -48,8 +48,8 @@ def _load_and_exec(script_name, user_args): "Allows users to draw a mission profile for use in Aviary."), 'dashboard': (_dashboard_setup_parser, _dashboard_cmd, "Run the Dashboard tool"), - 'hanger': (_setup_hanger_parser, _exec_hanger, - "Allows users that pip installed Aviary to download models from the Aviary Hanger"), + 'hangar': (_setup_hangar_parser, _exec_hangar, + "Allows users that pip installed Aviary to download models from the Aviary hangar"), } From b436c864e7796f0612c41945f32ec17b9de5e6c4 Mon Sep 17 00:00:00 2001 From: crecine <51181861+crecine@users.noreply.github.com> Date: Wed, 31 Jan 2024 09:43:48 -0800 Subject: [PATCH 5/5] Update aviary_commands.ipynb --- aviary/docs/user_guide/aviary_commands.ipynb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aviary/docs/user_guide/aviary_commands.ipynb b/aviary/docs/user_guide/aviary_commands.ipynb index d5adabdb4..e3fe21a4e 100644 --- a/aviary/docs/user_guide/aviary_commands.ipynb +++ b/aviary/docs/user_guide/aviary_commands.ipynb @@ -180,21 +180,21 @@ "(aviary-hangar-command)=\n", "### aviary hangar\n", "\n", - "The `aviary hangar` command will download files and folders from the Aviary hangar.\n", + "The `aviary hangar` command will copy files and folders from the Aviary hangar to an accessible directory.\n", "This is useful for users that have pip installed Aviary, but want to experiment with the included examples.\n", "\n", "The only required input is the name of an input deck.\n", - "This can be specified as the name of the file, the path from [aviary/models](https://github.com/OpenMDAO/Aviary/tree/main/aviary/models), the name of a folder in aviary/models. Multiple files and folders can be downloaded at once.\n", - "Optionally, the output directory can be specified; if it is not, the files will be downloaded into the current working directory in a folder caller aviary_models.\n", + "This can be specified as the name of the file, the path from [aviary/models](https://github.com/OpenMDAO/Aviary/tree/main/aviary/models), the name of a folder in aviary/models. Multiple files and folders can be copied at once.\n", + "Optionally, the output directory can be specified; if it is not, the files will be copied into the current working directory in a folder called aviary_models.\n", "If specified, the output directory will be created as needed.\n", "\n", "Example usage:\n", "```\n", - "`aviary hangar engines` Download all files in the engines folder\n", - "`aviary hangar turbofan_22k.txt` Download the 22k turbofan deck\n", - "`aviary hangar N3CC/N3CC_data.py` Download the N3CC data\n", - "`aviary hangar small_single_aisle_GwGm.dat small_single_aisle_GwGm.csv` Download the Fortran and Aviary input decks for the small single aisle\n", - "`aviary hangar turbofan_22k.txt ~/example_files` Download the engine model into ~/example_files\n", + "`aviary hangar engines` Copy all files in the engines folder\n", + "`aviary hangar turbofan_22k.txt` Copy the 22k turbofan deck\n", + "`aviary hangar N3CC/N3CC_data.py` Copy the N3CC data\n", + "`aviary hangar small_single_aisle_GwGm.dat small_single_aisle_GwGm.csv` Copy the Fortran and Aviary input decks for the small single aisle\n", + "`aviary hangar turbofan_22k.txt ~/example_files` Copy the engine model into ~/example_files\n", "```\n" ] },