Source code for ClearMap.Analysis.graphs.utils

import sys
import mmap
import struct

import graph_tool as gt
import numpy as np

from ClearMap.External import pickle_python_3 as pickle



# ---- Type tables for properties scanner -----------------------------------------------------------
# Known value-type byte codes used by graph-tool. These have been stable for years.
# If you hit a ValueError with an unknown code, add it here (and in TYPE_SIZES).
TYPE_NAMES = {
    0x00: "bool",            # stored as 1 byte (uchar)
    0x01: "int16_t",
    0x02: "int32_t",
    0x03: "int64_t",
    0x04: "double",
    0x05: "long double",     # 16 bytes on x86_64 builds; treat as 16 here
    0x06: "string",
    0x07: "vector<bool>",
    0x08: "vector<int16_t>",
    0x09: "vector<int32_t>",
    0x0A: "vector<int64_t>",
    0x0B: "vector<double>",
    0x0C: "vector<long double>",
    0x0D: "vector<string>",
    0x0E: "python::object",  # pickled bytes (length-prefixed string)
    # The following two are present in recent versions:
    0x0F: "int8_t",          # signed 8-bit (rare but supported)
    0x10: "uint64_t",        # unsigned long (common for IDs/counters)
}

# Fixed-width sizes for scalar codes (others are length-prefixed)
TYPE_SIZES = {
    0x00: 1,   # bool (uchar)
    0x0F: 1,   # int8_t
    0x01: 2,   # int16_t
    0x02: 4,   # int32_t
    0x03: 8,   # int64_t
    0x04: 8,   # double
    0x05: 16,  # long double (graph-tool writes 16 bytes)
    0x10: 8,   # uint64_t
}

# vector element sizes
VEC_ELEM_SIZE = {
    0x07: 1,    # vector<bool> (as bytes)
    0x08: 2,    # vector<int16_t>
    0x09: 4,    # vector<int32_t>
    0x0A: 8,    # vector<int64_t>
    0x0B: 8,    # vector<double>
    0x0C: 16,   # vector<long double)
}

SCOPE = {0x00: "graph", 0x01: "vertex", 0x02: "edge"}

# ---- Helpers for properties scanner ---------------------------------------------------------------
[docs] def u8(mm, off): return mm[off], off + 1
[docs] def u64(mm, off, endian): return struct.unpack(endian + 'Q', mm[off:off+8])[0], off + 8
[docs] def read_str(mm, off, endian): L, off = u64(mm, off, endian) s = mm[off:off+L].decode('utf-8', errors='strict') return s, off + L
[docs] def skip_str(mm, off, endian): L, off = u64(mm, off, endian) return off + L
[docs] def skip_vec_scalar(mm, off, endian, elem_size): L, off = u64(mm, off, endian) return off + L * elem_size
[docs] def skip_vec_string(mm, off, endian): L, off = u64(mm, off, endian) for _ in range(L): off = skip_str(mm, off, endian) return off
[docs] def idx_width_for_N(N): if N <= 0xFF: return 1 if N <= 0xFFFF: return 2 if N <= 0xFFFFFFFF: return 4 return 8
# -------------- properties scanner ----------------------------------------------------------------
[docs] def scan_gt_props(path): """Scan a .gt file and return a list of (scope, name, type-name) for each property.""" out = [] with open(path, 'rb') as f, mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm: off = 0 # Magic "\xe2\x9b\xbe gt" (the teacup), version, endianness if len(mm) < 8 or mm[:6] != b'\xe2\x9b\xbe gt': raise ValueError("Not a .gt file (bad magic)") off = 6 version, off = u8(mm, off) endian_flag, off = u8(mm, off) endian = '<' if endian_flag == 0x00 else '>' # Comment (length-prefixed UTF-8). Your example shows stats in here. comment, off = read_str(mm, off, endian) # print("comment:", comment) # optional # Graph structure directed_flag, off = u8(mm, off) N, off = u64(mm, off, endian) idx_sz = idx_width_for_N(N) E = 0 for _ in range(N): d_i, off = u64(mm, off, endian) E += d_i off += d_i * idx_sz # skip neighbors # If undirected, E is the number of stored adjacency entries (one per edge). # That is exactly the number of edge-property values that follow. # Properties total_props, off = u64(mm, off, endian) for _ in range(total_props): scope_code, off = u8(mm, off) name, off = read_str(mm, off, endian) vtype, off = u8(mm, off) scope = SCOPE.get(scope_code, f"unknown({scope_code})") vname = TYPE_NAMES.get(vtype, None) if vname is None: raise ValueError(f"Unknown value-type code 0x{vtype:02x} at file offset {off-1}. " f"Please extend TYPE_NAMES/TYPE_SIZES/VEC_ELEM_SIZE as needed.") out.append((scope, name, vname)) # Decide how many values to skip count = 1 if scope_code == 0x00 else (N if scope_code == 0x01 else E) # Skip payload if vtype in TYPE_SIZES: # fixed-width scalar (incl. uint64 and long double) off += TYPE_SIZES[vtype] * count elif vtype == 0x06: # string for _ in range(count): off = skip_str(mm, off, endian) elif vtype in VEC_ELEM_SIZE: # vector<T> where T is scalar for _ in range(count): off = skip_vec_scalar(mm, off, endian, VEC_ELEM_SIZE[vtype]) elif vtype == 0x0D: # vector<string> for _ in range(count): off = skip_vec_string(mm, off, endian) elif vtype == 0x0E: # python::object (bytes, length-prefixed) for _ in range(count): off = skip_str(mm, off, endian) else: # Should be unreachable due to earlier guard raise ValueError(f"Unhandled type 0x{vtype:02x} at {off}") return out
# --------------------------------------------------------------------------------------------------
[docs] def pickler(stream, obj): sstream = gt.gt_io.BytesIO() pickle.dump(obj, sstream) # , gt.gt_io.GT_PICKLE_PROTOCOL) stream.write(sstream.getvalue())
[docs] def unpickler(stream): data = stream.read(buflen=2**31) # print('unpickler loaded %d' % len(data)) sstream = gt.gt_io.BytesIO(data) if sys.version_info < (3,): return pickle.load(sstream) return pickle.load(sstream, encoding="bytes")
[docs] def edges_to_vertices(edges): return np.array([[int(e.source()), int(e.target())] for e in edges])
# return np.array([[e.source(), e.target()] for e in edges], dtype=int) # TODO: see which is better cpu/RAM wise