Skip to content

Commit

Permalink
sagemathgh-37955: `Graph.{[weighted_]adjacency_matrix,kirchhoff_matri…
Browse files Browse the repository at this point in the history
…x}`: 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
  • Loading branch information
Release Manager committed May 12, 2024
2 parents d9695cb + 98323cb commit feba57a
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 72 deletions.
254 changes: 191 additions & 63 deletions src/sage/graphs/generic_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,72 @@ def to_dictionary(self, edge_labels=False, multiple_edges=False):

return d

def _vertex_indices_and_keys(self, vertices=None, *, sort=None):
r"""
Process a ``vertices`` parameter.

This is a helper function for :meth:`adjacency_matrix`,
:meth:`incidence_matrix`, :meth:`weighted_adjacency_matrix`,
and :meth:`kirchhoff_matrix`.

INPUT:

- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``)

- when a list, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering of ``vertices``,
- when ``None``, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering given by
:meth:`GenericGraph.vertices`
- when ``True``, construct an endomorphism of a free module instead of
a matrix, where the module's basis is indexed by the vertices.

- ``sort`` -- boolean or ``None`` (default); passed to :meth:`vertices`
when ``vertices`` is not a list.

OUTPUT: pair of:

- ``vertex_indices`` -- a dictionary mapping vertices to numerical indices,
- ``keys`` -- either a tuple of basis keys (when using a
:class:`CombinatorialFreeModule`) or ``None`` (when using a
:class:`FreeModule`, :func:`matrix`).

EXAMPLES::

sage: G = graphs.PathGraph(5)
sage: G.relabel(['o....', '.o...', '..o..', '...o.', '....o'])
sage: G._vertex_indices_and_keys(None)
({'....o': 0, '...o.': 1, '..o..': 2, '.o...': 3, 'o....': 4},
None)
sage: G._vertex_indices_and_keys(None, sort=False)
({'....o': 4, '...o.': 3, '..o..': 2, '.o...': 1, 'o....': 0},
None)
sage: G._vertex_indices_and_keys(['..o..', '.o...', '...o.', 'o....', '....o'])
({'....o': 4, '...o.': 2, '..o..': 0, '.o...': 1, 'o....': 3},
None)
sage: G._vertex_indices_and_keys(True)
({'....o': 4, '...o.': 3, '..o..': 2, '.o...': 1, 'o....': 0},
('o....', '.o...', '..o..', '...o.', '....o'))
sage: G._vertex_indices_and_keys(True, sort=True)
({'....o': 0, '...o.': 1, '..o..': 2, '.o...': 3, 'o....': 4},
('....o', '...o.', '..o..', '.o...', 'o....'))
"""
n = self.order()
keys = None
if vertices is True:
vertices = self.vertices(sort=sort if sort is not None else False)
keys = tuple(vertices) # tuple to make it hashable
elif vertices is None:
try:
vertices = self.vertices(sort=sort if sort is not None else True)
except TypeError:
raise TypeError("Vertex labels are not comparable. You must "
"specify an ordering using parameter 'vertices'")
elif (len(vertices) != n or
set(vertices) != set(self.vertex_iterator())):
raise ValueError("parameter 'vertices' must be a permutation of the vertices")
return {v: i for i, v in enumerate(vertices)}, keys

def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds):
r"""
Return the adjacency matrix of the (di)graph.
Expand All @@ -1911,10 +1977,16 @@ def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds
- ``sparse`` -- boolean (default: ``None``); whether to represent with a
sparse matrix

- ``vertices`` -- list (default: ``None``); the ordering of
the vertices defining how they should appear in the
matrix. By default, the ordering given by
:meth:`GenericGraph.vertices` with ``sort=True`` is used.
- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``);

- when a list, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering of ``vertices``,
- when ``None``, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering given by
:meth:`GenericGraph.vertices` with ``sort=True``.
- when ``True``, construct an endomorphism of a free module instead of
a matrix, where the module's basis is indexed by the vertices.

If the vertices are not comparable, the keyword ``vertices`` must be
used to specify an ordering, or a :class:`TypeError` exception will
be raised.
Expand Down Expand Up @@ -2025,27 +2097,45 @@ def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds
ValueError: matrix is immutable; please change a copy instead
(i.e., use copy(M) to change a copy of M).

Creating a module endomorphism::

sage: # needs sage.modules
sage: D12 = posets.DivisorLattice(12).hasse_diagram()
sage: phi = D12.adjacency_matrix(vertices=True); phi
Generic endomorphism of
Free module generated by {1, 2, 3, 4, 6, 12} over Integer Ring
sage: print(phi._unicode_art_matrix())
1 2 3 4 6 12
1⎛ 0 1 1 0 0 0⎞
2⎜ 0 0 0 1 1 0⎟
3⎜ 0 0 0 0 1 0⎟
4⎜ 0 0 0 0 0 1⎟
6⎜ 0 0 0 0 0 1⎟
12⎝ 0 0 0 0 0 0⎠

TESTS::

sage: graphs.CubeGraph(8).adjacency_matrix().parent() # needs sage.modules
sage: # needs sage.modules
sage: graphs.CubeGraph(8).adjacency_matrix().parent()
Full MatrixSpace of 256 by 256 dense matrices over Integer Ring
sage: graphs.CubeGraph(9).adjacency_matrix().parent() # needs sage.modules
sage: graphs.CubeGraph(9).adjacency_matrix().parent()
Full MatrixSpace of 512 by 512 sparse matrices over Integer Ring
sage: Graph([(i, i+1) for i in range(500)] + [(0,1),], # needs sage.modules
sage: Graph([(i, i+1) for i in range(500)] + [(0,1),],
....: multiedges=True).adjacency_matrix().parent()
Full MatrixSpace of 501 by 501 dense matrices over Integer Ring
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[0,0,0,0,0]) # needs sage.modules
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[0,0,0,0,0])
Traceback (most recent call last):
...
ValueError: parameter vertices must be a permutation of the vertices
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[1,2,3]) # needs sage.modules
ValueError: parameter 'vertices' must be a permutation of the vertices
sage: graphs.PathGraph(5).adjacency_matrix(vertices=[1,2,3])
Traceback (most recent call last):
...
ValueError: parameter vertices must be a permutation of the vertices
ValueError: parameter 'vertices' must be a permutation of the vertices

sage: Graph ([[0, 42, 'John'], [(42, 'John')]]).adjacency_matrix()
Traceback (most recent call last):
...
TypeError: Vertex labels are not comparable. You must specify an ordering using parameter ``vertices``
TypeError: Vertex labels are not comparable. You must specify an ordering using parameter 'vertices'
sage: Graph ([[0, 42, 'John'], [(42, 'John')]]).adjacency_matrix(vertices=['John', 42, 0])
[0 1 0]
[1 0 0]
Expand All @@ -2056,25 +2146,17 @@ def adjacency_matrix(self, sparse=None, vertices=None, *, base_ring=None, **kwds
sparse = True
if self.has_multiple_edges() or n <= 256 or self.density() > 0.05:
sparse = False
vertex_indices, keys = self._vertex_indices_and_keys(vertices)
if keys is not None:
kwds = copy(kwds)
kwds['row_keys'] = kwds['column_keys'] = keys

if vertices is None:
try:
vertices = self.vertices(sort=True)
except TypeError:
raise TypeError("Vertex labels are not comparable. You must "
"specify an ordering using parameter "
"``vertices``")
elif (len(vertices) != n or
set(vertices) != set(self.vertex_iterator())):
raise ValueError("parameter vertices must be a permutation of the vertices")

new_indices = {v: i for i, v in enumerate(vertices)}
D = {}
directed = self._directed
multiple_edges = self.allows_multiple_edges()
for u, v, l in self.edge_iterator():
i = new_indices[u]
j = new_indices[v]
i = vertex_indices[u]
j = vertex_indices[v]
if multiple_edges and (i, j) in D:
D[i, j] += 1
if not directed and i != j:
Expand Down Expand Up @@ -2298,7 +2380,7 @@ def incidence_matrix(self, oriented=None, sparse=True, vertices=None, edges=None
sage: P5.incidence_matrix(vertices=[1] * P5.order()) # needs sage.modules
Traceback (most recent call last):
...
ValueError: parameter vertices must be a permutation of the vertices
ValueError: parameter 'vertices' must be a permutation of the vertices
sage: P5.incidence_matrix(edges=[(0, 1)] * P5.size()) # needs sage.modules
Traceback (most recent call last):
...
Expand All @@ -2313,18 +2395,9 @@ def incidence_matrix(self, oriented=None, sparse=True, vertices=None, edges=None
if oriented is None:
oriented = self.is_directed()

row_keys = None
if vertices is True:
vertices = self.vertices(sort=False)
row_keys = tuple(vertices) # because a list is not hashable
elif vertices is None:
vertices = self.vertices(sort=False)
elif (len(vertices) != self.num_verts() or
set(vertices) != set(self.vertex_iterator())):
raise ValueError("parameter vertices must be a permutation of the vertices")
vertex_indices, row_keys = self._vertex_indices_and_keys(vertices, sort=False)

column_keys = None
verts = {v: i for i, v in enumerate(vertices)}
use_edge_labels = kwds.pop('use_edge_labels', False)
if edges is True:
edges = self.edges(labels=use_edge_labels)
Expand All @@ -2336,13 +2409,13 @@ def incidence_matrix(self, oriented=None, sparse=True, vertices=None, edges=None
else:
# We check that we have the same set of unlabeled edges
if oriented:
i_edges = [(verts[e[0]], verts[e[1]]) for e in edges]
s_edges = [(verts[u], verts[v]) for u, v in self.edge_iterator(labels=False)]
i_edges = [(vertex_indices[e[0]], vertex_indices[e[1]]) for e in edges]
s_edges = [(vertex_indices[u], vertex_indices[v]) for u, v in self.edge_iterator(labels=False)]
else:
def reorder(u, v):
return (u, v) if u <= v else (v, u)
i_edges = [reorder(verts[e[0]], verts[e[1]]) for e in edges]
s_edges = [reorder(verts[u], verts[v]) for u, v in self.edge_iterator(labels=False)]
i_edges = [reorder(vertex_indices[e[0]], vertex_indices[e[1]]) for e in edges]
s_edges = [reorder(vertex_indices[u], vertex_indices[v]) for u, v in self.edge_iterator(labels=False)]
if sorted(i_edges) != sorted(s_edges):
raise ValueError("parameter edges must be a permutation of the edges")

Expand All @@ -2355,12 +2428,12 @@ def reorder(u, v):
if oriented:
for i, e in enumerate(edges):
if e[0] != e[1]:
m[verts[e[0]], i] = -1
m[verts[e[1]], i] = +1
m[vertex_indices[e[0]], i] = -1
m[vertex_indices[e[1]], i] = +1
else:
for i, e in enumerate(edges):
m[verts[e[0]], i] += 1
m[verts[e[1]], i] += 1
m[vertex_indices[e[0]], i] += 1
m[vertex_indices[e[1]], i] += 1

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

- ``vertices`` -- list (default: ``None``); when specified, each vertex
is represented by its position in the list ``vertices``, otherwise
each vertex is represented by its position in the list returned by
method :meth:`vertices`
- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``);

- when a list, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering of ``vertices``,
- when ``None``, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering given by
:meth:`GenericGraph.vertices` with ``sort=True``.
- when ``True``, construct an endomorphism of a free module instead of
a matrix, where the module's basis is indexed by the vertices.

If the vertices are not comparable, the keyword ``vertices`` must be
used to specify an ordering, or a :class:`TypeError` exception will
be raised.

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

Creating a module morphism::

sage: # needs sage.modules
sage: G = Graph(sparse=True, weighted=True)
sage: G.add_edges([('A', 'B', 1), ('B', 'C', 2), ('A', 'C', 3), ('A', 'D', 4)])
sage: phi = G.weighted_adjacency_matrix(vertices=True); phi
Generic endomorphism of
Free module generated by {'A', 'B', 'C', 'D'} over Integer Ring
sage: print(phi._unicode_art_matrix())
A B C D
A⎛0 1 3 4⎞
B⎜1 0 2 0⎟
C⎜3 2 0 0⎟
D⎝4 0 0 0⎠

TESTS:

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

if vertices is None:
vertices = self.vertices(sort=True)
elif (len(vertices) != self.num_verts() or
set(vertices) != set(self.vertex_iterator())):
raise ValueError("parameter vertices must be a permutation of the vertices")
vertex_indices, row_column_keys = self._vertex_indices_and_keys(vertices)
if row_column_keys is not None:
kwds = copy(kwds)
kwds['row_keys'] = kwds['column_keys'] = row_column_keys

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

new_indices = {v: i for i,v in enumerate(vertices)}

D = {}
if self._directed:
for u, v, label in self.edge_iterator():
i = new_indices[u]
j = new_indices[v]
i = vertex_indices[u]
j = vertex_indices[v]
D[i, j] = func(u, v, label)
else:
for u, v, label in self.edge_iterator():
i = new_indices[u]
j = new_indices[v]
i = vertex_indices[u]
j = vertex_indices[v]
label = func(u, v, label)
D[i, j] = label
D[j, i] = label
Expand Down Expand Up @@ -2707,6 +2801,20 @@ def kirchhoff_matrix(self, weighted=None, indegree=True, normalized=False, signl

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

- ``vertices`` -- list, ``None``, or ``True`` (default: ``None``);

- when a list, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering of ``vertices``,
- when ``None``, the `i`-th row and column of the matrix correspond to
the `i`-th vertex in the ordering given by
:meth:`GenericGraph.vertices` with ``sort=True``.
- when ``True``, construct an endomorphism of a free module instead of
a matrix, where the module's basis is indexed by the vertices.

If the vertices are not comparable, the keyword ``vertices`` must be
used to specify an ordering, or a :class:`TypeError` exception will
be raised.

Note that any additional keywords will be passed on to either the
:meth:`~GenericGraph.adjacency_matrix` or
:meth:`~GenericGraph.weighted_adjacency_matrix` method.
Expand Down Expand Up @@ -2788,18 +2896,36 @@ def kirchhoff_matrix(self, weighted=None, indegree=True, normalized=False, signl
sage: M = G.kirchhoff_matrix(vertices=[0, 1], immutable=True) # needs sage.modules
sage: M.is_immutable() # needs sage.modules
True

Creating a module morphism::

sage: # needs sage.modules
sage: G = Graph(sparse=True, weighted=True)
sage: G.add_edges([('A', 'B', 1), ('B', 'C', 2), ('A', 'C', 3), ('A', 'D', 4)])
sage: phi = G.laplacian_matrix(weighted=True, vertices=True); phi
Generic endomorphism of
Free module generated by {'A', 'B', 'C', 'D'} over Integer Ring
sage: print(phi._unicode_art_matrix())
A B C D
A⎛ 8 -1 -3 -4⎞
B⎜-1 3 -2 0⎟
C⎜-3 -2 5 0⎟
D⎝-4 0 0 4⎠

"""
from sage.matrix.constructor import diagonal_matrix
from sage.matrix.constructor import diagonal_matrix, matrix

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

vertex_indices, keys = self._vertex_indices_and_keys(kwds.pop('vertices', None))

if weighted is None:
weighted = self._weighted

if weighted:
M = self.weighted_adjacency_matrix(immutable=True, **kwds)
M = self.weighted_adjacency_matrix(vertices=list(vertex_indices), immutable=True, **kwds)
else:
M = self.adjacency_matrix(immutable=True, **kwds)
M = self.adjacency_matrix(vertices=list(vertex_indices), immutable=True, **kwds)

D = M.parent(0)

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

if keys is not None:
return matrix(ret, row_keys=keys, column_keys=keys)
if set_immutable:
ret.set_immutable()
return ret
Expand Down
Loading

0 comments on commit feba57a

Please sign in to comment.