Return the `DOT`_ (graph) description of an Expression tree as a string

    Args:
        expr (object): The expression to render into a graph. Typically an
            instance of :class:`~qnet.algebra.abstract_algebra.Expression`, but
            with appropriate `get_children`, `labelfunc`, and `id_func`, this
            could be any tree-like object
        styles (list or None): A list of tuples ``(expr_filter, style_dict)``
            where ``expr_filter`` is a callable and ``style_dict`` is a list
            of `DOT`_ node properties that should be used when rendering a node
            for which ``expr_filter(expr)`` return True.
        maxdepth (int or None): The maximum depth of the resulting tree (any
            node at `maxdepth` will be drawn as a leaf)
        repeat (bool): By default, if identical sub-expressions occur in
            multiple locations (as identified by `idfunc`, they will be
            repeated in the graph. If ``repeat=False`` is given, each unique
            (sub-)expression is only drawn once.  The resulting graph may no
            longer be a proper tree, as recurring expressions will have
            multiple parents.
        labelfunc (callable): A function that receives `expr` and a boolean
            ``is_leaf`` and returns the label of the corresponding node in the
            graph. Defaults to ``expr_labelfunc(str, str)``.
        idfunc (callable or None): A function that returns the ID of the node
            representing a given expression. Expressions for which `idfunc`
            returns identical results are considered identical if `repeat` is
            False. The default value None uses a function that is appropriate
            to a single standalone DOT file. If this is insufficient, something
            like ``hash`` or ``str`` would make a good `idfunc`.
        get_children (callable): A function that return a list of
            sub-expressions (the children of `expr`). Defaults to the operands
            of an :class:`~qnet.algebra.abstract_algebra.Operation` (thus,
            anything that is not an Operation is a leaf)
        kwargs: All further keyword arguments set custom `DOT`_ graph
            attributes

    Returns:
        str: a multiline str representing a graph in the `DOT`_ language

    Notes:
        The node `styles` are additive. For example, consider the following
        custom styles::

            styles = [
                (lambda expr: isinstance(expr, SCALAR_TYPES),
                    {'color': 'blue', 'shape': 'box', 'fontsize': 12}),
                (lambda expr: isinstance(expr, Expression),
                    {'color': 'red', 'shape': 'box', 'fontsize': 12}),
                (lambda expr: isinstance(expr, Operation),
                    {'color': 'black', 'shape': 'ellipse'})]

        For Operations (which are a subclass of Expression) the color and shape
        are overwritten, while the fontsize 12 is inherited.

        Keyword arguments are directly translated into graph styles. For
        example, in order to produce a horizontal instead of vertical graph,
        use ``dotprint(..., rankdir='LR')``.

    See also:
        :func:`sympy.printing.dot.dotprint` provides an equivalent function for
        SymPy expressions.

def dotprint(
        expr, styles=None, maxdepth=None, repeat=True,
        labelfunc=expr_labelfunc(str, str),
        idfunc=None, get_children=_op_children, **kwargs):
    """Return the `DOT`_ (graph) description of an Expression tree as a string

    Args:
        expr (object): The expression to render into a graph. Typically an
            instance of :class:`~qnet.algebra.abstract_algebra.Expression`, but
            with appropriate `get_children`, `labelfunc`, and `id_func`, this
            could be any tree-like object
        styles (list or None): A list of tuples ``(expr_filter, style_dict)``
            where ``expr_filter`` is a callable and ``style_dict`` is a list
            of `DOT`_ node properties that should be used when rendering a node
            for which ``expr_filter(expr)`` return True.
        maxdepth (int or None): The maximum depth of the resulting tree (any
            node at `maxdepth` will be drawn as a leaf)
        repeat (bool): By default, if identical sub-expressions occur in
            multiple locations (as identified by `idfunc`, they will be
            repeated in the graph. If ``repeat=False`` is given, each unique
            (sub-)expression is only drawn once.  The resulting graph may no
            longer be a proper tree, as recurring expressions will have
            multiple parents.
        labelfunc (callable): A function that receives `expr` and a boolean
            ``is_leaf`` and returns the label of the corresponding node in the
            graph. Defaults to ``expr_labelfunc(str, str)``.
        idfunc (callable or None): A function that returns the ID of the node
            representing a given expression. Expressions for which `idfunc`
            returns identical results are considered identical if `repeat` is
            False. The default value None uses a function that is appropriate
            to a single standalone DOT file. If this is insufficient, something
            like ``hash`` or ``str`` would make a good `idfunc`.
        get_children (callable): A function that return a list of
            sub-expressions (the children of `expr`). Defaults to the operands
            of an :class:`~qnet.algebra.abstract_algebra.Operation` (thus,
            anything that is not an Operation is a leaf)
        kwargs: All further keyword arguments set custom `DOT`_ graph
            attributes

    Returns:
        str: a multiline str representing a graph in the `DOT`_ language

    Notes:
        The node `styles` are additive. For example, consider the following
        custom styles::

            styles = [
                (lambda expr: isinstance(expr, SCALAR_TYPES),
                    {'color': 'blue', 'shape': 'box', 'fontsize': 12}),
                (lambda expr: isinstance(expr, Expression),
                    {'color': 'red', 'shape': 'box', 'fontsize': 12}),
                (lambda expr: isinstance(expr, Operation),
                    {'color': 'black', 'shape': 'ellipse'})]

        For Operations (which are a subclass of Expression) the color and shape
        are overwritten, while the fontsize 12 is inherited.

        Keyword arguments are directly translated into graph styles. For
        example, in order to produce a horizontal instead of vertical graph,
        use ``dotprint(..., rankdir='LR')``.

    See also:
        :func:`sympy.printing.dot.dotprint` provides an equivalent function for
        SymPy expressions.
    """
    # the routine is called 'dotprint' to match sympy (even though most of the
    # similar routines for the other printers are called e.g. 'latex', not
    # 'latexprint'
    if idfunc is None:
        if repeat:
            idfunc = lambda expr: 'node'
        else:
            idfunc = hash
    graphstyle = {'rankdir': 'TD', 'ordering': 'out'}
    graphstyle.update(kwargs)
    nodes = []
    edges = []
    level = 0
    pos = 0
    pos_counter = defaultdict(int)  # level => current pos
    stack = [(level, pos, expr)]
    if styles is None:
        styles = []
    while len(stack) > 0:
        level, pos, expr = stack.pop(0)
        node_id = _node_id(expr, (level, pos), idfunc, repeat)
        children = get_children(expr)
        is_leaf = len(children) == 0
        if maxdepth is not None and level >= maxdepth:
            is_leaf = True
        style = _styleof(expr, styles)
        style['label'] = labelfunc(expr, is_leaf)
        nodes.append('"%s" [%s];' % (node_id, _attrprint(style)))
        if not is_leaf:
            try:
                for expr_sub in children:
                    i_sub = pos_counter[level+1]
                    id_sub = _node_id(
                        expr_sub, (level+1, i_sub), idfunc, repeat)
                    edges.append('"%s" -> "%s"' % (node_id, id_sub))
                    stack.append((level+1, i_sub, expr_sub))
                    pos_counter[level+1] += 1
            except AttributeError:
                pass
    return template % {
        'graphstyle': _attrprint(graphstyle, delimiter='\n'),
        'nodes': '\n'.join(nodes),
        'edges': '\n'.join(edges)}