diff --git a/docs/requirements.txt b/docs/requirements.txt index ccfb254ab..80b70f888 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -16,8 +16,8 @@ certifi==2021.10.8; python_version >= "3.6" and python_full_version < "3.0.0" or cffi==1.15.0; implementation_name == "pypy" and python_version >= "3.6" cfgv==3.3.1; python_full_version >= "3.6.1" charset-normalizer==2.0.7; python_full_version >= "3.6.0" and python_version >= "3.6" -click==8.0.3; python_version >= "3.6" and python_full_version >= "3.6.2" -colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" and python_version < "4.0" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0" and python_version < "4.0" and platform_system == "Windows" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") +click==8.0.3; python_version >= "3.6" +colorama==0.4.4; python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" and sys_platform == "win32" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or platform_system == "Windows" and python_version >= "3.7" and python_full_version >= "3.5.0" and sys_platform == "win32" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0" and python_version < "4") cycler==0.10.0; python_version >= "3.7" debugpy==1.5.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" diff --git a/poetry.lock b/poetry.lock index c433086d2..172b78d51 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1838,7 +1838,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "9f723c4ddb2dc6e2d0b32c2b4f2a29357b8d0ee4a54440b2cd755bd5b769b91c" +content-hash = "ea67ff21131a2bc32dd6af694dff26a36cfd2b374f02fa356bbad4fab01bc8c5" [metadata.files] aenum = [ diff --git a/pyproject.toml b/pyproject.toml index 58cffceab..4cc511956 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,6 +94,7 @@ jupyter-client = "!=7.0.0, !=7.0.1, !=7.0.2, !=7.0.3, !=7.0.4, !=7.0.5" # v7.0. notebook = "^6.0" stdatm = "^0.1.0" Deprecated = "^1.2.13" +click = "^8.0.3" [tool.poetry.dev-dependencies] pytest = "^6.2" @@ -110,8 +111,8 @@ flake8 = "^4.0.1" matplotlib = "^3.1.2" [tool.poetry.scripts] -fastoad = "fastoad.cmd.fast:main" -fast-oad = "fastoad.cmd.fast:main" +fastoad = "fastoad.cmd.cli:fast_oad" +fast-oad = "fastoad.cmd.cli:fast_oad" [tool.poetry.plugins."fastoad_model"] "internal_models" = "fastoad.models" diff --git a/src/fastoad/cmd/api.py b/src/fastoad/cmd/api.py index 3c8208145..7db267f6e 100644 --- a/src/fastoad/cmd/api.py +++ b/src/fastoad/cmd/api.py @@ -18,8 +18,9 @@ import os.path as pth import sys import textwrap as tw +from collections import Iterable from time import time -from typing import IO, Union, List +from typing import IO, List, Union import openmdao.api as om from IPython import InteractiveShell @@ -57,8 +58,10 @@ def generate_configuration_file(configuration_file_path: str, overwrite: bool = :param configuration_file_path: the path of file to be written :param overwrite: if True, the file will be written, even if it already exists + :return: path of generated file :raise FastFileExistsError: if overwrite==False and configuration_file_path already exists """ + configuration_file_path = pth.abspath(configuration_file_path) if not overwrite and pth.exists(configuration_file_path): raise FastFileExistsError( "Configuration file %s not written because it already exists. " @@ -70,6 +73,7 @@ def generate_configuration_file(configuration_file_path: str, overwrite: bool = copy_resource(resources, SAMPLE_FILENAME, configuration_file_path) _LOGGER.info("Sample configuration written in %s", configuration_file_path) + return configuration_file_path def generate_inputs( @@ -133,8 +137,8 @@ def list_variables( :param tablefmt: The formatting of the requested table. Options are the same as those available to the tabulate package. See tabulate.tabulate_formats for a complete list. If "var_desc" the file will use the variable_descriptions.txt format. - :raise FastFileExistsError: if overwrite==False and out parameter is a file path and the file - exists + :return: path of generated file, or None if no file was generated. + :raise FastFileExistsError: if `overwrite==False` and `out` is a file path and the file exists """ if out is None: out = sys.stdout @@ -162,6 +166,7 @@ def list_variables( ) if isinstance(out, str): + out = pth.abspath(out) if not overwrite and pth.exists(out): raise FastFileExistsError( "File %s not written because it already exists. " @@ -173,7 +178,7 @@ def list_variables( else: if out == sys.stdout and InteractiveShell.initialized() and not force_text_output: display(HTML(variables_df.to_html(index=False))) - return + return None # Here we continue with text output out_file = out @@ -187,7 +192,10 @@ def list_variables( if isinstance(out, str): out_file.close() - _LOGGER.info("Output list written in %s", out_file) + _LOGGER.info("Output list written in %s", out) + return out + + return None def _generate_var_desc_format(variables_df): @@ -220,20 +228,21 @@ def list_modules( force_text_output: bool = False, ): """ - Writes list of available systems. - If source_path is given and if it defines paths where there are registered systems, - they will be listed too. - - :param source_path: either a configuration file path, folder path, or list of folder path - :param out: the output stream or a path for the output file (None means sys.stdout) - :param overwrite: if True and out is a file path, the file will be written even if one already - exists - :param verbose: if True, shows detailed information for each system - if False, shows only identifier and path of each system - :param force_text_output: if True, list will be written as text, even if command is used in an - interactive IPython shell (Jupyter notebook). Has no effect in other - shells or if out parameter is not sys.stdout - :raise FastFileExistsError: if overwrite==False and out is a file path and the file exists + Writes list of available systems. + If source_path is given and if it defines paths where there are registered systems, + they will be listed too. + + :param source_path: either a configuration file path, folder path, or list of folder path + :param out: the output stream or a path for the output file (None means sys.stdout) + :param overwrite: if True and out is a file path, the file will be written even if one already + exists + :param verbose: if True, shows detailed information for each system + if False, shows only identifier and path of each system + :param force_text_output: if True, list will be written as text, even if command is used in an + interactive IPython shell (Jupyter notebook). Has no effect in other + shells or if out parameter is not sys.stdout + :return: path of generated file, or None if no file was generated. + :raise FastFileExistsError: if `overwrite==False` and `out` is a file path and the file exists """ if out is None: out = sys.stdout @@ -248,7 +257,7 @@ def list_modules( RegisterOpenMDAOSystem.explore_folder(source_path) else: raise FileNotFoundError("Could not find %s" % source_path) - elif isinstance(source_path, list): + elif isinstance(source_path, Iterable): for folder_path in source_path: if not pth.isdir(folder_path): _LOGGER.warning("SKIPPED %s: folder does not exist.", folder_path) @@ -263,6 +272,7 @@ def list_modules( cell_list = _get_simple_system_list() if isinstance(out, str): + out = pth.abspath(out) if not overwrite and pth.exists(out): raise FastFileExistsError( "File %s not written because it already exists. " @@ -280,7 +290,7 @@ def list_modules( and not verbose ): display(HTML(tabulate(cell_list, tablefmt="html"))) - return + return None out_file = out @@ -289,7 +299,10 @@ def list_modules( if isinstance(out, str): out_file.close() - _LOGGER.info("System list written in %s", out_file) + _LOGGER.info("System list written in %s", out) + return out + + return None def _get_simple_system_list(): @@ -346,15 +359,21 @@ def _get_detailed_system_list(): return cell_list -def write_n2(configuration_file_path: str, n2_file_path: str = "n2.html", overwrite: bool = False): +def write_n2(configuration_file_path: str, n2_file_path: str = None, overwrite: bool = False): """ Write the N2 diagram of the problem in file n2.html :param configuration_file_path: - :param n2_file_path: + :param n2_file_path: if None, will default to `n2.html` :param overwrite: + :return: path of generated file. + :raise FastFileExistsError: if overwrite==False and n2_file_path already exists """ + if not n2_file_path: + n2_file_path = "n2.html" + n2_file_path = pth.abspath(n2_file_path) + if not overwrite and pth.exists(n2_file_path): raise FastFileExistsError( "N2-diagram file %s not written because it already exists. " @@ -370,8 +389,10 @@ def write_n2(configuration_file_path: str, n2_file_path: str = "n2.html", overwr problem.final_setup() om.n2(problem, outfile=n2_file_path, show_browser=False) - clear_output() + if InteractiveShell.initialized(): + clear_output() _LOGGER.info("N2 diagram written in %s", pth.abspath(n2_file_path)) + return n2_file_path def write_xdsm( @@ -391,7 +412,8 @@ def write_xdsm( :param wop_server_url: URL of WhatsOpt server (if None, ether.onera.fr/whatsopt will be used) :param dry_run: if True, will run wop without sending any request to the server. Generated XDSM will be empty. (for test purpose only) - :return: + :return: path of generated file. + :raise FastFileExistsError: if overwrite==False and xdsm_file_path already exists """ if not xdsm_file_path: xdsm_file_path = pth.join(pth.dirname(configuration_file_path), "xdsm.html") @@ -413,6 +435,7 @@ def write_xdsm( problem.final_setup() fastoad.openmdao.whatsopt.write_xdsm(problem, xdsm_file_path, depth, wop_server_url, dry_run) + return xdsm_file_path def _run_problem( @@ -430,6 +453,7 @@ def _run_problem( :param auto_scaling: if True, automatic scaling is performed for design variables and constraints :return: the OpenMDAO problem after run + :raise FastFileExistsError: if overwrite==False and output data file of problem already exists """ conf = FASTOADProblemConfigurator(configuration_file_path) @@ -472,6 +496,7 @@ def evaluate_problem(configuration_file_path: str, overwrite: bool = False) -> F :param configuration_file_path: problem definition :param overwrite: if True, output file will be overwritten :return: the OpenMDAO problem after run + :raise FastFileExistsError: if overwrite==False and output data file of problem already exists """ return _run_problem(configuration_file_path, overwrite, "run_model") @@ -487,6 +512,7 @@ def optimize_problem( :param auto_scaling: if True, automatic scaling is performed for design variables and constraints :return: the OpenMDAO problem after run + :raise FastFileExistsError: if overwrite==False and output data file of problem already exists """ return _run_problem(configuration_file_path, overwrite, "run_driver", auto_scaling=auto_scaling) diff --git a/src/fastoad/cmd/cli.py b/src/fastoad/cmd/cli.py new file mode 100644 index 000000000..9b7eef5f3 --- /dev/null +++ b/src/fastoad/cmd/cli.py @@ -0,0 +1,251 @@ +"""Command Line Interface.""" +# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design +# Copyright (C) 2021 ONERA & ISAE-SUPAERO +# FAST is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import os +import os.path as pth +import shutil + +import click +import tabulate + +import fastoad +from fastoad import api, notebooks +from fastoad._utils.resource_management.copy import copy_resource_folder +from fastoad.cmd.cli_utils import ( + manage_overwrite, + out_file_option, + overwrite_option, +) + +NOTEBOOK_FOLDER_NAME = "FAST-OAD_notebooks" + + +@click.group( + context_settings=dict( + help_option_names=["-h", "--help"], + ) +) +@click.version_option(fastoad.__version__, "-v", "--version") +def fast_oad(): + """FAST-OAD main program""" + + +def fast_oad_subcommand(func): + """Decorator for adding a command as subcommand to `fast_oad`.""" + return fast_oad.add_command(func) + + +@fast_oad_subcommand +@click.command(name="gen_conf") +@click.argument("conf_file", nargs=1) +@overwrite_option +def gen_conf(conf_file, force): + """Generate a sample configuration file with given argument as name.""" + manage_overwrite( + api.generate_configuration_file, + configuration_file_path=conf_file, + overwrite=force, + ) + + +@fast_oad_subcommand +@click.command(name="gen_inputs") +@click.argument("conf_file", nargs=1) +@click.argument("source_file", nargs=1, required=False) +@overwrite_option +@click.option( + "--legacy", is_flag=True, help="To be used if the source XML file is in legacy format." +) +def gen_inputs(conf_file, source_file, force, legacy): + """ + Generate the input file (specified in the configuration file) with needed variables. + + \b + Examples: + --------- + # For the problem defined in conf_file.yml, generates the input file with default + # values (when default values are defined): + fastoad gen_inputs conf_file.yml + + \b + # Same as above, except that values are taken from some_file.xml when possible: + fastoad gen_inputs conf_file.yml some_file.xml + + \b + # Same as above, some_file.xml is formatted with the legacy FAST schema + fastoad gen_inputs conf_file.yml some_file.xml --legacy + """ + schema = "legacy" if legacy else "native" + manage_overwrite( + api.generate_inputs, + configuration_file_path=conf_file, + source_path=source_file, + source_path_schema=schema, + overwrite=force, + ) + + +@fast_oad_subcommand +@click.command(name="list_modules") +@out_file_option +@click.option("-v", "--verbose", is_flag=True, help="Shows detailed information for each system.") +@click.argument("source_path", nargs=-1) +def list_modules(out_file, force, verbose, source_path): + """ + Provide the identifiers of available systems. + + SOURCE_PATH argument can be a configuration file, or a list of folders where + custom modules are declared. + """ + # If a configuration file or a single path is provided make sure it is sent as a + # string not a list + if len(source_path) == 1: + source_path = source_path[0] + + if manage_overwrite( + api.list_modules, + source_path=source_path, + out=out_file, + overwrite=force, + verbose=verbose, + ): + print("\nDone. Use --verbose (-v) option for detailed information.") + + +@fast_oad_subcommand +@click.command(name="list_variables") +@click.argument("conf_file", nargs=1) +@out_file_option +@click.option( + "--format", + "table_format", + default="grid", + show_default=True, + help=f"format of the list. Available options are {['var_desc'] + tabulate.tabulate_formats}. " + '"var_desc" is the variable_descriptions.txt format. Other formats are part of the ' + "tabulate package.", +) +def list_variables(conf_file, out_file, force, table_format): + """List the variables of the problem defined in CONF_FILE.""" + manage_overwrite( + api.list_variables, + configuration_file_path=conf_file, + out=out_file, + overwrite=force, + tablefmt=table_format, + ) + + +@fast_oad_subcommand +@click.command(name="n2") +@click.argument("conf_file", nargs=1) +@click.argument("n2_file", nargs=1, default="n2.html", required=False) +@overwrite_option +def write_n2(conf_file, n2_file, force): + """ + Write an HTML file that shows the N2 diagram of the problem defined in CONF_FILE. + + The name of generated file is `n2.html`, or the given name for argument N2_FILE. + """ + manage_overwrite( + api.write_n2, + configuration_file_path=conf_file, + n2_file_path=n2_file, + overwrite=force, + ) + + +@fast_oad_subcommand +@click.command(name="xdsm") +@click.argument("conf_file", nargs=1) +@click.argument("xdsm_file", nargs=1, default="xdsm.html", required=False) +@overwrite_option +@click.option("--depth", default=2, show_default=True, help="Depth of analysis.") +@click.option( + "--server", + help="URL of WhatsOpt server. For advanced users only.", +) +def write_xdsm(conf_file, xdsm_file, depth, server, force): + """ + Write an HTML file that shows the XDSM diagram of the problem defined in CONF_FILE. + + The name of generated file is `xdsm.html`, or the given name for argument XDSM_FILE. + """ + manage_overwrite( + api.write_xdsm, + configuration_file_path=conf_file, + xdsm_file_path=xdsm_file, + overwrite=force, + depth=depth, + wop_server_url=server, + ) + + +@fast_oad_subcommand +@click.command(name="eval") +@click.argument("conf_file", nargs=1) +@overwrite_option +def evaluate(conf_file, force): + """Run the analysis for problem defined in CONF_FILE.""" + manage_overwrite( + api.evaluate_problem, + filename_func=lambda pb: pb.output_file_path, + configuration_file_path=conf_file, + overwrite=force, + ) + + +@fast_oad_subcommand +@click.command(name="optim") +@click.argument("conf_file", nargs=1) +@overwrite_option +def optimize(conf_file, force): + """Run the optimization for problem defined in CONF_FILE.""" + manage_overwrite( + api.optimize_problem, + filename_func=lambda pb: pb.output_file_path, + configuration_file_path=conf_file, + overwrite=force, + ) + + +@fast_oad_subcommand +@click.command(name="notebooks") +@click.argument("path", nargs=1, default=".", required=False) +def create_notebooks(path): + """ + Creates a FAST-OAD_notebooks/ folder with pre-configured Jupyter notebooks. + + If PATH is given, FAST-OAD_notebooks/ will be created in that folder. + + Please note that all content of an existing FAST-OAD_notebooks/ will be overwritten. + """ + # Create and copy folder + target_path = pth.abspath(pth.join(path, NOTEBOOK_FOLDER_NAME)) + if pth.exists(target_path): + shutil.rmtree(target_path) + os.makedirs(target_path) + + copy_resource_folder(notebooks, target_path) + # Note: copy_resource_folder(tutorial, target_path) would fail because of IPython imports + + # Give info for running Jupyter + print("") + print("Notebooks have been created in %s" % target_path) + print("You may now run Jupyter with:") + print(' jupyter lab "%s"' % target_path) + + +if __name__ == "__main__": + fast_oad() diff --git a/src/fastoad/cmd/cli_utils.py b/src/fastoad/cmd/cli_utils.py new file mode 100644 index 000000000..436d61111 --- /dev/null +++ b/src/fastoad/cmd/cli_utils.py @@ -0,0 +1,86 @@ +"""Utility functions for CLI interface.""" +# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design +# Copyright (C) 2021 ONERA & ISAE-SUPAERO +# FAST is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from typing import Callable + +import click + +from fastoad.cmd.exceptions import FastFileExistsError + + +def overwrite_option(func): + """ + Decorator for adding the option for overwriting existing file. + + Use `force` as argument of the function. + """ + return click.option( + "-f", + "--force", + is_flag=True, + help="Do not ask before overwriting files.", + )(func) + + +def out_file_option(func): + """ + Decorator for writing command output in a file. + + Use `out_file` and `force` as argument of the function. + """ + return click.option( + "-o", + "--out_file", + help="If provided, command output will be written in indicated file instead of " + "being printed in terminal.", + )(overwrite_option(func)) + + +def manage_overwrite(func: Callable, filename_func: Callable = None, **kwargs): + """ + Runs `func`, that is expected to write a file, with provided keyword arguments `args`. + + If the run throws FastFileExistsError, a question is displayed and user is + asked for a yes/no answer. If `yes` is given, arg["overwrite"] is set to True + and `func` is run again. + + :param func: callable that will do the operation + :param filename_func: a function that provides the name of written file, given the + value returned by func + :param kwargs: keyword arguments for func + :return: True if the file has been written, + """ + written = False + try: + written = _run_func(func, filename_func, **kwargs) + + except FastFileExistsError as exc: + if click.confirm(f'File "{exc.args[1]}" already exists. Do you want to overwrite it?'): + kwargs["overwrite"] = True + written = _run_func(func, filename_func, **kwargs) + else: + click.echo("No file written.") + + return written + + +def _run_func(func: Callable, filename_func: Callable = None, **kwargs): + result = func(**kwargs) + if result: + if filename_func: + result = filename_func(result) + click.echo(f'File "{result}" has been written.') + return True + + return False diff --git a/src/fastoad/cmd/fast.py b/src/fastoad/cmd/fast.py deleted file mode 100644 index ef48eac94..000000000 --- a/src/fastoad/cmd/fast.py +++ /dev/null @@ -1,422 +0,0 @@ -"""Command Line Interface.""" -# This file is part of FAST-OAD : A framework for rapid Overall Aircraft Design -# Copyright (C) 2021 ONERA & ISAE-SUPAERO -# FAST is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import logging -import os -import os.path as pth -import shutil -import textwrap -from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, RawDescriptionHelpFormatter -from distutils.util import strtobool - -import tabulate - -import fastoad -from fastoad import notebooks -from fastoad._utils.resource_management.copy import copy_resource_folder -from fastoad.cmd import api -from fastoad.cmd.exceptions import FastFileExistsError - -NOTEBOOK_FOLDER_NAME = "FAST-OAD_notebooks" - - -# TODO: it has become a bit messy down here... Refactoring needed, maybe -# with a better organization of code by sub-commands - - -class Main: - """ - Class for managing command line and doing associated actions - """ - - def __init__(self): - class _CustomFormatter(RawDescriptionHelpFormatter, ArgumentDefaultsHelpFormatter): - pass - - self.parser = ArgumentParser( - description="FAST-OAD main program", formatter_class=_CustomFormatter - ) - self.problem = None - - # ACTIONS ====================================================================================== - @staticmethod - def _generate_conf_file(args): - """Generates a sample configuration file.""" - try: - api.generate_configuration_file(args.conf_file, args.force) - except FastFileExistsError: - if _query_yes_no( - 'Configuration file "%s" already exists. Do you want to overwrite it?' - % args.conf_file - ): - api.generate_configuration_file(args.conf_file, True) - else: - print("No file written.") - - @staticmethod - def _generate_inputs(args): - """Generates input file according to command line arguments.""" - schema = "legacy" if args.legacy else "native" - try: - api.generate_inputs(args.conf_file, args.source, schema, args.force) - except FastFileExistsError as exc: - if _query_yes_no( - 'Input file "%s" already exists. Do you want to overwrite it?' % exc.args[1] - ): - api.generate_inputs(args.conf_file, args.source, schema, True) - else: - print("No file written.") - - @staticmethod - def _list_modules(args): - """Prints list of system identifiers.""" - # If a configuration file or a single path is provided make sure it is sent as a - # string not a list - if len(args.source_path) == 1: - source_path = args.source_path[0] - else: - source_path = args.source_path - api.list_modules(source_path, out=args.out_file, overwrite=args.force, verbose=args.verbose) - print("\nDone. Use --verbose (-v) option for detailed information.") - - @staticmethod - def _list_variables(args): - """Prints list of system outputs.""" - api.list_variables( - args.conf_file, - out=args.out_file, - overwrite=args.force, - tablefmt=args.format, - ) - - @staticmethod - def _write_n2(args): - """Creates N2 html file.""" - try: - api.write_n2(args.conf_file, args.n2_file, args.force) - except FastFileExistsError: - if _query_yes_no( - 'N2 file "%s" already exists. Do you want to overwrite it?' % args.n2_file - ): - api.write_n2(args.conf_file, args.n2_file, True) - else: - print("No file written.") - - @staticmethod - def _write_xdsm(args): - """Creates XDSM html file.""" - - def _write(overwrite): - api.write_xdsm( - args.conf_file, - args.xdsm_file, - overwrite=overwrite, - wop_server_url=args.wop_server, - depth=args.depth, - ) - - try: - _write(args.force) - except FastFileExistsError: - if _query_yes_no( - 'XDSM file "%s" already exists. Do you want to overwrite it?' % args.xdsm_file - ): - _write(True) - else: - print("No file written.") - - @staticmethod - def _evaluate(args): - """Runs model according to provided problem file.""" - try: - api.evaluate_problem(args.conf_file, args.force) - except FastFileExistsError as exc: - if _query_yes_no( - 'Output file "%s" already exists. Do you want to overwrite it?' % exc.args[1] - ): - api.evaluate_problem(args.conf_file, True) - else: - print("Computation not run.") - - @staticmethod - def _optimize(args): - """Runs driver according to provided problem file.""" - try: - api.optimize_problem(args.conf_file, args.force) - except FastFileExistsError as exc: - if _query_yes_no( - 'Output file "%s" already exists. Do you want to overwrite it?' % exc.args[1] - ): - api.optimize_problem(args.conf_file, True) - else: - print("Computation not run.") - - @staticmethod - def _notebooks(args): - """Copies the notebook folder in user-selected destination.""" - # Create and copy folder - target_path = pth.abspath(pth.join(args.path, NOTEBOOK_FOLDER_NAME)) - if pth.exists(target_path): - shutil.rmtree(target_path) - os.makedirs(target_path) - - copy_resource_folder(notebooks, target_path) - # Note: copy_resource_folder(tutorial, target_path) would fail because of IPython imports - - # Give info for running Jupyter - print("") - print("Notebooks have been created in %s" % target_path) - print("You may now run Jupyter with:") - print(' jupyter lab "%s"' % target_path) - - # UTILITIES ==================================================================================== - - # PARSER CONFIGURATION ========================================================================= - @staticmethod - def _add_conf_file_argument(parser: ArgumentParser, required=True): - kwargs = {"type": str, "help": "the configuration file for setting the problem"} - if not required: - kwargs["nargs"] = "?" - kwargs["help"] += " (not required)" - parser.add_argument("conf_file", **kwargs) - - @staticmethod - def _add_overwrite_argument(parser: ArgumentParser): - parser.add_argument( - "-f", "--force", action="store_true", help="do not ask before overwriting files" - ) - - @staticmethod - def _add_output_file_argument(parser: ArgumentParser): - parser.add_argument( - "-o", - "--out_file", - type=str, - help="if provided, command output will be written in indicated file instead of being " - "printed in terminal.", - ) - - # ENTRY POINT ================================================================================== - def run(self): - """Main function.""" - subparsers = self.parser.add_subparsers(title="sub-commands") - - # option for version ----------------------------------------------------------------------- - self.parser.add_argument( - "-v", - "--version", - action="version", - version="FAST-OAD " + fastoad.__version__, - help="shows version number", - ) - - # sub-command for generating sample configuration file ------------------------------------- - parser_gen_conf = subparsers.add_parser( - "gen_conf", - help="generates a sample configuration file", - description="generates the configuration file with sample data", - ) - parser_gen_conf.help = parser_gen_conf.description - parser_gen_conf.add_argument( - "conf_file", type=str, help="the name of configuration file to be written" - ) - self._add_overwrite_argument(parser_gen_conf) - parser_gen_conf.set_defaults(func=self._generate_conf_file) - - # sub-command for generating input file ---------------------------------------------------- - parser_gen_inputs = subparsers.add_parser( - "gen_inputs", - formatter_class=RawDescriptionHelpFormatter, - help="generates the input file", - description="generates the input file (specified in the configuration file)" - " with needed variables", - ) - self._add_conf_file_argument(parser_gen_inputs) - self._add_overwrite_argument(parser_gen_inputs) - parser_gen_inputs.add_argument( - "source", - nargs="?", - help="if provided, generated input file will be fed with values from provided XML file", - ) - parser_gen_inputs.add_argument( - "--legacy", - action="store_true", - help="to be used if the source XML file is in legacy format", - ) - parser_gen_inputs.set_defaults(func=self._generate_inputs) - parser_gen_inputs.epilog = textwrap.dedent( - """\ - Examples: - --------- - # For the problem defined in conf_file.yml, generates the input file with default - # values (when default values are defined): - %(prog)s conf_file.yml - - # Same as above, except that values are taken from some_file.xml when possible: - %(prog)s conf_file.yml some_file.xml - - # Same as above, some_file.xml is in the legacy FAST schema - %(prog)s conf_file.yml some_file.xml --legacy - """ - ) - - # sub-command for listing registered systems ----------------------------------------------- - parser_list_modules = subparsers.add_parser( - "list_modules", - help="Provides the identifiers of available systems", - description="Provides the identifiers of available systems", - ) - self._add_output_file_argument(parser_list_modules) - self._add_overwrite_argument(parser_list_modules) - parser_list_modules.add_argument( - "-v", - "--verbose", - action="store_true", - help="shows detailed information for each system", - ) - parser_list_modules.add_argument( - "source_path", - nargs="*", - default=None, - help="either a configuration file path, folder path, or list of folder path", - ) - parser_list_modules.set_defaults(func=self._list_modules) - - # sub-command for listing problem variables ------------------------------------------------ - parser_list_variables = subparsers.add_parser( - "list_variables", - help="Lists the variables of the problem", - description="Lists the variables of the problem", - ) - self._add_conf_file_argument(parser_list_variables) - self._add_output_file_argument(parser_list_variables) - self._add_overwrite_argument(parser_list_variables) - parser_list_variables.add_argument( - "--format", - nargs="?", - default="grid", - help="format of the list. Available options are " - f"{['var_desc'] + tabulate.tabulate_formats}. " - '"var_desc" is the variable_descriptions.txt format. Other formats are part of the ' - "tabulate package. (default: %(default)s)", - ) - parser_list_variables.set_defaults(func=self._list_variables) - - # sub-command for writing N2 diagram ------------------------------------------------------- - parser_n2 = subparsers.add_parser( - "n2", - help="Writes the N2 diagram of the problem", - description="Writes an HTML file that shows the N2 diagram of the problem", - ) - self._add_conf_file_argument(parser_n2) - self._add_overwrite_argument(parser_n2) - parser_n2.add_argument( - "n2_file", - nargs="?", - default="n2.html", - help="path of file to be written (default: %(default)s)", - ) - parser_n2.set_defaults(func=self._write_n2) - - # sub-command for writing XDSM diagram ----------------------------------------------------- - parser_xdsm = subparsers.add_parser( - "xdsm", - help="Writes the XDSM diagram of the problem", - description="Writes an HTML file that shows the XDSM diagram of the problem", - ) - self._add_conf_file_argument(parser_xdsm) - self._add_overwrite_argument(parser_xdsm) - parser_xdsm.add_argument( - "xdsm_file", - nargs="?", - default="xdsm.html", - help="path of file to be written (default: %(default)s)", - ) - parser_xdsm.add_argument( - "--depth", nargs="?", default=2, help="Depth of analysis", type=int - ) - parser_xdsm.add_argument( - "--server", - nargs="?", - default=None, - dest="wop_server", - help="URL of WhatsOpt server. For advanced users only.", - ) - parser_xdsm.set_defaults(func=self._write_xdsm) - - # sub-command for running the model -------------------------------------------------------- - parser_run_model = subparsers.add_parser( - "eval", help="Runs the analysis", description="Runs the analysis" - ) - self._add_conf_file_argument(parser_run_model) - self._add_overwrite_argument(parser_run_model) - parser_run_model.set_defaults(func=self._evaluate) - - # sub-command for running the driver ------------------------------------------------------- - parser_run_driver = subparsers.add_parser( - "optim", help="Runs the optimization", description="Runs the optimization" - ) - self._add_conf_file_argument(parser_run_driver) - self._add_overwrite_argument(parser_run_driver) - parser_run_driver.set_defaults(func=self._optimize) - - # sub-command for running Jupyter notebooks ------------------------------------------------ - parser_notebooks = subparsers.add_parser( - "notebooks", - help="Create ready-to-use notebooks", - description="Creates a %(nb_folder)s/ folder with pre-configured Jupyter notebooks. " - "Please note that all content of an existing %(nb_folder)s/ will be overwritten." - % {"nb_folder": NOTEBOOK_FOLDER_NAME}, - ) - - parser_notebooks.add_argument( - "path", - default=".", - nargs="?", - help="The path where the %s/ folder will be added" % NOTEBOOK_FOLDER_NAME, - ) - - parser_notebooks.set_defaults(func=self._notebooks) - - # Parse ------------------------------------------------------------------------------------ - args = self.parser.parse_args() - args.func(args) - - -def _query_yes_no(question): - """ - Ask a yes/no question via input() and return its answer as boolean. - - Keeps asking while answer is not similar to "yes" or "no" - The returned value is True for "yes" or False for "no". - """ - answer = None - while answer is None: - raw_answer = input(question + "\n") - try: - answer = strtobool(raw_answer) - except ValueError: - pass - - return answer == 1 - - -def main(): - log_format = "%(levelname)-8s: %(message)s" - logging.basicConfig(level=logging.INFO, format=log_format) - Main().run() - - -if __name__ == "__main__": - main()