├── pyxdsm ├── __init__.py ├── diagram_styles.tex ├── matrix_eqn.py └── XDSM.py ├── .github ├── CODEOWNERS └── workflows │ └── test.yaml ├── doc ├── requirements.txt ├── images │ ├── mdf.png │ ├── kitchen_sink.png │ └── matrix_eqn.png ├── API.rst ├── install.rst ├── Makefile ├── make.bat ├── conf.py ├── examples.rst └── index.rst ├── .git-blame-ignore-revs ├── .gitignore ├── .readthedocs.yaml ├── LICENSE.txt ├── examples ├── mdf.py ├── mat_eqn.py └── kitchen_sink.py ├── setup.py ├── README.md └── tests └── test_xdsm.py /pyxdsm/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "2.3.1" 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @mdolab/pyxdsm_maintainers 2 | -------------------------------------------------------------------------------- /doc/requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc 2 | sphinx_mdolab_theme 3 | -------------------------------------------------------------------------------- /doc/images/mdf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/pyXDSM/HEAD/doc/images/mdf.png -------------------------------------------------------------------------------- /doc/images/kitchen_sink.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/pyXDSM/HEAD/doc/images/kitchen_sink.png -------------------------------------------------------------------------------- /doc/images/matrix_eqn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mdolab/pyXDSM/HEAD/doc/images/matrix_eqn.png -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Switch to pre-commit/ruff formatting 2 | 8016e956dcf0252cb76d44d72ec82e3db6744016 3 | -------------------------------------------------------------------------------- /doc/API.rst: -------------------------------------------------------------------------------- 1 | .. _pyXDSM_API: 2 | 3 | pyXDSM API 4 | ---------- 5 | .. currentmodule:: pyxdsm.XDSM 6 | 7 | .. autoclass:: pyxdsm.XDSM.XDSM 8 | :members: 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | examples/* 2 | !examples/*.py 3 | 4 | *.so 5 | *.o 6 | *.pyc 7 | *~ 8 | .*~ 9 | *.orig 10 | *.mod 11 | *.pyf 12 | *.a 13 | *.dat 14 | *.mk 15 | *.xyz 16 | *.phy 17 | *.fmt 18 | *.lay 19 | *.plt 20 | _build* 21 | *.egg-info 22 | *.vscode 23 | .pre-commit-config.yaml 24 | ruff.toml 25 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | .. _pyXDSM_install: 2 | 3 | Installation 4 | ============ 5 | 6 | This package is available on `pyPI` and can be installed using: 7 | 8 | .. code-block:: bash 9 | 10 | pip install pyxdsm 11 | 12 | 13 | Alternatively, clone this repo or download the zip and unzip it, then: 14 | 15 | .. code-block:: bash 16 | 17 | cd pyxdsm 18 | pip install . 19 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | version: 2 6 | 7 | build: 8 | os: ubuntu-22.04 9 | tools: 10 | python: "3.11" 11 | 12 | sphinx: 13 | configuration: doc/conf.py 14 | 15 | python: 16 | install: 17 | - requirements: doc/requirements.txt 18 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); 2 | you may not use this software except in compliance with the License. 3 | You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | See the License for the specific language governing permissions and 11 | limitations under the License. 12 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | from sphinx_mdolab_theme.config import * 2 | 3 | # -- Path setup -------------------------------------------------------------- 4 | 5 | # If extensions (or modules to document with autodoc) are in another directory, 6 | # add these directories to sys.path here. If the directory is relative to the 7 | # documentation root, use os.path.abspath to make it absolute, like shown here. 8 | 9 | import os 10 | import sys 11 | 12 | sys.path.insert(0, os.path.abspath("../")) 13 | 14 | # -- Project information ----------------------------------------------------- 15 | 16 | project = "pyXDSM" 17 | 18 | # -- General configuration --------------------------------------------------- 19 | 20 | # Add any Sphinx extension module names here, as strings. They can be 21 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 22 | # ones. 23 | extensions.extend(["numpydoc"]) 24 | numpydoc_show_class_members = False 25 | 26 | # mock import for autodoc 27 | autodoc_mock_imports = ["numpy"] 28 | -------------------------------------------------------------------------------- /examples/mdf.py: -------------------------------------------------------------------------------- 1 | from pyxdsm.XDSM import XDSM, OPT, SOLVER, FUNC, LEFT 2 | 3 | # Change `use_sfmath` to False to use computer modern 4 | x = XDSM(use_sfmath=True) 5 | 6 | x.add_system("opt", OPT, r"\text{Optimizer}") 7 | x.add_system("solver", SOLVER, r"\text{Newton}") 8 | x.add_system("D1", FUNC, "D_1") 9 | x.add_system("D2", FUNC, "D_2") 10 | x.add_system("F", FUNC, "F") 11 | x.add_system("G", FUNC, "G") 12 | 13 | x.connect("opt", "D1", "x, z") 14 | x.connect("opt", "D2", "z") 15 | x.connect("opt", "F", "x, z") 16 | x.connect("solver", "D1", "y_2") 17 | x.connect("solver", "D2", "y_1") 18 | x.connect("D1", "solver", r"\mathcal{R}(y_1)") 19 | x.connect("solver", "F", "y_1, y_2") 20 | x.connect("D2", "solver", r"\mathcal{R}(y_2)") 21 | x.connect("solver", "G", "y_1, y_2") 22 | 23 | x.connect("F", "opt", "f") 24 | x.connect("G", "opt", "g") 25 | 26 | x.add_output("opt", "x^*, z^*", side=LEFT) 27 | x.add_output("D1", "y_1^*", side=LEFT) 28 | x.add_output("D2", "y_2^*", side=LEFT) 29 | x.add_output("F", "f^*", side=LEFT) 30 | x.add_output("G", "g^*", side=LEFT) 31 | x.write("mdf") 32 | -------------------------------------------------------------------------------- /doc/examples.rst: -------------------------------------------------------------------------------- 1 | .. _pyXDSM_examples: 2 | 3 | Examples 4 | ======== 5 | Here is a simple example. 6 | There are some other more advanced things you can do as well. 7 | Check out the `examples folder `_ for more complex scripts. 8 | 9 | .. literalinclude:: ../examples/mdf.py 10 | 11 | 12 | This will output ``mdf.tex``, a standalone tex document that (by default) is also compiled to ``mdf.pdf``, shown below: 13 | 14 | .. image:: images/mdf.png 15 | :scale: 30 16 | 17 | 18 | More complicated example 19 | ------------------------ 20 | 21 | Here is an example that uses a whole bunch of the more advanced features in ``pyXDSM``. 22 | 23 | .. image:: images/kitchen_sink.png 24 | :scale: 30 25 | 26 | It is mostly just a reference for all the customizations you can do. 27 | The code for this diagram is `provided here `_ 28 | 29 | 30 | Block matrix equation 31 | --------------------- 32 | 33 | ``pyXDSM`` can also generate a figure of a block matrix equation. 34 | An example script is available `here `_. 35 | 36 | .. image:: images/matrix_eqn.png 37 | :scale: 15 38 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: pyxdsm 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: 7 | - v*.*.* 8 | pull_request: 9 | branches: [main] 10 | 11 | jobs: 12 | format-and-lint: 13 | uses: mdolab/.github/.github/workflows/format-and-lint.yaml@main 14 | 15 | test: 16 | runs-on: ubuntu-latest 17 | strategy: 18 | matrix: 19 | python-version: [3.9, 3.11] 20 | numpy-version: ["~=1.21.0", "~=1.26.0"] 21 | exclude: 22 | - python-version: 3.11 23 | numpy-version: "~=1.21.0" 24 | steps: 25 | - uses: actions/checkout@v5 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v4 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | sudo apt update 33 | sudo apt-get install texlive-pictures texlive-latex-extra -y 34 | pip install -U pip wheel 35 | pip install testflo numpy${{ matrix.numpy-version }} 36 | pip install . 37 | - name: Test examples 38 | run: | 39 | testflo . -v 40 | 41 | # --- publish to PyPI 42 | pypi: 43 | needs: [test, format-and-lint] 44 | uses: mdolab/.github/.github/workflows/pypi.yaml@main 45 | secrets: inherit 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | import re 3 | from os import path 4 | 5 | __version__ = re.findall( 6 | r"""__version__ = ["']+([0-9\.]*)["']+""", 7 | open("pyxdsm/__init__.py").read(), 8 | )[0] 9 | 10 | this_directory = path.abspath(path.dirname(__file__)) 11 | with open(path.join(this_directory, "README.md"), encoding="utf-8") as f: 12 | long_description = f.read() 13 | 14 | setup( 15 | name="pyXDSM", 16 | version=__version__, 17 | description="Python script to generate PDF XDSM diagrams using TikZ and LaTeX", 18 | long_description=long_description, 19 | long_description_content_type="text/markdown", 20 | keywords="optimization multidisciplinary multi-disciplinary analysis n2 xdsm", 21 | author="", 22 | author_email="", 23 | url="https://github.com/mdolab/pyXDSM", 24 | license="Apache License Version 2.0", 25 | packages=[ 26 | "pyxdsm", 27 | ], 28 | package_data={"pyxdsm": ["*.tex"]}, 29 | install_requires=["numpy>=1.21"], 30 | python_requires=">=3", 31 | classifiers=[ 32 | "Operating System :: OS Independent", 33 | "Programming Language :: Python", 34 | "Topic :: Scientific/Engineering", 35 | "Programming Language :: Python", 36 | "Programming Language :: Python :: 3.6", 37 | "Programming Language :: Python :: 3.7", 38 | "Programming Language :: Python :: 3.8", 39 | ], 40 | ) 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyXDSM 2 | [![Build Status](https://github.com/mdolab/pyXDSM/actions/workflows/test.yaml/badge.svg)](https://github.com/mdolab/pyXDSM/actions/workflows/test.yaml) 3 | [![Documentation Status](https://readthedocs.com/projects/mdolab-pyxdsm/badge/?version=latest)](https://mdolab-pyxdsm.readthedocs-hosted.com/?badge=latest) 4 | [![PyPI](https://img.shields.io/pypi/v/pyxdsm)](https://pypi.org/project/pyXDSM/) 5 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/pyXDSM) 6 | 7 | A python library for generating publication quality PDF XDSM diagrams. 8 | This library is a thin wrapper that uses the TikZ library and LaTeX to build the PDFs. 9 | 10 | # Documentation 11 | 12 | Please see the [documentation](https://mdolab-pyxdsm.readthedocs-hosted.com) for installation details and API documentation. 13 | 14 | To locally build the documentation, enter the `doc` folder and enter `make html` in the terminal. 15 | You can then view the built documentation in the `_build` folder. 16 | 17 | ### XDSM diagram 18 | ![XDSM of MDF](doc/images/mdf.png) 19 | 20 | ### Block matrix equation 21 | ![Block matrix equation](doc/images/matrix_eqn.png) 22 | 23 | ## Citation 24 | Please cite the [paper by Lambe and Martins](http://mdolab.engin.umich.edu/bibliography/Lambe2012a.html) when using XDSM. 25 | Here is the bibtex entry for that paper: 26 | 27 | @article {Lambe2012, 28 | title = {Extensions to the Design Structure Matrix for the Description of Multidisciplinary Design, Analysis, and Optimization Processes}, 29 | journal = {Structural and Multidisciplinary Optimization}, 30 | volume = {46}, 31 | year = {2012}, 32 | pages = {273-284}, 33 | doi = {10.1007/s00158-012-0763-y}, 34 | author = {Andrew B. Lambe and Joaquim R. R. A. Martins} 35 | } 36 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | .. pyXDSM documentation master file, created by 2 | sphinx-quickstart on Fri Oct 16 11:16:12 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. _pyXDSM: 7 | 8 | ====== 9 | pyXDSM 10 | ====== 11 | 12 | Introduction 13 | ============ 14 | 15 | ``pyXDSM`` is a python library to generate XDSM diagrams in high-quality pdf format. 16 | 17 | What is XDSM? 18 | ------------- 19 | 20 | The eXtended Design Structure Matrix (XDSM) is a graphical language for describing the movement of data and the execution sequence for a multidisciplinary optimization problem. 21 | You can read the `paper by Lambe and Martins `_ for all the details. 22 | 23 | How to use it 24 | ============= 25 | 26 | The following pages provide detailed info on how to use the python library: 27 | 28 | .. toctree:: 29 | :caption: User guide 30 | :maxdepth: 1 31 | 32 | install 33 | examples 34 | API 35 | 36 | TikZ and LaTeX 37 | -------------- 38 | You need to install these libraries for pyXDSM to work. See the `install guide `_ for your platform. 39 | 40 | Embedding the diagram directly in LaTeX 41 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 42 | 43 | In addition, the file, ``mdf.tikz``, can be embedded in another `.tex` file using 44 | the ``\input`` command: 45 | 46 | .. code-block:: latex 47 | 48 | \begin{figure} 49 | \caption{Example of an MDF XDSM.} 50 | \centering 51 | \input{mdf.tikz} 52 | \label{fig:xdsm} 53 | \end{figure} 54 | 55 | 56 | The following is required to be in the preamble of the document: 57 | 58 | .. code-block:: latex 59 | 60 | \usepackage{geometry} 61 | \usepackage{amsfonts} 62 | \usepackage{amsmath} 63 | \usepackage{amssymb} 64 | \usepackage{tikz} 65 | 66 | \usetikzlibrary{arrows,chains,positioning,scopes,shapes.geometric,shapes.misc,shadows} 67 | -------------------------------------------------------------------------------- /examples/mat_eqn.py: -------------------------------------------------------------------------------- 1 | from pyxdsm.matrix_eqn import MatrixEquation 2 | 3 | ################################ 4 | # define the system 5 | ################################ 6 | 7 | lin_system = MatrixEquation() 8 | 9 | lin_system.add_variable(1, size=1, text="a") 10 | lin_system.add_variable(2, size=1, text="b") 11 | 12 | lin_system.add_variable(3, size=1, text="c") 13 | lin_system.add_variable(4, size=2) 14 | lin_system.add_variable(5, size=2) 15 | 16 | lin_system.add_variable(6, size=1, text="d") 17 | lin_system.add_variable(7, size=2) 18 | lin_system.add_variable(8, size=2) 19 | 20 | lin_system.add_variable(9, size=1, text="e") 21 | lin_system.add_variable(10, size=2) 22 | lin_system.add_variable(11, size=2) 23 | 24 | # variable identifiers can be any hashable object 25 | lin_system.add_variable("f", size=1, text="f") 26 | 27 | lin_system.connect(1, [4, 5, 7, 8, 10, 11]) 28 | lin_system.connect(2, [4, 5, 7, 8, 10, 11]) 29 | 30 | lin_system.connect(3, 4) 31 | lin_system.connect(4, 5) 32 | lin_system.connect(5, 4) 33 | 34 | lin_system.connect(6, 7) 35 | lin_system.connect(7, 8) 36 | lin_system.connect(8, 7) 37 | 38 | lin_system.connect(9, 10) 39 | lin_system.connect(10, 11) 40 | lin_system.connect(11, 10) 41 | 42 | lin_system.connect(11, "f") 43 | 44 | ################################ 45 | # setup the equation 46 | ################################ 47 | lin_system.jacobian() 48 | lin_system.spacer() 49 | lin_system.vector(base_color="red", highlight=[0, 0, 2, 2, 2, 0, 0, 0, 0, 0, 0, 0]) 50 | lin_system.spacer() 51 | lin_system.vector(base_color="red", highlight=[0, 0, 0, 0, 0, 2, 2, 2, 0, 0, 0, 0]) 52 | lin_system.spacer() 53 | lin_system.vector(base_color="red", highlight=[0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 2, 2]) 54 | lin_system.spacer() 55 | lin_system.operator("=") 56 | lin_system.vector(base_color="green", highlight=[1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1]) 57 | lin_system.spacer() 58 | lin_system.vector(base_color="green", highlight=[1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1]) 59 | lin_system.spacer() 60 | lin_system.vector(base_color="green", highlight=[1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1]) 61 | lin_system.spacer() 62 | 63 | lin_system.write("mat_eqn_example") 64 | -------------------------------------------------------------------------------- /examples/kitchen_sink.py: -------------------------------------------------------------------------------- 1 | from pyxdsm.XDSM import ( 2 | XDSM, 3 | OPT, 4 | SUBOPT, 5 | SOLVER, 6 | DOE, 7 | IFUNC, 8 | FUNC, 9 | GROUP, 10 | IGROUP, 11 | METAMODEL, 12 | LEFT, 13 | RIGHT, 14 | ) 15 | 16 | x = XDSM( 17 | auto_fade={ 18 | # "inputs": "none", 19 | "outputs": "connected", 20 | "connections": "outgoing", 21 | # "processes": "none", 22 | } 23 | ) 24 | 25 | x.add_system("opt", OPT, r"\text{Optimizer}") 26 | x.add_system("DOE", DOE, r"\text{DOE}") 27 | x.add_system("MDA", SOLVER, r"\text{Newton}") 28 | x.add_system("D1", FUNC, "D_1") 29 | 30 | # can fade out blocks to allow for emphasis on sub-sections of XDSM 31 | x.add_system("D2", IFUNC, "D_2", faded=True) 32 | 33 | x.add_system("D3", IFUNC, "D_3") 34 | x.add_system("subopt", SUBOPT, "SubOpt", faded=True) 35 | x.add_system("G1", GROUP, "G_1") 36 | x.add_system("G2", IGROUP, "G_2") 37 | x.add_system("MM", METAMODEL, "MM") 38 | 39 | # if you give the label as a list or tuple, it splits it onto multiple lines 40 | x.add_system("F", FUNC, ("F", r"\text{Functional}")) 41 | 42 | # stacked can be used to represent multiple instances that can be run in parallel 43 | x.add_system("H", FUNC, "H", stack=True) 44 | 45 | x.add_process( 46 | ["opt", "DOE", "MDA", "D1", "D2", "subopt", "G1", "G2", "MM", "F", "H", "opt"], 47 | arrow=True, 48 | ) 49 | 50 | x.connect("opt", "D1", ["x", "z", "y_2"], label_width=2) 51 | x.connect("opt", "D2", ["z", "y_1"]) 52 | x.connect("opt", "D3", "z, y_1") 53 | x.connect("opt", "subopt", "z, y_1") 54 | x.connect("D3", "G1", "y_3") 55 | x.connect("subopt", "G1", "z_2") 56 | x.connect("subopt", "G2", "z_2") 57 | x.connect("subopt", "MM", "z_2") 58 | x.connect("subopt", "F", "f") 59 | x.connect("MM", "subopt", "f") 60 | x.connect("opt", "G2", "z") 61 | x.connect("opt", "F", "x, z") 62 | x.connect("opt", "F", "y_1, y_2") 63 | 64 | # you can also stack variables 65 | x.connect("opt", "H", "y_1, y_2", stack=True) 66 | 67 | x.connect("D1", "opt", r"\mathcal{R}(y_1)") 68 | x.connect("D2", "opt", r"\mathcal{R}(y_2)") 69 | 70 | x.connect("F", "opt", "f") 71 | x.connect("H", "opt", "h", stack=True) 72 | 73 | # can specify inputs to represent external information coming into the XDSM 74 | x.add_input("D1", "P_1") 75 | x.add_input("D2", "P_2") 76 | x.add_input("opt", r"x_0", stack=True) 77 | 78 | # can put outputs on the left or right sides 79 | x.add_output("opt", r"x^*, z^*", side=RIGHT) 80 | x.add_output("D1", r"y_1^*", side=LEFT) 81 | x.add_output("D2", r"y_2^*", side=LEFT) 82 | x.add_output("F", r"f^*", side=RIGHT) 83 | x.add_output("H", r"h^*", side=RIGHT) 84 | x.add_output("opt", r"y^*", side=LEFT) 85 | 86 | x.add_process(["output_opt", "opt", "left_output_opt"]) 87 | 88 | x.write("kitchen_sink", cleanup=False) 89 | x.write_sys_specs("sink_specs") 90 | -------------------------------------------------------------------------------- /pyxdsm/diagram_styles.tex: -------------------------------------------------------------------------------- 1 | % Define all the styles used to produce XDSMs for MDO 2 | 3 | % Tableau 20 color palette, taken from 4 | % https://jrnold.github.io/ggthemes/reference/tableau_color_pal.html 5 | % we use the lighter variants here with 80% opacity 6 | % Blue 7 | \definecolor{red}{HTML}{A0CBE8} 8 | % Orange 9 | \definecolor{orange}{HTML}{FFBE7D} 10 | % Cyan 11 | \definecolor{cyan}{HTML}{86BCB6} 12 | % Green 13 | \definecolor{green}{HTML}{8CD17D} 14 | % Yellow 15 | \definecolor{yellow}{HTML}{F1CE63} 16 | % Salmon 17 | \definecolor{salmon}{HTML}{FF9D9A} 18 | 19 | \tikzstyle{every node}=[font=\sffamily,align=center] 20 | 21 | \newcommand{\fillOpacity}{80} 22 | 23 | % Component shapes 24 | \newcommand{\compShape}{rectangle} 25 | \newcommand{\groupShape}{chamfered rectangle} 26 | \newcommand{\procShape}{rounded rectangle} 27 | 28 | % Colors 29 | \newcommand{\explicitColor}{green} 30 | \newcommand{\implicitColor}{salmon} 31 | \newcommand{\optimizationColor}{red} % also used by DOE 32 | 33 | % Component types 34 | \tikzstyle{Optimization} = [\procShape,draw,fill=\optimizationColor!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 35 | \tikzstyle{MDA} = [\procShape,draw,fill=orange!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 36 | \tikzstyle{DOE} = [\procShape,draw,fill=\optimizationColor!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 37 | \tikzstyle{SubOptimization} = [\groupShape,draw,fill=\optimizationColor!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 38 | \tikzstyle{Group} = [\groupShape,draw,fill=\explicitColor!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 39 | \tikzstyle{ImplicitGroup} = [\groupShape,draw,fill=\implicitColor!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 40 | \tikzstyle{Function} = [\compShape,draw,fill=\explicitColor!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 41 | \tikzstyle{ImplicitFunction} = [\compShape,draw,fill=\implicitColor!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 42 | \tikzstyle{Metamodel} = [\compShape,draw,fill=yellow!\fillOpacity,inner sep=6pt,minimum height=1cm,text badly centered] 43 | 44 | %% A simple command to give the repeated structure look for components and data 45 | \tikzstyle{stack} = [double copy shadow={shadow xshift=.75ex, shadow yshift=-.75ex}] 46 | %% A simple command to fade components and data, e.g. demonstrating a sequence of steps in an animation 47 | \tikzstyle{faded} = [draw=black!10,fill=white,text opacity=0.2] 48 | 49 | %% Simple fading commands for the lines 50 | \tikzstyle{fadeddata} = [color=black!20] 51 | \tikzstyle{fadedprocess} = [color=black!50] 52 | 53 | % Data types 54 | \newcommand{\dataRightAngle}{105} 55 | \newcommand{\dataLeftAngle}{75} 56 | 57 | \tikzstyle{DataInter} = [trapezium,trapezium left angle=\dataLeftAngle,trapezium right angle=\dataRightAngle,draw,fill=black!10] 58 | \tikzstyle{DataIO} = [trapezium,trapezium left angle=\dataLeftAngle,trapezium right angle=\dataRightAngle,draw,fill=white] 59 | 60 | % Edges 61 | \tikzstyle{DataLine} = [color=black!40,line width=5pt,line cap=rect] 62 | \tikzstyle{ProcessHV} = [-,line width=1pt,to path={-| (\tikztotarget)}] 63 | \tikzstyle{ProcessHVA} = [->,line width=1pt,to path={-| (\tikztotarget)}] 64 | \tikzstyle{ProcessTip} = [-,line width=1pt] 65 | \tikzstyle{ProcessTipA} = [->, line width=1pt] 66 | \tikzstyle{FadedProcessHV} = [-,line width=1pt,to path={-| (\tikztotarget)},color=black!30] 67 | \tikzstyle{FadedProcessHVA} = [->,line width=1pt,to path={-| (\tikztotarget)},color=black!30] 68 | \tikzstyle{FadedProcessTip} = [-,line width=1pt,color=black!30] 69 | \tikzstyle{FadedProcessTipA} = [->, line width=1pt,color=black!30] 70 | 71 | % Matrix options 72 | \tikzstyle{MatrixSetup} = [row sep=3mm, column sep=2mm] 73 | 74 | % Declare a background layer for showing node connections 75 | \pgfdeclarelayer{data} 76 | \pgfdeclarelayer{process} 77 | \pgfsetlayers{data,process,main} 78 | 79 | % A new command to split the component text over multiple lines 80 | 81 | \newcommand{\MultilineComponent}[2] 82 | { 83 | \begin{minipage}{#1} 84 | \begin{center} 85 | #2 86 | \end{center} 87 | \end{minipage} 88 | } 89 | 90 | \newcommand{\TwolineComponent}[3] 91 | { 92 | \begin{minipage}{#1} 93 | \begin{center} 94 | #2 \linebreak #3 95 | \end{center} 96 | \end{minipage} 97 | } 98 | 99 | \newcommand{\ThreelineComponent}[4] 100 | { 101 | \begin{minipage}{#1} 102 | \begin{center} 103 | #2 \linebreak #3 \linebreak #4 104 | \end{center} 105 | \end{minipage} 106 | } 107 | 108 | % A new command to split the component text over multiple columns 109 | \newcommand{\MultiColumnComponent}[5] 110 | { 111 | \begin{minipage}{#1} 112 | \begin{center} 113 | #2 \linebreak #3 114 | \end{center} 115 | \begin{minipage}{0.49\textwidth} 116 | \begin{center} 117 | #4 118 | \end{center} 119 | \end{minipage} 120 | \begin{minipage}{0.49\textwidth} 121 | \begin{center} 122 | #5 123 | \end{center} 124 | \end{minipage} 125 | \end{minipage} 126 | } 127 | 128 | \def\arraystretch{1.3} 129 | -------------------------------------------------------------------------------- /tests/test_xdsm.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import os 3 | import shutil 4 | import tempfile 5 | import subprocess 6 | from pyxdsm.XDSM import XDSM, OPT, FUNC, SOLVER, LEFT, RIGHT 7 | from numpy.distutils.exec_command import find_executable 8 | 9 | basedir = os.path.dirname(os.path.abspath(__file__)) 10 | 11 | 12 | def filter_lines(lns): 13 | # Empty lines are excluded. 14 | # Leading and trailing whitespaces are removed 15 | # Comments are removed. 16 | return [ln.strip() for ln in lns if ln.strip() and not ln.strip().startswith("%")] 17 | 18 | 19 | class TestXDSM(unittest.TestCase): 20 | def setUp(self): 21 | self.tempdir = tempfile.mkdtemp(prefix="testdir-") 22 | os.chdir(self.tempdir) 23 | 24 | def tearDown(self): 25 | os.chdir(basedir) 26 | 27 | try: 28 | shutil.rmtree(self.tempdir) 29 | except OSError: 30 | pass 31 | 32 | def test_examples(self): 33 | """ 34 | This test just builds the three examples, and assert that the output files exist. 35 | Unlike the other tests, this one requires LaTeX to be available. 36 | """ 37 | # we first copy the examples to the temp dir 38 | shutil.copytree(os.path.join(basedir, "../examples"), os.path.join(self.tempdir, "examples")) 39 | os.chdir(os.path.join(self.tempdir, "examples")) 40 | 41 | filenames = ["kitchen_sink", "mdf"] 42 | for f in filenames: 43 | subprocess.run(["python", f"{f}.py"], check=True) 44 | self.assertTrue(os.path.isfile(f + ".tikz")) 45 | self.assertTrue(os.path.isfile(f + ".tex")) 46 | # look for the pdflatex executable 47 | pdflatex = find_executable("pdflatex") is not None 48 | # if no pdflatex, then do not assert that the pdf was compiled 49 | self.assertTrue(not pdflatex or os.path.isfile(f + ".pdf")) 50 | subprocess.run(["python", "mat_eqn.py"], check=True) 51 | self.assertTrue(os.path.isfile("mat_eqn_example.pdf")) 52 | # change back to previous directory 53 | os.chdir(self.tempdir) 54 | 55 | def test_connect(self): 56 | x = XDSM(use_sfmath=False) 57 | x.add_system("D1", FUNC, "D_1", label_width=2) 58 | x.add_system("D2", FUNC, "D_2", stack=False) 59 | 60 | try: 61 | x.connect("D1", "D2", r"\mathcal{R}(y_1)", "foobar") 62 | except ValueError as err: 63 | self.assertEquals(str(err), "label_width argument must be an integer") 64 | else: 65 | self.fail("Expected ValueError") 66 | 67 | def test_options(self): 68 | filename = "xdsm_test_options" 69 | spec_dir = filename + "_specs" 70 | 71 | # Change `use_sfmath` to False to use computer modern 72 | x = XDSM(use_sfmath=False) 73 | 74 | x.add_system("opt", OPT, r"\text{Optimizer}") 75 | x.add_system("solver", SOLVER, r"\text{Newton}") 76 | x.add_system("D1", FUNC, "D_1", label_width=2) 77 | x.add_system("D2", FUNC, "D_2", stack=False) 78 | x.add_system("F", FUNC, "F", faded=True) 79 | x.add_system("G", FUNC, "G", spec_name="G_spec") 80 | 81 | x.connect("opt", "D1", "x, z") 82 | x.connect("opt", "D2", "z") 83 | x.connect("opt", "F", "x, z") 84 | x.connect("solver", "D1", "y_2") 85 | x.connect("solver", "D2", "y_1") 86 | x.connect("D1", "solver", r"\mathcal{R}(y_1)") 87 | x.connect("solver", "F", "y_1, y_2") 88 | x.connect("D2", "solver", r"\mathcal{R}(y_2)") 89 | x.connect("solver", "G", "y_1, y_2") 90 | 91 | x.connect("F", "opt", "f") 92 | x.connect("G", "opt", "g") 93 | 94 | x.add_output("opt", "x^*, z^*", side=RIGHT) 95 | x.add_output("D1", "y_1^*", side=LEFT, stack=True) 96 | x.add_output("D2", "y_2^*", side=LEFT) 97 | x.add_output("F", "f^*", side=LEFT) 98 | x.add_output("G", "g^*") 99 | x.write(filename) 100 | x.write_sys_specs(spec_dir) 101 | 102 | # Test if files where created 103 | self.assertTrue(os.path.isfile(filename + ".tikz")) 104 | self.assertTrue(os.path.isfile(filename + ".tex")) 105 | self.assertTrue(os.path.isdir(spec_dir)) 106 | self.assertTrue(os.path.isfile(os.path.join(spec_dir, "F.json"))) 107 | self.assertTrue(os.path.isfile(os.path.join(spec_dir, "G_spec.json"))) 108 | 109 | def test_stacked_system(self): 110 | x = XDSM() 111 | 112 | x.add_system("test", OPT, r"\text{test}", stack=True) 113 | 114 | file_name = "stacked_test" 115 | x.write(file_name) 116 | 117 | tikz_file = file_name + ".tikz" 118 | with open(tikz_file, "r") as f: 119 | tikz = f.read() 120 | 121 | self.assertIn(r"\node [Optimization,stack]", tikz) 122 | 123 | def test_tikz_content(self): 124 | # Check if TiKZ file was created. 125 | # Compare the content of the sample below and the newly created TiKZ file. 126 | 127 | sample_txt = r""" 128 | 129 | %%% Preamble Requirements %%% 130 | % \usepackage{geometry} 131 | % \usepackage{amsfonts} 132 | % \usepackage{amsmath} 133 | % \usepackage{amssymb} 134 | % \usepackage{tikz} 135 | 136 | % Optional packages such as sfmath set through python interface 137 | % \usepackage{sfmath} 138 | 139 | % \usetikzlibrary{arrows,chains,positioning,scopes,shapes.geometric,shapes.misc,shadows} 140 | 141 | %%% End Preamble Requirements %%% 142 | 143 | \input{"path/to/diagram_styles"} 144 | \begin{tikzpicture} 145 | 146 | \matrix[MatrixSetup]{ 147 | %Row 0 148 | \node [DataIO] (left_output_opt) {$x^*, z^*$};& 149 | \node [Optimization] (opt) {$\text{Optimizer}$};& 150 | & 151 | \node [DataInter] (opt-D1) {$x, z$};& 152 | \node [DataInter] (opt-D2) {$z$};& 153 | \node [DataInter] (opt-F) {$x, z$};& 154 | \\ 155 | %Row 1 156 | & 157 | & 158 | \node [MDA] (solver) {$\text{Newton}$};& 159 | \node [DataInter] (solver-D1) {$y_2$};& 160 | \node [DataInter] (solver-D2) {$y_1$};& 161 | \node [DataInter] (solver-F) {$y_1, y_2$};& 162 | \node [DataInter] (solver-G) {$y_1, y_2$};\\ 163 | %Row 2 164 | \node [DataIO] (left_output_D1) {$y_1^*$};& 165 | & 166 | \node [DataInter] (D1-solver) {$\mathcal{R}(y_1)$};& 167 | \node [Function] (D1) {$D_1$};& 168 | & 169 | & 170 | \\ 171 | %Row 3 172 | \node [DataIO] (left_output_D2) {$y_2^*$};& 173 | & 174 | \node [DataInter] (D2-solver) {$\mathcal{R}(y_2)$};& 175 | & 176 | \node [Function] (D2) {$D_2$};& 177 | & 178 | \\ 179 | %Row 4 180 | \node [DataIO] (left_output_F) {$f^*$};& 181 | \node [DataInter] (F-opt) {$f$};& 182 | & 183 | & 184 | & 185 | \node [Function] (F) {$F$};& 186 | \\ 187 | %Row 5 188 | \node [DataIO] (left_output_G) {$g^*$};& 189 | \node [DataInter] (G-opt) {$g$};& 190 | & 191 | & 192 | & 193 | & 194 | \node [Function] (G) {$G$};\\ 195 | %Row 6 196 | & 197 | & 198 | & 199 | & 200 | & 201 | & 202 | \\ 203 | }; 204 | 205 | % XDSM process chains 206 | 207 | 208 | \begin{pgfonlayer}{data} 209 | \path 210 | % Horizontal edges 211 | (opt) edge [DataLine] (opt-D1) 212 | (opt) edge [DataLine] (opt-D2) 213 | (opt) edge [DataLine] (opt-F) 214 | (solver) edge [DataLine] (solver-D1) 215 | (solver) edge [DataLine] (solver-D2) 216 | (D1) edge [DataLine] (D1-solver) 217 | (solver) edge [DataLine] (solver-F) 218 | (D2) edge [DataLine] (D2-solver) 219 | (solver) edge [DataLine] (solver-G) 220 | (F) edge [DataLine] (F-opt) 221 | (G) edge [DataLine] (G-opt) 222 | (opt) edge [DataLine] (left_output_opt) 223 | (D1) edge [DataLine] (left_output_D1) 224 | (D2) edge [DataLine] (left_output_D2) 225 | (F) edge [DataLine] (left_output_F) 226 | (G) edge [DataLine] (left_output_G) 227 | % Vertical edges 228 | (opt-D1) edge [DataLine] (D1) 229 | (opt-D2) edge [DataLine] (D2) 230 | (opt-F) edge [DataLine] (F) 231 | (solver-D1) edge [DataLine] (D1) 232 | (solver-D2) edge [DataLine] (D2) 233 | (D1-solver) edge [DataLine] (solver) 234 | (solver-F) edge [DataLine] (F) 235 | (D2-solver) edge [DataLine] (solver) 236 | (solver-G) edge [DataLine] (G) 237 | (F-opt) edge [DataLine] (opt) 238 | (G-opt) edge [DataLine] (opt); 239 | \end{pgfonlayer} 240 | 241 | \end{tikzpicture}""" 242 | 243 | filename = "xdsm_test_tikz" 244 | 245 | x = XDSM(use_sfmath=True) 246 | 247 | x.add_system("opt", OPT, r"\text{Optimizer}") 248 | x.add_system("solver", SOLVER, r"\text{Newton}") 249 | x.add_system("D1", FUNC, "D_1") 250 | x.add_system("D2", FUNC, "D_2") 251 | x.add_system("F", FUNC, "F") 252 | x.add_system("G", FUNC, "G") 253 | 254 | x.connect("opt", "D1", "x, z") 255 | x.connect("opt", "D2", "z") 256 | x.connect("opt", "F", "x, z") 257 | x.connect("solver", "D1", "y_2") 258 | x.connect("solver", "D2", "y_1") 259 | x.connect("D1", "solver", r"\mathcal{R}(y_1)") 260 | x.connect("solver", "F", "y_1, y_2") 261 | x.connect("D2", "solver", r"\mathcal{R}(y_2)") 262 | x.connect("solver", "G", "y_1, y_2") 263 | 264 | x.connect("F", "opt", "f") 265 | x.connect("G", "opt", "g") 266 | 267 | x.add_output("opt", "x^*, z^*", side="left") 268 | x.add_output("D1", "y_1^*", side="left") 269 | x.add_output("D2", "y_2^*", side="left") 270 | x.add_output("F", "f^*", side="left") 271 | x.add_output("G", "g^*", side="left") 272 | x.write(filename) 273 | 274 | # Check if file was created 275 | tikz_file = filename + ".tikz" 276 | 277 | self.assertTrue(os.path.isfile(tikz_file)) 278 | 279 | sample_lines = sample_txt.split("\n") 280 | sample_lines = filter_lines(sample_lines) 281 | 282 | with open(tikz_file, "r") as f: 283 | new_lines = filter_lines(f.readlines()) 284 | 285 | sample_no_match = [] # Sample text 286 | new_no_match = [] # New text 287 | 288 | for new_line, sample_line in zip(new_lines, sample_lines): 289 | if new_line.startswith(r"\input{"): 290 | continue 291 | if new_line != sample_line: # else everything is okay 292 | # This can be because of the different ordering of lines or because of an error. 293 | sample_no_match.append(new_line) 294 | new_no_match.append(sample_line) 295 | 296 | # Sort both sets of suspicious lines 297 | sample_no_match.sort() 298 | new_no_match.sort() 299 | 300 | for sample_line, new_line in zip(sample_no_match, new_no_match): 301 | # Now the lines should match, if only the ordering was different 302 | self.assertEqual(new_line, sample_line) 303 | 304 | # To be sure, check the length, otherwise a missing last line could get unnoticed because of using zip 305 | self.assertEqual(len(new_lines), len(sample_lines)) 306 | 307 | def test_write_outdir(self): 308 | fname = "test" 309 | 310 | for abspath in [True, False]: 311 | subdir = tempfile.mkdtemp(dir=self.tempdir) 312 | outdir = subdir if abspath else os.path.basename(subdir) 313 | 314 | x = XDSM() 315 | x.add_system("x", FUNC, "x") 316 | x.write(fname, outdir=outdir) 317 | 318 | for ext in [".tex", ".tikz", ".pdf"]: 319 | self.assertTrue(os.path.isfile(os.path.join(subdir, fname + ext))) 320 | 321 | # no files outside the subdirs 322 | self.assertFalse(any(os.path.isfile(fp) for fp in os.listdir(self.tempdir))) 323 | 324 | 325 | if __name__ == "__main__": 326 | unittest.main() 327 | -------------------------------------------------------------------------------- /pyxdsm/matrix_eqn.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from collections import namedtuple 4 | import numpy as np 5 | 6 | 7 | # color pallette link: http://paletton.com/#uid=72Q1j0kllllkS5tKC9H96KClOKC 8 | 9 | base_file_start = r"""\documentclass[border=0pt]{standalone} 10 | % Justin S. Gray 2018 11 | % Based off code by John T. Hwang (2014), who based his code off Alec Jacobson: http://www.alecjacobson.com/weblog/?p=1289 12 | 13 | % nc = necessary comment [do not remove] 14 | 15 | % Four rules for using these macros: 16 | % 1. Always start with a row 17 | % 2. Rows contain cols and cols contain rows 18 | % 3. Mats should be on at least the 3rd level (row->col->mat, minimum) 19 | % 4. If a row contains the mat, add &; if a col contains the mat, add \\ 20 | 21 | % --------------------------------------- 22 | 23 | \usepackage{tikz} 24 | \usepackage{ifthen} 25 | \usepackage{esdiff} 26 | \usepackage{varwidth} 27 | 28 | \definecolor{tableau0}{RGB}{77, 121, 168} 29 | \definecolor{tableau1}{RGB}{242, 142, 43} 30 | \definecolor{tableau2}{RGB}{255, 87, 88} 31 | \definecolor{tableau3}{RGB}{118, 183, 178} 32 | \definecolor{tableau4}{RGB}{89, 161, 78} 33 | \definecolor{tableau5}{RGB}{237, 201, 72} 34 | \definecolor{tableau6}{RGB}{176, 121, 162} 35 | \definecolor{tableau7}{RGB}{255, 157, 167} 36 | \definecolor{tableau8}{RGB}{156, 116, 94} 37 | \definecolor{tableau9}{RGB}{186, 176, 172} 38 | 39 | 40 | \newcommand{\thk}{0.01in} 41 | \newcommand{\thkln}{0.02in} 42 | 43 | % \blockmat{width}{height}{text}{block_options}{other} 44 | \newcommand{\blockmat}[5]{ 45 | \begin{tikzpicture} 46 | \draw[draw=white,fill=white,#4,line width=\thk] (0,0) rectangle( #1-\thk,#2-\thk); 47 | #5 48 | \draw (#1/2, #2/2) node {#3}; 49 | \end{tikzpicture} 50 | } 51 | 52 | % blockempty{width}{height}{text} 53 | \newcommand{\blockempty}[3]{ 54 | \blockmat{#1}{#2}{#3}{draw=white,fill=white}{}% 55 | } 56 | 57 | % \blockmat{width}{height}{text}{block_options}{diagonal_width}{diagonal_options} 58 | \newcommand{\blockdiag}[6]{ 59 | \blockmat{#1}{#2}{#3}{#4} 60 | { 61 | \draw[#6,line width=\thk] (0,#2-\thk) -- (#5,#2-\thk) -- ( #1-\thk,#5) -- ( #1-\thk,0) -- ( #1-\thk - #5,0) -- (0,#2-\thk -#5) --cycle; 62 | }% 63 | } 64 | 65 | % \blockddots{width}{height}{text}{block_options}{dot_radius}{dot_options}{dot_h}{dot_v} 66 | \newcommand{\blockdots}[8]{ 67 | \blockmat{#1}{#2}{#3}{#4}% 68 | {% 69 | \ifthenelse{\equal{#5}{}} 70 | {\newcommand\dotradius{0.01in}} 71 | {\newcommand\dotradius{#5}}% 72 | \filldraw[#6] (#1/2, #2/2) circle (0.5*\dotradius);% 73 | \filldraw[#6] (#1/2 + #7, #2/2 + #8) circle (0.5*\dotradius);% 74 | \filldraw[#6] (#1/2 - #7, #2/2 - #8) circle (0.5*\dotradius);% 75 | }% 76 | } 77 | 78 | % \leftbracket{width}{height}{options} 79 | \newcommand{\leftbracket}[3]{ 80 | \begin{tikzpicture} 81 | \coordinate (iSW) at (\thk+\thkln/2,\thk+\thkln/2); 82 | \coordinate (iNW) at (\thk+\thkln/2,#2-\thk-\thkln/2); 83 | \coordinate (iSE) at (#1-\thk-\thkln/2,\thk+\thkln/2); 84 | \coordinate (iNE) at (#1-\thk-\thkln/2,#2-\thk-\thkln/2); 85 | \coordinate (oSW) at (\thk/2,\thk/2); 86 | \coordinate (oNW) at (\thk/2,#2-\thk/2); 87 | \coordinate (oSE) at (#1-\thk/2,\thk/2); 88 | \coordinate (oNE) at (#1-\thk/2,#2-\thk/2); 89 | \draw[#3,line width=\thkln] (iNE) -- (iNW) -- (iSW) -- (iSE); 90 | \draw[draw=white,line width=\thk] (oNE) -- (oNW) -- (oSW) -- (oSE); 91 | \end{tikzpicture}%nc 92 | } 93 | 94 | % \rightbracket{width}{height}{options} 95 | \newcommand{\rightbracket}[3]{ 96 | \begin{tikzpicture} 97 | \coordinate (iSW) at (\thk+\thkln/2,\thk+\thkln/2); 98 | \coordinate (iNW) at (\thk+\thkln/2,#2-\thk-\thkln/2); 99 | \coordinate (iSE) at (#1-\thk-\thkln/2,\thk+\thkln/2); 100 | \coordinate (iNE) at (#1-\thk-\thkln/2,#2-\thk-\thkln/2); 101 | \coordinate (oSW) at (\thk/2,\thk/2); 102 | \coordinate (oNW) at (\thk/2,#2-\thk/2); 103 | \coordinate (oSE) at (#1-\thk/2,\thk/2); 104 | \coordinate (oNE) at (#1-\thk/2,#2-\thk/2); 105 | \draw[#3,line width=\thkln] (iNW) -- (iNE) -- (iSE) -- (iSW); 106 | \draw[draw=white,line width=\thk] (oNW) -- (oNE) -- (oSE) -- (oSW); 107 | \end{tikzpicture}%nc 108 | } 109 | 110 | % \upperbracket{width}{height}{options} 111 | \newcommand{\upperbracket}[3]{ 112 | \begin{tikzpicture} 113 | \coordinate (iSW) at (\thk+\thkln/2,\thk+\thkln/2); 114 | \coordinate (iNW) at (\thk+\thkln/2,#2-\thk-\thkln/2); 115 | \coordinate (iSE) at (#1-\thk-\thkln/2,\thk+\thkln/2); 116 | \coordinate (iNE) at (#1-\thk-\thkln/2,#2-\thk-\thkln/2); 117 | \coordinate (oSW) at (\thk/2,\thk/2); 118 | \coordinate (oNW) at (\thk/2,#2-\thk/2); 119 | \coordinate (oSE) at (#1-\thk/2,\thk/2); 120 | \coordinate (oNE) at (#1-\thk/2,#2-\thk/2); 121 | \draw[#3,line width=\thkln] (iSW) -- (iNW) -- (iNE) -- (iSE); 122 | \draw[draw=white,line width=\thk] (oSW) -- (oNW) -- (oNE) -- (oSE); 123 | \end{tikzpicture}%nc 124 | } 125 | 126 | % \lowerbracket{width}{height}{options} 127 | \newcommand{\lowerbracket}[3]{ 128 | \begin{tikzpicture} 129 | \coordinate (iSW) at (\thk+\thkln/2,\thk+\thkln/2); 130 | \coordinate (iNW) at (\thk+\thkln/2,#2-\thk-\thkln/2); 131 | \coordinate (iSE) at (#1-\thk-\thkln/2,\thk+\thkln/2); 132 | \coordinate (iNE) at (#1-\thk-\thkln/2,#2-\thk-\thkln/2); 133 | \coordinate (oSW) at (\thk/2,\thk/2); 134 | \coordinate (oNW) at (\thk/2,#2-\thk/2); 135 | \coordinate (oSE) at (#1-\thk/2,\thk/2); 136 | \coordinate (oNE) at (#1-\thk/2,#2-\thk/2); 137 | \draw[#3,line width=\thkln] (iNW) -- (iSW) -- (iSE) -- (iNE); 138 | \draw[draw=white,line width=\thk] (oNW) -- (oSW) -- (oSE) -- (oNE); 139 | \end{tikzpicture}%nc 140 | } 141 | 142 | % a hack so that I don't have to worry about the number of columns or 143 | % spaces between columns in the tabular environment 144 | \newenvironment{blockmatrixtabular} 145 | {%nc 146 | \renewcommand{\arraystretch}{0}%nc 147 | \begin{tabular}{ 148 | @{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l 149 | @{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l 150 | @{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l@{}l 151 | @{} 152 | }%nc 153 | } 154 | { 155 | \end{tabular}%nc 156 | } 157 | 158 | % \blockcol{ 159 | % } 160 | \newcommand{\blockcol}[1]{\vtop{\null\hbox{\begin{blockmatrixtabular}#1\end{blockmatrixtabular}}}&} 161 | 162 | % \blockrow{ 163 | % } 164 | \newcommand{\blockrow}[1]{\begin{blockmatrixtabular}#1\end{blockmatrixtabular}\\} 165 | 166 | 167 | \begin{document} 168 | \begin{varwidth}{10\textwidth} 169 | 170 | \newcommand\mwid{0.5in} 171 | \newcommand\wid{0.15in} 172 | \newcommand\comp{0.3in} 173 | \newcommand\ext{0.5in} 174 | \newcommand\dt{0.03in} 175 | \newcommand\txt{0.8in} 176 | 177 | \definecolor{Tgrey}{rgb}{0.9,0.9,0.9} 178 | \definecolor{Tred}{rgb}{1.0,0.722,0.714} 179 | \definecolor{Tgreen}{rgb}{0.639,0.89,0.655} 180 | \definecolor{Tblue}{rgb}{0.667,0.631,0.843} 181 | \definecolor{Tyellow}{rgb}{1,0.941,0.714} 182 | 183 | \definecolor{Lred}{rgb}{17.3,0.063,0.059} 184 | \definecolor{Lgreen}{rgb}{0.047,0.133,0.051} 185 | \definecolor{Lblue}{rgb}{0.063,0.051,0.118} 186 | \definecolor{Lyellow}{rgb}{0.173,0.149,0.059} 187 | 188 | \definecolor{Dgrey}{rgb}{0.4,0.4,0.4} 189 | \definecolor{Dred}{rgb}{1.0,0.333,0.318} 190 | \definecolor{Dgreen}{rgb}{0.282,0.89,0.322} 191 | \definecolor{Dblue}{rgb}{0.42,0.341,0.843} 192 | \definecolor{Dyellow}{rgb}{1.0,0.863,0.318} 193 | 194 | \definecolor{Bred}{rgb}{0.302,0.8,0.0} 195 | \definecolor{Bgreen}{rgb}{0.4,1.0,0.4} 196 | \definecolor{Bblue}{rgb}{0.043,0.012,0.208} 197 | \definecolor{Byellow}{rgb}{0.302,0.243,0.0} 198 | 199 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 200 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 201 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 202 | """ 203 | base_file_end = r""" 204 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 205 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 206 | %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 207 | \end{varwidth} 208 | \end{document}""" 209 | 210 | 211 | Variable = namedtuple("Variable", field_names=["size", "idx", "text", "color"]) 212 | 213 | CellData = namedtuple("CellData", field_names=["text", "color", "highlight"]) 214 | 215 | 216 | def _color(base_color, h_light): 217 | if h_light == -1: 218 | color = "white" 219 | elif h_light == 0: 220 | color = "Tgrey" 221 | elif h_light == 1: 222 | color = "T{}".format(base_color) 223 | elif h_light == 2: 224 | color = "D{}".format(base_color) 225 | elif h_light == 3: 226 | color = "B{}".format(base_color) 227 | 228 | elif h_light == "diag": 229 | color = base_color 230 | 231 | return color 232 | 233 | 234 | def _write_tikz(tikz, out_file, build=True, cleanup=True): 235 | with open("{}.tex".format(out_file), "w") as f: 236 | f.write(base_file_start) 237 | f.write(tikz) 238 | f.write(base_file_end) 239 | 240 | if build: 241 | subprocess.run(["pdflatex", f"{out_file}.tex"], check=True) 242 | 243 | if cleanup: 244 | for ext in ["aux", "fdb_latexmk", "fls", "log", "tex"]: 245 | f_name = "{}.{}".format(out_file, ext) 246 | if os.path.exists(f_name): 247 | os.remove(f_name) 248 | 249 | 250 | class TotalJacobian(object): 251 | def __init__(self): 252 | self._variables = {} 253 | self._j_inputs = {} 254 | self._n_inputs = 0 255 | 256 | self._i_outputs = {} 257 | self._n_outputs = 0 258 | 259 | self._connections = {} 260 | self._ij_connections = {} 261 | 262 | self._setup = False 263 | 264 | def add_input(self, name, size=1, text=""): 265 | self._variables[name] = Variable(size=size, idx=self._n_inputs, text=text, color=None) 266 | self._j_inputs[self._n_inputs] = self._variables[name] 267 | self._n_inputs += 1 268 | 269 | def add_output(self, name, size=1, text=""): 270 | self._variables[name] = Variable(size=size, idx=self._n_outputs, text=text, color=None) 271 | self._i_outputs[self._n_outputs] = self._variables[name] 272 | self._n_outputs += 1 273 | 274 | def connect(self, src, target, text="", color="tableau0"): 275 | if isinstance(target, (list, tuple)): 276 | for t in target: 277 | self._connections[src, t] = CellData(text=text, color=color, highlight="diag") 278 | else: 279 | self._connections[src, target] = CellData(text=text, color=color, highlight="diag") 280 | 281 | def _process_vars(self): 282 | if self._setup: 283 | return 284 | 285 | # deal with connections 286 | for (src, target), cell_data in self._connections.items(): 287 | i_src = self._variables[src].idx 288 | j_target = self._variables[target].idx 289 | 290 | self._ij_connections[i_src, j_target] = cell_data 291 | 292 | self._setup = True 293 | 294 | def write(self, out_file=None, build=True, cleanup=True): 295 | """ 296 | Write output files for the matrix equation diagram. This produces the following: 297 | 298 | - {file_name}.tikz 299 | A file containing the TIKZ definition of the tikz diagram. 300 | - {file_name}.tex 301 | A standalone document wrapped around an include of the TIKZ file which can 302 | be compiled to a pdf. 303 | - {file_name}.pdf 304 | An optional compiled version of the standalone tex file. 305 | 306 | Parameters 307 | ---------- 308 | file_name : str 309 | The prefix to be used for the output files 310 | build : bool 311 | Flag that determines whether the standalone PDF of the XDSM will be compiled. 312 | Default is True. 313 | cleanup: bool 314 | Flag that determines if padlatex build files will be deleted after build is complete 315 | """ 316 | self._process_vars() 317 | 318 | tikz = [] 319 | 320 | # label the columns 321 | tikz.append(r"\blockrow{") 322 | # emtpy column for the row labels 323 | tikz.append(r" \blockcol{") 324 | tikz.append(r" \blockempty{%s*\comp}{%s*\comp}{%s}\\" % (1, 1, "")) 325 | tikz.append(r" }") 326 | for j in range(self._n_inputs): 327 | var = self._j_inputs[j] 328 | col_size = var.size 329 | tikz.append(r" \blockcol{") 330 | tikz.append(r" \blockempty{%s*\comp}{%s*\comp}{%s}\\" % (col_size, 1, var.text)) 331 | tikz.append(r" }") 332 | tikz.append(r"}") 333 | 334 | for i in range(self._n_outputs): 335 | output = self._i_outputs[i] 336 | row_size = output.size 337 | 338 | tikz.append(r"\blockrow{") 339 | 340 | # label the row with the output name 341 | tikz.append(r" \blockcol{") 342 | tikz.append(r" \blockempty{%s*\comp}{%s*\comp}{%s}\\" % (1, row_size, output.text)) 343 | tikz.append(r" }") 344 | 345 | for j in range(self._n_inputs): 346 | var = self._j_inputs[j] 347 | col_size = var.size 348 | tikz.append(r" \blockcol{") 349 | if (j, i) in self._ij_connections: 350 | cell_data = self._ij_connections[(j, i)] 351 | conn_color = "T{}".format(var.color) 352 | if cell_data.color is not None: 353 | conn_color = _color(cell_data.color, cell_data.highlight) 354 | tikz.append( 355 | r" \blockmat{%s*\comp}{%s*\comp}{%s}{draw=white,fill=%s}{}\\" 356 | % (col_size, row_size, cell_data.text, conn_color) 357 | ) 358 | else: 359 | tikz.append(r" \blockempty{%s*\comp}{%s*\comp}{}\\" % (col_size, row_size)) 360 | tikz.append(r" }") 361 | 362 | tikz.append(r"}") 363 | 364 | jac_tikz = "\n".join(tikz) 365 | 366 | _write_tikz(jac_tikz, out_file, build, cleanup) 367 | 368 | 369 | class MatrixEquation(object): 370 | def __init__(self): 371 | self._variables = {} 372 | self._ij_variables = {} 373 | 374 | self._n_vars = 0 375 | 376 | self._connections = {} 377 | self._ij_connections = {} 378 | 379 | self._text = {} 380 | self._ij_text = {} 381 | 382 | self._total_size = 0 383 | 384 | self._setup = False 385 | 386 | self._terms = [] 387 | 388 | def clear_terms(self): 389 | self._terms = [] 390 | 391 | def add_variable(self, name, size=1, text="", color="blue"): 392 | self._variables[name] = Variable(size=size, idx=self._n_vars, text=text, color=color) 393 | self._ij_variables[self._n_vars] = self._variables[name] 394 | self._n_vars += 1 395 | 396 | self._total_size += size 397 | 398 | def connect(self, src, target, text="", color=None, highlight=1): 399 | if isinstance(target, (list, tuple)): 400 | for t in target: 401 | self._connections[src, t] = CellData(text=text, color=color, highlight=highlight) 402 | else: 403 | self._connections[src, target] = CellData(text=text, color=color, highlight=highlight) 404 | 405 | def text(self, src, target, text): 406 | """Don't connect the src and target, but put some text where a connection would be""" 407 | self._text[src, target] = CellData(text=text, color=None, highlight=-1) 408 | 409 | def _process_vars(self): 410 | """Map all the data onto i,j grid""" 411 | 412 | if self._setup: 413 | return 414 | 415 | # deal with connections 416 | for (src, target), cell_data in self._connections.items(): 417 | i_src = self._variables[src].idx 418 | i_target = self._variables[target].idx 419 | 420 | self._ij_connections[i_src, i_target] = cell_data 421 | 422 | for (src, target), cell_data in self._text.items(): 423 | i_src = self._variables[src].idx 424 | j_target = self._variables[target].idx 425 | 426 | self._ij_text[i_src, j_target] = cell_data 427 | 428 | self._setup = True 429 | 430 | def jacobian(self, transpose=False): 431 | self._process_vars() 432 | 433 | tikz = [] 434 | 435 | for i in range(self._n_vars): 436 | tikz.append(r"\blockrow{") 437 | 438 | row_size = self._ij_variables[i].size 439 | for j in range(self._n_vars): 440 | var = self._ij_variables[j] 441 | col_size = var.size 442 | tikz.append(r" \blockcol{") 443 | 444 | if transpose: 445 | location = (i, j) 446 | else: 447 | location = (j, i) 448 | 449 | if i == j: 450 | tikz.append( 451 | r" \blockmat{%s*\comp}{%s*\comp}{%s}{draw=white,fill=D%s}{}\\" 452 | % (col_size, row_size, var.text, var.color) 453 | ) 454 | elif location in self._ij_connections: 455 | cell_data = self._ij_connections[location] 456 | conn_color = "T{}".format(var.color) 457 | if cell_data.color is not None: 458 | conn_color = _color(cell_data.color, cell_data.highlight) 459 | tikz.append( 460 | r" \blockmat{%s*\comp}{%s*\comp}{%s}{draw=white,fill=%s}{}\\" 461 | % (col_size, row_size, cell_data.text, conn_color) 462 | ) 463 | elif location in self._ij_text: 464 | cell_data = self._ij_text[location] 465 | tikz.append(r" \blockempty{%s*\comp}{%s*\comp}{%s}\\" % (col_size, row_size, cell_data.text)) 466 | else: 467 | tikz.append(r" \blockempty{%s*\comp}{%s*\comp}{}\\" % (col_size, row_size)) 468 | tikz.append(r" }") 469 | 470 | tikz.append(r"}") 471 | 472 | lhs_tikz = "\n".join(tikz) 473 | 474 | self._terms.append(lhs_tikz) 475 | return lhs_tikz 476 | 477 | def vector(self, base_color="red", highlight=None): 478 | self._process_vars() 479 | 480 | tikz = [] 481 | 482 | if highlight is None: 483 | highlight = np.ones(self._n_vars) 484 | 485 | for i, h_light in enumerate(highlight): 486 | color = _color(base_color, h_light) 487 | 488 | row_size = self._ij_variables[i].size 489 | 490 | tikz.append(r"\blockrow{\blockcol{") 491 | if h_light == "diag": 492 | tikz.append( 493 | r" \blockdiag{1*\comp}{%s*\comp}{}{draw=white,fill=T%s}{\dt}{draw=white,fill=D%s}\\" 494 | % (row_size, color, color) 495 | ) 496 | else: 497 | tikz.append(r" \blockmat{1*\comp}{%s*\comp}{}{draw=white,fill=%s}{}\\" % (row_size, color)) 498 | 499 | tikz.append(r"}}") 500 | 501 | vec_tikz = "\n".join(tikz) 502 | 503 | self._terms.append(vec_tikz) 504 | return vec_tikz 505 | 506 | def operator(self, opperator="="): 507 | self._process_vars() 508 | 509 | tikz = [] 510 | 511 | padding_size = (self._total_size - 1) / 2 512 | 513 | tikz.append(r"\blockrow{") 514 | tikz.append(r" \blockempty{\mwid}{%s*\comp}{} \\" % (padding_size)) 515 | tikz.append(r" \blockmat{\mwid}{1*\comp}{\huge $%s$}{draw=white,fill=white}{}\\" % (opperator)) 516 | tikz.append(r" \blockempty{\mwid}{%s*\comp}{} \\" % (padding_size)) 517 | tikz.append(r"}") 518 | 519 | op_tikz = "\n".join(tikz) 520 | 521 | self._terms.append(op_tikz) 522 | return op_tikz 523 | 524 | def spacer(self): 525 | self._process_vars() 526 | 527 | tikz = [] 528 | 529 | for i in range(self._n_vars): 530 | row_size = self._ij_variables[i].size 531 | 532 | tikz.append(r"\blockrow{\blockcol{") 533 | tikz.append(r" \blockmat{.25*\mwid}{%s*\comp}{}{draw=white,fill=white}{}\\" % (row_size)) 534 | tikz.append(r"}}") 535 | 536 | spacer_tikz = "\n".join(tikz) 537 | 538 | self._terms.append(spacer_tikz) 539 | return spacer_tikz 540 | 541 | def write(self, out_file=None, build=True, cleanup=True): 542 | """ 543 | Write output files for the matrix equation diagram. This produces the following: 544 | 545 | - {file_name}.tikz 546 | A file containing the TIKZ definition of the tikz diagram. 547 | - {file_name}.tex 548 | A standalone document wrapped around an include of the TIKZ file which can 549 | be compiled to a pdf. 550 | - {file_name}.pdf 551 | An optional compiled version of the standalone tex file. 552 | 553 | Parameters 554 | ---------- 555 | file_name : str 556 | The prefix to be used for the output files 557 | build : bool 558 | Flag that determines whether the standalone PDF of the XDSM will be compiled. 559 | Default is True. 560 | cleanup: bool 561 | Flag that determines if padlatex build files will be deleted after build is complete 562 | """ 563 | tikz = [] 564 | tikz.append(r"\blockrow{") 565 | 566 | for term in self._terms: 567 | tikz.append(r"\blockcol{") 568 | tikz.append(term) 569 | tikz.append(r"}") 570 | tikz.append(r"}") 571 | 572 | eqn_tikz = "\n".join(tikz) 573 | 574 | if out_file: 575 | _write_tikz(eqn_tikz, out_file, build, cleanup) 576 | 577 | 578 | if __name__ == "__main__": 579 | lst = MatrixEquation() 580 | 581 | lst.add_variable("x", text=r"$x$") 582 | lst.add_variable("y", size=3, text=r"$y$") 583 | lst.add_variable("z") 584 | 585 | lst.connect("x", "y") 586 | lst.connect("y", "z") 587 | lst.text("z", "x", r"$0$") 588 | 589 | lst.jacobian(transpose=True) 590 | lst.spacer() 591 | lst.vector(base_color="green", highlight=[3, 2, "diag"]) 592 | lst.operator("=") 593 | lst.vector(base_color="red") 594 | lst.vector(base_color="red") 595 | 596 | lst.write("test") 597 | 598 | J = TotalJacobian() 599 | J.add_input("a", text=r"$a$") 600 | J.add_input("b", text=r"$b$") 601 | J.add_input("c", text=r"$c$") 602 | J.add_input("d", text=r"$d$") 603 | J.add_input("e", text=r"$e$") 604 | 605 | J.add_output("gc", text=r"$g_c$") 606 | J.add_output("gd", text=r"$g_d$") 607 | J.add_output("ge", text=r"$g_e$") 608 | J.add_output("f", text=r"$f$") 609 | 610 | J.connect("a", ("gc", "gd", "ge", "f")) 611 | J.connect("b", ("gc", "gd", "ge", "f")) 612 | J.connect("c", "gc") 613 | J.connect("d", "gd") 614 | J.connect("e", ("ge", "f")) 615 | 616 | J.write("J_test", cleanup=False) 617 | -------------------------------------------------------------------------------- /pyxdsm/XDSM.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import os 3 | import numpy as np 4 | import json 5 | import subprocess 6 | from collections import namedtuple 7 | 8 | from pyxdsm import __version__ as pyxdsm_version 9 | 10 | OPT = "Optimization" 11 | SUBOPT = "SubOptimization" 12 | SOLVER = "MDA" 13 | DOE = "DOE" 14 | IFUNC = "ImplicitFunction" 15 | FUNC = "Function" 16 | GROUP = "Group" 17 | IGROUP = "ImplicitGroup" 18 | METAMODEL = "Metamodel" 19 | LEFT = "left" 20 | RIGHT = "right" 21 | 22 | tikzpicture_template = r""" 23 | %%% Preamble Requirements %%% 24 | % \usepackage{{geometry}} 25 | % \usepackage{{amsfonts}} 26 | % \usepackage{{amsmath}} 27 | % \usepackage{{amssymb}} 28 | % \usepackage{{tikz}} 29 | 30 | % Optional packages such as sfmath set through python interface 31 | % \usepackage{{{optional_packages}}} 32 | 33 | % \usetikzlibrary{{arrows,chains,positioning,scopes,shapes.geometric,shapes.misc,shadows}} 34 | 35 | %%% End Preamble Requirements %%% 36 | 37 | \input{{"{diagram_styles_path}"}} 38 | \begin{{tikzpicture}} 39 | 40 | \matrix[MatrixSetup]{{ 41 | {nodes}}}; 42 | 43 | % XDSM process chains 44 | {process} 45 | 46 | \begin{{pgfonlayer}}{{data}} 47 | \path 48 | {edges} 49 | \end{{pgfonlayer}} 50 | 51 | \end{{tikzpicture}} 52 | """ 53 | 54 | tex_template = r""" 55 | % XDSM diagram created with pyXDSM {version}. 56 | \documentclass{{article}} 57 | \usepackage{{geometry}} 58 | \usepackage{{amsfonts}} 59 | \usepackage{{amsmath}} 60 | \usepackage{{amssymb}} 61 | \usepackage{{tikz}} 62 | 63 | % Optional packages such as sfmath set through python interface 64 | \usepackage{{{optional_packages}}} 65 | 66 | % Define the set of TikZ packages to be included in the architecture diagram document 67 | \usetikzlibrary{{arrows,chains,positioning,scopes,shapes.geometric,shapes.misc,shadows}} 68 | 69 | 70 | % Set the border around all of the architecture diagrams to be tight to the diagrams themselves 71 | % (i.e. no longer need to tinker with page size parameters) 72 | \usepackage[active,tightpage]{{preview}} 73 | \PreviewEnvironment{{tikzpicture}} 74 | \setlength{{\PreviewBorder}}{{5pt}} 75 | 76 | \begin{{document}} 77 | 78 | \input{{"{tikzpicture_path}"}} 79 | 80 | \end{{document}} 81 | """ 82 | 83 | 84 | def chunk_label(label, n_chunks): 85 | # looping till length l 86 | for i in range(0, len(label), n_chunks): 87 | yield label[i : i + n_chunks] 88 | 89 | 90 | def _parse_label(label, label_width=None): 91 | if isinstance(label, (tuple, list)): 92 | if label_width is None: 93 | return r"$\begin{array}{c}" + r" \\ ".join(label) + r"\end{array}$" 94 | else: 95 | labels = [] 96 | for chunk in chunk_label(label, label_width): 97 | labels.append(", ".join(chunk)) 98 | return r"$\begin{array}{c}" + r" \\ ".join(labels) + r"\end{array}$" 99 | else: 100 | return r"${}$".format(label) 101 | 102 | 103 | def _label_to_spec(label, spec): 104 | if isinstance(label, str): 105 | label = [ 106 | label, 107 | ] 108 | for var in label: 109 | if var: 110 | spec.add(var) 111 | 112 | 113 | System = namedtuple("System", "node_name style label stack faded label_width spec_name") 114 | Input = namedtuple("Input", "node_name label label_width style stack faded") 115 | Output = namedtuple("Output", "node_name label label_width style stack faded side") 116 | Connection = namedtuple("Connection", "src target label label_width style stack faded src_faded target_faded") 117 | Process = namedtuple("Process", "systems arrow faded") 118 | 119 | 120 | class XDSM: 121 | def __init__(self, use_sfmath=True, optional_latex_packages=None, auto_fade=None): 122 | """Initialize XDSM object 123 | 124 | Parameters 125 | ---------- 126 | use_sfmath : bool, optional 127 | Whether to use the sfmath latex package, by default True 128 | optional_latex_packages : string or list of strings, optional 129 | Additional latex packages to use when creating the pdf and tex versions of the diagram, by default None 130 | auto_fade : dictionary, optional 131 | Controls the automatic fading of inputs, outputs, connections and processes based on the fading of diagonal blocks. For each key "inputs", "outputs", "connections", and "processes", the value can be one of: 132 | - "all" : fade all blocks 133 | - "connected" : fade all components connected to faded blocks (both source and target must be faded for a conncection to be faded) 134 | - "none" : do not auto-fade anything 135 | For connections there are two additional options: 136 | - "incoming" : Fade all connections that are incoming to faded blocks. 137 | - "outgoing" : Fade all connections that are outgoing from faded blocks. 138 | """ 139 | self.systems = [] 140 | self.connections = [] 141 | self.left_outs = {} 142 | self.right_outs = {} 143 | self.ins = {} 144 | self.processes = [] 145 | 146 | self.use_sfmath = use_sfmath 147 | if optional_latex_packages is None: 148 | self.optional_packages = [] 149 | else: 150 | if isinstance(optional_latex_packages, str): 151 | self.optional_packages = [optional_latex_packages] 152 | elif isinstance(optional_latex_packages, list): 153 | self.optional_packages = optional_latex_packages 154 | else: 155 | raise ValueError("optional_latex_packages must be a string or a list of strings") 156 | 157 | self.auto_fade = {"inputs": "none", "outputs": "none", "connections": "none", "processes": "none"} 158 | fade_options = ["all", "connected", "none"] 159 | if auto_fade is not None: 160 | if any([key not in self.auto_fade for key in auto_fade.keys()]): 161 | raise ValueError( 162 | "The supplied 'auto_fade' dictionary contains keys that are not recognized. " 163 | + "valid keys are 'inputs', 'outputs', 'connections', 'processes'." 164 | ) 165 | 166 | self.auto_fade.update(auto_fade) 167 | for key in self.auto_fade.keys(): 168 | option_is_valid = self.auto_fade[key] in fade_options or ( 169 | key == "connections" and self.auto_fade[key] in ["incoming", "outgoing"] 170 | ) 171 | if not option_is_valid: 172 | raise ValueError( 173 | f"The supplied 'auto_fade' dictionary contains an invalid value: '{key}'. " 174 | + "valid values are 'all', 'connected', 'none', 'incoming', 'outgoing'." 175 | ) 176 | 177 | def add_system( 178 | self, 179 | node_name, 180 | style, 181 | label, 182 | stack=False, 183 | faded=False, 184 | label_width=None, 185 | spec_name=None, 186 | ): 187 | r""" 188 | Add a "system" block, which will be placed on the diagonal of the XDSM diagram. 189 | 190 | Parameters 191 | ---------- 192 | node_name : str 193 | The unique name given to this component 194 | 195 | style : str 196 | The type of the component 197 | 198 | label : str or list/tuple of strings 199 | The label to appear on the diagram. There are two options for this: 200 | - a single string 201 | - a list or tuple of strings, which is used for line breaking 202 | In either case, they should probably be enclosed in \text{} declarations to make sure 203 | the font is upright. 204 | 205 | stack : bool 206 | If true, the system will be displayed as several stacked rectangles, 207 | indicating the component is executed in parallel. 208 | 209 | faded : bool 210 | If true, the component will be faded, in order to highlight some other system. 211 | 212 | label_width : int or None 213 | If not None, AND if ``label`` is given as either a tuple or list, then this parameter 214 | controls how many items in the tuple/list will be displayed per line. 215 | If None, the label will be printed one item per line if given as a tuple or list, 216 | otherwise the string will be printed on a single line. 217 | 218 | spec_name : str 219 | The spec name used for the spec file. 220 | 221 | """ 222 | if spec_name is None: 223 | spec_name = node_name 224 | 225 | sys = System(node_name, style, label, stack, faded, label_width, spec_name) 226 | self.systems.append(sys) 227 | 228 | def add_input(self, name, label, label_width=None, style="DataIO", stack=False, faded=False): 229 | r""" 230 | Add an input, which will appear in the top row of the diagram. 231 | 232 | Parameters 233 | ---------- 234 | name : str 235 | The unique name given to this component 236 | 237 | label : str or list/tuple of strings 238 | The label to appear on the diagram. There are two options for this: 239 | - a single string 240 | - a list or tuple of strings, which is used for line breaking 241 | In either case, they should probably be enclosed in \text{} declarations to make sure 242 | the font is upright. 243 | 244 | label_width : int or None 245 | If not None, AND if ``label`` is given as either a tuple or list, then this parameter 246 | controls how many items in the tuple/list will be displayed per line. 247 | If None, the label will be printed one item per line if given as a tuple or list, 248 | otherwise the string will be printed on a single line. 249 | 250 | style : str 251 | The style given to this component. Can be one of ['DataInter', 'DataIO'] 252 | 253 | stack : bool 254 | If true, the system will be displayed as several stacked rectangles, 255 | indicating the component is executed in parallel. 256 | 257 | faded : bool 258 | If true, the component will be faded, in order to highlight some other system. 259 | """ 260 | sys_faded = {} 261 | for s in self.systems: 262 | sys_faded[s.node_name] = s.faded 263 | if (self.auto_fade["inputs"] == "all") or ( 264 | self.auto_fade["inputs"] == "connected" and name in sys_faded and sys_faded[name] 265 | ): 266 | faded = True 267 | self.ins[name] = Input("output_" + name, label, label_width, style, stack, faded) 268 | 269 | def add_output(self, name, label, label_width=None, style="DataIO", stack=False, faded=False, side="left"): 270 | r""" 271 | Add an output, which will appear in the left or right-most column of the diagram. 272 | 273 | Parameters 274 | ---------- 275 | name : str 276 | The unique name given to this component 277 | 278 | label : str or list/tuple of strings 279 | The label to appear on the diagram. There are two options for this: 280 | - a single string 281 | - a list or tuple of strings, which is used for line breaking 282 | In either case, they should probably be enclosed in \text{} declarations to make sure 283 | the font is upright. 284 | 285 | label_width : int or None 286 | If not None, AND if ``label`` is given as either a tuple or list, then this parameter 287 | controls how many items in the tuple/list will be displayed per line. 288 | If None, the label will be printed one item per line if given as a tuple or list, 289 | otherwise the string will be printed on a single line. 290 | 291 | style : str 292 | The style given to this component. Can be one of ``['DataInter', 'DataIO']`` 293 | 294 | stack : bool 295 | If true, the system will be displayed as several stacked rectangles, 296 | indicating the component is executed in parallel. 297 | 298 | faded : bool 299 | If true, the component will be faded, in order to highlight some other system. 300 | 301 | side : str 302 | Must be one of ``['left', 'right']``. This parameter controls whether the output 303 | is placed on the left-most column or the right-most column of the diagram. 304 | """ 305 | sys_faded = {} 306 | for s in self.systems: 307 | sys_faded[s.node_name] = s.faded 308 | if (self.auto_fade["outputs"] == "all") or ( 309 | self.auto_fade["outputs"] == "connected" and name in sys_faded and sys_faded[name] 310 | ): 311 | faded = True 312 | if side == "left": 313 | self.left_outs[name] = Output("left_output_" + name, label, label_width, style, stack, faded, side) 314 | elif side == "right": 315 | self.right_outs[name] = Output("right_output_" + name, label, label_width, style, stack, faded, side) 316 | else: 317 | raise ValueError("The option 'side' must be given as either 'left' or 'right'!") 318 | 319 | def connect( 320 | self, 321 | src, 322 | target, 323 | label, 324 | label_width=None, 325 | style="DataInter", 326 | stack=False, 327 | faded=False, 328 | ): 329 | r""" 330 | Connects two components with a data line, and adds a label to indicate 331 | the data being transferred. 332 | 333 | Parameters 334 | ---------- 335 | src : str 336 | The name of the source component. 337 | 338 | target : str 339 | The name of the target component. 340 | 341 | label : str or list/tuple of strings 342 | The label to appear on the diagram. There are two options for this: 343 | - a single string 344 | - a list or tuple of strings, which is used for line breaking 345 | In either case, they should probably be enclosed in \text{} declarations to make sure 346 | the font is upright. 347 | 348 | label_width : int or None 349 | If not None, AND if ``label`` is given as either a tuple or list, then this parameter 350 | controls how many items in the tuple/list will be displayed per line. 351 | If None, the label will be printed one item per line if given as a tuple or list, 352 | otherwise the string will be printed on a single line. 353 | 354 | style : str 355 | The style given to this component. Can be one of ``['DataInter', 'DataIO']`` 356 | 357 | stack : bool 358 | If true, the system will be displayed as several stacked rectangles, 359 | indicating the component is executed in parallel. 360 | 361 | faded : bool 362 | If true, the component will be faded, in order to highlight some other system. 363 | """ 364 | if src == target: 365 | raise ValueError("Can not connect component to itself") 366 | 367 | if (not isinstance(label_width, int)) and (label_width is not None): 368 | raise ValueError("label_width argument must be an integer") 369 | 370 | sys_faded = {} 371 | for s in self.systems: 372 | sys_faded[s.node_name] = s.faded 373 | 374 | allFaded = self.auto_fade["connections"] == "all" 375 | srcFaded = src in sys_faded and sys_faded[src] 376 | targetFaded = target in sys_faded and sys_faded[target] 377 | if ( 378 | allFaded 379 | or (self.auto_fade["connections"] == "connected" and (srcFaded and targetFaded)) 380 | or (self.auto_fade["connections"] == "incoming" and targetFaded) 381 | or (self.auto_fade["connections"] == "outgoing" and srcFaded) 382 | ): 383 | faded = True 384 | 385 | self.connections.append(Connection(src, target, label, label_width, style, stack, faded, srcFaded, targetFaded)) 386 | 387 | def add_process(self, systems, arrow=True, faded=False): 388 | """ 389 | Add a process line between a list of systems, to indicate process flow. 390 | 391 | Parameters 392 | ---------- 393 | systems : list 394 | The names of the components, in the order in which they should be connected. 395 | For a complete cycle, repeat the first component as the last component. 396 | 397 | arrow : bool 398 | If true, arrows will be added to the process lines to indicate the direction 399 | of the process flow. 400 | """ 401 | sys_faded = {} 402 | for s in self.systems: 403 | sys_faded[s.node_name] = s.faded 404 | if (self.auto_fade["processes"] == "all") or ( 405 | self.auto_fade["processes"] == "connected" 406 | and any( 407 | [sys_faded[s] for s in systems if s in sys_faded.keys()] 408 | ) # sometimes a process may contain off-diagonal blocks 409 | ): 410 | faded = True 411 | self.processes.append(Process(systems, arrow, faded)) 412 | 413 | def _build_node_grid(self): 414 | size = len(self.systems) 415 | 416 | comps_rows = np.arange(size) 417 | comps_cols = np.arange(size) 418 | 419 | if self.ins: 420 | size += 1 421 | # move all comps down one row 422 | comps_rows += 1 423 | 424 | if self.left_outs: 425 | size += 1 426 | # shift all comps to the right by one, to make room for inputs 427 | comps_cols += 1 428 | 429 | if self.right_outs: 430 | size += 1 431 | # don't need to shift anything in this case 432 | 433 | # build a map between comp node_names and row idx for ordering calculations 434 | row_idx_map = {} 435 | col_idx_map = {} 436 | 437 | node_str = r"\node [{style}] ({node_name}) {{{node_label}}};" 438 | 439 | grid = np.empty((size, size), dtype=object) 440 | grid[:] = "" 441 | 442 | # add all the components on the diagonal 443 | for i_row, j_col, comp in zip(comps_rows, comps_cols, self.systems): 444 | style = comp.style 445 | if comp.stack: 446 | style += ",stack" 447 | if comp.faded: 448 | style += ",faded" 449 | 450 | label = _parse_label(comp.label, comp.label_width) 451 | node = node_str.format(style=style, node_name=comp.node_name, node_label=label) 452 | grid[i_row, j_col] = node 453 | 454 | row_idx_map[comp.node_name] = i_row 455 | col_idx_map[comp.node_name] = j_col 456 | 457 | # add all the off diagonal nodes from components 458 | for conn in self.connections: 459 | # src, target, style, label, stack, faded, label_width 460 | src_row = row_idx_map[conn.src] 461 | target_col = col_idx_map[conn.target] 462 | 463 | loc = (src_row, target_col) 464 | 465 | style = conn.style 466 | if conn.stack: 467 | style += ",stack" 468 | if conn.faded: 469 | style += ",faded" 470 | 471 | label = _parse_label(conn.label, conn.label_width) 472 | 473 | node_name = "{}-{}".format(conn.src, conn.target) 474 | 475 | node = node_str.format(style=style, node_name=node_name, node_label=label) 476 | 477 | grid[loc] = node 478 | 479 | # add the nodes for left outputs 480 | for comp_name, out in self.left_outs.items(): 481 | style = out.style 482 | if out.stack: 483 | style += ",stack" 484 | if out.faded: 485 | style += ",faded" 486 | 487 | i_row = row_idx_map[comp_name] 488 | loc = (i_row, 0) 489 | 490 | label = _parse_label(out.label, out.label_width) 491 | node = node_str.format(style=style, node_name=out.node_name, node_label=label) 492 | 493 | grid[loc] = node 494 | 495 | # add the nodes for right outputs 496 | for comp_name, out in self.right_outs.items(): 497 | style = out.style 498 | if out.stack: 499 | style += ",stack" 500 | if out.faded: 501 | style += ",faded" 502 | 503 | i_row = row_idx_map[comp_name] 504 | loc = (i_row, -1) 505 | label = _parse_label(out.label, out.label_width) 506 | node = node_str.format(style=style, node_name=out.node_name, node_label=label) 507 | 508 | grid[loc] = node 509 | 510 | # add the inputs to the top of the grid 511 | for comp_name, inp in self.ins.items(): 512 | # node_name, style, label, stack = in_data 513 | style = inp.style 514 | if inp.stack: 515 | style += ",stack" 516 | if inp.faded: 517 | style += ",faded" 518 | 519 | j_col = col_idx_map[comp_name] 520 | loc = (0, j_col) 521 | label = _parse_label(inp.label, label_width=inp.label_width) 522 | node = node_str.format(style=style, node_name=inp.node_name, node_label=label) 523 | 524 | grid[loc] = node 525 | 526 | # mash the grid data into a string 527 | rows_str = "" 528 | for i, row in enumerate(grid): 529 | rows_str += "%Row {}\n".format(i) + "&\n".join(row) + r"\\" + "\n" 530 | 531 | return rows_str 532 | 533 | def _build_edges(self): 534 | h_edges = [] 535 | v_edges = [] 536 | 537 | edge_format_string = "({start}) edge [{style}] ({end})" 538 | for conn in self.connections: 539 | h_edge_style = "DataLine" 540 | v_edge_style = "DataLine" 541 | if conn.src_faded or conn.faded: 542 | h_edge_style += ",faded" 543 | if conn.target_faded or conn.faded: 544 | v_edge_style += ",faded" 545 | od_node_name = "{}-{}".format(conn.src, conn.target) 546 | 547 | h_edges.append(edge_format_string.format(start=conn.src, end=od_node_name, style=h_edge_style)) 548 | v_edges.append(edge_format_string.format(start=od_node_name, end=conn.target, style=v_edge_style)) 549 | 550 | for comp_name, out in self.left_outs.items(): 551 | style = "DataLine" 552 | if out.faded: 553 | style += ",faded" 554 | node_name = out.node_name 555 | h_edges.append(edge_format_string.format(start=comp_name, end=node_name, style=style)) 556 | 557 | for comp_name, out in self.right_outs.items(): 558 | style = "DataLine" 559 | if out.faded: 560 | style += ",faded" 561 | node_name = out.node_name 562 | h_edges.append(edge_format_string.format(start=comp_name, end=node_name, style=style)) 563 | 564 | for comp_name, inp in self.ins.items(): 565 | style = "DataLine" 566 | if inp.faded: 567 | style += ",faded" 568 | node_name = inp.node_name 569 | v_edges.append(edge_format_string.format(start=comp_name, end=node_name, style=style)) 570 | 571 | h_edges = sorted(h_edges, key=lambda s: "faded" in s) 572 | v_edges = sorted(v_edges, key=lambda s: "faded" in s) 573 | 574 | paths_str = "% Horizontal edges\n" + "\n".join(h_edges) + "\n" 575 | paths_str += "% Vertical edges\n" + "\n".join(v_edges) + ";" 576 | 577 | return paths_str 578 | 579 | def _build_process_chain(self): 580 | sys_names = [s.node_name for s in self.systems] 581 | output_names = ( 582 | [data[0] for _, data in self.ins.items()] 583 | + [data[0] for _, data in self.left_outs.items()] 584 | + [data[0] for _, data in self.right_outs.items()] 585 | ) 586 | # comp_name, in_data in self.ins.items(): 587 | # node_name, style, label, stack = in_data 588 | chain_str = "" 589 | 590 | for proc in self.processes: 591 | chain_str += "{ [start chain=process]\n \\begin{pgfonlayer}{process} \n" 592 | start_tip = False 593 | for i, sys in enumerate(proc.systems): 594 | if sys not in sys_names and sys not in output_names: 595 | raise ValueError( 596 | 'process includes a system named "{}" but no system with that name exists.'.format(sys) 597 | ) 598 | if sys in output_names and i == 0: 599 | start_tip = True 600 | if i == 0: 601 | chain_str += "\\chainin ({});\n".format(sys) 602 | else: 603 | if sys in output_names or (i == 1 and start_tip): 604 | if proc.arrow: 605 | style = "ProcessTipA" 606 | else: 607 | style = "ProcessTip" 608 | else: 609 | if proc.arrow: 610 | style = "ProcessHVA" 611 | else: 612 | style = "ProcessHV" 613 | if proc.faded: 614 | style = "Faded" + style 615 | chain_str += "\\chainin ({}) [join=by {}];\n".format(sys, style) 616 | chain_str += "\\end{pgfonlayer}\n}" 617 | 618 | return chain_str 619 | 620 | def _compose_optional_package_list(self): 621 | # Check for optional LaTeX packages 622 | optional_packages_list = self.optional_packages 623 | if self.use_sfmath: 624 | optional_packages_list.append("sfmath") 625 | 626 | # Join all packages into one string separated by comma 627 | optional_packages_str = ",".join(optional_packages_list) 628 | 629 | return optional_packages_str 630 | 631 | def write(self, file_name, build=True, cleanup=True, quiet=False, outdir="."): 632 | """ 633 | Write output files for the XDSM diagram. This produces the following: 634 | 635 | - {file_name}.tikz 636 | A file containing the TikZ definition of the XDSM diagram. 637 | - {file_name}.tex 638 | A standalone document wrapped around an include of the TikZ file which can 639 | be compiled to a pdf. 640 | - {file_name}.pdf 641 | An optional compiled version of the standalone tex file. 642 | 643 | Parameters 644 | ---------- 645 | file_name : str 646 | The prefix to be used for the output files 647 | build : bool 648 | Flag that determines whether the standalone PDF of the XDSM will be compiled. 649 | Default is True. 650 | cleanup : bool 651 | Flag that determines if pdflatex build files will be deleted after build is complete 652 | quiet : bool 653 | Set to True to suppress output from pdflatex. 654 | outdir : str 655 | Path to an existing directory in which to place output files. If a relative 656 | path is given, it is interpreted relative to the current working directory. 657 | """ 658 | nodes = self._build_node_grid() 659 | edges = self._build_edges() 660 | process = self._build_process_chain() 661 | 662 | module_path = os.path.dirname(__file__) 663 | diagram_styles_path = os.path.join(module_path, "diagram_styles") 664 | # Hack for Windows. MiKTeX needs Linux style paths. 665 | diagram_styles_path = diagram_styles_path.replace("\\", "/") 666 | 667 | optional_packages_str = self._compose_optional_package_list() 668 | 669 | tikzpicture_str = tikzpicture_template.format( 670 | nodes=nodes, 671 | edges=edges, 672 | process=process, 673 | diagram_styles_path=diagram_styles_path, 674 | optional_packages=optional_packages_str, 675 | ) 676 | 677 | base_output_fp = os.path.join(outdir, file_name) 678 | with open(base_output_fp + ".tikz", "w") as f: 679 | f.write(tikzpicture_str) 680 | 681 | tex_str = tex_template.format( 682 | nodes=nodes, 683 | edges=edges, 684 | tikzpicture_path=file_name + ".tikz", 685 | diagram_styles_path=diagram_styles_path, 686 | optional_packages=optional_packages_str, 687 | version=pyxdsm_version, 688 | ) 689 | 690 | with open(base_output_fp + ".tex", "w") as f: 691 | f.write(tex_str) 692 | 693 | if build: 694 | command = [ 695 | "pdflatex", 696 | "-halt-on-error", 697 | "-interaction=nonstopmode", 698 | "-output-directory={}".format(outdir), 699 | ] 700 | if quiet: 701 | command += ["-interaction=batchmode", "-halt-on-error"] 702 | command += [f"{file_name}.tex"] 703 | subprocess.run(command, check=True) 704 | if cleanup: 705 | for ext in ["aux", "fdb_latexmk", "fls", "log"]: 706 | f_name = "{}.{}".format(base_output_fp, ext) 707 | if os.path.exists(f_name): 708 | os.remove(f_name) 709 | 710 | def write_sys_specs(self, folder_name): 711 | """ 712 | Write I/O spec json files for systems to specified folder 713 | 714 | An I/O spec of a system is the collection of all variables going into and out of it. 715 | That includes any variables being passed between systems, as well as all inputs and outputs. 716 | This information is useful for comparing implementations (such as components and groups in OpenMDAO) 717 | to the XDSM diagrams. 718 | 719 | The json spec files can be used to write testing utilities that compare the inputs/outputs of an implementation 720 | to the XDSM, and thus allow you to verify that your codes match the XDSM diagram precisely. 721 | This technique is especially useful when large engineering teams are collaborating on 722 | model development. It allows them to use the XDSM as a shared contract between team members 723 | so everyone can be sure that their codes will sync up. 724 | 725 | Parameters 726 | ---------- 727 | folder_name: str 728 | name of the folder, which will be created if it doesn't exist, to put spec files into 729 | """ 730 | 731 | # find un-connected to each system by looking at Inputs 732 | specs = {} 733 | for sys in self.systems: 734 | specs[sys.node_name] = {"inputs": set(), "outputs": set()} 735 | 736 | for sys_name, inp in self.ins.items(): 737 | _label_to_spec(inp.label, specs[sys_name]["inputs"]) 738 | 739 | # find connected inputs/outputs to each system by looking at Connections 740 | for conn in self.connections: 741 | _label_to_spec(conn.label, specs[conn.target]["inputs"]) 742 | 743 | _label_to_spec(conn.label, specs[conn.src]["outputs"]) 744 | 745 | # find unconnected outputs to each system by looking at Outputs 746 | for sys_name, out in self.left_outs.items(): 747 | _label_to_spec(out.label, specs[sys_name]["outputs"]) 748 | for sys_name, out in self.right_outs.items(): 749 | _label_to_spec(out.label, specs[sys_name]["outputs"]) 750 | 751 | if not os.path.isdir(folder_name): 752 | os.mkdir(folder_name) 753 | 754 | for sys in self.systems: 755 | if sys.spec_name is not False: 756 | path = os.path.join(folder_name, sys.spec_name + ".json") 757 | with open(path, "w") as f: 758 | spec = specs[sys.node_name] 759 | spec["inputs"] = list(spec["inputs"]) 760 | spec["outputs"] = list(spec["outputs"]) 761 | json_str = json.dumps(spec, indent=2) 762 | f.write(json_str) 763 | --------------------------------------------------------------------------------