Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 32 additions & 16 deletions docs_src/api.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
# Python API

The Python API is contained in the *idaes_connectivity.base* module.
The basic workflow is:
* Create an instance of the [Connectivity](connectivity-class) class from a model.
* Format the connectivity information with one of the subclasses of the [Formatter](formatter-classes) class, writing to a file
or returning the value as a string.
The Python API is contained in the *idaes_connectivity.base* module. The basic
workflow is:
* Create an instance of the [Connectivity](connectivity-class) class from a
model.
* Format the connectivity information with one of the subclasses of the
[Formatter](formatter-classes) class, writing to a file or returning the value
as a string.
- You can output CSV for viewing in a text editor or spreadsheet program.
- You can also output a text-based diagram specification for viewing in a tool such as Mermaid or D2. Find more details on the [diagrams](diagrams.md) page.
- You can also output a text-based diagram specification for viewing in a tool
such as Mermaid or D2. Find more details on the [diagrams](diagrams.md)
page.

The rest of this page provides some [examples](api-examples) of API usage and details of the [Connectivity](connectivity-class) and [Formatter](formatter-classes) classes.
The rest of this page provides some [examples](api-examples) of API usage and
details of the [Connectivity](connectivity-class) and
[Formatter](formatter-classes) classes.

(api-examples)=
## Examples

In these examples, we will assume we have an instance of Connectivity for a given model.
In these examples, we will assume we have an instance of Connectivity for a
given model.
```
from idaes_connectivity.tests.example_flowsheet import build
model = build()
Expand All @@ -39,7 +46,14 @@ d2_fmt = D2(conn)
d2_fmt.write("myfile.d2")
```

Returning as a string requires `None` as the file argument (full example to show output):
You can directly create a rendered Mermaid image diagram:
```
from idaes_connectivity.base import MermaidImage
MermaidImage(conn).write("flowsheet_diagram.png")
```

Returning as a string requires `None` as the file argument (full example to show
output):
```
from idaes_connectivity.base import D2, Connectivity
from idaes_connectivity.tests.example_flowsheet import build
Expand All @@ -58,8 +72,9 @@ Unit_B -> Unit_C
Unit_C -> Unit_D
```

A convenience method, `display_connectivity`, displays the Mermaid diagram inline in a Jupyter Notebook.
This requires JupyterLab 4.1 or Notebook 7.1, or later.
A convenience method, `display_connectivity`, displays the Mermaid diagram
inline in a Jupyter Notebook. This requires JupyterLab 4.1 or Notebook 7.1, or
later.
```
from idaes_connectivity.base import Connectivity
from idaes_connectivity.jupyter import display_connectivity
Expand Down Expand Up @@ -107,8 +122,8 @@ The basic usage is:

### CSV formatter

The CSV formatter writes out text as comma-separated values.
It ignores the `direction` argument in the `Formatter` base class constructor.
The CSV formatter writes out text as comma-separated values. It ignores the
`direction` argument in the `Formatter` base class constructor.

```{eval-rst}
.. autoclass:: idaes_connectivity.base.CSV
Expand All @@ -128,9 +143,10 @@ The Mermaid formatter writes out a Mermaid text description.

#### Jupyter

Mermaid is supported by newer versions of Jupyter Notebooks and Jupyter Lab.
The *display_connectivity* function allows one to easily display a diagram in a Jupyter notebook.
This function is also shown in the [Jupyter Notebook example](./example.md).
Mermaid is supported by newer versions of Jupyter Notebooks and Jupyter Lab. The
*display_connectivity* function allows one to easily display a diagram in a
Jupyter notebook. This function is also shown in the
[Jupyter Notebook example](./example.md).

```{eval-rst}
.. autofunction:: idaes_connectivity.jupyter.display_connectivity
Expand Down
71 changes: 37 additions & 34 deletions docs_src/diagrams.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,24 @@ myst:
# Making diagrams

Formatting of connectivity information as a diagram relies on external tools.
While this does require an extra step to install and run these tools, it also provides flexibility and leverages the full power of the tools' user communities.
While this does require an extra step to install and run these tools, it also
provides flexibility and leverages the full power of the tools' user
communities.

Below are links an instructions for the supported tools, [](mermaid-tool) and [](d2-tool).
Both tools do roughly the same thing: create diagrams from text, with automatic layout. Mermaid, written in JavaScript, has an online editor and built-in support in GitHub and Jupyter notebooks whereas D2, written in Go, has more flexible layout options and is easier to use in the console.
Below are links an instructions for the supported tools, [](mermaid-tool) and
[](d2-tool). Both tools do roughly the same thing: create diagrams from text,
with automatic layout. Mermaid, written in JavaScript, has an online editor and
built-in support in GitHub and Jupyter notebooks whereas D2, written in Go, has
more flexible layout options and is easier to use in the console.

(mermaid-tool)=
## Mermaid
[Mermaid](https://mermaid.js.org/) describes itself as a "JavaScript based diagramming and charting tool that renders Markdown-inspired text definitions to create and modify diagrams dynamically".
The {{ proj }} can create Mermaid text definitions and also has a convenience function to help render Mermaid diagrams inside a Jupyter Notebook.

### Generate text definition
For example, to generate Mermaid text from connectivity data in the CSV file *model_conn.csv*,
you could run:
For example, to generate Mermaid text from connectivity data in the CSV file
*model_conn.csv*, you could run:

```
idaes-conn --to mermaid model_conn.csv -O "-"
Expand All @@ -30,7 +35,8 @@ This will print the text definition of the diagram to the console.

#### Jupyter / GitHub

You could then paste the output into a Jupyter Notebook markdown cell, or GitHub markdown page, like this:
You could then paste the output into a Jupyter Notebook markdown cell, or GitHub
markdown page, like this:

:::{code}
```mermaid
Expand All @@ -40,47 +46,39 @@ You could then paste the output into a Jupyter Notebook markdown cell, or GitHub

#### Online Editor

You could also load these diagrams into the online [Mermaid Live Editor](https://mermaid.live/).
You could also load these diagrams into the online
[Mermaid Live Editor](https://mermaid.live/).

#### Console
If you want to generate the diagram locally, and are willing and able to install NodeJS packages on your machine, then follow these instructions:
If you want to generate the diagram locally, and are willing and able to install
NodeJS packages on your machine, then follow these instructions:

First install the mermaid-cli with the [Node Package Manager](https://www.npmjs.com/) (install that first if you don't have it):
First install the mermaid-cli with the
[Node Package Manager](https://www.npmjs.com/) (install that first if you don't
have it):
```
npm install @mermaid-js/mermaid-cli
```
This will install wherever you ran the command. Make sure you run the next commands in the same directory.
Next, paste the following into a script we will call `run-mermaid.js`:
```
const { run } = await import("@mermaid-js/mermaid-cli");
const input_file = process.argv[2];
const output_file = process.argv[3];
console.log("Generating " + output_file + " from " + input_file);
await run(input_file, output_file);
```

An optional but useful step to avoid some warnings: edit the *package.json* file (this was created when you did the `npm install` command) and add a line at the top.
```
{
"type": "module",
# .. rest of file ..
}
```
This will install wherever you ran the command. Make sure you run the next
commands in the same directory.

Finally, you can convert a Mermaid diagram to an SVG (Scalable Vector Graphics image) file with this command:
Now, you can generate an image with the `idaes-conn` command by adding the
option `--mmdc`:
```
node run-mermaid.js <INPUT.mmd> <OUTPUT.svg>
idaes-conn --to mermaid --mmdc model_conn.csv -O model_diagram.png
```

The `--to mermaid` is optional as Mermaid is the default.

(d2-tool)=
## D2
[Declarative Diagramming (D2)](https://d2lang.com/) describes itself as "A modern language that turns text to diagrams".
Like Mermaid, D2 generates a SVG diagram from a simple text description.
[Declarative Diagramming (D2)](https://d2lang.com/) describes itself as "A
modern language that turns text to diagrams". Like Mermaid, D2 generates a SVG
diagram from a simple text description.

### Generate text definition
For example, to generate D2 text from connectivity data in the CSV file *model_conn.csv*,
you could run:
For example, to generate D2 text from connectivity data in the CSV file
*model_conn.csv*, you could run:
```
idaes-conn --to d2 model_conn.csv -O model_conn.d2
```
Expand All @@ -89,12 +87,17 @@ This will print the text definition of the diagram to the file *model_conn.d2*

### Generate diagram

To generate the diagram, [install D2 on your computer](https://d2lang.com/tour/install) and then run the `d2` command-line interface (CLI) with the file you generated above as input:
To generate the diagram,
[install D2 on your computer](https://d2lang.com/tour/install) and then run the
`d2` command-line interface (CLI) with the file you generated above as input:
```
d2 model_conn.d2 model_conn.svg
```

There are numerous options to the `d2` CLI that can help modify layout and style, as well as the program behavior. For example, specifying an output file with ".png" as the suffix will generate a PNG image. Run `d2 -h` to see them and/or visit the documentation on the website.
There are numerous options to the `d2` CLI that can help modify layout and
style, as well as the program behavior. For example, specifying an output file
with ".png" as the suffix will generate a PNG image. Run `d2 -h` to see them
and/or visit the documentation on the website.

```{note}
Unlike Mermaid, D2 does not have an online editor or Jupyter integration. On the other hand, generating diagrams locally is straightforward.
Expand Down
2 changes: 2 additions & 0 deletions idaes_connectivity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from idaes_connectivity import version

__version__ = version.VERSION

from idaes_connectivity.base import Mermaid, MermaidImage, Connectivity
137 changes: 133 additions & 4 deletions idaes_connectivity/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,20 @@
import logging
from pathlib import Path
import re
import sys
from typing import TextIO, Tuple, Union, Optional, List, Dict
import shutil
import subprocess
from tempfile import NamedTemporaryFile
import time
from typing import TextIO, Tuple, Union, Optional, List, Dict, Any
import warnings

from PIL import Image as im
import base64
import io, requests

# third-party
from IPython.display import Markdown

try:
import pyomo
from pyomo.network import Arc
Expand Down Expand Up @@ -623,8 +628,57 @@ class Formatter(abc.ABC):

defaults = {} # extend in subclasses

def __init__(self, connectivity: Connectivity, **kwargs):
self._conn = connectivity
def __init__(self, connectivity: Connectivity | Any, **kwargs):
"""Constructor.

Arguments:
connectivity: Either a Connectivity instance or any valid value
for `input_*` arguments that could be passed to
create a Connectivity instance.

Raises:
ValueError: Unable to construct Connectivity instance from provided arg
"""
if isinstance(connectivity, Connectivity):
self._conn = connectivity
else:
self._conn = self._connectivity_factory(connectivity)

def _connectivity_factory(self, arg) -> Connectivity:
kwargs = {}
# a string can be many things:
# path, CSV, module name
if isinstance(arg, str):
try:
# is this a path?
path = Path(arg)
if not path.exists():
raise ValueError()
kwargs["input_file"] = path
except ValueError:
# not a path. is it a CSV blob?
csv_data = arg.split("\n")
if len(csv_data) > 1:
hdr = csv_data[0].split(",")
if len(hdr) > 1:
kwargs["input_data"] = arg
else:
# not CSV. is it a module name?
kwargs["input_module"] = arg
# things that are specifically file paths
elif isinstance(arg, TextIO) or isinstance(arg, Path):
kwargs["input_file"] = arg
# otherwise it is probably a model
elif hasattr(arg, "component_objects"):
kwargs["input_model"] = arg

if not kwargs: # nothing matched!
raise ValueError(
"Argument is not an input file, Pyomo/IDAES model, "
"module name, or CSV text data"
)

return Connectivity(**kwargs)

@staticmethod
def _parse_direction(d):
Expand Down Expand Up @@ -776,6 +830,11 @@ def _start_image_server(self, component_image_dir: Path | str) -> bool:

return started

def _repr_markdown_(self):
"""Display using Markdown in a Jupyter Notebook."""
graph_str = self.write(None)
return f"```mermaid\n{graph_str}\n```"

def write(self, output_file: Union[str, TextIO, None]) -> Optional[str]:
"""Write Mermaid text description."""
f = self._get_output_stream(output_file)
Expand Down Expand Up @@ -901,6 +960,76 @@ def _clean_stream_label(label):
return label


class MermaidImage(Formatter):
"""Formatter that calls mermaid-cli command line program (mmdc) in order to
generate the diagram as an aimage file.

For more information about mermaid-cli, see https://github.com/mermaid-js/mermaid-cli

Example usage::

from idaes_connectivity.base import Connectivity, MermaidImage
# somehow create connectivity object, e.g. from a CSV file
conn = Connectivity(input_file="idaes_connectivity/tests/example_flowsheet.csv")
# create an image
MermaidImage(conn).write("flowsheet_diagram.png")
"""

def __init__(self, conn: Connectivity, **kwargs):
"""Constructor.

Args:
conn: Connectivity to graph
kwargs: Same as for the `Mermaid` class, except for an additional
section for (optional) keywords related to the mmdc program::
{ "mmdc":
"bin": "<path>", # path to the binary
"options": ["<opt>", "<opt2>", ..] # extra CLI opts
}
"""
if "mmdc" in kwargs:
self._bin = kwargs["mmdc"].get("bin", self.find_mmdc())
self._opt = kwargs["mmdc"].get("options", [])
del kwargs["mmdc"]
else:
self._bin = self.find_mmdc()
self._opt = []
self._formatter = Mermaid(conn, **kwargs)

def write(self, output_file: Path | str):
"""Write to image file.

Arguments:
output_file: Image file name. Extension determines image type, as decided
by the mermaid-cli program.
"""
_log.info(f"Use 'mmdc' to create output in '{output_file}'")
# write mermaid output to a named temporary file
tmpfile = NamedTemporaryFile(mode="w")
self._formatter.write(tmpfile)
tmpfile.flush()
if _log.isEnabledFor(logging.DEBUG):
with open(tmpfile.name, "r") as f:
buf = f.read()
_log.debug(f"Contents of temporary file ({tmpfile.name}):\n{buf}")
time.sleep(1) # lame, but safer
# run mmdc on temporary file, writing its image output to user-provided file
args = [self._bin, "-i", tmpfile.name]
if not hasattr(output_file, "close"): # e.g. stdout
args.extend(["-o", output_file])
args += self._opt
_log.info(f"running: {' '.join(args)}")
try:
subprocess.check_call(args, stderr=subprocess.DEVNULL)
except (subprocess.CalledProcessError, FileNotFoundError) as err:
raise RuntimeError(err)

@staticmethod
def find_mmdc() -> str | None:
"""Find CLI program for mermaid-cli (mmdc)."""
return shutil.which("mmdc")


class D2(Formatter):
"""Create output in Terraform D2 syntax.

Expand Down
Loading
Loading