Skip to content

Commit

Permalink
sagemathgh-38791: non recursive version of method gomory_hu_tree fo…
Browse files Browse the repository at this point in the history
…r graphs

    
This PR implements a non recursive version of method `gomory_hu_tree`
for graphs. This fixes the max recursion depth error reported in
https://ask.sagemath.org/question/79577/graphs-gomory-hu-tree-memory-
blow-up-and-max-recursion-depth/. The memory consumption is seriously
reduced.

Without this PR:
```py
sage: def test(g):
....:     from datetime import datetime
....:     import psutil
....:     start_time = datetime.now()
....:     process = psutil.Process(os.getpid())
....:     mem = process.memory_info()[0] / float(2 ** 20)
....:     print("Mem usage at start:", mem, "MiB")
....:     try:
....:         print("Vertices found:", g.order(), "and edges:",
g.size())
....:         T = g.gomory_hu_tree(algorithm="FF")
....:     except Exception as error:
....:         print("Error detected:", error)
....:     finally:
....:         end_time = datetime.now()
....:         print("Runtime =", end_time - start_time)
....:         mem = process.memory_info()[0] / float(2 ** 20)
....:         print("Mem usage at end:", mem, "MiB")
....:
sage: test(graphs.SierpinskiGasketGraph(5))
Mem usage at start: 241.0234375 MiB
Vertices found: 123 and edges: 243
Runtime = 0:00:00.243477
Mem usage at end: 252.1171875 MiB
sage: test(graphs.SierpinskiGasketGraph(6))
Mem usage at start: 252.1171875 MiB
Vertices found: 366 and edges: 729
Runtime = 0:00:02.050924
Mem usage at end: 324.30078125 MiB
sage: test(graphs.SierpinskiGasketGraph(7))
Mem usage at start: 324.30078125 MiB
Vertices found: 1095 and edges: 2187
Runtime = 0:00:21.207451
Mem usage at end: 968.97265625 MiB
sage: test(graphs.SierpinskiGasketGraph(8))
Mem usage at start: 950.25390625 MiB
Vertices found: 3282 and edges: 6561
Error detected: maximum recursion depth exceeded
Runtime = 0:04:36.154550
Mem usage at end: 6767.39453125 MiB
```

With this PR
```py
sage: test(graphs.SierpinskiGasketGraph(5))
Mem usage at start: 246.0859375 MiB
Vertices found: 123 and edges: 243
Runtime = 0:00:00.219925
Mem usage at end: 246.7109375 MiB
sage: test(graphs.SierpinskiGasketGraph(6))
Mem usage at start: 247.0234375 MiB
Vertices found: 366 and edges: 729
Runtime = 0:00:01.900761
Mem usage at end: 248.2734375 MiB
sage: test(graphs.SierpinskiGasketGraph(7))
Mem usage at start: 248.5859375 MiB
Vertices found: 1095 and edges: 2187
Runtime = 0:00:18.535145
Mem usage at end: 252.4921875 MiB
sage: test(graphs.SierpinskiGasketGraph(8))
Mem usage at start: 253.1171875 MiB
Vertices found: 3282 and edges: 6561
Runtime = 0:02:57.325506
Mem usage at end: 265.15234375 MiB
sage: test(graphs.SierpinskiGasketGraph(9))
Mem usage at start: 263.625 MiB
Vertices found: 9843 and edges: 19683
Runtime = 0:29:03.321870
Mem usage at end: 296.8359375 MiB
sage: test(graphs.SierpinskiGasketGraph(10))
Mem usage at start: 303.11328125 MiB
Vertices found: 29526 and edges: 59049
Runtime = 5:01:45.355463
Mem usage at end: 399.3984375 MiB
```




### :memo: Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [x] The title is concise and informative.
- [x] The description explains in detail what this PR is about.
- [x] I have linked a relevant issue or discussion.
- [ ] I have created tests covering the changes.
- [x] I have updated the documentation and checked the documentation
preview.

### :hourglass: Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - sagemath#12345: short description why this is a dependency -->
<!-- - sagemath#34567: ... -->
    
URL: sagemath#38791
Reported by: David Coudert
Reviewer(s): Dima Pasechnik
  • Loading branch information
Release Manager committed Nov 7, 2024
2 parents 9f1486d + b844daa commit 3075cea
Showing 1 changed file with 96 additions and 100 deletions.
196 changes: 96 additions & 100 deletions src/sage/graphs/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -7673,97 +7673,9 @@ def is_prime(self, algorithm=None):

return D[0] == NodeType.PRIME and len(D[1]) == self.order()

def _gomory_hu_tree(self, vertices, algorithm=None):
r"""
Return a Gomory-Hu tree associated to ``self``.
This function is the private counterpart of ``gomory_hu_tree()``, with
the difference that it has an optional argument needed for recursive
computations, which the user is not interested in defining himself.
See the documentation of ``gomory_hu_tree()`` for more information.
INPUT:
- ``vertices`` -- set of "real" vertices, as opposed to the fakes one
introduced during the computations. This variable is useful for the
algorithm and for recursion purposes.
- ``algorithm`` -- select the algorithm used by the :meth:`edge_cut`
method. Refer to its documentation for allowed values and default
behaviour.
EXAMPLES:
This function is actually tested in ``gomory_hu_tree()``, this example
is only present to have a doctest coverage of 100%::
sage: g = graphs.PetersenGraph()
sage: t = g._gomory_hu_tree(frozenset(g.vertices(sort=False)))
"""
self._scream_if_not_simple()

# Small case, not really a problem ;-)
if len(vertices) == 1:
g = Graph()
g.add_vertices(vertices)
return g

# Take any two vertices (u,v)
it = iter(vertices)
u, v = next(it), next(it)

# Compute a uv min-edge-cut.
#
# The graph is split into U,V with u \in U and v\in V.
flow, edges, [U, V] = self.edge_cut(u, v, use_edge_labels=True,
vertices=True, algorithm=algorithm)

# One graph for each part of the previous one
gU, gV = self.subgraph(U, immutable=False), self.subgraph(V, immutable=False)

# A fake vertex fU (resp. fV) to represent U (resp. V)
fU = frozenset(U)
fV = frozenset(V)

# Each edge (uu,vv) with uu \in U and vv\in V yields:
# - an edge (uu,fV) in gU
# - an edge (vv,fU) in gV
#
# If the same edge is added several times their capacities add up.

from sage.rings.real_mpfr import RR
for uu, vv, capacity in edges:
capacity = capacity if capacity in RR else 1

# Assume uu is in gU
if uu in V:
uu, vv = vv, uu

# Create the new edges if necessary
if not gU.has_edge(uu, fV):
gU.add_edge(uu, fV, 0)
if not gV.has_edge(vv, fU):
gV.add_edge(vv, fU, 0)

# update the capacities
gU.set_edge_label(uu, fV, gU.edge_label(uu, fV) + capacity)
gV.set_edge_label(vv, fU, gV.edge_label(vv, fU) + capacity)

# Recursion on each side
gU_tree = gU._gomory_hu_tree(vertices & frozenset(gU), algorithm=algorithm)
gV_tree = gV._gomory_hu_tree(vertices & frozenset(gV), algorithm=algorithm)

# Union of the two partial trees
g = gU_tree.union(gV_tree)

# An edge to connect them, with the appropriate label
g.add_edge(u, v, flow)

return g

@doc_index("Connectivity, orientations, trees")
def gomory_hu_tree(self, algorithm=None):
def gomory_hu_tree(self, algorithm=None, solver=None, verbose=0,
*, integrality_tolerance=1e-3):
r"""
Return a Gomory-Hu tree of ``self``.
Expand All @@ -7784,6 +7696,27 @@ def gomory_hu_tree(self, algorithm=None):
method. Refer to its documentation for allowed values and default
behaviour.
- ``solver`` -- string (default: ``None``); specifies a Mixed Integer
Linear Programming (MILP) solver to be used. If set to ``None``, the
default one is used. For more information on MILP solvers and which
default solver is used, see the method :meth:`solve
<sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
:class:`MixedIntegerLinearProgram
<sage.numerical.mip.MixedIntegerLinearProgram>`.
Only useful when ``algorithm == "LP"``.
- ``verbose`` -- integer (default: 0); sets the level of
verbosity. Set to 0 by default, which means quiet.
Only useful when ``algorithm == "LP"``.
- ``integrality_tolerance`` -- float; parameter for use with MILP
solvers over an inexact base ring; see
:meth:`MixedIntegerLinearProgram.get_values`.
Only useful when ``algorithm == "LP"``.
OUTPUT: a graph with labeled edges
EXAMPLES:
Expand Down Expand Up @@ -7842,18 +7775,81 @@ def gomory_hu_tree(self, algorithm=None):
sage: graphs.EmptyGraph().gomory_hu_tree()
Graph on 0 vertices
"""
if not self.order():
return Graph()
if not self.is_connected():
g = Graph()
for cc in self.connected_components_subgraphs():
g = g.union(cc._gomory_hu_tree(frozenset(cc.vertex_iterator()), algorithm=algorithm))
else:
g = self._gomory_hu_tree(frozenset(self.vertex_iterator()), algorithm=algorithm)
self._scream_if_not_simple()

if self.order() <= 1:
return Graph([self, []], format='vertices_and_edges')

from sage.rings.real_mpfr import RR

# Graph to store the Gomory-Hu tree
T = Graph([self, []], format='vertices_and_edges')
if self.get_pos() is not None:
g.set_pos(dict(self.get_pos()))
return g
T.set_pos(dict(self.get_pos()))

# We use a stack to avoid recursion. An element of the stack contains
# the graph to be processed and the corresponding set of "real" vertices
# (as opposed to the fakes one introduced during the computations.
if self.is_connected():
stack = [(self, frozenset(self))]
else:
stack = [(cc, frozenset(cc)) for cc in self.connected_components_subgraphs()]

# We now iteratively decompose the graph to build the tree
while stack:
G, vertices = stack.pop()

if len(vertices) == 1:
continue

# Take any two vertices (u,v)
it = iter(vertices)
u, v = next(it), next(it)

# Compute a uv min-edge-cut.
#
# The graph is split into U,V with u \in U and v\in V.
flow, edges, [U, V] = G.edge_cut(u, v, use_edge_labels=True,
vertices=True, algorithm=algorithm,
solver=solver, verbose=verbose,
integrality_tolerance=integrality_tolerance)

# Add edge (u, v, flow) to the Gomory-Hu tree
T.add_edge(u, v, flow)

# Build one graph for each part of the previous graph and store the
# instances to process
for X, Y in ((U, V), (V, U)):
if len(X) == 1 or len(vertices & frozenset(X)) == 1:
continue

# build the graph of part X
gX = G.subgraph(X, immutable=False)

# A fake vertex fY to represent Y
fY = frozenset(Y)

# For each edge (x, y) in G with x \in X and y\in Y, add edge
# (x, fY) in gX. If the same edge is added several times their
# capacities add up.
for xx, yy, capacity in edges:
capacity = capacity if capacity in RR else 1

# Assume xx is in gX
if xx in fY:
xx, yy = yy, xx

# Create the new edge or update its capacity
if gX.has_edge(xx, fY):
gX.set_edge_label(xx, fY, gX.edge_label(xx, fY) + capacity)
else:
gX.add_edge(xx, fY, capacity)

# Store instance to process
stack.append((gX, vertices & frozenset(gX)))

# Finally return the Gomory-Hu tree
return T

@doc_index("Leftovers")
def two_factor_petersen(self, solver=None, verbose=0, *, integrality_tolerance=1e-3):
Expand Down

0 comments on commit 3075cea

Please sign in to comment.