diff --git a/aviary/docs/user_guide/aviary_commands.ipynb b/aviary/docs/user_guide/aviary_commands.ipynb index 87d179dd8..e3fe21a4e 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-hangar-command)=\n", + "### aviary hangar\n", + "\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 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` 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" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "```\n", + "aviary hangar -h\n", + "```" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!aviary hangar -h" + ] } ], "metadata": { diff --git a/aviary/interface/cmd_entry_points.py b/aviary/interface/cmd_entry_points.py index 56dff8119..3652a4d60 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_hangar, _setup_hangar_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"), + 'hangar': (_setup_hangar_parser, _exec_hangar, + "Allows users that pip installed Aviary to download models from the Aviary hangar"), } diff --git a/aviary/interface/download_models.py b/aviary/interface/download_models.py new file mode 100644 index 000000000..e58c3dea1 --- /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 hangar" + ) + 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, creating directories as needed. + ''' + 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_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 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." + ) + parser.add_argument( + "-v", "--verbose", + action="store_true", + help="Enable verbose outputs", + ) + + +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: + 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..c8fa4d036 --- /dev/null +++ b/aviary/interface/test/test_download_models.py @@ -0,0 +1,66 @@ +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_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', 'hangar'] + 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_hangar(filename) + + def test_single_file_with_path(self): + filename = 'engines/turbofan_22k.deck' + 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_hangar(filenames) + + def test_folder(self): + filename = 'engines' + self.run_and_test_hangar(filename) + + def test_single_file_custom_outdir(self): + filename = 'small_single_aisle_GwGm.csv' + out_dir = '~/test_hangar' + self.run_and_test_hangar(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()