1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048 |
- """
- Algorithms for finding optimum branchings and spanning arborescences.
- This implementation is based on:
- J. Edmonds, Optimum branchings, J. Res. Natl. Bur. Standards 71B (1967),
- 233–240. URL: http://archive.org/details/jresv71Bn4p233
- """
- # TODO: Implement method from Gabow, Galil, Spence and Tarjan:
- #
- # @article{
- # year={1986},
- # issn={0209-9683},
- # journal={Combinatorica},
- # volume={6},
- # number={2},
- # doi={10.1007/BF02579168},
- # title={Efficient algorithms for finding minimum spanning trees in
- # undirected and directed graphs},
- # url={https://doi.org/10.1007/BF02579168},
- # publisher={Springer-Verlag},
- # keywords={68 B 15; 68 C 05},
- # author={Gabow, Harold N. and Galil, Zvi and Spencer, Thomas and Tarjan,
- # Robert E.},
- # pages={109-122},
- # language={English}
- # }
- import string
- from dataclasses import dataclass, field
- from enum import Enum
- from operator import itemgetter
- from queue import PriorityQueue
- import networkx as nx
- from networkx.utils import py_random_state
- from .recognition import is_arborescence, is_branching
- __all__ = [
- "branching_weight",
- "greedy_branching",
- "maximum_branching",
- "minimum_branching",
- "maximum_spanning_arborescence",
- "minimum_spanning_arborescence",
- "ArborescenceIterator",
- "Edmonds",
- ]
- KINDS = {"max", "min"}
- STYLES = {
- "branching": "branching",
- "arborescence": "arborescence",
- "spanning arborescence": "arborescence",
- }
- INF = float("inf")
- @py_random_state(1)
- def random_string(L=15, seed=None):
- return "".join([seed.choice(string.ascii_letters) for n in range(L)])
- def _min_weight(weight):
- return -weight
- def _max_weight(weight):
- return weight
- def branching_weight(G, attr="weight", default=1):
- """
- Returns the total weight of a branching.
- You must access this function through the networkx.algorithms.tree module.
- Parameters
- ----------
- G : DiGraph
- The directed graph.
- attr : str
- The attribute to use as weights. If None, then each edge will be
- treated equally with a weight of 1.
- default : float
- When `attr` is not None, then if an edge does not have that attribute,
- `default` specifies what value it should take.
- Returns
- -------
- weight: int or float
- The total weight of the branching.
- Examples
- --------
- >>> G = nx.DiGraph()
- >>> G.add_weighted_edges_from([(0, 1, 2), (1, 2, 4), (2, 3, 3), (3, 4, 2)])
- >>> nx.tree.branching_weight(G)
- 11
- """
- return sum(edge[2].get(attr, default) for edge in G.edges(data=True))
- @py_random_state(4)
- def greedy_branching(G, attr="weight", default=1, kind="max", seed=None):
- """
- Returns a branching obtained through a greedy algorithm.
- This algorithm is wrong, and cannot give a proper optimal branching.
- However, we include it for pedagogical reasons, as it can be helpful to
- see what its outputs are.
- The output is a branching, and possibly, a spanning arborescence. However,
- it is not guaranteed to be optimal in either case.
- Parameters
- ----------
- G : DiGraph
- The directed graph to scan.
- attr : str
- The attribute to use as weights. If None, then each edge will be
- treated equally with a weight of 1.
- default : float
- When `attr` is not None, then if an edge does not have that attribute,
- `default` specifies what value it should take.
- kind : str
- The type of optimum to search for: 'min' or 'max' greedy branching.
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- Returns
- -------
- B : directed graph
- The greedily obtained branching.
- """
- if kind not in KINDS:
- raise nx.NetworkXException("Unknown value for `kind`.")
- if kind == "min":
- reverse = False
- else:
- reverse = True
- if attr is None:
- # Generate a random string the graph probably won't have.
- attr = random_string(seed=seed)
- edges = [(u, v, data.get(attr, default)) for (u, v, data) in G.edges(data=True)]
- # We sort by weight, but also by nodes to normalize behavior across runs.
- try:
- edges.sort(key=itemgetter(2, 0, 1), reverse=reverse)
- except TypeError:
- # This will fail in Python 3.x if the nodes are of varying types.
- # In that case, we use the arbitrary order.
- edges.sort(key=itemgetter(2), reverse=reverse)
- # The branching begins with a forest of no edges.
- B = nx.DiGraph()
- B.add_nodes_from(G)
- # Now we add edges greedily so long we maintain the branching.
- uf = nx.utils.UnionFind()
- for i, (u, v, w) in enumerate(edges):
- if uf[u] == uf[v]:
- # Adding this edge would form a directed cycle.
- continue
- elif B.in_degree(v) == 1:
- # The edge would increase the degree to be greater than one.
- continue
- else:
- # If attr was None, then don't insert weights...
- data = {}
- if attr is not None:
- data[attr] = w
- B.add_edge(u, v, **data)
- uf.union(u, v)
- return B
- class MultiDiGraph_EdgeKey(nx.MultiDiGraph):
- """
- MultiDiGraph which assigns unique keys to every edge.
- Adds a dictionary edge_index which maps edge keys to (u, v, data) tuples.
- This is not a complete implementation. For Edmonds algorithm, we only use
- add_node and add_edge, so that is all that is implemented here. During
- additions, any specified keys are ignored---this means that you also
- cannot update edge attributes through add_node and add_edge.
- Why do we need this? Edmonds algorithm requires that we track edges, even
- as we change the head and tail of an edge, and even changing the weight
- of edges. We must reliably track edges across graph mutations.
- """
- def __init__(self, incoming_graph_data=None, **attr):
- cls = super()
- cls.__init__(incoming_graph_data=incoming_graph_data, **attr)
- self._cls = cls
- self.edge_index = {}
- def remove_node(self, n):
- keys = set()
- for keydict in self.pred[n].values():
- keys.update(keydict)
- for keydict in self.succ[n].values():
- keys.update(keydict)
- for key in keys:
- del self.edge_index[key]
- self._cls.remove_node(n)
- def remove_nodes_from(self, nbunch):
- for n in nbunch:
- self.remove_node(n)
- def add_edge(self, u_for_edge, v_for_edge, key_for_edge, **attr):
- """
- Key is now required.
- """
- u, v, key = u_for_edge, v_for_edge, key_for_edge
- if key in self.edge_index:
- uu, vv, _ = self.edge_index[key]
- if (u != uu) or (v != vv):
- raise Exception(f"Key {key!r} is already in use.")
- self._cls.add_edge(u, v, key, **attr)
- self.edge_index[key] = (u, v, self.succ[u][v][key])
- def add_edges_from(self, ebunch_to_add, **attr):
- for u, v, k, d in ebunch_to_add:
- self.add_edge(u, v, k, **d)
- def remove_edge_with_key(self, key):
- try:
- u, v, _ = self.edge_index[key]
- except KeyError as err:
- raise KeyError(f"Invalid edge key {key!r}") from err
- else:
- del self.edge_index[key]
- self._cls.remove_edge(u, v, key)
- def remove_edges_from(self, ebunch):
- raise NotImplementedError
- def get_path(G, u, v):
- """
- Returns the edge keys of the unique path between u and v.
- This is not a generic function. G must be a branching and an instance of
- MultiDiGraph_EdgeKey.
- """
- nodes = nx.shortest_path(G, u, v)
- # We are guaranteed that there is only one edge connected every node
- # in the shortest path.
- def first_key(i, vv):
- # Needed for 2.x/3.x compatibilitity
- keys = G[nodes[i]][vv].keys()
- # Normalize behavior
- keys = list(keys)
- return keys[0]
- edges = [first_key(i, vv) for i, vv in enumerate(nodes[1:])]
- return nodes, edges
- class Edmonds:
- """
- Edmonds algorithm [1]_ for finding optimal branchings and spanning
- arborescences.
- This algorithm can find both minimum and maximum spanning arborescences and
- branchings.
- Notes
- -----
- While this algorithm can find a minimum branching, since it isn't required
- to be spanning, the minimum branching is always from the set of negative
- weight edges which is most likely the empty set for most graphs.
- References
- ----------
- .. [1] J. Edmonds, Optimum Branchings, Journal of Research of the National
- Bureau of Standards, 1967, Vol. 71B, p.233-240,
- https://archive.org/details/jresv71Bn4p233
- """
- def __init__(self, G, seed=None):
- self.G_original = G
- # Need to fix this. We need the whole tree.
- self.store = True
- # The final answer.
- self.edges = []
- # Since we will be creating graphs with new nodes, we need to make
- # sure that our node names do not conflict with the real node names.
- self.template = random_string(seed=seed) + "_{0}"
- def _init(self, attr, default, kind, style, preserve_attrs, seed, partition):
- if kind not in KINDS:
- raise nx.NetworkXException("Unknown value for `kind`.")
- # Store inputs.
- self.attr = attr
- self.default = default
- self.kind = kind
- self.style = style
- # Determine how we are going to transform the weights.
- if kind == "min":
- self.trans = trans = _min_weight
- else:
- self.trans = trans = _max_weight
- if attr is None:
- # Generate a random attr the graph probably won't have.
- attr = random_string(seed=seed)
- # This is the actual attribute used by the algorithm.
- self._attr = attr
- # This attribute is used to store whether a particular edge is still
- # a candidate. We generate a random attr to remove clashes with
- # preserved edges
- self.candidate_attr = "candidate_" + random_string(seed=seed)
- # The object we manipulate at each step is a multidigraph.
- self.G = G = MultiDiGraph_EdgeKey()
- for key, (u, v, data) in enumerate(self.G_original.edges(data=True)):
- d = {attr: trans(data.get(attr, default))}
- if data.get(partition) is not None:
- d[partition] = data.get(partition)
- if preserve_attrs:
- for d_k, d_v in data.items():
- if d_k != attr:
- d[d_k] = d_v
- G.add_edge(u, v, key, **d)
- self.level = 0
- # These are the "buckets" from the paper.
- #
- # As in the paper, G^i are modified versions of the original graph.
- # D^i and E^i are nodes and edges of the maximal edges that are
- # consistent with G^i. These are dashed edges in figures A-F of the
- # paper. In this implementation, we store D^i and E^i together as a
- # graph B^i. So we will have strictly more B^i than the paper does.
- self.B = MultiDiGraph_EdgeKey()
- self.B.edge_index = {}
- self.graphs = [] # G^i
- self.branchings = [] # B^i
- self.uf = nx.utils.UnionFind()
- # A list of lists of edge indexes. Each list is a circuit for graph G^i.
- # Note the edge list will not, in general, be a circuit in graph G^0.
- self.circuits = []
- # Stores the index of the minimum edge in the circuit found in G^i
- # and B^i. The ordering of the edges seems to preserve the weight
- # ordering from G^0. So even if the circuit does not form a circuit
- # in G^0, it is still true that the minimum edge of the circuit in
- # G^i is still the minimum edge in circuit G^0 (despite their weights
- # being different).
- self.minedge_circuit = []
- def find_optimum(
- self,
- attr="weight",
- default=1,
- kind="max",
- style="branching",
- preserve_attrs=False,
- partition=None,
- seed=None,
- ):
- """
- Returns a branching from G.
- Parameters
- ----------
- attr : str
- The edge attribute used to in determining optimality.
- default : float
- The value of the edge attribute used if an edge does not have
- the attribute `attr`.
- kind : {'min', 'max'}
- The type of optimum to search for, either 'min' or 'max'.
- style : {'branching', 'arborescence'}
- If 'branching', then an optimal branching is found. If `style` is
- 'arborescence', then a branching is found, such that if the
- branching is also an arborescence, then the branching is an
- optimal spanning arborescences. A given graph G need not have
- an optimal spanning arborescence.
- preserve_attrs : bool
- If True, preserve the other edge attributes of the original
- graph (that are not the one passed to `attr`)
- partition : str
- The edge attribute holding edge partition data. Used in the
- spanning arborescence iterator.
- seed : integer, random_state, or None (default)
- Indicator of random number generation state.
- See :ref:`Randomness<randomness>`.
- Returns
- -------
- H : (multi)digraph
- The branching.
- """
- self._init(attr, default, kind, style, preserve_attrs, seed, partition)
- uf = self.uf
- # This enormous while loop could use some refactoring...
- G, B = self.G, self.B
- D = set()
- nodes = iter(list(G.nodes()))
- attr = self._attr
- G_pred = G.pred
- def desired_edge(v):
- """
- Find the edge directed toward v with maximal weight.
- If an edge partition exists in this graph, return the included edge
- if it exists and no not return any excluded edges. There can only
- be one included edge for each vertex otherwise the edge partition is
- empty.
- """
- edge = None
- weight = -INF
- for u, _, key, data in G.in_edges(v, data=True, keys=True):
- # Skip excluded edges
- if data.get(partition) == nx.EdgePartition.EXCLUDED:
- continue
- new_weight = data[attr]
- # Return the included edge
- if data.get(partition) == nx.EdgePartition.INCLUDED:
- weight = new_weight
- edge = (u, v, key, new_weight, data)
- return edge, weight
- # Find the best open edge
- if new_weight > weight:
- weight = new_weight
- edge = (u, v, key, new_weight, data)
- return edge, weight
- while True:
- # (I1): Choose a node v in G^i not in D^i.
- try:
- v = next(nodes)
- except StopIteration:
- # If there are no more new nodes to consider, then we *should*
- # meet the break condition (b) from the paper:
- # (b) every node of G^i is in D^i and E^i is a branching
- # Construction guarantees that it's a branching.
- assert len(G) == len(B)
- if len(B):
- assert is_branching(B)
- if self.store:
- self.graphs.append(G.copy())
- self.branchings.append(B.copy())
- # Add these to keep the lengths equal. Element i is the
- # circuit at level i that was merged to form branching i+1.
- # There is no circuit for the last level.
- self.circuits.append([])
- self.minedge_circuit.append(None)
- break
- else:
- if v in D:
- # print("v in D", v)
- continue
- # Put v into bucket D^i.
- # print(f"Adding node {v}")
- D.add(v)
- B.add_node(v)
- edge, weight = desired_edge(v)
- # print(f"Max edge is {edge!r}")
- if edge is None:
- # If there is no edge, continue with a new node at (I1).
- continue
- else:
- # Determine if adding the edge to E^i would mean its no longer
- # a branching. Presently, v has indegree 0 in B---it is a root.
- u = edge[0]
- if uf[u] == uf[v]:
- # Then adding the edge will create a circuit. Then B
- # contains a unique path P from v to u. So condition (a)
- # from the paper does hold. We need to store the circuit
- # for future reference.
- Q_nodes, Q_edges = get_path(B, v, u)
- Q_edges.append(edge[2]) # Edge key
- else:
- # Then B with the edge is still a branching and condition
- # (a) from the paper does not hold.
- Q_nodes, Q_edges = None, None
- # Conditions for adding the edge.
- # If weight < 0, then it cannot help in finding a maximum branching.
- if self.style == "branching" and weight <= 0:
- acceptable = False
- else:
- acceptable = True
- # print(f"Edge is acceptable: {acceptable}")
- if acceptable:
- dd = {attr: weight}
- if edge[4].get(partition) is not None:
- dd[partition] = edge[4].get(partition)
- B.add_edge(u, v, edge[2], **dd)
- G[u][v][edge[2]][self.candidate_attr] = True
- uf.union(u, v)
- if Q_edges is not None:
- # print("Edge introduced a simple cycle:")
- # print(Q_nodes, Q_edges)
- # Move to method
- # Previous meaning of u and v is no longer important.
- # Apply (I2).
- # Get the edge in the cycle with the minimum weight.
- # Also, save the incoming weights for each node.
- minweight = INF
- minedge = None
- Q_incoming_weight = {}
- for edge_key in Q_edges:
- u, v, data = B.edge_index[edge_key]
- # We cannot remove an included edges, even if it is
- # the minimum edge in the circuit
- w = data[attr]
- Q_incoming_weight[v] = w
- if data.get(partition) == nx.EdgePartition.INCLUDED:
- continue
- if w < minweight:
- minweight = w
- minedge = edge_key
- self.circuits.append(Q_edges)
- self.minedge_circuit.append(minedge)
- if self.store:
- self.graphs.append(G.copy())
- # Always need the branching with circuits.
- self.branchings.append(B.copy())
- # Now we mutate it.
- new_node = self.template.format(self.level)
- # print(minweight, minedge, Q_incoming_weight)
- G.add_node(new_node)
- new_edges = []
- for u, v, key, data in G.edges(data=True, keys=True):
- if u in Q_incoming_weight:
- if v in Q_incoming_weight:
- # Circuit edge, do nothing for now.
- # Eventually delete it.
- continue
- else:
- # Outgoing edge. Make it from new node
- dd = data.copy()
- new_edges.append((new_node, v, key, dd))
- else:
- if v in Q_incoming_weight:
- # Incoming edge. Change its weight
- w = data[attr]
- w += minweight - Q_incoming_weight[v]
- dd = data.copy()
- dd[attr] = w
- new_edges.append((u, new_node, key, dd))
- else:
- # Outside edge. No modification necessary.
- continue
- G.remove_nodes_from(Q_nodes)
- B.remove_nodes_from(Q_nodes)
- D.difference_update(set(Q_nodes))
- for u, v, key, data in new_edges:
- G.add_edge(u, v, key, **data)
- if self.candidate_attr in data:
- del data[self.candidate_attr]
- B.add_edge(u, v, key, **data)
- uf.union(u, v)
- nodes = iter(list(G.nodes()))
- self.level += 1
- # (I3) Branch construction.
- # print(self.level)
- H = self.G_original.__class__()
- def is_root(G, u, edgekeys):
- """
- Returns True if `u` is a root node in G.
- Node `u` will be a root node if its in-degree, restricted to the
- specified edges, is equal to 0.
- """
- if u not in G:
- # print(G.nodes(), u)
- raise Exception(f"{u!r} not in G")
- for v in G.pred[u]:
- for edgekey in G.pred[u][v]:
- if edgekey in edgekeys:
- return False, edgekey
- else:
- return True, None
- # Start with the branching edges in the last level.
- edges = set(self.branchings[self.level].edge_index)
- while self.level > 0:
- self.level -= 1
- # The current level is i, and we start counting from 0.
- # We need the node at level i+1 that results from merging a circuit
- # at level i. randomname_0 is the first merged node and this
- # happens at level 1. That is, randomname_0 is a node at level 1
- # that results from merging a circuit at level 0.
- merged_node = self.template.format(self.level)
- # The circuit at level i that was merged as a node the graph
- # at level i+1.
- circuit = self.circuits[self.level]
- # print
- # print(merged_node, self.level, circuit)
- # print("before", edges)
- # Note, we ask if it is a root in the full graph, not the branching.
- # The branching alone doesn't have all the edges.
- isroot, edgekey = is_root(self.graphs[self.level + 1], merged_node, edges)
- edges.update(circuit)
- if isroot:
- minedge = self.minedge_circuit[self.level]
- if minedge is None:
- raise Exception
- # Remove the edge in the cycle with minimum weight.
- edges.remove(minedge)
- else:
- # We have identified an edge at next higher level that
- # transitions into the merged node at the level. That edge
- # transitions to some corresponding node at the current level.
- # We want to remove an edge from the cycle that transitions
- # into the corresponding node.
- # print("edgekey is: ", edgekey)
- # print("circuit is: ", circuit)
- # The branching at level i
- G = self.graphs[self.level]
- # print(G.edge_index)
- target = G.edge_index[edgekey][1]
- for edgekey in circuit:
- u, v, data = G.edge_index[edgekey]
- if v == target:
- break
- else:
- raise Exception("Couldn't find edge incoming to merged node.")
- # print(f"not a root. removing {edgekey}")
- edges.remove(edgekey)
- self.edges = edges
- H.add_nodes_from(self.G_original)
- for edgekey in edges:
- u, v, d = self.graphs[0].edge_index[edgekey]
- dd = {self.attr: self.trans(d[self.attr])}
- # Optionally, preserve the other edge attributes of the original
- # graph
- if preserve_attrs:
- for key, value in d.items():
- if key not in [self.attr, self.candidate_attr]:
- dd[key] = value
- # TODO: make this preserve the key.
- H.add_edge(u, v, **dd)
- return H
- def maximum_branching(
- G, attr="weight", default=1, preserve_attrs=False, partition=None
- ):
- ed = Edmonds(G)
- B = ed.find_optimum(
- attr,
- default,
- kind="max",
- style="branching",
- preserve_attrs=preserve_attrs,
- partition=partition,
- )
- return B
- def minimum_branching(
- G, attr="weight", default=1, preserve_attrs=False, partition=None
- ):
- ed = Edmonds(G)
- B = ed.find_optimum(
- attr,
- default,
- kind="min",
- style="branching",
- preserve_attrs=preserve_attrs,
- partition=partition,
- )
- return B
- def maximum_spanning_arborescence(
- G, attr="weight", default=1, preserve_attrs=False, partition=None
- ):
- ed = Edmonds(G)
- B = ed.find_optimum(
- attr,
- default,
- kind="max",
- style="arborescence",
- preserve_attrs=preserve_attrs,
- partition=partition,
- )
- if not is_arborescence(B):
- msg = "No maximum spanning arborescence in G."
- raise nx.exception.NetworkXException(msg)
- return B
- def minimum_spanning_arborescence(
- G, attr="weight", default=1, preserve_attrs=False, partition=None
- ):
- ed = Edmonds(G)
- B = ed.find_optimum(
- attr,
- default,
- kind="min",
- style="arborescence",
- preserve_attrs=preserve_attrs,
- partition=partition,
- )
- if not is_arborescence(B):
- msg = "No minimum spanning arborescence in G."
- raise nx.exception.NetworkXException(msg)
- return B
- docstring_branching = """
- Returns a {kind} {style} from G.
- Parameters
- ----------
- G : (multi)digraph-like
- The graph to be searched.
- attr : str
- The edge attribute used to in determining optimality.
- default : float
- The value of the edge attribute used if an edge does not have
- the attribute `attr`.
- preserve_attrs : bool
- If True, preserve the other attributes of the original graph (that are not
- passed to `attr`)
- partition : str
- The key for the edge attribute containing the partition
- data on the graph. Edges can be included, excluded or open using the
- `EdgePartition` enum.
- Returns
- -------
- B : (multi)digraph-like
- A {kind} {style}.
- """
- docstring_arborescence = (
- docstring_branching
- + """
- Raises
- ------
- NetworkXException
- If the graph does not contain a {kind} {style}.
- """
- )
- maximum_branching.__doc__ = docstring_branching.format(
- kind="maximum", style="branching"
- )
- minimum_branching.__doc__ = docstring_branching.format(
- kind="minimum", style="branching"
- )
- maximum_spanning_arborescence.__doc__ = docstring_arborescence.format(
- kind="maximum", style="spanning arborescence"
- )
- minimum_spanning_arborescence.__doc__ = docstring_arborescence.format(
- kind="minimum", style="spanning arborescence"
- )
- class ArborescenceIterator:
- """
- Iterate over all spanning arborescences of a graph in either increasing or
- decreasing cost.
- Notes
- -----
- This iterator uses the partition scheme from [1]_ (included edges,
- excluded edges and open edges). It generates minimum spanning
- arborescences using a modified Edmonds' Algorithm which respects the
- partition of edges. For arborescences with the same weight, ties are
- broken arbitrarily.
- References
- ----------
- .. [1] G.K. Janssens, K. Sörensen, An algorithm to generate all spanning
- trees in order of increasing cost, Pesquisa Operacional, 2005-08,
- Vol. 25 (2), p. 219-229,
- https://www.scielo.br/j/pope/a/XHswBwRwJyrfL88dmMwYNWp/?lang=en
- """
- @dataclass(order=True)
- class Partition:
- """
- This dataclass represents a partition and stores a dict with the edge
- data and the weight of the minimum spanning arborescence of the
- partition dict.
- """
- mst_weight: float
- partition_dict: dict = field(compare=False)
- def __copy__(self):
- return ArborescenceIterator.Partition(
- self.mst_weight, self.partition_dict.copy()
- )
- def __init__(self, G, weight="weight", minimum=True, init_partition=None):
- """
- Initialize the iterator
- Parameters
- ----------
- G : nx.DiGraph
- The directed graph which we need to iterate trees over
- weight : String, default = "weight"
- The edge attribute used to store the weight of the edge
- minimum : bool, default = True
- Return the trees in increasing order while true and decreasing order
- while false.
- init_partition : tuple, default = None
- In the case that certain edges have to be included or excluded from
- the arborescences, `init_partition` should be in the form
- `(included_edges, excluded_edges)` where each edges is a
- `(u, v)`-tuple inside an iterable such as a list or set.
- """
- self.G = G.copy()
- self.weight = weight
- self.minimum = minimum
- self.method = (
- minimum_spanning_arborescence if minimum else maximum_spanning_arborescence
- )
- # Randomly create a key for an edge attribute to hold the partition data
- self.partition_key = (
- "ArborescenceIterators super secret partition attribute name"
- )
- if init_partition is not None:
- partition_dict = {}
- for e in init_partition[0]:
- partition_dict[e] = nx.EdgePartition.INCLUDED
- for e in init_partition[1]:
- partition_dict[e] = nx.EdgePartition.EXCLUDED
- self.init_partition = ArborescenceIterator.Partition(0, partition_dict)
- else:
- self.init_partition = None
- def __iter__(self):
- """
- Returns
- -------
- ArborescenceIterator
- The iterator object for this graph
- """
- self.partition_queue = PriorityQueue()
- self._clear_partition(self.G)
- # Write the initial partition if it exists.
- if self.init_partition is not None:
- self._write_partition(self.init_partition)
- mst_weight = self.method(
- self.G,
- self.weight,
- partition=self.partition_key,
- preserve_attrs=True,
- ).size(weight=self.weight)
- self.partition_queue.put(
- self.Partition(
- mst_weight if self.minimum else -mst_weight,
- {}
- if self.init_partition is None
- else self.init_partition.partition_dict,
- )
- )
- return self
- def __next__(self):
- """
- Returns
- -------
- (multi)Graph
- The spanning tree of next greatest weight, which ties broken
- arbitrarily.
- """
- if self.partition_queue.empty():
- del self.G, self.partition_queue
- raise StopIteration
- partition = self.partition_queue.get()
- self._write_partition(partition)
- next_arborescence = self.method(
- self.G,
- self.weight,
- partition=self.partition_key,
- preserve_attrs=True,
- )
- self._partition(partition, next_arborescence)
- self._clear_partition(next_arborescence)
- return next_arborescence
- def _partition(self, partition, partition_arborescence):
- """
- Create new partitions based of the minimum spanning tree of the
- current minimum partition.
- Parameters
- ----------
- partition : Partition
- The Partition instance used to generate the current minimum spanning
- tree.
- partition_arborescence : nx.Graph
- The minimum spanning arborescence of the input partition.
- """
- # create two new partitions with the data from the input partition dict
- p1 = self.Partition(0, partition.partition_dict.copy())
- p2 = self.Partition(0, partition.partition_dict.copy())
- for e in partition_arborescence.edges:
- # determine if the edge was open or included
- if e not in partition.partition_dict:
- # This is an open edge
- p1.partition_dict[e] = nx.EdgePartition.EXCLUDED
- p2.partition_dict[e] = nx.EdgePartition.INCLUDED
- self._write_partition(p1)
- try:
- p1_mst = self.method(
- self.G,
- self.weight,
- partition=self.partition_key,
- preserve_attrs=True,
- )
- p1_mst_weight = p1_mst.size(weight=self.weight)
- p1.mst_weight = p1_mst_weight if self.minimum else -p1_mst_weight
- self.partition_queue.put(p1.__copy__())
- except nx.NetworkXException:
- pass
- p1.partition_dict = p2.partition_dict.copy()
- def _write_partition(self, partition):
- """
- Writes the desired partition into the graph to calculate the minimum
- spanning tree. Also, if one incoming edge is included, mark all others
- as excluded so that if that vertex is merged during Edmonds' algorithm
- we cannot still pick another of that vertex's included edges.
- Parameters
- ----------
- partition : Partition
- A Partition dataclass describing a partition on the edges of the
- graph.
- """
- for u, v, d in self.G.edges(data=True):
- if (u, v) in partition.partition_dict:
- d[self.partition_key] = partition.partition_dict[(u, v)]
- else:
- d[self.partition_key] = nx.EdgePartition.OPEN
- for n in self.G:
- included_count = 0
- excluded_count = 0
- for u, v, d in self.G.in_edges(nbunch=n, data=True):
- if d.get(self.partition_key) == nx.EdgePartition.INCLUDED:
- included_count += 1
- elif d.get(self.partition_key) == nx.EdgePartition.EXCLUDED:
- excluded_count += 1
- # Check that if there is an included edges, all other incoming ones
- # are excluded. If not fix it!
- if included_count == 1 and excluded_count != self.G.in_degree(n) - 1:
- for u, v, d in self.G.in_edges(nbunch=n, data=True):
- if d.get(self.partition_key) != nx.EdgePartition.INCLUDED:
- d[self.partition_key] = nx.EdgePartition.EXCLUDED
- def _clear_partition(self, G):
- """
- Removes partition data from the graph
- """
- for u, v, d in G.edges(data=True):
- if self.partition_key in d:
- del d[self.partition_key]
|