Skip to content

Commit feba57a

Browse files
author
Release Manager
committed
sagemathgh-37955: `Graph.{[weighted_]adjacency_matrix,kirchhoff_matrix}`: Support constructing `End(CombinatorialFreeModule)` elements <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes sagemath#12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes sagemath#12345". --> This is a follow-up after - sagemath#37692 ... to cover a few more methods. The methods can now create endomorphisms of free modules whose bases are indexed by the vertices. To help with this, we make the `matrix` constructor a bit more flexible. This is also preparation for making the spectral graph theory methods ready for `CombinatorialFreeModule`: - sagemath#37943 ### 📝 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. - [ ] I have updated the documentation and checked the documentation preview. ### ⌛ 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#37955 Reported by: Matthias Köppe Reviewer(s): David Coudert, Matthias Köppe
2 parents d9695cb + 98323cb commit feba57a

File tree

3 files changed

+217
-72
lines changed

3 files changed

+217
-72
lines changed

src/sage/graphs/generic_graph.py

Lines changed: 191 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1900,6 +1900,72 @@ def to_dictionary(self, edge_labels=False, multiple_edges=False):
19001900

19011901
return d
19021902

1903+
def _vertex_indices_and_keys(self, vertices=None, *, sort=None):
1904+
r"""
1905+
Process a ``vertices`` parameter.
1906+
1907+
This is a helper function for :meth:`adjacency_matrix`,
1908+
:meth:`incidence_matrix`, :meth:`weighted_adjacency_matrix`,
1909+
and :meth:`kirchhoff_matrix`.
1910+
1911+
INPUT:
1912+
1913+
- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``)
1914+
1915+
- when a list, the `i`-th row and column of the matrix correspond to
1916+
the `i`-th vertex in the ordering of ``vertices``,
1917+
- when ``None``, the `i`-th row and column of the matrix correspond to
1918+
the `i`-th vertex in the ordering given by
1919+
:meth:`GenericGraph.vertices`
1920+
- when ``True``, construct an endomorphism of a free module instead of
1921+
a matrix, where the module's basis is indexed by the vertices.
1922+
1923+
- ``sort`` -- boolean or ``None`` (default); passed to :meth:`vertices`
1924+
when ``vertices`` is not a list.
1925+
1926+
OUTPUT: pair of:
1927+
1928+
- ``vertex_indices`` -- a dictionary mapping vertices to numerical indices,
1929+
- ``keys`` -- either a tuple of basis keys (when using a
1930+
:class:`CombinatorialFreeModule`) or ``None`` (when using a
1931+
:class:`FreeModule`, :func:`matrix`).
1932+
1933+
EXAMPLES::
1934+
1935+
sage: G = graphs.PathGraph(5)
1936+
sage: G.relabel(['o....', '.o...', '..o..', '...o.', '....o'])
1937+
sage: G._vertex_indices_and_keys(None)
1938+
({'....o': 0, '...o.': 1, '..o..': 2, '.o...': 3, 'o....': 4},
1939+
None)
1940+
sage: G._vertex_indices_and_keys(None, sort=False)
1941+
({'....o': 4, '...o.': 3, '..o..': 2, '.o...': 1, 'o....': 0},
1942+
None)
1943+
sage: G._vertex_indices_and_keys(['..o..', '.o...', '...o.', 'o....', '....o'])
1944+
({'....o': 4, '...o.': 2, '..o..': 0, '.o...': 1, 'o....': 3},
1945+
None)
1946+
sage: G._vertex_indices_and_keys(True)
1947+
({'....o': 4, '...o.': 3, '..o..': 2, '.o...': 1, 'o....': 0},
1948+
('o....', '.o...', '..o..', '...o.', '....o'))
1949+
sage: G._vertex_indices_and_keys(True, sort=True)
1950+
({'....o': 0, '...o.': 1, '..o..': 2, '.o...': 3, 'o....': 4},
1951+
('....o', '...o.', '..o..', '.o...', 'o....'))
1952+
"""
1953+
n = self.order()
1954+
keys = None
1955+
if vertices is True:
1956+
vertices = self.vertices(sort=sort if sort is not None else False)
1957+
keys = tuple(vertices) # tuple to make it hashable
1958+
elif vertices is None:
1959+
try:
1960+
vertices = self.vertices(sort=sort if sort is not None else True)
1961+
except TypeError:
1962+
raise TypeError("Vertex labels are not comparable. You must "
1963+
"specify an ordering using parameter 'vertices'")
1964+
elif (len(vertices) != n or
1965+
set(vertices) != set(self.vertex_iterator())):
1966+
raise ValueError("parameter 'vertices' must be a permutation of the vertices")
1967+
return {v: i for i, v in enumerate(vertices)}, keys
1968+
19031969
def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds):
19041970
r"""
19051971
Return the adjacency matrix of the (di)graph.
@@ -1911,10 +1977,16 @@ def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds
19111977
- ``sparse`` -- boolean (default: ``None``); whether to represent with a
19121978
sparse matrix
19131979

1914-
- ``vertices`` -- list (default: ``None``); the ordering of
1915-
the vertices defining how they should appear in the
1916-
matrix. By default, the ordering given by
1917-
:meth:`GenericGraph.vertices` with ``sort=True`` is used.
1980+
- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``);
1981+
1982+
- when a list, the `i`-th row and column of the matrix correspond to
1983+
the `i`-th vertex in the ordering of ``vertices``,
1984+
- when ``None``, the `i`-th row and column of the matrix correspond to
1985+
the `i`-th vertex in the ordering given by
1986+
:meth:`GenericGraph.vertices` with ``sort=True``.
1987+
- when ``True``, construct an endomorphism of a free module instead of
1988+
a matrix, where the module's basis is indexed by the vertices.
1989+
19181990
If the vertices are not comparable, the keyword ``vertices`` must be
19191991
used to specify an ordering, or a :class:`TypeError` exception will
19201992
be raised.
@@ -2025,27 +2097,45 @@ def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds
20252097
ValueError: matrix is immutable; please change a copy instead
20262098
(i.e., use copy(M) to change a copy of M).
20272099

2100+
Creating a module endomorphism::
2101+
2102+
sage: # needs sage.modules
2103+
sage: D12 = posets.DivisorLattice(12).hasse_diagram()
2104+
sage: phi = D12.adjacency_matrix(vertices=True); phi
2105+
Generic endomorphism of
2106+
Free module generated by {1, 2, 3, 4, 6, 12} over Integer Ring
2107+
sage: print(phi._unicode_art_matrix())
2108+
1 2 3 4 6 12
2109+
1⎛ 0 1 1 0 0 0⎞
2110+
2⎜ 0 0 0 1 1 0⎟
2111+
3⎜ 0 0 0 0 1 0⎟
2112+
4⎜ 0 0 0 0 0 1⎟
2113+
6⎜ 0 0 0 0 0 1⎟
2114+
12⎝ 0 0 0 0 0 0⎠
2115+
20282116
TESTS::
20292117

2030-
sage: graphs.CubeGraph(8).adjacency_matrix().parent() # needs sage.modules
2118+
sage: # needs sage.modules
2119+
sage: graphs.CubeGraph(8).adjacency_matrix().parent()
20312120
Full MatrixSpace of 256 by 256 dense matrices over Integer Ring
2032-
sage: graphs.CubeGraph(9).adjacency_matrix().parent() # needs sage.modules
2121+
sage: graphs.CubeGraph(9).adjacency_matrix().parent()
20332122
Full MatrixSpace of 512 by 512 sparse matrices over Integer Ring
2034-
sage: Graph([(i, i+1) for i in range(500)] + [(0,1),], # needs sage.modules
2123+
sage: Graph([(i, i+1) for i in range(500)] + [(0,1),],
20352124
....: multiedges=True).adjacency_matrix().parent()
20362125
Full MatrixSpace of 501 by 501 dense matrices over Integer Ring
2037-
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[0,0,0,0,0]) # needs sage.modules
2126+
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[0,0,0,0,0])
20382127
Traceback (most recent call last):
20392128
...
2040-
ValueError: parameter vertices must be a permutation of the vertices
2041-
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[1,2,3]) # needs sage.modules
2129+
ValueError: parameter 'vertices' must be a permutation of the vertices
2130+
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[1,2,3])
20422131
Traceback (most recent call last):
20432132
...
2044-
ValueError: parameter vertices must be a permutation of the vertices
2133+
ValueError: parameter 'vertices' must be a permutation of the vertices
2134+
20452135
sage: Graph ([[0, 42, 'John'], [(42, 'John')]]).adjacency_matrix()
20462136
Traceback (most recent call last):
20472137
...
2048-
TypeError: Vertex labels are not comparable. You must specify an ordering using parameter ``vertices``
2138+
TypeError: Vertex labels are not comparable. You must specify an ordering using parameter 'vertices'
20492139
sage: Graph ([[0, 42, 'John'], [(42, 'John')]]).adjacency_matrix(vertices=['John', 42, 0])
20502140
[0 1 0]
20512141
[1 0 0]
@@ -2056,25 +2146,17 @@ def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds
20562146
sparse = True
20572147
if self.has_multiple_edges() or n <= 256 or self.density() > 0.05:
20582148
sparse = False
2149+
vertex_indices, keys = self._vertex_indices_and_keys(vertices)
2150+
if keys is not None:
2151+
kwds = copy(kwds)
2152+
kwds['row_keys'] = kwds['column_keys'] = keys
20592153

2060-
if vertices is None:
2061-
try:
2062-
vertices = self.vertices(sort=True)
2063-
except TypeError:
2064-
raise TypeError("Vertex labels are not comparable. You must "
2065-
"specify an ordering using parameter "
2066-
"``vertices``")
2067-
elif (len(vertices) != n or
2068-
set(vertices) != set(self.vertex_iterator())):
2069-
raise ValueError("parameter vertices must be a permutation of the vertices")
2070-
2071-
new_indices = {v: i for i, v in enumerate(vertices)}
20722154
D = {}
20732155
directed = self._directed
20742156
multiple_edges = self.allows_multiple_edges()
20752157
for u, v, l in self.edge_iterator():
2076-
i = new_indices[u]
2077-
j = new_indices[v]
2158+
i = vertex_indices[u]
2159+
j = vertex_indices[v]
20782160
if multiple_edges and (i, j) in D:
20792161
D[i, j] += 1
20802162
if not directed and i != j:
@@ -2298,7 +2380,7 @@ def incidence_matrix(self, oriented=None, sparse=True, vertices=None, edges=None
22982380
sage: P5.incidence_matrix(vertices=[1] * P5.order()) # needs sage.modules
22992381
Traceback (most recent call last):
23002382
...
2301-
ValueError: parameter vertices must be a permutation of the vertices
2383+
ValueError: parameter 'vertices' must be a permutation of the vertices
23022384
sage: P5.incidence_matrix(edges=[(0, 1)] * P5.size()) # needs sage.modules
23032385
Traceback (most recent call last):
23042386
...
@@ -2313,18 +2395,9 @@ def incidence_matrix(self, oriented=None, sparse=True, vertices=None, edges=None
23132395
if oriented is None:
23142396
oriented = self.is_directed()
23152397

2316-
row_keys = None
2317-
if vertices is True:
2318-
vertices = self.vertices(sort=False)
2319-
row_keys = tuple(vertices) # because a list is not hashable
2320-
elif vertices is None:
2321-
vertices = self.vertices(sort=False)
2322-
elif (len(vertices) != self.num_verts() or
2323-
set(vertices) != set(self.vertex_iterator())):
2324-
raise ValueError("parameter vertices must be a permutation of the vertices")
2398+
vertex_indices, row_keys = self._vertex_indices_and_keys(vertices, sort=False)
23252399

23262400
column_keys = None
2327-
verts = {v: i for i, v in enumerate(vertices)}
23282401
use_edge_labels = kwds.pop('use_edge_labels', False)
23292402
if edges is True:
23302403
edges = self.edges(labels=use_edge_labels)
@@ -2336,13 +2409,13 @@ def incidence_matrix(self, oriented=None, sparse=True, vertices=None, edges=None
23362409
else:
23372410
# We check that we have the same set of unlabeled edges
23382411
if oriented:
2339-
i_edges = [(verts[e[0]], verts[e[1]]) for e in edges]
2340-
s_edges = [(verts[u], verts[v]) for u, v in self.edge_iterator(labels=False)]
2412+
i_edges = [(vertex_indices[e[0]], vertex_indices[e[1]]) for e in edges]
2413+
s_edges = [(vertex_indices[u], vertex_indices[v]) for u, v in self.edge_iterator(labels=False)]
23412414
else:
23422415
def reorder(u, v):
23432416
return (u, v) if u <= v else (v, u)
2344-
i_edges = [reorder(verts[e[0]], verts[e[1]]) for e in edges]
2345-
s_edges = [reorder(verts[u], verts[v]) for u, v in self.edge_iterator(labels=False)]
2417+
i_edges = [reorder(vertex_indices[e[0]], vertex_indices[e[1]]) for e in edges]
2418+
s_edges = [reorder(vertex_indices[u], vertex_indices[v]) for u, v in self.edge_iterator(labels=False)]
23462419
if sorted(i_edges) != sorted(s_edges):
23472420
raise ValueError("parameter edges must be a permutation of the edges")
23482421

@@ -2355,12 +2428,12 @@ def reorder(u, v):
23552428
if oriented:
23562429
for i, e in enumerate(edges):
23572430
if e[0] != e[1]:
2358-
m[verts[e[0]], i] = -1
2359-
m[verts[e[1]], i] = +1
2431+
m[vertex_indices[e[0]], i] = -1
2432+
m[vertex_indices[e[1]], i] = +1
23602433
else:
23612434
for i, e in enumerate(edges):
2362-
m[verts[e[0]], i] += 1
2363-
m[verts[e[1]], i] += 1
2435+
m[vertex_indices[e[0]], i] += 1
2436+
m[vertex_indices[e[1]], i] += 1
23642437

23652438
if row_keys is not None or column_keys is not None:
23662439
m.set_immutable()
@@ -2524,10 +2597,19 @@ def weighted_adjacency_matrix(self, sparse=True, vertices=None,
25242597
- ``sparse`` -- boolean (default: ``True``); whether to use a sparse or
25252598
a dense matrix
25262599

2527-
- ``vertices`` -- list (default: ``None``); when specified, each vertex
2528-
is represented by its position in the list ``vertices``, otherwise
2529-
each vertex is represented by its position in the list returned by
2530-
method :meth:`vertices`
2600+
- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``);
2601+
2602+
- when a list, the `i`-th row and column of the matrix correspond to
2603+
the `i`-th vertex in the ordering of ``vertices``,
2604+
- when ``None``, the `i`-th row and column of the matrix correspond to
2605+
the `i`-th vertex in the ordering given by
2606+
:meth:`GenericGraph.vertices` with ``sort=True``.
2607+
- when ``True``, construct an endomorphism of a free module instead of
2608+
a matrix, where the module's basis is indexed by the vertices.
2609+
2610+
If the vertices are not comparable, the keyword ``vertices`` must be
2611+
used to specify an ordering, or a :class:`TypeError` exception will
2612+
be raised.
25312613

25322614
- ``default_weight`` -- (default: ``None``); specifies the weight to
25332615
replace any ``None`` edge label. When not specified an error is raised
@@ -2579,6 +2661,21 @@ def weighted_adjacency_matrix(self, sparse=True, vertices=None,
25792661
ValueError: matrix is immutable; please change a copy instead
25802662
(i.e., use copy(M) to change a copy of M).
25812663

2664+
Creating a module morphism::
2665+
2666+
sage: # needs sage.modules
2667+
sage: G = Graph(sparse=True, weighted=True)
2668+
sage: G.add_edges([('A', 'B', 1), ('B', 'C', 2), ('A', 'C', 3), ('A', 'D', 4)])
2669+
sage: phi = G.weighted_adjacency_matrix(vertices=True); phi
2670+
Generic endomorphism of
2671+
Free module generated by {'A', 'B', 'C', 'D'} over Integer Ring
2672+
sage: print(phi._unicode_art_matrix())
2673+
A B C D
2674+
A⎛0 1 3 4⎞
2675+
B⎜1 0 2 0⎟
2676+
C⎜3 2 0 0⎟
2677+
D⎝4 0 0 0⎠
2678+
25822679
TESTS:
25832680

25842681
The following doctest verifies that :issue:`4888` is fixed::
@@ -2609,11 +2706,10 @@ def weighted_adjacency_matrix(self, sparse=True, vertices=None,
26092706
if self.has_multiple_edges():
26102707
raise NotImplementedError("don't know how to represent weights for a multigraph")
26112708

2612-
if vertices is None:
2613-
vertices = self.vertices(sort=True)
2614-
elif (len(vertices) != self.num_verts() or
2615-
set(vertices) != set(self.vertex_iterator())):
2616-
raise ValueError("parameter vertices must be a permutation of the vertices")
2709+
vertex_indices, row_column_keys = self._vertex_indices_and_keys(vertices)
2710+
if row_column_keys is not None:
2711+
kwds = copy(kwds)
2712+
kwds['row_keys'] = kwds['column_keys'] = row_column_keys
26172713

26182714
# Method for checking edge weights and setting default weight
26192715
if default_weight is None:
@@ -2628,18 +2724,16 @@ def func(u, v, label):
26282724
return default_weight
26292725
return label
26302726

2631-
new_indices = {v: i for i,v in enumerate(vertices)}
2632-
26332727
D = {}
26342728
if self._directed:
26352729
for u, v, label in self.edge_iterator():
2636-
i = new_indices[u]
2637-
j = new_indices[v]
2730+
i = vertex_indices[u]
2731+
j = vertex_indices[v]
26382732
D[i, j] = func(u, v, label)
26392733
else:
26402734
for u, v, label in self.edge_iterator():
2641-
i = new_indices[u]
2642-
j = new_indices[v]
2735+
i = vertex_indices[u]
2736+
j = vertex_indices[v]
26432737
label = func(u, v, label)
26442738
D[i, j] = label
26452739
D[j, i] = label
@@ -2707,6 +2801,20 @@ def kirchhoff_matrix(self, weighted=None, indegree=True, normalized=False, signl
27072801

27082802
- Else, `D-M` is used in calculation of Kirchhoff matrix
27092803

2804+
- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``);
2805+
2806+
- when a list, the `i`-th row and column of the matrix correspond to
2807+
the `i`-th vertex in the ordering of ``vertices``,
2808+
- when ``None``, the `i`-th row and column of the matrix correspond to
2809+
the `i`-th vertex in the ordering given by
2810+
:meth:`GenericGraph.vertices` with ``sort=True``.
2811+
- when ``True``, construct an endomorphism of a free module instead of
2812+
a matrix, where the module's basis is indexed by the vertices.
2813+
2814+
If the vertices are not comparable, the keyword ``vertices`` must be
2815+
used to specify an ordering, or a :class:`TypeError` exception will
2816+
be raised.
2817+
27102818
Note that any additional keywords will be passed on to either the
27112819
:meth:`~GenericGraph.adjacency_matrix` or
27122820
:meth:`~GenericGraph.weighted_adjacency_matrix` method.
@@ -2788,18 +2896,36 @@ def kirchhoff_matrix(self, weighted=None, indegree=True, normalized=False, signl
27882896
sage: M = G.kirchhoff_matrix(vertices=[0, 1], immutable=True) # needs sage.modules
27892897
sage: M.is_immutable() # needs sage.modules
27902898
True
2899+
2900+
Creating a module morphism::
2901+
2902+
sage: # needs sage.modules
2903+
sage: G = Graph(sparse=True, weighted=True)
2904+
sage: G.add_edges([('A', 'B', 1), ('B', 'C', 2), ('A', 'C', 3), ('A', 'D', 4)])
2905+
sage: phi = G.laplacian_matrix(weighted=True, vertices=True); phi
2906+
Generic endomorphism of
2907+
Free module generated by {'A', 'B', 'C', 'D'} over Integer Ring
2908+
sage: print(phi._unicode_art_matrix())
2909+
A B C D
2910+
A⎛ 8 -1 -3 -4⎞
2911+
B⎜-1 3 -2 0⎟
2912+
C⎜-3 -2 5 0⎟
2913+
D⎝-4 0 0 4⎠
2914+
27912915
"""
2792-
from sage.matrix.constructor import diagonal_matrix
2916+
from sage.matrix.constructor import diagonal_matrix, matrix
27932917

27942918
set_immutable = kwds.pop('immutable', False)
27952919

2920+
vertex_indices, keys = self._vertex_indices_and_keys(kwds.pop('vertices', None))
2921+
27962922
if weighted is None:
27972923
weighted = self._weighted
27982924

27992925
if weighted:
2800-
M = self.weighted_adjacency_matrix(immutable=True, **kwds)
2926+
M = self.weighted_adjacency_matrix(vertices=list(vertex_indices), immutable=True, **kwds)
28012927
else:
2802-
M = self.adjacency_matrix(immutable=True, **kwds)
2928+
M = self.adjacency_matrix(vertices=list(vertex_indices), immutable=True, **kwds)
28032929

28042930
D = M.parent(0)
28052931

@@ -2839,6 +2965,8 @@ def kirchhoff_matrix(self, weighted=None, indegree=True, normalized=False, signl
28392965
else:
28402966
ret = D - M
28412967

2968+
if keys is not None:
2969+
return matrix(ret, row_keys=keys, column_keys=keys)
28422970
if set_immutable:
28432971
ret.set_immutable()
28442972
return ret

0 commit comments

Comments
 (0)