Source code for

# Copyright Iris contributors
# This file is part of Iris and is released under the BSD license.
# See LICENSE in the root of the repository for full licensing details.
"""Provides Creation and saving of DOT graphs for a :class:`iris.cube.Cube`."""

import os
import subprocess

import iris
from iris._deprecation import warn_deprecated
import iris.util

wmsg = (
    " has been deprecated and will be removed in a "
    "future release. If you make use of this functionality, please contact "
    "the Iris Developers to discuss how to retain it (which may involve "
    "reversing the deprecation)."

_GRAPH_INDENT = " " * 4


def _dot_path():

    if _DOT_CHECKED:
        path = _DOT_EXECUTABLE_PATH
        path = iris.config.get_option("System", "dot_path", default="dot")
        if not os.path.exists(path):
            if not os.path.isabs(path):
                    # Check PATH
                    subprocess.check_output([path, "-V"], stderr=subprocess.STDOUT)
                except (OSError, subprocess.CalledProcessError):
                    path = None
                path = None
        _DOT_EXECUTABLE_PATH = path
        _DOT_CHECKED = True
    return path

#: Whether the 'dot' program is present (required for "dotpng" output).
DOT_AVAILABLE = _dot_path() is not None

[docs] def save(cube, target): """Save a dot representation of the cube. Parameters ---------- cube : :class:`iris.cube.Cube` target : A filename or open file handle. See Also -------- : Save one or more Cubes to file (or other writeable). """ if isinstance(target, str): dot_file = open(target, "wt") elif hasattr(target, "write"): if hasattr(target, "mode") and "b" in target.mode: raise ValueError("Target is binary") dot_file = target else: raise ValueError("Can only save dot to filename or filehandle") try: dot_file.write(cube_text(cube)) finally: if isinstance(target, str): dot_file.close()
[docs] def save_png(source, target, launch=False): """Produce a "dot" instance diagram by calling dot. Produce a "dot" instance diagram by calling dot and optionally launching the resulting image. Parameters ---------- source : :class:`iris.cube.Cube`, or dot filename target : A filename or open file handle. If passing a file handle, take care to open it for binary output. launch : bool, default=False Display the image. Default is False. See Also -------- : Save one or more Cubes to file (or other writeable). """ # From cube or dot file? if isinstance(source, iris.cube.Cube): # Create dot file dot_file_path = iris.util.create_temp_filename(".dot") save(source, dot_file_path) elif isinstance(source, str): dot_file_path = source else: raise ValueError("Can only write dot png for a Cube or DOT file") # Create png data if not _dot_path(): raise ValueError( 'Executable "dot" not found: ' "Review dot_path setting in site.cfg." ) # To filename or open file handle? if isinstance(target, str):[_dot_path(), "-T", "png", "-o", target, dot_file_path]) elif hasattr(target, "write"): if hasattr(target, "mode") and "b" not in target.mode: raise ValueError("Target not binary")[_dot_path(), "-T", "png", dot_file_path], stdout=target) else: raise ValueError("Can only write dot png for a filename or writable") # Display? if launch: if == "mac":"open", target)) elif == "nt":"start", target)) elif == "posix":"firefox", target)) else: raise iris.exceptions.NotYetImplementedError( "Unhandled operating system. The image has been created in %s" % target ) # Remove the dot file if we created it if isinstance(source, iris.cube.Cube): os.remove(dot_file_path)
[docs] def cube_text(cube): """Return a DOT text representation a `iris.cube.Cube`. Parameters ---------- cube : The cube for which to create DOT text. """ # We use r'' type string constructor as when we type \n in a string without the r'' constructor # we get back a new line character - this is not what DOT expects. # Therefore, newline characters should be created explicitly by having multi-lined strings. relationships = r"" relationships_association = r"" dimension_nodes = r""" subgraph clusterCubeDimensions { label="Cube data" """ # TODO: Separate dim_coords from aux_coords. coord_nodes = r""" subgraph clusterCoords { label = "Coords" """ coord_system_nodes = r""" subgraph clusterCoordSystems { label = "CoordSystems" """ for i, size in enumerate(cube.shape): dimension_nodes += "\n" + _dot_node( _SUBGRAPH_INDENT, "CubeDimension_" + str(i), str(i), [("len", size)], ) # Coords and their coord_systems coords = sorted(cube.coords(), key=lambda c: written_cs = [] for i, coord in enumerate(coords): coord_label = "Coord_" + str(i) coord_nodes += _coord_text(coord_label, coord) cs = coord.coord_system if cs: # Create the cs node - or find an identical, already written cs if cs not in written_cs: written_cs.append(cs) uid = written_cs.index(cs) coord_system_nodes += _coord_system_text(cs, uid) else: uid = written_cs.index(cs) relationships += '\n "%s" -> "CoordSystem_%s_%s"' % ( coord_label, coord.coord_system.__class__.__name__, uid, ) relationships += '\n ":Cube" -> "%s"' % coord_label # Are there any relationships to data dimensions? dims = cube.coord_dims(coord) for dim in dims: relationships_association += '\n "%s" -> "CubeDimension_%s":w' % ( coord_label, dim, ) dimension_nodes += """ } """ coord_nodes += """ } """ coord_system_nodes += """ } """ # return a string pulling everything together template = """ digraph CubeGraph{ rankdir = "LR" fontname = "Bitstream Vera Sans" fontsize = 8 node [ fontname = "Bitstream Vera Sans" fontsize = 8 shape = "record" ] # Nodes %(cube_node)s %(dimension_nodes)s %(coord_nodes)s %(coord_sys_nodes)s edge [ arrowhead = "normal" ] # RELATIONSHIPS # Containment %(relationships)s edge [ style="dashed" arrowhead = "onormal" ] # Association %(associations)s } """ cube_attributes = list(sorted(cube.attributes.items(), key=lambda item: item[0])) cube_node = _dot_node(_GRAPH_INDENT, ":Cube", "Cube", cube_attributes) res_string = template % { "cube_node": cube_node, "dimension_nodes": dimension_nodes, "coord_nodes": coord_nodes, "coord_sys_nodes": coord_system_nodes, "relationships": relationships, "associations": relationships_association, } return res_string
def _coord_text(label, coord): """Return a string containing the dot representation for a single coordinate node. Parameters ---------- label : The dot ID of the coordinate node. coord : The coordinate to convert. """ # Which bits to write? # Note: This is is not very OO but we are achieving a separation of DOT from cdm by doing this. if isinstance(coord, iris.coords.DimCoord): _dot_attrs = ("standard_name", "long_name", "units", "circular") elif isinstance(coord, iris.coords.AuxCoord): _dot_attrs = ("standard_name", "long_name", "units") else: raise ValueError("Unhandled coordinate type: " + str(type(coord))) attrs = [(name, getattr(coord, name)) for name in _dot_attrs] if coord.attributes: custom_attrs = sorted(coord.attributes.items(), key=lambda item: item[0]) attrs.extend(custom_attrs) node = _dot_node(_SUBGRAPH_INDENT, label, coord.__class__.__name__, attrs) return node def _coord_system_text(cs, uid): """Return string containing dot representation for a single coordinate system node. Parameters ---------- cs : The coordinate system to convert. uid : The uid allows/distinguishes non-identical CoordSystems of the same type. """ attrs = [] for k, v in cs.__dict__.items(): if isinstance(v, iris.cube.Cube): attrs.append((k, "defined")) else: attrs.append((k, v)) attrs.sort(key=lambda attr: attr[0]) label = "CoordSystem_%s_%s" % (cs.__class__.__name__, uid) node = _dot_node(_SUBGRAPH_INDENT, label, cs.__class__.__name__, attrs) return node def _dot_node(indent, id, name, attributes): """Return a string containing the dot representation for a single node. Parameters ---------- id : The ID of the node. name : The visual name of the node. attributes : An iterable of (name, value) attribute pairs. """ # noqa: D410, D411 attributes = r"\n".join("%s: %s" % item for item in attributes) template = """%(indent)s"%(id)s" [ %(indent)s label = "%(name)s|%(attributes)s" %(indent)s] """ node = template % { "id": id, "name": name, "attributes": attributes, "indent": indent, } return node