Source code for forml.provider.runner.graphviz
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""
Runtime that just renders the pipeline DAG visualization.
"""
import logging
import pathlib
import typing
import graphviz as grviz
from forml import flow, runtime, setup
from forml.pipeline import payload
if typing.TYPE_CHECKING:
from forml import io
from forml.io import asset
LOGGER = logging.getLogger(__name__)
[docs]class Runner(runtime.Runner, alias='graphviz'):
"""(Pseudo)runner using the :doc:`Graphviz drawing software <graphviz:index>` for rendering
graphical visualization of the workflow task graph.
The workflow obviously does not get really executed!
For better readability, the runner is using the following shapes to plot the different objects:
=========== =======================================
Shape Meaning
=========== =======================================
Square box Actor in train mode.
Round box Actor in apply mode.
Ellipse System actor for output port selection.
Cylinder System actor for state persistence.
Solid edge Data transfer.
Dotted edge State transfer.
=========== =======================================
Args:
filepath: Target path for producing the DOT file.
view: If True, open the rendered result with the default application.
options: Any of the supported (and non-colliding) :class:`graphviz.Digraph` keyword
arguments.
The provider can be enabled using the following :ref:`platform configuration <platform-config>`:
.. code-block:: toml
:caption: config.toml
[RUNNER.visual]
provider = "graphviz"
format = "svg"
engine = "dot"
graph_attr = { rankdir = "LR", bgcolor = "transparent" }
node_attr = { nodesep = "0.75", ranksep = "0.75" }
edge_attr = { weight = "1.2" }
Important:
Select the ``graphviz`` :ref:`extras to install <install-extras>` ForML together with the
Graphviz support. Additionally, download and install also the `native Graphviz system
binary <https://www.graphviz.org/download/>`_ (OS specific procedure).
"""
FILEPATH = f'{setup.APPNAME}.dot'
OPTIONS = {'graph_attr': {'bgcolor': 'transparent'}}
def __init__(
self,
instance: typing.Optional['asset.Instance'] = None,
feed: typing.Optional['io.Feed'] = None,
sink: typing.Optional['io.Sink'] = None,
filepath: typing.Optional[typing.Union[str, pathlib.Path]] = None,
view: bool = True,
**options: typing.Any,
):
sniffer: typing.Optional[payload.Sniff] = None
if isinstance(sink, runtime.Virtual.Sink):
sniffer = sink._sniffer # pylint: disable=protected-access
super().__init__(instance, feed, sink, filepath=filepath, view=view, options=options, sniffer=sniffer)
@classmethod
def run(cls, symbols: typing.Collection[flow.Symbol], **kwargs) -> None:
dot: grviz.Digraph = grviz.Digraph(**(cls.OPTIONS | kwargs['options']))
for sym in symbols:
nodekw = {'shape': 'ellipse'}
outkw = {'style': 'solid'}
if isinstance(sym.instruction, flow.Functor):
nodekw.update(shape='box')
if flow.Train not in sym.instruction.action:
nodekw.update(style='rounded')
elif isinstance(sym.instruction, (flow.Loader, flow.Dumper, flow.Committer)):
nodekw.update(shape='cylinder')
outkw.update(style='dotted')
dot.node(repr(id(sym.instruction)), repr(sym.instruction), **nodekw)
for idx, arg in enumerate(sym.arguments):
inkw = dict(outkw)
if isinstance(arg, (flow.Loader, flow.Dumper)) or (
isinstance(arg, flow.Functor) and flow.Train in arg.action
):
inkw.update(style='dotted')
dot.edge(repr(id(arg)), repr(id(sym.instruction)), label=repr(idx), **inkw)
dot.render(pathlib.Path(kwargs['filepath'] or cls.FILEPATH), view=kwargs['view'])
if kwargs['sniffer'] and (client := kwargs['sniffer']._client): # pylint: disable=protected-access
client.set(dot)