def test_problem_definition_with_xml_ref_run_optim(cleanup):
    """
    Tests what happens when writing inputs using data from existing XML file
    and running an optimization problem
    """
    conf = FASTOADProblemConfigurator(
        pth.join(DATA_FOLDER_PATH, "valid_sellar.toml"))

    input_data = pth.join(DATA_FOLDER_PATH, "ref_inputs.xml")
    conf.write_needed_inputs(input_data)

    # Runs optimization problem with semi-analytic FD
    problem1 = conf.get_problem(read_inputs=True)
    problem1.setup()
    problem1.run_model()
    assert problem1["f"] == pytest.approx(28.58830817, abs=1e-6)
    problem1.run_driver()
    assert problem1["f"] == pytest.approx(3.18339395, abs=1e-6)

    # Runs optimization problem with monolithic FD
    problem2 = conf.get_problem(read_inputs=True)
    problem2.model.approx_totals()
    problem2.setup()
    problem2.run_model()  # checks problem has been reset
    assert problem2["f"] == pytest.approx(28.58830817, abs=1e-6)
    problem2.run_driver()
    assert problem2["f"] == pytest.approx(3.18339395, abs=1e-6)
Example #2
0
def test_problem_definition_module_folder_as_one_string(cleanup):
    for extension in ["toml", "yml"]:
        clear_openmdao_registry()
        conf = FASTOADProblemConfigurator()
        conf.load(
            pth.join(pth.dirname(__file__), "data",
                     "module_folder_as_one_string.%s" % extension))
        conf.get_problem()
def test_problem_definition_no_module_folder(cleanup):
    for extension in ["toml", "yml"]:
        clear_openmdao_registry()
        conf = FASTOADProblemConfigurator()
        conf.load(pth.join(DATA_FOLDER_PATH, "no_module_folder.%s" % extension))
        with pytest.raises(FastBundleLoaderUnknownFactoryNameError) as exc_info:
            conf.get_problem()
        assert exc_info.value.factory_name == "configuration_test.sellar.functions"
def test_problem_definition_with_xml_ref_with_indep(cleanup):
    """Tests what happens when writing inputs of a problem with indeps using data from existing XML file"""
    for extension in ["toml", "yml"]:
        clear_openmdao_registry()
        conf = FASTOADProblemConfigurator(
            pth.join(DATA_FOLDER_PATH, "valid_sellar_with_indep.%s" % extension)
        )

        result_folder_path = pth.join(
            RESULTS_FOLDER_PATH, "problem_definition_with_xml_ref_with_indep"
        )
        conf.input_file_path = pth.join(result_folder_path, "inputs.xml")
        conf.output_file_path = pth.join(result_folder_path, "outputs.xml")
        ref_input_data_path = pth.join(DATA_FOLDER_PATH, "ref_inputs.xml")
        conf.write_needed_inputs(ref_input_data_path)
        input_data = DataFile(conf.input_file_path)
        assert len(input_data) == 2
        assert "system:x" in input_data.names()
        assert "z" in input_data.names()

        problem = conf.get_problem(read_inputs=True, auto_scaling=True)
        # runs evaluation without optimization loop to check that inputs are taken into account
        problem.setup()
        # system:x is not in ref_inputs.xml
        problem["system:x"] = 1.0
        problem.run_model()
        assert problem["f"] == pytest.approx(28.58830817, abs=1e-6)
        problem.write_outputs()
Example #5
0
def test_oad_process(cleanup):
    """
    Test for the overall aircraft design process.
    """

    configurator = FASTOADProblemConfigurator(pth.join(DATA_FOLDER_PATH, "oad_process.yml"))

    # Create inputs
    ref_inputs = pth.join(DATA_FOLDER_PATH, "CeRAS01_legacy.xml")
    configurator.write_needed_inputs(ref_inputs)

    # Create problems with inputs
    problem = configurator.get_problem(read_inputs=True)
    problem.setup()
    problem.run_model()
    problem.write_outputs()

    if not pth.exists(RESULTS_FOLDER_PATH):
        os.mkdir(RESULTS_FOLDER_PATH)
    om.view_connections(
        problem, outfile=pth.join(RESULTS_FOLDER_PATH, "connections.html"), show_browser=False
    )
    om.n2(problem, outfile=pth.join(RESULTS_FOLDER_PATH, "n2.html"), show_browser=False)

    # Check that weight-performances loop correctly converged
    _check_weight_performance_loop(problem)
def test_problem_definition_incorrect_attribute(cleanup):
    for extension in ["toml", "yml"]:
        clear_openmdao_registry()
        conf = FASTOADProblemConfigurator()
        conf.load(pth.join(DATA_FOLDER_PATH, "invalid_attribute.%s" % extension))
        with pytest.raises(FASTConfigurationBadOpenMDAOInstructionError) as exc_info:
            problem = conf.get_problem(read_inputs=False)
        assert exc_info.value.key == "model.cycle.other_group.nonlinear_solver"
Example #7
0
def test_problem_definition_with_nan_inputs(cleanup):
    """ Tests what happens when writing inputs using existing XML with some unwanted var"""
    conf = FASTOADProblemConfigurator(
        pth.join(DATA_FOLDER_PATH, "valid_sellar.toml"))

    input_data_path = pth.join(DATA_FOLDER_PATH, "nan_inputs.xml")
    os.makedirs(RESULTS_FOLDER_PATH, exist_ok=True)
    shutil.copy(input_data_path, conf.input_file_path)

    with pytest.raises(FASTConfigurationNanInInputFile) as exc:
        problem = conf.get_problem(read_inputs=True, auto_scaling=True)
        assert exc.input_file_path == input_data_path
        assert exc.nan_variable_names == ["x"]
def test_problem_definition_with_xml_ref(cleanup):
    """Tests what happens when writing inputs using data from existing XML file"""
    for extension in ["toml", "yml"]:
        clear_openmdao_registry()
        conf = FASTOADProblemConfigurator(pth.join(DATA_FOLDER_PATH, "valid_sellar.%s" % extension))

        result_folder_path = pth.join(RESULTS_FOLDER_PATH, "problem_definition_with_xml_ref")
        conf.input_file_path = pth.join(result_folder_path, "inputs.xml")
        conf.output_file_path = pth.join(result_folder_path, "outputs.xml")
        ref_input_data_path = pth.join(DATA_FOLDER_PATH, "ref_inputs.xml")
        conf.write_needed_inputs(ref_input_data_path)
        input_data = DataFile(conf.input_file_path)
        assert len(input_data) == 2
        assert "x" in input_data.names()
        assert "z" in input_data.names()

        problem = conf.get_problem(read_inputs=True, auto_scaling=True)
        # runs evaluation without optimization loop to check that inputs are taken into account
        problem.setup()
        problem.run_model()
        assert problem["f"] == pytest.approx(28.58830817, abs=1e-6)
        problem.write_outputs()

        # Test with alternate submodel #########################################
        alt_conf = FASTOADProblemConfigurator(
            pth.join(DATA_FOLDER_PATH, "valid_sellar_alternate.%s" % extension)
        )
        alt_conf.input_file_path = pth.join(result_folder_path, "inputs.xml")
        alt_conf.output_file_path = pth.join(result_folder_path, "outputs_alt.xml")
        alt_problem = alt_conf.get_problem(read_inputs=True, auto_scaling=True)
        # runs evaluation without optimization loop to check that inputs are taken into account
        alt_problem.setup()
        alt_problem.run_model()
        alt_problem.write_outputs()

        assert alt_problem["f"] == pytest.approx(0.58830817, abs=1e-6)
        assert alt_problem["g2"] == pytest.approx(-11.94151185, abs=1e-6)
        with pytest.raises(KeyError):
            alt_problem["g1"]  # submodel for g1 computation has been deactivated.
def test_oad_process(cleanup):
    """
    Test for the overall aircraft design process.
    """

    configurator = FASTOADProblemConfigurator(
        pth.join(DATA_FOLDER_PATH, "oad_process.toml"))

    # Create inputs
    ref_inputs = pth.join(DATA_FOLDER_PATH, "CeRAS01_legacy.xml")
    configurator.write_needed_inputs(ref_inputs)

    # Create problems with inputs
    problem = configurator.get_problem(read_inputs=True)
    problem.setup()
    problem.run_model()
    problem.write_outputs()

    if not pth.exists(RESULTS_FOLDER_PATH):
        os.mkdir(RESULTS_FOLDER_PATH)
    om.view_connections(problem,
                        outfile=pth.join(RESULTS_FOLDER_PATH,
                                         "connections.html"),
                        show_browser=False)
    om.n2(problem,
          outfile=pth.join(RESULTS_FOLDER_PATH, "n2.html"),
          show_browser=False)

    # Check that weight-performances loop correctly converged
    assert_allclose(
        problem["data:weight:aircraft:OWE"],
        problem["data:weight:airframe:mass"] +
        problem["data:weight:propulsion:mass"] +
        problem["data:weight:systems:mass"] +
        problem["data:weight:furniture:mass"] +
        problem["data:weight:crew:mass"],
        atol=1,
    )
    assert_allclose(
        problem["data:weight:aircraft:MZFW"],
        problem["data:weight:aircraft:OWE"] +
        problem["data:weight:aircraft:max_payload"],
        atol=1,
    )
    assert_allclose(
        problem["data:weight:aircraft:MTOW"],
        problem["data:weight:aircraft:OWE"] +
        problem["data:weight:aircraft:payload"] +
        problem["data:mission:sizing:needed_block_fuel"],
        atol=1,
    )
Example #10
0
def test_problem_definition_with_xml_ref_run_optim(cleanup):
    """
    Tests what happens when writing inputs using data from existing XML file
    and running an optimization problem
    """
    for extension in ["toml", "yml"]:
        clear_openmdao_registry()
        conf = FASTOADProblemConfigurator(
            pth.join(DATA_FOLDER_PATH, "valid_sellar.%s" % extension))

        result_folder_path = pth.join(
            RESULTS_FOLDER_PATH, "problem_definition_with_xml_ref_run_optim")
        conf.input_file_path = pth.join(result_folder_path, "inputs.xml")
        input_data = pth.join(DATA_FOLDER_PATH, "ref_inputs.xml")
        conf.write_needed_inputs(input_data)

        # Runs optimization problem with semi-analytic FD
        problem1 = conf.get_problem(read_inputs=True)
        problem1.setup()
        problem1.run_model()
        assert problem1["f"] == pytest.approx(28.58830817, abs=1e-6)
        problem1.run_driver()
        assert problem1["f"] == pytest.approx(3.18339395, abs=1e-6)
        problem1.output_file_path = pth.join(result_folder_path,
                                             "outputs_1.xml")
        problem1.write_outputs()

        # Runs optimization problem with monolithic FD
        problem2 = conf.get_problem(read_inputs=True)
        problem2.model.approx_totals()
        problem2.setup()
        problem2.run_model()  # checks problem has been reset
        assert problem2["f"] == pytest.approx(28.58830817, abs=1e-6)
        problem2.run_driver()
        assert problem2["f"] == pytest.approx(3.18339395, abs=1e-6)
        problem2.output_file_path = pth.join(result_folder_path,
                                             "outputs_2.xml")
        problem2.write_outputs()
def test_problem_definition_with_xml_ref(cleanup):
    """ Tests what happens when writing inputs using data from existing XML file"""
    conf = FASTOADProblemConfigurator(
        pth.join(DATA_FOLDER_PATH, "valid_sellar.toml"))

    input_data = pth.join(DATA_FOLDER_PATH, "ref_inputs.xml")
    conf.write_needed_inputs(input_data)

    problem = conf.get_problem(read_inputs=True, auto_scaling=True)
    # runs evaluation without optimization loop to check that inputs are taken into account
    problem.setup()
    problem.run_model()

    assert problem["f"] == pytest.approx(28.58830817, abs=1e-6)
    problem.write_outputs()
def test_problem_definition_with_custom_xml(cleanup):
    """Tests what happens when writing inputs using existing XML with some unwanted var"""
    conf = FASTOADProblemConfigurator(pth.join(DATA_FOLDER_PATH, "valid_sellar.toml"))

    result_folder_path = pth.join(RESULTS_FOLDER_PATH, "problem_definition_with_custom_xml")
    conf.input_file_path = pth.join(result_folder_path, "inputs.xml")
    conf.output_file_path = pth.join(result_folder_path, "outputs.xml")

    input_data = pth.join(DATA_FOLDER_PATH, "ref_inputs.xml")
    os.makedirs(result_folder_path, exist_ok=True)
    shutil.copy(input_data, conf.input_file_path)

    problem = conf.get_problem(read_inputs=True, auto_scaling=True)
    problem.setup()
    problem.run_model()

    assert problem["f"] == pytest.approx(28.58830817, abs=1e-6)
    problem.write_outputs()
Example #13
0
def run_non_regression_test(
    conf_file,
    legacy_result_file,
    result_dir,
    use_xfoil=False,
    vars_to_check=None,
    tolerance=5.0e-3,
    check_weight_perfo_loop=True,
):
    results_folder_path = pth.join(RESULTS_FOLDER_PATH, result_dir)
    configuration_file_path = pth.join(results_folder_path, conf_file)

    # Copy of configuration file and generation of problem instance ------------------
    api.generate_configuration_file(
        configuration_file_path)  # just ensure folders are created...
    shutil.copy(pth.join(DATA_FOLDER_PATH, conf_file), configuration_file_path)
    configurator = FASTOADProblemConfigurator(configuration_file_path)
    configurator._set_configuration_modifier(XFOILConfigurator(use_xfoil))

    # Generation of inputs ----------------------------------------
    ref_inputs = pth.join(DATA_FOLDER_PATH, legacy_result_file)
    configurator.write_needed_inputs(ref_inputs)

    # Get problem with inputs -------------------------------------
    problem = configurator.get_problem(read_inputs=True)
    problem.setup()

    # Run model ---------------------------------------------------------------
    problem.run_model()
    problem.write_outputs()

    om.view_connections(problem,
                        outfile=pth.join(results_folder_path,
                                         "connections.html"),
                        show_browser=False)

    if check_weight_perfo_loop:
        # Check that weight-performances loop correctly converged
        assert_allclose(
            problem["data:weight:aircraft:OWE"],
            problem["data:weight:airframe:mass"] +
            problem["data:weight:propulsion:mass"] +
            problem["data:weight:systems:mass"] +
            problem["data:weight:furniture:mass"] +
            problem["data:weight:crew:mass"],
            atol=1,
        )
        assert_allclose(
            problem["data:weight:aircraft:MZFW"],
            problem["data:weight:aircraft:OWE"] +
            problem["data:weight:aircraft:max_payload"],
            atol=1,
        )
        assert_allclose(
            problem["data:weight:aircraft:MTOW"],
            problem["data:weight:aircraft:OWE"] +
            problem["data:weight:aircraft:payload"] +
            problem["data:mission:sizing:needed_block_fuel"],
            atol=1,
        )

    ref_var_list = VariableIO(pth.join(DATA_FOLDER_PATH,
                                       legacy_result_file), ).read()

    row_list = []
    for ref_var in ref_var_list:
        try:
            value = problem.get_val(ref_var.name, units=ref_var.units)[0]
        except KeyError:
            continue
        row_list.append({
            "name": ref_var.name,
            "units": ref_var.units,
            "ref_value": ref_var.value[0],
            "value": value,
        })

    df = pd.DataFrame(row_list)
    df["rel_delta"] = (df.value - df.ref_value) / df.ref_value
    df["rel_delta"][(df.ref_value == 0) & (abs(df.value) <= 1e-10)] = 0.0
    df["abs_rel_delta"] = np.abs(df.rel_delta)

    pd.set_option("display.max_rows", None)
    pd.set_option("display.max_columns", None)
    pd.set_option("display.width", 1000)
    pd.set_option("display.max_colwidth", 120)
    print(df.sort_values(by=["abs_rel_delta"]))

    if vars_to_check is not None:
        for name in vars_to_check:
            row = df.loc[df.name == name]
            assert_allclose(row.ref_value, row.value, rtol=tolerance)
            # assert np.all(df.abs_rel_delta.loc[df.name == name] < tolerance)
    else:
        assert np.all(df.abs_rel_delta < tolerance)
def test_problem_definition(cleanup):
    """ Test conf definition from configuration files """

    # no input file
    conf = FASTOADProblemConfigurator()
    with pytest.raises(FASTConfigurationError) as exc_info:
        conf.load(
            pth.join(pth.dirname(__file__), "data", "missing_input_file.toml"))
    assert exc_info.value.missing_key == KEY_INPUT_FILE

    # no output file
    conf = FASTOADProblemConfigurator()
    with pytest.raises(FASTConfigurationError) as exc_info:
        conf.load(
            pth.join(pth.dirname(__file__), "data",
                     "missing_output_file.toml"))
    assert exc_info.value.missing_key == KEY_OUTPUT_FILE

    # Missing model definition
    conf = FASTOADProblemConfigurator()
    with pytest.raises(FASTConfigurationError) as exc_info:
        conf.load(pth.join(pth.dirname(__file__), "data",
                           "missing_model.toml"))
    assert exc_info.value.missing_section == TABLE_MODEL

    # Incorrect attribute
    conf = FASTOADProblemConfigurator()
    conf.load(pth.join(pth.dirname(__file__), "data",
                       "invalid_attribute.toml"))
    with pytest.raises(
            FASTConfigurationBadOpenMDAOInstructionError) as exc_info:
        problem = conf.get_problem(read_inputs=False)
    assert exc_info.value.key == "model.cycle.other_group.nonlinear_solver"

    # Reading of a minimal conf (model = ExplicitComponent)
    conf = FASTOADProblemConfigurator()
    conf.load(pth.join(pth.dirname(__file__), "data", "disc1.toml"))
    problem = conf.get_problem(read_inputs=False)
    assert isinstance(problem.model.system, om.ExplicitComponent)

    # Reading of correct conf definition
    conf = FASTOADProblemConfigurator()
    conf.load(pth.join(pth.dirname(__file__), "data", "valid_sellar.toml"))
    assert conf.input_file_path == pth.join(RESULTS_FOLDER_PATH, "inputs.xml")
    assert conf.output_file_path == pth.join(RESULTS_FOLDER_PATH,
                                             "outputs.xml")

    # Just running these methods to check there is no crash. As simple assemblies of
    # other methods, their results should already be unit-tested.
    conf.write_needed_inputs()
    problem = conf.get_problem(read_inputs=True)

    problem.setup()
    assert isinstance(problem.model.cycle, om.Group)
    assert isinstance(problem.model.cycle.disc1, om.ExplicitComponent)
    assert isinstance(problem.model.cycle.disc2, om.ExplicitComponent)
    assert isinstance(problem.model.functions, om.ExplicitComponent)

    assert isinstance(problem.driver, om.ScipyOptimizeDriver)
    assert problem.driver.options["optimizer"] == "SLSQP"
    assert isinstance(problem.model.cycle.nonlinear_solver,
                      om.NonlinearBlockGS)

    problem.run_driver()

    problem.run_model()
    assert np.isnan(problem["f"])
Example #15
0
def run_non_regression_test(
    conf_file,
    legacy_result_file,
    result_dir,
    use_xfoil=False,
    global_tolerance=1e-2,
    vars_to_check=None,
    specific_tolerance=5.0e-3,
    check_weight_perfo_loop=True,
):
    """
    Convenience function for non regression tests
    :param conf_file: FAST-OAD configuration file
    :param legacy_result_file: reference data for inputs and outputs
    :param result_dir: relative name, folder will be in RESULTS_FOLDER_PATH
    :param use_xfoil: if True, XFOIL computation will be activated
    :param vars_to_check: variables that will be concerned by specific_tolerance
    :param specific_tolerance: test will fail if absolute relative error between computed and
                               reference values is beyond this value for variables in vars_to_check
    :param global_tolerance: test will fail if absolute relative error between computed and
                             reference values is beyond this value for ANY variable
    :param check_weight_perfo_loop: if True, consistency of weights will be checked
    """
    results_folder_path = pth.join(RESULTS_FOLDER_PATH, result_dir)
    configuration_file_path = pth.join(results_folder_path, conf_file)

    # Copy of configuration file and generation of problem instance ------------------
    api.generate_configuration_file(configuration_file_path)  # just ensure folders are created...
    shutil.copy(pth.join(DATA_FOLDER_PATH, conf_file), configuration_file_path)
    configurator = FASTOADProblemConfigurator(configuration_file_path)
    configurator._set_configuration_modifier(XFOILConfigurator(use_xfoil))

    # Generation of inputs ----------------------------------------
    ref_inputs = pth.join(DATA_FOLDER_PATH, legacy_result_file)
    configurator.write_needed_inputs(ref_inputs)

    # Get problem with inputs -------------------------------------
    problem = configurator.get_problem(read_inputs=True)
    problem.setup()

    # Run model ---------------------------------------------------------------
    problem.run_model()
    problem.write_outputs()

    om.view_connections(
        problem, outfile=pth.join(results_folder_path, "connections.html"), show_browser=False
    )

    if check_weight_perfo_loop:
        _check_weight_performance_loop(problem)

    ref_data = DataFile(pth.join(DATA_FOLDER_PATH, legacy_result_file))

    row_list = []
    for ref_var in ref_data:
        try:
            value = problem.get_val(ref_var.name, units=ref_var.units)[0]
        except KeyError:
            continue
        row_list.append(
            {
                "name": ref_var.name,
                "units": ref_var.units,
                "ref_value": ref_var.value[0],
                "value": value,
            }
        )

    df = pd.DataFrame(row_list)
    df["rel_delta"] = (df.value - df.ref_value) / df.ref_value
    df["rel_delta"][(df.ref_value == 0) & (abs(df.value) <= 1e-10)] = 0.0
    df["abs_rel_delta"] = np.abs(df.rel_delta)

    pd.set_option("display.max_rows", None)
    pd.set_option("display.max_columns", None)
    pd.set_option("display.width", 1000)
    pd.set_option("display.max_colwidth", 120)
    print(df.sort_values(by=["abs_rel_delta"]))

    if vars_to_check is not None:
        for name in vars_to_check:
            assert_allclose(df.ref_value, df.value, rtol=global_tolerance)
            row = df.loc[df.name == name]
            assert_allclose(row.ref_value, row.value, rtol=specific_tolerance)
    else:
        assert np.all(df.abs_rel_delta < specific_tolerance)
Example #16
0
def list_variables(
    configuration_file_path: str,
    out: Union[IO, str] = sys.stdout,
    overwrite: bool = False,
    force_text_output: bool = False,
):
    """
    Writes list of system outputs for the :class:`FASTOADProblem` specified in configuration_file_path.

    List is generally written as text. It can be displayed as a scrollable table view if:
    - function is used in an interactive IPython shell
    - out == sys.stdout
    - force_text_output == False

    :param configuration_file_path:
    :param out: the output stream or a path for the output file
    :param overwrite: if True and out parameter is a file path, the file will be written even if one already
                      exists
    :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 parameter is a file path and the file exists
    """
    conf = FASTOADProblemConfigurator(configuration_file_path)
    problem = conf.get_problem()

    # Extracting inputs and outputs
    variables = VariableList.from_problem(problem, promoted_only=False)
    variables.sort(key=lambda var: var.name)
    input_variables = VariableList([var for var in variables if var.is_input])
    output_variables = VariableList(
        [var for var in variables if not var.is_input])

    if isinstance(out, str):
        if not overwrite and pth.exists(out):
            raise FastFileExistsError(
                "File %s not written because it already exists. "
                "Use overwrite=True to bypass." % out,
                out,
            )
        make_parent_dir(out)
        out_file = open(out, "w")
        table_width = MAX_TABLE_WIDTH
    else:
        if out == sys.stdout and InteractiveShell.initialized(
        ) and not force_text_output:
            # Here we display the variable list as VariableViewer in a notebook
            for var in input_variables:
                var.metadata["I/O"] = "IN"
            for var in output_variables:
                var.metadata["I/O"] = "OUT"

            df = ((input_variables + output_variables).to_dataframe()[[
                "I/O", "name", "desc"
            ]].rename(columns={
                "name": "Name",
                "desc": "Description"
            }))
            display(HTML(df.to_html()))
            return

        # Here we continue with text output
        out_file = out
        table_width = min(get_terminal_size().columns, MAX_TABLE_WIDTH) - 1

    pd.set_option("display.max_colwidth", 1000)
    max_name_length = np.max([
        len(name)
        for name in input_variables.names() + output_variables.names()
    ])
    description_text_width = table_width - max_name_length - 2

    def _write_variables(out_f, variables):
        """Writes variables and their description as a pandas DataFrame"""
        df = variables.to_dataframe()

        # Create a new Series where description are wrapped on several lines if needed.
        # Each line becomes an element of the Series
        df["desc"] = [
            "\n".join(tw.wrap(s, description_text_width)) for s in df["desc"]
        ]
        new_desc = df.desc.str.split("\n", expand=True).stack()

        # Create a Series for name that will match new_desc Series. Variable name will be in front of
        # first line of description. An empty string will be in front of other lines.
        new_name = [
            df.name.loc[i] if j == 0 else "" for i, j in new_desc.index
        ]

        # Create the DataFrame that will be displayed
        new_df = pd.DataFrame({"NAME": new_name, "DESCRIPTION": new_desc})

        out_f.write(
            new_df.to_string(
                index=False,
                columns=["NAME", "DESCRIPTION"],
                justify="center",
                formatters={  # Formatters are needed for enforcing left justification
                    "NAME": ("{:%s}" % max_name_length).format,
                    "DESCRIPTION": ("{:%s}" % description_text_width).format,
                },
            )
        )
        out_file.write("\n")

    def _write_text_with_line(txt: str, line_length: int):
        """ Writes a line of given length with provided text inside """
        out_file.write("-" + txt + "-" * (line_length - 1 - len(txt)) + "\n")

    # Inputs
    _write_text_with_line(" INPUTS OF THE PROBLEM ", table_width)
    _write_variables(out_file, input_variables)

    # Outputs
    out_file.write("\n")
    _write_text_with_line(" OUTPUTS OF THE PROBLEM ", table_width)
    _write_variables(out_file, output_variables)
    _write_text_with_line("", table_width)

    if isinstance(out, str):
        out_file.close()
        _LOGGER.info("Output list written in %s", out_file)