docs: 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)}