Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graph.{[weighted_]adjacency_matrix,kirchhoff_matrix}: Support constructing End(CombinatorialFreeModule) elements #37955

Merged
merged 11 commits into from
May 12, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 128 additions & 46 deletions src/sage/graphs/generic_graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1900,6 +1900,23 @@ def to_dictionary(self, edge_labels=False, multiple_edges=False):

return d

def _vertices_keys(self, vertices=None, *, sort=None):
mkoeppe marked this conversation as resolved.
Show resolved Hide resolved
n = self.order()
keys = None
if vertices is True:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be you could add that this is a helper function for methods like adjacency_matrix, kirchhoff_matrix, etc.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea, done in 98323cb

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 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 +1928,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 +2048,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,18 +2097,10 @@ 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

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")

vertices, keys = self._vertices_keys(vertices)
if keys is not None:
kwds = copy(kwds)
kwds['row_keys'] = kwds['column_keys'] = keys
new_indices = {v: i for i, v in enumerate(vertices)}
D = {}
directed = self._directed
Expand Down Expand Up @@ -2298,7 +2331,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,15 +2346,7 @@ 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")
vertices, row_keys = self._vertices_keys(vertices, sort=False)

column_keys = None
verts = {v: i for i, v in enumerate(vertices)}
Expand Down Expand Up @@ -2524,10 +2549,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 +2613,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 +2658,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")
vertices, row_column_keys = self._vertices_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 Down Expand Up @@ -2707,6 +2755,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 +2850,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)

vertices, keys = self._vertices_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=vertices, immutable=True, **kwds)
else:
M = self.adjacency_matrix(immutable=True, **kwds)
M = self.adjacency_matrix(vertices=vertices, immutable=True, **kwds)

D = M.parent(0)

Expand Down Expand Up @@ -2839,6 +2919,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
11 changes: 8 additions & 3 deletions src/sage/matrix/args.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -390,10 +390,14 @@ cdef class MatrixArgs:
if argi == argc:
return

# check nrows and ncols argument
# check positional nrows and ncols argument
# even if redundant with row_keys, column_keys given as keywords;
# but do not check for positional row_keys, column_keys arguments
# -- we do not allow those, as they would be too easy to
# confuse with entries
cdef int k
cdef long v
if self.nrows == -1 and self.ncols == -1 and self.row_keys is None and self.column_keys is None:
if self.nrows == -1 and self.ncols == -1:
for k in range(2):
arg = args[argi]
if is_numpy_type(type(arg)):
Expand All @@ -410,7 +414,8 @@ cdef class MatrixArgs:
else:
self.set_ncols(v)
argi += 1
if argi == argc: return
if argi == argc:
return

# check for entries argument
if self.entries is None:
Expand Down
24 changes: 18 additions & 6 deletions src/sage/matrix/constructor.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,11 @@ def matrix(*args, **kwds):
determine this from the given entries, falling back to ``ZZ`` if
no entries are given.

- ``nrows`` or ``row_keys`` -- the number of rows in the matrix,
or a finite or enumerated family of arbitrary objects
that index the rows of the matrix
- ``nrows`` -- the number of rows in the matrix, or a finite or
enumerated family of arbitrary objects that index the rows of the matrix

- ``ncols`` or ``column_keys`` -- the number of columns in the
matrix, or a finite or enumerated family of arbitrary objects
that index the columns of the matrix
- ``ncols`` -- the number of columns in the matrix, or a finite or
enumerated family of arbitrary objects that index the columns of the matrix

- ``entries`` -- see examples below.

Expand All @@ -82,6 +80,12 @@ def matrix(*args, **kwds):
``True`` when the entries are given as a dictionary, otherwise
defaults to ``False``.

- ``row_keys`` -- a finite or enumerated family of arbitrary objects
that index the rows of the matrix

- ``column_keys`` -- a finite or enumerated family of arbitrary objects
that index the columns of the matrix

- ``space`` -- matrix space which will be the parent of the output
matrix. This determines ``base_ring``, ``nrows``, ``row_keys``,
``ncols``, ``column_keys``, and ``sparse``.
Expand Down Expand Up @@ -257,6 +261,14 @@ def matrix(*args, **kwds):
u⎛1 2 3⎞
v⎝4 5 6⎠

It is allowed to specify dimensions redundantly::

sage: M = matrix(2, 3, [[1,2,3], [4,5,6]],
....: column_keys=['a','b','c'], row_keys=['u','v']); M
Generic morphism:
From: Free module generated by {'a', 'b', 'c'} over Integer Ring
To: Free module generated by {'u', 'v'} over Integer Ring

TESTS:

There are many ways to create an empty matrix::
Expand Down
Loading