Source code for gemseo.utils.xdsm_to_pdf

# Copyright 2021 IRT Saint Exupéry, https://www.irt-saintexupery.com
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License version 3 as published by the Free Software Foundation.
#
# 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
# Contributors:
#    INITIAL AUTHORS - API and implementation and/or documentation
#       :author: Matthias De Lozzo
#    OTHER AUTHORS   - MACROSCOPIC CHANGES
"""Provide routines for XDSM and tikz."""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any

from pyxdsm.XDSM import XDSM

if TYPE_CHECKING:
    from collections.abc import Sequence


[docs] class XDSMToPDFConverter: """Convert an XDSM to a PDF file with tikz and latex.""" def __init__(self) -> None: # noqa: D107 self.__xdsm = XDSM()
[docs] def convert( self, xdsm_data: dict[str, Any], directory_path: str | Path, file_name: str, scenario: str, build: bool = True, cleanup: bool = True, batchmode: bool = True, ) -> None: """Convert a dictionary representation of a XDSM into a pdf. Args: xdsm_data: The XDSM representation. directory_path: The path to the output directory where the pdf is generated. file_name: The name of the output file. scenario: The name of the scenario. build: Whether the standalone pdf of the XDSM will be built. cleanup: Whether pdflatex built files will be cleaned up after build is complete. batchmode: Whether pdflatex is run in `batchmode`. """ workflow = xdsm_data[scenario]["workflow"][1] numbers = {} self.__get_numbers(numbers, workflow) self.__add_nodes(numbers, xdsm_data[scenario]["nodes"]) self.__add_edges(xdsm_data[scenario]["edges"]) self.__add_processes(workflow) self.__xdsm.write( file_name, outdir=str(directory_path), build=build, cleanup=cleanup, quiet=batchmode, ) if build and ( not (Path(directory_path) / file_name).with_suffix(".pdf").exists() ): msg = ( "Something went wrong during the Latex compilation," " as xdsm.pdf has not been generated. Please have a look at the" " Latex log file to investigate the root cause of the error." ) raise RuntimeError(msg) # Build XDSM for sub-scenarios if scenario == "root": subscenarios = [key for key in xdsm_data if "scn-" in key] for subscenario in subscenarios: sub_xdsm = XDSMToPDFConverter() sub_xdsm.convert( xdsm_data, directory_path, file_name=f"{file_name}_{subscenario}", scenario=subscenario, build=build, cleanup=cleanup, batchmode=batchmode, )
def __add_processes( self, workflow: list[Any], prev: str | list[str] | None = None ) -> None: """Create the pyXDSM processes from a given workflow in a recursive way. Args: workflow: The workflow component of the dictionary storing the XDSM. prev: The name of the previous node. """ last_node = prev for system in workflow: if isinstance(system, str): # system is a node if isinstance(last_node, list): # case of previous parallel nodes for node in last_node: self.__xdsm.add_process([node, system]) else: if last_node: self.__xdsm.add_process([last_node, system]) last_node = system elif isinstance(system, list): # system is a group of nodes (MDA, chain...) self.__add_processes(system, last_node) elif isinstance(system, dict): # system is parallel last_nodes = [] for sub_sys in system["parallel"]: # add a sub-process for each parallel element if isinstance(sub_sys, str): sub_workflow = [last_node, sub_sys] elif isinstance(sub_sys, list): sub_workflow = [last_node, *sub_sys] self.__add_processes(sub_workflow) if isinstance(sub_sys, list): # take the last node when it is a chain (e.g. [d1, d2, d2...]), # but take the first node when it is an iterative struct # (e.g. MDA, [d1, [d2, d3]]) last_nodes.append( sub_sys[0] if isinstance(sub_sys[-1], list) else sub_sys[-1] ) else: last_nodes.append(sub_sys) last_node = last_nodes # return loop form last node to previous node if prev: last_node = [last_node] if not isinstance(last_node, list) else last_node for node in last_node: self.__xdsm.add_process([node, prev]) def __get_numbers( self, numbers: Sequence[int], nodes, current: int = 0, end: int = 0, following: int = 1, ): """Give number to the different nodes in a recursive way. Args: numbers: dictionary. nodes (list): workflow component of the dictionary storing the XDSM. current (int): current step index. end (int): end-loop step index. following (int): following step index. Returns: The following step index. """ prev_node = "undefined" for node in nodes: if isinstance(node, str): current = following following = current + 1 end = current current_l = [current] if node in list(numbers.keys()): current_l = numbers[node]["current"] + current_l numbers[node] = {"current": current_l, "next": following, "end": end} prev_node = node elif isinstance(node, list): following = self.__get_numbers(numbers, node, current, end, following) numbers[prev_node]["end"] = following following += 1 elif isinstance(node, dict): init_following = following followings = [] for sub_node in node["parallel"]: if not isinstance(sub_node, list): followings.append( self.__get_numbers( numbers, [sub_node], current, end, init_following ) ) else: followings.append( self.__get_numbers( numbers, sub_node, current, end, init_following ) ) following = max(followings) return following def __add_nodes(self, numbers: Sequence[int], nodes) -> None: """Add the different nodes, called 'systems', in the XDSM.""" for node in nodes: name = ",".join([ str(current) for current in numbers[node["id"]]["current"] ]) name_1 = name + ",{}-{}:".format( str(numbers[node["id"]]["end"]), str(numbers[node["id"]]["next"]) ) name_2 = name + ":" if node["type"] == "optimization": node_type = "Optimization" name = name_1 elif node["type"] == "lp_optimization": node_type = "LP_Optimization" name = name_1 elif node["type"] == "doe": node_type = "DOE" name = name_1 elif node["type"] == "mda": node_type = "MDA" name = name_1 elif node["type"] == "mdo": node_type = "SubOptimization" name = name_2 elif node["type"] in {"analysis", "function"}: node_type = "Function" name = name_2 elif node["type"] == "metamodel": node_type = "Metamodel" name = name_2 node_replaced = node["name"] escaped_characters = ["_", "$", "&", "{", "}", "%"] for char in escaped_characters: node_replaced = node_replaced.replace(char, rf"\{char}") name = name + node_replaced self.__xdsm.add_system(node["id"], node_type, r"\text{" + name + "}") def __add_edges(self, edges) -> None: """Add the edges called connections, inputs, outputs to the XDSM.""" for edge in edges: old_names = edge["name"].split(",") names = [ name.replace("_", r"\_").replace("(0)", "{(0)}") for name in old_names ] if len(names) > 2: names = f"{', '.join(names[:2])}..., ({len(names)})" else: names = ", ".join(names) if edge["to"] == "_U_": self.__xdsm.add_output(edge["from"], r"" + names + "") elif edge["from"] != "_U_": self.__xdsm.connect(edge["from"], edge["to"], r"" + names + "") elif edge["from"] == "_U_": self.__xdsm.add_input("Opt", r"" + names + "")
[docs] def xdsm_data_to_pdf( xdsm_data: dict[str, Any], directory_path: Path | str, file_name: str = "xdsm", scenario: str = "root", pdf_build: bool = True, pdf_cleanup: bool = True, pdf_batchmode: bool = True, ) -> None: """Convert a dictionary representation of a XDSM to a pdf. Args: xdsm_data: The XDSM representation. directory_path: The output directory where the pdf is generated. file_name: The output file name (without extension). scenario: The name of the scenario name. pdf_build: Whether the standalone pdf of the XDSM will be built. pdf_cleanup: Whether pdflatex built files will be cleaned up after build is complete. pdf_batchmode: Whether pdflatex is run in `batchmode`. """ converter = XDSMToPDFConverter() converter.convert( xdsm_data, str(directory_path), file_name, scenario, pdf_build, pdf_cleanup, pdf_batchmode, )