Source code for ClearMap.Analysis.graphs.graph_gt

# -*- coding: utf-8 -*-
"""
graph_gt
========

Graph class and utilities built on the `graph-tool <https://graph-tool.skewed.de>`_ library.

:class:`Graph` is the central data structure for vasculature and network
analyses in ClearMap.  It wraps a ``graph_tool.Graph`` backend and adds:

* **Spatial embedding** — vertex coordinates in voxels
  (``coordinates``), physical units (``coordinates_units``), and atlas
  space (``coordinates_atlas``).
* **Edge geometry** — variable-length arrays stored along each edge that
  capture intermediate vertex positions, radii, and labels between branch
  points.  Two storage modes are supported: ``'graph'`` (a single flat
  array at graph level, indexed by per-edge start/end pairs) and ``'edge'``
  (variable-length vectors stored in each edge property).  Convert between
  them with :meth:`Graph.set_edge_geometry_type`.
* **Vessel typing** — properties for artery/vein binary labels, signal
  intensity, radii in voxels and µm, distance to surface, and atlas
  annotation.
* **Subgraph extraction** — :meth:`Graph.sub_graph` (vertex/edge masks),
  :meth:`Graph.sub_slice` (axis-aligned spatial window),
  :meth:`Graph.largest_component`.
* **Atlas annotation** — :meth:`Graph.annotate_properties` and
  :meth:`Graph.transform_properties` apply arbitrary callables to vertex
  or edge properties in one pass.
* **Morphological graph operations** — binary dilation, erosion, opening,
  and closing on vertex and edge label arrays, propagating along graph
  topology.
* **Persistence** — :meth:`Graph.save` / :meth:`Graph.load` via the
  graph-tool ``.gt`` format.

Typical usage
-------------
Graphs are produced by
:func:`~ClearMap.Analysis.graphs.graph_processing.graph_from_skeleton`
and then simplified by cleaning and reduction

See also
--------
:mod:`ClearMap.Analysis.graphs.graph_processing` :
    Graph construction, cleaning, reduction, and label tracing.
:class:`~ClearMap.pipeline_orchestrators.tube_map.VesselGraphProcessor` :
    Pipeline orchestrator that builds and annotates vasculature graphs.
"""
from __future__ import annotations

__author__ = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__ = 'GPLv3 - GNU General Public License v3 (see LICENSE)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__ = 'https://idisco.info'
__download__ = 'https://www.github.com/ChristophKirst/ClearMap2'


import copy
import numbers
import pathlib
from typing import Optional, Iterable, Dict
from pathlib import Path

import numpy as np

import graph_tool as gt
import graph_tool.util as gtu
import graph_tool.topology as gtt
import graph_tool.generation as gtg
import warnings

# fix graph tool saving / loading for very large arrays
import ClearMap.Analysis.graphs.graph as grp
from ClearMap.Analysis.graphs.type_conversions import dtype_to_gtype, gtype_from_source, vertex_property_map_to_python, \
  edge_property_map_to_python, vertex_property_map_from_python, set_vertex_property_map, edge_property_map_from_python, \
  set_edge_property_map
from ClearMap.Analysis.graphs.utils import pickler, unpickler, edges_to_vertices, scan_gt_props

from ClearMap.Utils.array_utils import remap_array_ranges

LARGE_GRAPH_N_EDGES_THRESHOLD = 10 ** 7

gt.gt_io.clean_picklers()
gt.gt_io.libgraph_tool_core.set_pickler(pickler)
gt.gt_io.libgraph_tool_core.set_unpickler(unpickler)


[docs] class Graph(grp.AnnotatedGraph): """Graph class to handle graph construction and analysis. Note ---- This is an interface from ClearMap graphs to graph_tool. """ DEFAULT_N_DIMS = 3 SCOPES = ("vertex", "edge", "graph") def __init__(self, name=None, n_vertices=None, edges=None, directed=None, vertex_coordinates=None, vertex_radii=None, edge_coordinates=None, edge_radii=None, edge_geometries=None, shape=None, vertex_labels=None, edge_labels=None, annotation=None, base=None, edge_geometry_type='graph'): self.path = '' if base is None: base = gt.Graph(directed=directed) self.base = base # add default graph properties self.add_graph_property('shape', None, dtype='object') self.add_graph_property('edge_geometry_type', edge_geometry_type, dtype='object') super(Graph, self).__init__(name=name, n_vertices=n_vertices, edges=edges, directed=directed, vertex_coordinates=vertex_coordinates, vertex_radii=vertex_radii, edge_coordinates=edge_coordinates, edge_radii=edge_radii, edge_geometries=edge_geometries, shape=shape, vertex_labels=None, edge_labels=None, annotation=None) else: self.base = base super(Graph, self).__init__(name=name) @property def base(self): return self._base @base.setter def base(self, value): if not isinstance(value, gt.Graph): raise ValueError('Base graph not a graph_tool Graph') self._base = value @property def directed(self): return self._base.is_directed() @directed.setter def directed(self, value): self._base.set_directed(value) @property def is_view(self): return isinstance(self.base, gt.GraphView) # ## Vertices @property def n_vertices(self): return self._base.num_vertices()
[docs] def vertex(self, vertex): if isinstance(vertex, gt.Vertex): return vertex else: return self._base.vertex(vertex)
[docs] def first_vertex(self): return self._base.vertices().next()
@property def vertices(self): return list(self.base.vertices())
[docs] def vertex_iterator(self): return self._base.vertices()
[docs] def vertex_index(self, vertex): return int(vertex)
[docs] def vertex_indices(self): return self._base.get_vertices()
[docs] def add_vertex(self, n_vertices=None, vertex=None): if n_vertices is not None: self._base.add_vertex(n_vertices) elif isinstance(vertex, int): self._base.vertex(vertex, add_missing=True) # elif isinstance(vertex, gt.Vertex): # v = self._base.add_vertex(1) # v = vertex #analysis:ignore else: raise ValueError('Cannot add vertices.')
[docs] def remove_vertex(self, vertex): self._base.remove_vertex(vertex)
[docs] def vertex_property(self, name, vertex=None, as_array=True): """ .. warning:: This risks creating a copy of the vertex property map if `as_array` is True. Parameters ---------- name vertex as_array Returns ------- """ try: v_prop = self._base.vertex_properties[name] except KeyError as err: raise KeyError(f'Graph has no vertex property with name "{name}" ' f'Possible vertex properties are: {list(self.vertex_properties)};' f'{err}') if vertex is not None: return v_prop[self.vertex(vertex)] else: return vertex_property_map_to_python(v_prop, as_array=as_array)
[docs] def vertex_property_map(self, name): return self._base.vertex_properties[name]
[docs] def has_vertex_property(self, prop_name: str): return prop_name in self.vertex_properties
@property def vertex_properties(self): return self._base.vertex_properties.keys()
[docs] def add_vertex_property(self, name, source=None, dtype=None): v_prop = vertex_property_map_from_python(source, self, dtype=dtype) self._base.vertex_properties[name] = v_prop
[docs] def set_vertex_property(self, name, source, vertex=None): if name not in self._base.vertex_properties: raise ValueError(f'Graph has no vertex property with name {name}!') v_prop = self._base.vertex_properties[name] if vertex is not None: v_prop[vertex] = source else: set_vertex_property_map(v_prop, source)
[docs] def define_vertex_property(self, name, source, vertex=None, dtype=None): if name in self.vertex_properties: self.set_vertex_property(name, source, vertex=vertex) else: if vertex is None: self.add_vertex_property(name, source, dtype=dtype) else: dtype = gtype_from_source(source) if dtype is None else dtype self.add_vertex_property(name, dtype=dtype) self.set_vertex_property(name, source, vertex=vertex)
[docs] def remove_vertex_property(self, name): if name not in self._base.vertex_properties: raise ValueError(f'Graph has no vertex property with name {name}!') del self._base.vertex_properties[name]
[docs] def vertex_degrees(self): return self._base.get_out_degrees(self._base.get_vertices())
[docs] def vertex_degree(self, index): return self._base.get_out_degrees([index])[0]
[docs] def vertex_out_degrees(self): return self._base.get_out_degrees(self._base.get_vertices())
[docs] def vertex_out_degree(self, index): return self._base.get_out_degrees([index])[0]
[docs] def vertex_in_degrees(self): return self._base.get_in_degrees(self._base.get_vertices())
[docs] def vertex_in_degree(self, index): return self._base.get_in_degrees([index])[0]
[docs] def vertex_neighbours(self, index): return self._base.get_out_neighbours(index)
[docs] def vertex_out_neighbours(self, index): return self._base.get_out_neighbours(index)
[docs] def vertex_in_neighbours(self, index): return self._base.get_in_neighbours(index)
# ## Edges @property def n_edges(self): return self._base.num_edges()
[docs] def edge(self, edge): if isinstance(edge, gt.Edge): return edge elif isinstance(edge, tuple): # FIXME: what about list ? return self._base.edge(*edge) elif isinstance(edge, int): return gtu.find_edge(self._base, self._base.edge_index, edge)[0] elif isinstance(edge, list) and len(edge) == 2: return self.edge(tuple(edge)) elif isinstance(edge, np.ndarray) and edge.shape == (2,): return self.edge(tuple(edge)) else: raise ValueError(f'Edge specification {edge} is not valid!')
[docs] def first_edge(self): return self._base.edges().next()
[docs] def edge_index(self, edge): return self._base.edge_index[self.edge(edge)]
[docs] def edge_indices(self): # TODO: explain what this does table = self._base.get_edges(eprops=[self._base.edge_index]) return table[:, 2]
[docs] def add_edge(self, edge): if isinstance(edge, tuple): self._base.add_edge(*edge) else: self._base.add_edge_list(edge)
[docs] def remove_edge(self, edge): edge = self.edge(edge) self._base.remove_edge(edge)
@property def edges(self): return list(self._base.edges())
[docs] def get_edges(self, eprops=[]): return self._base.get_edges(eprops=eprops)
[docs] def edge_iterator(self): return self._base.edges()
[docs] def edge_connectivity(self, order='src_vertex'): # PERFORMANCE: see if better to cache property and invalidate when edeges added or removed if order == 'src_vertex': return self._base.get_edges()[:, :2] elif order == 'eid': table = self._base.get_edges([self._base.edge_index]) # Sort by the eid (third column) → insertion / ID order return table[np.argsort(table[:, 2])][:, :2] else: raise NotImplementedError(f'Invalid edge connectivity order "{order}"! ')
[docs] def edge_property(self, name, edge=None, as_array=True): e_prop = self._base.edge_properties[name] if edge is not None: return e_prop[self.edge(edge)] else: return edge_property_map_to_python(e_prop, as_array=True)
[docs] def edge_property_map(self, name): return self._base.edge_properties[name]
@property def edge_properties(self): return self._base.edge_properties.keys()
[docs] def has_edge_property(self, prop_name: str): return prop_name in self.edge_properties
[docs] def add_edge_property(self, name, source=None, dtype=None): p = edge_property_map_from_python(source, self) self._base.edge_properties[name] = p
[docs] def set_edge_property(self, name, source, edge=None): if name not in self._base.edge_properties: raise ValueError(f'Graph has no edge property with name {name}!') p = self._base.edge_properties[name] if edge is not None: p[self.edge(edge)] = source else: set_edge_property_map(p, source)
[docs] def define_edge_property(self, name, source, edge=None, dtype=None): if name in self.edge_properties: self.set_edge_property(name, source, edge=edge) else: if edge is None: self.add_edge_property(name, source, dtype=dtype) else: dtype = gtype_from_source(source) if dtype is None else dtype self.add_edge_property(name, dtype=dtype) self.set_edge_property(name, source, edge=edge)
[docs] def remove_edge_property(self, name): if name not in self.edge_properties: raise ValueError(f'Graph does not have edge property with name {name}!') del self._base.edge_properties[name]
[docs] def vertex_edges(self, vertex): return edges_to_vertices(self.vertex_edges_iterator(vertex))
[docs] def vertex_out_edges(self, vertex): return edges_to_vertices(self.vertex_out_edges_iterator(vertex))
[docs] def vertex_in_edges(self, vertex): return edges_to_vertices(self.vertex_in_edges_iterator(vertex))
[docs] def vertex_edges_iterator(self, vertex): return self._base.vertex(vertex).out_edges()
[docs] def vertex_out_edges_iterator(self, vertex): return self._base.vertex(vertex).out_edges()
[docs] def vertex_in_edges_iterator(self, vertex): return self._base.vertex(vertex).in_edges()
# ## Graph properties
[docs] def graph_property(self, name): return self._base.graph_properties[name]
[docs] def graph_property_map(self, name): return self._base.graph_properties[name]
@property def graph_properties(self): return self._base.graph_properties.keys()
[docs] def has_graph_property(self, prop_name: str): return prop_name in self.graph_properties
[docs] def add_graph_property(self, name, source, dtype=None): if dtype is None: dtype = 'object' gtype = dtype_to_gtype(dtype) g_prop = self._base.new_graph_property(gtype) g_prop.set_value(source) self._base.graph_properties[name] = g_prop
[docs] def set_graph_property(self, name, source): if name not in self.graph_properties: raise ValueError(f'Graph has no property named "{name}"') if source is not None: self._base.graph_properties[name] = source
[docs] def define_graph_property(self, name, source, dtype=None): if name in self.graph_properties: self.set_graph_property(name, source) else: self.add_graph_property(name, source, dtype=dtype)
[docs] def remove_graph_property(self, name): if name not in self.graph_properties: raise ValueError(f'Graph does not have graph property named {name}!') del self._base.graph_properties[name]
# ## Geometry @property def shape(self): """The shape of the space in which the graph is embedded. Returns ------- shape : tuple of int The shape of the graph space. """ return self.graph_property('shape') @shape.setter def shape(self, value): self.define_graph_property('shape', value) @property def ndim(self): if self.shape is None: return Graph.DEFAULT_N_DIMS else: return len(self.shape)
[docs] def axis_indices(self, axis=None, as_list=False): if axis is None: return range(self.ndim) axis_to_index = {k: i for i, k in enumerate('xyz')} if as_list and not isinstance(axis, (tuple, list)): axis = [axis] if isinstance(axis, (tuple, list)): return [axis_to_index[a] if a in axis_to_index.keys() else a for a in axis] else: return axis_to_index[axis] if axis in axis_to_index.keys() else axis
@property def has_vertex_coordinates(self): return 'coordinates' in self.vertex_properties
[docs] def vertex_coordinates(self, vertex=None, axis=None): p = self.vertex_property_map('coordinates') if vertex is not None: coordinates = p[vertex] if axis is None: return coordinates else: indices = self.axis_indices(axis) return coordinates[indices] else: indices = self.axis_indices(axis, as_list=True) coordinates = p.get_2d_array(indices) if axis is not None and not isinstance(axis, (tuple, list)): return coordinates[0] else: return coordinates.T
# FIXME: not very useful
[docs] def set_vertex_coordinates(self, coordinates, vertex=None, dtype=float): self.define_vertex_property('coordinates', coordinates, vertex=vertex, dtype=dtype)
# def set_vertex_coordinate(self, vertex, coordinate): # self.define_vertex_property('coordinates', coordinate, vertex=vertex) @property def has_vertex_radii(self): return 'radii' in self.vertex_properties
[docs] def vertex_radii_voxels(self, vertex=None) -> np.ndarray: """ Vertex radii in **voxels**. Raises ------ KeyError If radii have not been measured yet. Call _measure_radii() first. For physical units use :meth:`vertex_radii_units`. """ if 'radii' not in self.vertex_properties: raise KeyError("'radii' (voxel) property not found. " "Ensure graph_processing._measure_radii() was called before accessing vertex_radii_voxels(). " "For physical units use vertex_radii_units().") return self.vertex_property('radii', vertex=vertex)
[docs] def vertex_radii_units(self, vertex=None) -> np.ndarray: """ Vertex radii in **physical units** (µm). Raises ------ KeyError If radius_units have not been measured yet. Call _measure_radii() with a valid spacing array first. For voxel units use :meth:`vertex_radii_voxels`. """ if 'radius_units' not in self.vertex_properties: raise KeyError("'radius_units' (µm) property not found. " "Ensure graph_processing._measure_radii() was called with " "a valid spacing array before accessing vertex_radii_units().") return self.vertex_property('radius_units', vertex=vertex)
[docs] def vertex_radii(self, vertex=None) -> np.ndarray: """ .. deprecated:: Use :meth:`vertex_radii_voxels` or :meth:`vertex_radii_units` explicitly. This method returns voxel radii and will be removed in a future version. """ warnings.warn("vertex_radii() is ambiguous and deprecated. Use vertex_radii_voxels() for voxel units " "or vertex_radii_units() for physical units (µm).", DeprecationWarning, stacklevel=2) return self.vertex_radii_voxels(vertex=vertex)
[docs] def set_vertex_radii(self, radii, vertex=None): self.define_vertex_property('radii', radii, vertex=vertex)
[docs] def set_vertex_radius(self, vertex, radius): self.define_vertex_property('radii', radius, vertex=vertex)
@property def has_edge_coordinates(self): return 'coordinates' in self.edge_properties
[docs] def edge_coordinates(self, edge=None): return self.edge_property('coordinates', edge=edge)
[docs] def set_edge_coordinates(self, coordinates, edge=None): self.define_edge_property('coordinates', coordinates, edge=edge)
@property def has_edge_radii(self): return 'radii' in self.edge_properties @property def has_edge_radii_um(self) -> bool: """True if µm radii have been propagated to edges.""" return 'radius_units' in self.edge_properties
[docs] def edge_radii_voxels(self, edge=None) -> np.ndarray: """ Edge radii in **voxels** (aggregated from vertex radii during reduce_graph). Raises ------ KeyError If radii have not been propagated to edges yet. """ if 'radii' not in self.edge_properties: raise KeyError( "'radii' (voxel) edge property not found. " "Ensure reduce_graph() ran with 'radii' in vertex_to_edge_mappings.") return self.edge_property('radii', edge=edge)
[docs] def edge_radii_um(self, edge=None) -> np.ndarray: """ Edge radii in **physical units** (µm). Raises ------ KeyError If radius_units have not been propagated to edges yet. """ if 'radius_units' not in self.edge_properties: raise KeyError( "'radius_units' (µm) edge property not found. " "Ensure reduce_graph() ran with 'radius_units' in vertex_to_edge_mappings.") return self.edge_property('radius_units', edge=edge)
[docs] def edge_radii(self, edge=None) -> np.ndarray: """ .. deprecated:: Use :meth:`edge_radii_voxels` or :meth:`edge_radii_um` explicitly. """ import warnings warnings.warn( "edge_radii() is ambiguous and deprecated. Use edge_radii_voxels() or edge_radii_um().", DeprecationWarning, stacklevel=2) return self.edge_radii_voxels(edge=edge)
[docs] def set_edge_radii(self, radii, edge=None): self.define_edge_property('radii', radii, edge=edge)
# ## Edge geometry @property def edge_geometry_type(self): """Type for storing edge properties Returns ------- type : 'graph' or 'edge' 'graph' : Stores edge coordinates in a graph property array and start end indices in edges. 'edge' : Stores the edge coordinates in variable length vectors in each edge. """ return self.graph_property('edge_geometry_type') @edge_geometry_type.setter def edge_geometry_type(self, value): self.set_edge_geometry_type(value)
[docs] def edge_geometry_property_name(self, name='coordinates', prefix='edge_geometry'): return f'{prefix}_{name}'
@property def edge_geometry_property_names(self): prefix = self.edge_geometry_property_name(name='') if self.edge_geometry_type == 'graph': properties = self.graph_properties else: properties = self.edge_properties # return the graph properties that are arrays with n_pixels elements return [p for p in properties if p.startswith(prefix) and p != 'edge_geometry_type']
[docs] def edge_geometry_property(self, name): name = self.edge_geometry_property_name(name) if self.edge_geometry_type == 'graph': return self.graph_property(name) else: return self.edge_property(name)
@property def edge_geometry_properties(self): prefix_len = len(self.edge_geometry_property_name(name='')) properties = [p[prefix_len:] for p in self.edge_geometry_property_names] return properties
[docs] def has_edge_geometry(self, name='coordinates'): # FIXME: should probably check for indices too return self.edge_geometry_property_name(name=name) in self.edge_geometry_property_names
# edge geometry stored at each edge def _edge_geometry_scalar_edge(self, name, edge=None): name = self.edge_geometry_property_name(name) return self.edge_property(name, edge=edge) def _edge_geometry_vector_edge(self, name, edge=None, reshape=True, ndim=None, as_list=True): name = self.edge_geometry_property_name(name) geometry = self.edge_property(name, edge=edge) if reshape: if ndim is None: ndim = self.ndim if edge is None: geometry = [g.reshape((-1, ndim), order='A') for g in geometry] if as_list: return geometry else: return np.vstack(geometry) else: return geometry.reshape(-1, ndim) else: return geometry def _edge_geometry_indices_edge(self): lengths = self.edge_geometry_lengths() indices = np.cumsum(lengths) indices = np.array([np.hstack([0, indices[:-1]]), indices]).T return indices def _edge_geometry_edge(self, name, edge=None, reshape=True, ndim=None, as_list=True, return_indices=False): if name in ['coordinates', 'mesh']: edge_geometry = self._edge_geometry_vector_edge(name, edge=edge, reshape=reshape, ndim=ndim, as_list=as_list) else: edge_geometry = self._edge_geometry_scalar_edge(name, edge=edge) if return_indices: indices = self._edge_geometry_indices_edge() return edge_geometry, indices else: return edge_geometry def _set_edge_geometry_scalar_edge(self, name, scalars, edge=None, dtype=None): name = self.edge_geometry_property_name(name) self.define_edge_property(name, scalars, edge=edge, dtype=dtype) def _set_edge_geometry_vector_edge(self, name, vectors, indices=None, edge=None): name = self.edge_geometry_property_name(name) if edge is None: if indices is None: vectors = [v.reshape(-1, order='A') for v in vectors] else: vectors = [vectors[s:e].reshape(-1, order='A') for s, e in indices] self.define_edge_property(name, vectors, edge=edge, dtype='vector<double>') def _set_edge_geometry_edge(self, name, values, indices=None, edge=None): if name in ['coordinates', 'mesh']: return self._set_edge_geometry_vector_edge(name, values, indices=indices, edge=edge) elif name in ['radii']: return self._set_edge_geometry_scalar_edge(name, values, edge=edge) else: return self._set_edge_geometry_scalar_edge(name, values, edge=edge, dtype=object) def _remove_edge_geometry_edge(self, name): name = self.edge_geometry_property_name(name) self.remove_edge_property(name) # EDGE GEOMETRY GRAPH # edge geometry data stored in a single array, start,end indices stored in edge def _edge_geometry_indices_name_graph(self, name='indices'): return self.edge_geometry_property_name(name) def _edge_geometry_indices_graph(self, edge=None): return self.edge_property(self._edge_geometry_indices_name_graph(), edge=edge) def _set_edge_geometry_indices_graph(self, indices, edge=None): self.set_edge_property(self._edge_geometry_indices_name_graph(), indices, edge=edge) def _edge_geometry_graph(self, name, edge=None, return_indices=False, as_list=False): name = self.edge_geometry_property_name(name) if edge is None: values = self.graph_property(name) if return_indices or as_list: indices = self._edge_geometry_indices_graph() if as_list: values = [values[start:end] for start, end in indices] if return_indices: return values, indices else: return values else: start, end = self._edge_geometry_indices_graph(edge=edge) values = self.graph_property(name) return values[start:end] def _set_edge_geometry_graph(self, name, values, indices=None, edge=None): if edge is not None: raise NotImplementedError("Setting individual edge geometries not implemented for 'graph' mode!") if isinstance(values, list): if indices is None: indices = np.cumsum([len(v) for v in values]) indices = np.array([np.hstack([[0], indices[:-1]]), indices], dtype=int).T.astype(np.int64) first_val = values[0] # flatten the list of arrays so that it is a single array (indexed by the indices) if first_val.ndim == 1: # if the first value is a 1D array, we assume all values are 1D arrays values = np.concatenate(values) else: # if the first value is a 2D array, we assume all values are 2D arrays values = np.vstack(values) # if values.ndim == 1: # if the values are a 1D array, we assume they are scalars # prop_dtype = f'vector<{gtype_from_source(values)}>' # Store as *vector* to store as a single cpp array if values.ndim <= 2: # can't store 2d as vector, so store as object (pickled np.ndarray) prop_dtype = 'object' # Store as *object* to store as a single ndarray else: raise ValueError(f'Edge geometry values must be 1D or 2D arrays, got {values.ndim}D array!') if indices is not None: # FIXME: see if we should update the indices in case exists but mismatched self.define_edge_geometry_indices_graph(indices) egp_name = self.edge_geometry_property_name(name) self.define_graph_property(egp_name, values, dtype=prop_dtype)
[docs] def define_edge_geometry_indices_graph(self, indices): name_indices = self._edge_geometry_indices_name_graph() # if name_indices not in self.edge_properties: # Set if missing self.define_edge_property(name_indices, indices, dtype='vector<int64_t>')
def _remove_edge_geometry_graph(self, name): name = self.edge_geometry_property_name(name) if name in self.graph_properties: self.remove_graph_property(name) def _remove_edge_geometry_indices_graph(self): name = self._edge_geometry_indices_name_graph() if name in self.edge_properties: self.remove_edge_property(name)
[docs] def prune_edge_geometry(self): """ Remove the unused edge geometries from the graph. This computes the new indices and remaps the edge geometry properties to the new indices. """ if not self.has_edge_geometry() or self.edge_geometry_type != 'graph': return # adjust indices indices = self._edge_geometry_indices_graph() indices_new = np.diff(indices, axis=1)[:, 0] indices_new = np.cumsum(indices_new) indices_new = np.array([np.hstack([0, indices_new[:-1]]), indices_new]).T self._set_edge_geometry_indices_graph(indices_new) self._remap_edge_geometry_properties(indices, indices_new)
[docs] def remap_edge_geometry_properties(self, new_indices): """ Remap all properties in self.edge_geometry_properties (edge_geometry_<>) to the new indices i.e., copy every edge-geometry_<> array so old ranges → new ranges Parameters ---------- new_indices : np.ndarray The new indices to remap the edge geometry properties to. """ indices = self._edge_geometry_indices_graph() self._set_edge_geometry_indices_graph(new_indices) self._remap_edge_geometry_properties(indices, new_indices)
def _remap_edge_geometry_properties(self, indices, indices_new): """ Remap all properties in self.edge_geometry_properties (edge_geometry_<>) to the new indices i.e., copy every edge-geometry_<> array so old ranges → new ranges For example, if for the edge_geometry_coordinates, which has a shape of (n_voxels, 3), the indices would be (n_edges, 2) and the indices_new would be (n_edges_new, 2). The function would then remap the coordinates from the old indices to the new indices like this: for i in range(indices.shape[0]): prop_new[indices_new[i, 0]:indices_new[i, 1]] = prop[indices[i, 0]:indices[i, 1]] Parameters ---------- indices indices_new Returns ------- """ n = indices_new[-1, -1] for prop_name in self.edge_geometry_property_names: prop = self.graph_property(prop_name) shape_new = (n,) + prop.shape[1:] prop_new = np.zeros(shape_new, dtype=prop.dtype) # init empty, will then be filled with remapped values prop_new = remap_array_ranges(prop, prop_new, indices, indices_new) self.set_graph_property(prop_name, prop_new)
[docs] def edge_geometry(self, name='coordinates', edge=None, as_list=True, return_indices=False, reshape=True, ndim=None): if self.edge_geometry_type == 'graph': return self._edge_geometry_graph(name=name, edge=edge, return_indices=return_indices, as_list=as_list) else: # edge geometry type return self._edge_geometry_edge(name=name, edge=edge, return_indices=return_indices, as_list=as_list, reshape=reshape, ndim=ndim)
[docs] def set_edge_geometry(self, name, values, indices=None, edge=None): """ Set the given edge geometry property for the graph. .. warning:: ``edge`` is not supported for ``'graph'`` edge geometry type. Parameters ---------- name : str The name of the original vertex or edge property to set as edge geometry. As an edge geometry property, the name will be prefixed with ``'edge_geometry_*'`` (e.g. ``'edge_geometry_coordinates'``). values : list or np.ndarray The values to set as edge geometry. indices : np.ndarray or None How to slice the values to map to edges. edge : gt.Edge or int, optional The edge to set the geometry for. If ``None``, the geometry is set for all edges. Not supported when ``edge_geometry_type`` is ``'graph'``. """ if self.edge_geometry_type == 'graph': self._set_edge_geometry_graph(name, values, indices=indices, edge=edge) else: self._set_edge_geometry_edge(name, values, indices=indices, edge=edge)
[docs] def remove_edge_geometry(self, name=None): if name is None: if self.edge_geometry_type == 'graph': self._remove_edge_geometry_indices_graph() name = self.edge_geometry_properties if not isinstance(name, list): name = [name] for n in name: if self.edge_geometry_type == 'graph': self._remove_edge_geometry_graph(name=n) else: self._remove_edge_geometry_edge(name=n)
[docs] def set_edge_geometry_vertex_properties(self, original_graph, edge_geometry_vertex_properties, branch_indices, indices): """ Set the edge geometry properties from the vertex properties of the original graph. .. note:: The property is processed only if it exists in the original graph and is not already set as an edge geometry property in the current graph. Parameters ---------- original_graph edge_geometry_vertex_properties branch_indices indices Returns ------- """ for v_prop_name in edge_geometry_vertex_properties: if v_prop_name in original_graph.vertex_properties: # If already exists, skip if self.edge_geometry_property_name(v_prop_name) in self.edge_geometry_property_names: continue # Skip if already set, it will be handled by the edge aggregation v_prop = original_graph.vertex_property(v_prop_name)[branch_indices] self.set_edge_geometry(name=v_prop_name, values=v_prop, indices=indices)
[docs] def set_edge_geometry_edge_properties(self, original_graph, edge_geometry_edge_properties, indices, edge_to_edge_map): first_edge = edge_to_edge_map[0][0] for e_prop_name in edge_geometry_edge_properties: if e_prop_name in original_graph.edge_properties: # If already exists if self.edge_geometry_property_name(f'edge_{e_prop_name}') in self.edge_geometry_property_names: continue # Skip if already set, it will be handled by the edge aggregation values = original_graph.edge_property_map(e_prop_name) # there is one fewer edge than vertices in each reduced edge ! if isinstance(first_edge, gt.Edge): values = [[values[e] for e in edges + [edges[-1]]] for edges in edge_to_edge_map] elif isinstance(first_edge, (numbers.Integral, numbers.Real)): values = values.fa values = [values[np.append(edges, edges[-1])] for edges in edge_to_edge_map] else: raise ValueError(f'Edge type "{type(first_edge)}" not supported for edge geometry!') # it seems that we repeat the last edge to have the same number of edges as vertices ? self.set_edge_geometry(name=f'edge_{e_prop_name}', values=values, indices=indices)
[docs] def edge_geometry_indices(self): if self.edge_geometry_type == 'graph': return self._edge_geometry_indices_graph() else: return self._edge_geometry_indices_edge()
[docs] def edge_geometry_lengths(self, name='coordinates'): if self.edge_geometry_type == 'graph': indices = self._edge_geometry_indices_graph() return np.diff(indices, axis=1)[:, 0] else: values = self.edge_geometry(name) return np.array([len(v) for v in values], dtype=int)
[docs] def set_edge_geometry_type(self, edge_geometry_type): if edge_geometry_type not in ['graph', 'edge']: raise ValueError(f"Edge geometry must be 'graph' or 'edge', got '{edge_geometry_type}'!") if self.edge_geometry_type == edge_geometry_type: return else: if self.edge_geometry_type == 'graph': # graph -> edge indices = self._edge_geometry_indices_graph() for name in self.edge_geometry_property_names: values = self.edge_geometry(name, as_list=False) self._remove_edge_geometry_graph(name) self._set_edge_geometry_edge(name, values, indices=indices) self._remove_edge_geometry_indices_graph() else: # self.edge_geometry_type == 'edge': edge -> graph for name in self.edge_geometry_property_names: values = self.edge_geometry(name) self._remove_edge_geometry_edge(name) self._set_edge_geometry_graph(name, values) self.set_graph_property('edge_geometry_type', edge_geometry_type)
[docs] def is_edge_geometry_consistent(self, verbose=False): eg, ei = self.edge_geometry(as_list=False, return_indices=True) vc = self.vertex_coordinates() ec = self.edge_connectivity() # check edge sources check = vc[ec[:, 0]] == eg[ei[:, 0]] if not np.all(check): if verbose: errors = np.where(check == False)[0] print(f'Found {len(errors)} errors in edge sources at {errors}') return False # check edge targets check = vc[ec[:, 1]] == eg[ei[:, 1]-1] if not np.all(check): if verbose: errors = np.where(check == False)[0] print(f'Found {len(errors)} errors in edge targets at {errors}') return False return True
[docs] def edge_geometry_from_edge_property(self, edge_property_name, edge_geometry_name=None): edge_property = self.edge_property(edge_property_name) indices = self.edge_geometry_indices() shape = (len(indices),) + edge_property.shape[1:] edge_geometry = np.zeros(shape, dtype=edge_property.dtype) for i, e in zip(indices, edge_property): si, ei = i edge_geometry[si:ei] = e if edge_geometry_name is None: edge_geometry_name = edge_property_name self.set_edge_geometry(name=edge_geometry_name, values=edge_geometry, indices=indices)
# def edge_meshes(self, edge=None): # """Returns a mesh triangulation for the geometry of each edge. # # Note # ---- # This functionality can be used to store geometric information of edges as # meshes, e.g. useful for graph rendering. # """ # # pass # ## Label # def add_label(self, annotation=None, key='id', value='order'): # # # lbl.AnnotationFile # # label points # aba = np.array(io.read(annotation), dtype=int) # # # get vertex coordinates # x,y,z = self.vertex_coordinates().T # # ids = np.ones(len(x), dtype = bool) # for a,s in zip([x,y,z], aba.shape): # ids = np.logical_and(ids, a >= 0) # ids = np.logical_and(ids, a < s) # # # label points # g_ids = np.zeros(len(x), dtype=int) # g_ids[ids] = aba[x[ids], y[ids], z[ids]] # # if value is not None: # id_to_order = lbl.getMap(key=key, value=value) # g_order = id_to_order[g_ids] # else: # value = key # # self.add_vertex_property(value, g_order) # ## Functionality
[docs] def sub_graph(self, vertex_filter=None, edge_filter=None, view=False): """ Construct a subgraph using graph-tool filtering, returning either a view or a pruned copy. Parameters ---------- vertex_filter : None or array-like or gt.PropertyMap Vertex selection mask or property map. Commonly a 1-D boolean numpy array of length ``n_vertices``. ``True`` means the vertex is retained in the view. edge_filter : None or array-like or gt.PropertyMap Edge selection mask or property map. Commonly a 1-D boolean numpy array of length ``n_edges``. ``True`` means the edge is retained in the view. view : bool If ``True``, return a ``Graph`` wrapping a ``gt.GraphView`` (no pruning/copy). If ``False``, materialise a pruned copy via ``gt.Graph(gv, prune=True)``. Returns ------- Graph A graph restricted by the provided filters. Notes ----- Edge geometry handling: - If the resulting graph has edges and the source graph has edge geometry, ``prune_edge_geometry()`` is called to compact edge-geometry arrays to the retained edge set. - If the resulting graph has no edges, edge geometry is removed (``remove_edge_geometry()``). """ gv = gt.GraphView(self.base, vfilt=vertex_filter, efilt=edge_filter) if view: return Graph(base=gv) else: g = gt.Graph(gv, prune=True) g = Graph(base=g) if g.n_edges and self.has_edge_geometry(): g.prune_edge_geometry() else: g.remove_edge_geometry() # Drop geometries (to be readded) if we filter all edges (typically for reduc) # TODO: see if we keep the egeom props but with shape([n], 0) return g
[docs] def view(self, vertex_filter=None, edge_filter=None): return gt.GraphView(self.base, vfilt=vertex_filter, efilt=edge_filter)
[docs] def remove_self_loops(self): gt.stats.remove_self_loops(self.base)
[docs] def remove_isolated_vertices(self): non_isolated = self.vertex_degrees() > 0 new_graph = self.sub_graph(vertex_filter=non_isolated) self._base = new_graph._base
[docs] def label_components(self, return_vertex_counts=False): components, vertex_counts = gtt.label_components(self.base) components = np.array(components.a) if return_vertex_counts: return components, vertex_counts else: return components
[docs] def largest_component(self, view=False): components, counts = self.label_components(return_vertex_counts=True) i = np.argmax(counts) vertex_filter = components == i return self.sub_graph(vertex_filter=vertex_filter, view=view)
[docs] def vertex_coloring(self): colors = gtt.sequential_vertex_coloring(self.base) colors = vertex_property_map_to_python(colors) return colors
[docs] def edge_target_label(self, vertex_label, as_array=True): if isinstance(vertex_label, str): vertex_label = self.vertex_property(vertex_label) if not isinstance(vertex_label, gt.PropertyMap): vertex_label = vertex_property_map_from_python(vertex_label, self) et = gt.edge_endpoint_property(self.base, vertex_label, endpoint='target') return edge_property_map_to_python(et, as_array=as_array)
[docs] def edge_source_label(self, vertex_label, as_array=True): if isinstance(vertex_label, str): vertex_label = self.vertex_property(vertex_label) if not isinstance(vertex_label, gt.PropertyMap): vertex_label = vertex_property_map_from_python(vertex_label, self) et = gt.edge_endpoint_property(self.base, vertex_label, endpoint='source') return edge_property_map_to_python(et, as_array=as_array)
[docs] def remove_isolated_edges(self): vertex_degree = self.vertex_degrees() vertex_degree = vertex_property_map_from_python(vertex_degree, self) es = self.edge_source_label(vertex_degree, as_array=True) et = self.edge_target_label(vertex_degree, as_array=True) edge_filter = np.logical_not(np.logical_and(es == 1, et == 1)) new_graph = self.sub_graph(edge_filter=edge_filter) self._base = new_graph._base self.remove_isolated_vertices()
[docs] def edge_graph(self, return_edge_map=False): line_graph, emap = gtg.line_graph(self.base) line_graph = Graph(base=line_graph) if return_edge_map: emap = vertex_property_map_to_python(emap) return line_graph, emap else: return line_graph
# ## Binary morphological graph operations
[docs] def vertex_propagate(self, label, value, steps=1): if value is not None and not hasattr(value, '__len__'): value = [value] p = vertex_property_map_from_python(label, self) for s in range(steps): gt.infect_vertex_property(self.base, p, vals=value) label = vertex_property_map_to_python(p) return label
[docs] def vertex_dilate_binary(self, label, steps=1): return self.vertex_propagate(label, value=True, steps=steps)
[docs] def vertex_erode_binary(self, label, steps=1): return self.vertex_propagate(label, value=False, steps=steps)
[docs] def vertex_open_binary(self, label, steps=1): label = self.vertex_erode_binary(label, steps=steps) return self.vertex_dilate_binary(label, steps=steps)
[docs] def vertex_close_binary(self, label, steps=1): label = self.vertex_dilate_binary(label, steps=steps) return self.vertex_erode_binary(label, steps=steps)
[docs] def expand_vertex_filter(self, vertex_filter, steps=1): return self.vertex_dilate_binary(vertex_filter, steps=steps)
[docs] def edge_propagate(self, label, value, steps=1): label = np.array(label) if steps is None: return label for s in range(steps): edges = label == value ec = self.edge_connectivity() ec = ec[edges] vertices = np.unique(ec) for v in vertices: for e in self.vertex_edges_iterator(v): i = self.edge_index(e) label[i] = value return label
[docs] def edge_dilate_binary(self, label, steps=1): return self.edge_propagate(label, value=True, steps=steps)
[docs] def edge_erode_binary(self, label, steps=1): return self.edge_propagate(label, value=False, steps=steps)
[docs] def edge_open_binary(self, label, steps=1): label = self.edge_erode_binary(label, steps=steps) return self.edge_dilate_binary(label, steps=steps)
[docs] def edge_close_binary(self, label, steps=1): label = self.edge_dilate_binary(label, steps=steps) return self.edge_erode_binary(label, steps=steps)
[docs] def edge_to_vertex_label(self, edge_label, method='max', as_array=True): if isinstance(edge_label, str): edge_label = self.edge_property(edge_label) if not isinstance(edge_label, gt.PropertyMap): edge_label = edge_property_map_from_python(edge_label, self) vertex_label = gt.incident_edges_op(self.base, 'in', method, edge_label) return vertex_property_map_to_python(vertex_label, as_array=as_array)
[docs] def edge_to_vertex_label_or(self, edge_label): label = np.zeros(self.n_vertices, dtype=edge_label.dtype) ec = self.edge_connectivity() # label[ec[:,0]] = edge_label # label[ec[:,1]] = np.logical_or(edge_label, label[ec[:,1]]) ids = np.unique(ec[edge_label].flatten()) label[ids] = True return label
[docs] def vertex_to_edge_label(self, vertex_label, method=None): label = np.zeros(self.n_edges, dtype=vertex_label.dtype) ec = self.edge_connectivity() if method is None: if vertex_label.dtype == bool: label = np.mean([vertex_label[ec[:, 0]], vertex_label[ec[:, 1]]], axis=0) == 1 else: label = np.mean([vertex_label[ec[:, 0]], vertex_label[ec[:, 1]]], axis=0) else: label = method(vertex_label[ec[:, 0]], vertex_label[ec[:, 1]]) return label
# ## Geometric manipulation
[docs] def sub_slice(self, slicing, view=False, coordinates=None, cut_edges='exclusive'): """ Slice the graph by an axis-aligned spatial selection, with optional boundary-edge policy. The slice is defined by applying ``slicing`` to per-vertex coordinates (by default the ``'coordinates'`` vertex property). This produces an initial vertex mask ``V0``. Two boundary policies are supported via ``cut_edges``: - ``'exclusive'`` (default): return the induced subgraph on ``V0`` (retain an edge only if both endpoints are in ``V0``). - ``'inclusive'``: retain an edge if at least one endpoint is in ``V0``, then expand the vertex set to include both endpoints of every retained edge. Parameters ---------- slicing : tuple of slice or int A slicing spec accepted by ``ClearMap.IO.IO.slc.unpack_slicing`` (slices/ints per axis). view : bool If ``True``, return a lightweight ``gt.GraphView``-backed wrapper. If ``False``, return a pruned copy (see :meth:`sub_graph`). coordinates : None or str or np.ndarray Coordinate source to slice against: - ``None``: uses ``self.vertex_coordinates()``. - ``str``: name of a vertex property to use. - ``np.ndarray``: explicit ``(N, ndim)`` coordinates array. cut_edges : str Boundary edge policy: ``'exclusive'`` or ``'inclusive'``. Returns ------- Graph The sliced graph (view or pruned copy). If edge geometry exists and edges are retained, geometry is compacted via ``prune_edge_geometry()`` in :meth:`sub_graph`. """ valid = self.sub_slice_vertex_filter(slicing, coordinates=coordinates) match cut_edges: case 'exclusive': return self.sub_graph(vertex_filter=valid, view=view) case 'inclusive': ec = self.edge_connectivity(order='eid') src = ec[:, 0] dst = ec[:, 1] e_keep = valid[src] | valid[dst] # Expand vertex set: if an edge is kept, keep both its endpoints. v_keep = valid.copy() if e_keep.any(): v_keep[np.unique(ec[e_keep].reshape(-1))] = True subg1 = self.sub_graph(vertex_filter=v_keep, edge_filter=e_keep, view=view) valid2 = subg1.sub_slice_vertex_filter(slicing, coordinates=coordinates) ec = subg1.edge_connectivity(order='eid') src = ec[:, 0] dst = ec[:, 1] e_keep2 = valid2[src] | valid2[dst] return subg1.sub_graph(edge_filter=e_keep2) case _: raise ValueError(f'cut_edges must be one of: "exclusive", "inclusive"; got {cut_edges!r}')
def _slice_coordinates(self, coordinates, slicing, size): import ClearMap.IO.IO as io slicing = io.slc.unpack_slicing(slicing, self.ndim) valid = np.ones(size, dtype=bool) for d, s in enumerate(slicing): if isinstance(s, slice): if s.start is not None: valid = np.logical_and(valid, s.start <= coordinates[:, d]) if s.stop is not None: valid = np.logical_and(valid, coordinates[:, d] < s.stop) elif isinstance(s, int): valid = np.logical_and(valid, coordinates[:, d] == s) else: raise ValueError(f'Invalid slicing {s} in dimension {d} for sub slicing the graph') return valid
[docs] def sub_slice_vertex_filter(self, slicing, coordinates=None): if coordinates is None: coordinates = self.vertex_coordinates() elif isinstance(coordinates, str): coordinates = self.vertex_property(coordinates) valid = self._slice_coordinates(coordinates, slicing, size=self.n_vertices) return valid
[docs] def sub_slice_edge_filter(self, slicing, coordinates=None): if coordinates is None: coordinates = self.edge_coordinates() elif isinstance(coordinates, str): coordinates = self.edge_property(coordinates) valid = self._slice_coordinates(coordinates, slicing, size=self.n_edges) return valid
[docs] def transform_properties(self, transformation, vertex_properties=None, edge_properties=None, edge_geometry_properties=None, verbose=False): def properties_to_dict(properties): if properties is None: properties = {} if isinstance(properties, list): properties = {n: n for n in properties} return properties vertex_properties = properties_to_dict(vertex_properties) edge_properties = properties_to_dict(edge_properties) edge_geometry_properties = properties_to_dict(edge_geometry_properties) for p in vertex_properties.keys(): # if p in self.vertex_properties: if verbose: print(f'Transforming vertex property: {p} -> {vertex_properties[p]}') values = self.vertex_property(p) values = transformation(values) self.define_vertex_property(vertex_properties[p], values) for p in edge_properties.keys(): # if p in self.edge_properties: if verbose: print(f'Transforming edge property: {p} -> {edge_properties[p]}') values = self.edge_property(p) values = transformation(values) self.define_edge_property(edge_properties[p], values) as_list = self.edge_geometry_type != 'graph' for p in edge_geometry_properties.keys(): # if p in self.edge_geometry_properties: if verbose: print(f'Transforming edge geometry: {p} -> {edge_geometry_properties[p]}') values = self.edge_geometry(p, as_list=as_list) if as_list: values = [transformation(v) for v in values] else: values = transformation(values) self.set_edge_geometry(edge_geometry_properties[p], values=values)
# ## Annotation
[docs] def vertex_annotation(self, vertex=None): return self.vertex_property('annotation', vertex=vertex)
[docs] def set_vertex_annotation(self, annotation, vertex=None, dtype='int32'): self.define_vertex_property('annotation', annotation, vertex=vertex, dtype=dtype)
[docs] def edge_annotation(self, edge=None): return self.edge_property('annotation', edge=edge)
[docs] def set_edge_annotation(self, annotation, edge=None, dtype='int32'): self.define_edge_property('annotation', annotation, edge=edge, dtype=dtype)
[docs] def annotate_properties(self, annotation, vertex_properties=None, edge_properties=None, edge_geometry_properties=None): self.transform_properties(annotation, vertex_properties=vertex_properties, edge_properties=edge_properties, edge_geometry_properties=edge_geometry_properties)
# ## Generic
[docs] def info(self): print(self.__str__()) self._base.list_properties()
[docs] def save(self, filename: str | Path): self._base.save(str(filename))
[docs] def export_vertex_properties(self, output_path: str | Path, v_props: list[str] | None = None): """ Export vertex properties as a pandas DataFrame. The export format will be determined by the file extension. Parameters ---------- v_props: list of str The vertex properties to export. output_path: str | Path The output file path. """ output_path = str(output_path) if v_props is None: v_props = list(self._base.vertex_properties) data = {'vertex_id': self.vertex_indices()} for prop_name in v_props: prop_array = self.vertex_property(prop_name) if prop_array.ndim == 1: # Scalar property data[prop_name] = prop_array else: # Vector property if 'coordinates' in prop_name: # We know how to name these coord_type = prop_name.replace('coordinates', '').strip('_') coord_prefix = coord_type + "_" if coord_type else "" for i, axis in enumerate('xyz'): data[f'{coord_prefix}{axis}'] = prop_array[:, i] else: # Generic names for i in range(prop_array.shape[1]): data[f'{prop_name}_dim{i}'] = prop_array[:, i] import pandas as pd df = pd.DataFrame(data) extension = Path(output_path).suffix if extension == '.csv': df.to_csv(output_path, index=False) elif extension in ('.feather', '.fthr'): df.to_feather(output_path) else: raise NotImplementedError(f'Export format {Path(output_path).suffix} is not yet supported!')
[docs] def copy(self, from_disk=False, path=''): if from_disk: return load(path if path else self.path) else: if (len(list(self.edge_properties)) > 0) and self.n_edges <= LARGE_GRAPH_N_EDGES_THRESHOLD: # Small graph, copy properties return Graph(name=copy.copy(self.name), base=gt.Graph(self.base)) else: # RAM runs away on direct copy of edge_properties for large graphs bare_view = gt.GraphView( self._base, skip_properties=True, skip_vfilt=True, skip_efilt=True) # topological copy, no properties new_base = gt.Graph(bare_view, prune=(False, False, True)) # keep all V/E # vertex properties for name, p in self._base.vp.items(): new_base.vp[name] = new_base.copy_property(p, g=self._base) # graph properties for name, p in self._base.gp.items(): q = new_base.new_graph_property(p.value_type()) q[new_base] = p[self._base] new_base.gp[name] = q # edge properties edge_order = self.edge_indices() for name, p in self._base.ep.items(): q = new_base.new_edge_property(p.value_type()) if p.fa is not None: q.fa = p.fa.copy() # one contiguous memcpy q.a = q.a[edge_order] # FIXME: check if this is correct, it should be! else: prop_arr = self.edge_property(name) # Get the numpy array directly set_edge_property_map(q, prop_arr) new_base.ep[name] = q return Graph(name=copy.copy(self.name), base=new_base)
[docs] @staticmethod def scan_gt_properties(filename: str, as_dict: bool = False): """ Scan the graph-tool file for its properties without loading the entire graph. Parameters ---------- filename : str The path to the graph-tool file. as_dict : bool If True, return a dictionary of property names. If False, return a list of tuples (scope, name, dtype). """ props = scan_gt_props(filename) if as_dict: out = { "vertex": [name for scope, name, _ in props if scope == 'vertex'], "edge": [name for scope, name, _ in props if scope == 'edge'], "graph": [name for scope, name, _ in props if scope == 'graph'], } return out return props
[docs] @classmethod def load(cls, filename, ignore_vp=None, ignore_ep=None, ignore_gp=None): g = gt.load_graph(str(filename), ignore_vp=ignore_vp, ignore_ep=ignore_ep, ignore_gp=ignore_gp) graph = cls(base=g) graph.path = str(filename) return graph
[docs] @classmethod def partial_load(cls, filename: str, *, exclude_edge_geometry_properties: bool = False, include_dict: Optional[Dict[str, Iterable[str]]] = None, exclude_dict: Optional[Dict[str, Iterable[str]]] = None, include: Optional[Iterable[str]] = None, exclude: Optional[Iterable[str]] = None): """ Partially load a graph from a file, allowing for selective property inclusion/exclusion. Five options are available, in order of precedence: 1. ``exclude_edge_geometry_properties``: if ``True``, all edge geometry properties (those starting with ``'edge_geometry_'``) are excluded. 2. ``include_dict``: a dictionary specifying which properties to include for each scope (``'vertex'``, ``'edge'``, ``'graph'``). 3. ``exclude_dict``: a dictionary specifying which properties to exclude for each scope. 4. ``include``: a list of property names to include across all scopes. 5. ``exclude``: a list of property names to exclude across all scopes. .. note:: To inspect which properties are available in the file, use ``Graph.scan_gt_properties(filename, as_dict=True)``. """ props = cls.scan_gt_properties(filename, as_dict=True) props_sets = {s: set(props.get(s, ())) for s in cls.SCOPES} def _as_scope_map(x): """ Normalize iterable-or-dict into {scope: set(...)}. Makes it easy to broadcast to all include/exclude options, whether supplied as dict or iterable. - dict: per-scope values - iterable: applies to all scopes - strings are treated as a single value, not an iterable of chars """ if isinstance(x, dict): return {scope: set(x.get(scope, ())) for scope in cls.SCOPES} if isinstance(x, str): # Because strings are iterable return {scope: {x} for scope in cls.SCOPES} return {scope: set(x) for scope in cls.SCOPES} if exclude_edge_geometry_properties: props["graph"] = [p for p in props["graph"] if p.startswith('edge_geometry_')] return cls.load(filename, ignore_gp=props["graph"]) elif include_dict: allow = _as_scope_map(include_dict) ignore = {s: props_sets[s] - allow[s] for s in cls.SCOPES} elif exclude_dict: block = _as_scope_map(exclude_dict) ignore = {s: props_sets[s] & block[s] for s in cls.SCOPES} elif include: allow = _as_scope_map(include) ignore = {s: props_sets[s] - allow[s] for s in cls.SCOPES} elif exclude: block = _as_scope_map(exclude) ignore = {s: props_sets[s] & block[s] for s in cls.SCOPES} else: return cls.load(filename) ignore = {scope: list(v) for scope, v in ignore.items()} return cls.load(filename, ignore_vp=ignore['vertex'], ignore_ep=ignore['edge'], ignore_gp=ignore['graph'])
[docs] def load(filename): warnings.warn("Use Graph.load() instead of load()", DeprecationWarning) return Graph.load(filename)
[docs] def save(filename, graph): graph.save(str(filename))
############################################################################### # ## Tests ############################################################################### def _test(): import numpy as np import ClearMap.Analysis.graphs.graph_gt as ggt from importlib import reload reload(ggt) g = ggt.Graph('test') g.add_vertex(10) el = [[1, 3], [2, 5], [6, 7], [7, 9]] g.add_edge(el) print(g) coords = np.random.rand(10,3) g.set_vertex_coordinates(coords) g.vertex_coordinates() # edge geometry elen = [3, 4, 5, 6] geometry = [np.random.rand(l, 3) for l in elen] g.set_edge_geometry(geometry) g.edge_geometry() g.add_edge_property('test', [3, 4, 5, 6]) g2 = ggt.Graph('test2') g2.add_vertex(10) g2.add_edge([[1, 3], [2, 5], [6, 7], [7, 9]]) g2.edge_geometry_type = 'edge' elen = [3, 4, 5, 6] geometry = [np.random.rand(l, 3) for l in elen] g2.set_edge_geometry(geometry) g2.edge_geometry() # graph properties reload(ggt) g = ggt.Graph('test') g.add_vertex(10) g.add_edge([[1, 3], [2, 5], [6, 7], [7, 9]]) # scalar vertex property g.add_vertex_property('test', np.arange(g.n_vertices)) print(g.vertex_property('test') == np.arange(g.n_vertices)) # vector vertex property x = np.random.rand(g.n_vertices, 5) g.add_vertex_property('vector', x) print(np.all(g.vertex_property('vector') == x)) # vector vertex property with different lengths y = [np.arange(i) for i in range(g.n_vertices)] g.define_vertex_property('list', y) z = g.vertex_property('list', as_array=False) print(z == y) # edge properties x = 10 * np.arange(g.n_edges) g.add_edge_property('test', x) assert g.edge_property('test') == x g.info() # filtering / sub-graphs v_filter = [True] * 5 + [False] * 5 s = g.sub_graph(vertex_filter=v_filter) p = s.vertex_property_map('test') print(p.a) p = s.edge_property_map('test') print(p.a) print(s.vertex_property('list', as_array=False)) # views v_filter = [False] * 5 + [True] * 5 v = g.sub_graph(vertex_filter=v_filter, view=True) print(v.edge_property('test')) print(v.vertex_property('list', as_array=False)) # sub-graphs and edge geometry reload(ggt) g = ggt.Graph('edge_geometry') g.add_vertex(5) g.add_edge([[0, 1], [1, 2], [2, 3], [3, 4]]) geometry = [np.random.rand(l, 3) for l in [3, 4, 5, 6]] g.set_edge_geometry(geometry) # note te difference ! s = g.sub_graph(vertex_filter=[False]*2 + [True]*3) s.edge_geometry() s.edge_geometry(as_list=False) s._edge_geometry_indices_graph() v = g.sub_graph(vertex_filter=[False]*2 + [True]*3, view=True) v.edge_geometry() v.edge_geometry(as_list=False) v._edge_geometry_indices_graph() # vertex expansion reload(ggt) g = ggt.Graph() g.add_vertex(5) g.add_edge([[0, 1], [1, 2], [2, 3], [3, 4]]) vertex_filter = np.array([False, False, True, False, False], dtype='bool') expanded = g.expand_vertex_filter(vertex_filter, steps=1) print(expanded) # test large arrays in graphs import numpy as np import ClearMap.IO.IO as io import ClearMap.Analysis.graphs.graph_gt as ggt reload(ggt) g = ggt.Graph('test') g.add_vertex(10) x = np.zeros(2147483648, dtype='uint8') g.define_graph_property('test', x) g.save('test.gt') # this gives an error when using unmodified graph_tool del g del x import ClearMap.Analysis.graphs.graph_gt as ggt f = ggt.load('test.gt') f.info() print(f.graph_property('test').shape) io.delete_file('test.gt')