3. Plotting and Debugging

Plotting

For Errors & debugging it is necessary to visualize the graph-operation. You may plot any plottable and annotate on top the execution plan and solution of the last computation, calling methods with arguments like this:

netop.plot(True)                   # open a matplotlib window
netop.plot("netop.svg")            # other supported formats: png, jpg, pdf, ...
netop.plot()                       # without arguments return a pydot.DOT object
netop.plot(solution=solution)      # annotate graph with solution values
solution.plot()                    # plot solution only

… or for the last …:

solution.plot(...)
execution plan
Graphtik Legend

The legend for all graphtik diagrams, generated by legend().

The same Plottable.plot() method applies also for:

each one capable to producing diagrams with increasing complexity. Whenever possible, the top-level plot() methods will delegate to the ones below; specifically, the netop keeps a transient reference to the last plan. BUT the plan does not hold such a reference, you have to plot the solution.

For instance, when a net-operation has just been composed, plotting it will come out bare bone, with just the 2 types of nodes (data & operations), their dependencies, and the sequence of the execution-plan.

barebone graph

But as soon as you run it, the net plot calls will print more of the internals. Internally it delegates to ExecutionPlan.plot() of NetworkOperation.last_plan attribute, which caches the last run to facilitate debugging. If you want the bare-bone diagram, plot the network:

netop.net.plot(...)

If you want all details, plot the solution:

solution.net.plot(...)

Note

For plots, Graphviz program must be in your PATH, and pydot & matplotlib python packages installed. You may install both when installing graphtik with its plot extras:

pip install graphtik[plot]

Tip

A description of the similar API to pydot.Dot instance returned by plot() methods is here: https://pydotplus.readthedocs.io/reference.html#pydotplus.graphviz.Dot

Jupyter notebooks

The pydot.Dot instances returned by Plottable.plot() are rendered directly in Jupyter/IPython notebooks as SVG images.

You may increase the height of the SVG cell output with something like this:

netop.plot(jupyter_render={"svg_element_styles": "height: 600px; width: 100%"})

See default_jupyter_render for those defaults and recommendations.

Sphinx-generated sites

This library contains a new Sphinx extension (adapted from the sphinx.ext.doctest) that can render plottables in sites from python code in “doctests”.

To enabled it, append module graphtik.sphinxext as a string in you docs/conf.py : extensions list, and then intersperse the graphtik or graphtik-output directives with regular doctest-code to embed graph-plots into the site; you may refer to those plotted graphs with the graphtik role referring to their :name: option(see Examples below).

Hint

Note that Sphinx is not doctesting the actual python modules, unless the plotting code has ended up, somehow, in the site (e.g. through some autodoc directive). Contrary to pytest and doctest standard module, the module’s globals are not imported (until sphinx#6590 is resolved), so you may need to import it in your doctests with e.g. a testsetup directive, like this:

.. testsetup::

   from <this.module> import *
   __name__ = <this.module>

Unfortunately, you cannot use relative import, and have to write your module’s full name.

Directives

.. graphtik::

Renders a figure with a graphtik plots from doctest code.

It supports:

  • all configurations from sphinx.ext.doctest sphinx-extension, plus those described below, in Configurations.

  • all options from ‘doctest’ directive,

    • hide

    • options

    • pyversion

    • skipif

  • these options from image directive, except target (plot elements may already link to URLs):

    • height

    • width

    • scale

    • class

    • alt

  • these options from figure directive:

    • name

    • align

    • figwidth

    • figclass

  • and the following new options:

    • graphvar

    • graph-format

    • caption

Specifically the “interesting” options are these:

:graphvar: (string, optional) varname (`str`)

the variable name containing what to render, which it can be:

If missing, it renders the last variable in the doctest code assigned with the above types.

:graph-format: png | svg | svgz | pdf | `None` (choice, default: `None`)
if None, format decided according to active builder, roughly:
  • “html”-like: svg

  • “latex”: pdf

Note that SVGs support zooming, tooltips & URL links, while PNGs support image maps for linkable areas.

:zoomable: <empty>, (true, 1, yes, on) | (false, 0, no, off) (`bool`)

Enable/disable interactive pan+zoom of SVGs; if missing/empty, graphtik_zoomable assumed.

:zoomable-opts: <empty>, (true, 1, yes, on) | (false, 0, no, off) (`str`)

A JS-object with the options for the interactive zoom+pan pf SVGs. If missing, graphtik_zoomable_options assumed. Specify {} explicitly to force library’s default options.

:name: link target id (`str`)

Make this netop a hyperlink target identified by this name. If :name: given and no :caption: given, one is created out of this, to act as a permalink.

:caption: figure's caption (`str`)

Text to put underneath the netop.

:alt: (`str`)

If not given, derived from string representation of the netop.

.. graphtik-output::

Like graphtik, but works like doctest’s testoutput directive.

:graphtik:

An interpreted text role to refer to graphs plotted by graphtik or graphtik-output directives by their :name: option.

Configurations

graphtik_default_graph_format
  • type: Union[str, None]

  • default: None

The file extension of the generated plot images (without the leading dot .`), used when no :graph-format: option is given in a graphtik or graphtik-output directive.

If None, the format is chosen from graphtik_graph_formats_by_builder configuration.

graphtik_graph_formats_by_builder
  • type: Map[str, str]

  • default: check the sources

a dictionary defining which plot image formats to choose, depending on the active builder.

  • Keys are regexes matching the name of the active builder;

  • values are strings from the supported formats for pydot library, e.g. png (see supported_plot_formats()).

If a builder does not match to any key, and no format given in the directive, no graphtik plot is rendered; so by default, it only generates plots for html & latex.

graphtik_zoomable_svg
  • type: bool

  • default: True

Whether to render SVGs with the zoom-and-pan javascript library, unless the :zoomable: directive-option is given (and not empty).

Attention

Zoom-and-pan does not work in Sphinx sites for Chrome locally - serve the HTML files through some HTTP server, e.g. launch this command to view the site of this project:

python -m http.server 8080 --directory build/sphinx/html/
graphtik_zoomable_options
  • type: str

  • default: {controlIconsEnabled: true, zoomScaleSensitivity: 0.4, fit: true}

A JS-object with the options for the interactive zoom+pan pf SVGs, when the :zoomable-opts: directive option is missing. If empty, {} assumed (library’s default options).

graphtik_plot_keywords
  • type: dict

  • default: {}

Arguments or build_pydot() to apply when rendering plottables.

graphtik_warning_is_error
  • type: bool

  • default: false

If false, suppress doctest errors, and avoid failures when building site with -W option, since these are unrelated to the building of the site.

doctest_test_doctest_blocks (foreign config)

Don’t disable doctesting of literal-blocks, ie, don’t reset the doctest_test_doctest_blocks configuration value, or else, such code would be invisible to graphtik directive.

trim_doctest_flags (foreign config)

This configuration is forced to False (default was True).

Attention

This means that in the rendered site, options-in-comments like # doctest: +SKIP and <BLACKLINE> artifacts will be visible.

Plot customizations

plotters` & style constants

Rendering of plots is performed by plot.Plotter instances. Simple values theming Graphviz attributes are defined on the plot.Style class, which is an attribute of plotter.

You may customize the styles and/or plotter behavior with various methods, ordered by breadth of the effects (most broadly effecting method at the top):

  1. Get and modify in-place the styles of the default active plotter, like that:

    get_active_plotter().style.kw_op["fillcolor"] = "purple"
    
    • This will affect all Plottable.plot() calls for a python session.

    • You cannot change the plotter instance with this method - only styles (and monkeypatching plotter’s methods).

  2. Create a new Plotter with customized Plotter.style, or clone and customize the styles of an existing plotter by the use of its Plotter.with_styles() method, and make that the new active plotter.

    • This will affect all calls in context.

    • If customizing style constants is not enough, you may subclass Plotter and install it.

  3. Take any plotter, customize its clone, and then call Plottable.plot(), with something like that:

    netop.plot(plotter=get_active_plotter().with_styles(kw_legend=None))
    

This project dogfoods (2) in its own docs/source/conf.py sphinx file. In particular, it configures the base-url of operation node links (by default, nodes do not link to any url).

Examples

The following directive renders a diagram of its doctest code, beneath it:

.. graphtik::
   :graphvar: addmul
   :name: addmul-operation

   >>> from graphtik import compose, operation
   >>> addmul = compose(
   ...       "addmul",
   ...       operation(name="add", needs="abc".split(), provides="ab")(lambda a, b, c: (a + b) * c)
   ... )

plottable: addmul-operation

which you may reference with this syntax:

you may :graphtik:`reference <addmul-operation>` with ...

Hint

In this case, the :graphvar: parameter is not really needed, since the code contains just one variable assignment receiving a subclass of Plottable or pydot.Dot instance.

Additionally, the doctest code producing the plottables does not have to be contained in the graphtik directive as a whole.

So the above could have been simply written like this:

>>> from graphtik import compose, operation
>>> addmul = compose(
...       "addmul",
...       operation(name="add", needs="abc".split(), provides="ab")(lambda a, b, c: (a + b) * c)
... )

.. graphtik::
   :name: addmul-operation

Errors & debugging

Graphs may become arbitrary deep. Launching a debugger-session to inspect deeply nested stacks is notoriously hard

As an aid, you may either increase the logging verbosity, enable the set_debug() configurations function, or both.

Tip

The various network objects & exceptions print augmented string-representations when debug() flag is enabled. Actually you may wrap the code you are interested in with this flag as “context-manager”, to get augmented print-outs for selected code-paths only.

Additionally, when some operation fails, the original exception gets annotated with the following properties, as a debug aid:

>>> from graphtik import compose, operation
>>> from pprint import pprint
>>> def scream(*args):
...     raise ValueError("Wrong!")
>>> try:
...     compose("errgraph",
...             operation(name="screamer", needs=['a'], provides=["foo"])(scream)
...     )(a=None)
... except ValueError as ex:
...     pprint(ex.jetsam)
{'aliases': None,
 'args': {'kwargs': {}, 'positional': [None], 'varargs': []},
 'network': Network(x3 nodes, x1 ops: screamer),
 'operation': FunctionalOperation(name='screamer', needs=['a'], provides=['foo'], fn='scream'),
 'outputs': None,
 'plan': ExecutionPlan(needs=['a'], provides=['foo'], x1 steps: screamer),
 'provides': None,
 'results_fn': None,
 'results_op': None,
 'solution': {'a': None},
 'task': OpTask(FunctionalOperation(name='screamer', needs=['a'], provides=['foo'], fn='scream'), sol_keys=['a'])}

In interactive REPL console you may use this to get the last raised exception:

import sys

sys.last_value.jetsam

The following annotated attributes might have meaningful value on an exception:

network

the innermost network owning the failed operation/function

plan

the innermost plan that executing when a operation crashed

operation

the innermost operation that failed

args

either the input arguments list fed into the function, or a dict with both args & kwargs keys in it.

outputs

the names of the outputs the function was expected to return

provides

the names eventually the graph needed from the operation; a subset of the above, and not always what has been declared in the operation.

fn_results

the raw results of the operation’s function, if any

op_results

the results, always a dictionary, as matched with operation’s provides

solution

an instance of Solution, contains inputs & outputs till the error happened; note that Solution.executed contain the list of executed operations so far.

Of course you may use many of the above “jetsam” values when plotting.

Note

The Plotting capabilities, along with the above annotation of exceptions with the internal state of plan/operation often renders a debugger session unnecessary. But since the state of the annotated values might be incomplete, you may not always avoid one.