Skip to content

Commit

Permalink
gh-39297: more care about parameter immutable in `sage/graphs/gener…
Browse files Browse the repository at this point in the history
…ic_graph.py`

    
Following #39280, #39285, #39287, #39296 and discussions in #39177, we
add parameter immutable to methods  in `sage/graphs/generic_graph.py`:
- `longest_cycle`
- `longest_path`
- `hamiltonian_path`
- and add tests in `cycle_basis`

### 📝 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.
- [x] I have created tests covering the changes.
- [x] I have updated the documentation and checked the documentation
preview.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - #12345: short description why this is a dependency -->
<!-- - #34567: ... -->
    
URL: #39297
Reported by: David Coudert
Reviewer(s): Kwankyu Lee
  • Loading branch information
Release Manager committed Feb 10, 2025
2 parents 924392a + 6dc4315 commit ef384b4
Showing 1 changed file with 117 additions and 24 deletions.
141 changes: 117 additions & 24 deletions src/sage/graphs/generic_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -5488,6 +5488,17 @@ def cycle_basis(self, output='vertex'):
[[(2, 3, 'c'), (3, 2, 'b')],
[(3, 4, 'd'), (4, 1, 'f'), (1, 2, 'a'), (2, 3, 'b')],
[(3, 4, 'e'), (4, 1, 'f'), (1, 2, 'a'), (2, 3, 'b')]]

Check that the method is valid for immutable graphs::

sage: G = Graph(graphs.CycleGraph(3), immutable=True)
sage: G.cycle_basis()
[[0, 1, 2]]
sage: G = Graph([(1, 2, 'a'), (2, 3, 'b'), (2, 3, 'c'),
....: (3, 4, 'd'), (3, 4, 'e'), (4, 1, 'f')],
....: multiedges=True, immutable=True)
sage: G.cycle_basis()
[[3, 2], [4, 1, 2, 3], [4, 1, 2, 3]]
"""
if output not in ['vertex', 'edge']:
raise ValueError('output must be either vertex or edge')
Expand All @@ -5503,7 +5514,7 @@ def cycle_basis(self, output='vertex'):

from sage.graphs.graph import Graph
T = Graph(self.min_spanning_tree(), multiedges=True, format='list_of_edges')
H = self.copy()
H = self.copy(immutable=False)
H.delete_edges(T.edge_iterator())
root = next(T.vertex_iterator())
rank = dict(T.breadth_first_search(root, report_distance=True))
Expand Down Expand Up @@ -7044,7 +7055,7 @@ def planar_dual(self, embedding=None):
if e:
e = e.pop() # just one edge since self and its dual are simple
edges.append([v1, v2, self.edge_label(e[0], e[1])])
return Graph([verts, edges])
return Graph([verts, edges], format='vertices_and_edges')

# Connectivity

Expand Down Expand Up @@ -8176,6 +8187,7 @@ def good_edge(e):
return val

def longest_cycle(self, induced=False, use_edge_labels=False,
immutable=None,
solver=None, verbose=0, *, integrality_tolerance=0.001):
r"""
Return the longest (induced) cycle of ``self``.
Expand Down Expand Up @@ -8207,6 +8219,10 @@ def longest_cycle(self, induced=False, use_edge_labels=False,
considered as a weight of `1`), or to compute a cycle with the largest
possible number of edges (i.e., edge weights are set to 1)

- ``immutable`` -- boolean (default: ``None``); whether to create a
mutable/immutable (di)graph. ``immutable=None`` (default) means that
the (di)graph and its longest cycle will behave the same way.

- ``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
Expand Down Expand Up @@ -8317,6 +8333,32 @@ def longest_cycle(self, induced=False, use_edge_labels=False,
longest cycle from Subgraph of (Circuit disjoint_union Circuit): Digraph on 5 vertices
sage: D.longest_cycle(induced=True)
longest induced cycle from Subgraph of (Circuit disjoint_union Circuit): Digraph on 5 vertices

Check the behavior of parameter ``immutable``::

sage: G = Graph([(0, 1), (1, 2), (0, 2)], immutable=True)
sage: G.longest_cycle().is_immutable()
True
sage: G = graphs.Grid2dGraph(3, 4)
sage: G.longest_cycle(induced=False, immutable=True).is_immutable()
True
sage: G.longest_cycle(induced=True, immutable=True).is_immutable()
True
sage: D = digraphs.Circuit(15)
sage: for u, v in D.edges(labels=False):
....: D.set_edge_label(u, v, 1)
sage: D.add_edge(0, 10, 50)
sage: D.add_edge(11, 1, 1)
sage: D.add_edge(13, 0, 1)
sage: D.longest_cycle(induced=False, use_edge_labels=False, immutable=True).is_immutable()
True
sage: D.longest_cycle(induced=False, use_edge_labels=True, immutable=True)[1].is_immutable()
True
sage: D = DiGraph(D, immutable=True)
sage: D.longest_cycle(induced=False, use_edge_labels=True)[1].is_immutable()
True
sage: D.longest_cycle(induced=False, use_edge_labels=True, immutable=False)[1].is_immutable()
False
"""
self._scream_if_not_simple()
G = self
Expand All @@ -8338,7 +8380,8 @@ def total_weight(gg):
return gg.order()

directed = G.is_directed()
immutable = G.is_immutable()
if immutable is None:
immutable = G.is_immutable()
if directed:
from sage.graphs.digraph import DiGraph as MyGraph
blocks = G.strongly_connected_components()
Expand All @@ -8356,6 +8399,7 @@ def total_weight(gg):
h = G.subgraph(vertices=block)
C = h.longest_cycle(induced=induced,
use_edge_labels=use_edge_labels,
immutable=immutable,
solver=solver, verbose=verbose,
integrality_tolerance=integrality_tolerance)
if total_weight(C) > best_w:
Expand All @@ -8374,8 +8418,7 @@ def total_weight(gg):
return MyGraph(name=name, immutable=immutable)
if (not induced and ((directed and G.order() == 2) or
(not directed and G.order() == 3))):
answer = G.copy()
answer.name(name)
answer = MyGraph(G, immutable=immutable, name=name)
if use_edge_labels:
return total_weight(answer), answer
return answer
Expand Down Expand Up @@ -8494,7 +8537,8 @@ def F(e):
best.set_pos({u: pp for u, pp in G.get_pos().items() if u in best})
return (best_w, best) if use_edge_labels else best

def longest_path(self, s=None, t=None, use_edge_labels=False, algorithm='MILP',
def longest_path(self, s=None, t=None, use_edge_labels=False,
algorithm='MILP', immutable=None,
solver=None, verbose=0, *, integrality_tolerance=1e-3):
r"""
Return a longest path of ``self``.
Expand Down Expand Up @@ -8535,6 +8579,10 @@ def longest_path(self, s=None, t=None, use_edge_labels=False, algorithm='MILP',
parameters ``s``, ``t`` and ``use_edge_labels``. An error is raised
if these parameters are set.

- ``immutable`` -- boolean (default: ``None``); whether to create a
mutable/immutable (di)graph. ``immutable=None`` (default) means that
the (di)graph and its longest path will behave the same way.

- ``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
Expand Down Expand Up @@ -8702,6 +8750,24 @@ def longest_path(self, s=None, t=None, use_edge_labels=False, algorithm='MILP',
...
ValueError: parameters s, t, and use_edge_labels can not be used in
combination with algorithm 'heuristic'

Check the behavior of parameter ``immutable``::

sage: # needs sage.numerical.mip
sage: g1 = digraphs.RandomDirectedGNP(15, 0.2)
sage: for u,v in g.edge_iterator(labels=False):
....: g.set_edge_label(u, v, random())
sage: g2 = DiGraph(2 * g1, immutable=True)
sage: lp1 = g1.longest_path(use_edge_labels=True)
sage: lp2 = g2.longest_path(use_edge_labels=True)
sage: lp1[0] == lp2[0]
True
sage: not lp1[1].is_immutable() and lp2[1].is_immutable()
True
sage: lp1 = g1.longest_path(use_edge_labels=True, immutable=True)
sage: lp2 = g2.longest_path(use_edge_labels=True, immutable=False)
sage: lp1[1].is_immutable() and not lp2[1].is_immutable()
True
"""
self._scream_if_not_simple()

Expand All @@ -8717,16 +8783,19 @@ def longest_path(self, s=None, t=None, use_edge_labels=False, algorithm='MILP',
raise ValueError("parameters s, t, and use_edge_labels can not "
"be used in combination with algorithm 'heuristic'")

if immutable is None:
immutable = self.is_immutable()

# Quick improvement
if not self.is_connected():
if use_edge_labels:
return max((g.longest_path(s=s, t=t,
return max((g.longest_path(s=s, t=t, immutable=immutable,
use_edge_labels=use_edge_labels,
algorithm=algorithm)
for g in self.connected_components_subgraphs()),
key=lambda x: x[0])

return max((g.longest_path(s=s, t=t,
return max((g.longest_path(s=s, t=t, immutable=immutable,
use_edge_labels=use_edge_labels,
algorithm=algorithm)
for g in self.connected_components_subgraphs()),
Expand Down Expand Up @@ -8755,16 +8824,18 @@ def longest_path(self, s=None, t=None, use_edge_labels=False, algorithm='MILP',
(self._directed and (s is not None) and (t is not None) and
not self.shortest_path(s, t))):
if self._directed:
from sage.graphs.digraph import DiGraph
return [0, DiGraph()] if use_edge_labels else DiGraph()
from sage.graphs.graph import Graph
return [0, Graph()] if use_edge_labels else Graph()
from sage.graphs.digraph import DiGraph as MyGraph
else:
from sage.graphs.graph import Graph as MyGraph
GG = MyGraph(immutable=immutable)
return [0, GG] if use_edge_labels else GG

# Calling the heuristic if asked
if algorithm == "heuristic":
from sage.graphs.generic_graph_pyx import find_hamiltonian as fh
x = fh(self, find_path=True)[1]
return self.subgraph(vertices=x, edges=list(zip(x[:-1], x[1:])))
return self.subgraph(vertices=x, edges=list(zip(x[:-1], x[1:])),
immutable=immutable)

##################
# LP Formulation #
Expand Down Expand Up @@ -8894,21 +8965,20 @@ def weight(x):
edge_used = p.get_values(edge_used, convert=bool, tolerance=integrality_tolerance)
vertex_used = p.get_values(vertex_used, convert=bool, tolerance=integrality_tolerance)
if self._directed:
g = self.subgraph(vertices=(v for v in self if vertex_used[v]),
edges=((u, v, l) for u, v, l in self.edge_iterator()
if edge_used[u, v]))
edges = ((u, v, l) for u, v, l in self.edge_iterator()
if edge_used[u, v])
else:
g = self.subgraph(
vertices=(v for v in self if vertex_used[v]),
edges=((u, v, l) for u, v, l in self.edge_iterator()
if edge_used[frozenset((u, v))]))
edges = ((u, v, l) for u, v, l in self.edge_iterator()
if edge_used[frozenset((u, v))])
g = self.subgraph(vertices=(v for v in self if vertex_used[v]),
edges=edges, immutable=immutable)
if use_edge_labels:
return sum(map(weight, g.edge_labels())), g
return g

def hamiltonian_path(self, s=None, t=None, use_edge_labels=False,
maximize=False, algorithm='MILP', solver=None, verbose=0,
*, integrality_tolerance=1e-3):
maximize=False, algorithm='MILP', immutable=None,
solver=None, verbose=0, *, integrality_tolerance=1e-3):
r"""
Return a Hamiltonian path of the current graph/digraph.

Expand Down Expand Up @@ -8954,6 +9024,10 @@ def hamiltonian_path(self, s=None, t=None, use_edge_labels=False,
* The backtrack algorithm does not support edge weighting, so setting
``use_edge_labels=True`` will force the use of the MILP algorithm.

- ``immutable`` -- boolean (default: ``None``); whether to create a
mutable/immutable (di)graph. ``immutable=None`` (default) means that
the (di)graph and its hamiltonian path will behave the same way.

- ``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
Expand Down Expand Up @@ -9039,6 +9113,20 @@ def hamiltonian_path(self, s=None, t=None, use_edge_labels=False,
Traceback (most recent call last):
...
ValueError: algorithm must be either 'backtrack' or 'MILP'

Check the behavior of parameter ``immutable``::

sage: # needs sage.numerical.mip
sage: g = graphs.Grid2dGraph(3, 3)
sage: g.hamiltonian_path().is_immutable()
False
sage: g.hamiltonian_path(immutable=True).is_immutable()
True
sage: g = Graph(g, immutable=True)
sage: g.hamiltonian_path().is_immutable()
True
sage: g.hamiltonian_path(use_edge_labels=True)[1].is_immutable()
True
"""
if use_edge_labels or algorithm is None:
# We force the algorithm to 'MILP'
Expand All @@ -9053,6 +9141,8 @@ def hamiltonian_path(self, s=None, t=None, use_edge_labels=False,
if not self.is_connected():
return (0, None) if use_edge_labels else None

if immutable is None:
immutable = self.is_immutable()
#
# Deal with loops and multiple edges
#
Expand Down Expand Up @@ -9116,7 +9206,8 @@ def hamiltonian_path(self, s=None, t=None, use_edge_labels=False,
new_t = ones.pop()

if not use_edge_labels and algorithm == "backtrack":
path = g.longest_path(s=new_s, t=new_t, algorithm='backtrack')
path = g.longest_path(s=new_s, t=new_t, algorithm='backtrack',
immutable=immutable)
return path if path.order() == g.order() else None

#
Expand Down Expand Up @@ -9163,6 +9254,8 @@ def hamiltonian_path(self, s=None, t=None, use_edge_labels=False,

tsp.delete_vertices(extra_vertices)
tsp.name("Hamiltonian path from {}".format(self.name()))
if immutable:
tsp = tsp.copy(immutable=True)

def weight(label):
return 1 if label is None else label
Expand Down Expand Up @@ -10799,7 +10892,7 @@ def multicommodity_flow(self, terminals, integer=True, use_edge_labels=False,
solvers over an inexact base ring; see
:meth:`MixedIntegerLinearProgram.get_values`.

Only useful when parameter ``ìnteger`` is ``True``.
Only useful when parameter ``integer`` is ``True``.

ALGORITHM:

Expand Down

0 comments on commit ef384b4

Please sign in to comment.