Spaces:
Running
Running
# Original author: D. Eppstein, UC Irvine, August 12, 2003. | |
# The original code at https://www.ics.uci.edu/~eppstein/PADS/ is public domain. | |
"""Functions for reading and writing graphs in the *sparse6* format. | |
The *sparse6* file format is a space-efficient format for large sparse | |
graphs. For small graphs or large dense graphs, use the *graph6* file | |
format. | |
For more information, see the `sparse6`_ homepage. | |
.. _sparse6: https://users.cecs.anu.edu.au/~bdm/data/formats.html | |
""" | |
import networkx as nx | |
from networkx.exception import NetworkXError | |
from networkx.readwrite.graph6 import data_to_n, n_to_data | |
from networkx.utils import not_implemented_for, open_file | |
__all__ = ["from_sparse6_bytes", "read_sparse6", "to_sparse6_bytes", "write_sparse6"] | |
def _generate_sparse6_bytes(G, nodes, header): | |
"""Yield bytes in the sparse6 encoding of a graph. | |
`G` is an undirected simple graph. `nodes` is the list of nodes for | |
which the node-induced subgraph will be encoded; if `nodes` is the | |
list of all nodes in the graph, the entire graph will be | |
encoded. `header` is a Boolean that specifies whether to generate | |
the header ``b'>>sparse6<<'`` before the remaining data. | |
This function generates `bytes` objects in the following order: | |
1. the header (if requested), | |
2. the encoding of the number of nodes, | |
3. each character, one-at-a-time, in the encoding of the requested | |
node-induced subgraph, | |
4. a newline character. | |
This function raises :exc:`ValueError` if the graph is too large for | |
the graph6 format (that is, greater than ``2 ** 36`` nodes). | |
""" | |
n = len(G) | |
if n >= 2**36: | |
raise ValueError( | |
"sparse6 is only defined if number of nodes is less " "than 2 ** 36" | |
) | |
if header: | |
yield b">>sparse6<<" | |
yield b":" | |
for d in n_to_data(n): | |
yield str.encode(chr(d + 63)) | |
k = 1 | |
while 1 << k < n: | |
k += 1 | |
def enc(x): | |
"""Big endian k-bit encoding of x""" | |
return [1 if (x & 1 << (k - 1 - i)) else 0 for i in range(k)] | |
edges = sorted((max(u, v), min(u, v)) for u, v in G.edges()) | |
bits = [] | |
curv = 0 | |
for v, u in edges: | |
if v == curv: # current vertex edge | |
bits.append(0) | |
bits.extend(enc(u)) | |
elif v == curv + 1: # next vertex edge | |
curv += 1 | |
bits.append(1) | |
bits.extend(enc(u)) | |
else: # skip to vertex v and then add edge to u | |
curv = v | |
bits.append(1) | |
bits.extend(enc(v)) | |
bits.append(0) | |
bits.extend(enc(u)) | |
if k < 6 and n == (1 << k) and ((-len(bits)) % 6) >= k and curv < (n - 1): | |
# Padding special case: small k, n=2^k, | |
# more than k bits of padding needed, | |
# current vertex is not (n-1) -- | |
# appending 1111... would add a loop on (n-1) | |
bits.append(0) | |
bits.extend([1] * ((-len(bits)) % 6)) | |
else: | |
bits.extend([1] * ((-len(bits)) % 6)) | |
data = [ | |
(bits[i + 0] << 5) | |
+ (bits[i + 1] << 4) | |
+ (bits[i + 2] << 3) | |
+ (bits[i + 3] << 2) | |
+ (bits[i + 4] << 1) | |
+ (bits[i + 5] << 0) | |
for i in range(0, len(bits), 6) | |
] | |
for d in data: | |
yield str.encode(chr(d + 63)) | |
yield b"\n" | |
def from_sparse6_bytes(string): | |
"""Read an undirected graph in sparse6 format from string. | |
Parameters | |
---------- | |
string : string | |
Data in sparse6 format | |
Returns | |
------- | |
G : Graph | |
Raises | |
------ | |
NetworkXError | |
If the string is unable to be parsed in sparse6 format | |
Examples | |
-------- | |
>>> G = nx.from_sparse6_bytes(b":A_") | |
>>> sorted(G.edges()) | |
[(0, 1), (0, 1), (0, 1)] | |
See Also | |
-------- | |
read_sparse6, write_sparse6 | |
References | |
---------- | |
.. [1] Sparse6 specification | |
<https://users.cecs.anu.edu.au/~bdm/data/formats.html> | |
""" | |
if string.startswith(b">>sparse6<<"): | |
string = string[11:] | |
if not string.startswith(b":"): | |
raise NetworkXError("Expected leading colon in sparse6") | |
chars = [c - 63 for c in string[1:]] | |
n, data = data_to_n(chars) | |
k = 1 | |
while 1 << k < n: | |
k += 1 | |
def parseData(): | |
"""Returns stream of pairs b[i], x[i] for sparse6 format.""" | |
chunks = iter(data) | |
d = None # partial data word | |
dLen = 0 # how many unparsed bits are left in d | |
while 1: | |
if dLen < 1: | |
try: | |
d = next(chunks) | |
except StopIteration: | |
return | |
dLen = 6 | |
dLen -= 1 | |
b = (d >> dLen) & 1 # grab top remaining bit | |
x = d & ((1 << dLen) - 1) # partially built up value of x | |
xLen = dLen # how many bits included so far in x | |
while xLen < k: # now grab full chunks until we have enough | |
try: | |
d = next(chunks) | |
except StopIteration: | |
return | |
dLen = 6 | |
x = (x << 6) + d | |
xLen += 6 | |
x = x >> (xLen - k) # shift back the extra bits | |
dLen = xLen - k | |
yield b, x | |
v = 0 | |
G = nx.MultiGraph() | |
G.add_nodes_from(range(n)) | |
multigraph = False | |
for b, x in parseData(): | |
if b == 1: | |
v += 1 | |
# padding with ones can cause overlarge number here | |
if x >= n or v >= n: | |
break | |
elif x > v: | |
v = x | |
else: | |
if G.has_edge(x, v): | |
multigraph = True | |
G.add_edge(x, v) | |
if not multigraph: | |
G = nx.Graph(G) | |
return G | |
def to_sparse6_bytes(G, nodes=None, header=True): | |
"""Convert an undirected graph to bytes in sparse6 format. | |
Parameters | |
---------- | |
G : Graph (undirected) | |
nodes: list or iterable | |
Nodes are labeled 0...n-1 in the order provided. If None the ordering | |
given by ``G.nodes()`` is used. | |
header: bool | |
If True add '>>sparse6<<' bytes to head of data. | |
Raises | |
------ | |
NetworkXNotImplemented | |
If the graph is directed. | |
ValueError | |
If the graph has at least ``2 ** 36`` nodes; the sparse6 format | |
is only defined for graphs of order less than ``2 ** 36``. | |
Examples | |
-------- | |
>>> nx.to_sparse6_bytes(nx.path_graph(2)) | |
b'>>sparse6<<:An\\n' | |
See Also | |
-------- | |
to_sparse6_bytes, read_sparse6, write_sparse6_bytes | |
Notes | |
----- | |
The returned bytes end with a newline character. | |
The format does not support edge or node labels. | |
References | |
---------- | |
.. [1] Graph6 specification | |
<https://users.cecs.anu.edu.au/~bdm/data/formats.html> | |
""" | |
if nodes is not None: | |
G = G.subgraph(nodes) | |
G = nx.convert_node_labels_to_integers(G, ordering="sorted") | |
return b"".join(_generate_sparse6_bytes(G, nodes, header)) | |
def read_sparse6(path): | |
"""Read an undirected graph in sparse6 format from path. | |
Parameters | |
---------- | |
path : file or string | |
File or filename to write. | |
Returns | |
------- | |
G : Graph/Multigraph or list of Graphs/MultiGraphs | |
If the file contains multiple lines then a list of graphs is returned | |
Raises | |
------ | |
NetworkXError | |
If the string is unable to be parsed in sparse6 format | |
Examples | |
-------- | |
You can read a sparse6 file by giving the path to the file:: | |
>>> import tempfile | |
>>> with tempfile.NamedTemporaryFile(delete=False) as f: | |
... _ = f.write(b">>sparse6<<:An\\n") | |
... _ = f.seek(0) | |
... G = nx.read_sparse6(f.name) | |
>>> list(G.edges()) | |
[(0, 1)] | |
You can also read a sparse6 file by giving an open file-like object:: | |
>>> import tempfile | |
>>> with tempfile.NamedTemporaryFile() as f: | |
... _ = f.write(b">>sparse6<<:An\\n") | |
... _ = f.seek(0) | |
... G = nx.read_sparse6(f) | |
>>> list(G.edges()) | |
[(0, 1)] | |
See Also | |
-------- | |
read_sparse6, from_sparse6_bytes | |
References | |
---------- | |
.. [1] Sparse6 specification | |
<https://users.cecs.anu.edu.au/~bdm/data/formats.html> | |
""" | |
glist = [] | |
for line in path: | |
line = line.strip() | |
if not len(line): | |
continue | |
glist.append(from_sparse6_bytes(line)) | |
if len(glist) == 1: | |
return glist[0] | |
else: | |
return glist | |
def write_sparse6(G, path, nodes=None, header=True): | |
"""Write graph G to given path in sparse6 format. | |
Parameters | |
---------- | |
G : Graph (undirected) | |
path : file or string | |
File or filename to write | |
nodes: list or iterable | |
Nodes are labeled 0...n-1 in the order provided. If None the ordering | |
given by G.nodes() is used. | |
header: bool | |
If True add '>>sparse6<<' string to head of data | |
Raises | |
------ | |
NetworkXError | |
If the graph is directed | |
Examples | |
-------- | |
You can write a sparse6 file by giving the path to the file:: | |
>>> import tempfile | |
>>> with tempfile.NamedTemporaryFile(delete=False) as f: | |
... nx.write_sparse6(nx.path_graph(2), f.name) | |
... print(f.read()) | |
b'>>sparse6<<:An\\n' | |
You can also write a sparse6 file by giving an open file-like object:: | |
>>> with tempfile.NamedTemporaryFile() as f: | |
... nx.write_sparse6(nx.path_graph(2), f) | |
... _ = f.seek(0) | |
... print(f.read()) | |
b'>>sparse6<<:An\\n' | |
See Also | |
-------- | |
read_sparse6, from_sparse6_bytes | |
Notes | |
----- | |
The format does not support edge or node labels. | |
References | |
---------- | |
.. [1] Sparse6 specification | |
<https://users.cecs.anu.edu.au/~bdm/data/formats.html> | |
""" | |
if nodes is not None: | |
G = G.subgraph(nodes) | |
G = nx.convert_node_labels_to_integers(G, ordering="sorted") | |
for b in _generate_sparse6_bytes(G, nodes, header): | |
path.write(b) | |