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()