| | |
| | |
| | """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: |
| | bits.append(0) |
| | bits.extend(enc(u)) |
| | elif v == curv + 1: |
| | curv += 1 |
| | bits.append(1) |
| | bits.extend(enc(u)) |
| | else: |
| | 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): |
| | |
| | |
| | |
| | |
| | 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" |
| |
|
| |
|
| | @nx._dispatchable(graphs=None, returns_graph=True) |
| | 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 |
| | dLen = 0 |
| |
|
| | while 1: |
| | if dLen < 1: |
| | try: |
| | d = next(chunks) |
| | except StopIteration: |
| | return |
| | dLen = 6 |
| | dLen -= 1 |
| | b = (d >> dLen) & 1 |
| |
|
| | x = d & ((1 << dLen) - 1) |
| | xLen = dLen |
| | while xLen < k: |
| | try: |
| | d = next(chunks) |
| | except StopIteration: |
| | return |
| | dLen = 6 |
| | x = (x << 6) + d |
| | xLen += 6 |
| | x = x >> (xLen - k) |
| | 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 |
| | |
| | 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)) |
| |
|
| |
|
| | @open_file(0, mode="rb") |
| | @nx._dispatchable(graphs=None, returns_graph=True) |
| | def read_sparse6(path): |
| | """Read an undirected graph in sparse6 format from path. |
| | |
| | Parameters |
| | ---------- |
| | path : file or string |
| | Filename or file handle to read. |
| | Filenames ending in .gz or .bz2 will be decompressed. |
| | |
| | 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 |
| |
|
| |
|
| | @not_implemented_for("directed") |
| | @open_file(1, mode="wb") |
| | 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. |
| | Filenames ending in .gz or .bz2 will be compressed. |
| | |
| | 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) |
| |
|