diff --git a/.gitattributes b/.gitattributes index e0ebbf2ce722ee1fcdb8cb72d62dc81cdf0908f6..df73a11d91886544ba5304debb393ab4a2929247 100644 --- a/.gitattributes +++ b/.gitattributes @@ -59,3 +59,4 @@ reference_sample_wavs/syuukovoice_200918_3_01.wav filter=lfs diff=lfs merge=lfs .venv/Lib/site-packages/scipy/interpolate/_rbfinterp_pythran.cp39-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text .venv/Lib/site-packages/scipy/io/_fast_matrix_market/_fmm_core.cp39-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text .venv/Lib/site-packages/scipy/linalg/_flapack.cp39-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text +.venv/Lib/site-packages/scipy/misc/face.dat filter=lfs diff=lfs merge=lfs -text diff --git a/.venv/Lib/site-packages/scipy/misc/face.dat b/.venv/Lib/site-packages/scipy/misc/face.dat new file mode 100644 index 0000000000000000000000000000000000000000..cd6128288bacd3f04131b9e8d13f133e3b6ecd4b --- /dev/null +++ b/.venv/Lib/site-packages/scipy/misc/face.dat @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d8b0b4d081313e2b485748c770472e5a95ed1738146883d84c7030493e82886 +size 1581821 diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/__init__.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2805c2df26a3c9985013feedce45c8e81b3d9c0b --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/__init__.py @@ -0,0 +1,208 @@ +r""" +Compressed sparse graph routines (:mod:`scipy.sparse.csgraph`) +============================================================== + +.. currentmodule:: scipy.sparse.csgraph + +Fast graph algorithms based on sparse matrix representations. + +Contents +-------- + +.. autosummary:: + :toctree: generated/ + + connected_components -- determine connected components of a graph + laplacian -- compute the laplacian of a graph + shortest_path -- compute the shortest path between points on a positive graph + dijkstra -- use Dijkstra's algorithm for shortest path + floyd_warshall -- use the Floyd-Warshall algorithm for shortest path + bellman_ford -- use the Bellman-Ford algorithm for shortest path + johnson -- use Johnson's algorithm for shortest path + breadth_first_order -- compute a breadth-first order of nodes + depth_first_order -- compute a depth-first order of nodes + breadth_first_tree -- construct the breadth-first tree from a given node + depth_first_tree -- construct a depth-first tree from a given node + minimum_spanning_tree -- construct the minimum spanning tree of a graph + reverse_cuthill_mckee -- compute permutation for reverse Cuthill-McKee ordering + maximum_flow -- solve the maximum flow problem for a graph + maximum_bipartite_matching -- compute a maximum matching of a bipartite graph + min_weight_full_bipartite_matching - compute a minimum weight full matching of a bipartite graph + structural_rank -- compute the structural rank of a graph + NegativeCycleError + +.. autosummary:: + :toctree: generated/ + + construct_dist_matrix + csgraph_from_dense + csgraph_from_masked + csgraph_masked_from_dense + csgraph_to_dense + csgraph_to_masked + reconstruct_path + +Graph Representations +--------------------- +This module uses graphs which are stored in a matrix format. A +graph with N nodes can be represented by an (N x N) adjacency matrix G. +If there is a connection from node i to node j, then G[i, j] = w, where +w is the weight of the connection. For nodes i and j which are +not connected, the value depends on the representation: + +- for dense array representations, non-edges are represented by + G[i, j] = 0, infinity, or NaN. + +- for dense masked representations (of type np.ma.MaskedArray), non-edges + are represented by masked values. This can be useful when graphs with + zero-weight edges are desired. + +- for sparse array representations, non-edges are represented by + non-entries in the matrix. This sort of sparse representation also + allows for edges with zero weights. + +As a concrete example, imagine that you would like to represent the following +undirected graph:: + + G + + (0) + / \ + 1 2 + / \ + (2) (1) + +This graph has three nodes, where node 0 and 1 are connected by an edge of +weight 2, and nodes 0 and 2 are connected by an edge of weight 1. +We can construct the dense, masked, and sparse representations as follows, +keeping in mind that an undirected graph is represented by a symmetric matrix:: + + >>> import numpy as np + >>> G_dense = np.array([[0, 2, 1], + ... [2, 0, 0], + ... [1, 0, 0]]) + >>> G_masked = np.ma.masked_values(G_dense, 0) + >>> from scipy.sparse import csr_matrix + >>> G_sparse = csr_matrix(G_dense) + +This becomes more difficult when zero edges are significant. For example, +consider the situation when we slightly modify the above graph:: + + G2 + + (0) + / \ + 0 2 + / \ + (2) (1) + +This is identical to the previous graph, except nodes 0 and 2 are connected +by an edge of zero weight. In this case, the dense representation above +leads to ambiguities: how can non-edges be represented if zero is a meaningful +value? In this case, either a masked or sparse representation must be used +to eliminate the ambiguity:: + + >>> import numpy as np + >>> G2_data = np.array([[np.inf, 2, 0 ], + ... [2, np.inf, np.inf], + ... [0, np.inf, np.inf]]) + >>> G2_masked = np.ma.masked_invalid(G2_data) + >>> from scipy.sparse.csgraph import csgraph_from_dense + >>> # G2_sparse = csr_matrix(G2_data) would give the wrong result + >>> G2_sparse = csgraph_from_dense(G2_data, null_value=np.inf) + >>> G2_sparse.data + array([ 2., 0., 2., 0.]) + +Here we have used a utility routine from the csgraph submodule in order to +convert the dense representation to a sparse representation which can be +understood by the algorithms in submodule. By viewing the data array, we +can see that the zero values are explicitly encoded in the graph. + +Directed vs. undirected +^^^^^^^^^^^^^^^^^^^^^^^ +Matrices may represent either directed or undirected graphs. This is +specified throughout the csgraph module by a boolean keyword. Graphs are +assumed to be directed by default. In a directed graph, traversal from node +i to node j can be accomplished over the edge G[i, j], but not the edge +G[j, i]. Consider the following dense graph:: + + >>> import numpy as np + >>> G_dense = np.array([[0, 1, 0], + ... [2, 0, 3], + ... [0, 4, 0]]) + +When ``directed=True`` we get the graph:: + + ---1--> ---3--> + (0) (1) (2) + <--2--- <--4--- + +In a non-directed graph, traversal from node i to node j can be +accomplished over either G[i, j] or G[j, i]. If both edges are not null, +and the two have unequal weights, then the smaller of the two is used. + +So for the same graph, when ``directed=False`` we get the graph:: + + (0)--1--(1)--3--(2) + +Note that a symmetric matrix will represent an undirected graph, regardless +of whether the 'directed' keyword is set to True or False. In this case, +using ``directed=True`` generally leads to more efficient computation. + +The routines in this module accept as input either scipy.sparse representations +(csr, csc, or lil format), masked representations, or dense representations +with non-edges indicated by zeros, infinities, and NaN entries. +""" # noqa: E501 + +__docformat__ = "restructuredtext en" + +__all__ = ['connected_components', + 'laplacian', + 'shortest_path', + 'floyd_warshall', + 'dijkstra', + 'bellman_ford', + 'johnson', + 'breadth_first_order', + 'depth_first_order', + 'breadth_first_tree', + 'depth_first_tree', + 'minimum_spanning_tree', + 'reverse_cuthill_mckee', + 'maximum_flow', + 'maximum_bipartite_matching', + 'min_weight_full_bipartite_matching', + 'structural_rank', + 'construct_dist_matrix', + 'reconstruct_path', + 'csgraph_masked_from_dense', + 'csgraph_from_dense', + 'csgraph_from_masked', + 'csgraph_to_dense', + 'csgraph_to_masked', + 'NegativeCycleError'] + +from ._laplacian import laplacian +from ._shortest_path import ( + shortest_path, floyd_warshall, dijkstra, bellman_ford, johnson, + NegativeCycleError +) +from ._traversal import ( + breadth_first_order, depth_first_order, breadth_first_tree, + depth_first_tree, connected_components +) +from ._min_spanning_tree import minimum_spanning_tree +from ._flow import maximum_flow +from ._matching import ( + maximum_bipartite_matching, min_weight_full_bipartite_matching +) +from ._reordering import reverse_cuthill_mckee, structural_rank +from ._tools import ( + construct_dist_matrix, reconstruct_path, csgraph_from_dense, + csgraph_to_dense, csgraph_masked_from_dense, csgraph_from_masked, + csgraph_to_masked +) + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/_traversal.cp39-win_amd64.pyd b/.venv/Lib/site-packages/scipy/sparse/csgraph/_traversal.cp39-win_amd64.pyd new file mode 100644 index 0000000000000000000000000000000000000000..0a0782bf6b0618e29aef363cdddb87dbb3f98a33 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/csgraph/_traversal.cp39-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/_validation.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/_validation.py new file mode 100644 index 0000000000000000000000000000000000000000..fe448374c065e7d554035aaa451fe8bc62b18532 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/_validation.py @@ -0,0 +1,61 @@ +import numpy as np +from scipy.sparse import csr_matrix, issparse +from scipy.sparse._sputils import convert_pydata_sparse_to_scipy +from scipy.sparse.csgraph._tools import ( + csgraph_to_dense, csgraph_from_dense, + csgraph_masked_from_dense, csgraph_from_masked +) + +DTYPE = np.float64 + + +def validate_graph(csgraph, directed, dtype=DTYPE, + csr_output=True, dense_output=True, + copy_if_dense=False, copy_if_sparse=False, + null_value_in=0, null_value_out=np.inf, + infinity_null=True, nan_null=True): + """Routine for validation and conversion of csgraph inputs""" + if not (csr_output or dense_output): + raise ValueError("Internal: dense or csr output must be true") + + csgraph = convert_pydata_sparse_to_scipy(csgraph) + + # if undirected and csc storage, then transposing in-place + # is quicker than later converting to csr. + if (not directed) and issparse(csgraph) and csgraph.format == "csc": + csgraph = csgraph.T + + if issparse(csgraph): + if csr_output: + csgraph = csr_matrix(csgraph, dtype=DTYPE, copy=copy_if_sparse) + else: + csgraph = csgraph_to_dense(csgraph, null_value=null_value_out) + elif np.ma.isMaskedArray(csgraph): + if dense_output: + mask = csgraph.mask + csgraph = np.array(csgraph.data, dtype=DTYPE, copy=copy_if_dense) + csgraph[mask] = null_value_out + else: + csgraph = csgraph_from_masked(csgraph) + else: + if dense_output: + csgraph = csgraph_masked_from_dense(csgraph, + copy=copy_if_dense, + null_value=null_value_in, + nan_null=nan_null, + infinity_null=infinity_null) + mask = csgraph.mask + csgraph = np.asarray(csgraph.data, dtype=DTYPE) + csgraph[mask] = null_value_out + else: + csgraph = csgraph_from_dense(csgraph, null_value=null_value_in, + infinity_null=infinity_null, + nan_null=nan_null) + + if csgraph.ndim != 2: + raise ValueError("compressed-sparse graph must be 2-D") + + if csgraph.shape[0] != csgraph.shape[1]: + raise ValueError("compressed-sparse graph must be shape (N, N)") + + return csgraph diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_connected_components.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_connected_components.py new file mode 100644 index 0000000000000000000000000000000000000000..6ed7e321a2c5ca8539e19c6fe72447af5c524223 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_connected_components.py @@ -0,0 +1,119 @@ +import numpy as np +from numpy.testing import assert_equal, assert_array_almost_equal +from scipy.sparse import csgraph, csr_array + + +def test_weak_connections(): + Xde = np.array([[0, 1, 0], + [0, 0, 0], + [0, 0, 0]]) + + Xsp = csgraph.csgraph_from_dense(Xde, null_value=0) + + for X in Xsp, Xde: + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='weak') + + assert_equal(n_components, 2) + assert_array_almost_equal(labels, [0, 0, 1]) + + +def test_strong_connections(): + X1de = np.array([[0, 1, 0], + [0, 0, 0], + [0, 0, 0]]) + X2de = X1de + X1de.T + + X1sp = csgraph.csgraph_from_dense(X1de, null_value=0) + X2sp = csgraph.csgraph_from_dense(X2de, null_value=0) + + for X in X1sp, X1de: + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='strong') + + assert_equal(n_components, 3) + labels.sort() + assert_array_almost_equal(labels, [0, 1, 2]) + + for X in X2sp, X2de: + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='strong') + + assert_equal(n_components, 2) + labels.sort() + assert_array_almost_equal(labels, [0, 0, 1]) + + +def test_strong_connections2(): + X = np.array([[0, 0, 0, 0, 0, 0], + [1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0]]) + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='strong') + assert_equal(n_components, 5) + labels.sort() + assert_array_almost_equal(labels, [0, 1, 2, 2, 3, 4]) + + +def test_weak_connections2(): + X = np.array([[0, 0, 0, 0, 0, 0], + [1, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 1, 0], + [0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0]]) + n_components, labels =\ + csgraph.connected_components(X, directed=True, + connection='weak') + assert_equal(n_components, 2) + labels.sort() + assert_array_almost_equal(labels, [0, 0, 1, 1, 1, 1]) + + +def test_ticket1876(): + # Regression test: this failed in the original implementation + # There should be two strongly-connected components; previously gave one + g = np.array([[0, 1, 1, 0], + [1, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 1, 0]]) + n_components, labels = csgraph.connected_components(g, connection='strong') + + assert_equal(n_components, 2) + assert_equal(labels[0], labels[1]) + assert_equal(labels[2], labels[3]) + + +def test_fully_connected_graph(): + # Fully connected dense matrices raised an exception. + # https://github.com/scipy/scipy/issues/3818 + g = np.ones((4, 4)) + n_components, labels = csgraph.connected_components(g) + assert_equal(n_components, 1) + + +def test_int64_indices_undirected(): + # See https://github.com/scipy/scipy/issues/18716 + g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) + assert g.indices.dtype == np.int64 + n, labels = csgraph.connected_components(g, directed=False) + assert n == 1 + assert_array_almost_equal(labels, [0, 0]) + + +def test_int64_indices_directed(): + # See https://github.com/scipy/scipy/issues/18716 + g = csr_array(([1], np.array([[0], [1]], dtype=np.int64)), shape=(2, 2)) + assert g.indices.dtype == np.int64 + n, labels = csgraph.connected_components(g, directed=True, + connection='strong') + assert n == 2 + assert_array_almost_equal(labels, [1, 0]) + diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_conversions.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_conversions.py new file mode 100644 index 0000000000000000000000000000000000000000..6e102992389e677b78a91f49f5d0bf1b430d1931 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_conversions.py @@ -0,0 +1,61 @@ +import numpy as np +from numpy.testing import assert_array_almost_equal +from scipy.sparse import csr_matrix +from scipy.sparse.csgraph import csgraph_from_dense, csgraph_to_dense + + +def test_csgraph_from_dense(): + np.random.seed(1234) + G = np.random.random((10, 10)) + some_nulls = (G < 0.4) + all_nulls = (G < 0.8) + + for null_value in [0, np.nan, np.inf]: + G[all_nulls] = null_value + with np.errstate(invalid="ignore"): + G_csr = csgraph_from_dense(G, null_value=0) + + G[all_nulls] = 0 + assert_array_almost_equal(G, G_csr.toarray()) + + for null_value in [np.nan, np.inf]: + G[all_nulls] = 0 + G[some_nulls] = null_value + with np.errstate(invalid="ignore"): + G_csr = csgraph_from_dense(G, null_value=0) + + G[all_nulls] = 0 + assert_array_almost_equal(G, G_csr.toarray()) + + +def test_csgraph_to_dense(): + np.random.seed(1234) + G = np.random.random((10, 10)) + nulls = (G < 0.8) + G[nulls] = np.inf + + G_csr = csgraph_from_dense(G) + + for null_value in [0, 10, -np.inf, np.inf]: + G[nulls] = null_value + assert_array_almost_equal(G, csgraph_to_dense(G_csr, null_value)) + + +def test_multiple_edges(): + # create a random square matrix with an even number of elements + np.random.seed(1234) + X = np.random.random((10, 10)) + Xcsr = csr_matrix(X) + + # now double-up every other column + Xcsr.indices[::2] = Xcsr.indices[1::2] + + # normal sparse toarray() will sum the duplicated edges + Xdense = Xcsr.toarray() + assert_array_almost_equal(Xdense[:, 1::2], + X[:, ::2] + X[:, 1::2]) + + # csgraph_to_dense chooses the minimum of each duplicated edge + Xdense = csgraph_to_dense(Xcsr) + assert_array_almost_equal(Xdense[:, 1::2], + np.minimum(X[:, ::2], X[:, 1::2])) diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_flow.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..e8f7875303cc328184d2fdea0fb4bddc8d4e7be1 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_flow.py @@ -0,0 +1,201 @@ +import numpy as np +from numpy.testing import assert_array_equal +import pytest + +from scipy.sparse import csr_matrix, csc_matrix +from scipy.sparse.csgraph import maximum_flow +from scipy.sparse.csgraph._flow import ( + _add_reverse_edges, _make_edge_pointers, _make_tails +) + +methods = ['edmonds_karp', 'dinic'] + +def test_raises_on_dense_input(): + with pytest.raises(TypeError): + graph = np.array([[0, 1], [0, 0]]) + maximum_flow(graph, 0, 1) + maximum_flow(graph, 0, 1, method='edmonds_karp') + + +def test_raises_on_csc_input(): + with pytest.raises(TypeError): + graph = csc_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, 0, 1) + maximum_flow(graph, 0, 1, method='edmonds_karp') + + +def test_raises_on_floating_point_input(): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1.5], [0, 0]], dtype=np.float64) + maximum_flow(graph, 0, 1) + maximum_flow(graph, 0, 1, method='edmonds_karp') + + +def test_raises_on_non_square_input(): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1, 2], [2, 1, 0]]) + maximum_flow(graph, 0, 1) + + +def test_raises_when_source_is_sink(): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, 0, 0) + maximum_flow(graph, 0, 0, method='edmonds_karp') + + +@pytest.mark.parametrize('method', methods) +@pytest.mark.parametrize('source', [-1, 2, 3]) +def test_raises_when_source_is_out_of_bounds(source, method): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, source, 1, method=method) + + +@pytest.mark.parametrize('method', methods) +@pytest.mark.parametrize('sink', [-1, 2, 3]) +def test_raises_when_sink_is_out_of_bounds(sink, method): + with pytest.raises(ValueError): + graph = csr_matrix([[0, 1], [0, 0]]) + maximum_flow(graph, 0, sink, method=method) + + +@pytest.mark.parametrize('method', methods) +def test_simple_graph(method): + # This graph looks as follows: + # (0) --5--> (1) + graph = csr_matrix([[0, 5], [0, 0]]) + res = maximum_flow(graph, 0, 1, method=method) + assert res.flow_value == 5 + expected_flow = np.array([[0, 5], [-5, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_bottle_neck_graph(method): + # This graph cannot use the full capacity between 0 and 1: + # (0) --5--> (1) --3--> (2) + graph = csr_matrix([[0, 5, 0], [0, 0, 3], [0, 0, 0]]) + res = maximum_flow(graph, 0, 2, method=method) + assert res.flow_value == 3 + expected_flow = np.array([[0, 3, 0], [-3, 0, 3], [0, -3, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_backwards_flow(method): + # This example causes backwards flow between vertices 3 and 4, + # and so this test ensures that we handle that accordingly. See + # https://stackoverflow.com/q/38843963/5085211 + # for more information. + graph = csr_matrix([[0, 10, 0, 0, 10, 0, 0, 0], + [0, 0, 10, 0, 0, 0, 0, 0], + [0, 0, 0, 10, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 10], + [0, 0, 0, 10, 0, 10, 0, 0], + [0, 0, 0, 0, 0, 0, 10, 0], + [0, 0, 0, 0, 0, 0, 0, 10], + [0, 0, 0, 0, 0, 0, 0, 0]]) + res = maximum_flow(graph, 0, 7, method=method) + assert res.flow_value == 20 + expected_flow = np.array([[0, 10, 0, 0, 10, 0, 0, 0], + [-10, 0, 10, 0, 0, 0, 0, 0], + [0, -10, 0, 10, 0, 0, 0, 0], + [0, 0, -10, 0, 0, 0, 0, 10], + [-10, 0, 0, 0, 0, 10, 0, 0], + [0, 0, 0, 0, -10, 0, 10, 0], + [0, 0, 0, 0, 0, -10, 0, 10], + [0, 0, 0, -10, 0, 0, -10, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_example_from_clrs_chapter_26_1(method): + # See page 659 in CLRS second edition, but note that the maximum flow + # we find is slightly different than the one in CLRS; we push a flow of + # 12 to v_1 instead of v_2. + graph = csr_matrix([[0, 16, 13, 0, 0, 0], + [0, 0, 10, 12, 0, 0], + [0, 4, 0, 0, 14, 0], + [0, 0, 9, 0, 0, 20], + [0, 0, 0, 7, 0, 4], + [0, 0, 0, 0, 0, 0]]) + res = maximum_flow(graph, 0, 5, method=method) + assert res.flow_value == 23 + expected_flow = np.array([[0, 12, 11, 0, 0, 0], + [-12, 0, 0, 12, 0, 0], + [-11, 0, 0, 0, 11, 0], + [0, -12, 0, 0, -7, 19], + [0, 0, -11, 7, 0, 4], + [0, 0, 0, -19, -4, 0]]) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_disconnected_graph(method): + # This tests the following disconnected graph: + # (0) --5--> (1) (2) --3--> (3) + graph = csr_matrix([[0, 5, 0, 0], + [0, 0, 0, 0], + [0, 0, 9, 3], + [0, 0, 0, 0]]) + res = maximum_flow(graph, 0, 3, method=method) + assert res.flow_value == 0 + expected_flow = np.zeros((4, 4), dtype=np.int32) + assert_array_equal(res.flow.toarray(), expected_flow) + + +@pytest.mark.parametrize('method', methods) +def test_add_reverse_edges_large_graph(method): + # Regression test for https://github.com/scipy/scipy/issues/14385 + n = 100_000 + indices = np.arange(1, n) + indptr = np.array(list(range(n)) + [n - 1]) + data = np.ones(n - 1, dtype=np.int32) + graph = csr_matrix((data, indices, indptr), shape=(n, n)) + res = maximum_flow(graph, 0, n - 1, method=method) + assert res.flow_value == 1 + expected_flow = graph - graph.transpose() + assert_array_equal(res.flow.data, expected_flow.data) + assert_array_equal(res.flow.indices, expected_flow.indices) + assert_array_equal(res.flow.indptr, expected_flow.indptr) + + +@pytest.mark.parametrize("a,b_data_expected", [ + ([[]], []), + ([[0], [0]], []), + ([[1, 0, 2], [0, 0, 0], [0, 3, 0]], [1, 2, 0, 0, 3]), + ([[9, 8, 7], [4, 5, 6], [0, 0, 0]], [9, 8, 7, 4, 5, 6, 0, 0])]) +def test_add_reverse_edges(a, b_data_expected): + """Test that the reversal of the edges of the input graph works + as expected. + """ + a = csr_matrix(a, dtype=np.int32, shape=(len(a), len(a))) + b = _add_reverse_edges(a) + assert_array_equal(b.data, b_data_expected) + + +@pytest.mark.parametrize("a,expected", [ + ([[]], []), + ([[0]], []), + ([[1]], [0]), + ([[0, 1], [10, 0]], [1, 0]), + ([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 3, 4, 1, 2]) +]) +def test_make_edge_pointers(a, expected): + a = csr_matrix(a, dtype=np.int32) + rev_edge_ptr = _make_edge_pointers(a) + assert_array_equal(rev_edge_ptr, expected) + + +@pytest.mark.parametrize("a,expected", [ + ([[]], []), + ([[0]], []), + ([[1]], [0]), + ([[0, 1], [10, 0]], [0, 1]), + ([[1, 0, 2], [0, 0, 3], [4, 5, 0]], [0, 0, 1, 2, 2]) +]) +def test_make_tails(a, expected): + a = csr_matrix(a, dtype=np.int32) + tails = _make_tails(a) + assert_array_equal(tails, expected) diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_graph_laplacian.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_graph_laplacian.py new file mode 100644 index 0000000000000000000000000000000000000000..cabc5f015826511c82cbd81af50e89d7ff961f21 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_graph_laplacian.py @@ -0,0 +1,369 @@ +import pytest +import numpy as np +from numpy.testing import assert_allclose +from pytest import raises as assert_raises +from scipy import sparse + +from scipy.sparse import csgraph +from scipy._lib._util import np_long, np_ulong + + +def check_int_type(mat): + return np.issubdtype(mat.dtype, np.signedinteger) or np.issubdtype( + mat.dtype, np_ulong + ) + + +def test_laplacian_value_error(): + for t in int, float, complex: + for m in ([1, 1], + [[[1]]], + [[1, 2, 3], [4, 5, 6]], + [[1, 2], [3, 4], [5, 5]]): + A = np.array(m, dtype=t) + assert_raises(ValueError, csgraph.laplacian, A) + + +def _explicit_laplacian(x, normed=False): + if sparse.issparse(x): + x = x.toarray() + x = np.asarray(x) + y = -1.0 * x + for j in range(y.shape[0]): + y[j,j] = x[j,j+1:].sum() + x[j,:j].sum() + if normed: + d = np.diag(y).copy() + d[d == 0] = 1.0 + y /= d[:,None]**.5 + y /= d[None,:]**.5 + return y + + +def _check_symmetric_graph_laplacian(mat, normed, copy=True): + if not hasattr(mat, 'shape'): + mat = eval(mat, dict(np=np, sparse=sparse)) + + if sparse.issparse(mat): + sp_mat = mat + mat = sp_mat.toarray() + else: + sp_mat = sparse.csr_matrix(mat) + + mat_copy = np.copy(mat) + sp_mat_copy = sparse.csr_matrix(sp_mat, copy=True) + + n_nodes = mat.shape[0] + explicit_laplacian = _explicit_laplacian(mat, normed=normed) + laplacian = csgraph.laplacian(mat, normed=normed, copy=copy) + sp_laplacian = csgraph.laplacian(sp_mat, normed=normed, + copy=copy) + + if copy: + assert_allclose(mat, mat_copy) + _assert_allclose_sparse(sp_mat, sp_mat_copy) + else: + if not (normed and check_int_type(mat)): + assert_allclose(laplacian, mat) + if sp_mat.format == 'coo': + _assert_allclose_sparse(sp_laplacian, sp_mat) + + assert_allclose(laplacian, sp_laplacian.toarray()) + + for tested in [laplacian, sp_laplacian.toarray()]: + if not normed: + assert_allclose(tested.sum(axis=0), np.zeros(n_nodes)) + assert_allclose(tested.T, tested) + assert_allclose(tested, explicit_laplacian) + + +def test_symmetric_graph_laplacian(): + symmetric_mats = ( + 'np.arange(10) * np.arange(10)[:, np.newaxis]', + 'np.ones((7, 7))', + 'np.eye(19)', + 'sparse.diags([1, 1], [-1, 1], shape=(4, 4))', + 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).toarray()', + 'sparse.diags([1, 1], [-1, 1], shape=(4, 4)).todense()', + 'np.vander(np.arange(4)) + np.vander(np.arange(4)).T' + ) + for mat in symmetric_mats: + for normed in True, False: + for copy in True, False: + _check_symmetric_graph_laplacian(mat, normed, copy) + + +def _assert_allclose_sparse(a, b, **kwargs): + # helper function that can deal with sparse matrices + if sparse.issparse(a): + a = a.toarray() + if sparse.issparse(b): + b = b.toarray() + assert_allclose(a, b, **kwargs) + + +def _check_laplacian_dtype_none( + A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type +): + mat = arr_type(A, dtype=dtype) + L, d = csgraph.laplacian( + mat, + normed=normed, + return_diag=True, + use_out_degree=use_out_degree, + copy=copy, + dtype=None, + ) + if normed and check_int_type(mat): + assert L.dtype == np.float64 + assert d.dtype == np.float64 + _assert_allclose_sparse(L, desired_L, atol=1e-12) + _assert_allclose_sparse(d, desired_d, atol=1e-12) + else: + assert L.dtype == dtype + assert d.dtype == dtype + desired_L = np.asarray(desired_L).astype(dtype) + desired_d = np.asarray(desired_d).astype(dtype) + _assert_allclose_sparse(L, desired_L, atol=1e-12) + _assert_allclose_sparse(d, desired_d, atol=1e-12) + + if not copy: + if not (normed and check_int_type(mat)): + if type(mat) is np.ndarray: + assert_allclose(L, mat) + elif mat.format == "coo": + _assert_allclose_sparse(L, mat) + + +def _check_laplacian_dtype( + A, desired_L, desired_d, normed, use_out_degree, copy, dtype, arr_type +): + mat = arr_type(A, dtype=dtype) + L, d = csgraph.laplacian( + mat, + normed=normed, + return_diag=True, + use_out_degree=use_out_degree, + copy=copy, + dtype=dtype, + ) + assert L.dtype == dtype + assert d.dtype == dtype + desired_L = np.asarray(desired_L).astype(dtype) + desired_d = np.asarray(desired_d).astype(dtype) + _assert_allclose_sparse(L, desired_L, atol=1e-12) + _assert_allclose_sparse(d, desired_d, atol=1e-12) + + if not copy: + if not (normed and check_int_type(mat)): + if type(mat) is np.ndarray: + assert_allclose(L, mat) + elif mat.format == 'coo': + _assert_allclose_sparse(L, mat) + + +INT_DTYPES = {np.intc, np_long, np.longlong} +REAL_DTYPES = {np.float32, np.float64, np.longdouble} +COMPLEX_DTYPES = {np.complex64, np.complex128, np.clongdouble} +# use sorted list to ensure fixed order of tests +DTYPES = sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str) + + +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("arr_type", [np.array, + sparse.csr_matrix, + sparse.coo_matrix, + sparse.csr_array, + sparse.coo_array]) +@pytest.mark.parametrize("copy", [True, False]) +@pytest.mark.parametrize("normed", [True, False]) +@pytest.mark.parametrize("use_out_degree", [True, False]) +def test_asymmetric_laplacian(use_out_degree, normed, + copy, dtype, arr_type): + # adjacency matrix + A = [[0, 1, 0], + [4, 2, 0], + [0, 0, 0]] + A = arr_type(np.array(A), dtype=dtype) + A_copy = A.copy() + + if not normed and use_out_degree: + # Laplacian matrix using out-degree + L = [[1, -1, 0], + [-4, 4, 0], + [0, 0, 0]] + d = [1, 4, 0] + + if normed and use_out_degree: + # normalized Laplacian matrix using out-degree + L = [[1, -0.5, 0], + [-2, 1, 0], + [0, 0, 0]] + d = [1, 2, 1] + + if not normed and not use_out_degree: + # Laplacian matrix using in-degree + L = [[4, -1, 0], + [-4, 1, 0], + [0, 0, 0]] + d = [4, 1, 0] + + if normed and not use_out_degree: + # normalized Laplacian matrix using in-degree + L = [[1, -0.5, 0], + [-2, 1, 0], + [0, 0, 0]] + d = [2, 1, 1] + + _check_laplacian_dtype_none( + A, + L, + d, + normed=normed, + use_out_degree=use_out_degree, + copy=copy, + dtype=dtype, + arr_type=arr_type, + ) + + _check_laplacian_dtype( + A_copy, + L, + d, + normed=normed, + use_out_degree=use_out_degree, + copy=copy, + dtype=dtype, + arr_type=arr_type, + ) + + +@pytest.mark.parametrize("fmt", ['csr', 'csc', 'coo', 'lil', + 'dok', 'dia', 'bsr']) +@pytest.mark.parametrize("normed", [True, False]) +@pytest.mark.parametrize("copy", [True, False]) +def test_sparse_formats(fmt, normed, copy): + mat = sparse.diags([1, 1], [-1, 1], shape=(4, 4), format=fmt) + _check_symmetric_graph_laplacian(mat, normed, copy) + + +@pytest.mark.parametrize( + "arr_type", [np.asarray, + sparse.csr_matrix, + sparse.coo_matrix, + sparse.csr_array, + sparse.coo_array] +) +@pytest.mark.parametrize("form", ["array", "function", "lo"]) +def test_laplacian_symmetrized(arr_type, form): + # adjacency matrix + n = 3 + mat = arr_type(np.arange(n * n).reshape(n, n)) + L_in, d_in = csgraph.laplacian( + mat, + return_diag=True, + form=form, + ) + L_out, d_out = csgraph.laplacian( + mat, + return_diag=True, + use_out_degree=True, + form=form, + ) + Ls, ds = csgraph.laplacian( + mat, + return_diag=True, + symmetrized=True, + form=form, + ) + Ls_normed, ds_normed = csgraph.laplacian( + mat, + return_diag=True, + symmetrized=True, + normed=True, + form=form, + ) + mat += mat.T + Lss, dss = csgraph.laplacian(mat, return_diag=True, form=form) + Lss_normed, dss_normed = csgraph.laplacian( + mat, + return_diag=True, + normed=True, + form=form, + ) + + assert_allclose(ds, d_in + d_out) + assert_allclose(ds, dss) + assert_allclose(ds_normed, dss_normed) + + d = {} + for L in ["L_in", "L_out", "Ls", "Ls_normed", "Lss", "Lss_normed"]: + if form == "array": + d[L] = eval(L) + else: + d[L] = eval(L)(np.eye(n, dtype=mat.dtype)) + + _assert_allclose_sparse(d["Ls"], d["L_in"] + d["L_out"].T) + _assert_allclose_sparse(d["Ls"], d["Lss"]) + _assert_allclose_sparse(d["Ls_normed"], d["Lss_normed"]) + + +@pytest.mark.parametrize( + "arr_type", [np.asarray, + sparse.csr_matrix, + sparse.coo_matrix, + sparse.csr_array, + sparse.coo_array] +) +@pytest.mark.parametrize("dtype", DTYPES) +@pytest.mark.parametrize("normed", [True, False]) +@pytest.mark.parametrize("symmetrized", [True, False]) +@pytest.mark.parametrize("use_out_degree", [True, False]) +@pytest.mark.parametrize("form", ["function", "lo"]) +def test_format(dtype, arr_type, normed, symmetrized, use_out_degree, form): + n = 3 + mat = [[0, 1, 0], [4, 2, 0], [0, 0, 0]] + mat = arr_type(np.array(mat), dtype=dtype) + Lo, do = csgraph.laplacian( + mat, + return_diag=True, + normed=normed, + symmetrized=symmetrized, + use_out_degree=use_out_degree, + dtype=dtype, + ) + La, da = csgraph.laplacian( + mat, + return_diag=True, + normed=normed, + symmetrized=symmetrized, + use_out_degree=use_out_degree, + dtype=dtype, + form="array", + ) + assert_allclose(do, da) + _assert_allclose_sparse(Lo, La) + + L, d = csgraph.laplacian( + mat, + return_diag=True, + normed=normed, + symmetrized=symmetrized, + use_out_degree=use_out_degree, + dtype=dtype, + form=form, + ) + assert_allclose(d, do) + assert d.dtype == dtype + Lm = L(np.eye(n, dtype=mat.dtype)).astype(dtype) + _assert_allclose_sparse(Lm, Lo, rtol=2e-7, atol=2e-7) + x = np.arange(6).reshape(3, 2) + if not (normed and dtype in INT_DTYPES): + assert_allclose(L(x), Lo @ x) + else: + # Normalized Lo is casted to integer, but L() is not + pass + + +def test_format_error_message(): + with pytest.raises(ValueError, match="Invalid form: 'toto'"): + _ = csgraph.laplacian(np.eye(1), form='toto') diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_matching.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_matching.py new file mode 100644 index 0000000000000000000000000000000000000000..4883d796c41adb754611bfb0d71b0e2341e4cab8 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_matching.py @@ -0,0 +1,294 @@ +from itertools import product + +import numpy as np +from numpy.testing import assert_array_equal, assert_equal +import pytest + +from scipy.sparse import csr_matrix, coo_matrix, diags +from scipy.sparse.csgraph import ( + maximum_bipartite_matching, min_weight_full_bipartite_matching +) + + +def test_maximum_bipartite_matching_raises_on_dense_input(): + with pytest.raises(TypeError): + graph = np.array([[0, 1], [0, 0]]) + maximum_bipartite_matching(graph) + + +def test_maximum_bipartite_matching_empty_graph(): + graph = csr_matrix((0, 0)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + expected_matching = np.array([]) + assert_array_equal(expected_matching, x) + assert_array_equal(expected_matching, y) + + +def test_maximum_bipartite_matching_empty_left_partition(): + graph = csr_matrix((2, 0)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert_array_equal(np.array([]), x) + assert_array_equal(np.array([-1, -1]), y) + + +def test_maximum_bipartite_matching_empty_right_partition(): + graph = csr_matrix((0, 3)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert_array_equal(np.array([-1, -1, -1]), x) + assert_array_equal(np.array([]), y) + + +def test_maximum_bipartite_matching_graph_with_no_edges(): + graph = csr_matrix((2, 2)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert_array_equal(np.array([-1, -1]), x) + assert_array_equal(np.array([-1, -1]), y) + + +def test_maximum_bipartite_matching_graph_that_causes_augmentation(): + # In this graph, column 1 is initially assigned to row 1, but it should be + # reassigned to make room for row 2. + graph = csr_matrix([[1, 1], [1, 0]]) + x = maximum_bipartite_matching(graph, perm_type='column') + y = maximum_bipartite_matching(graph, perm_type='row') + expected_matching = np.array([1, 0]) + assert_array_equal(expected_matching, x) + assert_array_equal(expected_matching, y) + + +def test_maximum_bipartite_matching_graph_with_more_rows_than_columns(): + graph = csr_matrix([[1, 1], [1, 0], [0, 1]]) + x = maximum_bipartite_matching(graph, perm_type='column') + y = maximum_bipartite_matching(graph, perm_type='row') + assert_array_equal(np.array([0, -1, 1]), x) + assert_array_equal(np.array([0, 2]), y) + + +def test_maximum_bipartite_matching_graph_with_more_columns_than_rows(): + graph = csr_matrix([[1, 1, 0], [0, 0, 1]]) + x = maximum_bipartite_matching(graph, perm_type='column') + y = maximum_bipartite_matching(graph, perm_type='row') + assert_array_equal(np.array([0, 2]), x) + assert_array_equal(np.array([0, -1, 1]), y) + + +def test_maximum_bipartite_matching_explicit_zeros_count_as_edges(): + data = [0, 0] + indices = [1, 0] + indptr = [0, 1, 2] + graph = csr_matrix((data, indices, indptr), shape=(2, 2)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + expected_matching = np.array([1, 0]) + assert_array_equal(expected_matching, x) + assert_array_equal(expected_matching, y) + + +def test_maximum_bipartite_matching_feasibility_of_result(): + # This is a regression test for GitHub issue #11458 + data = np.ones(50, dtype=int) + indices = [11, 12, 19, 22, 23, 5, 22, 3, 8, 10, 5, 6, 11, 12, 13, 5, 13, + 14, 20, 22, 3, 15, 3, 13, 14, 11, 12, 19, 22, 23, 5, 22, 3, 8, + 10, 5, 6, 11, 12, 13, 5, 13, 14, 20, 22, 3, 15, 3, 13, 14] + indptr = [0, 5, 7, 10, 10, 15, 20, 22, 22, 23, 25, 30, 32, 35, 35, 40, 45, + 47, 47, 48, 50] + graph = csr_matrix((data, indices, indptr), shape=(20, 25)) + x = maximum_bipartite_matching(graph, perm_type='row') + y = maximum_bipartite_matching(graph, perm_type='column') + assert (x != -1).sum() == 13 + assert (y != -1).sum() == 13 + # Ensure that each element of the matching is in fact an edge in the graph. + for u, v in zip(range(graph.shape[0]), y): + if v != -1: + assert graph[u, v] + for u, v in zip(x, range(graph.shape[1])): + if u != -1: + assert graph[u, v] + + +def test_matching_large_random_graph_with_one_edge_incident_to_each_vertex(): + np.random.seed(42) + A = diags(np.ones(25), offsets=0, format='csr') + rand_perm = np.random.permutation(25) + rand_perm2 = np.random.permutation(25) + + Rrow = np.arange(25) + Rcol = rand_perm + Rdata = np.ones(25, dtype=int) + Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr() + + Crow = rand_perm2 + Ccol = np.arange(25) + Cdata = np.ones(25, dtype=int) + Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr() + # Randomly permute identity matrix + B = Rmat * A * Cmat + + # Row permute + perm = maximum_bipartite_matching(B, perm_type='row') + Rrow = np.arange(25) + Rcol = perm + Rdata = np.ones(25, dtype=int) + Rmat = coo_matrix((Rdata, (Rrow, Rcol))).tocsr() + C1 = Rmat * B + + # Column permute + perm2 = maximum_bipartite_matching(B, perm_type='column') + Crow = perm2 + Ccol = np.arange(25) + Cdata = np.ones(25, dtype=int) + Cmat = coo_matrix((Cdata, (Crow, Ccol))).tocsr() + C2 = B * Cmat + + # Should get identity matrix back + assert_equal(any(C1.diagonal() == 0), False) + assert_equal(any(C2.diagonal() == 0), False) + + +@pytest.mark.parametrize('num_rows,num_cols', [(0, 0), (2, 0), (0, 3)]) +def test_min_weight_full_matching_trivial_graph(num_rows, num_cols): + biadjacency_matrix = csr_matrix((num_cols, num_rows)) + row_ind, col_ind = min_weight_full_bipartite_matching(biadjacency_matrix) + assert len(row_ind) == 0 + assert len(col_ind) == 0 + + +@pytest.mark.parametrize('biadjacency_matrix', + [ + [[1, 1, 1], [1, 0, 0], [1, 0, 0]], + [[1, 1, 1], [0, 0, 1], [0, 0, 1]], + [[1, 0, 0, 1], [1, 1, 0, 1], [0, 0, 0, 0]], + [[1, 0, 0], [2, 0, 0]], + [[0, 1, 0], [0, 2, 0]], + [[1, 0], [2, 0], [5, 0]] + ]) +def test_min_weight_full_matching_infeasible_problems(biadjacency_matrix): + with pytest.raises(ValueError): + min_weight_full_bipartite_matching(csr_matrix(biadjacency_matrix)) + + +def test_min_weight_full_matching_large_infeasible(): + # Regression test for GitHub issue #17269 + a = np.asarray([ + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.001], + [0.0, 0.11687445, 0.0, 0.0, 0.01319788, 0.07509257, 0.0, + 0.0, 0.0, 0.74228317, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.81087935, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.8408466, 0.0, 0.0, 0.0, 0.0, 0.01194389, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.82994211, 0.0, 0.0, 0.0, 0.11468516, 0.0, 0.0, 0.0, + 0.11173505, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.0, 0.0], + [0.18796507, 0.0, 0.04002318, 0.0, 0.0, 0.0, 0.0, 0.0, 0.75883335, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.71545464, 0.0, 0.0, 0.0, 0.0, 0.0, 0.02748488, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.78470564, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.14829198, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.10870609, 0.0, 0.0, 0.0, 0.8918677, 0.0, 0.0, 0.0, 0.06306644, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, + 0.63844085, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.7442354, 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09850549, 0.0, 0.0, 0.18638258, + 0.2769244, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.73182464, 0.0, 0.0, 0.46443561, + 0.38589284, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.29510278, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.09666032, 0.0, + 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] + ]) + with pytest.raises(ValueError, match='no full matching exists'): + min_weight_full_bipartite_matching(csr_matrix(a)) + + +def test_explicit_zero_causes_warning(): + with pytest.warns(UserWarning): + biadjacency_matrix = csr_matrix(((2, 0, 3), (0, 1, 1), (0, 2, 3))) + min_weight_full_bipartite_matching(biadjacency_matrix) + + +# General test for linear sum assignment solvers to make it possible to rely +# on the same tests for scipy.optimize.linear_sum_assignment. +def linear_sum_assignment_assertions( + solver, array_type, sign, test_case +): + cost_matrix, expected_cost = test_case + maximize = sign == -1 + cost_matrix = sign * array_type(cost_matrix) + expected_cost = sign * np.array(expected_cost) + + row_ind, col_ind = solver(cost_matrix, maximize=maximize) + assert_array_equal(row_ind, np.sort(row_ind)) + assert_array_equal(expected_cost, + np.array(cost_matrix[row_ind, col_ind]).flatten()) + + cost_matrix = cost_matrix.T + row_ind, col_ind = solver(cost_matrix, maximize=maximize) + assert_array_equal(row_ind, np.sort(row_ind)) + assert_array_equal(np.sort(expected_cost), + np.sort(np.array( + cost_matrix[row_ind, col_ind])).flatten()) + + +linear_sum_assignment_test_cases = product( + [-1, 1], + [ + # Square + ([[400, 150, 400], + [400, 450, 600], + [300, 225, 300]], + [150, 400, 300]), + + # Rectangular variant + ([[400, 150, 400, 1], + [400, 450, 600, 2], + [300, 225, 300, 3]], + [150, 2, 300]), + + ([[10, 10, 8], + [9, 8, 1], + [9, 7, 4]], + [10, 1, 7]), + + # Square + ([[10, 10, 8, 11], + [9, 8, 1, 1], + [9, 7, 4, 10]], + [10, 1, 4]), + + # Rectangular variant + ([[10, float("inf"), float("inf")], + [float("inf"), float("inf"), 1], + [float("inf"), 7, float("inf")]], + [10, 1, 7]) + ]) + + +@pytest.mark.parametrize('sign,test_case', linear_sum_assignment_test_cases) +def test_min_weight_full_matching_small_inputs(sign, test_case): + linear_sum_assignment_assertions( + min_weight_full_bipartite_matching, csr_matrix, sign, test_case) diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_pydata_sparse.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_pydata_sparse.py new file mode 100644 index 0000000000000000000000000000000000000000..4def322c249b6cdc05b830a2317b17bfca3ef891 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_pydata_sparse.py @@ -0,0 +1,149 @@ +import pytest + +import numpy as np +import scipy.sparse as sp +import scipy.sparse.csgraph as spgraph + +from numpy.testing import assert_equal + +try: + import sparse +except Exception: + sparse = None + +pytestmark = pytest.mark.skipif(sparse is None, + reason="pydata/sparse not installed") + + +msg = "pydata/sparse (0.15.1) does not implement necessary operations" + + +sparse_params = (pytest.param("COO"), + pytest.param("DOK", marks=[pytest.mark.xfail(reason=msg)])) + + +@pytest.fixture(params=sparse_params) +def sparse_cls(request): + return getattr(sparse, request.param) + + +@pytest.fixture +def graphs(sparse_cls): + graph = [ + [0, 1, 1, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0, 0], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 0], + ] + A_dense = np.array(graph) + A_sparse = sparse_cls(A_dense) + return A_dense, A_sparse + + +@pytest.mark.parametrize( + "func", + [ + spgraph.shortest_path, + spgraph.dijkstra, + spgraph.floyd_warshall, + spgraph.bellman_ford, + spgraph.johnson, + spgraph.reverse_cuthill_mckee, + spgraph.maximum_bipartite_matching, + spgraph.structural_rank, + ] +) +def test_csgraph_equiv(func, graphs): + A_dense, A_sparse = graphs + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + assert_equal(actual, desired) + + +def test_connected_components(graphs): + A_dense, A_sparse = graphs + func = spgraph.connected_components + + actual_comp, actual_labels = func(A_sparse) + desired_comp, desired_labels, = func(sp.csc_matrix(A_dense)) + + assert actual_comp == desired_comp + assert_equal(actual_labels, desired_labels) + + +def test_laplacian(graphs): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + func = spgraph.laplacian + + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + + assert isinstance(actual, sparse_cls) + + assert_equal(actual.todense(), desired.todense()) + + +@pytest.mark.parametrize( + "func", [spgraph.breadth_first_order, spgraph.depth_first_order] +) +def test_order_search(graphs, func): + A_dense, A_sparse = graphs + + actual = func(A_sparse, 0) + desired = func(sp.csc_matrix(A_dense), 0) + + assert_equal(actual, desired) + + +@pytest.mark.parametrize( + "func", [spgraph.breadth_first_tree, spgraph.depth_first_tree] +) +def test_tree_search(graphs, func): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + + actual = func(A_sparse, 0) + desired = func(sp.csc_matrix(A_dense), 0) + + assert isinstance(actual, sparse_cls) + + assert_equal(actual.todense(), desired.todense()) + + +def test_minimum_spanning_tree(graphs): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + func = spgraph.minimum_spanning_tree + + actual = func(A_sparse) + desired = func(sp.csc_matrix(A_dense)) + + assert isinstance(actual, sparse_cls) + + assert_equal(actual.todense(), desired.todense()) + + +def test_maximum_flow(graphs): + A_dense, A_sparse = graphs + sparse_cls = type(A_sparse) + func = spgraph.maximum_flow + + actual = func(A_sparse, 0, 2) + desired = func(sp.csr_matrix(A_dense), 0, 2) + + assert actual.flow_value == desired.flow_value + assert isinstance(actual.flow, sparse_cls) + + assert_equal(actual.flow.todense(), desired.flow.todense()) + + +def test_min_weight_full_bipartite_matching(graphs): + A_dense, A_sparse = graphs + func = spgraph.min_weight_full_bipartite_matching + + actual = func(A_sparse[0:2, 1:3]) + desired = func(sp.csc_matrix(A_dense)[0:2, 1:3]) + + assert_equal(actual, desired) diff --git a/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_reordering.py b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_reordering.py new file mode 100644 index 0000000000000000000000000000000000000000..51dda43646af7b12119b6f4c14b8ee28aafbbe13 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/csgraph/tests/test_reordering.py @@ -0,0 +1,70 @@ +import numpy as np +from numpy.testing import assert_equal +from scipy.sparse.csgraph import reverse_cuthill_mckee, structural_rank +from scipy.sparse import csc_matrix, csr_matrix, coo_matrix + + +def test_graph_reverse_cuthill_mckee(): + A = np.array([[1, 0, 0, 0, 1, 0, 0, 0], + [0, 1, 1, 0, 0, 1, 0, 1], + [0, 1, 1, 0, 1, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 1, 0], + [1, 0, 1, 0, 1, 0, 0, 0], + [0, 1, 0, 0, 0, 1, 0, 1], + [0, 0, 0, 1, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 1, 0, 1]], dtype=int) + + graph = csr_matrix(A) + perm = reverse_cuthill_mckee(graph) + correct_perm = np.array([6, 3, 7, 5, 1, 2, 4, 0]) + assert_equal(perm, correct_perm) + + # Test int64 indices input + graph.indices = graph.indices.astype('int64') + graph.indptr = graph.indptr.astype('int64') + perm = reverse_cuthill_mckee(graph, True) + assert_equal(perm, correct_perm) + + +def test_graph_reverse_cuthill_mckee_ordering(): + data = np.ones(63,dtype=int) + rows = np.array([0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, + 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, + 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 8, 9, 9, + 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, + 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, + 14, 15, 15, 15, 15, 15]) + cols = np.array([0, 2, 5, 8, 10, 1, 3, 9, 11, 0, 2, + 7, 10, 1, 3, 11, 4, 6, 12, 14, 0, 7, 13, + 15, 4, 6, 14, 2, 5, 7, 15, 0, 8, 10, 13, + 1, 9, 11, 0, 2, 8, 10, 15, 1, 3, 9, 11, + 4, 12, 14, 5, 8, 13, 15, 4, 6, 12, 14, + 5, 7, 10, 13, 15]) + graph = coo_matrix((data, (rows,cols))).tocsr() + perm = reverse_cuthill_mckee(graph) + correct_perm = np.array([12, 14, 4, 6, 10, 8, 2, 15, + 0, 13, 7, 5, 9, 11, 1, 3]) + assert_equal(perm, correct_perm) + + +def test_graph_structural_rank(): + # Test square matrix #1 + A = csc_matrix([[1, 1, 0], + [1, 0, 1], + [0, 1, 0]]) + assert_equal(structural_rank(A), 3) + + # Test square matrix #2 + rows = np.array([0,0,0,0,0,1,1,2,2,3,3,3,3,3,3,4,4,5,5,6,6,7,7]) + cols = np.array([0,1,2,3,4,2,5,2,6,0,1,3,5,6,7,4,5,5,6,2,6,2,4]) + data = np.ones_like(rows) + B = coo_matrix((data,(rows,cols)), shape=(8,8)) + assert_equal(structural_rank(B), 6) + + #Test non-square matrix + C = csc_matrix([[1, 0, 2, 0], + [2, 0, 4, 0]]) + assert_equal(structural_rank(C), 2) + + #Test tall matrix + assert_equal(structural_rank(C.T), 2) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..84e3dfd002b0232c1de1839e01c970c2ffb16c8f --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/__init__.py @@ -0,0 +1,146 @@ +""" +Sparse linear algebra (:mod:`scipy.sparse.linalg`) +================================================== + +.. currentmodule:: scipy.sparse.linalg + +Abstract linear operators +------------------------- + +.. autosummary:: + :toctree: generated/ + + LinearOperator -- abstract representation of a linear operator + aslinearoperator -- convert an object to an abstract linear operator + +Matrix Operations +----------------- + +.. autosummary:: + :toctree: generated/ + + inv -- compute the sparse matrix inverse + expm -- compute the sparse matrix exponential + expm_multiply -- compute the product of a matrix exponential and a matrix + matrix_power -- compute the matrix power by raising a matrix to an exponent + +Matrix norms +------------ + +.. autosummary:: + :toctree: generated/ + + norm -- Norm of a sparse matrix + onenormest -- Estimate the 1-norm of a sparse matrix + +Solving linear problems +----------------------- + +Direct methods for linear equation systems: + +.. autosummary:: + :toctree: generated/ + + spsolve -- Solve the sparse linear system Ax=b + spsolve_triangular -- Solve sparse linear system Ax=b for a triangular A. + factorized -- Pre-factorize matrix to a function solving a linear system + MatrixRankWarning -- Warning on exactly singular matrices + use_solver -- Select direct solver to use + +Iterative methods for linear equation systems: + +.. autosummary:: + :toctree: generated/ + + bicg -- Use BIConjugate Gradient iteration to solve Ax = b + bicgstab -- Use BIConjugate Gradient STABilized iteration to solve Ax = b + cg -- Use Conjugate Gradient iteration to solve Ax = b + cgs -- Use Conjugate Gradient Squared iteration to solve Ax = b + gmres -- Use Generalized Minimal RESidual iteration to solve Ax = b + lgmres -- Solve a matrix equation using the LGMRES algorithm + minres -- Use MINimum RESidual iteration to solve Ax = b + qmr -- Use Quasi-Minimal Residual iteration to solve Ax = b + gcrotmk -- Solve a matrix equation using the GCROT(m,k) algorithm + tfqmr -- Use Transpose-Free Quasi-Minimal Residual iteration to solve Ax = b + +Iterative methods for least-squares problems: + +.. autosummary:: + :toctree: generated/ + + lsqr -- Find the least-squares solution to a sparse linear equation system + lsmr -- Find the least-squares solution to a sparse linear equation system + +Matrix factorizations +--------------------- + +Eigenvalue problems: + +.. autosummary:: + :toctree: generated/ + + eigs -- Find k eigenvalues and eigenvectors of the square matrix A + eigsh -- Find k eigenvalues and eigenvectors of a symmetric matrix + lobpcg -- Solve symmetric partial eigenproblems with optional preconditioning + +Singular values problems: + +.. autosummary:: + :toctree: generated/ + + svds -- Compute k singular values/vectors for a sparse matrix + +The `svds` function supports the following solvers: + +.. toctree:: + + sparse.linalg.svds-arpack + sparse.linalg.svds-lobpcg + sparse.linalg.svds-propack + +Complete or incomplete LU factorizations + +.. autosummary:: + :toctree: generated/ + + splu -- Compute a LU decomposition for a sparse matrix + spilu -- Compute an incomplete LU decomposition for a sparse matrix + SuperLU -- Object representing an LU factorization + +Sparse arrays with structure +---------------------------- + +.. autosummary:: + :toctree: generated/ + + LaplacianNd -- Laplacian on a uniform rectangular grid in ``N`` dimensions + +Exceptions +---------- + +.. autosummary:: + :toctree: generated/ + + ArpackNoConvergence + ArpackError + +""" + +from ._isolve import * +from ._dsolve import * +from ._interface import * +from ._eigen import * +from ._matfuncs import * +from ._onenormest import * +from ._norm import * +from ._expm_multiply import * +from ._special_sparse_arrays import * + +# Deprecated namespaces, to be removed in v2.0.0 +from . import isolve, dsolve, interface, eigen, matfuncs + +__all__ = [s for s in dir() if not s.startswith('_')] + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2b99d7e6358bf27e9e6ab3528e05b1d2625e3544 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__init__.py @@ -0,0 +1,71 @@ +""" +Linear Solvers +============== + +The default solver is SuperLU (included in the scipy distribution), +which can solve real or complex linear systems in both single and +double precisions. It is automatically replaced by UMFPACK, if +available. Note that UMFPACK works in double precision only, so +switch it off by:: + + >>> from scipy.sparse.linalg import spsolve, use_solver + >>> use_solver(useUmfpack=False) + +to solve in the single precision. See also use_solver documentation. + +Example session:: + + >>> from scipy.sparse import csc_matrix, spdiags + >>> from numpy import array + >>> + >>> print("Inverting a sparse linear system:") + >>> print("The sparse matrix (constructed from diagonals):") + >>> a = spdiags([[1, 2, 3, 4, 5], [6, 5, 8, 9, 10]], [0, 1], 5, 5) + >>> b = array([1, 2, 3, 4, 5]) + >>> print("Solve: single precision complex:") + >>> use_solver( useUmfpack = False ) + >>> a = a.astype('F') + >>> x = spsolve(a, b) + >>> print(x) + >>> print("Error: ", a@x-b) + >>> + >>> print("Solve: double precision complex:") + >>> use_solver( useUmfpack = True ) + >>> a = a.astype('D') + >>> x = spsolve(a, b) + >>> print(x) + >>> print("Error: ", a@x-b) + >>> + >>> print("Solve: double precision:") + >>> a = a.astype('d') + >>> x = spsolve(a, b) + >>> print(x) + >>> print("Error: ", a@x-b) + >>> + >>> print("Solve: single precision:") + >>> use_solver( useUmfpack = False ) + >>> a = a.astype('f') + >>> x = spsolve(a, b.astype('f')) + >>> print(x) + >>> print("Error: ", a@x-b) + +""" + +#import umfpack +#__doc__ = '\n\n'.join( (__doc__, umfpack.__doc__) ) +#del umfpack + +from .linsolve import * +from ._superlu import SuperLU +from . import _add_newdocs +from . import linsolve + +__all__ = [ + 'MatrixRankWarning', 'SuperLU', 'factorized', + 'spilu', 'splu', 'spsolve', + 'spsolve_triangular', 'use_solver' +] + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/__init__.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56b33f903e43f454f776f0986aabf84caab328ec Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/__init__.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/_add_newdocs.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/_add_newdocs.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ac4b29b5616fbf76b562881f99c15fd507ca1285 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/_add_newdocs.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/linsolve.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/linsolve.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3428bb5d42582f69c6f1e9c03746c07ac3544283 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/__pycache__/linsolve.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_add_newdocs.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_add_newdocs.py new file mode 100644 index 0000000000000000000000000000000000000000..f702d56d196563686ddaefd9a0cc6a018425c426 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_add_newdocs.py @@ -0,0 +1,153 @@ +from numpy.lib import add_newdoc + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', + """ + LU factorization of a sparse matrix. + + Factorization is represented as:: + + Pr @ A @ Pc = L @ U + + To construct these `SuperLU` objects, call the `splu` and `spilu` + functions. + + Attributes + ---------- + shape + nnz + perm_c + perm_r + L + U + + Methods + ------- + solve + + Notes + ----- + + .. versionadded:: 0.14.0 + + Examples + -------- + The LU decomposition can be used to solve matrix equations. Consider: + + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import splu + >>> A = csc_matrix([[1,2,0,4], [1,0,0,1], [1,0,2,1], [2,2,1,0.]]) + + This can be solved for a given right-hand side: + + >>> lu = splu(A) + >>> b = np.array([1, 2, 3, 4]) + >>> x = lu.solve(b) + >>> A.dot(x) + array([ 1., 2., 3., 4.]) + + The ``lu`` object also contains an explicit representation of the + decomposition. The permutations are represented as mappings of + indices: + + >>> lu.perm_r + array([2, 1, 3, 0], dtype=int32) # may vary + >>> lu.perm_c + array([0, 1, 3, 2], dtype=int32) # may vary + + The L and U factors are sparse matrices in CSC format: + + >>> lu.L.toarray() + array([[ 1. , 0. , 0. , 0. ], # may vary + [ 0.5, 1. , 0. , 0. ], + [ 0.5, -1. , 1. , 0. ], + [ 0.5, 1. , 0. , 1. ]]) + >>> lu.U.toarray() + array([[ 2. , 2. , 0. , 1. ], # may vary + [ 0. , -1. , 1. , -0.5], + [ 0. , 0. , 5. , -1. ], + [ 0. , 0. , 0. , 2. ]]) + + The permutation matrices can be constructed: + + >>> Pr = csc_matrix((np.ones(4), (lu.perm_r, np.arange(4)))) + >>> Pc = csc_matrix((np.ones(4), (np.arange(4), lu.perm_c))) + + We can reassemble the original matrix: + + >>> (Pr.T @ (lu.L @ lu.U) @ Pc.T).toarray() + array([[ 1., 2., 0., 4.], + [ 1., 0., 0., 1.], + [ 1., 0., 2., 1.], + [ 2., 2., 1., 0.]]) + """) + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('solve', + """ + solve(rhs[, trans]) + + Solves linear system of equations with one or several right-hand sides. + + Parameters + ---------- + rhs : ndarray, shape (n,) or (n, k) + Right hand side(s) of equation + trans : {'N', 'T', 'H'}, optional + Type of system to solve:: + + 'N': A @ x == rhs (default) + 'T': A^T @ x == rhs + 'H': A^H @ x == rhs + + i.e., normal, transposed, and hermitian conjugate. + + Returns + ------- + x : ndarray, shape ``rhs.shape`` + Solution vector(s) + """)) + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('L', + """ + Lower triangular factor with unit diagonal as a + `scipy.sparse.csc_matrix`. + + .. versionadded:: 0.14.0 + """)) + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('U', + """ + Upper triangular factor as a `scipy.sparse.csc_matrix`. + + .. versionadded:: 0.14.0 + """)) + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('shape', + """ + Shape of the original matrix as a tuple of ints. + """)) + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('nnz', + """ + Number of nonzero elements in the matrix. + """)) + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('perm_c', + """ + Permutation Pc represented as an array of indices. + + The column permutation matrix can be reconstructed via: + + >>> Pc = np.zeros((n, n)) + >>> Pc[np.arange(n), perm_c] = 1 + """)) + +add_newdoc('scipy.sparse.linalg._dsolve._superlu', 'SuperLU', ('perm_r', + """ + Permutation Pr represented as an array of indices. + + The row permutation matrix can be reconstructed via: + + >>> Pr = np.zeros((n, n)) + >>> Pr[perm_r, np.arange(n)] = 1 + """)) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_superlu.cp39-win_amd64.dll.a b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_superlu.cp39-win_amd64.dll.a new file mode 100644 index 0000000000000000000000000000000000000000..cb7e040ed73d2189715910d84afe45e72de44c7b Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_superlu.cp39-win_amd64.dll.a differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_superlu.cp39-win_amd64.pyd b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_superlu.cp39-win_amd64.pyd new file mode 100644 index 0000000000000000000000000000000000000000..fe30d4105a3718ce06aa68fd85353411aba18711 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/_superlu.cp39-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/linsolve.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/linsolve.py new file mode 100644 index 0000000000000000000000000000000000000000..dd0a43416f40eed54d026623a748b6fd69501060 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/linsolve.py @@ -0,0 +1,746 @@ +from warnings import warn + +import numpy as np +from numpy import asarray +from scipy.sparse import (issparse, + SparseEfficiencyWarning, csc_matrix, csr_matrix) +from scipy.sparse._sputils import is_pydata_spmatrix, convert_pydata_sparse_to_scipy +from scipy.linalg import LinAlgError +import copy + +from . import _superlu + +noScikit = False +try: + import scikits.umfpack as umfpack +except ImportError: + noScikit = True + +useUmfpack = not noScikit + +__all__ = ['use_solver', 'spsolve', 'splu', 'spilu', 'factorized', + 'MatrixRankWarning', 'spsolve_triangular'] + + +class MatrixRankWarning(UserWarning): + pass + + +def use_solver(**kwargs): + """ + Select default sparse direct solver to be used. + + Parameters + ---------- + useUmfpack : bool, optional + Use UMFPACK [1]_, [2]_, [3]_, [4]_. over SuperLU. Has effect only + if ``scikits.umfpack`` is installed. Default: True + assumeSortedIndices : bool, optional + Allow UMFPACK to skip the step of sorting indices for a CSR/CSC matrix. + Has effect only if useUmfpack is True and ``scikits.umfpack`` is + installed. Default: False + + Notes + ----- + The default sparse solver is UMFPACK when available + (``scikits.umfpack`` is installed). This can be changed by passing + useUmfpack = False, which then causes the always present SuperLU + based solver to be used. + + UMFPACK requires a CSR/CSC matrix to have sorted column/row indices. If + sure that the matrix fulfills this, pass ``assumeSortedIndices=True`` + to gain some speed. + + References + ---------- + .. [1] T. A. Davis, Algorithm 832: UMFPACK - an unsymmetric-pattern + multifrontal method with a column pre-ordering strategy, ACM + Trans. on Mathematical Software, 30(2), 2004, pp. 196--199. + https://dl.acm.org/doi/abs/10.1145/992200.992206 + + .. [2] T. A. Davis, A column pre-ordering strategy for the + unsymmetric-pattern multifrontal method, ACM Trans. + on Mathematical Software, 30(2), 2004, pp. 165--195. + https://dl.acm.org/doi/abs/10.1145/992200.992205 + + .. [3] T. A. Davis and I. S. Duff, A combined unifrontal/multifrontal + method for unsymmetric sparse matrices, ACM Trans. on + Mathematical Software, 25(1), 1999, pp. 1--19. + https://doi.org/10.1145/305658.287640 + + .. [4] T. A. Davis and I. S. Duff, An unsymmetric-pattern multifrontal + method for sparse LU factorization, SIAM J. Matrix Analysis and + Computations, 18(1), 1997, pp. 140--158. + https://doi.org/10.1137/S0895479894246905T. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg import use_solver, spsolve + >>> from scipy.sparse import csc_matrix + >>> R = np.random.randn(5, 5) + >>> A = csc_matrix(R) + >>> b = np.random.randn(5) + >>> use_solver(useUmfpack=False) # enforce superLU over UMFPACK + >>> x = spsolve(A, b) + >>> np.allclose(A.dot(x), b) + True + >>> use_solver(useUmfpack=True) # reset umfPack usage to default + """ + if 'useUmfpack' in kwargs: + globals()['useUmfpack'] = kwargs['useUmfpack'] + if useUmfpack and 'assumeSortedIndices' in kwargs: + umfpack.configure(assumeSortedIndices=kwargs['assumeSortedIndices']) + +def _get_umf_family(A): + """Get umfpack family string given the sparse matrix dtype.""" + _families = { + (np.float64, np.int32): 'di', + (np.complex128, np.int32): 'zi', + (np.float64, np.int64): 'dl', + (np.complex128, np.int64): 'zl' + } + + # A.dtype.name can only be "float64" or + # "complex128" in control flow + f_type = getattr(np, A.dtype.name) + # control flow may allow for more index + # types to get through here + i_type = getattr(np, A.indices.dtype.name) + + try: + family = _families[(f_type, i_type)] + + except KeyError as e: + msg = ('only float64 or complex128 matrices with int32 or int64 ' + f'indices are supported! (got: matrix: {f_type}, indices: {i_type})') + raise ValueError(msg) from e + + # See gh-8278. Considered converting only if + # A.shape[0]*A.shape[1] > np.iinfo(np.int32).max, + # but that didn't always fix the issue. + family = family[0] + "l" + A_new = copy.copy(A) + A_new.indptr = np.asarray(A.indptr, dtype=np.int64) + A_new.indices = np.asarray(A.indices, dtype=np.int64) + + return family, A_new + +def _safe_downcast_indices(A): + # check for safe downcasting + max_value = np.iinfo(np.intc).max + + if A.indptr[-1] > max_value: # indptr[-1] is max b/c indptr always sorted + raise ValueError("indptr values too large for SuperLU") + + if max(*A.shape) > max_value: # only check large enough arrays + if np.any(A.indices > max_value): + raise ValueError("indices values too large for SuperLU") + + indices = A.indices.astype(np.intc, copy=False) + indptr = A.indptr.astype(np.intc, copy=False) + return indices, indptr + +def spsolve(A, b, permc_spec=None, use_umfpack=True): + """Solve the sparse linear system Ax=b, where b may be a vector or a matrix. + + Parameters + ---------- + A : ndarray or sparse matrix + The square matrix A will be converted into CSC or CSR form + b : ndarray or sparse matrix + The matrix or vector representing the right hand side of the equation. + If a vector, b.shape must be (n,) or (n, 1). + permc_spec : str, optional + How to permute the columns of the matrix for sparsity preservation. + (default: 'COLAMD') + + - ``NATURAL``: natural ordering. + - ``MMD_ATA``: minimum degree ordering on the structure of A^T A. + - ``MMD_AT_PLUS_A``: minimum degree ordering on the structure of A^T+A. + - ``COLAMD``: approximate minimum degree column ordering [1]_, [2]_. + + use_umfpack : bool, optional + if True (default) then use UMFPACK for the solution [3]_, [4]_, [5]_, + [6]_ . This is only referenced if b is a vector and + ``scikits.umfpack`` is installed. + + Returns + ------- + x : ndarray or sparse matrix + the solution of the sparse linear equation. + If b is a vector, then x is a vector of size A.shape[1] + If b is a matrix, then x is a matrix of size (A.shape[1], b.shape[1]) + + Notes + ----- + For solving the matrix expression AX = B, this solver assumes the resulting + matrix X is sparse, as is often the case for very sparse inputs. If the + resulting X is dense, the construction of this sparse result will be + relatively expensive. In that case, consider converting A to a dense + matrix and using scipy.linalg.solve or its variants. + + References + ---------- + .. [1] T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, Algorithm 836: + COLAMD, an approximate column minimum degree ordering algorithm, + ACM Trans. on Mathematical Software, 30(3), 2004, pp. 377--380. + :doi:`10.1145/1024074.1024080` + + .. [2] T. A. Davis, J. R. Gilbert, S. Larimore, E. Ng, A column approximate + minimum degree ordering algorithm, ACM Trans. on Mathematical + Software, 30(3), 2004, pp. 353--376. :doi:`10.1145/1024074.1024079` + + .. [3] T. A. Davis, Algorithm 832: UMFPACK - an unsymmetric-pattern + multifrontal method with a column pre-ordering strategy, ACM + Trans. on Mathematical Software, 30(2), 2004, pp. 196--199. + https://dl.acm.org/doi/abs/10.1145/992200.992206 + + .. [4] T. A. Davis, A column pre-ordering strategy for the + unsymmetric-pattern multifrontal method, ACM Trans. + on Mathematical Software, 30(2), 2004, pp. 165--195. + https://dl.acm.org/doi/abs/10.1145/992200.992205 + + .. [5] T. A. Davis and I. S. Duff, A combined unifrontal/multifrontal + method for unsymmetric sparse matrices, ACM Trans. on + Mathematical Software, 25(1), 1999, pp. 1--19. + https://doi.org/10.1145/305658.287640 + + .. [6] T. A. Davis and I. S. Duff, An unsymmetric-pattern multifrontal + method for sparse LU factorization, SIAM J. Matrix Analysis and + Computations, 18(1), 1997, pp. 140--158. + https://doi.org/10.1137/S0895479894246905T. + + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import spsolve + >>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float) + >>> B = csc_matrix([[2, 0], [-1, 0], [2, 0]], dtype=float) + >>> x = spsolve(A, B) + >>> np.allclose(A.dot(x).toarray(), B.toarray()) + True + """ + is_pydata_sparse = is_pydata_spmatrix(b) + pydata_sparse_cls = b.__class__ if is_pydata_sparse else None + A = convert_pydata_sparse_to_scipy(A) + b = convert_pydata_sparse_to_scipy(b) + + if not (issparse(A) and A.format in ("csc", "csr")): + A = csc_matrix(A) + warn('spsolve requires A be CSC or CSR matrix format', + SparseEfficiencyWarning, stacklevel=2) + + # b is a vector only if b have shape (n,) or (n, 1) + b_is_sparse = issparse(b) + if not b_is_sparse: + b = asarray(b) + b_is_vector = ((b.ndim == 1) or (b.ndim == 2 and b.shape[1] == 1)) + + # sum duplicates for non-canonical format + A.sum_duplicates() + A = A._asfptype() # upcast to a floating point format + result_dtype = np.promote_types(A.dtype, b.dtype) + if A.dtype != result_dtype: + A = A.astype(result_dtype) + if b.dtype != result_dtype: + b = b.astype(result_dtype) + + # validate input shapes + M, N = A.shape + if (M != N): + raise ValueError(f"matrix must be square (has shape {(M, N)})") + + if M != b.shape[0]: + raise ValueError(f"matrix - rhs dimension mismatch ({A.shape} - {b.shape[0]})") + + use_umfpack = use_umfpack and useUmfpack + + if b_is_vector and use_umfpack: + if b_is_sparse: + b_vec = b.toarray() + else: + b_vec = b + b_vec = asarray(b_vec, dtype=A.dtype).ravel() + + if noScikit: + raise RuntimeError('Scikits.umfpack not installed.') + + if A.dtype.char not in 'dD': + raise ValueError("convert matrix data to double, please, using" + " .astype(), or set linsolve.useUmfpack = False") + + umf_family, A = _get_umf_family(A) + umf = umfpack.UmfpackContext(umf_family) + x = umf.linsolve(umfpack.UMFPACK_A, A, b_vec, + autoTranspose=True) + else: + if b_is_vector and b_is_sparse: + b = b.toarray() + b_is_sparse = False + + if not b_is_sparse: + if A.format == "csc": + flag = 1 # CSC format + else: + flag = 0 # CSR format + + indices = A.indices.astype(np.intc, copy=False) + indptr = A.indptr.astype(np.intc, copy=False) + options = dict(ColPerm=permc_spec) + x, info = _superlu.gssv(N, A.nnz, A.data, indices, indptr, + b, flag, options=options) + if info != 0: + warn("Matrix is exactly singular", MatrixRankWarning, stacklevel=2) + x.fill(np.nan) + if b_is_vector: + x = x.ravel() + else: + # b is sparse + Afactsolve = factorized(A) + + if not (b.format == "csc" or is_pydata_spmatrix(b)): + warn('spsolve is more efficient when sparse b ' + 'is in the CSC matrix format', + SparseEfficiencyWarning, stacklevel=2) + b = csc_matrix(b) + + # Create a sparse output matrix by repeatedly applying + # the sparse factorization to solve columns of b. + data_segs = [] + row_segs = [] + col_segs = [] + for j in range(b.shape[1]): + # TODO: replace this with + # bj = b[:, j].toarray().ravel() + # once 1D sparse arrays are supported. + # That is a slightly faster code path. + bj = b[:, [j]].toarray().ravel() + xj = Afactsolve(bj) + w = np.flatnonzero(xj) + segment_length = w.shape[0] + row_segs.append(w) + col_segs.append(np.full(segment_length, j, dtype=int)) + data_segs.append(np.asarray(xj[w], dtype=A.dtype)) + sparse_data = np.concatenate(data_segs) + sparse_row = np.concatenate(row_segs) + sparse_col = np.concatenate(col_segs) + x = A.__class__((sparse_data, (sparse_row, sparse_col)), + shape=b.shape, dtype=A.dtype) + + if is_pydata_sparse: + x = pydata_sparse_cls.from_scipy_sparse(x) + + return x + + +def splu(A, permc_spec=None, diag_pivot_thresh=None, + relax=None, panel_size=None, options=dict()): + """ + Compute the LU decomposition of a sparse, square matrix. + + Parameters + ---------- + A : sparse matrix + Sparse matrix to factorize. Most efficient when provided in CSC + format. Other formats will be converted to CSC before factorization. + permc_spec : str, optional + How to permute the columns of the matrix for sparsity preservation. + (default: 'COLAMD') + + - ``NATURAL``: natural ordering. + - ``MMD_ATA``: minimum degree ordering on the structure of A^T A. + - ``MMD_AT_PLUS_A``: minimum degree ordering on the structure of A^T+A. + - ``COLAMD``: approximate minimum degree column ordering + + diag_pivot_thresh : float, optional + Threshold used for a diagonal entry to be an acceptable pivot. + See SuperLU user's guide for details [1]_ + relax : int, optional + Expert option for customizing the degree of relaxing supernodes. + See SuperLU user's guide for details [1]_ + panel_size : int, optional + Expert option for customizing the panel size. + See SuperLU user's guide for details [1]_ + options : dict, optional + Dictionary containing additional expert options to SuperLU. + See SuperLU user guide [1]_ (section 2.4 on the 'Options' argument) + for more details. For example, you can specify + ``options=dict(Equil=False, IterRefine='SINGLE'))`` + to turn equilibration off and perform a single iterative refinement. + + Returns + ------- + invA : scipy.sparse.linalg.SuperLU + Object, which has a ``solve`` method. + + See also + -------- + spilu : incomplete LU decomposition + + Notes + ----- + This function uses the SuperLU library. + + References + ---------- + .. [1] SuperLU https://portal.nersc.gov/project/sparse/superlu/ + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import splu + >>> A = csc_matrix([[1., 0., 0.], [5., 0., 2.], [0., -1., 0.]], dtype=float) + >>> B = splu(A) + >>> x = np.array([1., 2., 3.], dtype=float) + >>> B.solve(x) + array([ 1. , -3. , -1.5]) + >>> A.dot(B.solve(x)) + array([ 1., 2., 3.]) + >>> B.solve(A.dot(x)) + array([ 1., 2., 3.]) + """ + + if is_pydata_spmatrix(A): + def csc_construct_func(*a, cls=type(A)): + return cls.from_scipy_sparse(csc_matrix(*a)) + A = A.to_scipy_sparse().tocsc() + else: + csc_construct_func = csc_matrix + + if not (issparse(A) and A.format == "csc"): + A = csc_matrix(A) + warn('splu converted its input to CSC format', + SparseEfficiencyWarning, stacklevel=2) + + # sum duplicates for non-canonical format + A.sum_duplicates() + A = A._asfptype() # upcast to a floating point format + + M, N = A.shape + if (M != N): + raise ValueError("can only factor square matrices") # is this true? + + indices, indptr = _safe_downcast_indices(A) + + _options = dict(DiagPivotThresh=diag_pivot_thresh, ColPerm=permc_spec, + PanelSize=panel_size, Relax=relax) + if options is not None: + _options.update(options) + + # Ensure that no column permutations are applied + if (_options["ColPerm"] == "NATURAL"): + _options["SymmetricMode"] = True + + return _superlu.gstrf(N, A.nnz, A.data, indices, indptr, + csc_construct_func=csc_construct_func, + ilu=False, options=_options) + + +def spilu(A, drop_tol=None, fill_factor=None, drop_rule=None, permc_spec=None, + diag_pivot_thresh=None, relax=None, panel_size=None, options=None): + """ + Compute an incomplete LU decomposition for a sparse, square matrix. + + The resulting object is an approximation to the inverse of `A`. + + Parameters + ---------- + A : (N, N) array_like + Sparse matrix to factorize. Most efficient when provided in CSC format. + Other formats will be converted to CSC before factorization. + drop_tol : float, optional + Drop tolerance (0 <= tol <= 1) for an incomplete LU decomposition. + (default: 1e-4) + fill_factor : float, optional + Specifies the fill ratio upper bound (>= 1.0) for ILU. (default: 10) + drop_rule : str, optional + Comma-separated string of drop rules to use. + Available rules: ``basic``, ``prows``, ``column``, ``area``, + ``secondary``, ``dynamic``, ``interp``. (Default: ``basic,area``) + + See SuperLU documentation for details. + + Remaining other options + Same as for `splu` + + Returns + ------- + invA_approx : scipy.sparse.linalg.SuperLU + Object, which has a ``solve`` method. + + See also + -------- + splu : complete LU decomposition + + Notes + ----- + To improve the better approximation to the inverse, you may need to + increase `fill_factor` AND decrease `drop_tol`. + + This function uses the SuperLU library. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import spilu + >>> A = csc_matrix([[1., 0., 0.], [5., 0., 2.], [0., -1., 0.]], dtype=float) + >>> B = spilu(A) + >>> x = np.array([1., 2., 3.], dtype=float) + >>> B.solve(x) + array([ 1. , -3. , -1.5]) + >>> A.dot(B.solve(x)) + array([ 1., 2., 3.]) + >>> B.solve(A.dot(x)) + array([ 1., 2., 3.]) + """ + + if is_pydata_spmatrix(A): + def csc_construct_func(*a, cls=type(A)): + return cls.from_scipy_sparse(csc_matrix(*a)) + A = A.to_scipy_sparse().tocsc() + else: + csc_construct_func = csc_matrix + + if not (issparse(A) and A.format == "csc"): + A = csc_matrix(A) + warn('spilu converted its input to CSC format', + SparseEfficiencyWarning, stacklevel=2) + + # sum duplicates for non-canonical format + A.sum_duplicates() + A = A._asfptype() # upcast to a floating point format + + M, N = A.shape + if (M != N): + raise ValueError("can only factor square matrices") # is this true? + + indices, indptr = _safe_downcast_indices(A) + + _options = dict(ILU_DropRule=drop_rule, ILU_DropTol=drop_tol, + ILU_FillFactor=fill_factor, + DiagPivotThresh=diag_pivot_thresh, ColPerm=permc_spec, + PanelSize=panel_size, Relax=relax) + if options is not None: + _options.update(options) + + # Ensure that no column permutations are applied + if (_options["ColPerm"] == "NATURAL"): + _options["SymmetricMode"] = True + + return _superlu.gstrf(N, A.nnz, A.data, indices, indptr, + csc_construct_func=csc_construct_func, + ilu=True, options=_options) + + +def factorized(A): + """ + Return a function for solving a sparse linear system, with A pre-factorized. + + Parameters + ---------- + A : (N, N) array_like + Input. A in CSC format is most efficient. A CSR format matrix will + be converted to CSC before factorization. + + Returns + ------- + solve : callable + To solve the linear system of equations given in `A`, the `solve` + callable should be passed an ndarray of shape (N,). + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg import factorized + >>> from scipy.sparse import csc_matrix + >>> A = np.array([[ 3. , 2. , -1. ], + ... [ 2. , -2. , 4. ], + ... [-1. , 0.5, -1. ]]) + >>> solve = factorized(csc_matrix(A)) # Makes LU decomposition. + >>> rhs1 = np.array([1, -2, 0]) + >>> solve(rhs1) # Uses the LU factors. + array([ 1., -2., -2.]) + + """ + if is_pydata_spmatrix(A): + A = A.to_scipy_sparse().tocsc() + + if useUmfpack: + if noScikit: + raise RuntimeError('Scikits.umfpack not installed.') + + if not (issparse(A) and A.format == "csc"): + A = csc_matrix(A) + warn('splu converted its input to CSC format', + SparseEfficiencyWarning, stacklevel=2) + + A = A._asfptype() # upcast to a floating point format + + if A.dtype.char not in 'dD': + raise ValueError("convert matrix data to double, please, using" + " .astype(), or set linsolve.useUmfpack = False") + + umf_family, A = _get_umf_family(A) + umf = umfpack.UmfpackContext(umf_family) + + # Make LU decomposition. + umf.numeric(A) + + def solve(b): + with np.errstate(divide="ignore", invalid="ignore"): + # Ignoring warnings with numpy >= 1.23.0, see gh-16523 + result = umf.solve(umfpack.UMFPACK_A, A, b, autoTranspose=True) + + return result + + return solve + else: + return splu(A).solve + + +def spsolve_triangular(A, b, lower=True, overwrite_A=False, overwrite_b=False, + unit_diagonal=False): + """ + Solve the equation ``A x = b`` for `x`, assuming A is a triangular matrix. + + Parameters + ---------- + A : (M, M) sparse matrix + A sparse square triangular matrix. Should be in CSR format. + b : (M,) or (M, N) array_like + Right-hand side matrix in ``A x = b`` + lower : bool, optional + Whether `A` is a lower or upper triangular matrix. + Default is lower triangular matrix. + overwrite_A : bool, optional + Allow changing `A`. The indices of `A` are going to be sorted and zero + entries are going to be removed. + Enabling gives a performance gain. Default is False. + overwrite_b : bool, optional + Allow overwriting data in `b`. + Enabling gives a performance gain. Default is False. + If `overwrite_b` is True, it should be ensured that + `b` has an appropriate dtype to be able to store the result. + unit_diagonal : bool, optional + If True, diagonal elements of `a` are assumed to be 1 and will not be + referenced. + + .. versionadded:: 1.4.0 + + Returns + ------- + x : (M,) or (M, N) ndarray + Solution to the system ``A x = b``. Shape of return matches shape + of `b`. + + Raises + ------ + LinAlgError + If `A` is singular or not triangular. + ValueError + If shape of `A` or shape of `b` do not match the requirements. + + Notes + ----- + .. versionadded:: 0.19.0 + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csr_matrix + >>> from scipy.sparse.linalg import spsolve_triangular + >>> A = csr_matrix([[3, 0, 0], [1, -1, 0], [2, 0, 1]], dtype=float) + >>> B = np.array([[2, 0], [-1, 0], [2, 0]], dtype=float) + >>> x = spsolve_triangular(A, B) + >>> np.allclose(A.dot(x), B) + True + """ + + if is_pydata_spmatrix(A): + A = A.to_scipy_sparse().tocsr() + + # Check the input for correct type and format. + if not (issparse(A) and A.format == "csr"): + warn('CSR matrix format is required. Converting to CSR matrix.', + SparseEfficiencyWarning, stacklevel=2) + A = csr_matrix(A) + elif not overwrite_A: + A = A.copy() + + if A.shape[0] != A.shape[1]: + raise ValueError( + f'A must be a square matrix but its shape is {A.shape}.') + + # sum duplicates for non-canonical format + A.sum_duplicates() + + b = np.asanyarray(b) + + if b.ndim not in [1, 2]: + raise ValueError( + f'b must have 1 or 2 dims but its shape is {b.shape}.') + if A.shape[0] != b.shape[0]: + raise ValueError( + 'The size of the dimensions of A must be equal to ' + 'the size of the first dimension of b but the shape of A is ' + f'{A.shape} and the shape of b is {b.shape}.' + ) + + # Init x as (a copy of) b. + x_dtype = np.result_type(A.data, b, np.float64) + if overwrite_b: + if np.can_cast(b.dtype, x_dtype, casting='same_kind'): + x = b + else: + raise ValueError( + f'Cannot overwrite b (dtype {b.dtype}) with result ' + f'of type {x_dtype}.' + ) + else: + x = b.astype(x_dtype, copy=True) + + # Choose forward or backward order. + if lower: + row_indices = range(len(b)) + else: + row_indices = range(len(b) - 1, -1, -1) + + # Fill x iteratively. + for i in row_indices: + + # Get indices for i-th row. + indptr_start = A.indptr[i] + indptr_stop = A.indptr[i + 1] + + if lower: + A_diagonal_index_row_i = indptr_stop - 1 + A_off_diagonal_indices_row_i = slice(indptr_start, indptr_stop - 1) + else: + A_diagonal_index_row_i = indptr_start + A_off_diagonal_indices_row_i = slice(indptr_start + 1, indptr_stop) + + # Check regularity and triangularity of A. + if not unit_diagonal and (indptr_stop <= indptr_start + or A.indices[A_diagonal_index_row_i] < i): + raise LinAlgError( + f'A is singular: diagonal {i} is zero.') + if not unit_diagonal and A.indices[A_diagonal_index_row_i] > i: + raise LinAlgError( + 'A is not triangular: A[{}, {}] is nonzero.' + ''.format(i, A.indices[A_diagonal_index_row_i])) + + # Incorporate off-diagonal entries. + A_column_indices_in_row_i = A.indices[A_off_diagonal_indices_row_i] + A_values_in_row_i = A.data[A_off_diagonal_indices_row_i] + x[i] -= np.dot(x[A_column_indices_in_row_i].T, A_values_in_row_i) + + # Compute i-th entry of x. + if not unit_diagonal: + x[i] /= A.data[A_diagonal_index_row_i] + + return x diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/tests/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/tests/test_linsolve.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/tests/test_linsolve.py new file mode 100644 index 0000000000000000000000000000000000000000..e73aebd9fadc9c33629383c92004c406e66dbc27 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_dsolve/tests/test_linsolve.py @@ -0,0 +1,805 @@ +import sys +import threading + +import numpy as np +from numpy import array, finfo, arange, eye, all, unique, ones, dot +import numpy.random as random +from numpy.testing import ( + assert_array_almost_equal, assert_almost_equal, + assert_equal, assert_array_equal, assert_, assert_allclose, + assert_warns, suppress_warnings) +import pytest +from pytest import raises as assert_raises + +import scipy.linalg +from scipy.linalg import norm, inv +from scipy.sparse import (spdiags, SparseEfficiencyWarning, csc_matrix, + csr_matrix, identity, issparse, dok_matrix, lil_matrix, bsr_matrix) +from scipy.sparse.linalg import SuperLU +from scipy.sparse.linalg._dsolve import (spsolve, use_solver, splu, spilu, + MatrixRankWarning, _superlu, spsolve_triangular, factorized) +import scipy.sparse + +from scipy._lib._testutils import check_free_memory +from scipy._lib._util import ComplexWarning + + +sup_sparse_efficiency = suppress_warnings() +sup_sparse_efficiency.filter(SparseEfficiencyWarning) + +# scikits.umfpack is not a SciPy dependency but it is optionally used in +# dsolve, so check whether it's available +try: + import scikits.umfpack as umfpack + has_umfpack = True +except ImportError: + has_umfpack = False + +def toarray(a): + if issparse(a): + return a.toarray() + else: + return a + + +def setup_bug_8278(): + N = 2 ** 6 + h = 1/N + Ah1D = scipy.sparse.diags([-1, 2, -1], [-1, 0, 1], + shape=(N-1, N-1))/(h**2) + eyeN = scipy.sparse.eye(N - 1) + A = (scipy.sparse.kron(eyeN, scipy.sparse.kron(eyeN, Ah1D)) + + scipy.sparse.kron(eyeN, scipy.sparse.kron(Ah1D, eyeN)) + + scipy.sparse.kron(Ah1D, scipy.sparse.kron(eyeN, eyeN))) + b = np.random.rand((N-1)**3) + return A, b + + +class TestFactorized: + def setup_method(self): + n = 5 + d = arange(n) + 1 + self.n = n + self.A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), n, n).tocsc() + random.seed(1234) + + def _check_singular(self): + A = csc_matrix((5,5), dtype='d') + b = ones(5) + assert_array_almost_equal(0. * b, factorized(A)(b)) + + def _check_non_singular(self): + # Make a diagonal dominant, to make sure it is not singular + n = 5 + a = csc_matrix(random.rand(n, n)) + b = ones(n) + + expected = splu(a).solve(b) + assert_array_almost_equal(factorized(a)(b), expected) + + def test_singular_without_umfpack(self): + use_solver(useUmfpack=False) + with assert_raises(RuntimeError, match="Factor is exactly singular"): + self._check_singular() + + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_singular_with_umfpack(self): + use_solver(useUmfpack=True) + with suppress_warnings() as sup: + sup.filter(RuntimeWarning, "divide by zero encountered in double_scalars") + assert_warns(umfpack.UmfpackWarning, self._check_singular) + + def test_non_singular_without_umfpack(self): + use_solver(useUmfpack=False) + self._check_non_singular() + + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_non_singular_with_umfpack(self): + use_solver(useUmfpack=True) + self._check_non_singular() + + def test_cannot_factorize_nonsquare_matrix_without_umfpack(self): + use_solver(useUmfpack=False) + msg = "can only factor square matrices" + with assert_raises(ValueError, match=msg): + factorized(self.A[:, :4]) + + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_factorizes_nonsquare_matrix_with_umfpack(self): + use_solver(useUmfpack=True) + # does not raise + factorized(self.A[:,:4]) + + def test_call_with_incorrectly_sized_matrix_without_umfpack(self): + use_solver(useUmfpack=False) + solve = factorized(self.A) + b = random.rand(4) + B = random.rand(4, 3) + BB = random.rand(self.n, 3, 9) + + with assert_raises(ValueError, match="is of incompatible size"): + solve(b) + with assert_raises(ValueError, match="is of incompatible size"): + solve(B) + with assert_raises(ValueError, + match="object too deep for desired array"): + solve(BB) + + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_call_with_incorrectly_sized_matrix_with_umfpack(self): + use_solver(useUmfpack=True) + solve = factorized(self.A) + b = random.rand(4) + B = random.rand(4, 3) + BB = random.rand(self.n, 3, 9) + + # does not raise + solve(b) + msg = "object too deep for desired array" + with assert_raises(ValueError, match=msg): + solve(B) + with assert_raises(ValueError, match=msg): + solve(BB) + + def test_call_with_cast_to_complex_without_umfpack(self): + use_solver(useUmfpack=False) + solve = factorized(self.A) + b = random.rand(4) + for t in [np.complex64, np.complex128]: + with assert_raises(TypeError, match="Cannot cast array data"): + solve(b.astype(t)) + + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_call_with_cast_to_complex_with_umfpack(self): + use_solver(useUmfpack=True) + solve = factorized(self.A) + b = random.rand(4) + for t in [np.complex64, np.complex128]: + assert_warns(ComplexWarning, solve, b.astype(t)) + + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_assume_sorted_indices_flag(self): + # a sparse matrix with unsorted indices + unsorted_inds = np.array([2, 0, 1, 0]) + data = np.array([10, 16, 5, 0.4]) + indptr = np.array([0, 1, 2, 4]) + A = csc_matrix((data, unsorted_inds, indptr), (3, 3)) + b = ones(3) + + # should raise when incorrectly assuming indices are sorted + use_solver(useUmfpack=True, assumeSortedIndices=True) + with assert_raises(RuntimeError, + match="UMFPACK_ERROR_invalid_matrix"): + factorized(A) + + # should sort indices and succeed when not assuming indices are sorted + use_solver(useUmfpack=True, assumeSortedIndices=False) + expected = splu(A.copy()).solve(b) + + assert_equal(A.has_sorted_indices, 0) + assert_array_almost_equal(factorized(A)(b), expected) + + @pytest.mark.slow + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_bug_8278(self): + check_free_memory(8000) + use_solver(useUmfpack=True) + A, b = setup_bug_8278() + A = A.tocsc() + f = factorized(A) + x = f(b) + assert_array_almost_equal(A @ x, b) + + +class TestLinsolve: + def setup_method(self): + use_solver(useUmfpack=False) + + def test_singular(self): + A = csc_matrix((5,5), dtype='d') + b = array([1, 2, 3, 4, 5],dtype='d') + with suppress_warnings() as sup: + sup.filter(MatrixRankWarning, "Matrix is exactly singular") + x = spsolve(A, b) + assert_(not np.isfinite(x).any()) + + def test_singular_gh_3312(self): + # "Bad" test case that leads SuperLU to call LAPACK with invalid + # arguments. Check that it fails moderately gracefully. + ij = np.array([(17, 0), (17, 6), (17, 12), (10, 13)], dtype=np.int32) + v = np.array([0.284213, 0.94933781, 0.15767017, 0.38797296]) + A = csc_matrix((v, ij.T), shape=(20, 20)) + b = np.arange(20) + + try: + # should either raise a runtime error or return value + # appropriate for singular input (which yields the warning) + with suppress_warnings() as sup: + sup.filter(MatrixRankWarning, "Matrix is exactly singular") + x = spsolve(A, b) + assert not np.isfinite(x).any() + except RuntimeError: + pass + + @pytest.mark.parametrize('format', ['csc', 'csr']) + @pytest.mark.parametrize('idx_dtype', [np.int32, np.int64]) + def test_twodiags(self, format: str, idx_dtype: np.dtype): + A = spdiags([[1, 2, 3, 4, 5], [6, 5, 8, 9, 10]], [0, 1], 5, 5, + format=format) + b = array([1, 2, 3, 4, 5]) + + # condition number of A + cond_A = norm(A.toarray(), 2) * norm(inv(A.toarray()), 2) + + for t in ['f','d','F','D']: + eps = finfo(t).eps # floating point epsilon + b = b.astype(t) + Asp = A.astype(t) + Asp.indices = Asp.indices.astype(idx_dtype, copy=False) + Asp.indptr = Asp.indptr.astype(idx_dtype, copy=False) + + x = spsolve(Asp, b) + assert_(norm(b - Asp@x) < 10 * cond_A * eps) + + def test_bvector_smoketest(self): + Adense = array([[0., 1., 1.], + [1., 0., 1.], + [0., 0., 1.]]) + As = csc_matrix(Adense) + random.seed(1234) + x = random.randn(3) + b = As@x + x2 = spsolve(As, b) + + assert_array_almost_equal(x, x2) + + def test_bmatrix_smoketest(self): + Adense = array([[0., 1., 1.], + [1., 0., 1.], + [0., 0., 1.]]) + As = csc_matrix(Adense) + random.seed(1234) + x = random.randn(3, 4) + Bdense = As.dot(x) + Bs = csc_matrix(Bdense) + x2 = spsolve(As, Bs) + assert_array_almost_equal(x, x2.toarray()) + + @sup_sparse_efficiency + def test_non_square(self): + # A is not square. + A = ones((3, 4)) + b = ones((4, 1)) + assert_raises(ValueError, spsolve, A, b) + # A2 and b2 have incompatible shapes. + A2 = csc_matrix(eye(3)) + b2 = array([1.0, 2.0]) + assert_raises(ValueError, spsolve, A2, b2) + + @sup_sparse_efficiency + def test_example_comparison(self): + row = array([0,0,1,2,2,2]) + col = array([0,2,2,0,1,2]) + data = array([1,2,3,-4,5,6]) + sM = csr_matrix((data,(row,col)), shape=(3,3), dtype=float) + M = sM.toarray() + + row = array([0,0,1,1,0,0]) + col = array([0,2,1,1,0,0]) + data = array([1,1,1,1,1,1]) + sN = csr_matrix((data, (row,col)), shape=(3,3), dtype=float) + N = sN.toarray() + + sX = spsolve(sM, sN) + X = scipy.linalg.solve(M, N) + + assert_array_almost_equal(X, sX.toarray()) + + @sup_sparse_efficiency + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_shape_compatibility(self): + use_solver(useUmfpack=True) + A = csc_matrix([[1., 0], [0, 2]]) + bs = [ + [1, 6], + array([1, 6]), + [[1], [6]], + array([[1], [6]]), + csc_matrix([[1], [6]]), + csr_matrix([[1], [6]]), + dok_matrix([[1], [6]]), + bsr_matrix([[1], [6]]), + array([[1., 2., 3.], [6., 8., 10.]]), + csc_matrix([[1., 2., 3.], [6., 8., 10.]]), + csr_matrix([[1., 2., 3.], [6., 8., 10.]]), + dok_matrix([[1., 2., 3.], [6., 8., 10.]]), + bsr_matrix([[1., 2., 3.], [6., 8., 10.]]), + ] + + for b in bs: + x = np.linalg.solve(A.toarray(), toarray(b)) + for spmattype in [csc_matrix, csr_matrix, dok_matrix, lil_matrix]: + x1 = spsolve(spmattype(A), b, use_umfpack=True) + x2 = spsolve(spmattype(A), b, use_umfpack=False) + + # check solution + if x.ndim == 2 and x.shape[1] == 1: + # interprets also these as "vectors" + x = x.ravel() + + assert_array_almost_equal(toarray(x1), x, + err_msg=repr((b, spmattype, 1))) + assert_array_almost_equal(toarray(x2), x, + err_msg=repr((b, spmattype, 2))) + + # dense vs. sparse output ("vectors" are always dense) + if issparse(b) and x.ndim > 1: + assert_(issparse(x1), repr((b, spmattype, 1))) + assert_(issparse(x2), repr((b, spmattype, 2))) + else: + assert_(isinstance(x1, np.ndarray), repr((b, spmattype, 1))) + assert_(isinstance(x2, np.ndarray), repr((b, spmattype, 2))) + + # check output shape + if x.ndim == 1: + # "vector" + assert_equal(x1.shape, (A.shape[1],)) + assert_equal(x2.shape, (A.shape[1],)) + else: + # "matrix" + assert_equal(x1.shape, x.shape) + assert_equal(x2.shape, x.shape) + + A = csc_matrix((3, 3)) + b = csc_matrix((1, 3)) + assert_raises(ValueError, spsolve, A, b) + + @sup_sparse_efficiency + def test_ndarray_support(self): + A = array([[1., 2.], [2., 0.]]) + x = array([[1., 1.], [0.5, -0.5]]) + b = array([[2., 0.], [2., 2.]]) + + assert_array_almost_equal(x, spsolve(A, b)) + + def test_gssv_badinput(self): + N = 10 + d = arange(N) + 1.0 + A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), N, N) + + for spmatrix in (csc_matrix, csr_matrix): + A = spmatrix(A) + b = np.arange(N) + + def not_c_contig(x): + return x.repeat(2)[::2] + + def not_1dim(x): + return x[:,None] + + def bad_type(x): + return x.astype(bool) + + def too_short(x): + return x[:-1] + + badops = [not_c_contig, not_1dim, bad_type, too_short] + + for badop in badops: + msg = f"{spmatrix!r} {badop!r}" + # Not C-contiguous + assert_raises((ValueError, TypeError), _superlu.gssv, + N, A.nnz, badop(A.data), A.indices, A.indptr, + b, int(spmatrix == csc_matrix), err_msg=msg) + assert_raises((ValueError, TypeError), _superlu.gssv, + N, A.nnz, A.data, badop(A.indices), A.indptr, + b, int(spmatrix == csc_matrix), err_msg=msg) + assert_raises((ValueError, TypeError), _superlu.gssv, + N, A.nnz, A.data, A.indices, badop(A.indptr), + b, int(spmatrix == csc_matrix), err_msg=msg) + + def test_sparsity_preservation(self): + ident = csc_matrix([ + [1, 0, 0], + [0, 1, 0], + [0, 0, 1]]) + b = csc_matrix([ + [0, 1], + [1, 0], + [0, 0]]) + x = spsolve(ident, b) + assert_equal(ident.nnz, 3) + assert_equal(b.nnz, 2) + assert_equal(x.nnz, 2) + assert_allclose(x.A, b.A, atol=1e-12, rtol=1e-12) + + def test_dtype_cast(self): + A_real = scipy.sparse.csr_matrix([[1, 2, 0], + [0, 0, 3], + [4, 0, 5]]) + A_complex = scipy.sparse.csr_matrix([[1, 2, 0], + [0, 0, 3], + [4, 0, 5 + 1j]]) + b_real = np.array([1,1,1]) + b_complex = np.array([1,1,1]) + 1j*np.array([1,1,1]) + x = spsolve(A_real, b_real) + assert_(np.issubdtype(x.dtype, np.floating)) + x = spsolve(A_real, b_complex) + assert_(np.issubdtype(x.dtype, np.complexfloating)) + x = spsolve(A_complex, b_real) + assert_(np.issubdtype(x.dtype, np.complexfloating)) + x = spsolve(A_complex, b_complex) + assert_(np.issubdtype(x.dtype, np.complexfloating)) + + @pytest.mark.slow + @pytest.mark.skipif(not has_umfpack, reason="umfpack not available") + def test_bug_8278(self): + check_free_memory(8000) + use_solver(useUmfpack=True) + A, b = setup_bug_8278() + x = spsolve(A, b) + assert_array_almost_equal(A @ x, b) + + +class TestSplu: + def setup_method(self): + use_solver(useUmfpack=False) + n = 40 + d = arange(n) + 1 + self.n = n + self.A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), n, n, format='csc') + random.seed(1234) + + def _smoketest(self, spxlu, check, dtype, idx_dtype): + if np.issubdtype(dtype, np.complexfloating): + A = self.A + 1j*self.A.T + else: + A = self.A + + A = A.astype(dtype) + A.indices = A.indices.astype(idx_dtype, copy=False) + A.indptr = A.indptr.astype(idx_dtype, copy=False) + lu = spxlu(A) + + rng = random.RandomState(1234) + + # Input shapes + for k in [None, 1, 2, self.n, self.n+2]: + msg = f"k={k!r}" + + if k is None: + b = rng.rand(self.n) + else: + b = rng.rand(self.n, k) + + if np.issubdtype(dtype, np.complexfloating): + b = b + 1j*rng.rand(*b.shape) + b = b.astype(dtype) + + x = lu.solve(b) + check(A, b, x, msg) + + x = lu.solve(b, 'T') + check(A.T, b, x, msg) + + x = lu.solve(b, 'H') + check(A.T.conj(), b, x, msg) + + @sup_sparse_efficiency + def test_splu_smoketest(self): + self._internal_test_splu_smoketest() + + def _internal_test_splu_smoketest(self): + # Check that splu works at all + def check(A, b, x, msg=""): + eps = np.finfo(A.dtype).eps + r = A @ x + assert_(abs(r - b).max() < 1e3*eps, msg) + + for dtype in [np.float32, np.float64, np.complex64, np.complex128]: + for idx_dtype in [np.int32, np.int64]: + self._smoketest(splu, check, dtype, idx_dtype) + + @sup_sparse_efficiency + def test_spilu_smoketest(self): + self._internal_test_spilu_smoketest() + + def _internal_test_spilu_smoketest(self): + errors = [] + + def check(A, b, x, msg=""): + r = A @ x + err = abs(r - b).max() + assert_(err < 1e-2, msg) + if b.dtype in (np.float64, np.complex128): + errors.append(err) + + for dtype in [np.float32, np.float64, np.complex64, np.complex128]: + for idx_dtype in [np.int32, np.int64]: + self._smoketest(spilu, check, dtype, idx_dtype) + + assert_(max(errors) > 1e-5) + + @sup_sparse_efficiency + def test_spilu_drop_rule(self): + # Test passing in the drop_rule argument to spilu. + A = identity(2) + + rules = [ + b'basic,area'.decode('ascii'), # unicode + b'basic,area', # ascii + [b'basic', b'area'.decode('ascii')] + ] + for rule in rules: + # Argument should be accepted + assert_(isinstance(spilu(A, drop_rule=rule), SuperLU)) + + def test_splu_nnz0(self): + A = csc_matrix((5,5), dtype='d') + assert_raises(RuntimeError, splu, A) + + def test_spilu_nnz0(self): + A = csc_matrix((5,5), dtype='d') + assert_raises(RuntimeError, spilu, A) + + def test_splu_basic(self): + # Test basic splu functionality. + n = 30 + rng = random.RandomState(12) + a = rng.rand(n, n) + a[a < 0.95] = 0 + # First test with a singular matrix + a[:, 0] = 0 + a_ = csc_matrix(a) + # Matrix is exactly singular + assert_raises(RuntimeError, splu, a_) + + # Make a diagonal dominant, to make sure it is not singular + a += 4*eye(n) + a_ = csc_matrix(a) + lu = splu(a_) + b = ones(n) + x = lu.solve(b) + assert_almost_equal(dot(a, x), b) + + def test_splu_perm(self): + # Test the permutation vectors exposed by splu. + n = 30 + a = random.random((n, n)) + a[a < 0.95] = 0 + # Make a diagonal dominant, to make sure it is not singular + a += 4*eye(n) + a_ = csc_matrix(a) + lu = splu(a_) + # Check that the permutation indices do belong to [0, n-1]. + for perm in (lu.perm_r, lu.perm_c): + assert_(all(perm > -1)) + assert_(all(perm < n)) + assert_equal(len(unique(perm)), len(perm)) + + # Now make a symmetric, and test that the two permutation vectors are + # the same + # Note: a += a.T relies on undefined behavior. + a = a + a.T + a_ = csc_matrix(a) + lu = splu(a_) + assert_array_equal(lu.perm_r, lu.perm_c) + + @pytest.mark.parametrize("splu_fun, rtol", [(splu, 1e-7), (spilu, 1e-1)]) + def test_natural_permc(self, splu_fun, rtol): + # Test that the "NATURAL" permc_spec does not permute the matrix + np.random.seed(42) + n = 500 + p = 0.01 + A = scipy.sparse.random(n, n, p) + x = np.random.rand(n) + # Make A diagonal dominant to make sure it is not singular + A += (n+1)*scipy.sparse.identity(n) + A_ = csc_matrix(A) + b = A_ @ x + + # without permc_spec, permutation is not identity + lu = splu_fun(A_) + assert_(np.any(lu.perm_c != np.arange(n))) + + # with permc_spec="NATURAL", permutation is identity + lu = splu_fun(A_, permc_spec="NATURAL") + assert_array_equal(lu.perm_c, np.arange(n)) + + # Also, lu decomposition is valid + x2 = lu.solve(b) + assert_allclose(x, x2, rtol=rtol) + + @pytest.mark.skipif(not hasattr(sys, 'getrefcount'), reason="no sys.getrefcount") + def test_lu_refcount(self): + # Test that we are keeping track of the reference count with splu. + n = 30 + a = random.random((n, n)) + a[a < 0.95] = 0 + # Make a diagonal dominant, to make sure it is not singular + a += 4*eye(n) + a_ = csc_matrix(a) + lu = splu(a_) + + # And now test that we don't have a refcount bug + rc = sys.getrefcount(lu) + for attr in ('perm_r', 'perm_c'): + perm = getattr(lu, attr) + assert_equal(sys.getrefcount(lu), rc + 1) + del perm + assert_equal(sys.getrefcount(lu), rc) + + def test_bad_inputs(self): + A = self.A.tocsc() + + assert_raises(ValueError, splu, A[:,:4]) + assert_raises(ValueError, spilu, A[:,:4]) + + for lu in [splu(A), spilu(A)]: + b = random.rand(42) + B = random.rand(42, 3) + BB = random.rand(self.n, 3, 9) + assert_raises(ValueError, lu.solve, b) + assert_raises(ValueError, lu.solve, B) + assert_raises(ValueError, lu.solve, BB) + assert_raises(TypeError, lu.solve, + b.astype(np.complex64)) + assert_raises(TypeError, lu.solve, + b.astype(np.complex128)) + + @sup_sparse_efficiency + def test_superlu_dlamch_i386_nan(self): + # SuperLU 4.3 calls some functions returning floats without + # declaring them. On i386@linux call convention, this fails to + # clear floating point registers after call. As a result, NaN + # can appear in the next floating point operation made. + # + # Here's a test case that triggered the issue. + n = 8 + d = np.arange(n) + 1 + A = spdiags((d, 2*d, d[::-1]), (-3, 0, 5), n, n) + A = A.astype(np.float32) + spilu(A) + A = A + 1j*A + B = A.A + assert_(not np.isnan(B).any()) + + @sup_sparse_efficiency + def test_lu_attr(self): + + def check(dtype, complex_2=False): + A = self.A.astype(dtype) + + if complex_2: + A = A + 1j*A.T + + n = A.shape[0] + lu = splu(A) + + # Check that the decomposition is as advertised + + Pc = np.zeros((n, n)) + Pc[np.arange(n), lu.perm_c] = 1 + + Pr = np.zeros((n, n)) + Pr[lu.perm_r, np.arange(n)] = 1 + + Ad = A.toarray() + lhs = Pr.dot(Ad).dot(Pc) + rhs = (lu.L @ lu.U).toarray() + + eps = np.finfo(dtype).eps + + assert_allclose(lhs, rhs, atol=100*eps) + + check(np.float32) + check(np.float64) + check(np.complex64) + check(np.complex128) + check(np.complex64, True) + check(np.complex128, True) + + @pytest.mark.slow + @sup_sparse_efficiency + def test_threads_parallel(self): + oks = [] + + def worker(): + try: + self.test_splu_basic() + self._internal_test_splu_smoketest() + self._internal_test_spilu_smoketest() + oks.append(True) + except Exception: + pass + + threads = [threading.Thread(target=worker) + for k in range(20)] + for t in threads: + t.start() + for t in threads: + t.join() + + assert_equal(len(oks), 20) + + +class TestSpsolveTriangular: + def setup_method(self): + use_solver(useUmfpack=False) + + def test_zero_diagonal(self): + n = 5 + rng = np.random.default_rng(43876432987) + A = rng.standard_normal((n, n)) + b = np.arange(n) + A = scipy.sparse.tril(A, k=0, format='csr') + + x = spsolve_triangular(A, b, unit_diagonal=True, lower=True) + + A.setdiag(1) + assert_allclose(A.dot(x), b) + + # Regression test from gh-15199 + A = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0]], dtype=np.float64) + b = np.array([1., 2., 3.]) + with suppress_warnings() as sup: + sup.filter(SparseEfficiencyWarning, "CSR matrix format is") + spsolve_triangular(A, b, unit_diagonal=True) + + def test_singular(self): + n = 5 + A = csr_matrix((n, n)) + b = np.arange(n) + for lower in (True, False): + assert_raises(scipy.linalg.LinAlgError, + spsolve_triangular, A, b, lower=lower) + + @sup_sparse_efficiency + def test_bad_shape(self): + # A is not square. + A = np.zeros((3, 4)) + b = ones((4, 1)) + assert_raises(ValueError, spsolve_triangular, A, b) + # A2 and b2 have incompatible shapes. + A2 = csr_matrix(eye(3)) + b2 = array([1.0, 2.0]) + assert_raises(ValueError, spsolve_triangular, A2, b2) + + @sup_sparse_efficiency + def test_input_types(self): + A = array([[1., 0.], [1., 2.]]) + b = array([[2., 0.], [2., 2.]]) + for matrix_type in (array, csc_matrix, csr_matrix): + x = spsolve_triangular(matrix_type(A), b, lower=True) + assert_array_almost_equal(A.dot(x), b) + + @pytest.mark.slow + @pytest.mark.timeout(120) # prerelease_deps_coverage_64bit_blas job + @sup_sparse_efficiency + def test_random(self): + def random_triangle_matrix(n, lower=True): + A = scipy.sparse.random(n, n, density=0.1, format='coo') + if lower: + A = scipy.sparse.tril(A) + else: + A = scipy.sparse.triu(A) + A = A.tocsr(copy=False) + for i in range(n): + A[i, i] = np.random.rand() + 1 + return A + + np.random.seed(1234) + for lower in (True, False): + for n in (10, 10**2, 10**3): + A = random_triangle_matrix(n, lower=lower) + for m in (1, 10): + for b in (np.random.rand(n, m), + np.random.randint(-9, 9, (n, m)), + np.random.randint(-9, 9, (n, m)) + + np.random.randint(-9, 9, (n, m)) * 1j): + x = spsolve_triangular(A, b, lower=lower) + assert_array_almost_equal(A.dot(x), b) + x = spsolve_triangular(A, b, lower=lower, + unit_diagonal=True) + A.setdiag(1) + assert_array_almost_equal(A.dot(x), b) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cd1c15a629369096452696a905bfdca6d2fcd23e --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__init__.py @@ -0,0 +1,22 @@ +""" +Sparse Eigenvalue Solvers +------------------------- + +The submodules of sparse.linalg._eigen: + 1. lobpcg: Locally Optimal Block Preconditioned Conjugate Gradient Method + +""" +from .arpack import * +from .lobpcg import * +from ._svds import svds + +from . import arpack + +__all__ = [ + 'ArpackError', 'ArpackNoConvergence', + 'eigs', 'eigsh', 'lobpcg', 'svds' +] + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__pycache__/__init__.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d7bd4aea7982cf5df7db3d947a7a62f9a5dbd79a Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__pycache__/__init__.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__pycache__/_svds.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__pycache__/_svds.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d32390442a5b4b34ab4694979526a41456a5f80d Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/__pycache__/_svds.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/_svds.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/_svds.py new file mode 100644 index 0000000000000000000000000000000000000000..b00e7c7b728b6a196c3912272a97b7176ea7ce58 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/_svds.py @@ -0,0 +1,545 @@ +import numpy as np + +from .arpack import _arpack # type: ignore[attr-defined] +from . import eigsh + +from scipy._lib._util import check_random_state +from scipy.sparse.linalg._interface import LinearOperator, aslinearoperator +from scipy.sparse.linalg._eigen.lobpcg import lobpcg # type: ignore[no-redef] +from scipy.sparse.linalg._svdp import _svdp +from scipy.linalg import svd + +arpack_int = _arpack.timing.nbx.dtype +__all__ = ['svds'] + + +def _herm(x): + return x.T.conj() + + +def _iv(A, k, ncv, tol, which, v0, maxiter, + return_singular, solver, random_state): + + # input validation/standardization for `solver` + # out of order because it's needed for other parameters + solver = str(solver).lower() + solvers = {"arpack", "lobpcg", "propack"} + if solver not in solvers: + raise ValueError(f"solver must be one of {solvers}.") + + # input validation/standardization for `A` + A = aslinearoperator(A) # this takes care of some input validation + if not (np.issubdtype(A.dtype, np.complexfloating) + or np.issubdtype(A.dtype, np.floating)): + message = "`A` must be of floating or complex floating data type." + raise ValueError(message) + if np.prod(A.shape) == 0: + message = "`A` must not be empty." + raise ValueError(message) + + # input validation/standardization for `k` + kmax = min(A.shape) if solver == 'propack' else min(A.shape) - 1 + if int(k) != k or not (0 < k <= kmax): + message = "`k` must be an integer satisfying `0 < k < min(A.shape)`." + raise ValueError(message) + k = int(k) + + # input validation/standardization for `ncv` + if solver == "arpack" and ncv is not None: + if int(ncv) != ncv or not (k < ncv < min(A.shape)): + message = ("`ncv` must be an integer satisfying " + "`k < ncv < min(A.shape)`.") + raise ValueError(message) + ncv = int(ncv) + + # input validation/standardization for `tol` + if tol < 0 or not np.isfinite(tol): + message = "`tol` must be a non-negative floating point value." + raise ValueError(message) + tol = float(tol) + + # input validation/standardization for `which` + which = str(which).upper() + whichs = {'LM', 'SM'} + if which not in whichs: + raise ValueError(f"`which` must be in {whichs}.") + + # input validation/standardization for `v0` + if v0 is not None: + v0 = np.atleast_1d(v0) + if not (np.issubdtype(v0.dtype, np.complexfloating) + or np.issubdtype(v0.dtype, np.floating)): + message = ("`v0` must be of floating or complex floating " + "data type.") + raise ValueError(message) + + shape = (A.shape[0],) if solver == 'propack' else (min(A.shape),) + if v0.shape != shape: + message = f"`v0` must have shape {shape}." + raise ValueError(message) + + # input validation/standardization for `maxiter` + if maxiter is not None and (int(maxiter) != maxiter or maxiter <= 0): + message = "`maxiter` must be a positive integer." + raise ValueError(message) + maxiter = int(maxiter) if maxiter is not None else maxiter + + # input validation/standardization for `return_singular_vectors` + # not going to be flexible with this; too complicated for little gain + rs_options = {True, False, "vh", "u"} + if return_singular not in rs_options: + raise ValueError(f"`return_singular_vectors` must be in {rs_options}.") + + random_state = check_random_state(random_state) + + return (A, k, ncv, tol, which, v0, maxiter, + return_singular, solver, random_state) + + +def svds(A, k=6, ncv=None, tol=0, which='LM', v0=None, + maxiter=None, return_singular_vectors=True, + solver='arpack', random_state=None, options=None): + """ + Partial singular value decomposition of a sparse matrix. + + Compute the largest or smallest `k` singular values and corresponding + singular vectors of a sparse matrix `A`. The order in which the singular + values are returned is not guaranteed. + + In the descriptions below, let ``M, N = A.shape``. + + Parameters + ---------- + A : ndarray, sparse matrix, or LinearOperator + Matrix to decompose of a floating point numeric dtype. + k : int, default: 6 + Number of singular values and singular vectors to compute. + Must satisfy ``1 <= k <= kmax``, where ``kmax=min(M, N)`` for + ``solver='propack'`` and ``kmax=min(M, N) - 1`` otherwise. + ncv : int, optional + When ``solver='arpack'``, this is the number of Lanczos vectors + generated. See :ref:`'arpack' ` for details. + When ``solver='lobpcg'`` or ``solver='propack'``, this parameter is + ignored. + tol : float, optional + Tolerance for singular values. Zero (default) means machine precision. + which : {'LM', 'SM'} + Which `k` singular values to find: either the largest magnitude ('LM') + or smallest magnitude ('SM') singular values. + v0 : ndarray, optional + The starting vector for iteration; see method-specific + documentation (:ref:`'arpack' `, + :ref:`'lobpcg' `), or + :ref:`'propack' ` for details. + maxiter : int, optional + Maximum number of iterations; see method-specific + documentation (:ref:`'arpack' `, + :ref:`'lobpcg' `), or + :ref:`'propack' ` for details. + return_singular_vectors : {True, False, "u", "vh"} + Singular values are always computed and returned; this parameter + controls the computation and return of singular vectors. + + - ``True``: return singular vectors. + - ``False``: do not return singular vectors. + - ``"u"``: if ``M <= N``, compute only the left singular vectors and + return ``None`` for the right singular vectors. Otherwise, compute + all singular vectors. + - ``"vh"``: if ``M > N``, compute only the right singular vectors and + return ``None`` for the left singular vectors. Otherwise, compute + all singular vectors. + + If ``solver='propack'``, the option is respected regardless of the + matrix shape. + + solver : {'arpack', 'propack', 'lobpcg'}, optional + The solver used. + :ref:`'arpack' `, + :ref:`'lobpcg' `, and + :ref:`'propack' ` are supported. + Default: `'arpack'`. + random_state : {None, int, `numpy.random.Generator`, + `numpy.random.RandomState`}, optional + + Pseudorandom number generator state used to generate resamples. + + If `random_state` is ``None`` (or `np.random`), the + `numpy.random.RandomState` singleton is used. + If `random_state` is an int, a new ``RandomState`` instance is used, + seeded with `random_state`. + If `random_state` is already a ``Generator`` or ``RandomState`` + instance then that instance is used. + options : dict, optional + A dictionary of solver-specific options. No solver-specific options + are currently supported; this parameter is reserved for future use. + + Returns + ------- + u : ndarray, shape=(M, k) + Unitary matrix having left singular vectors as columns. + s : ndarray, shape=(k,) + The singular values. + vh : ndarray, shape=(k, N) + Unitary matrix having right singular vectors as rows. + + Notes + ----- + This is a naive implementation using ARPACK or LOBPCG as an eigensolver + on the matrix ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on + which one is smaller size, followed by the Rayleigh-Ritz method + as postprocessing; see + Using the normal matrix, in Rayleigh-Ritz method, (2022, Nov. 19), + Wikipedia, https://w.wiki/4zms. + + Alternatively, the PROPACK solver can be called. + + Choices of the input matrix `A` numeric dtype may be limited. + Only ``solver="lobpcg"`` supports all floating point dtypes + real: 'np.float32', 'np.float64', 'np.longdouble' and + complex: 'np.complex64', 'np.complex128', 'np.clongdouble'. + The ``solver="arpack"`` supports only + 'np.float32', 'np.float64', and 'np.complex128'. + + Examples + -------- + Construct a matrix `A` from singular values and vectors. + + >>> import numpy as np + >>> from scipy import sparse, linalg, stats + >>> from scipy.sparse.linalg import svds, aslinearoperator, LinearOperator + + Construct a dense matrix `A` from singular values and vectors. + + >>> rng = np.random.default_rng(258265244568965474821194062361901728911) + >>> orthogonal = stats.ortho_group.rvs(10, random_state=rng) + >>> s = [1e-3, 1, 2, 3, 4] # non-zero singular values + >>> u = orthogonal[:, :5] # left singular vectors + >>> vT = orthogonal[:, 5:].T # right singular vectors + >>> A = u @ np.diag(s) @ vT + + With only four singular values/vectors, the SVD approximates the original + matrix. + + >>> u4, s4, vT4 = svds(A, k=4) + >>> A4 = u4 @ np.diag(s4) @ vT4 + >>> np.allclose(A4, A, atol=1e-3) + True + + With all five non-zero singular values/vectors, we can reproduce + the original matrix more accurately. + + >>> u5, s5, vT5 = svds(A, k=5) + >>> A5 = u5 @ np.diag(s5) @ vT5 + >>> np.allclose(A5, A) + True + + The singular values match the expected singular values. + + >>> np.allclose(s5, s) + True + + Since the singular values are not close to each other in this example, + every singular vector matches as expected up to a difference in sign. + + >>> (np.allclose(np.abs(u5), np.abs(u)) and + ... np.allclose(np.abs(vT5), np.abs(vT))) + True + + The singular vectors are also orthogonal. + + >>> (np.allclose(u5.T @ u5, np.eye(5)) and + ... np.allclose(vT5 @ vT5.T, np.eye(5))) + True + + If there are (nearly) multiple singular values, the corresponding + individual singular vectors may be unstable, but the whole invariant + subspace containing all such singular vectors is computed accurately + as can be measured by angles between subspaces via 'subspace_angles'. + + >>> rng = np.random.default_rng(178686584221410808734965903901790843963) + >>> s = [1, 1 + 1e-6] # non-zero singular values + >>> u, _ = np.linalg.qr(rng.standard_normal((99, 2))) + >>> v, _ = np.linalg.qr(rng.standard_normal((99, 2))) + >>> vT = v.T + >>> A = u @ np.diag(s) @ vT + >>> A = A.astype(np.float32) + >>> u2, s2, vT2 = svds(A, k=2, random_state=rng) + >>> np.allclose(s2, s) + True + + The angles between the individual exact and computed singular vectors + may not be so small. To check use: + + >>> (linalg.subspace_angles(u2[:, :1], u[:, :1]) + + ... linalg.subspace_angles(u2[:, 1:], u[:, 1:])) + array([0.06562513]) # may vary + >>> (linalg.subspace_angles(vT2[:1, :].T, vT[:1, :].T) + + ... linalg.subspace_angles(vT2[1:, :].T, vT[1:, :].T)) + array([0.06562507]) # may vary + + As opposed to the angles between the 2-dimensional invariant subspaces + that these vectors span, which are small for rights singular vectors + + >>> linalg.subspace_angles(u2, u).sum() < 1e-6 + True + + as well as for left singular vectors. + + >>> linalg.subspace_angles(vT2.T, vT.T).sum() < 1e-6 + True + + The next example follows that of 'sklearn.decomposition.TruncatedSVD'. + + >>> rng = np.random.RandomState(0) + >>> X_dense = rng.random(size=(100, 100)) + >>> X_dense[:, 2 * np.arange(50)] = 0 + >>> X = sparse.csr_matrix(X_dense) + >>> _, singular_values, _ = svds(X, k=5, random_state=rng) + >>> print(singular_values) + [ 4.3293... 4.4491... 4.5420... 4.5987... 35.2410...] + + The function can be called without the transpose of the input matrix + ever explicitly constructed. + + >>> rng = np.random.default_rng(102524723947864966825913730119128190974) + >>> G = sparse.rand(8, 9, density=0.5, random_state=rng) + >>> Glo = aslinearoperator(G) + >>> _, singular_values_svds, _ = svds(Glo, k=5, random_state=rng) + >>> _, singular_values_svd, _ = linalg.svd(G.toarray()) + >>> np.allclose(singular_values_svds, singular_values_svd[-4::-1]) + True + + The most memory efficient scenario is where neither + the original matrix, nor its transpose, is explicitly constructed. + Our example computes the smallest singular values and vectors + of 'LinearOperator' constructed from the numpy function 'np.diff' used + column-wise to be consistent with 'LinearOperator' operating on columns. + + >>> diff0 = lambda a: np.diff(a, axis=0) + + Let us create the matrix from 'diff0' to be used for validation only. + + >>> n = 5 # The dimension of the space. + >>> M_from_diff0 = diff0(np.eye(n)) + >>> print(M_from_diff0.astype(int)) + [[-1 1 0 0 0] + [ 0 -1 1 0 0] + [ 0 0 -1 1 0] + [ 0 0 0 -1 1]] + + The matrix 'M_from_diff0' is bi-diagonal and could be alternatively + created directly by + + >>> M = - np.eye(n - 1, n, dtype=int) + >>> np.fill_diagonal(M[:,1:], 1) + >>> np.allclose(M, M_from_diff0) + True + + Its transpose + + >>> print(M.T) + [[-1 0 0 0] + [ 1 -1 0 0] + [ 0 1 -1 0] + [ 0 0 1 -1] + [ 0 0 0 1]] + + can be viewed as the incidence matrix; see + Incidence matrix, (2022, Nov. 19), Wikipedia, https://w.wiki/5YXU, + of a linear graph with 5 vertices and 4 edges. The 5x5 normal matrix + ``M.T @ M`` thus is + + >>> print(M.T @ M) + [[ 1 -1 0 0 0] + [-1 2 -1 0 0] + [ 0 -1 2 -1 0] + [ 0 0 -1 2 -1] + [ 0 0 0 -1 1]] + + the graph Laplacian, while the actually used in 'svds' smaller size + 4x4 normal matrix ``M @ M.T`` + + >>> print(M @ M.T) + [[ 2 -1 0 0] + [-1 2 -1 0] + [ 0 -1 2 -1] + [ 0 0 -1 2]] + + is the so-called edge-based Laplacian; see + Symmetric Laplacian via the incidence matrix, in Laplacian matrix, + (2022, Nov. 19), Wikipedia, https://w.wiki/5YXW. + + The 'LinearOperator' setup needs the options 'rmatvec' and 'rmatmat' + of multiplication by the matrix transpose ``M.T``, but we want to be + matrix-free to save memory, so knowing how ``M.T`` looks like, we + manually construct the following function to be + used in ``rmatmat=diff0t``. + + >>> def diff0t(a): + ... if a.ndim == 1: + ... a = a[:,np.newaxis] # Turn 1D into 2D array + ... d = np.zeros((a.shape[0] + 1, a.shape[1]), dtype=a.dtype) + ... d[0, :] = - a[0, :] + ... d[1:-1, :] = a[0:-1, :] - a[1:, :] + ... d[-1, :] = a[-1, :] + ... return d + + We check that our function 'diff0t' for the matrix transpose is valid. + + >>> np.allclose(M.T, diff0t(np.eye(n-1))) + True + + Now we setup our matrix-free 'LinearOperator' called 'diff0_func_aslo' + and for validation the matrix-based 'diff0_matrix_aslo'. + + >>> def diff0_func_aslo_def(n): + ... return LinearOperator(matvec=diff0, + ... matmat=diff0, + ... rmatvec=diff0t, + ... rmatmat=diff0t, + ... shape=(n - 1, n)) + >>> diff0_func_aslo = diff0_func_aslo_def(n) + >>> diff0_matrix_aslo = aslinearoperator(M_from_diff0) + + And validate both the matrix and its transpose in 'LinearOperator'. + + >>> np.allclose(diff0_func_aslo(np.eye(n)), + ... diff0_matrix_aslo(np.eye(n))) + True + >>> np.allclose(diff0_func_aslo.T(np.eye(n-1)), + ... diff0_matrix_aslo.T(np.eye(n-1))) + True + + Having the 'LinearOperator' setup validated, we run the solver. + + >>> n = 100 + >>> diff0_func_aslo = diff0_func_aslo_def(n) + >>> u, s, vT = svds(diff0_func_aslo, k=3, which='SM') + + The singular values squared and the singular vectors are known + explicitly; see + Pure Dirichlet boundary conditions, in + Eigenvalues and eigenvectors of the second derivative, + (2022, Nov. 19), Wikipedia, https://w.wiki/5YX6, + since 'diff' corresponds to first + derivative, and its smaller size n-1 x n-1 normal matrix + ``M @ M.T`` represent the discrete second derivative with the Dirichlet + boundary conditions. We use these analytic expressions for validation. + + >>> se = 2. * np.sin(np.pi * np.arange(1, 4) / (2. * n)) + >>> ue = np.sqrt(2 / n) * np.sin(np.pi * np.outer(np.arange(1, n), + ... np.arange(1, 4)) / n) + >>> np.allclose(s, se, atol=1e-3) + True + >>> print(np.allclose(np.abs(u), np.abs(ue), atol=1e-6)) + True + + """ + args = _iv(A, k, ncv, tol, which, v0, maxiter, return_singular_vectors, + solver, random_state) + (A, k, ncv, tol, which, v0, maxiter, + return_singular_vectors, solver, random_state) = args + + largest = (which == 'LM') + n, m = A.shape + + if n >= m: + X_dot = A.matvec + X_matmat = A.matmat + XH_dot = A.rmatvec + XH_mat = A.rmatmat + transpose = False + else: + X_dot = A.rmatvec + X_matmat = A.rmatmat + XH_dot = A.matvec + XH_mat = A.matmat + transpose = True + + dtype = getattr(A, 'dtype', None) + if dtype is None: + dtype = A.dot(np.zeros([m, 1])).dtype + + def matvec_XH_X(x): + return XH_dot(X_dot(x)) + + def matmat_XH_X(x): + return XH_mat(X_matmat(x)) + + XH_X = LinearOperator(matvec=matvec_XH_X, dtype=A.dtype, + matmat=matmat_XH_X, + shape=(min(A.shape), min(A.shape))) + + # Get a low rank approximation of the implicitly defined gramian matrix. + # This is not a stable way to approach the problem. + if solver == 'lobpcg': + + if k == 1 and v0 is not None: + X = np.reshape(v0, (-1, 1)) + else: + X = random_state.standard_normal(size=(min(A.shape), k)) + + _, eigvec = lobpcg(XH_X, X, tol=tol ** 2, maxiter=maxiter, + largest=largest) + + elif solver == 'propack': + jobu = return_singular_vectors in {True, 'u'} + jobv = return_singular_vectors in {True, 'vh'} + irl_mode = (which == 'SM') + res = _svdp(A, k=k, tol=tol**2, which=which, maxiter=None, + compute_u=jobu, compute_v=jobv, irl_mode=irl_mode, + kmax=maxiter, v0=v0, random_state=random_state) + + u, s, vh, _ = res # but we'll ignore bnd, the last output + + # PROPACK order appears to be largest first. `svds` output order is not + # guaranteed, according to documentation, but for ARPACK and LOBPCG + # they actually are ordered smallest to largest, so reverse for + # consistency. + s = s[::-1] + u = u[:, ::-1] + vh = vh[::-1] + + u = u if jobu else None + vh = vh if jobv else None + + if return_singular_vectors: + return u, s, vh + else: + return s + + elif solver == 'arpack' or solver is None: + if v0 is None: + v0 = random_state.standard_normal(size=(min(A.shape),)) + _, eigvec = eigsh(XH_X, k=k, tol=tol ** 2, maxiter=maxiter, + ncv=ncv, which=which, v0=v0) + # arpack do not guarantee exactly orthonormal eigenvectors + # for clustered eigenvalues, especially in complex arithmetic + eigvec, _ = np.linalg.qr(eigvec) + + # the eigenvectors eigvec must be orthonomal here; see gh-16712 + Av = X_matmat(eigvec) + if not return_singular_vectors: + s = svd(Av, compute_uv=False, overwrite_a=True) + return s[::-1] + + # compute the left singular vectors of X and update the right ones + # accordingly + u, s, vh = svd(Av, full_matrices=False, overwrite_a=True) + u = u[:, ::-1] + s = s[::-1] + vh = vh[::-1] + + jobu = return_singular_vectors in {True, 'u'} + jobv = return_singular_vectors in {True, 'vh'} + + if transpose: + u_tmp = eigvec @ _herm(vh) if jobu else None + vh = _herm(u) if jobv else None + u = u_tmp + else: + if not jobu: + u = None + vh = vh @ _herm(eigvec) if jobv else None + + return u, s, vh diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/_svds_doc.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/_svds_doc.py new file mode 100644 index 0000000000000000000000000000000000000000..02312458e016295eff55903459c5854ea5f4252b --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/_svds_doc.py @@ -0,0 +1,400 @@ +def _svds_arpack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None, + maxiter=None, return_singular_vectors=True, + solver='arpack', random_state=None): + """ + Partial singular value decomposition of a sparse matrix using ARPACK. + + Compute the largest or smallest `k` singular values and corresponding + singular vectors of a sparse matrix `A`. The order in which the singular + values are returned is not guaranteed. + + In the descriptions below, let ``M, N = A.shape``. + + Parameters + ---------- + A : sparse matrix or LinearOperator + Matrix to decompose. + k : int, optional + Number of singular values and singular vectors to compute. + Must satisfy ``1 <= k <= min(M, N) - 1``. + Default is 6. + ncv : int, optional + The number of Lanczos vectors generated. + The default is ``min(n, max(2*k + 1, 20))``. + If specified, must satistify ``k + 1 < ncv < min(M, N)``; ``ncv > 2*k`` + is recommended. + tol : float, optional + Tolerance for singular values. Zero (default) means machine precision. + which : {'LM', 'SM'} + Which `k` singular values to find: either the largest magnitude ('LM') + or smallest magnitude ('SM') singular values. + v0 : ndarray, optional + The starting vector for iteration: + an (approximate) left singular vector if ``N > M`` and a right singular + vector otherwise. Must be of length ``min(M, N)``. + Default: random + maxiter : int, optional + Maximum number of Arnoldi update iterations allowed; + default is ``min(M, N) * 10``. + return_singular_vectors : {True, False, "u", "vh"} + Singular values are always computed and returned; this parameter + controls the computation and return of singular vectors. + + - ``True``: return singular vectors. + - ``False``: do not return singular vectors. + - ``"u"``: if ``M <= N``, compute only the left singular vectors and + return ``None`` for the right singular vectors. Otherwise, compute + all singular vectors. + - ``"vh"``: if ``M > N``, compute only the right singular vectors and + return ``None`` for the left singular vectors. Otherwise, compute + all singular vectors. + + solver : {'arpack', 'propack', 'lobpcg'}, optional + This is the solver-specific documentation for ``solver='arpack'``. + :ref:`'lobpcg' ` and + :ref:`'propack' ` + are also supported. + random_state : {None, int, `numpy.random.Generator`, + `numpy.random.RandomState`}, optional + + Pseudorandom number generator state used to generate resamples. + + If `random_state` is ``None`` (or `np.random`), the + `numpy.random.RandomState` singleton is used. + If `random_state` is an int, a new ``RandomState`` instance is used, + seeded with `random_state`. + If `random_state` is already a ``Generator`` or ``RandomState`` + instance then that instance is used. + options : dict, optional + A dictionary of solver-specific options. No solver-specific options + are currently supported; this parameter is reserved for future use. + + Returns + ------- + u : ndarray, shape=(M, k) + Unitary matrix having left singular vectors as columns. + s : ndarray, shape=(k,) + The singular values. + vh : ndarray, shape=(k, N) + Unitary matrix having right singular vectors as rows. + + Notes + ----- + This is a naive implementation using ARPACK as an eigensolver + on ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on which one is more + efficient. + + Examples + -------- + Construct a matrix ``A`` from singular values and vectors. + + >>> import numpy as np + >>> from scipy.stats import ortho_group + >>> from scipy.sparse import csc_matrix, diags + >>> from scipy.sparse.linalg import svds + >>> rng = np.random.default_rng() + >>> orthogonal = csc_matrix(ortho_group.rvs(10, random_state=rng)) + >>> s = [0.0001, 0.001, 3, 4, 5] # singular values + >>> u = orthogonal[:, :5] # left singular vectors + >>> vT = orthogonal[:, 5:].T # right singular vectors + >>> A = u @ diags(s) @ vT + + With only three singular values/vectors, the SVD approximates the original + matrix. + + >>> u2, s2, vT2 = svds(A, k=3, solver='arpack') + >>> A2 = u2 @ np.diag(s2) @ vT2 + >>> np.allclose(A2, A.toarray(), atol=1e-3) + True + + With all five singular values/vectors, we can reproduce the original + matrix. + + >>> u3, s3, vT3 = svds(A, k=5, solver='arpack') + >>> A3 = u3 @ np.diag(s3) @ vT3 + >>> np.allclose(A3, A.toarray()) + True + + The singular values match the expected singular values, and the singular + vectors are as expected up to a difference in sign. + + >>> (np.allclose(s3, s) and + ... np.allclose(np.abs(u3), np.abs(u.toarray())) and + ... np.allclose(np.abs(vT3), np.abs(vT.toarray()))) + True + + The singular vectors are also orthogonal. + + >>> (np.allclose(u3.T @ u3, np.eye(5)) and + ... np.allclose(vT3 @ vT3.T, np.eye(5))) + True + """ + pass + + +def _svds_lobpcg_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None, + maxiter=None, return_singular_vectors=True, + solver='lobpcg', random_state=None): + """ + Partial singular value decomposition of a sparse matrix using LOBPCG. + + Compute the largest or smallest `k` singular values and corresponding + singular vectors of a sparse matrix `A`. The order in which the singular + values are returned is not guaranteed. + + In the descriptions below, let ``M, N = A.shape``. + + Parameters + ---------- + A : sparse matrix or LinearOperator + Matrix to decompose. + k : int, default: 6 + Number of singular values and singular vectors to compute. + Must satisfy ``1 <= k <= min(M, N) - 1``. + ncv : int, optional + Ignored. + tol : float, optional + Tolerance for singular values. Zero (default) means machine precision. + which : {'LM', 'SM'} + Which `k` singular values to find: either the largest magnitude ('LM') + or smallest magnitude ('SM') singular values. + v0 : ndarray, optional + If `k` is 1, the starting vector for iteration: + an (approximate) left singular vector if ``N > M`` and a right singular + vector otherwise. Must be of length ``min(M, N)``. + Ignored otherwise. + Default: random + maxiter : int, default: 20 + Maximum number of iterations. + return_singular_vectors : {True, False, "u", "vh"} + Singular values are always computed and returned; this parameter + controls the computation and return of singular vectors. + + - ``True``: return singular vectors. + - ``False``: do not return singular vectors. + - ``"u"``: if ``M <= N``, compute only the left singular vectors and + return ``None`` for the right singular vectors. Otherwise, compute + all singular vectors. + - ``"vh"``: if ``M > N``, compute only the right singular vectors and + return ``None`` for the left singular vectors. Otherwise, compute + all singular vectors. + + solver : {'arpack', 'propack', 'lobpcg'}, optional + This is the solver-specific documentation for ``solver='lobpcg'``. + :ref:`'arpack' ` and + :ref:`'propack' ` + are also supported. + random_state : {None, int, `numpy.random.Generator`, + `numpy.random.RandomState`}, optional + + Pseudorandom number generator state used to generate resamples. + + If `random_state` is ``None`` (or `np.random`), the + `numpy.random.RandomState` singleton is used. + If `random_state` is an int, a new ``RandomState`` instance is used, + seeded with `random_state`. + If `random_state` is already a ``Generator`` or ``RandomState`` + instance then that instance is used. + options : dict, optional + A dictionary of solver-specific options. No solver-specific options + are currently supported; this parameter is reserved for future use. + + Returns + ------- + u : ndarray, shape=(M, k) + Unitary matrix having left singular vectors as columns. + s : ndarray, shape=(k,) + The singular values. + vh : ndarray, shape=(k, N) + Unitary matrix having right singular vectors as rows. + + Notes + ----- + This is a naive implementation using LOBPCG as an eigensolver + on ``A.conj().T @ A`` or ``A @ A.conj().T``, depending on which one is more + efficient. + + Examples + -------- + Construct a matrix ``A`` from singular values and vectors. + + >>> import numpy as np + >>> from scipy.stats import ortho_group + >>> from scipy.sparse import csc_matrix, diags + >>> from scipy.sparse.linalg import svds + >>> rng = np.random.default_rng() + >>> orthogonal = csc_matrix(ortho_group.rvs(10, random_state=rng)) + >>> s = [0.0001, 0.001, 3, 4, 5] # singular values + >>> u = orthogonal[:, :5] # left singular vectors + >>> vT = orthogonal[:, 5:].T # right singular vectors + >>> A = u @ diags(s) @ vT + + With only three singular values/vectors, the SVD approximates the original + matrix. + + >>> u2, s2, vT2 = svds(A, k=3, solver='lobpcg') + >>> A2 = u2 @ np.diag(s2) @ vT2 + >>> np.allclose(A2, A.toarray(), atol=1e-3) + True + + With all five singular values/vectors, we can reproduce the original + matrix. + + >>> u3, s3, vT3 = svds(A, k=5, solver='lobpcg') + >>> A3 = u3 @ np.diag(s3) @ vT3 + >>> np.allclose(A3, A.toarray()) + True + + The singular values match the expected singular values, and the singular + vectors are as expected up to a difference in sign. + + >>> (np.allclose(s3, s) and + ... np.allclose(np.abs(u3), np.abs(u.todense())) and + ... np.allclose(np.abs(vT3), np.abs(vT.todense()))) + True + + The singular vectors are also orthogonal. + + >>> (np.allclose(u3.T @ u3, np.eye(5)) and + ... np.allclose(vT3 @ vT3.T, np.eye(5))) + True + + """ + pass + + +def _svds_propack_doc(A, k=6, ncv=None, tol=0, which='LM', v0=None, + maxiter=None, return_singular_vectors=True, + solver='propack', random_state=None): + """ + Partial singular value decomposition of a sparse matrix using PROPACK. + + Compute the largest or smallest `k` singular values and corresponding + singular vectors of a sparse matrix `A`. The order in which the singular + values are returned is not guaranteed. + + In the descriptions below, let ``M, N = A.shape``. + + Parameters + ---------- + A : sparse matrix or LinearOperator + Matrix to decompose. If `A` is a ``LinearOperator`` + object, it must define both ``matvec`` and ``rmatvec`` methods. + k : int, default: 6 + Number of singular values and singular vectors to compute. + Must satisfy ``1 <= k <= min(M, N)``. + ncv : int, optional + Ignored. + tol : float, optional + The desired relative accuracy for computed singular values. + Zero (default) means machine precision. + which : {'LM', 'SM'} + Which `k` singular values to find: either the largest magnitude ('LM') + or smallest magnitude ('SM') singular values. Note that choosing + ``which='SM'`` will force the ``irl`` option to be set ``True``. + v0 : ndarray, optional + Starting vector for iterations: must be of length ``A.shape[0]``. + If not specified, PROPACK will generate a starting vector. + maxiter : int, optional + Maximum number of iterations / maximal dimension of the Krylov + subspace. Default is ``10 * k``. + return_singular_vectors : {True, False, "u", "vh"} + Singular values are always computed and returned; this parameter + controls the computation and return of singular vectors. + + - ``True``: return singular vectors. + - ``False``: do not return singular vectors. + - ``"u"``: compute only the left singular vectors; return ``None`` for + the right singular vectors. + - ``"vh"``: compute only the right singular vectors; return ``None`` + for the left singular vectors. + + solver : {'arpack', 'propack', 'lobpcg'}, optional + This is the solver-specific documentation for ``solver='propack'``. + :ref:`'arpack' ` and + :ref:`'lobpcg' ` + are also supported. + random_state : {None, int, `numpy.random.Generator`, + `numpy.random.RandomState`}, optional + + Pseudorandom number generator state used to generate resamples. + + If `random_state` is ``None`` (or `np.random`), the + `numpy.random.RandomState` singleton is used. + If `random_state` is an int, a new ``RandomState`` instance is used, + seeded with `random_state`. + If `random_state` is already a ``Generator`` or ``RandomState`` + instance then that instance is used. + options : dict, optional + A dictionary of solver-specific options. No solver-specific options + are currently supported; this parameter is reserved for future use. + + Returns + ------- + u : ndarray, shape=(M, k) + Unitary matrix having left singular vectors as columns. + s : ndarray, shape=(k,) + The singular values. + vh : ndarray, shape=(k, N) + Unitary matrix having right singular vectors as rows. + + Notes + ----- + This is an interface to the Fortran library PROPACK [1]_. + The current default is to run with IRL mode disabled unless seeking the + smallest singular values/vectors (``which='SM'``). + + References + ---------- + + .. [1] Larsen, Rasmus Munk. "PROPACK-Software for large and sparse SVD + calculations." Available online. URL + http://sun.stanford.edu/~rmunk/PROPACK (2004): 2008-2009. + + Examples + -------- + Construct a matrix ``A`` from singular values and vectors. + + >>> import numpy as np + >>> from scipy.stats import ortho_group + >>> from scipy.sparse import csc_matrix, diags + >>> from scipy.sparse.linalg import svds + >>> rng = np.random.default_rng() + >>> orthogonal = csc_matrix(ortho_group.rvs(10, random_state=rng)) + >>> s = [0.0001, 0.001, 3, 4, 5] # singular values + >>> u = orthogonal[:, :5] # left singular vectors + >>> vT = orthogonal[:, 5:].T # right singular vectors + >>> A = u @ diags(s) @ vT + + With only three singular values/vectors, the SVD approximates the original + matrix. + + >>> u2, s2, vT2 = svds(A, k=3, solver='propack') + >>> A2 = u2 @ np.diag(s2) @ vT2 + >>> np.allclose(A2, A.todense(), atol=1e-3) + True + + With all five singular values/vectors, we can reproduce the original + matrix. + + >>> u3, s3, vT3 = svds(A, k=5, solver='propack') + >>> A3 = u3 @ np.diag(s3) @ vT3 + >>> np.allclose(A3, A.todense()) + True + + The singular values match the expected singular values, and the singular + vectors are as expected up to a difference in sign. + + >>> (np.allclose(s3, s) and + ... np.allclose(np.abs(u3), np.abs(u.toarray())) and + ... np.allclose(np.abs(vT3), np.abs(vT.toarray()))) + True + + The singular vectors are also orthogonal. + + >>> (np.allclose(u3.T @ u3, np.eye(5)) and + ... np.allclose(vT3 @ vT3.T, np.eye(5))) + True + + """ + pass diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/COPYING b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/COPYING new file mode 100644 index 0000000000000000000000000000000000000000..1b0627eb465cdaafa7fe9053261e65f5a3f8216d --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/COPYING @@ -0,0 +1,45 @@ + +BSD Software License + +Pertains to ARPACK and P_ARPACK + +Copyright (c) 1996-2008 Rice University. +Developed by D.C. Sorensen, R.B. Lehoucq, C. Yang, and K. Maschhoff. +All rights reserved. + +Arpack has been renamed to arpack-ng. + +Copyright (c) 2001-2011 - Scilab Enterprises +Updated by Allan Cornet, Sylvestre Ledru. + +Copyright (c) 2010 - Jordi Gutiérrez Hermoso (Octave patch) + +Copyright (c) 2007 - Sébastien Fabbro (gentoo patch) + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +- Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer listed + in this license in the documentation and/or other materials + provided with the distribution. + +- Neither the name of the copyright holders nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..bb9c1834f320bab957a3364d706027f1aea29e58 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__init__.py @@ -0,0 +1,20 @@ +""" +Eigenvalue solver using iterative methods. + +Find k eigenvectors and eigenvalues of a matrix A using the +Arnoldi/Lanczos iterative methods from ARPACK [1]_,[2]_. + +These methods are most useful for large sparse matrices. + + - eigs(A,k) + - eigsh(A,k) + +References +---------- +.. [1] ARPACK Software, http://www.caam.rice.edu/software/ARPACK/ +.. [2] R. B. Lehoucq, D. C. Sorensen, and C. Yang, ARPACK USERS GUIDE: + Solution of Large Scale Eigenvalue Problems by Implicitly Restarted + Arnoldi Methods. SIAM, Philadelphia, PA, 1998. + +""" +from .arpack import * diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__pycache__/__init__.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f073e9b06c6c00ae69e4db9c540d6c03eb089a7b Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__pycache__/__init__.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__pycache__/arpack.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__pycache__/arpack.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..16e1b7a51885ca87533d4d5ca1014c5a9abd8444 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/__pycache__/arpack.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/_arpack.cp39-win_amd64.dll.a b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/_arpack.cp39-win_amd64.dll.a new file mode 100644 index 0000000000000000000000000000000000000000..74399f0d4ec12eddc4144b4c1497188f73d66e42 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/_arpack.cp39-win_amd64.dll.a differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/_arpack.cp39-win_amd64.pyd b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/_arpack.cp39-win_amd64.pyd new file mode 100644 index 0000000000000000000000000000000000000000..96987f3c8afa33f2fe304c65fdff9f2bcf000269 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/_arpack.cp39-win_amd64.pyd differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/arpack.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/arpack.py new file mode 100644 index 0000000000000000000000000000000000000000..77fde786673da50a2216a1f3af2f28ebf0dbdebb --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/arpack.py @@ -0,0 +1,1702 @@ +""" +Find a few eigenvectors and eigenvalues of a matrix. + + +Uses ARPACK: https://github.com/opencollab/arpack-ng + +""" +# Wrapper implementation notes +# +# ARPACK Entry Points +# ------------------- +# The entry points to ARPACK are +# - (s,d)seupd : single and double precision symmetric matrix +# - (s,d,c,z)neupd: single,double,complex,double complex general matrix +# This wrapper puts the *neupd (general matrix) interfaces in eigs() +# and the *seupd (symmetric matrix) in eigsh(). +# There is no specialized interface for complex Hermitian matrices. +# To find eigenvalues of a complex Hermitian matrix you +# may use eigsh(), but eigsh() will simply call eigs() +# and return the real part of the eigenvalues thus obtained. + +# Number of eigenvalues returned and complex eigenvalues +# ------------------------------------------------------ +# The ARPACK nonsymmetric real and double interface (s,d)naupd return +# eigenvalues and eigenvectors in real (float,double) arrays. +# Since the eigenvalues and eigenvectors are, in general, complex +# ARPACK puts the real and imaginary parts in consecutive entries +# in real-valued arrays. This wrapper puts the real entries +# into complex data types and attempts to return the requested eigenvalues +# and eigenvectors. + + +# Solver modes +# ------------ +# ARPACK and handle shifted and shift-inverse computations +# for eigenvalues by providing a shift (sigma) and a solver. + +import numpy as np +import warnings +from scipy.sparse.linalg._interface import aslinearoperator, LinearOperator +from scipy.sparse import eye, issparse +from scipy.linalg import eig, eigh, lu_factor, lu_solve +from scipy.sparse._sputils import isdense, is_pydata_spmatrix +from scipy.sparse.linalg import gmres, splu +from scipy._lib._util import _aligned_zeros +from scipy._lib._threadsafety import ReentrancyLock + +from . import _arpack +arpack_int = _arpack.timing.nbx.dtype + +__docformat__ = "restructuredtext en" + +__all__ = ['eigs', 'eigsh', 'ArpackError', 'ArpackNoConvergence'] + + +_type_conv = {'f': 's', 'd': 'd', 'F': 'c', 'D': 'z'} +_ndigits = {'f': 5, 'd': 12, 'F': 5, 'D': 12} + +DNAUPD_ERRORS = { + 0: "Normal exit.", + 1: "Maximum number of iterations taken. " + "All possible eigenvalues of OP has been found. IPARAM(5) " + "returns the number of wanted converged Ritz values.", + 2: "No longer an informational error. Deprecated starting " + "with release 2 of ARPACK.", + 3: "No shifts could be applied during a cycle of the " + "Implicitly restarted Arnoldi iteration. One possibility " + "is to increase the size of NCV relative to NEV. ", + -1: "N must be positive.", + -2: "NEV must be positive.", + -3: "NCV-NEV >= 2 and less than or equal to N.", + -4: "The maximum number of Arnoldi update iterations allowed " + "must be greater than zero.", + -5: " WHICH must be one of 'LM', 'SM', 'LR', 'SR', 'LI', 'SI'", + -6: "BMAT must be one of 'I' or 'G'.", + -7: "Length of private work array WORKL is not sufficient.", + -8: "Error return from LAPACK eigenvalue calculation;", + -9: "Starting vector is zero.", + -10: "IPARAM(7) must be 1,2,3,4.", + -11: "IPARAM(7) = 1 and BMAT = 'G' are incompatible.", + -12: "IPARAM(1) must be equal to 0 or 1.", + -13: "NEV and WHICH = 'BE' are incompatible.", + -9999: "Could not build an Arnoldi factorization. " + "IPARAM(5) returns the size of the current Arnoldi " + "factorization. The user is advised to check that " + "enough workspace and array storage has been allocated." +} + +SNAUPD_ERRORS = DNAUPD_ERRORS + +ZNAUPD_ERRORS = DNAUPD_ERRORS.copy() +ZNAUPD_ERRORS[-10] = "IPARAM(7) must be 1,2,3." + +CNAUPD_ERRORS = ZNAUPD_ERRORS + +DSAUPD_ERRORS = { + 0: "Normal exit.", + 1: "Maximum number of iterations taken. " + "All possible eigenvalues of OP has been found.", + 2: "No longer an informational error. Deprecated starting with " + "release 2 of ARPACK.", + 3: "No shifts could be applied during a cycle of the Implicitly " + "restarted Arnoldi iteration. One possibility is to increase " + "the size of NCV relative to NEV. ", + -1: "N must be positive.", + -2: "NEV must be positive.", + -3: "NCV must be greater than NEV and less than or equal to N.", + -4: "The maximum number of Arnoldi update iterations allowed " + "must be greater than zero.", + -5: "WHICH must be one of 'LM', 'SM', 'LA', 'SA' or 'BE'.", + -6: "BMAT must be one of 'I' or 'G'.", + -7: "Length of private work array WORKL is not sufficient.", + -8: "Error return from trid. eigenvalue calculation; " + "Informational error from LAPACK routine dsteqr .", + -9: "Starting vector is zero.", + -10: "IPARAM(7) must be 1,2,3,4,5.", + -11: "IPARAM(7) = 1 and BMAT = 'G' are incompatible.", + -12: "IPARAM(1) must be equal to 0 or 1.", + -13: "NEV and WHICH = 'BE' are incompatible. ", + -9999: "Could not build an Arnoldi factorization. " + "IPARAM(5) returns the size of the current Arnoldi " + "factorization. The user is advised to check that " + "enough workspace and array storage has been allocated.", +} + +SSAUPD_ERRORS = DSAUPD_ERRORS + +DNEUPD_ERRORS = { + 0: "Normal exit.", + 1: "The Schur form computed by LAPACK routine dlahqr " + "could not be reordered by LAPACK routine dtrsen. " + "Re-enter subroutine dneupd with IPARAM(5)NCV and " + "increase the size of the arrays DR and DI to have " + "dimension at least dimension NCV and allocate at least NCV " + "columns for Z. NOTE: Not necessary if Z and V share " + "the same space. Please notify the authors if this error" + "occurs.", + -1: "N must be positive.", + -2: "NEV must be positive.", + -3: "NCV-NEV >= 2 and less than or equal to N.", + -5: "WHICH must be one of 'LM', 'SM', 'LR', 'SR', 'LI', 'SI'", + -6: "BMAT must be one of 'I' or 'G'.", + -7: "Length of private work WORKL array is not sufficient.", + -8: "Error return from calculation of a real Schur form. " + "Informational error from LAPACK routine dlahqr .", + -9: "Error return from calculation of eigenvectors. " + "Informational error from LAPACK routine dtrevc.", + -10: "IPARAM(7) must be 1,2,3,4.", + -11: "IPARAM(7) = 1 and BMAT = 'G' are incompatible.", + -12: "HOWMNY = 'S' not yet implemented", + -13: "HOWMNY must be one of 'A' or 'P' if RVEC = .true.", + -14: "DNAUPD did not find any eigenvalues to sufficient " + "accuracy.", + -15: "DNEUPD got a different count of the number of converged " + "Ritz values than DNAUPD got. This indicates the user " + "probably made an error in passing data from DNAUPD to " + "DNEUPD or that the data was modified before entering " + "DNEUPD", +} + +SNEUPD_ERRORS = DNEUPD_ERRORS.copy() +SNEUPD_ERRORS[1] = ("The Schur form computed by LAPACK routine slahqr " + "could not be reordered by LAPACK routine strsen . " + "Re-enter subroutine dneupd with IPARAM(5)=NCV and " + "increase the size of the arrays DR and DI to have " + "dimension at least dimension NCV and allocate at least " + "NCV columns for Z. NOTE: Not necessary if Z and V share " + "the same space. Please notify the authors if this error " + "occurs.") +SNEUPD_ERRORS[-14] = ("SNAUPD did not find any eigenvalues to sufficient " + "accuracy.") +SNEUPD_ERRORS[-15] = ("SNEUPD got a different count of the number of " + "converged Ritz values than SNAUPD got. This indicates " + "the user probably made an error in passing data from " + "SNAUPD to SNEUPD or that the data was modified before " + "entering SNEUPD") + +ZNEUPD_ERRORS = {0: "Normal exit.", + 1: "The Schur form computed by LAPACK routine csheqr " + "could not be reordered by LAPACK routine ztrsen. " + "Re-enter subroutine zneupd with IPARAM(5)=NCV and " + "increase the size of the array D to have " + "dimension at least dimension NCV and allocate at least " + "NCV columns for Z. NOTE: Not necessary if Z and V share " + "the same space. Please notify the authors if this error " + "occurs.", + -1: "N must be positive.", + -2: "NEV must be positive.", + -3: "NCV-NEV >= 1 and less than or equal to N.", + -5: "WHICH must be one of 'LM', 'SM', 'LR', 'SR', 'LI', 'SI'", + -6: "BMAT must be one of 'I' or 'G'.", + -7: "Length of private work WORKL array is not sufficient.", + -8: "Error return from LAPACK eigenvalue calculation. " + "This should never happened.", + -9: "Error return from calculation of eigenvectors. " + "Informational error from LAPACK routine ztrevc.", + -10: "IPARAM(7) must be 1,2,3", + -11: "IPARAM(7) = 1 and BMAT = 'G' are incompatible.", + -12: "HOWMNY = 'S' not yet implemented", + -13: "HOWMNY must be one of 'A' or 'P' if RVEC = .true.", + -14: "ZNAUPD did not find any eigenvalues to sufficient " + "accuracy.", + -15: "ZNEUPD got a different count of the number of " + "converged Ritz values than ZNAUPD got. This " + "indicates the user probably made an error in passing " + "data from ZNAUPD to ZNEUPD or that the data was " + "modified before entering ZNEUPD" + } + +CNEUPD_ERRORS = ZNEUPD_ERRORS.copy() +CNEUPD_ERRORS[-14] = ("CNAUPD did not find any eigenvalues to sufficient " + "accuracy.") +CNEUPD_ERRORS[-15] = ("CNEUPD got a different count of the number of " + "converged Ritz values than CNAUPD got. This indicates " + "the user probably made an error in passing data from " + "CNAUPD to CNEUPD or that the data was modified before " + "entering CNEUPD") + +DSEUPD_ERRORS = { + 0: "Normal exit.", + -1: "N must be positive.", + -2: "NEV must be positive.", + -3: "NCV must be greater than NEV and less than or equal to N.", + -5: "WHICH must be one of 'LM', 'SM', 'LA', 'SA' or 'BE'.", + -6: "BMAT must be one of 'I' or 'G'.", + -7: "Length of private work WORKL array is not sufficient.", + -8: ("Error return from trid. eigenvalue calculation; " + "Information error from LAPACK routine dsteqr."), + -9: "Starting vector is zero.", + -10: "IPARAM(7) must be 1,2,3,4,5.", + -11: "IPARAM(7) = 1 and BMAT = 'G' are incompatible.", + -12: "NEV and WHICH = 'BE' are incompatible.", + -14: "DSAUPD did not find any eigenvalues to sufficient accuracy.", + -15: "HOWMNY must be one of 'A' or 'S' if RVEC = .true.", + -16: "HOWMNY = 'S' not yet implemented", + -17: ("DSEUPD got a different count of the number of converged " + "Ritz values than DSAUPD got. This indicates the user " + "probably made an error in passing data from DSAUPD to " + "DSEUPD or that the data was modified before entering " + "DSEUPD.") +} + +SSEUPD_ERRORS = DSEUPD_ERRORS.copy() +SSEUPD_ERRORS[-14] = ("SSAUPD did not find any eigenvalues " + "to sufficient accuracy.") +SSEUPD_ERRORS[-17] = ("SSEUPD got a different count of the number of " + "converged " + "Ritz values than SSAUPD got. This indicates the user " + "probably made an error in passing data from SSAUPD to " + "SSEUPD or that the data was modified before entering " + "SSEUPD.") + +_SAUPD_ERRORS = {'d': DSAUPD_ERRORS, + 's': SSAUPD_ERRORS} +_NAUPD_ERRORS = {'d': DNAUPD_ERRORS, + 's': SNAUPD_ERRORS, + 'z': ZNAUPD_ERRORS, + 'c': CNAUPD_ERRORS} +_SEUPD_ERRORS = {'d': DSEUPD_ERRORS, + 's': SSEUPD_ERRORS} +_NEUPD_ERRORS = {'d': DNEUPD_ERRORS, + 's': SNEUPD_ERRORS, + 'z': ZNEUPD_ERRORS, + 'c': CNEUPD_ERRORS} + +# accepted values of parameter WHICH in _SEUPD +_SEUPD_WHICH = ['LM', 'SM', 'LA', 'SA', 'BE'] + +# accepted values of parameter WHICH in _NAUPD +_NEUPD_WHICH = ['LM', 'SM', 'LR', 'SR', 'LI', 'SI'] + + +class ArpackError(RuntimeError): + """ + ARPACK error + """ + + def __init__(self, info, infodict=_NAUPD_ERRORS): + msg = infodict.get(info, "Unknown error") + RuntimeError.__init__(self, "ARPACK error %d: %s" % (info, msg)) + + +class ArpackNoConvergence(ArpackError): + """ + ARPACK iteration did not converge + + Attributes + ---------- + eigenvalues : ndarray + Partial result. Converged eigenvalues. + eigenvectors : ndarray + Partial result. Converged eigenvectors. + + """ + + def __init__(self, msg, eigenvalues, eigenvectors): + ArpackError.__init__(self, -1, {-1: msg}) + self.eigenvalues = eigenvalues + self.eigenvectors = eigenvectors + + +def choose_ncv(k): + """ + Choose number of lanczos vectors based on target number + of singular/eigen values and vectors to compute, k. + """ + return max(2 * k + 1, 20) + + +class _ArpackParams: + def __init__(self, n, k, tp, mode=1, sigma=None, + ncv=None, v0=None, maxiter=None, which="LM", tol=0): + if k <= 0: + raise ValueError("k must be positive, k=%d" % k) + + if maxiter is None: + maxiter = n * 10 + if maxiter <= 0: + raise ValueError("maxiter must be positive, maxiter=%d" % maxiter) + + if tp not in 'fdFD': + raise ValueError("matrix type must be 'f', 'd', 'F', or 'D'") + + if v0 is not None: + # ARPACK overwrites its initial resid, make a copy + self.resid = np.array(v0, copy=True) + info = 1 + else: + # ARPACK will use a random initial vector. + self.resid = np.zeros(n, tp) + info = 0 + + if sigma is None: + #sigma not used + self.sigma = 0 + else: + self.sigma = sigma + + if ncv is None: + ncv = choose_ncv(k) + ncv = min(ncv, n) + + self.v = np.zeros((n, ncv), tp) # holds Ritz vectors + self.iparam = np.zeros(11, arpack_int) + + # set solver mode and parameters + ishfts = 1 + self.mode = mode + self.iparam[0] = ishfts + self.iparam[2] = maxiter + self.iparam[3] = 1 + self.iparam[6] = mode + + self.n = n + self.tol = tol + self.k = k + self.maxiter = maxiter + self.ncv = ncv + self.which = which + self.tp = tp + self.info = info + + self.converged = False + self.ido = 0 + + def _raise_no_convergence(self): + msg = "No convergence (%d iterations, %d/%d eigenvectors converged)" + k_ok = self.iparam[4] + num_iter = self.iparam[2] + try: + ev, vec = self.extract(True) + except ArpackError as err: + msg = f"{msg} [{err}]" + ev = np.zeros((0,)) + vec = np.zeros((self.n, 0)) + k_ok = 0 + raise ArpackNoConvergence(msg % (num_iter, k_ok, self.k), ev, vec) + + +class _SymmetricArpackParams(_ArpackParams): + def __init__(self, n, k, tp, matvec, mode=1, M_matvec=None, + Minv_matvec=None, sigma=None, + ncv=None, v0=None, maxiter=None, which="LM", tol=0): + # The following modes are supported: + # mode = 1: + # Solve the standard eigenvalue problem: + # A*x = lambda*x : + # A - symmetric + # Arguments should be + # matvec = left multiplication by A + # M_matvec = None [not used] + # Minv_matvec = None [not used] + # + # mode = 2: + # Solve the general eigenvalue problem: + # A*x = lambda*M*x + # A - symmetric + # M - symmetric positive definite + # Arguments should be + # matvec = left multiplication by A + # M_matvec = left multiplication by M + # Minv_matvec = left multiplication by M^-1 + # + # mode = 3: + # Solve the general eigenvalue problem in shift-invert mode: + # A*x = lambda*M*x + # A - symmetric + # M - symmetric positive semi-definite + # Arguments should be + # matvec = None [not used] + # M_matvec = left multiplication by M + # or None, if M is the identity + # Minv_matvec = left multiplication by [A-sigma*M]^-1 + # + # mode = 4: + # Solve the general eigenvalue problem in Buckling mode: + # A*x = lambda*AG*x + # A - symmetric positive semi-definite + # AG - symmetric indefinite + # Arguments should be + # matvec = left multiplication by A + # M_matvec = None [not used] + # Minv_matvec = left multiplication by [A-sigma*AG]^-1 + # + # mode = 5: + # Solve the general eigenvalue problem in Cayley-transformed mode: + # A*x = lambda*M*x + # A - symmetric + # M - symmetric positive semi-definite + # Arguments should be + # matvec = left multiplication by A + # M_matvec = left multiplication by M + # or None, if M is the identity + # Minv_matvec = left multiplication by [A-sigma*M]^-1 + if mode == 1: + if matvec is None: + raise ValueError("matvec must be specified for mode=1") + if M_matvec is not None: + raise ValueError("M_matvec cannot be specified for mode=1") + if Minv_matvec is not None: + raise ValueError("Minv_matvec cannot be specified for mode=1") + + self.OP = matvec + self.B = lambda x: x + self.bmat = 'I' + elif mode == 2: + if matvec is None: + raise ValueError("matvec must be specified for mode=2") + if M_matvec is None: + raise ValueError("M_matvec must be specified for mode=2") + if Minv_matvec is None: + raise ValueError("Minv_matvec must be specified for mode=2") + + self.OP = lambda x: Minv_matvec(matvec(x)) + self.OPa = Minv_matvec + self.OPb = matvec + self.B = M_matvec + self.bmat = 'G' + elif mode == 3: + if matvec is not None: + raise ValueError("matvec must not be specified for mode=3") + if Minv_matvec is None: + raise ValueError("Minv_matvec must be specified for mode=3") + + if M_matvec is None: + self.OP = Minv_matvec + self.OPa = Minv_matvec + self.B = lambda x: x + self.bmat = 'I' + else: + self.OP = lambda x: Minv_matvec(M_matvec(x)) + self.OPa = Minv_matvec + self.B = M_matvec + self.bmat = 'G' + elif mode == 4: + if matvec is None: + raise ValueError("matvec must be specified for mode=4") + if M_matvec is not None: + raise ValueError("M_matvec must not be specified for mode=4") + if Minv_matvec is None: + raise ValueError("Minv_matvec must be specified for mode=4") + self.OPa = Minv_matvec + self.OP = lambda x: self.OPa(matvec(x)) + self.B = matvec + self.bmat = 'G' + elif mode == 5: + if matvec is None: + raise ValueError("matvec must be specified for mode=5") + if Minv_matvec is None: + raise ValueError("Minv_matvec must be specified for mode=5") + + self.OPa = Minv_matvec + self.A_matvec = matvec + + if M_matvec is None: + self.OP = lambda x: Minv_matvec(matvec(x) + sigma * x) + self.B = lambda x: x + self.bmat = 'I' + else: + self.OP = lambda x: Minv_matvec(matvec(x) + + sigma * M_matvec(x)) + self.B = M_matvec + self.bmat = 'G' + else: + raise ValueError("mode=%i not implemented" % mode) + + if which not in _SEUPD_WHICH: + raise ValueError("which must be one of %s" + % ' '.join(_SEUPD_WHICH)) + if k >= n: + raise ValueError("k must be less than ndim(A), k=%d" % k) + + _ArpackParams.__init__(self, n, k, tp, mode, sigma, + ncv, v0, maxiter, which, tol) + + if self.ncv > n or self.ncv <= k: + raise ValueError("ncv must be k= n - 1: + raise ValueError("k must be less than ndim(A)-1, k=%d" % k) + + _ArpackParams.__init__(self, n, k, tp, mode, sigma, + ncv, v0, maxiter, which, tol) + + if self.ncv > n or self.ncv <= k + 1: + raise ValueError("ncv must be k+1 k, so we'll + # throw out this case. + nreturned -= 1 + i += 1 + + else: + # real matrix, mode 3 or 4, imag(sigma) is nonzero: + # see remark 3 in neupd.f + # Build complex eigenvalues from real and imaginary parts + i = 0 + while i <= k: + if abs(d[i].imag) == 0: + d[i] = np.dot(zr[:, i], self.matvec(zr[:, i])) + else: + if i < k: + z[:, i] = zr[:, i] + 1.0j * zr[:, i + 1] + z[:, i + 1] = z[:, i].conjugate() + d[i] = ((np.dot(zr[:, i], + self.matvec(zr[:, i])) + + np.dot(zr[:, i + 1], + self.matvec(zr[:, i + 1]))) + + 1j * (np.dot(zr[:, i], + self.matvec(zr[:, i + 1])) + - np.dot(zr[:, i + 1], + self.matvec(zr[:, i])))) + d[i + 1] = d[i].conj() + i += 1 + else: + #last eigenvalue is complex: the imaginary part of + # the eigenvector has not been returned + #this can only happen if nreturned > k, so we'll + # throw out this case. + nreturned -= 1 + i += 1 + + # Now we have k+1 possible eigenvalues and eigenvectors + # Return the ones specified by the keyword "which" + + if nreturned <= k: + # we got less or equal as many eigenvalues we wanted + d = d[:nreturned] + z = z[:, :nreturned] + else: + # we got one extra eigenvalue (likely a cc pair, but which?) + if self.mode in (1, 2): + rd = d + elif self.mode in (3, 4): + rd = 1 / (d - self.sigma) + + if self.which in ['LR', 'SR']: + ind = np.argsort(rd.real) + elif self.which in ['LI', 'SI']: + # for LI,SI ARPACK returns largest,smallest + # abs(imaginary) (complex pairs come together) + ind = np.argsort(abs(rd.imag)) + else: + ind = np.argsort(abs(rd)) + + if self.which in ['LR', 'LM', 'LI']: + ind = ind[-k:][::-1] + elif self.which in ['SR', 'SM', 'SI']: + ind = ind[:k] + + d = d[ind] + z = z[:, ind] + else: + # complex is so much simpler... + d, z, ierr =\ + self._arpack_extract(return_eigenvectors, + howmny, sselect, self.sigma, workev, + self.bmat, self.which, k, self.tol, self.resid, + self.v, self.iparam, self.ipntr, + self.workd, self.workl, self.rwork, ierr) + + if ierr != 0: + raise ArpackError(ierr, infodict=self.extract_infodict) + + k_ok = self.iparam[4] + d = d[:k_ok] + z = z[:, :k_ok] + + if return_eigenvectors: + return d, z + else: + return d + + +def _aslinearoperator_with_dtype(m): + m = aslinearoperator(m) + if not hasattr(m, 'dtype'): + x = np.zeros(m.shape[1]) + m.dtype = (m * x).dtype + return m + + +class SpLuInv(LinearOperator): + """ + SpLuInv: + helper class to repeatedly solve M*x=b + using a sparse LU-decomposition of M + """ + + def __init__(self, M): + self.M_lu = splu(M) + self.shape = M.shape + self.dtype = M.dtype + self.isreal = not np.issubdtype(self.dtype, np.complexfloating) + + def _matvec(self, x): + # careful here: splu.solve will throw away imaginary + # part of x if M is real + x = np.asarray(x) + if self.isreal and np.issubdtype(x.dtype, np.complexfloating): + return (self.M_lu.solve(np.real(x).astype(self.dtype)) + + 1j * self.M_lu.solve(np.imag(x).astype(self.dtype))) + else: + return self.M_lu.solve(x.astype(self.dtype)) + + +class LuInv(LinearOperator): + """ + LuInv: + helper class to repeatedly solve M*x=b + using an LU-decomposition of M + """ + + def __init__(self, M): + self.M_lu = lu_factor(M) + self.shape = M.shape + self.dtype = M.dtype + + def _matvec(self, x): + return lu_solve(self.M_lu, x) + + +def gmres_loose(A, b, tol): + """ + gmres with looser termination condition. + """ + b = np.asarray(b) + min_tol = 1000 * np.sqrt(b.size) * np.finfo(b.dtype).eps + return gmres(A, b, rtol=max(tol, min_tol), atol=0) + + +class IterInv(LinearOperator): + """ + IterInv: + helper class to repeatedly solve M*x=b + using an iterative method. + """ + + def __init__(self, M, ifunc=gmres_loose, tol=0): + self.M = M + if hasattr(M, 'dtype'): + self.dtype = M.dtype + else: + x = np.zeros(M.shape[1]) + self.dtype = (M * x).dtype + self.shape = M.shape + + if tol <= 0: + # when tol=0, ARPACK uses machine tolerance as calculated + # by LAPACK's _LAMCH function. We should match this + tol = 2 * np.finfo(self.dtype).eps + self.ifunc = ifunc + self.tol = tol + + def _matvec(self, x): + b, info = self.ifunc(self.M, x, tol=self.tol) + if info != 0: + raise ValueError("Error in inverting M: function " + "%s did not converge (info = %i)." + % (self.ifunc.__name__, info)) + return b + + +class IterOpInv(LinearOperator): + """ + IterOpInv: + helper class to repeatedly solve [A-sigma*M]*x = b + using an iterative method + """ + + def __init__(self, A, M, sigma, ifunc=gmres_loose, tol=0): + self.A = A + self.M = M + self.sigma = sigma + + def mult_func(x): + return A.matvec(x) - sigma * M.matvec(x) + + def mult_func_M_None(x): + return A.matvec(x) - sigma * x + + x = np.zeros(A.shape[1]) + if M is None: + dtype = mult_func_M_None(x).dtype + self.OP = LinearOperator(self.A.shape, + mult_func_M_None, + dtype=dtype) + else: + dtype = mult_func(x).dtype + self.OP = LinearOperator(self.A.shape, + mult_func, + dtype=dtype) + self.shape = A.shape + + if tol <= 0: + # when tol=0, ARPACK uses machine tolerance as calculated + # by LAPACK's _LAMCH function. We should match this + tol = 2 * np.finfo(self.OP.dtype).eps + self.ifunc = ifunc + self.tol = tol + + def _matvec(self, x): + b, info = self.ifunc(self.OP, x, tol=self.tol) + if info != 0: + raise ValueError("Error in inverting [A-sigma*M]: function " + "%s did not converge (info = %i)." + % (self.ifunc.__name__, info)) + return b + + @property + def dtype(self): + return self.OP.dtype + + +def _fast_spmatrix_to_csc(A, hermitian=False): + """Convert sparse matrix to CSC (by transposing, if possible)""" + if (A.format == "csr" and hermitian + and not np.issubdtype(A.dtype, np.complexfloating)): + return A.T + elif is_pydata_spmatrix(A): + # No need to convert + return A + else: + return A.tocsc() + + +def get_inv_matvec(M, hermitian=False, tol=0): + if isdense(M): + return LuInv(M).matvec + elif issparse(M) or is_pydata_spmatrix(M): + M = _fast_spmatrix_to_csc(M, hermitian=hermitian) + return SpLuInv(M).matvec + else: + return IterInv(M, tol=tol).matvec + + +def get_OPinv_matvec(A, M, sigma, hermitian=False, tol=0): + if sigma == 0: + return get_inv_matvec(A, hermitian=hermitian, tol=tol) + + if M is None: + #M is the identity matrix + if isdense(A): + if (np.issubdtype(A.dtype, np.complexfloating) + or np.imag(sigma) == 0): + A = np.copy(A) + else: + A = A + 0j + A.flat[::A.shape[1] + 1] -= sigma + return LuInv(A).matvec + elif issparse(A) or is_pydata_spmatrix(A): + A = A - sigma * eye(A.shape[0]) + A = _fast_spmatrix_to_csc(A, hermitian=hermitian) + return SpLuInv(A).matvec + else: + return IterOpInv(_aslinearoperator_with_dtype(A), + M, sigma, tol=tol).matvec + else: + if ((not isdense(A) and not issparse(A) and not is_pydata_spmatrix(A)) or + (not isdense(M) and not issparse(M) and not is_pydata_spmatrix(A))): + return IterOpInv(_aslinearoperator_with_dtype(A), + _aslinearoperator_with_dtype(M), + sigma, tol=tol).matvec + elif isdense(A) or isdense(M): + return LuInv(A - sigma * M).matvec + else: + OP = A - sigma * M + OP = _fast_spmatrix_to_csc(OP, hermitian=hermitian) + return SpLuInv(OP).matvec + + +# ARPACK is not threadsafe or reentrant (SAVE variables), so we need a +# lock and a re-entering check. +_ARPACK_LOCK = ReentrancyLock("Nested calls to eigs/eighs not allowed: " + "ARPACK is not re-entrant") + + +def eigs(A, k=6, M=None, sigma=None, which='LM', v0=None, + ncv=None, maxiter=None, tol=0, return_eigenvectors=True, + Minv=None, OPinv=None, OPpart=None): + """ + Find k eigenvalues and eigenvectors of the square matrix A. + + Solves ``A @ x[i] = w[i] * x[i]``, the standard eigenvalue problem + for w[i] eigenvalues with corresponding eigenvectors x[i]. + + If M is specified, solves ``A @ x[i] = w[i] * M @ x[i]``, the + generalized eigenvalue problem for w[i] eigenvalues + with corresponding eigenvectors x[i] + + Parameters + ---------- + A : ndarray, sparse matrix or LinearOperator + An array, sparse matrix, or LinearOperator representing + the operation ``A @ x``, where A is a real or complex square matrix. + k : int, optional + The number of eigenvalues and eigenvectors desired. + `k` must be smaller than N-1. It is not possible to compute all + eigenvectors of a matrix. + M : ndarray, sparse matrix or LinearOperator, optional + An array, sparse matrix, or LinearOperator representing + the operation M@x for the generalized eigenvalue problem + + A @ x = w * M @ x. + + M must represent a real symmetric matrix if A is real, and must + represent a complex Hermitian matrix if A is complex. For best + results, the data type of M should be the same as that of A. + Additionally: + + If `sigma` is None, M is positive definite + + If sigma is specified, M is positive semi-definite + + If sigma is None, eigs requires an operator to compute the solution + of the linear equation ``M @ x = b``. This is done internally via a + (sparse) LU decomposition for an explicit matrix M, or via an + iterative solver for a general linear operator. Alternatively, + the user can supply the matrix or operator Minv, which gives + ``x = Minv @ b = M^-1 @ b``. + sigma : real or complex, optional + Find eigenvalues near sigma using shift-invert mode. This requires + an operator to compute the solution of the linear system + ``[A - sigma * M] @ x = b``, where M is the identity matrix if + unspecified. This is computed internally via a (sparse) LU + decomposition for explicit matrices A & M, or via an iterative + solver if either A or M is a general linear operator. + Alternatively, the user can supply the matrix or operator OPinv, + which gives ``x = OPinv @ b = [A - sigma * M]^-1 @ b``. + For a real matrix A, shift-invert can either be done in imaginary + mode or real mode, specified by the parameter OPpart ('r' or 'i'). + Note that when sigma is specified, the keyword 'which' (below) + refers to the shifted eigenvalues ``w'[i]`` where: + + If A is real and OPpart == 'r' (default), + ``w'[i] = 1/2 * [1/(w[i]-sigma) + 1/(w[i]-conj(sigma))]``. + + If A is real and OPpart == 'i', + ``w'[i] = 1/2i * [1/(w[i]-sigma) - 1/(w[i]-conj(sigma))]``. + + If A is complex, ``w'[i] = 1/(w[i]-sigma)``. + + v0 : ndarray, optional + Starting vector for iteration. + Default: random + ncv : int, optional + The number of Lanczos vectors generated + `ncv` must be greater than `k`; it is recommended that ``ncv > 2*k``. + Default: ``min(n, max(2*k + 1, 20))`` + which : str, ['LM' | 'SM' | 'LR' | 'SR' | 'LI' | 'SI'], optional + Which `k` eigenvectors and eigenvalues to find: + + 'LM' : largest magnitude + + 'SM' : smallest magnitude + + 'LR' : largest real part + + 'SR' : smallest real part + + 'LI' : largest imaginary part + + 'SI' : smallest imaginary part + + When sigma != None, 'which' refers to the shifted eigenvalues w'[i] + (see discussion in 'sigma', above). ARPACK is generally better + at finding large values than small values. If small eigenvalues are + desired, consider using shift-invert mode for better performance. + maxiter : int, optional + Maximum number of Arnoldi update iterations allowed + Default: ``n*10`` + tol : float, optional + Relative accuracy for eigenvalues (stopping criterion) + The default value of 0 implies machine precision. + return_eigenvectors : bool, optional + Return eigenvectors (True) in addition to eigenvalues + Minv : ndarray, sparse matrix or LinearOperator, optional + See notes in M, above. + OPinv : ndarray, sparse matrix or LinearOperator, optional + See notes in sigma, above. + OPpart : {'r' or 'i'}, optional + See notes in sigma, above + + Returns + ------- + w : ndarray + Array of k eigenvalues. + v : ndarray + An array of `k` eigenvectors. + ``v[:, i]`` is the eigenvector corresponding to the eigenvalue w[i]. + + Raises + ------ + ArpackNoConvergence + When the requested convergence is not obtained. + The currently converged eigenvalues and eigenvectors can be found + as ``eigenvalues`` and ``eigenvectors`` attributes of the exception + object. + + See Also + -------- + eigsh : eigenvalues and eigenvectors for symmetric matrix A + svds : singular value decomposition for a matrix A + + Notes + ----- + This function is a wrapper to the ARPACK [1]_ SNEUPD, DNEUPD, CNEUPD, + ZNEUPD, functions which use the Implicitly Restarted Arnoldi Method to + find the eigenvalues and eigenvectors [2]_. + + References + ---------- + .. [1] ARPACK Software, https://github.com/opencollab/arpack-ng + .. [2] R. B. Lehoucq, D. C. Sorensen, and C. Yang, ARPACK USERS GUIDE: + Solution of Large Scale Eigenvalue Problems by Implicitly Restarted + Arnoldi Methods. SIAM, Philadelphia, PA, 1998. + + Examples + -------- + Find 6 eigenvectors of the identity matrix: + + >>> import numpy as np + >>> from scipy.sparse.linalg import eigs + >>> id = np.eye(13) + >>> vals, vecs = eigs(id, k=6) + >>> vals + array([ 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j, 1.+0.j]) + >>> vecs.shape + (13, 6) + + """ + if A.shape[0] != A.shape[1]: + raise ValueError(f'expected square matrix (shape={A.shape})') + if M is not None: + if M.shape != A.shape: + raise ValueError(f'wrong M dimensions {M.shape}, should be {A.shape}') + if np.dtype(M.dtype).char.lower() != np.dtype(A.dtype).char.lower(): + warnings.warn('M does not have the same type precision as A. ' + 'This may adversely affect ARPACK convergence', + stacklevel=2) + + n = A.shape[0] + + if k <= 0: + raise ValueError("k=%d must be greater than 0." % k) + + if k >= n - 1: + warnings.warn("k >= N - 1 for N * N square matrix. " + "Attempting to use scipy.linalg.eig instead.", + RuntimeWarning, stacklevel=2) + + if issparse(A): + raise TypeError("Cannot use scipy.linalg.eig for sparse A with " + "k >= N - 1. Use scipy.linalg.eig(A.toarray()) or" + " reduce k.") + if isinstance(A, LinearOperator): + raise TypeError("Cannot use scipy.linalg.eig for LinearOperator " + "A with k >= N - 1.") + if isinstance(M, LinearOperator): + raise TypeError("Cannot use scipy.linalg.eig for LinearOperator " + "M with k >= N - 1.") + + return eig(A, b=M, right=return_eigenvectors) + + if sigma is None: + matvec = _aslinearoperator_with_dtype(A).matvec + + if OPinv is not None: + raise ValueError("OPinv should not be specified " + "with sigma = None.") + if OPpart is not None: + raise ValueError("OPpart should not be specified with " + "sigma = None or complex A") + + if M is None: + #standard eigenvalue problem + mode = 1 + M_matvec = None + Minv_matvec = None + if Minv is not None: + raise ValueError("Minv should not be " + "specified with M = None.") + else: + #general eigenvalue problem + mode = 2 + if Minv is None: + Minv_matvec = get_inv_matvec(M, hermitian=True, tol=tol) + else: + Minv = _aslinearoperator_with_dtype(Minv) + Minv_matvec = Minv.matvec + M_matvec = _aslinearoperator_with_dtype(M).matvec + else: + #sigma is not None: shift-invert mode + if np.issubdtype(A.dtype, np.complexfloating): + if OPpart is not None: + raise ValueError("OPpart should not be specified " + "with sigma=None or complex A") + mode = 3 + elif OPpart is None or OPpart.lower() == 'r': + mode = 3 + elif OPpart.lower() == 'i': + if np.imag(sigma) == 0: + raise ValueError("OPpart cannot be 'i' if sigma is real") + mode = 4 + else: + raise ValueError("OPpart must be one of ('r','i')") + + matvec = _aslinearoperator_with_dtype(A).matvec + if Minv is not None: + raise ValueError("Minv should not be specified when sigma is") + if OPinv is None: + Minv_matvec = get_OPinv_matvec(A, M, sigma, + hermitian=False, tol=tol) + else: + OPinv = _aslinearoperator_with_dtype(OPinv) + Minv_matvec = OPinv.matvec + if M is None: + M_matvec = None + else: + M_matvec = _aslinearoperator_with_dtype(M).matvec + + params = _UnsymmetricArpackParams(n, k, A.dtype.char, matvec, mode, + M_matvec, Minv_matvec, sigma, + ncv, v0, maxiter, which, tol) + + with _ARPACK_LOCK: + while not params.converged: + params.iterate() + + return params.extract(return_eigenvectors) + + +def eigsh(A, k=6, M=None, sigma=None, which='LM', v0=None, + ncv=None, maxiter=None, tol=0, return_eigenvectors=True, + Minv=None, OPinv=None, mode='normal'): + """ + Find k eigenvalues and eigenvectors of the real symmetric square matrix + or complex Hermitian matrix A. + + Solves ``A @ x[i] = w[i] * x[i]``, the standard eigenvalue problem for + w[i] eigenvalues with corresponding eigenvectors x[i]. + + If M is specified, solves ``A @ x[i] = w[i] * M @ x[i]``, the + generalized eigenvalue problem for w[i] eigenvalues + with corresponding eigenvectors x[i]. + + Note that there is no specialized routine for the case when A is a complex + Hermitian matrix. In this case, ``eigsh()`` will call ``eigs()`` and return the + real parts of the eigenvalues thus obtained. + + Parameters + ---------- + A : ndarray, sparse matrix or LinearOperator + A square operator representing the operation ``A @ x``, where ``A`` is + real symmetric or complex Hermitian. For buckling mode (see below) + ``A`` must additionally be positive-definite. + k : int, optional + The number of eigenvalues and eigenvectors desired. + `k` must be smaller than N. It is not possible to compute all + eigenvectors of a matrix. + + Returns + ------- + w : array + Array of k eigenvalues. + v : array + An array representing the `k` eigenvectors. The column ``v[:, i]`` is + the eigenvector corresponding to the eigenvalue ``w[i]``. + + Other Parameters + ---------------- + M : An N x N matrix, array, sparse matrix, or linear operator representing + the operation ``M @ x`` for the generalized eigenvalue problem + + A @ x = w * M @ x. + + M must represent a real symmetric matrix if A is real, and must + represent a complex Hermitian matrix if A is complex. For best + results, the data type of M should be the same as that of A. + Additionally: + + If sigma is None, M is symmetric positive definite. + + If sigma is specified, M is symmetric positive semi-definite. + + In buckling mode, M is symmetric indefinite. + + If sigma is None, eigsh requires an operator to compute the solution + of the linear equation ``M @ x = b``. This is done internally via a + (sparse) LU decomposition for an explicit matrix M, or via an + iterative solver for a general linear operator. Alternatively, + the user can supply the matrix or operator Minv, which gives + ``x = Minv @ b = M^-1 @ b``. + sigma : real + Find eigenvalues near sigma using shift-invert mode. This requires + an operator to compute the solution of the linear system + ``[A - sigma * M] x = b``, where M is the identity matrix if + unspecified. This is computed internally via a (sparse) LU + decomposition for explicit matrices A & M, or via an iterative + solver if either A or M is a general linear operator. + Alternatively, the user can supply the matrix or operator OPinv, + which gives ``x = OPinv @ b = [A - sigma * M]^-1 @ b``. + Note that when sigma is specified, the keyword 'which' refers to + the shifted eigenvalues ``w'[i]`` where: + + if mode == 'normal', ``w'[i] = 1 / (w[i] - sigma)``. + + if mode == 'cayley', ``w'[i] = (w[i] + sigma) / (w[i] - sigma)``. + + if mode == 'buckling', ``w'[i] = w[i] / (w[i] - sigma)``. + + (see further discussion in 'mode' below) + v0 : ndarray, optional + Starting vector for iteration. + Default: random + ncv : int, optional + The number of Lanczos vectors generated ncv must be greater than k and + smaller than n; it is recommended that ``ncv > 2*k``. + Default: ``min(n, max(2*k + 1, 20))`` + which : str ['LM' | 'SM' | 'LA' | 'SA' | 'BE'] + If A is a complex Hermitian matrix, 'BE' is invalid. + Which `k` eigenvectors and eigenvalues to find: + + 'LM' : Largest (in magnitude) eigenvalues. + + 'SM' : Smallest (in magnitude) eigenvalues. + + 'LA' : Largest (algebraic) eigenvalues. + + 'SA' : Smallest (algebraic) eigenvalues. + + 'BE' : Half (k/2) from each end of the spectrum. + + When k is odd, return one more (k/2+1) from the high end. + When sigma != None, 'which' refers to the shifted eigenvalues ``w'[i]`` + (see discussion in 'sigma', above). ARPACK is generally better + at finding large values than small values. If small eigenvalues are + desired, consider using shift-invert mode for better performance. + maxiter : int, optional + Maximum number of Arnoldi update iterations allowed. + Default: ``n*10`` + tol : float + Relative accuracy for eigenvalues (stopping criterion). + The default value of 0 implies machine precision. + Minv : N x N matrix, array, sparse matrix, or LinearOperator + See notes in M, above. + OPinv : N x N matrix, array, sparse matrix, or LinearOperator + See notes in sigma, above. + return_eigenvectors : bool + Return eigenvectors (True) in addition to eigenvalues. + This value determines the order in which eigenvalues are sorted. + The sort order is also dependent on the `which` variable. + + For which = 'LM' or 'SA': + If `return_eigenvectors` is True, eigenvalues are sorted by + algebraic value. + + If `return_eigenvectors` is False, eigenvalues are sorted by + absolute value. + + For which = 'BE' or 'LA': + eigenvalues are always sorted by algebraic value. + + For which = 'SM': + If `return_eigenvectors` is True, eigenvalues are sorted by + algebraic value. + + If `return_eigenvectors` is False, eigenvalues are sorted by + decreasing absolute value. + + mode : string ['normal' | 'buckling' | 'cayley'] + Specify strategy to use for shift-invert mode. This argument applies + only for real-valued A and sigma != None. For shift-invert mode, + ARPACK internally solves the eigenvalue problem + ``OP @ x'[i] = w'[i] * B @ x'[i]`` + and transforms the resulting Ritz vectors x'[i] and Ritz values w'[i] + into the desired eigenvectors and eigenvalues of the problem + ``A @ x[i] = w[i] * M @ x[i]``. + The modes are as follows: + + 'normal' : + OP = [A - sigma * M]^-1 @ M, + B = M, + w'[i] = 1 / (w[i] - sigma) + + 'buckling' : + OP = [A - sigma * M]^-1 @ A, + B = A, + w'[i] = w[i] / (w[i] - sigma) + + 'cayley' : + OP = [A - sigma * M]^-1 @ [A + sigma * M], + B = M, + w'[i] = (w[i] + sigma) / (w[i] - sigma) + + The choice of mode will affect which eigenvalues are selected by + the keyword 'which', and can also impact the stability of + convergence (see [2] for a discussion). + + Raises + ------ + ArpackNoConvergence + When the requested convergence is not obtained. + + The currently converged eigenvalues and eigenvectors can be found + as ``eigenvalues`` and ``eigenvectors`` attributes of the exception + object. + + See Also + -------- + eigs : eigenvalues and eigenvectors for a general (nonsymmetric) matrix A + svds : singular value decomposition for a matrix A + + Notes + ----- + This function is a wrapper to the ARPACK [1]_ SSEUPD and DSEUPD + functions which use the Implicitly Restarted Lanczos Method to + find the eigenvalues and eigenvectors [2]_. + + References + ---------- + .. [1] ARPACK Software, https://github.com/opencollab/arpack-ng + .. [2] R. B. Lehoucq, D. C. Sorensen, and C. Yang, ARPACK USERS GUIDE: + Solution of Large Scale Eigenvalue Problems by Implicitly Restarted + Arnoldi Methods. SIAM, Philadelphia, PA, 1998. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg import eigsh + >>> identity = np.eye(13) + >>> eigenvalues, eigenvectors = eigsh(identity, k=6) + >>> eigenvalues + array([1., 1., 1., 1., 1., 1.]) + >>> eigenvectors.shape + (13, 6) + + """ + # complex Hermitian matrices should be solved with eigs + if np.issubdtype(A.dtype, np.complexfloating): + if mode != 'normal': + raise ValueError("mode=%s cannot be used with " + "complex matrix A" % mode) + if which == 'BE': + raise ValueError("which='BE' cannot be used with complex matrix A") + elif which == 'LA': + which = 'LR' + elif which == 'SA': + which = 'SR' + ret = eigs(A, k, M=M, sigma=sigma, which=which, v0=v0, + ncv=ncv, maxiter=maxiter, tol=tol, + return_eigenvectors=return_eigenvectors, Minv=Minv, + OPinv=OPinv) + + if return_eigenvectors: + return ret[0].real, ret[1] + else: + return ret.real + + if A.shape[0] != A.shape[1]: + raise ValueError(f'expected square matrix (shape={A.shape})') + if M is not None: + if M.shape != A.shape: + raise ValueError(f'wrong M dimensions {M.shape}, should be {A.shape}') + if np.dtype(M.dtype).char.lower() != np.dtype(A.dtype).char.lower(): + warnings.warn('M does not have the same type precision as A. ' + 'This may adversely affect ARPACK convergence', + stacklevel=2) + + n = A.shape[0] + + if k <= 0: + raise ValueError("k must be greater than 0.") + + if k >= n: + warnings.warn("k >= N for N * N square matrix. " + "Attempting to use scipy.linalg.eigh instead.", + RuntimeWarning, stacklevel=2) + + if issparse(A): + raise TypeError("Cannot use scipy.linalg.eigh for sparse A with " + "k >= N. Use scipy.linalg.eigh(A.toarray()) or" + " reduce k.") + if isinstance(A, LinearOperator): + raise TypeError("Cannot use scipy.linalg.eigh for LinearOperator " + "A with k >= N.") + if isinstance(M, LinearOperator): + raise TypeError("Cannot use scipy.linalg.eigh for LinearOperator " + "M with k >= N.") + + return eigh(A, b=M, eigvals_only=not return_eigenvectors) + + if sigma is None: + A = _aslinearoperator_with_dtype(A) + matvec = A.matvec + + if OPinv is not None: + raise ValueError("OPinv should not be specified " + "with sigma = None.") + if M is None: + #standard eigenvalue problem + mode = 1 + M_matvec = None + Minv_matvec = None + if Minv is not None: + raise ValueError("Minv should not be " + "specified with M = None.") + else: + #general eigenvalue problem + mode = 2 + if Minv is None: + Minv_matvec = get_inv_matvec(M, hermitian=True, tol=tol) + else: + Minv = _aslinearoperator_with_dtype(Minv) + Minv_matvec = Minv.matvec + M_matvec = _aslinearoperator_with_dtype(M).matvec + else: + # sigma is not None: shift-invert mode + if Minv is not None: + raise ValueError("Minv should not be specified when sigma is") + + # normal mode + if mode == 'normal': + mode = 3 + matvec = None + if OPinv is None: + Minv_matvec = get_OPinv_matvec(A, M, sigma, + hermitian=True, tol=tol) + else: + OPinv = _aslinearoperator_with_dtype(OPinv) + Minv_matvec = OPinv.matvec + if M is None: + M_matvec = None + else: + M = _aslinearoperator_with_dtype(M) + M_matvec = M.matvec + + # buckling mode + elif mode == 'buckling': + mode = 4 + if OPinv is None: + Minv_matvec = get_OPinv_matvec(A, M, sigma, + hermitian=True, tol=tol) + else: + Minv_matvec = _aslinearoperator_with_dtype(OPinv).matvec + matvec = _aslinearoperator_with_dtype(A).matvec + M_matvec = None + + # cayley-transform mode + elif mode == 'cayley': + mode = 5 + matvec = _aslinearoperator_with_dtype(A).matvec + if OPinv is None: + Minv_matvec = get_OPinv_matvec(A, M, sigma, + hermitian=True, tol=tol) + else: + Minv_matvec = _aslinearoperator_with_dtype(OPinv).matvec + if M is None: + M_matvec = None + else: + M_matvec = _aslinearoperator_with_dtype(M).matvec + + # unrecognized mode + else: + raise ValueError("unrecognized mode '%s'" % mode) + + params = _SymmetricArpackParams(n, k, A.dtype.char, matvec, mode, + M_matvec, Minv_matvec, sigma, + ncv, v0, maxiter, which, tol) + + with _ARPACK_LOCK: + while not params.converged: + params.iterate() + + return params.extract(return_eigenvectors) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/tests/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/tests/test_arpack.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/tests/test_arpack.py new file mode 100644 index 0000000000000000000000000000000000000000..be06263e56ad8fc7915c12db5682fffcb39e5cd6 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/arpack/tests/test_arpack.py @@ -0,0 +1,718 @@ +__usage__ = """ +To run tests locally: + python tests/test_arpack.py [-l] [-v] + +""" + +import threading +import itertools + +import numpy as np + +from numpy.testing import assert_allclose, assert_equal, suppress_warnings +from pytest import raises as assert_raises +import pytest + +from numpy import dot, conj, random +from scipy.linalg import eig, eigh +from scipy.sparse import csc_matrix, csr_matrix, diags, rand +from scipy.sparse.linalg import LinearOperator, aslinearoperator +from scipy.sparse.linalg._eigen.arpack import (eigs, eigsh, arpack, + ArpackNoConvergence) + + +from scipy._lib._gcutils import assert_deallocated, IS_PYPY + + +# precision for tests +_ndigits = {'f': 3, 'd': 11, 'F': 3, 'D': 11} + + +def _get_test_tolerance(type_char, mattype=None, D_type=None, which=None): + """ + Return tolerance values suitable for a given test: + + Parameters + ---------- + type_char : {'f', 'd', 'F', 'D'} + Data type in ARPACK eigenvalue problem + mattype : {csr_matrix, aslinearoperator, asarray}, optional + Linear operator type + + Returns + ------- + tol + Tolerance to pass to the ARPACK routine + rtol + Relative tolerance for outputs + atol + Absolute tolerance for outputs + + """ + + rtol = {'f': 3000 * np.finfo(np.float32).eps, + 'F': 3000 * np.finfo(np.float32).eps, + 'd': 2000 * np.finfo(np.float64).eps, + 'D': 2000 * np.finfo(np.float64).eps}[type_char] + atol = rtol + tol = 0 + + if mattype is aslinearoperator and type_char in ('f', 'F'): + # iterative methods in single precision: worse errors + # also: bump ARPACK tolerance so that the iterative method converges + tol = 30 * np.finfo(np.float32).eps + rtol *= 5 + + if mattype is csr_matrix and type_char in ('f', 'F'): + # sparse in single precision: worse errors + rtol *= 5 + + if ( + which in ('LM', 'SM', 'LA') + and D_type.name == "gen-hermitian-Mc" + ): + if type_char == 'F': + # missing case 1, 2, and more, from PR 14798 + rtol *= 5 + + if type_char == 'D': + # missing more cases, from PR 14798 + rtol *= 10 + atol *= 10 + + return tol, rtol, atol + + +def generate_matrix(N, complex_=False, hermitian=False, + pos_definite=False, sparse=False): + M = np.random.random((N, N)) + if complex_: + M = M + 1j * np.random.random((N, N)) + + if hermitian: + if pos_definite: + if sparse: + i = np.arange(N) + j = np.random.randint(N, size=N-2) + i, j = np.meshgrid(i, j) + M[i, j] = 0 + M = np.dot(M.conj(), M.T) + else: + M = np.dot(M.conj(), M.T) + if sparse: + i = np.random.randint(N, size=N * N // 4) + j = np.random.randint(N, size=N * N // 4) + ind = np.nonzero(i == j) + j[ind] = (j[ind] + 1) % N + M[i, j] = 0 + M[j, i] = 0 + else: + if sparse: + i = np.random.randint(N, size=N * N // 2) + j = np.random.randint(N, size=N * N // 2) + M[i, j] = 0 + return M + + +def generate_matrix_symmetric(N, pos_definite=False, sparse=False): + M = np.random.random((N, N)) + + M = 0.5 * (M + M.T) # Make M symmetric + + if pos_definite: + Id = N * np.eye(N) + if sparse: + M = csr_matrix(M) + M += Id + else: + if sparse: + M = csr_matrix(M) + + return M + + +def assert_allclose_cc(actual, desired, **kw): + """Almost equal or complex conjugates almost equal""" + try: + assert_allclose(actual, desired, **kw) + except AssertionError: + assert_allclose(actual, conj(desired), **kw) + + +def argsort_which(eigenvalues, typ, k, which, + sigma=None, OPpart=None, mode=None): + """Return sorted indices of eigenvalues using the "which" keyword + from eigs and eigsh""" + if sigma is None: + reval = np.round(eigenvalues, decimals=_ndigits[typ]) + else: + if mode is None or mode == 'normal': + if OPpart is None: + reval = 1. / (eigenvalues - sigma) + elif OPpart == 'r': + reval = 0.5 * (1. / (eigenvalues - sigma) + + 1. / (eigenvalues - np.conj(sigma))) + elif OPpart == 'i': + reval = -0.5j * (1. / (eigenvalues - sigma) + - 1. / (eigenvalues - np.conj(sigma))) + elif mode == 'cayley': + reval = (eigenvalues + sigma) / (eigenvalues - sigma) + elif mode == 'buckling': + reval = eigenvalues / (eigenvalues - sigma) + else: + raise ValueError("mode='%s' not recognized" % mode) + + reval = np.round(reval, decimals=_ndigits[typ]) + + if which in ['LM', 'SM']: + ind = np.argsort(abs(reval)) + elif which in ['LR', 'SR', 'LA', 'SA', 'BE']: + ind = np.argsort(np.real(reval)) + elif which in ['LI', 'SI']: + # for LI,SI ARPACK returns largest,smallest abs(imaginary) why? + if typ.islower(): + ind = np.argsort(abs(np.imag(reval))) + else: + ind = np.argsort(np.imag(reval)) + else: + raise ValueError("which='%s' is unrecognized" % which) + + if which in ['LM', 'LA', 'LR', 'LI']: + return ind[-k:] + elif which in ['SM', 'SA', 'SR', 'SI']: + return ind[:k] + elif which == 'BE': + return np.concatenate((ind[:k//2], ind[k//2-k:])) + + +def eval_evec(symmetric, d, typ, k, which, v0=None, sigma=None, + mattype=np.asarray, OPpart=None, mode='normal'): + general = ('bmat' in d) + + if symmetric: + eigs_func = eigsh + else: + eigs_func = eigs + + if general: + err = ("error for {}:general, typ={}, which={}, sigma={}, " + "mattype={}, OPpart={}, mode={}".format(eigs_func.__name__, + typ, which, sigma, + mattype.__name__, + OPpart, mode)) + else: + err = ("error for {}:standard, typ={}, which={}, sigma={}, " + "mattype={}, OPpart={}, mode={}".format(eigs_func.__name__, + typ, which, sigma, + mattype.__name__, + OPpart, mode)) + + a = d['mat'].astype(typ) + ac = mattype(a) + + if general: + b = d['bmat'].astype(typ) + bc = mattype(b) + + # get exact eigenvalues + exact_eval = d['eval'].astype(typ.upper()) + ind = argsort_which(exact_eval, typ, k, which, + sigma, OPpart, mode) + exact_eval = exact_eval[ind] + + # compute arpack eigenvalues + kwargs = dict(which=which, v0=v0, sigma=sigma) + if eigs_func is eigsh: + kwargs['mode'] = mode + else: + kwargs['OPpart'] = OPpart + + # compute suitable tolerances + kwargs['tol'], rtol, atol = _get_test_tolerance(typ, mattype, d, which) + # on rare occasions, ARPACK routines return results that are proper + # eigenvalues and -vectors, but not necessarily the ones requested in + # the parameter which. This is inherent to the Krylov methods, and + # should not be treated as a failure. If such a rare situation + # occurs, the calculation is tried again (but at most a few times). + ntries = 0 + while ntries < 5: + # solve + if general: + try: + eigenvalues, evec = eigs_func(ac, k, bc, **kwargs) + except ArpackNoConvergence: + kwargs['maxiter'] = 20*a.shape[0] + eigenvalues, evec = eigs_func(ac, k, bc, **kwargs) + else: + try: + eigenvalues, evec = eigs_func(ac, k, **kwargs) + except ArpackNoConvergence: + kwargs['maxiter'] = 20*a.shape[0] + eigenvalues, evec = eigs_func(ac, k, **kwargs) + + ind = argsort_which(eigenvalues, typ, k, which, + sigma, OPpart, mode) + eigenvalues = eigenvalues[ind] + evec = evec[:, ind] + + try: + # check eigenvalues + assert_allclose_cc(eigenvalues, exact_eval, rtol=rtol, atol=atol, + err_msg=err) + check_evecs = True + except AssertionError: + check_evecs = False + ntries += 1 + + if check_evecs: + # check eigenvectors + LHS = np.dot(a, evec) + if general: + RHS = eigenvalues * np.dot(b, evec) + else: + RHS = eigenvalues * evec + + assert_allclose(LHS, RHS, rtol=rtol, atol=atol, err_msg=err) + break + + # check eigenvalues + assert_allclose_cc(eigenvalues, exact_eval, rtol=rtol, atol=atol, err_msg=err) + + +class DictWithRepr(dict): + def __init__(self, name): + self.name = name + + def __repr__(self): + return "<%s>" % self.name + + +class SymmetricParams: + def __init__(self): + self.eigs = eigsh + self.which = ['LM', 'SM', 'LA', 'SA', 'BE'] + self.mattypes = [csr_matrix, aslinearoperator, np.asarray] + self.sigmas_modes = {None: ['normal'], + 0.5: ['normal', 'buckling', 'cayley']} + + # generate matrices + # these should all be float32 so that the eigenvalues + # are the same in float32 and float64 + N = 6 + np.random.seed(2300) + Ar = generate_matrix(N, hermitian=True, + pos_definite=True).astype('f').astype('d') + M = generate_matrix(N, hermitian=True, + pos_definite=True).astype('f').astype('d') + Ac = generate_matrix(N, hermitian=True, pos_definite=True, + complex_=True).astype('F').astype('D') + Mc = generate_matrix(N, hermitian=True, pos_definite=True, + complex_=True).astype('F').astype('D') + v0 = np.random.random(N) + + # standard symmetric problem + SS = DictWithRepr("std-symmetric") + SS['mat'] = Ar + SS['v0'] = v0 + SS['eval'] = eigh(SS['mat'], eigvals_only=True) + + # general symmetric problem + GS = DictWithRepr("gen-symmetric") + GS['mat'] = Ar + GS['bmat'] = M + GS['v0'] = v0 + GS['eval'] = eigh(GS['mat'], GS['bmat'], eigvals_only=True) + + # standard hermitian problem + SH = DictWithRepr("std-hermitian") + SH['mat'] = Ac + SH['v0'] = v0 + SH['eval'] = eigh(SH['mat'], eigvals_only=True) + + # general hermitian problem + GH = DictWithRepr("gen-hermitian") + GH['mat'] = Ac + GH['bmat'] = M + GH['v0'] = v0 + GH['eval'] = eigh(GH['mat'], GH['bmat'], eigvals_only=True) + + # general hermitian problem with hermitian M + GHc = DictWithRepr("gen-hermitian-Mc") + GHc['mat'] = Ac + GHc['bmat'] = Mc + GHc['v0'] = v0 + GHc['eval'] = eigh(GHc['mat'], GHc['bmat'], eigvals_only=True) + + self.real_test_cases = [SS, GS] + self.complex_test_cases = [SH, GH, GHc] + + +class NonSymmetricParams: + def __init__(self): + self.eigs = eigs + self.which = ['LM', 'LR', 'LI'] # , 'SM', 'LR', 'SR', 'LI', 'SI'] + self.mattypes = [csr_matrix, aslinearoperator, np.asarray] + self.sigmas_OPparts = {None: [None], + 0.1: ['r'], + 0.1 + 0.1j: ['r', 'i']} + + # generate matrices + # these should all be float32 so that the eigenvalues + # are the same in float32 and float64 + N = 6 + np.random.seed(2300) + Ar = generate_matrix(N).astype('f').astype('d') + M = generate_matrix(N, hermitian=True, + pos_definite=True).astype('f').astype('d') + Ac = generate_matrix(N, complex_=True).astype('F').astype('D') + v0 = np.random.random(N) + + # standard real nonsymmetric problem + SNR = DictWithRepr("std-real-nonsym") + SNR['mat'] = Ar + SNR['v0'] = v0 + SNR['eval'] = eig(SNR['mat'], left=False, right=False) + + # general real nonsymmetric problem + GNR = DictWithRepr("gen-real-nonsym") + GNR['mat'] = Ar + GNR['bmat'] = M + GNR['v0'] = v0 + GNR['eval'] = eig(GNR['mat'], GNR['bmat'], left=False, right=False) + + # standard complex nonsymmetric problem + SNC = DictWithRepr("std-cmplx-nonsym") + SNC['mat'] = Ac + SNC['v0'] = v0 + SNC['eval'] = eig(SNC['mat'], left=False, right=False) + + # general complex nonsymmetric problem + GNC = DictWithRepr("gen-cmplx-nonsym") + GNC['mat'] = Ac + GNC['bmat'] = M + GNC['v0'] = v0 + GNC['eval'] = eig(GNC['mat'], GNC['bmat'], left=False, right=False) + + self.real_test_cases = [SNR, GNR] + self.complex_test_cases = [SNC, GNC] + + +def test_symmetric_modes(): + params = SymmetricParams() + k = 2 + symmetric = True + for D in params.real_test_cases: + for typ in 'fd': + for which in params.which: + for mattype in params.mattypes: + for (sigma, modes) in params.sigmas_modes.items(): + for mode in modes: + eval_evec(symmetric, D, typ, k, which, + None, sigma, mattype, None, mode) + + +def test_hermitian_modes(): + params = SymmetricParams() + k = 2 + symmetric = True + for D in params.complex_test_cases: + for typ in 'FD': + for which in params.which: + if which == 'BE': + continue # BE invalid for complex + for mattype in params.mattypes: + for sigma in params.sigmas_modes: + eval_evec(symmetric, D, typ, k, which, + None, sigma, mattype) + + +def test_symmetric_starting_vector(): + params = SymmetricParams() + symmetric = True + for k in [1, 2, 3, 4, 5]: + for D in params.real_test_cases: + for typ in 'fd': + v0 = random.rand(len(D['v0'])).astype(typ) + eval_evec(symmetric, D, typ, k, 'LM', v0) + + +def test_symmetric_no_convergence(): + np.random.seed(1234) + m = generate_matrix(30, hermitian=True, pos_definite=True) + tol, rtol, atol = _get_test_tolerance('d') + try: + w, v = eigsh(m, 4, which='LM', v0=m[:, 0], maxiter=5, tol=tol, ncv=9) + raise AssertionError("Spurious no-error exit") + except ArpackNoConvergence as err: + k = len(err.eigenvalues) + if k <= 0: + raise AssertionError("Spurious no-eigenvalues-found case") from err + w, v = err.eigenvalues, err.eigenvectors + assert_allclose(dot(m, v), w * v, rtol=rtol, atol=atol) + + +def test_real_nonsymmetric_modes(): + params = NonSymmetricParams() + k = 2 + symmetric = False + for D in params.real_test_cases: + for typ in 'fd': + for which in params.which: + for mattype in params.mattypes: + for sigma, OPparts in params.sigmas_OPparts.items(): + for OPpart in OPparts: + eval_evec(symmetric, D, typ, k, which, + None, sigma, mattype, OPpart) + + +def test_complex_nonsymmetric_modes(): + params = NonSymmetricParams() + k = 2 + symmetric = False + for D in params.complex_test_cases: + for typ in 'DF': + for which in params.which: + for mattype in params.mattypes: + for sigma in params.sigmas_OPparts: + eval_evec(symmetric, D, typ, k, which, + None, sigma, mattype) + + +def test_standard_nonsymmetric_starting_vector(): + params = NonSymmetricParams() + sigma = None + symmetric = False + for k in [1, 2, 3, 4]: + for d in params.complex_test_cases: + for typ in 'FD': + A = d['mat'] + n = A.shape[0] + v0 = random.rand(n).astype(typ) + eval_evec(symmetric, d, typ, k, "LM", v0, sigma) + + +def test_general_nonsymmetric_starting_vector(): + params = NonSymmetricParams() + sigma = None + symmetric = False + for k in [1, 2, 3, 4]: + for d in params.complex_test_cases: + for typ in 'FD': + A = d['mat'] + n = A.shape[0] + v0 = random.rand(n).astype(typ) + eval_evec(symmetric, d, typ, k, "LM", v0, sigma) + + +def test_standard_nonsymmetric_no_convergence(): + np.random.seed(1234) + m = generate_matrix(30, complex_=True) + tol, rtol, atol = _get_test_tolerance('d') + try: + w, v = eigs(m, 4, which='LM', v0=m[:, 0], maxiter=5, tol=tol) + raise AssertionError("Spurious no-error exit") + except ArpackNoConvergence as err: + k = len(err.eigenvalues) + if k <= 0: + raise AssertionError("Spurious no-eigenvalues-found case") from err + w, v = err.eigenvalues, err.eigenvectors + for ww, vv in zip(w, v.T): + assert_allclose(dot(m, vv), ww * vv, rtol=rtol, atol=atol) + + +def test_eigen_bad_shapes(): + # A is not square. + A = csc_matrix(np.zeros((2, 3))) + assert_raises(ValueError, eigs, A) + + +def test_eigen_bad_kwargs(): + # Test eigen on wrong keyword argument + A = csc_matrix(np.zeros((8, 8))) + assert_raises(ValueError, eigs, A, which='XX') + + +def test_ticket_1459_arpack_crash(): + for dtype in [np.float32, np.float64]: + # This test does not seem to catch the issue for float32, + # but we made the same fix there, just to be sure + + N = 6 + k = 2 + + np.random.seed(2301) + A = np.random.random((N, N)).astype(dtype) + v0 = np.array([-0.71063568258907849895, -0.83185111795729227424, + -0.34365925382227402451, 0.46122533684552280420, + -0.58001341115969040629, -0.78844877570084292984e-01], + dtype=dtype) + + # Should not crash: + evals, evecs = eigs(A, k, v0=v0) + + +@pytest.mark.skipif(IS_PYPY, reason="Test not meaningful on PyPy") +def test_linearoperator_deallocation(): + # Check that the linear operators used by the Arpack wrappers are + # deallocatable by reference counting -- they are big objects, so + # Python's cyclic GC may not collect them fast enough before + # running out of memory if eigs/eigsh are called in a tight loop. + + M_d = np.eye(10) + M_s = csc_matrix(M_d) + M_o = aslinearoperator(M_d) + + with assert_deallocated(lambda: arpack.SpLuInv(M_s)): + pass + with assert_deallocated(lambda: arpack.LuInv(M_d)): + pass + with assert_deallocated(lambda: arpack.IterInv(M_s)): + pass + with assert_deallocated(lambda: arpack.IterOpInv(M_o, None, 0.3)): + pass + with assert_deallocated(lambda: arpack.IterOpInv(M_o, M_o, 0.3)): + pass + +def test_parallel_threads(): + results = [] + v0 = np.random.rand(50) + + def worker(): + x = diags([1, -2, 1], [-1, 0, 1], shape=(50, 50)) + w, v = eigs(x, k=3, v0=v0) + results.append(w) + + w, v = eigsh(x, k=3, v0=v0) + results.append(w) + + threads = [threading.Thread(target=worker) for k in range(10)] + for t in threads: + t.start() + for t in threads: + t.join() + + worker() + + for r in results: + assert_allclose(r, results[-1]) + + +def test_reentering(): + # Just some linear operator that calls eigs recursively + def A_matvec(x): + x = diags([1, -2, 1], [-1, 0, 1], shape=(50, 50)) + w, v = eigs(x, k=1) + return v / w[0] + A = LinearOperator(matvec=A_matvec, dtype=float, shape=(50, 50)) + + # The Fortran code is not reentrant, so this fails (gracefully, not crashing) + assert_raises(RuntimeError, eigs, A, k=1) + assert_raises(RuntimeError, eigsh, A, k=1) + + +def test_regression_arpackng_1315(): + # Check that issue arpack-ng/#1315 is not present. + # Adapted from arpack-ng/TESTS/bug_1315_single.c + # If this fails, then the installed ARPACK library is faulty. + + for dtype in [np.float32, np.float64]: + np.random.seed(1234) + + w0 = np.arange(1, 1000+1).astype(dtype) + A = diags([w0], [0], shape=(1000, 1000)) + + v0 = np.random.rand(1000).astype(dtype) + w, v = eigs(A, k=9, ncv=2*9+1, which="LM", v0=v0) + + assert_allclose(np.sort(w), np.sort(w0[-9:]), + rtol=1e-4) + + +def test_eigs_for_k_greater(): + # Test eigs() for k beyond limits. + A_sparse = diags([1, -2, 1], [-1, 0, 1], shape=(4, 4)) # sparse + A = generate_matrix(4, sparse=False) + M_dense = np.random.random((4, 4)) + M_sparse = generate_matrix(4, sparse=True) + M_linop = aslinearoperator(M_dense) + eig_tuple1 = eig(A, b=M_dense) + eig_tuple2 = eig(A, b=M_sparse) + + with suppress_warnings() as sup: + sup.filter(RuntimeWarning) + + assert_equal(eigs(A, M=M_dense, k=3), eig_tuple1) + assert_equal(eigs(A, M=M_dense, k=4), eig_tuple1) + assert_equal(eigs(A, M=M_dense, k=5), eig_tuple1) + assert_equal(eigs(A, M=M_sparse, k=5), eig_tuple2) + + # M as LinearOperator + assert_raises(TypeError, eigs, A, M=M_linop, k=3) + + # Test 'A' for different types + assert_raises(TypeError, eigs, aslinearoperator(A), k=3) + assert_raises(TypeError, eigs, A_sparse, k=3) + + +def test_eigsh_for_k_greater(): + # Test eigsh() for k beyond limits. + A_sparse = diags([1, -2, 1], [-1, 0, 1], shape=(4, 4)) # sparse + A = generate_matrix(4, sparse=False) + M_dense = generate_matrix_symmetric(4, pos_definite=True) + M_sparse = generate_matrix_symmetric(4, pos_definite=True, sparse=True) + M_linop = aslinearoperator(M_dense) + eig_tuple1 = eigh(A, b=M_dense) + eig_tuple2 = eigh(A, b=M_sparse) + + with suppress_warnings() as sup: + sup.filter(RuntimeWarning) + + assert_equal(eigsh(A, M=M_dense, k=4), eig_tuple1) + assert_equal(eigsh(A, M=M_dense, k=5), eig_tuple1) + assert_equal(eigsh(A, M=M_sparse, k=5), eig_tuple2) + + # M as LinearOperator + assert_raises(TypeError, eigsh, A, M=M_linop, k=4) + + # Test 'A' for different types + assert_raises(TypeError, eigsh, aslinearoperator(A), k=4) + assert_raises(TypeError, eigsh, A_sparse, M=M_dense, k=4) + + +def test_real_eigs_real_k_subset(): + np.random.seed(1) + + n = 10 + A = rand(n, n, density=0.5) + A.data *= 2 + A.data -= 1 + + v0 = np.ones(n) + + whichs = ['LM', 'SM', 'LR', 'SR', 'LI', 'SI'] + dtypes = [np.float32, np.float64] + + for which, sigma, dtype in itertools.product(whichs, [None, 0, 5], dtypes): + prev_w = np.array([], dtype=dtype) + eps = np.finfo(dtype).eps + for k in range(1, 9): + w, z = eigs(A.astype(dtype), k=k, which=which, sigma=sigma, + v0=v0.astype(dtype), tol=0) + assert_allclose(np.linalg.norm(A.dot(z) - z * w), 0, atol=np.sqrt(eps)) + + # Check that the set of eigenvalues for `k` is a subset of that for `k+1` + dist = abs(prev_w[:,None] - w).min(axis=1) + assert_allclose(dist, 0, atol=np.sqrt(eps)) + + prev_w = w + + # Check sort order + if sigma is None: + d = w + else: + d = 1 / (w - sigma) + + if which == 'LM': + # ARPACK is systematic for 'LM', but sort order + # appears not well defined for other modes + assert np.all(np.diff(abs(d)) <= 1e-6) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e0a1e30d13efe3fe581ed26840907ac7274ac1e0 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__init__.py @@ -0,0 +1,16 @@ +""" +Locally Optimal Block Preconditioned Conjugate Gradient Method (LOBPCG) + +LOBPCG is a preconditioned eigensolver for large symmetric positive definite +(SPD) generalized eigenproblems. + +Call the function lobpcg - see help for lobpcg.lobpcg. + +""" +from .lobpcg import * + +__all__ = [s for s in dir() if not s.startswith('_')] + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__pycache__/__init__.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a03f50896b5723e5058ca7f77928d22b55754846 Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__pycache__/__init__.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__pycache__/lobpcg.cpython-39.pyc b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__pycache__/lobpcg.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5b8071c5d02bcd6b53543f6fb24381794fc423a Binary files /dev/null and b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/__pycache__/lobpcg.cpython-39.pyc differ diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py new file mode 100644 index 0000000000000000000000000000000000000000..6dc6d4fa24b26b396bb817d432de9f4ac7665f21 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/lobpcg.py @@ -0,0 +1,1112 @@ +""" +Locally Optimal Block Preconditioned Conjugate Gradient Method (LOBPCG). + +References +---------- +.. [1] A. V. Knyazev (2001), + Toward the Optimal Preconditioned Eigensolver: Locally Optimal + Block Preconditioned Conjugate Gradient Method. + SIAM Journal on Scientific Computing 23, no. 2, + pp. 517-541. :doi:`10.1137/S1064827500366124` + +.. [2] A. V. Knyazev, I. Lashuk, M. E. Argentati, and E. Ovchinnikov (2007), + Block Locally Optimal Preconditioned Eigenvalue Xolvers (BLOPEX) + in hypre and PETSc. :arxiv:`0705.2626` + +.. [3] A. V. Knyazev's C and MATLAB implementations: + https://github.com/lobpcg/blopex +""" + +import warnings +import numpy as np +from scipy.linalg import (inv, eigh, cho_factor, cho_solve, + cholesky, LinAlgError) +from scipy.sparse.linalg import LinearOperator +from scipy.sparse import issparse + +__all__ = ["lobpcg"] + + +def _report_nonhermitian(M, name): + """ + Report if `M` is not a Hermitian matrix given its type. + """ + from scipy.linalg import norm + + md = M - M.T.conj() + nmd = norm(md, 1) + tol = 10 * np.finfo(M.dtype).eps + tol = max(tol, tol * norm(M, 1)) + if nmd > tol: + warnings.warn( + f"Matrix {name} of the type {M.dtype} is not Hermitian: " + f"condition: {nmd} < {tol} fails.", + UserWarning, stacklevel=4 + ) + +def _as2d(ar): + """ + If the input array is 2D return it, if it is 1D, append a dimension, + making it a column vector. + """ + if ar.ndim == 2: + return ar + else: # Assume 1! + aux = np.asarray(ar) + aux.shape = (ar.shape[0], 1) + return aux + + +def _makeMatMat(m): + if m is None: + return None + elif callable(m): + return lambda v: m(v) + else: + return lambda v: m @ v + + +def _matmul_inplace(x, y, verbosityLevel=0): + """Perform 'np.matmul' in-place if possible. + + If some sufficient conditions for inplace matmul are met, do so. + Otherwise try inplace update and fall back to overwrite if that fails. + """ + if x.flags["CARRAY"] and x.shape[1] == y.shape[1] and x.dtype == y.dtype: + # conditions where we can guarantee that inplace updates will work; + # i.e. x is not a view/slice, x & y have compatible dtypes, and the + # shape of the result of x @ y matches the shape of x. + np.matmul(x, y, out=x) + else: + # ideally, we'd have an exhaustive list of conditions above when + # inplace updates are possible; since we don't, we opportunistically + # try if it works, and fall back to overwriting if necessary + try: + np.matmul(x, y, out=x) + except Exception: + if verbosityLevel: + warnings.warn( + "Inplace update of x = x @ y failed, " + "x needs to be overwritten.", + UserWarning, stacklevel=3 + ) + x = x @ y + return x + + +def _applyConstraints(blockVectorV, factYBY, blockVectorBY, blockVectorY): + """Changes blockVectorV in-place.""" + YBV = blockVectorBY.T.conj() @ blockVectorV + tmp = cho_solve(factYBY, YBV) + blockVectorV -= blockVectorY @ tmp + + +def _b_orthonormalize(B, blockVectorV, blockVectorBV=None, + verbosityLevel=0): + """in-place B-orthonormalize the given block vector using Cholesky.""" + if blockVectorBV is None: + if B is None: + blockVectorBV = blockVectorV + else: + try: + blockVectorBV = B(blockVectorV) + except Exception as e: + if verbosityLevel: + warnings.warn( + f"Secondary MatMul call failed with error\n" + f"{e}\n", + UserWarning, stacklevel=3 + ) + return None, None, None + if blockVectorBV.shape != blockVectorV.shape: + raise ValueError( + f"The shape {blockVectorV.shape} " + f"of the orthogonalized matrix not preserved\n" + f"and changed to {blockVectorBV.shape} " + f"after multiplying by the secondary matrix.\n" + ) + + VBV = blockVectorV.T.conj() @ blockVectorBV + try: + # VBV is a Cholesky factor from now on... + VBV = cholesky(VBV, overwrite_a=True) + VBV = inv(VBV, overwrite_a=True) + blockVectorV = _matmul_inplace( + blockVectorV, VBV, + verbosityLevel=verbosityLevel + ) + if B is not None: + blockVectorBV = _matmul_inplace( + blockVectorBV, VBV, + verbosityLevel=verbosityLevel + ) + return blockVectorV, blockVectorBV, VBV + except LinAlgError: + if verbosityLevel: + warnings.warn( + "Cholesky has failed.", + UserWarning, stacklevel=3 + ) + return None, None, None + + +def _get_indx(_lambda, num, largest): + """Get `num` indices into `_lambda` depending on `largest` option.""" + ii = np.argsort(_lambda) + if largest: + ii = ii[:-num - 1:-1] + else: + ii = ii[:num] + + return ii + + +def _handle_gramA_gramB_verbosity(gramA, gramB, verbosityLevel): + if verbosityLevel: + _report_nonhermitian(gramA, "gramA") + _report_nonhermitian(gramB, "gramB") + + +def lobpcg( + A, + X, + B=None, + M=None, + Y=None, + tol=None, + maxiter=None, + largest=True, + verbosityLevel=0, + retLambdaHistory=False, + retResidualNormsHistory=False, + restartControl=20, +): + """Locally Optimal Block Preconditioned Conjugate Gradient Method (LOBPCG). + + LOBPCG is a preconditioned eigensolver for large real symmetric and complex + Hermitian definite generalized eigenproblems. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator, callable object} + The Hermitian linear operator of the problem, usually given by a + sparse matrix. Often called the "stiffness matrix". + X : ndarray, float32 or float64 + Initial approximation to the ``k`` eigenvectors (non-sparse). + If `A` has ``shape=(n,n)`` then `X` must have ``shape=(n,k)``. + B : {sparse matrix, ndarray, LinearOperator, callable object} + Optional. By default ``B = None``, which is equivalent to identity. + The right hand side operator in a generalized eigenproblem if present. + Often called the "mass matrix". Must be Hermitian positive definite. + M : {sparse matrix, ndarray, LinearOperator, callable object} + Optional. By default ``M = None``, which is equivalent to identity. + Preconditioner aiming to accelerate convergence. + Y : ndarray, float32 or float64, default: None + An ``n-by-sizeY`` ndarray of constraints with ``sizeY < n``. + The iterations will be performed in the ``B``-orthogonal complement + of the column-space of `Y`. `Y` must be full rank if present. + tol : scalar, optional + The default is ``tol=n*sqrt(eps)``. + Solver tolerance for the stopping criterion. + maxiter : int, default: 20 + Maximum number of iterations. + largest : bool, default: True + When True, solve for the largest eigenvalues, otherwise the smallest. + verbosityLevel : int, optional + By default ``verbosityLevel=0`` no output. + Controls the solver standard/screen output. + retLambdaHistory : bool, default: False + Whether to return iterative eigenvalue history. + retResidualNormsHistory : bool, default: False + Whether to return iterative history of residual norms. + restartControl : int, optional. + Iterations restart if the residuals jump ``2**restartControl`` times + compared to the smallest recorded in ``retResidualNormsHistory``. + The default is ``restartControl=20``, making the restarts rare for + backward compatibility. + + Returns + ------- + lambda : ndarray of the shape ``(k, )``. + Array of ``k`` approximate eigenvalues. + v : ndarray of the same shape as ``X.shape``. + An array of ``k`` approximate eigenvectors. + lambdaHistory : ndarray, optional. + The eigenvalue history, if `retLambdaHistory` is ``True``. + ResidualNormsHistory : ndarray, optional. + The history of residual norms, if `retResidualNormsHistory` + is ``True``. + + Notes + ----- + The iterative loop runs ``maxit=maxiter`` (20 if ``maxit=None``) + iterations at most and finishes earlier if the tolerance is met. + Breaking backward compatibility with the previous version, LOBPCG + now returns the block of iterative vectors with the best accuracy rather + than the last one iterated, as a cure for possible divergence. + + If ``X.dtype == np.float32`` and user-provided operations/multiplications + by `A`, `B`, and `M` all preserve the ``np.float32`` data type, + all the calculations and the output are in ``np.float32``. + + The size of the iteration history output equals to the number of the best + (limited by `maxit`) iterations plus 3: initial, final, and postprocessing. + + If both `retLambdaHistory` and `retResidualNormsHistory` are ``True``, + the return tuple has the following format + ``(lambda, V, lambda history, residual norms history)``. + + In the following ``n`` denotes the matrix size and ``k`` the number + of required eigenvalues (smallest or largest). + + The LOBPCG code internally solves eigenproblems of the size ``3k`` on every + iteration by calling the dense eigensolver `eigh`, so if ``k`` is not + small enough compared to ``n``, it makes no sense to call the LOBPCG code. + Moreover, if one calls the LOBPCG algorithm for ``5k > n``, it would likely + break internally, so the code calls the standard function `eigh` instead. + It is not that ``n`` should be large for the LOBPCG to work, but rather the + ratio ``n / k`` should be large. It you call LOBPCG with ``k=1`` + and ``n=10``, it works though ``n`` is small. The method is intended + for extremely large ``n / k``. + + The convergence speed depends basically on three factors: + + 1. Quality of the initial approximations `X` to the seeking eigenvectors. + Randomly distributed around the origin vectors work well if no better + choice is known. + + 2. Relative separation of the desired eigenvalues from the rest + of the eigenvalues. One can vary ``k`` to improve the separation. + + 3. Proper preconditioning to shrink the spectral spread. + For example, a rod vibration test problem (under tests + directory) is ill-conditioned for large ``n``, so convergence will be + slow, unless efficient preconditioning is used. For this specific + problem, a good simple preconditioner function would be a linear solve + for `A`, which is easy to code since `A` is tridiagonal. + + References + ---------- + .. [1] A. V. Knyazev (2001), + Toward the Optimal Preconditioned Eigensolver: Locally Optimal + Block Preconditioned Conjugate Gradient Method. + SIAM Journal on Scientific Computing 23, no. 2, + pp. 517-541. :doi:`10.1137/S1064827500366124` + + .. [2] A. V. Knyazev, I. Lashuk, M. E. Argentati, and E. Ovchinnikov + (2007), Block Locally Optimal Preconditioned Eigenvalue Xolvers + (BLOPEX) in hypre and PETSc. :arxiv:`0705.2626` + + .. [3] A. V. Knyazev's C and MATLAB implementations: + https://github.com/lobpcg/blopex + + Examples + -------- + Our first example is minimalistic - find the largest eigenvalue of + a diagonal matrix by solving the non-generalized eigenvalue problem + ``A x = lambda x`` without constraints or preconditioning. + + >>> import numpy as np + >>> from scipy.sparse import spdiags + >>> from scipy.sparse.linalg import LinearOperator, aslinearoperator + >>> from scipy.sparse.linalg import lobpcg + + The square matrix size is + + >>> n = 100 + + and its diagonal entries are 1, ..., 100 defined by + + >>> vals = np.arange(1, n + 1).astype(np.int16) + + The first mandatory input parameter in this test is + the sparse diagonal matrix `A` + of the eigenvalue problem ``A x = lambda x`` to solve. + + >>> A = spdiags(vals, 0, n, n) + >>> A = A.astype(np.int16) + >>> A.toarray() + array([[ 1, 0, 0, ..., 0, 0, 0], + [ 0, 2, 0, ..., 0, 0, 0], + [ 0, 0, 3, ..., 0, 0, 0], + ..., + [ 0, 0, 0, ..., 98, 0, 0], + [ 0, 0, 0, ..., 0, 99, 0], + [ 0, 0, 0, ..., 0, 0, 100]], dtype=int16) + + The second mandatory input parameter `X` is a 2D array with the + row dimension determining the number of requested eigenvalues. + `X` is an initial guess for targeted eigenvectors. + `X` must have linearly independent columns. + If no initial approximations available, randomly oriented vectors + commonly work best, e.g., with components normally distributed + around zero or uniformly distributed on the interval [-1 1]. + Setting the initial approximations to dtype ``np.float32`` + forces all iterative values to dtype ``np.float32`` speeding up + the run while still allowing accurate eigenvalue computations. + + >>> k = 1 + >>> rng = np.random.default_rng() + >>> X = rng.normal(size=(n, k)) + >>> X = X.astype(np.float32) + + >>> eigenvalues, _ = lobpcg(A, X, maxiter=60) + >>> eigenvalues + array([100.]) + >>> eigenvalues.dtype + dtype('float32') + + `lobpcg` needs only access the matrix product with `A` rather + then the matrix itself. Since the matrix `A` is diagonal in + this example, one can write a function of the matrix product + ``A @ X`` using the diagonal values ``vals`` only, e.g., by + element-wise multiplication with broadcasting in the lambda-function + + >>> A_lambda = lambda X: vals[:, np.newaxis] * X + + or the regular function + + >>> def A_matmat(X): + ... return vals[:, np.newaxis] * X + + and use the handle to one of these callables as an input + + >>> eigenvalues, _ = lobpcg(A_lambda, X, maxiter=60) + >>> eigenvalues + array([100.]) + >>> eigenvalues, _ = lobpcg(A_matmat, X, maxiter=60) + >>> eigenvalues + array([100.]) + + The traditional callable `LinearOperator` is no longer + necessary but still supported as the input to `lobpcg`. + Specifying ``matmat=A_matmat`` explicitly improves performance. + + >>> A_lo = LinearOperator((n, n), matvec=A_matmat, matmat=A_matmat, dtype=np.int16) + >>> eigenvalues, _ = lobpcg(A_lo, X, maxiter=80) + >>> eigenvalues + array([100.]) + + The least efficient callable option is `aslinearoperator`: + + >>> eigenvalues, _ = lobpcg(aslinearoperator(A), X, maxiter=80) + >>> eigenvalues + array([100.]) + + We now switch to computing the three smallest eigenvalues specifying + + >>> k = 3 + >>> X = np.random.default_rng().normal(size=(n, k)) + + and ``largest=False`` parameter + + >>> eigenvalues, _ = lobpcg(A, X, largest=False, maxiter=80) + >>> print(eigenvalues) + [1. 2. 3.] + + The next example illustrates computing 3 smallest eigenvalues of + the same matrix `A` given by the function handle ``A_matmat`` but + with constraints and preconditioning. + + Constraints - an optional input parameter is a 2D array comprising + of column vectors that the eigenvectors must be orthogonal to + + >>> Y = np.eye(n, 3) + + The preconditioner acts as the inverse of `A` in this example, but + in the reduced precision ``np.float32`` even though the initial `X` + and thus all iterates and the output are in full ``np.float64``. + + >>> inv_vals = 1./vals + >>> inv_vals = inv_vals.astype(np.float32) + >>> M = lambda X: inv_vals[:, np.newaxis] * X + + Let us now solve the eigenvalue problem for the matrix `A` first + without preconditioning requesting 80 iterations + + >>> eigenvalues, _ = lobpcg(A_matmat, X, Y=Y, largest=False, maxiter=80) + >>> eigenvalues + array([4., 5., 6.]) + >>> eigenvalues.dtype + dtype('float64') + + With preconditioning we need only 20 iterations from the same `X` + + >>> eigenvalues, _ = lobpcg(A_matmat, X, Y=Y, M=M, largest=False, maxiter=20) + >>> eigenvalues + array([4., 5., 6.]) + + Note that the vectors passed in `Y` are the eigenvectors of the 3 + smallest eigenvalues. The results returned above are orthogonal to those. + + The primary matrix `A` may be indefinite, e.g., after shifting + ``vals`` by 50 from 1, ..., 100 to -49, ..., 50, we still can compute + the 3 smallest or largest eigenvalues. + + >>> vals = vals - 50 + >>> X = rng.normal(size=(n, k)) + >>> eigenvalues, _ = lobpcg(A_matmat, X, largest=False, maxiter=99) + >>> eigenvalues + array([-49., -48., -47.]) + >>> eigenvalues, _ = lobpcg(A_matmat, X, largest=True, maxiter=99) + >>> eigenvalues + array([50., 49., 48.]) + + """ + blockVectorX = X + bestblockVectorX = blockVectorX + blockVectorY = Y + residualTolerance = tol + if maxiter is None: + maxiter = 20 + + bestIterationNumber = maxiter + + sizeY = 0 + if blockVectorY is not None: + if len(blockVectorY.shape) != 2: + warnings.warn( + f"Expected rank-2 array for argument Y, instead got " + f"{len(blockVectorY.shape)}, " + f"so ignore it and use no constraints.", + UserWarning, stacklevel=2 + ) + blockVectorY = None + else: + sizeY = blockVectorY.shape[1] + + # Block size. + if blockVectorX is None: + raise ValueError("The mandatory initial matrix X cannot be None") + if len(blockVectorX.shape) != 2: + raise ValueError("expected rank-2 array for argument X") + + n, sizeX = blockVectorX.shape + + # Data type of iterates, determined by X, must be inexact + if not np.issubdtype(blockVectorX.dtype, np.inexact): + warnings.warn( + f"Data type for argument X is {blockVectorX.dtype}, " + f"which is not inexact, so casted to np.float32.", + UserWarning, stacklevel=2 + ) + blockVectorX = np.asarray(blockVectorX, dtype=np.float32) + + if retLambdaHistory: + lambdaHistory = np.zeros((maxiter + 3, sizeX), + dtype=blockVectorX.dtype) + if retResidualNormsHistory: + residualNormsHistory = np.zeros((maxiter + 3, sizeX), + dtype=blockVectorX.dtype) + + if verbosityLevel: + aux = "Solving " + if B is None: + aux += "standard" + else: + aux += "generalized" + aux += " eigenvalue problem with" + if M is None: + aux += "out" + aux += " preconditioning\n\n" + aux += "matrix size %d\n" % n + aux += "block size %d\n\n" % sizeX + if blockVectorY is None: + aux += "No constraints\n\n" + else: + if sizeY > 1: + aux += "%d constraints\n\n" % sizeY + else: + aux += "%d constraint\n\n" % sizeY + print(aux) + + if (n - sizeY) < (5 * sizeX): + warnings.warn( + f"The problem size {n} minus the constraints size {sizeY} " + f"is too small relative to the block size {sizeX}. " + f"Using a dense eigensolver instead of LOBPCG iterations." + f"No output of the history of the iterations.", + UserWarning, stacklevel=2 + ) + + sizeX = min(sizeX, n) + + if blockVectorY is not None: + raise NotImplementedError( + "The dense eigensolver does not support constraints." + ) + + # Define the closed range of indices of eigenvalues to return. + if largest: + eigvals = (n - sizeX, n - 1) + else: + eigvals = (0, sizeX - 1) + + try: + if isinstance(A, LinearOperator): + A = A(np.eye(n, dtype=int)) + elif callable(A): + A = A(np.eye(n, dtype=int)) + if A.shape != (n, n): + raise ValueError( + f"The shape {A.shape} of the primary matrix\n" + f"defined by a callable object is wrong.\n" + ) + elif issparse(A): + A = A.toarray() + else: + A = np.asarray(A) + except Exception as e: + raise Exception( + f"Primary MatMul call failed with error\n" + f"{e}\n") + + if B is not None: + try: + if isinstance(B, LinearOperator): + B = B(np.eye(n, dtype=int)) + elif callable(B): + B = B(np.eye(n, dtype=int)) + if B.shape != (n, n): + raise ValueError( + f"The shape {B.shape} of the secondary matrix\n" + f"defined by a callable object is wrong.\n" + ) + elif issparse(B): + B = B.toarray() + else: + B = np.asarray(B) + except Exception as e: + raise Exception( + f"Secondary MatMul call failed with error\n" + f"{e}\n") + + try: + vals, vecs = eigh(A, + B, + subset_by_index=eigvals, + check_finite=False) + if largest: + # Reverse order to be compatible with eigs() in 'LM' mode. + vals = vals[::-1] + vecs = vecs[:, ::-1] + + return vals, vecs + except Exception as e: + raise Exception( + f"Dense eigensolver failed with error\n" + f"{e}\n" + ) + + if (residualTolerance is None) or (residualTolerance <= 0.0): + residualTolerance = np.sqrt(np.finfo(blockVectorX.dtype).eps) * n + + A = _makeMatMat(A) + B = _makeMatMat(B) + M = _makeMatMat(M) + + # Apply constraints to X. + if blockVectorY is not None: + + if B is not None: + blockVectorBY = B(blockVectorY) + if blockVectorBY.shape != blockVectorY.shape: + raise ValueError( + f"The shape {blockVectorY.shape} " + f"of the constraint not preserved\n" + f"and changed to {blockVectorBY.shape} " + f"after multiplying by the secondary matrix.\n" + ) + else: + blockVectorBY = blockVectorY + + # gramYBY is a dense array. + gramYBY = blockVectorY.T.conj() @ blockVectorBY + try: + # gramYBY is a Cholesky factor from now on... + gramYBY = cho_factor(gramYBY, overwrite_a=True) + except LinAlgError as e: + raise ValueError("Linearly dependent constraints") from e + + _applyConstraints(blockVectorX, gramYBY, blockVectorBY, blockVectorY) + + ## + # B-orthonormalize X. + blockVectorX, blockVectorBX, _ = _b_orthonormalize( + B, blockVectorX, verbosityLevel=verbosityLevel) + if blockVectorX is None: + raise ValueError("Linearly dependent initial approximations") + + ## + # Compute the initial Ritz vectors: solve the eigenproblem. + blockVectorAX = A(blockVectorX) + if blockVectorAX.shape != blockVectorX.shape: + raise ValueError( + f"The shape {blockVectorX.shape} " + f"of the initial approximations not preserved\n" + f"and changed to {blockVectorAX.shape} " + f"after multiplying by the primary matrix.\n" + ) + + gramXAX = blockVectorX.T.conj() @ blockVectorAX + + _lambda, eigBlockVector = eigh(gramXAX, check_finite=False) + ii = _get_indx(_lambda, sizeX, largest) + _lambda = _lambda[ii] + if retLambdaHistory: + lambdaHistory[0, :] = _lambda + + eigBlockVector = np.asarray(eigBlockVector[:, ii]) + blockVectorX = _matmul_inplace( + blockVectorX, eigBlockVector, + verbosityLevel=verbosityLevel + ) + blockVectorAX = _matmul_inplace( + blockVectorAX, eigBlockVector, + verbosityLevel=verbosityLevel + ) + if B is not None: + blockVectorBX = _matmul_inplace( + blockVectorBX, eigBlockVector, + verbosityLevel=verbosityLevel + ) + + ## + # Active index set. + activeMask = np.ones((sizeX,), dtype=bool) + + ## + # Main iteration loop. + + blockVectorP = None # set during iteration + blockVectorAP = None + blockVectorBP = None + + smallestResidualNorm = np.abs(np.finfo(blockVectorX.dtype).max) + + iterationNumber = -1 + restart = True + forcedRestart = False + explicitGramFlag = False + while iterationNumber < maxiter: + iterationNumber += 1 + + if B is not None: + aux = blockVectorBX * _lambda[np.newaxis, :] + else: + aux = blockVectorX * _lambda[np.newaxis, :] + + blockVectorR = blockVectorAX - aux + + aux = np.sum(blockVectorR.conj() * blockVectorR, 0) + residualNorms = np.sqrt(np.abs(aux)) + if retResidualNormsHistory: + residualNormsHistory[iterationNumber, :] = residualNorms + residualNorm = np.sum(np.abs(residualNorms)) / sizeX + + if residualNorm < smallestResidualNorm: + smallestResidualNorm = residualNorm + bestIterationNumber = iterationNumber + bestblockVectorX = blockVectorX + elif residualNorm > 2**restartControl * smallestResidualNorm: + forcedRestart = True + blockVectorAX = A(blockVectorX) + if blockVectorAX.shape != blockVectorX.shape: + raise ValueError( + f"The shape {blockVectorX.shape} " + f"of the restarted iterate not preserved\n" + f"and changed to {blockVectorAX.shape} " + f"after multiplying by the primary matrix.\n" + ) + if B is not None: + blockVectorBX = B(blockVectorX) + if blockVectorBX.shape != blockVectorX.shape: + raise ValueError( + f"The shape {blockVectorX.shape} " + f"of the restarted iterate not preserved\n" + f"and changed to {blockVectorBX.shape} " + f"after multiplying by the secondary matrix.\n" + ) + + ii = np.where(residualNorms > residualTolerance, True, False) + activeMask = activeMask & ii + currentBlockSize = activeMask.sum() + + if verbosityLevel: + print(f"iteration {iterationNumber}") + print(f"current block size: {currentBlockSize}") + print(f"eigenvalue(s):\n{_lambda}") + print(f"residual norm(s):\n{residualNorms}") + + if currentBlockSize == 0: + break + + activeBlockVectorR = _as2d(blockVectorR[:, activeMask]) + + if iterationNumber > 0: + activeBlockVectorP = _as2d(blockVectorP[:, activeMask]) + activeBlockVectorAP = _as2d(blockVectorAP[:, activeMask]) + if B is not None: + activeBlockVectorBP = _as2d(blockVectorBP[:, activeMask]) + + if M is not None: + # Apply preconditioner T to the active residuals. + activeBlockVectorR = M(activeBlockVectorR) + + ## + # Apply constraints to the preconditioned residuals. + if blockVectorY is not None: + _applyConstraints(activeBlockVectorR, + gramYBY, + blockVectorBY, + blockVectorY) + + ## + # B-orthogonalize the preconditioned residuals to X. + if B is not None: + activeBlockVectorR = activeBlockVectorR - ( + blockVectorX @ + (blockVectorBX.T.conj() @ activeBlockVectorR) + ) + else: + activeBlockVectorR = activeBlockVectorR - ( + blockVectorX @ + (blockVectorX.T.conj() @ activeBlockVectorR) + ) + + ## + # B-orthonormalize the preconditioned residuals. + aux = _b_orthonormalize( + B, activeBlockVectorR, verbosityLevel=verbosityLevel) + activeBlockVectorR, activeBlockVectorBR, _ = aux + + if activeBlockVectorR is None: + warnings.warn( + f"Failed at iteration {iterationNumber} with accuracies " + f"{residualNorms}\n not reaching the requested " + f"tolerance {residualTolerance}.", + UserWarning, stacklevel=2 + ) + break + activeBlockVectorAR = A(activeBlockVectorR) + + if iterationNumber > 0: + if B is not None: + aux = _b_orthonormalize( + B, activeBlockVectorP, activeBlockVectorBP, + verbosityLevel=verbosityLevel + ) + activeBlockVectorP, activeBlockVectorBP, invR = aux + else: + aux = _b_orthonormalize(B, activeBlockVectorP, + verbosityLevel=verbosityLevel) + activeBlockVectorP, _, invR = aux + # Function _b_orthonormalize returns None if Cholesky fails + if activeBlockVectorP is not None: + activeBlockVectorAP = _matmul_inplace( + activeBlockVectorAP, invR, + verbosityLevel=verbosityLevel + ) + restart = forcedRestart + else: + restart = True + + ## + # Perform the Rayleigh Ritz Procedure: + # Compute symmetric Gram matrices: + + if activeBlockVectorAR.dtype == "float32": + myeps = 1 + else: + myeps = np.sqrt(np.finfo(activeBlockVectorR.dtype).eps) + + if residualNorms.max() > myeps and not explicitGramFlag: + explicitGramFlag = False + else: + # Once explicitGramFlag, forever explicitGramFlag. + explicitGramFlag = True + + # Shared memory assignments to simplify the code + if B is None: + blockVectorBX = blockVectorX + activeBlockVectorBR = activeBlockVectorR + if not restart: + activeBlockVectorBP = activeBlockVectorP + + # Common submatrices: + gramXAR = np.dot(blockVectorX.T.conj(), activeBlockVectorAR) + gramRAR = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorAR) + + gramDtype = activeBlockVectorAR.dtype + if explicitGramFlag: + gramRAR = (gramRAR + gramRAR.T.conj()) / 2 + gramXAX = np.dot(blockVectorX.T.conj(), blockVectorAX) + gramXAX = (gramXAX + gramXAX.T.conj()) / 2 + gramXBX = np.dot(blockVectorX.T.conj(), blockVectorBX) + gramRBR = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorBR) + gramXBR = np.dot(blockVectorX.T.conj(), activeBlockVectorBR) + else: + gramXAX = np.diag(_lambda).astype(gramDtype) + gramXBX = np.eye(sizeX, dtype=gramDtype) + gramRBR = np.eye(currentBlockSize, dtype=gramDtype) + gramXBR = np.zeros((sizeX, currentBlockSize), dtype=gramDtype) + + if not restart: + gramXAP = np.dot(blockVectorX.T.conj(), activeBlockVectorAP) + gramRAP = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorAP) + gramPAP = np.dot(activeBlockVectorP.T.conj(), activeBlockVectorAP) + gramXBP = np.dot(blockVectorX.T.conj(), activeBlockVectorBP) + gramRBP = np.dot(activeBlockVectorR.T.conj(), activeBlockVectorBP) + if explicitGramFlag: + gramPAP = (gramPAP + gramPAP.T.conj()) / 2 + gramPBP = np.dot(activeBlockVectorP.T.conj(), + activeBlockVectorBP) + else: + gramPBP = np.eye(currentBlockSize, dtype=gramDtype) + + gramA = np.block( + [ + [gramXAX, gramXAR, gramXAP], + [gramXAR.T.conj(), gramRAR, gramRAP], + [gramXAP.T.conj(), gramRAP.T.conj(), gramPAP], + ] + ) + gramB = np.block( + [ + [gramXBX, gramXBR, gramXBP], + [gramXBR.T.conj(), gramRBR, gramRBP], + [gramXBP.T.conj(), gramRBP.T.conj(), gramPBP], + ] + ) + + _handle_gramA_gramB_verbosity(gramA, gramB, verbosityLevel) + + try: + _lambda, eigBlockVector = eigh(gramA, + gramB, + check_finite=False) + except LinAlgError as e: + # raise ValueError("eigh failed in lobpcg iterations") from e + if verbosityLevel: + warnings.warn( + f"eigh failed at iteration {iterationNumber} \n" + f"with error {e} causing a restart.\n", + UserWarning, stacklevel=2 + ) + # try again after dropping the direction vectors P from RR + restart = True + + if restart: + gramA = np.block([[gramXAX, gramXAR], [gramXAR.T.conj(), gramRAR]]) + gramB = np.block([[gramXBX, gramXBR], [gramXBR.T.conj(), gramRBR]]) + + _handle_gramA_gramB_verbosity(gramA, gramB, verbosityLevel) + + try: + _lambda, eigBlockVector = eigh(gramA, + gramB, + check_finite=False) + except LinAlgError as e: + # raise ValueError("eigh failed in lobpcg iterations") from e + warnings.warn( + f"eigh failed at iteration {iterationNumber} with error\n" + f"{e}\n", + UserWarning, stacklevel=2 + ) + break + + ii = _get_indx(_lambda, sizeX, largest) + _lambda = _lambda[ii] + eigBlockVector = eigBlockVector[:, ii] + if retLambdaHistory: + lambdaHistory[iterationNumber + 1, :] = _lambda + + # Compute Ritz vectors. + if B is not None: + if not restart: + eigBlockVectorX = eigBlockVector[:sizeX] + eigBlockVectorR = eigBlockVector[sizeX: + sizeX + currentBlockSize] + eigBlockVectorP = eigBlockVector[sizeX + currentBlockSize:] + + pp = np.dot(activeBlockVectorR, eigBlockVectorR) + pp += np.dot(activeBlockVectorP, eigBlockVectorP) + + app = np.dot(activeBlockVectorAR, eigBlockVectorR) + app += np.dot(activeBlockVectorAP, eigBlockVectorP) + + bpp = np.dot(activeBlockVectorBR, eigBlockVectorR) + bpp += np.dot(activeBlockVectorBP, eigBlockVectorP) + else: + eigBlockVectorX = eigBlockVector[:sizeX] + eigBlockVectorR = eigBlockVector[sizeX:] + + pp = np.dot(activeBlockVectorR, eigBlockVectorR) + app = np.dot(activeBlockVectorAR, eigBlockVectorR) + bpp = np.dot(activeBlockVectorBR, eigBlockVectorR) + + blockVectorX = np.dot(blockVectorX, eigBlockVectorX) + pp + blockVectorAX = np.dot(blockVectorAX, eigBlockVectorX) + app + blockVectorBX = np.dot(blockVectorBX, eigBlockVectorX) + bpp + + blockVectorP, blockVectorAP, blockVectorBP = pp, app, bpp + + else: + if not restart: + eigBlockVectorX = eigBlockVector[:sizeX] + eigBlockVectorR = eigBlockVector[sizeX: + sizeX + currentBlockSize] + eigBlockVectorP = eigBlockVector[sizeX + currentBlockSize:] + + pp = np.dot(activeBlockVectorR, eigBlockVectorR) + pp += np.dot(activeBlockVectorP, eigBlockVectorP) + + app = np.dot(activeBlockVectorAR, eigBlockVectorR) + app += np.dot(activeBlockVectorAP, eigBlockVectorP) + else: + eigBlockVectorX = eigBlockVector[:sizeX] + eigBlockVectorR = eigBlockVector[sizeX:] + + pp = np.dot(activeBlockVectorR, eigBlockVectorR) + app = np.dot(activeBlockVectorAR, eigBlockVectorR) + + blockVectorX = np.dot(blockVectorX, eigBlockVectorX) + pp + blockVectorAX = np.dot(blockVectorAX, eigBlockVectorX) + app + + blockVectorP, blockVectorAP = pp, app + + if B is not None: + aux = blockVectorBX * _lambda[np.newaxis, :] + else: + aux = blockVectorX * _lambda[np.newaxis, :] + + blockVectorR = blockVectorAX - aux + + aux = np.sum(blockVectorR.conj() * blockVectorR, 0) + residualNorms = np.sqrt(np.abs(aux)) + # Use old lambda in case of early loop exit. + if retLambdaHistory: + lambdaHistory[iterationNumber + 1, :] = _lambda + if retResidualNormsHistory: + residualNormsHistory[iterationNumber + 1, :] = residualNorms + residualNorm = np.sum(np.abs(residualNorms)) / sizeX + if residualNorm < smallestResidualNorm: + smallestResidualNorm = residualNorm + bestIterationNumber = iterationNumber + 1 + bestblockVectorX = blockVectorX + + if np.max(np.abs(residualNorms)) > residualTolerance: + warnings.warn( + f"Exited at iteration {iterationNumber} with accuracies \n" + f"{residualNorms}\n" + f"not reaching the requested tolerance {residualTolerance}.\n" + f"Use iteration {bestIterationNumber} instead with accuracy \n" + f"{smallestResidualNorm}.\n", + UserWarning, stacklevel=2 + ) + + if verbosityLevel: + print(f"Final iterative eigenvalue(s):\n{_lambda}") + print(f"Final iterative residual norm(s):\n{residualNorms}") + + blockVectorX = bestblockVectorX + # Making eigenvectors "exactly" satisfy the blockVectorY constrains + if blockVectorY is not None: + _applyConstraints(blockVectorX, + gramYBY, + blockVectorBY, + blockVectorY) + + # Making eigenvectors "exactly" othonormalized by final "exact" RR + blockVectorAX = A(blockVectorX) + if blockVectorAX.shape != blockVectorX.shape: + raise ValueError( + f"The shape {blockVectorX.shape} " + f"of the postprocessing iterate not preserved\n" + f"and changed to {blockVectorAX.shape} " + f"after multiplying by the primary matrix.\n" + ) + gramXAX = np.dot(blockVectorX.T.conj(), blockVectorAX) + + blockVectorBX = blockVectorX + if B is not None: + blockVectorBX = B(blockVectorX) + if blockVectorBX.shape != blockVectorX.shape: + raise ValueError( + f"The shape {blockVectorX.shape} " + f"of the postprocessing iterate not preserved\n" + f"and changed to {blockVectorBX.shape} " + f"after multiplying by the secondary matrix.\n" + ) + + gramXBX = np.dot(blockVectorX.T.conj(), blockVectorBX) + _handle_gramA_gramB_verbosity(gramXAX, gramXBX, verbosityLevel) + gramXAX = (gramXAX + gramXAX.T.conj()) / 2 + gramXBX = (gramXBX + gramXBX.T.conj()) / 2 + try: + _lambda, eigBlockVector = eigh(gramXAX, + gramXBX, + check_finite=False) + except LinAlgError as e: + raise ValueError("eigh has failed in lobpcg postprocessing") from e + + ii = _get_indx(_lambda, sizeX, largest) + _lambda = _lambda[ii] + eigBlockVector = np.asarray(eigBlockVector[:, ii]) + + blockVectorX = np.dot(blockVectorX, eigBlockVector) + blockVectorAX = np.dot(blockVectorAX, eigBlockVector) + + if B is not None: + blockVectorBX = np.dot(blockVectorBX, eigBlockVector) + aux = blockVectorBX * _lambda[np.newaxis, :] + else: + aux = blockVectorX * _lambda[np.newaxis, :] + + blockVectorR = blockVectorAX - aux + + aux = np.sum(blockVectorR.conj() * blockVectorR, 0) + residualNorms = np.sqrt(np.abs(aux)) + + if retLambdaHistory: + lambdaHistory[bestIterationNumber + 1, :] = _lambda + if retResidualNormsHistory: + residualNormsHistory[bestIterationNumber + 1, :] = residualNorms + + if retLambdaHistory: + lambdaHistory = lambdaHistory[ + : bestIterationNumber + 2, :] + if retResidualNormsHistory: + residualNormsHistory = residualNormsHistory[ + : bestIterationNumber + 2, :] + + if np.max(np.abs(residualNorms)) > residualTolerance: + warnings.warn( + f"Exited postprocessing with accuracies \n" + f"{residualNorms}\n" + f"not reaching the requested tolerance {residualTolerance}.", + UserWarning, stacklevel=2 + ) + + if verbosityLevel: + print(f"Final postprocessing eigenvalue(s):\n{_lambda}") + print(f"Final residual norm(s):\n{residualNorms}") + + if retLambdaHistory: + lambdaHistory = np.vsplit(lambdaHistory, np.shape(lambdaHistory)[0]) + lambdaHistory = [np.squeeze(i) for i in lambdaHistory] + if retResidualNormsHistory: + residualNormsHistory = np.vsplit(residualNormsHistory, + np.shape(residualNormsHistory)[0]) + residualNormsHistory = [np.squeeze(i) for i in residualNormsHistory] + + if retLambdaHistory: + if retResidualNormsHistory: + return _lambda, blockVectorX, lambdaHistory, residualNormsHistory + else: + return _lambda, blockVectorX, lambdaHistory + else: + if retResidualNormsHistory: + return _lambda, blockVectorX, residualNormsHistory + else: + return _lambda, blockVectorX diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/tests/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py new file mode 100644 index 0000000000000000000000000000000000000000..c2efe58ab75e32cc0f639b8363cef2f842774ad0 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/lobpcg/tests/test_lobpcg.py @@ -0,0 +1,645 @@ +""" Test functions for the sparse.linalg._eigen.lobpcg module +""" + +import itertools +import platform +import sys +import pytest +import numpy as np +from numpy import ones, r_, diag +from numpy.testing import (assert_almost_equal, assert_equal, + assert_allclose, assert_array_less) + +from scipy import sparse +from scipy.linalg import eig, eigh, toeplitz, orth +from scipy.sparse import spdiags, diags, eye, csr_matrix +from scipy.sparse.linalg import eigs, LinearOperator +from scipy.sparse.linalg._eigen.lobpcg import lobpcg +from scipy.sparse.linalg._eigen.lobpcg.lobpcg import _b_orthonormalize +from scipy._lib._util import np_long, np_ulong + +_IS_32BIT = (sys.maxsize < 2**32) + +INT_DTYPES = {np.intc, np_long, np.longlong, np.uintc, np_ulong, np.ulonglong} +# np.half is unsupported on many test systems so excluded +REAL_DTYPES = {np.float32, np.float64, np.longdouble} +COMPLEX_DTYPES = {np.complex64, np.complex128, np.clongdouble} +# use sorted list to ensure fixed order of tests +VDTYPES = sorted(REAL_DTYPES ^ COMPLEX_DTYPES, key=str) +MDTYPES = sorted(INT_DTYPES ^ REAL_DTYPES ^ COMPLEX_DTYPES, key=str) + + +def sign_align(A, B): + """Align signs of columns of A match those of B: column-wise remove + sign of A by multiplying with its sign then multiply in sign of B. + """ + return np.array([col_A * np.sign(col_A[0]) * np.sign(col_B[0]) + for col_A, col_B in zip(A.T, B.T)]).T + +def ElasticRod(n): + """Build the matrices for the generalized eigenvalue problem of the + fixed-free elastic rod vibration model. + """ + L = 1.0 + le = L/n + rho = 7.85e3 + S = 1.e-4 + E = 2.1e11 + mass = rho*S*le/6. + k = E*S/le + A = k*(diag(r_[2.*ones(n-1), 1])-diag(ones(n-1), 1)-diag(ones(n-1), -1)) + B = mass*(diag(r_[4.*ones(n-1), 2])+diag(ones(n-1), 1)+diag(ones(n-1), -1)) + return A, B + + +def MikotaPair(n): + """Build a pair of full diagonal matrices for the generalized eigenvalue + problem. The Mikota pair acts as a nice test since the eigenvalues are the + squares of the integers n, n=1,2,... + """ + x = np.arange(1, n+1) + B = diag(1./x) + y = np.arange(n-1, 0, -1) + z = np.arange(2*n-1, 0, -2) + A = diag(z)-diag(y, -1)-diag(y, 1) + return A, B + + +def compare_solutions(A, B, m): + """Check eig vs. lobpcg consistency. + """ + n = A.shape[0] + rnd = np.random.RandomState(0) + V = rnd.random((n, m)) + X = orth(V) + eigvals, _ = lobpcg(A, X, B=B, tol=1e-2, maxiter=50, largest=False) + eigvals.sort() + w, _ = eig(A, b=B) + w.sort() + assert_almost_equal(w[:int(m/2)], eigvals[:int(m/2)], decimal=2) + + +def test_Small(): + A, B = ElasticRod(10) + with pytest.warns(UserWarning, match="The problem size"): + compare_solutions(A, B, 10) + A, B = MikotaPair(10) + with pytest.warns(UserWarning, match="The problem size"): + compare_solutions(A, B, 10) + + +def test_ElasticRod(): + A, B = ElasticRod(20) + msg = "Exited at iteration.*|Exited postprocessing with accuracies.*" + with pytest.warns(UserWarning, match=msg): + compare_solutions(A, B, 2) + + +def test_MikotaPair(): + A, B = MikotaPair(20) + compare_solutions(A, B, 2) + + +@pytest.mark.parametrize("n", [50]) +@pytest.mark.parametrize("m", [1, 2, 10]) +@pytest.mark.parametrize("Vdtype", sorted(REAL_DTYPES, key=str)) +@pytest.mark.parametrize("Bdtype", sorted(REAL_DTYPES, key=str)) +@pytest.mark.parametrize("BVdtype", sorted(REAL_DTYPES, key=str)) +def test_b_orthonormalize(n, m, Vdtype, Bdtype, BVdtype): + """Test B-orthonormalization by Cholesky with callable 'B'. + The function '_b_orthonormalize' is key in LOBPCG but may + lead to numerical instabilities. The input vectors are often + badly scaled, so the function needs scale-invariant Cholesky; + see https://netlib.org/lapack/lawnspdf/lawn14.pdf. + """ + rnd = np.random.RandomState(0) + X = rnd.standard_normal((n, m)).astype(Vdtype) + Xcopy = np.copy(X) + vals = np.arange(1, n+1, dtype=float) + B = diags([vals], [0], (n, n)).astype(Bdtype) + BX = B @ X + BX = BX.astype(BVdtype) + dtype = min(X.dtype, B.dtype, BX.dtype) + # np.longdouble tol cannot be achieved on most systems + atol = m * n * max(np.finfo(dtype).eps, np.finfo(np.float64).eps) + + Xo, BXo, _ = _b_orthonormalize(lambda v: B @ v, X, BX) + # Check in-place. + assert_equal(X, Xo) + assert_equal(id(X), id(Xo)) + assert_equal(BX, BXo) + assert_equal(id(BX), id(BXo)) + # Check BXo. + assert_allclose(B @ Xo, BXo, atol=atol, rtol=atol) + # Check B-orthonormality + assert_allclose(Xo.T.conj() @ B @ Xo, np.identity(m), + atol=atol, rtol=atol) + # Repeat without BX in outputs + X = np.copy(Xcopy) + Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X) + assert_allclose(Xo, Xo1, atol=atol, rtol=atol) + assert_allclose(BXo, BXo1, atol=atol, rtol=atol) + # Check in-place. + assert_equal(X, Xo1) + assert_equal(id(X), id(Xo1)) + # Check BXo1. + assert_allclose(B @ Xo1, BXo1, atol=atol, rtol=atol) + + # Introduce column-scaling in X. + scaling = 1.0 / np.geomspace(10, 1e10, num=m) + X = Xcopy * scaling + X = X.astype(Vdtype) + BX = B @ X + BX = BX.astype(BVdtype) + # Check scaling-invariance of Cholesky-based orthonormalization + Xo1, BXo1, _ = _b_orthonormalize(lambda v: B @ v, X, BX) + # The output should be the same, up the signs of the columns. + Xo1 = sign_align(Xo1, Xo) + assert_allclose(Xo, Xo1, atol=atol, rtol=atol) + BXo1 = sign_align(BXo1, BXo) + assert_allclose(BXo, BXo1, atol=atol, rtol=atol) + + +@pytest.mark.filterwarnings("ignore:Exited at iteration 0") +@pytest.mark.filterwarnings("ignore:Exited postprocessing") +def test_nonhermitian_warning(capsys): + """Check the warning of a Ritz matrix being not Hermitian + by feeding a non-Hermitian input matrix. + Also check stdout since verbosityLevel=1 and lack of stderr. + """ + n = 10 + X = np.arange(n * 2).reshape(n, 2).astype(np.float32) + A = np.arange(n * n).reshape(n, n).astype(np.float32) + with pytest.warns(UserWarning, match="Matrix gramA"): + _, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0) + out, err = capsys.readouterr() # Capture output + assert out.startswith("Solving standard eigenvalue") # Test stdout + assert err == '' # Test empty stderr + # Make the matrix symmetric and the UserWarning disappears. + A += A.T + _, _ = lobpcg(A, X, verbosityLevel=1, maxiter=0) + out, err = capsys.readouterr() # Capture output + assert out.startswith("Solving standard eigenvalue") # Test stdout + assert err == '' # Test empty stderr + + +def test_regression(): + """Check the eigenvalue of the identity matrix is one. + """ + # https://mail.python.org/pipermail/scipy-user/2010-October/026944.html + n = 10 + X = np.ones((n, 1)) + A = np.identity(n) + w, _ = lobpcg(A, X) + assert_allclose(w, [1]) + + +@pytest.mark.filterwarnings("ignore:The problem size") +@pytest.mark.parametrize('n, m, m_excluded', [(30, 4, 3), (4, 2, 0)]) +def test_diagonal(n, m, m_excluded): + """Test ``m - m_excluded`` eigenvalues and eigenvectors of + diagonal matrices of the size ``n`` varying matrix formats: + dense array, spare matrix, and ``LinearOperator`` for both + matrixes in the generalized eigenvalue problem ``Av = cBv`` + and for the preconditioner. + """ + rnd = np.random.RandomState(0) + + # Define the generalized eigenvalue problem Av = cBv + # where (c, v) is a generalized eigenpair, + # A is the diagonal matrix whose entries are 1,...n, + # B is the identity matrix. + vals = np.arange(1, n+1, dtype=float) + A_s = diags([vals], [0], (n, n)) + A_a = A_s.toarray() + + def A_f(x): + return A_s @ x + + A_lo = LinearOperator(matvec=A_f, + matmat=A_f, + shape=(n, n), dtype=float) + + B_a = eye(n) + B_s = csr_matrix(B_a) + + def B_f(x): + return B_a @ x + + B_lo = LinearOperator(matvec=B_f, + matmat=B_f, + shape=(n, n), dtype=float) + + # Let the preconditioner M be the inverse of A. + M_s = diags([1./vals], [0], (n, n)) + M_a = M_s.toarray() + + def M_f(x): + return M_s @ x + + M_lo = LinearOperator(matvec=M_f, + matmat=M_f, + shape=(n, n), dtype=float) + + # Pick random initial vectors. + X = rnd.normal(size=(n, m)) + + # Require that the returned eigenvectors be in the orthogonal complement + # of the first few standard basis vectors. + if m_excluded > 0: + Y = np.eye(n, m_excluded) + else: + Y = None + + for A in [A_a, A_s, A_lo]: + for B in [B_a, B_s, B_lo]: + for M in [M_a, M_s, M_lo]: + eigvals, vecs = lobpcg(A, X, B, M=M, Y=Y, + maxiter=40, largest=False) + + assert_allclose(eigvals, np.arange(1+m_excluded, + 1+m_excluded+m)) + _check_eigen(A, eigvals, vecs, rtol=1e-3, atol=1e-3) + + +def _check_eigen(M, w, V, rtol=1e-8, atol=1e-14): + """Check if the eigenvalue residual is small. + """ + mult_wV = np.multiply(w, V) + dot_MV = M.dot(V) + assert_allclose(mult_wV, dot_MV, rtol=rtol, atol=atol) + + +def _check_fiedler(n, p): + """Check the Fiedler vector computation. + """ + # This is not necessarily the recommended way to find the Fiedler vector. + col = np.zeros(n) + col[1] = 1 + A = toeplitz(col) + D = np.diag(A.sum(axis=1)) + L = D - A + # Compute the full eigendecomposition using tricks, e.g. + # http://www.cs.yale.edu/homes/spielman/561/2009/lect02-09.pdf + tmp = np.pi * np.arange(n) / n + analytic_w = 2 * (1 - np.cos(tmp)) + analytic_V = np.cos(np.outer(np.arange(n) + 1/2, tmp)) + _check_eigen(L, analytic_w, analytic_V) + # Compute the full eigendecomposition using eigh. + eigh_w, eigh_V = eigh(L) + _check_eigen(L, eigh_w, eigh_V) + # Check that the first eigenvalue is near zero and that the rest agree. + assert_array_less(np.abs([eigh_w[0], analytic_w[0]]), 1e-14) + assert_allclose(eigh_w[1:], analytic_w[1:]) + + # Check small lobpcg eigenvalues. + X = analytic_V[:, :p] + lobpcg_w, lobpcg_V = lobpcg(L, X, largest=False) + assert_equal(lobpcg_w.shape, (p,)) + assert_equal(lobpcg_V.shape, (n, p)) + _check_eigen(L, lobpcg_w, lobpcg_V) + assert_array_less(np.abs(np.min(lobpcg_w)), 1e-14) + assert_allclose(np.sort(lobpcg_w)[1:], analytic_w[1:p]) + + # Check large lobpcg eigenvalues. + X = analytic_V[:, -p:] + lobpcg_w, lobpcg_V = lobpcg(L, X, largest=True) + assert_equal(lobpcg_w.shape, (p,)) + assert_equal(lobpcg_V.shape, (n, p)) + _check_eigen(L, lobpcg_w, lobpcg_V) + assert_allclose(np.sort(lobpcg_w), analytic_w[-p:]) + + # Look for the Fiedler vector using good but not exactly correct guesses. + fiedler_guess = np.concatenate((np.ones(n//2), -np.ones(n-n//2))) + X = np.vstack((np.ones(n), fiedler_guess)).T + lobpcg_w, _ = lobpcg(L, X, largest=False) + # Mathematically, the smaller eigenvalue should be zero + # and the larger should be the algebraic connectivity. + lobpcg_w = np.sort(lobpcg_w) + assert_allclose(lobpcg_w, analytic_w[:2], atol=1e-14) + + +def test_fiedler_small_8(): + """Check the dense workaround path for small matrices. + """ + # This triggers the dense path because 8 < 2*5. + with pytest.warns(UserWarning, match="The problem size"): + _check_fiedler(8, 2) + + +def test_fiedler_large_12(): + """Check the dense workaround path avoided for non-small matrices. + """ + # This does not trigger the dense path, because 2*5 <= 12. + _check_fiedler(12, 2) + + +@pytest.mark.filterwarnings("ignore:Failed at iteration") +@pytest.mark.filterwarnings("ignore:Exited at iteration") +@pytest.mark.filterwarnings("ignore:Exited postprocessing") +def test_failure_to_run_iterations(): + """Check that the code exits gracefully without breaking. Issue #10974. + The code may or not issue a warning, filtered out. Issue #15935, #17954. + """ + rnd = np.random.RandomState(0) + X = rnd.standard_normal((100, 10)) + A = X @ X.T + Q = rnd.standard_normal((X.shape[0], 4)) + eigenvalues, _ = lobpcg(A, Q, maxiter=40, tol=1e-12) + assert np.max(eigenvalues) > 0 + + +def test_failure_to_run_iterations_nonsymmetric(): + """Check that the code exists gracefully without breaking + if the matrix in not symmetric. + """ + A = np.zeros((10, 10)) + A[0, 1] = 1 + Q = np.ones((10, 1)) + msg = "Exited at iteration 2|Exited postprocessing with accuracies.*" + with pytest.warns(UserWarning, match=msg): + eigenvalues, _ = lobpcg(A, Q, maxiter=20) + assert np.max(eigenvalues) > 0 + + +@pytest.mark.filterwarnings("ignore:The problem size") +def test_hermitian(): + """Check complex-value Hermitian cases. + """ + rnd = np.random.RandomState(0) + + sizes = [3, 12] + ks = [1, 2] + gens = [True, False] + + for s, k, gen, dh, dx, db in ( + itertools.product(sizes, ks, gens, gens, gens, gens) + ): + H = rnd.random((s, s)) + 1.j * rnd.random((s, s)) + H = 10 * np.eye(s) + H + H.T.conj() + H = H.astype(np.complex128) if dh else H.astype(np.complex64) + + X = rnd.standard_normal((s, k)) + X = X + 1.j * rnd.standard_normal((s, k)) + X = X.astype(np.complex128) if dx else X.astype(np.complex64) + + if not gen: + B = np.eye(s) + w, v = lobpcg(H, X, maxiter=99, verbosityLevel=0) + # Also test mixing complex H with real B. + wb, _ = lobpcg(H, X, B, maxiter=99, verbosityLevel=0) + assert_allclose(w, wb, rtol=1e-6) + w0, _ = eigh(H) + else: + B = rnd.random((s, s)) + 1.j * rnd.random((s, s)) + B = 10 * np.eye(s) + B.dot(B.T.conj()) + B = B.astype(np.complex128) if db else B.astype(np.complex64) + w, v = lobpcg(H, X, B, maxiter=99, verbosityLevel=0) + w0, _ = eigh(H, B) + + for wx, vx in zip(w, v.T): + # Check eigenvector + assert_allclose(np.linalg.norm(H.dot(vx) - B.dot(vx) * wx) + / np.linalg.norm(H.dot(vx)), + 0, atol=5e-2, rtol=0) + + # Compare eigenvalues + j = np.argmin(abs(w0 - wx)) + assert_allclose(wx, w0[j], rtol=1e-4) + + +# The n=5 case tests the alternative small matrix code path that uses eigh(). +@pytest.mark.filterwarnings("ignore:The problem size") +@pytest.mark.parametrize('n, atol', [(20, 1e-3), (5, 1e-8)]) +def test_eigs_consistency(n, atol): + """Check eigs vs. lobpcg consistency. + """ + vals = np.arange(1, n+1, dtype=np.float64) + A = spdiags(vals, 0, n, n) + rnd = np.random.RandomState(0) + X = rnd.standard_normal((n, 2)) + lvals, lvecs = lobpcg(A, X, largest=True, maxiter=100) + vals, _ = eigs(A, k=2) + + _check_eigen(A, lvals, lvecs, atol=atol, rtol=0) + assert_allclose(np.sort(vals), np.sort(lvals), atol=1e-14) + + +def test_verbosity(): + """Check that nonzero verbosity level code runs. + """ + rnd = np.random.RandomState(0) + X = rnd.standard_normal((10, 10)) + A = X @ X.T + Q = rnd.standard_normal((X.shape[0], 1)) + msg = "Exited at iteration.*|Exited postprocessing with accuracies.*" + with pytest.warns(UserWarning, match=msg): + _, _ = lobpcg(A, Q, maxiter=3, verbosityLevel=9) + + +@pytest.mark.xfail(_IS_32BIT and sys.platform == 'win32', + reason="tolerance violation on windows") +@pytest.mark.xfail(platform.machine() == 'ppc64le', + reason="fails on ppc64le") +@pytest.mark.filterwarnings("ignore:Exited postprocessing") +def test_tolerance_float32(): + """Check lobpcg for attainable tolerance in float32. + """ + rnd = np.random.RandomState(0) + n = 50 + m = 3 + vals = -np.arange(1, n + 1) + A = diags([vals], [0], (n, n)) + A = A.astype(np.float32) + X = rnd.standard_normal((n, m)) + X = X.astype(np.float32) + eigvals, _ = lobpcg(A, X, tol=1.25e-5, maxiter=50, verbosityLevel=0) + assert_allclose(eigvals, -np.arange(1, 1 + m), atol=2e-5, rtol=1e-5) + + +@pytest.mark.parametrize("vdtype", VDTYPES) +@pytest.mark.parametrize("mdtype", MDTYPES) +@pytest.mark.parametrize("arr_type", [np.array, + sparse.csr_matrix, + sparse.coo_matrix]) +def test_dtypes(vdtype, mdtype, arr_type): + """Test lobpcg in various dtypes. + """ + rnd = np.random.RandomState(0) + n = 12 + m = 2 + A = arr_type(np.diag(np.arange(1, n + 1)).astype(mdtype)) + X = rnd.random((n, m)) + X = X.astype(vdtype) + eigvals, eigvecs = lobpcg(A, X, tol=1e-2, largest=False) + assert_allclose(eigvals, np.arange(1, 1 + m), atol=1e-1) + # eigenvectors must be nearly real in any case + assert_allclose(np.sum(np.abs(eigvecs - eigvecs.conj())), 0, atol=1e-2) + + +@pytest.mark.filterwarnings("ignore:Exited at iteration") +@pytest.mark.filterwarnings("ignore:Exited postprocessing") +def test_inplace_warning(): + """Check lobpcg gives a warning in '_b_orthonormalize' + that in-place orthogonalization is impossible due to dtype mismatch. + """ + rnd = np.random.RandomState(0) + n = 6 + m = 1 + vals = -np.arange(1, n + 1) + A = diags([vals], [0], (n, n)) + A = A.astype(np.cdouble) + X = rnd.standard_normal((n, m)) + with pytest.warns(UserWarning, match="Inplace update"): + eigvals, _ = lobpcg(A, X, maxiter=2, verbosityLevel=1) + + +def test_maxit(): + """Check lobpcg if maxit=maxiter runs maxiter iterations and + if maxit=None runs 20 iterations (the default) + by checking the size of the iteration history output, which should + be the number of iterations plus 3 (initial, final, and postprocessing) + typically when maxiter is small and the choice of the best is passive. + """ + rnd = np.random.RandomState(0) + n = 50 + m = 4 + vals = -np.arange(1, n + 1) + A = diags([vals], [0], (n, n)) + A = A.astype(np.float32) + X = rnd.standard_normal((n, m)) + X = X.astype(np.float64) + msg = "Exited at iteration.*|Exited postprocessing with accuracies.*" + for maxiter in range(1, 4): + with pytest.warns(UserWarning, match=msg): + _, _, l_h, r_h = lobpcg(A, X, tol=1e-8, maxiter=maxiter, + retLambdaHistory=True, + retResidualNormsHistory=True) + assert_allclose(np.shape(l_h)[0], maxiter+3) + assert_allclose(np.shape(r_h)[0], maxiter+3) + with pytest.warns(UserWarning, match=msg): + l, _, l_h, r_h = lobpcg(A, X, tol=1e-8, + retLambdaHistory=True, + retResidualNormsHistory=True) + assert_allclose(np.shape(l_h)[0], 20+3) + assert_allclose(np.shape(r_h)[0], 20+3) + # Check that eigenvalue output is the last one in history + assert_allclose(l, l_h[-1]) + # Make sure that both history outputs are lists + assert isinstance(l_h, list) + assert isinstance(r_h, list) + # Make sure that both history lists are arrays-like + assert_allclose(np.shape(l_h), np.shape(np.asarray(l_h))) + assert_allclose(np.shape(r_h), np.shape(np.asarray(r_h))) + + +@pytest.mark.slow +@pytest.mark.parametrize("n", [15]) +@pytest.mark.parametrize("m", [1, 2]) +@pytest.mark.filterwarnings("ignore:Exited at iteration") +@pytest.mark.filterwarnings("ignore:Exited postprocessing") +def test_diagonal_data_types(n, m): + """Check lobpcg for diagonal matrices for all matrix types. + Constraints are imposed, so a dense eigensolver eig cannot run. + """ + rnd = np.random.RandomState(0) + # Define the generalized eigenvalue problem Av = cBv + # where (c, v) is a generalized eigenpair, + # and where we choose A and B to be diagonal. + vals = np.arange(1, n + 1) + + # list_sparse_format = ['bsr', 'coo', 'csc', 'csr', 'dia', 'dok', 'lil'] + list_sparse_format = ['coo'] + sparse_formats = len(list_sparse_format) + for s_f_i, s_f in enumerate(list_sparse_format): + + As64 = diags([vals * vals], [0], (n, n), format=s_f) + As32 = As64.astype(np.float32) + Af64 = As64.toarray() + Af32 = Af64.astype(np.float32) + + def As32f(x): + return As32 @ x + As32LO = LinearOperator(matvec=As32f, + matmat=As32f, + shape=(n, n), + dtype=As32.dtype) + + listA = [Af64, As64, Af32, As32, As32f, As32LO, lambda v: As32 @ v] + + Bs64 = diags([vals], [0], (n, n), format=s_f) + Bf64 = Bs64.toarray() + Bs32 = Bs64.astype(np.float32) + + def Bs32f(x): + return Bs32 @ x + Bs32LO = LinearOperator(matvec=Bs32f, + matmat=Bs32f, + shape=(n, n), + dtype=Bs32.dtype) + listB = [Bf64, Bs64, Bs32, Bs32f, Bs32LO, lambda v: Bs32 @ v] + + # Define the preconditioner function as LinearOperator. + Ms64 = diags([1./vals], [0], (n, n), format=s_f) + + def Ms64precond(x): + return Ms64 @ x + Ms64precondLO = LinearOperator(matvec=Ms64precond, + matmat=Ms64precond, + shape=(n, n), + dtype=Ms64.dtype) + Mf64 = Ms64.toarray() + + def Mf64precond(x): + return Mf64 @ x + Mf64precondLO = LinearOperator(matvec=Mf64precond, + matmat=Mf64precond, + shape=(n, n), + dtype=Mf64.dtype) + Ms32 = Ms64.astype(np.float32) + + def Ms32precond(x): + return Ms32 @ x + Ms32precondLO = LinearOperator(matvec=Ms32precond, + matmat=Ms32precond, + shape=(n, n), + dtype=Ms32.dtype) + Mf32 = Ms32.toarray() + + def Mf32precond(x): + return Mf32 @ x + Mf32precondLO = LinearOperator(matvec=Mf32precond, + matmat=Mf32precond, + shape=(n, n), + dtype=Mf32.dtype) + listM = [None, Ms64, Ms64precondLO, Mf64precondLO, Ms64precond, + Ms32, Ms32precondLO, Mf32precondLO, Ms32precond] + + # Setup matrix of the initial approximation to the eigenvectors + # (cannot be sparse array). + Xf64 = rnd.random((n, m)) + Xf32 = Xf64.astype(np.float32) + listX = [Xf64, Xf32] + + # Require that the returned eigenvectors be in the orthogonal complement + # of the first few standard basis vectors (cannot be sparse array). + m_excluded = 3 + Yf64 = np.eye(n, m_excluded, dtype=float) + Yf32 = np.eye(n, m_excluded, dtype=np.float32) + listY = [Yf64, Yf32] + + tests = list(itertools.product(listA, listB, listM, listX, listY)) + # This is one of the slower tests because there are >1,000 configs + # to test here, instead of checking product of all input, output types + # test each configuration for the first sparse format, and then + # for one additional sparse format. this takes 2/7=30% as long as + # testing all configurations for all sparse formats. + if s_f_i > 0: + tests = tests[s_f_i - 1::sparse_formats-1] + + for A, B, M, X, Y in tests: + eigvals, _ = lobpcg(A, X, B=B, M=M, Y=Y, tol=1e-4, + maxiter=100, largest=False) + assert_allclose(eigvals, + np.arange(1 + m_excluded, 1 + m_excluded + m), + atol=1e-5) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/tests/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/tests/test_svds.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/tests/test_svds.py new file mode 100644 index 0000000000000000000000000000000000000000..4b93edc941cae18305053ff29f840df469ae1a8d --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_eigen/tests/test_svds.py @@ -0,0 +1,862 @@ +import re +import copy +import numpy as np + +from numpy.testing import assert_allclose, assert_equal, assert_array_equal +import pytest + +from scipy.linalg import svd, null_space +from scipy.sparse import csc_matrix, issparse, spdiags, random +from scipy.sparse.linalg import LinearOperator, aslinearoperator +from scipy.sparse.linalg import svds +from scipy.sparse.linalg._eigen.arpack import ArpackNoConvergence + + +# --- Helper Functions / Classes --- + + +def sorted_svd(m, k, which='LM'): + # Compute svd of a dense matrix m, and return singular vectors/values + # sorted. + if issparse(m): + m = m.toarray() + u, s, vh = svd(m) + if which == 'LM': + ii = np.argsort(s)[-k:] + elif which == 'SM': + ii = np.argsort(s)[:k] + else: + raise ValueError(f"unknown which={which!r}") + + return u[:, ii], s[ii], vh[ii] + + +def _check_svds(A, k, u, s, vh, which="LM", check_usvh_A=False, + check_svd=True, atol=1e-10, rtol=1e-7): + n, m = A.shape + + # Check shapes. + assert_equal(u.shape, (n, k)) + assert_equal(s.shape, (k,)) + assert_equal(vh.shape, (k, m)) + + # Check that the original matrix can be reconstituted. + A_rebuilt = (u*s).dot(vh) + assert_equal(A_rebuilt.shape, A.shape) + if check_usvh_A: + assert_allclose(A_rebuilt, A, atol=atol, rtol=rtol) + + # Check that u is a semi-orthogonal matrix. + uh_u = np.dot(u.T.conj(), u) + assert_equal(uh_u.shape, (k, k)) + assert_allclose(uh_u, np.identity(k), atol=atol, rtol=rtol) + + # Check that vh is a semi-orthogonal matrix. + vh_v = np.dot(vh, vh.T.conj()) + assert_equal(vh_v.shape, (k, k)) + assert_allclose(vh_v, np.identity(k), atol=atol, rtol=rtol) + + # Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd + if check_svd: + u2, s2, vh2 = sorted_svd(A, k, which) + assert_allclose(np.abs(u), np.abs(u2), atol=atol, rtol=rtol) + assert_allclose(s, s2, atol=atol, rtol=rtol) + assert_allclose(np.abs(vh), np.abs(vh2), atol=atol, rtol=rtol) + + +def _check_svds_n(A, k, u, s, vh, which="LM", check_res=True, + check_svd=True, atol=1e-10, rtol=1e-7): + n, m = A.shape + + # Check shapes. + assert_equal(u.shape, (n, k)) + assert_equal(s.shape, (k,)) + assert_equal(vh.shape, (k, m)) + + # Check that u is a semi-orthogonal matrix. + uh_u = np.dot(u.T.conj(), u) + assert_equal(uh_u.shape, (k, k)) + error = np.sum(np.abs(uh_u - np.identity(k))) / (k * k) + assert_allclose(error, 0.0, atol=atol, rtol=rtol) + + # Check that vh is a semi-orthogonal matrix. + vh_v = np.dot(vh, vh.T.conj()) + assert_equal(vh_v.shape, (k, k)) + error = np.sum(np.abs(vh_v - np.identity(k))) / (k * k) + assert_allclose(error, 0.0, atol=atol, rtol=rtol) + + # Check residuals + if check_res: + ru = A.T.conj() @ u - vh.T.conj() * s + rus = np.sum(np.abs(ru)) / (n * k) + rvh = A @ vh.T.conj() - u * s + rvhs = np.sum(np.abs(rvh)) / (m * k) + assert_allclose(rus, 0.0, atol=atol, rtol=rtol) + assert_allclose(rvhs, 0.0, atol=atol, rtol=rtol) + + # Check that scipy.sparse.linalg.svds ~ scipy.linalg.svd + if check_svd: + u2, s2, vh2 = sorted_svd(A, k, which) + assert_allclose(s, s2, atol=atol, rtol=rtol) + A_rebuilt_svd = (u2*s2).dot(vh2) + A_rebuilt = (u*s).dot(vh) + assert_equal(A_rebuilt.shape, A.shape) + error = np.sum(np.abs(A_rebuilt_svd - A_rebuilt)) / (k * k) + assert_allclose(error, 0.0, atol=atol, rtol=rtol) + + +class CheckingLinearOperator(LinearOperator): + def __init__(self, A): + self.A = A + self.dtype = A.dtype + self.shape = A.shape + + def _matvec(self, x): + assert_equal(max(x.shape), np.size(x)) + return self.A.dot(x) + + def _rmatvec(self, x): + assert_equal(max(x.shape), np.size(x)) + return self.A.T.conjugate().dot(x) + + +# --- Test Input Validation --- +# Tests input validation on parameters `k` and `which`. +# Needs better input validation checks for all other parameters. + +class SVDSCommonTests: + + solver = None + + # some of these IV tests could run only once, say with solver=None + + _A_empty_msg = "`A` must not be empty." + _A_dtype_msg = "`A` must be of floating or complex floating data type" + _A_type_msg = "type not understood" + _A_ndim_msg = "array must have ndim <= 2" + _A_validation_inputs = [ + (np.asarray([[]]), ValueError, _A_empty_msg), + (np.asarray([[1, 2], [3, 4]]), ValueError, _A_dtype_msg), + ("hi", TypeError, _A_type_msg), + (np.asarray([[[1., 2.], [3., 4.]]]), ValueError, _A_ndim_msg)] + + @pytest.mark.parametrize("args", _A_validation_inputs) + def test_svds_input_validation_A(self, args): + A, error_type, message = args + with pytest.raises(error_type, match=message): + svds(A, k=1, solver=self.solver) + + @pytest.mark.parametrize("k", [-1, 0, 3, 4, 5, 1.5, "1"]) + def test_svds_input_validation_k_1(self, k): + rng = np.random.default_rng(0) + A = rng.random((4, 3)) + + # propack can do complete SVD + if self.solver == 'propack' and k == 3: + res = svds(A, k=k, solver=self.solver, random_state=0) + _check_svds(A, k, *res, check_usvh_A=True, check_svd=True) + return + + message = ("`k` must be an integer satisfying") + with pytest.raises(ValueError, match=message): + svds(A, k=k, solver=self.solver) + + def test_svds_input_validation_k_2(self): + # I think the stack trace is reasonable when `k` can't be converted + # to an int. + message = "int() argument must be a" + with pytest.raises(TypeError, match=re.escape(message)): + svds(np.eye(10), k=[], solver=self.solver) + + message = "invalid literal for int()" + with pytest.raises(ValueError, match=message): + svds(np.eye(10), k="hi", solver=self.solver) + + @pytest.mark.parametrize("tol", (-1, np.inf, np.nan)) + def test_svds_input_validation_tol_1(self, tol): + message = "`tol` must be a non-negative floating point value." + with pytest.raises(ValueError, match=message): + svds(np.eye(10), tol=tol, solver=self.solver) + + @pytest.mark.parametrize("tol", ([], 'hi')) + def test_svds_input_validation_tol_2(self, tol): + # I think the stack trace is reasonable here + message = "'<' not supported between instances" + with pytest.raises(TypeError, match=message): + svds(np.eye(10), tol=tol, solver=self.solver) + + @pytest.mark.parametrize("which", ('LA', 'SA', 'ekki', 0)) + def test_svds_input_validation_which(self, which): + # Regression test for a github issue. + # https://github.com/scipy/scipy/issues/4590 + # Function was not checking for eigenvalue type and unintended + # values could be returned. + with pytest.raises(ValueError, match="`which` must be in"): + svds(np.eye(10), which=which, solver=self.solver) + + @pytest.mark.parametrize("transpose", (True, False)) + @pytest.mark.parametrize("n", range(4, 9)) + def test_svds_input_validation_v0_1(self, transpose, n): + rng = np.random.default_rng(0) + A = rng.random((5, 7)) + v0 = rng.random(n) + if transpose: + A = A.T + k = 2 + message = "`v0` must have shape" + + required_length = (A.shape[0] if self.solver == 'propack' + else min(A.shape)) + if n != required_length: + with pytest.raises(ValueError, match=message): + svds(A, k=k, v0=v0, solver=self.solver) + + def test_svds_input_validation_v0_2(self): + A = np.ones((10, 10)) + v0 = np.ones((1, 10)) + message = "`v0` must have shape" + with pytest.raises(ValueError, match=message): + svds(A, k=1, v0=v0, solver=self.solver) + + @pytest.mark.parametrize("v0", ("hi", 1, np.ones(10, dtype=int))) + def test_svds_input_validation_v0_3(self, v0): + A = np.ones((10, 10)) + message = "`v0` must be of floating or complex floating data type." + with pytest.raises(ValueError, match=message): + svds(A, k=1, v0=v0, solver=self.solver) + + @pytest.mark.parametrize("maxiter", (-1, 0, 5.5)) + def test_svds_input_validation_maxiter_1(self, maxiter): + message = ("`maxiter` must be a positive integer.") + with pytest.raises(ValueError, match=message): + svds(np.eye(10), maxiter=maxiter, solver=self.solver) + + def test_svds_input_validation_maxiter_2(self): + # I think the stack trace is reasonable when `k` can't be converted + # to an int. + message = "int() argument must be a" + with pytest.raises(TypeError, match=re.escape(message)): + svds(np.eye(10), maxiter=[], solver=self.solver) + + message = "invalid literal for int()" + with pytest.raises(ValueError, match=message): + svds(np.eye(10), maxiter="hi", solver=self.solver) + + @pytest.mark.parametrize("rsv", ('ekki', 10)) + def test_svds_input_validation_return_singular_vectors(self, rsv): + message = "`return_singular_vectors` must be in" + with pytest.raises(ValueError, match=message): + svds(np.eye(10), return_singular_vectors=rsv, solver=self.solver) + + # --- Test Parameters --- + + @pytest.mark.parametrize("k", [3, 5]) + @pytest.mark.parametrize("which", ["LM", "SM"]) + def test_svds_parameter_k_which(self, k, which): + # check that the `k` parameter sets the number of eigenvalues/ + # eigenvectors returned. + # Also check that the `which` parameter sets whether the largest or + # smallest eigenvalues are returned + rng = np.random.default_rng(0) + A = rng.random((10, 10)) + if self.solver == 'lobpcg': + with pytest.warns(UserWarning, match="The problem size"): + res = svds(A, k=k, which=which, solver=self.solver, + random_state=0) + else: + res = svds(A, k=k, which=which, solver=self.solver, + random_state=0) + _check_svds(A, k, *res, which=which, atol=1e-9, rtol=2e-13) + + @pytest.mark.filterwarnings("ignore:Exited", + reason="Ignore LOBPCG early exit.") + # loop instead of parametrize for simplicity + def test_svds_parameter_tol(self): + # check the effect of the `tol` parameter on solver accuracy by solving + # the same problem with varying `tol` and comparing the eigenvalues + # against ground truth computed + n = 100 # matrix size + k = 3 # number of eigenvalues to check + + # generate a random, sparse-ish matrix + # effect isn't apparent for matrices that are too small + rng = np.random.default_rng(0) + A = rng.random((n, n)) + A[A > .1] = 0 + A = A @ A.T + + _, s, _ = svd(A) # calculate ground truth + + # calculate the error as a function of `tol` + A = csc_matrix(A) + + def err(tol): + _, s2, _ = svds(A, k=k, v0=np.ones(n), maxiter=1000, + solver=self.solver, tol=tol, random_state=0) + return np.linalg.norm((s2 - s[k-1::-1])/s[k-1::-1]) + + tols = [1e-4, 1e-2, 1e0] # tolerance levels to check + # for 'arpack' and 'propack', accuracies make discrete steps + accuracies = {'propack': [1e-12, 1e-6, 1e-4], + 'arpack': [2.5e-15, 1e-10, 1e-10], + 'lobpcg': [2e-12, 4e-2, 2]} + + for tol, accuracy in zip(tols, accuracies[self.solver]): + error = err(tol) + assert error < accuracy + + def test_svd_v0(self): + # check that the `v0` parameter affects the solution + n = 100 + k = 1 + # If k != 1, LOBPCG needs more initial vectors, which are generated + # with random_state, so it does not pass w/ k >= 2. + # For some other values of `n`, the AssertionErrors are not raised + # with different v0s, which is reasonable. + + rng = np.random.default_rng(0) + A = rng.random((n, n)) + + # with the same v0, solutions are the same, and they are accurate + # v0 takes precedence over random_state + v0a = rng.random(n) + res1a = svds(A, k, v0=v0a, solver=self.solver, random_state=0) + res2a = svds(A, k, v0=v0a, solver=self.solver, random_state=1) + for idx in range(3): + assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16) + _check_svds(A, k, *res1a) + + # with the same v0, solutions are the same, and they are accurate + v0b = rng.random(n) + res1b = svds(A, k, v0=v0b, solver=self.solver, random_state=2) + res2b = svds(A, k, v0=v0b, solver=self.solver, random_state=3) + for idx in range(3): + assert_allclose(res1b[idx], res2b[idx], rtol=1e-15, atol=2e-16) + _check_svds(A, k, *res1b) + + # with different v0, solutions can be numerically different + message = "Arrays are not equal" + with pytest.raises(AssertionError, match=message): + assert_equal(res1a, res1b) + + def test_svd_random_state(self): + # check that the `random_state` parameter affects the solution + # Admittedly, `n` and `k` are chosen so that all solver pass all + # these checks. That's a tall order, since LOBPCG doesn't want to + # achieve the desired accuracy and ARPACK often returns the same + # singular values/vectors for different v0. + n = 100 + k = 1 + + rng = np.random.default_rng(0) + A = rng.random((n, n)) + + # with the same random_state, solutions are the same and accurate + res1a = svds(A, k, solver=self.solver, random_state=0) + res2a = svds(A, k, solver=self.solver, random_state=0) + for idx in range(3): + assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16) + _check_svds(A, k, *res1a) + + # with the same random_state, solutions are the same and accurate + res1b = svds(A, k, solver=self.solver, random_state=1) + res2b = svds(A, k, solver=self.solver, random_state=1) + for idx in range(3): + assert_allclose(res1b[idx], res2b[idx], rtol=1e-15, atol=2e-16) + _check_svds(A, k, *res1b) + + # with different random_state, solutions can be numerically different + message = "Arrays are not equal" + with pytest.raises(AssertionError, match=message): + assert_equal(res1a, res1b) + + @pytest.mark.parametrize("random_state", (0, 1, + np.random.RandomState(0), + np.random.default_rng(0))) + def test_svd_random_state_2(self, random_state): + n = 100 + k = 1 + + rng = np.random.default_rng(0) + A = rng.random((n, n)) + + random_state_2 = copy.deepcopy(random_state) + + # with the same random_state, solutions are the same and accurate + res1a = svds(A, k, solver=self.solver, random_state=random_state) + res2a = svds(A, k, solver=self.solver, random_state=random_state_2) + for idx in range(3): + assert_allclose(res1a[idx], res2a[idx], rtol=1e-15, atol=2e-16) + _check_svds(A, k, *res1a) + + @pytest.mark.parametrize("random_state", (None, + np.random.RandomState(0), + np.random.default_rng(0))) + @pytest.mark.filterwarnings("ignore:Exited", + reason="Ignore LOBPCG early exit.") + def test_svd_random_state_3(self, random_state): + n = 100 + k = 5 + + rng = np.random.default_rng(0) + A = rng.random((n, n)) + + random_state = copy.deepcopy(random_state) + + # random_state in different state produces accurate - but not + # not necessarily identical - results + res1a = svds(A, k, solver=self.solver, random_state=random_state, maxiter=1000) + res2a = svds(A, k, solver=self.solver, random_state=random_state, maxiter=1000) + _check_svds(A, k, *res1a, atol=2e-7) + _check_svds(A, k, *res2a, atol=2e-7) + + message = "Arrays are not equal" + with pytest.raises(AssertionError, match=message): + assert_equal(res1a, res2a) + + @pytest.mark.filterwarnings("ignore:Exited postprocessing") + def test_svd_maxiter(self): + # check that maxiter works as expected: should not return accurate + # solution after 1 iteration, but should with default `maxiter` + A = np.diag(np.arange(9)).astype(np.float64) + k = 1 + u, s, vh = sorted_svd(A, k) + # Use default maxiter by default + maxiter = None + + if self.solver == 'arpack': + message = "ARPACK error -1: No convergence" + with pytest.raises(ArpackNoConvergence, match=message): + svds(A, k, ncv=3, maxiter=1, solver=self.solver) + elif self.solver == 'lobpcg': + # Set maxiter higher so test passes without changing + # default and breaking backward compatibility (gh-20221) + maxiter = 30 + with pytest.warns(UserWarning, match="Exited at iteration"): + svds(A, k, maxiter=1, solver=self.solver) + elif self.solver == 'propack': + message = "k=1 singular triplets did not converge within" + with pytest.raises(np.linalg.LinAlgError, match=message): + svds(A, k, maxiter=1, solver=self.solver) + + ud, sd, vhd = svds(A, k, solver=self.solver, maxiter=maxiter, + random_state=0) + _check_svds(A, k, ud, sd, vhd, atol=1e-8) + assert_allclose(np.abs(ud), np.abs(u), atol=1e-8) + assert_allclose(np.abs(vhd), np.abs(vh), atol=1e-8) + assert_allclose(np.abs(sd), np.abs(s), atol=1e-9) + + @pytest.mark.parametrize("rsv", (True, False, 'u', 'vh')) + @pytest.mark.parametrize("shape", ((5, 7), (6, 6), (7, 5))) + def test_svd_return_singular_vectors(self, rsv, shape): + # check that the return_singular_vectors parameter works as expected + rng = np.random.default_rng(0) + A = rng.random(shape) + k = 2 + M, N = shape + u, s, vh = sorted_svd(A, k) + + respect_u = True if self.solver == 'propack' else M <= N + respect_vh = True if self.solver == 'propack' else M > N + + if self.solver == 'lobpcg': + with pytest.warns(UserWarning, match="The problem size"): + if rsv is False: + s2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + assert_allclose(s2, s) + elif rsv == 'u' and respect_u: + u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + assert_allclose(np.abs(u2), np.abs(u)) + assert_allclose(s2, s) + assert vh2 is None + elif rsv == 'vh' and respect_vh: + u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + assert u2 is None + assert_allclose(s2, s) + assert_allclose(np.abs(vh2), np.abs(vh)) + else: + u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + if u2 is not None: + assert_allclose(np.abs(u2), np.abs(u)) + assert_allclose(s2, s) + if vh2 is not None: + assert_allclose(np.abs(vh2), np.abs(vh)) + else: + if rsv is False: + s2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + assert_allclose(s2, s) + elif rsv == 'u' and respect_u: + u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + assert_allclose(np.abs(u2), np.abs(u)) + assert_allclose(s2, s) + assert vh2 is None + elif rsv == 'vh' and respect_vh: + u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + assert u2 is None + assert_allclose(s2, s) + assert_allclose(np.abs(vh2), np.abs(vh)) + else: + u2, s2, vh2 = svds(A, k, return_singular_vectors=rsv, + solver=self.solver, random_state=rng) + if u2 is not None: + assert_allclose(np.abs(u2), np.abs(u)) + assert_allclose(s2, s) + if vh2 is not None: + assert_allclose(np.abs(vh2), np.abs(vh)) + + # --- Test Basic Functionality --- + # Tests the accuracy of each solver for real and complex matrices provided + # as list, dense array, sparse matrix, and LinearOperator. + + A1 = [[1, 2, 3], [3, 4, 3], [1 + 1j, 0, 2], [0, 0, 1]] + A2 = [[1, 2, 3, 8 + 5j], [3 - 2j, 4, 3, 5], [1, 0, 2, 3], [0, 0, 1, 0]] + + @pytest.mark.filterwarnings("ignore:k >= N - 1", + reason="needed to demonstrate #16725") + @pytest.mark.parametrize('A', (A1, A2)) + @pytest.mark.parametrize('k', range(1, 5)) + # PROPACK fails a lot if @pytest.mark.parametrize('which', ("SM", "LM")) + @pytest.mark.parametrize('real', (True, False)) + @pytest.mark.parametrize('transpose', (False, True)) + # In gh-14299, it was suggested the `svds` should _not_ work with lists + @pytest.mark.parametrize('lo_type', (np.asarray, csc_matrix, + aslinearoperator)) + def test_svd_simple(self, A, k, real, transpose, lo_type): + + A = np.asarray(A) + A = np.real(A) if real else A + A = A.T if transpose else A + A2 = lo_type(A) + + # could check for the appropriate errors, but that is tested above + if k > min(A.shape): + pytest.skip("`k` cannot be greater than `min(A.shape)`") + if self.solver != 'propack' and k >= min(A.shape): + pytest.skip("Only PROPACK supports complete SVD") + if self.solver == 'arpack' and not real and k == min(A.shape) - 1: + pytest.skip("#16725") + + atol = 3e-10 + if self.solver == 'propack': + atol = 3e-9 # otherwise test fails on Linux aarch64 (see gh-19855) + + if self.solver == 'lobpcg': + with pytest.warns(UserWarning, match="The problem size"): + u, s, vh = svds(A2, k, solver=self.solver, random_state=0) + else: + u, s, vh = svds(A2, k, solver=self.solver, random_state=0) + _check_svds(A, k, u, s, vh, atol=atol) + + def test_svd_linop(self): + solver = self.solver + + nmks = [(6, 7, 3), + (9, 5, 4), + (10, 8, 5)] + + def reorder(args): + U, s, VH = args + j = np.argsort(s) + return U[:, j], s[j], VH[j, :] + + for n, m, k in nmks: + # Test svds on a LinearOperator. + A = np.random.RandomState(52).randn(n, m) + L = CheckingLinearOperator(A) + + if solver == 'propack': + v0 = np.ones(n) + else: + v0 = np.ones(min(A.shape)) + if solver == 'lobpcg': + with pytest.warns(UserWarning, match="The problem size"): + U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver, + random_state=0)) + U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver, + random_state=0)) + else: + U1, s1, VH1 = reorder(svds(A, k, v0=v0, solver=solver, + random_state=0)) + U2, s2, VH2 = reorder(svds(L, k, v0=v0, solver=solver, + random_state=0)) + + assert_allclose(np.abs(U1), np.abs(U2)) + assert_allclose(s1, s2) + assert_allclose(np.abs(VH1), np.abs(VH2)) + assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)), + np.dot(U2, np.dot(np.diag(s2), VH2))) + + # Try again with which="SM". + A = np.random.RandomState(1909).randn(n, m) + L = CheckingLinearOperator(A) + + # TODO: arpack crashes when v0=v0, which="SM" + kwargs = {'v0': v0} if solver not in {None, 'arpack'} else {} + if self.solver == 'lobpcg': + with pytest.warns(UserWarning, match="The problem size"): + U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver, + random_state=0, **kwargs)) + U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver, + random_state=0, **kwargs)) + else: + U1, s1, VH1 = reorder(svds(A, k, which="SM", solver=solver, + random_state=0, **kwargs)) + U2, s2, VH2 = reorder(svds(L, k, which="SM", solver=solver, + random_state=0, **kwargs)) + + assert_allclose(np.abs(U1), np.abs(U2)) + assert_allclose(s1 + 1, s2 + 1) + assert_allclose(np.abs(VH1), np.abs(VH2)) + assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)), + np.dot(U2, np.dot(np.diag(s2), VH2))) + + if k < min(n, m) - 1: + # Complex input and explicit which="LM". + for (dt, eps) in [(complex, 1e-7), (np.complex64, 3e-3)]: + rng = np.random.RandomState(1648) + A = (rng.randn(n, m) + 1j * rng.randn(n, m)).astype(dt) + L = CheckingLinearOperator(A) + + if self.solver == 'lobpcg': + with pytest.warns(UserWarning, + match="The problem size"): + U1, s1, VH1 = reorder(svds(A, k, which="LM", + solver=solver, + random_state=0)) + U2, s2, VH2 = reorder(svds(L, k, which="LM", + solver=solver, + random_state=0)) + else: + U1, s1, VH1 = reorder(svds(A, k, which="LM", + solver=solver, + random_state=0)) + U2, s2, VH2 = reorder(svds(L, k, which="LM", + solver=solver, + random_state=0)) + + assert_allclose(np.abs(U1), np.abs(U2), rtol=eps) + assert_allclose(s1, s2, rtol=eps) + assert_allclose(np.abs(VH1), np.abs(VH2), rtol=eps) + assert_allclose(np.dot(U1, np.dot(np.diag(s1), VH1)), + np.dot(U2, np.dot(np.diag(s2), VH2)), + rtol=eps) + + SHAPES = ((100, 100), (100, 101), (101, 100)) + + @pytest.mark.filterwarnings("ignore:Exited at iteration") + @pytest.mark.filterwarnings("ignore:Exited postprocessing") + @pytest.mark.parametrize("shape", SHAPES) + # ARPACK supports only dtype float, complex, or np.float32 + @pytest.mark.parametrize("dtype", (float, complex, np.float32)) + def test_small_sigma_sparse(self, shape, dtype): + # https://github.com/scipy/scipy/pull/11829 + solver = self.solver + # 2do: PROPACK fails orthogonality of singular vectors + # if dtype == complex and self.solver == 'propack': + # pytest.skip("PROPACK unsupported for complex dtype") + rng = np.random.default_rng(0) + k = 5 + (m, n) = shape + S = random(m, n, density=0.1, random_state=rng) + if dtype == complex: + S = + 1j * random(m, n, density=0.1, random_state=rng) + e = np.ones(m) + e[0:5] *= 1e1 ** np.arange(-5, 0, 1) + S = spdiags(e, 0, m, m) @ S + S = S.astype(dtype) + u, s, vh = svds(S, k, which='SM', solver=solver, maxiter=1000, + random_state=0) + c_svd = False # partial SVD can be different from full SVD + _check_svds_n(S, k, u, s, vh, which="SM", check_svd=c_svd, atol=2e-1) + + # --- Test Edge Cases --- + # Checks a few edge cases. + + @pytest.mark.parametrize("shape", ((6, 5), (5, 5), (5, 6))) + @pytest.mark.parametrize("dtype", (float, complex)) + def test_svd_LM_ones_matrix(self, shape, dtype): + # Check that svds can deal with matrix_rank less than k in LM mode. + k = 3 + n, m = shape + A = np.ones((n, m), dtype=dtype) + + if self.solver == 'lobpcg': + with pytest.warns(UserWarning, match="The problem size"): + U, s, VH = svds(A, k, solver=self.solver, random_state=0) + else: + U, s, VH = svds(A, k, solver=self.solver, random_state=0) + + _check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False) + + # Check that the largest singular value is near sqrt(n*m) + # and the other singular values have been forced to zero. + assert_allclose(np.max(s), np.sqrt(n*m)) + s = np.array(sorted(s)[:-1]) + 1 + z = np.ones_like(s) + assert_allclose(s, z) + + @pytest.mark.filterwarnings("ignore:k >= N - 1", + reason="needed to demonstrate #16725") + @pytest.mark.parametrize("shape", ((3, 4), (4, 4), (4, 3), (4, 2))) + @pytest.mark.parametrize("dtype", (float, complex)) + def test_zero_matrix(self, shape, dtype): + # Check that svds can deal with matrices containing only zeros; + # see https://github.com/scipy/scipy/issues/3452/ + # shape = (4, 2) is included because it is the particular case + # reported in the issue + k = 1 + n, m = shape + A = np.zeros((n, m), dtype=dtype) + + if (self.solver == 'arpack' and dtype is complex + and k == min(A.shape) - 1): + pytest.skip("#16725") + + if self.solver == 'propack': + pytest.skip("PROPACK failures unrelated to PR #16712") + + if self.solver == 'lobpcg': + with pytest.warns(UserWarning, match="The problem size"): + U, s, VH = svds(A, k, solver=self.solver, random_state=0) + else: + U, s, VH = svds(A, k, solver=self.solver, random_state=0) + + # Check some generic properties of svd. + _check_svds(A, k, U, s, VH, check_usvh_A=True, check_svd=False) + + # Check that the singular values are zero. + assert_array_equal(s, 0) + + @pytest.mark.parametrize("shape", ((20, 20), (20, 21), (21, 20))) + # ARPACK supports only dtype float, complex, or np.float32 + @pytest.mark.parametrize("dtype", (float, complex, np.float32)) + @pytest.mark.filterwarnings("ignore:Exited", + reason="Ignore LOBPCG early exit.") + def test_small_sigma(self, shape, dtype): + rng = np.random.default_rng(179847540) + A = rng.random(shape).astype(dtype) + u, _, vh = svd(A, full_matrices=False) + if dtype == np.float32: + e = 10.0 + else: + e = 100.0 + t = e**(-np.arange(len(vh))).astype(dtype) + A = (u*t).dot(vh) + k = 4 + u, s, vh = svds(A, k, solver=self.solver, maxiter=100, random_state=0) + t = np.sum(s > 0) + assert_equal(t, k) + # LOBPCG needs larger atol and rtol to pass + _check_svds_n(A, k, u, s, vh, atol=1e-3, rtol=1e0, check_svd=False) + + # ARPACK supports only dtype float, complex, or np.float32 + @pytest.mark.filterwarnings("ignore:The problem size") + @pytest.mark.parametrize("dtype", (float, complex, np.float32)) + def test_small_sigma2(self, dtype): + rng = np.random.default_rng(179847540) + # create a 10x10 singular matrix with a 4-dim null space + dim = 4 + size = 10 + x = rng.random((size, size-dim)) + y = x[:, :dim] * rng.random(dim) + mat = np.hstack((x, y)) + mat = mat.astype(dtype) + + nz = null_space(mat) + assert_equal(nz.shape[1], dim) + + # Tolerances atol and rtol adjusted to pass np.float32 + # Use non-sparse svd + u, s, vh = svd(mat) + # Singular values are 0: + assert_allclose(s[-dim:], 0, atol=1e-6, rtol=1e0) + # Smallest right singular vectors in null space: + assert_allclose(mat @ vh[-dim:, :].T, 0, atol=1e-6, rtol=1e0) + + # Smallest singular values should be 0 + sp_mat = csc_matrix(mat) + su, ss, svh = svds(sp_mat, k=dim, which='SM', solver=self.solver, + random_state=0) + # Smallest dim singular values are 0: + assert_allclose(ss, 0, atol=1e-5, rtol=1e0) + # Smallest singular vectors via svds in null space: + n, m = mat.shape + if n < m: # else the assert fails with some libraries unclear why + assert_allclose(sp_mat.transpose() @ su, 0, atol=1e-5, rtol=1e0) + assert_allclose(sp_mat @ svh.T, 0, atol=1e-5, rtol=1e0) + +# --- Perform tests with each solver --- + + +class Test_SVDS_once: + @pytest.mark.parametrize("solver", ['ekki', object]) + def test_svds_input_validation_solver(self, solver): + message = "solver must be one of" + with pytest.raises(ValueError, match=message): + svds(np.ones((3, 4)), k=2, solver=solver) + + +class Test_SVDS_ARPACK(SVDSCommonTests): + + def setup_method(self): + self.solver = 'arpack' + + @pytest.mark.parametrize("ncv", list(range(-1, 8)) + [4.5, "5"]) + def test_svds_input_validation_ncv_1(self, ncv): + rng = np.random.default_rng(0) + A = rng.random((6, 7)) + k = 3 + if ncv in {4, 5}: + u, s, vh = svds(A, k=k, ncv=ncv, solver=self.solver, random_state=0) + # partial decomposition, so don't check that u@diag(s)@vh=A; + # do check that scipy.sparse.linalg.svds ~ scipy.linalg.svd + _check_svds(A, k, u, s, vh) + else: + message = ("`ncv` must be an integer satisfying") + with pytest.raises(ValueError, match=message): + svds(A, k=k, ncv=ncv, solver=self.solver) + + def test_svds_input_validation_ncv_2(self): + # I think the stack trace is reasonable when `ncv` can't be converted + # to an int. + message = "int() argument must be a" + with pytest.raises(TypeError, match=re.escape(message)): + svds(np.eye(10), ncv=[], solver=self.solver) + + message = "invalid literal for int()" + with pytest.raises(ValueError, match=message): + svds(np.eye(10), ncv="hi", solver=self.solver) + + # I can't see a robust relationship between `ncv` and relevant outputs + # (e.g. accuracy, time), so no test of the parameter. + + +class Test_SVDS_LOBPCG(SVDSCommonTests): + + def setup_method(self): + self.solver = 'lobpcg' + + +class Test_SVDS_PROPACK(SVDSCommonTests): + + def setup_method(self): + self.solver = 'propack' + + def test_svd_LM_ones_matrix(self): + message = ("PROPACK does not return orthonormal singular vectors " + "associated with zero singular values.") + # There are some other issues with this matrix of all ones, e.g. + # `which='sm'` and `k=1` returns the largest singular value + pytest.xfail(message) + + def test_svd_LM_zeros_matrix(self): + message = ("PROPACK does not return orthonormal singular vectors " + "associated with zero singular values.") + pytest.xfail(message) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_expm_multiply.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_expm_multiply.py new file mode 100644 index 0000000000000000000000000000000000000000..2df6209e405b6ce92f2ca8de3f1684766b17436d --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_expm_multiply.py @@ -0,0 +1,810 @@ +"""Compute the action of the matrix exponential.""" +from warnings import warn + +import numpy as np + +import scipy.linalg +import scipy.sparse.linalg +from scipy.linalg._decomp_qr import qr +from scipy.sparse._sputils import is_pydata_spmatrix +from scipy.sparse.linalg import aslinearoperator +from scipy.sparse.linalg._interface import IdentityOperator +from scipy.sparse.linalg._onenormest import onenormest + +__all__ = ['expm_multiply'] + + +def _exact_inf_norm(A): + # A compatibility function which should eventually disappear. + if scipy.sparse.issparse(A): + return max(abs(A).sum(axis=1).flat) + elif is_pydata_spmatrix(A): + return max(abs(A).sum(axis=1)) + else: + return np.linalg.norm(A, np.inf) + + +def _exact_1_norm(A): + # A compatibility function which should eventually disappear. + if scipy.sparse.issparse(A): + return max(abs(A).sum(axis=0).flat) + elif is_pydata_spmatrix(A): + return max(abs(A).sum(axis=0)) + else: + return np.linalg.norm(A, 1) + + +def _trace(A): + # A compatibility function which should eventually disappear. + if is_pydata_spmatrix(A): + return A.to_scipy_sparse().trace() + else: + return A.trace() + + +def traceest(A, m3, seed=None): + """Estimate `np.trace(A)` using `3*m3` matrix-vector products. + + The result is not deterministic. + + Parameters + ---------- + A : LinearOperator + Linear operator whose trace will be estimated. Has to be square. + m3 : int + Number of matrix-vector products divided by 3 used to estimate the + trace. + seed : optional + Seed for `numpy.random.default_rng`. + Can be provided to obtain deterministic results. + + Returns + ------- + trace : LinearOperator.dtype + Estimate of the trace + + Notes + ----- + This is the Hutch++ algorithm given in [1]_. + + References + ---------- + .. [1] Meyer, Raphael A., Cameron Musco, Christopher Musco, and David P. + Woodruff. "Hutch++: Optimal Stochastic Trace Estimation." In Symposium + on Simplicity in Algorithms (SOSA), pp. 142-155. Society for Industrial + and Applied Mathematics, 2021 + https://doi.org/10.1137/1.9781611976496.16 + + """ + rng = np.random.default_rng(seed) + if len(A.shape) != 2 or A.shape[-1] != A.shape[-2]: + raise ValueError("Expected A to be like a square matrix.") + n = A.shape[-1] + S = rng.choice([-1.0, +1.0], [n, m3]) + Q, _ = qr(A.matmat(S), overwrite_a=True, mode='economic') + trQAQ = np.trace(Q.conj().T @ A.matmat(Q)) + G = rng.choice([-1, +1], [n, m3]) + right = G - Q@(Q.conj().T @ G) + trGAG = np.trace(right.conj().T @ A.matmat(right)) + return trQAQ + trGAG/m3 + + +def _ident_like(A): + # A compatibility function which should eventually disappear. + if scipy.sparse.issparse(A): + # Creates a sparse matrix in dia format + out = scipy.sparse.eye(A.shape[0], A.shape[1], dtype=A.dtype) + if isinstance(A, scipy.sparse.spmatrix): + return out.asformat(A.format) + return scipy.sparse.dia_array(out).asformat(A.format) + elif is_pydata_spmatrix(A): + import sparse + return sparse.eye(A.shape[0], A.shape[1], dtype=A.dtype) + elif isinstance(A, scipy.sparse.linalg.LinearOperator): + return IdentityOperator(A.shape, dtype=A.dtype) + else: + return np.eye(A.shape[0], A.shape[1], dtype=A.dtype) + + +def expm_multiply(A, B, start=None, stop=None, num=None, + endpoint=None, traceA=None): + """ + Compute the action of the matrix exponential of A on B. + + Parameters + ---------- + A : transposable linear operator + The operator whose exponential is of interest. + B : ndarray + The matrix or vector to be multiplied by the matrix exponential of A. + start : scalar, optional + The starting time point of the sequence. + stop : scalar, optional + The end time point of the sequence, unless `endpoint` is set to False. + In that case, the sequence consists of all but the last of ``num + 1`` + evenly spaced time points, so that `stop` is excluded. + Note that the step size changes when `endpoint` is False. + num : int, optional + Number of time points to use. + endpoint : bool, optional + If True, `stop` is the last time point. Otherwise, it is not included. + traceA : scalar, optional + Trace of `A`. If not given the trace is estimated for linear operators, + or calculated exactly for sparse matrices. It is used to precondition + `A`, thus an approximate trace is acceptable. + For linear operators, `traceA` should be provided to ensure performance + as the estimation is not guaranteed to be reliable for all cases. + + .. versionadded:: 1.9.0 + + Returns + ------- + expm_A_B : ndarray + The result of the action :math:`e^{t_k A} B`. + + Warns + ----- + UserWarning + If `A` is a linear operator and ``traceA=None`` (default). + + Notes + ----- + The optional arguments defining the sequence of evenly spaced time points + are compatible with the arguments of `numpy.linspace`. + + The output ndarray shape is somewhat complicated so I explain it here. + The ndim of the output could be either 1, 2, or 3. + It would be 1 if you are computing the expm action on a single vector + at a single time point. + It would be 2 if you are computing the expm action on a vector + at multiple time points, or if you are computing the expm action + on a matrix at a single time point. + It would be 3 if you want the action on a matrix with multiple + columns at multiple time points. + If multiple time points are requested, expm_A_B[0] will always + be the action of the expm at the first time point, + regardless of whether the action is on a vector or a matrix. + + References + ---------- + .. [1] Awad H. Al-Mohy and Nicholas J. Higham (2011) + "Computing the Action of the Matrix Exponential, + with an Application to Exponential Integrators." + SIAM Journal on Scientific Computing, + 33 (2). pp. 488-511. ISSN 1064-8275 + http://eprints.ma.man.ac.uk/1591/ + + .. [2] Nicholas J. Higham and Awad H. Al-Mohy (2010) + "Computing Matrix Functions." + Acta Numerica, + 19. 159-208. ISSN 0962-4929 + http://eprints.ma.man.ac.uk/1451/ + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import expm, expm_multiply + >>> A = csc_matrix([[1, 0], [0, 1]]) + >>> A.toarray() + array([[1, 0], + [0, 1]], dtype=int64) + >>> B = np.array([np.exp(-1.), np.exp(-2.)]) + >>> B + array([ 0.36787944, 0.13533528]) + >>> expm_multiply(A, B, start=1, stop=2, num=3, endpoint=True) + array([[ 1. , 0.36787944], + [ 1.64872127, 0.60653066], + [ 2.71828183, 1. ]]) + >>> expm(A).dot(B) # Verify 1st timestep + array([ 1. , 0.36787944]) + >>> expm(1.5*A).dot(B) # Verify 2nd timestep + array([ 1.64872127, 0.60653066]) + >>> expm(2*A).dot(B) # Verify 3rd timestep + array([ 2.71828183, 1. ]) + """ + if all(arg is None for arg in (start, stop, num, endpoint)): + X = _expm_multiply_simple(A, B, traceA=traceA) + else: + X, status = _expm_multiply_interval(A, B, start, stop, num, + endpoint, traceA=traceA) + return X + + +def _expm_multiply_simple(A, B, t=1.0, traceA=None, balance=False): + """ + Compute the action of the matrix exponential at a single time point. + + Parameters + ---------- + A : transposable linear operator + The operator whose exponential is of interest. + B : ndarray + The matrix to be multiplied by the matrix exponential of A. + t : float + A time point. + traceA : scalar, optional + Trace of `A`. If not given the trace is estimated for linear operators, + or calculated exactly for sparse matrices. It is used to precondition + `A`, thus an approximate trace is acceptable + balance : bool + Indicates whether or not to apply balancing. + + Returns + ------- + F : ndarray + :math:`e^{t A} B` + + Notes + ----- + This is algorithm (3.2) in Al-Mohy and Higham (2011). + + """ + if balance: + raise NotImplementedError + if len(A.shape) != 2 or A.shape[0] != A.shape[1]: + raise ValueError('expected A to be like a square matrix') + if A.shape[1] != B.shape[0]: + raise ValueError('shapes of matrices A {} and B {} are incompatible' + .format(A.shape, B.shape)) + ident = _ident_like(A) + is_linear_operator = isinstance(A, scipy.sparse.linalg.LinearOperator) + n = A.shape[0] + if len(B.shape) == 1: + n0 = 1 + elif len(B.shape) == 2: + n0 = B.shape[1] + else: + raise ValueError('expected B to be like a matrix or a vector') + u_d = 2**-53 + tol = u_d + if traceA is None: + if is_linear_operator: + warn("Trace of LinearOperator not available, it will be estimated." + " Provide `traceA` to ensure performance.", stacklevel=3) + # m3=1 is bit arbitrary choice, a more accurate trace (larger m3) might + # speed up exponential calculation, but trace estimation is more costly + traceA = traceest(A, m3=1) if is_linear_operator else _trace(A) + mu = traceA / float(n) + A = A - mu * ident + A_1_norm = onenormest(A) if is_linear_operator else _exact_1_norm(A) + if t*A_1_norm == 0: + m_star, s = 0, 1 + else: + ell = 2 + norm_info = LazyOperatorNormInfo(t*A, A_1_norm=t*A_1_norm, ell=ell) + m_star, s = _fragment_3_1(norm_info, n0, tol, ell=ell) + return _expm_multiply_simple_core(A, B, t, mu, m_star, s, tol, balance) + + +def _expm_multiply_simple_core(A, B, t, mu, m_star, s, tol=None, balance=False): + """ + A helper function. + """ + if balance: + raise NotImplementedError + if tol is None: + u_d = 2 ** -53 + tol = u_d + F = B + eta = np.exp(t*mu / float(s)) + for i in range(s): + c1 = _exact_inf_norm(B) + for j in range(m_star): + coeff = t / float(s*(j+1)) + B = coeff * A.dot(B) + c2 = _exact_inf_norm(B) + F = F + B + if c1 + c2 <= tol * _exact_inf_norm(F): + break + c1 = c2 + F = eta * F + B = F + return F + + +# This table helps to compute bounds. +# They seem to have been difficult to calculate, involving symbolic +# manipulation of equations, followed by numerical root finding. +_theta = { + # The first 30 values are from table A.3 of Computing Matrix Functions. + 1: 2.29e-16, + 2: 2.58e-8, + 3: 1.39e-5, + 4: 3.40e-4, + 5: 2.40e-3, + 6: 9.07e-3, + 7: 2.38e-2, + 8: 5.00e-2, + 9: 8.96e-2, + 10: 1.44e-1, + # 11 + 11: 2.14e-1, + 12: 3.00e-1, + 13: 4.00e-1, + 14: 5.14e-1, + 15: 6.41e-1, + 16: 7.81e-1, + 17: 9.31e-1, + 18: 1.09, + 19: 1.26, + 20: 1.44, + # 21 + 21: 1.62, + 22: 1.82, + 23: 2.01, + 24: 2.22, + 25: 2.43, + 26: 2.64, + 27: 2.86, + 28: 3.08, + 29: 3.31, + 30: 3.54, + # The rest are from table 3.1 of + # Computing the Action of the Matrix Exponential. + 35: 4.7, + 40: 6.0, + 45: 7.2, + 50: 8.5, + 55: 9.9, + } + + +def _onenormest_matrix_power(A, p, + t=2, itmax=5, compute_v=False, compute_w=False): + """ + Efficiently estimate the 1-norm of A^p. + + Parameters + ---------- + A : ndarray + Matrix whose 1-norm of a power is to be computed. + p : int + Non-negative integer power. + t : int, optional + A positive parameter controlling the tradeoff between + accuracy versus time and memory usage. + Larger values take longer and use more memory + but give more accurate output. + itmax : int, optional + Use at most this many iterations. + compute_v : bool, optional + Request a norm-maximizing linear operator input vector if True. + compute_w : bool, optional + Request a norm-maximizing linear operator output vector if True. + + Returns + ------- + est : float + An underestimate of the 1-norm of the sparse matrix. + v : ndarray, optional + The vector such that ||Av||_1 == est*||v||_1. + It can be thought of as an input to the linear operator + that gives an output with particularly large norm. + w : ndarray, optional + The vector Av which has relatively large 1-norm. + It can be thought of as an output of the linear operator + that is relatively large in norm compared to the input. + + """ + #XXX Eventually turn this into an API function in the _onenormest module, + #XXX and remove its underscore, + #XXX but wait until expm_multiply goes into scipy. + from scipy.sparse.linalg._onenormest import onenormest + return onenormest(aslinearoperator(A) ** p) + +class LazyOperatorNormInfo: + """ + Information about an operator is lazily computed. + + The information includes the exact 1-norm of the operator, + in addition to estimates of 1-norms of powers of the operator. + This uses the notation of Computing the Action (2011). + This class is specialized enough to probably not be of general interest + outside of this module. + + """ + + def __init__(self, A, A_1_norm=None, ell=2, scale=1): + """ + Provide the operator and some norm-related information. + + Parameters + ---------- + A : linear operator + The operator of interest. + A_1_norm : float, optional + The exact 1-norm of A. + ell : int, optional + A technical parameter controlling norm estimation quality. + scale : int, optional + If specified, return the norms of scale*A instead of A. + + """ + self._A = A + self._A_1_norm = A_1_norm + self._ell = ell + self._d = {} + self._scale = scale + + def set_scale(self,scale): + """ + Set the scale parameter. + """ + self._scale = scale + + def onenorm(self): + """ + Compute the exact 1-norm. + """ + if self._A_1_norm is None: + self._A_1_norm = _exact_1_norm(self._A) + return self._scale*self._A_1_norm + + def d(self, p): + """ + Lazily estimate :math:`d_p(A) ~= || A^p ||^(1/p)` where :math:`||.||` is the 1-norm. + """ + if p not in self._d: + est = _onenormest_matrix_power(self._A, p, self._ell) + self._d[p] = est ** (1.0 / p) + return self._scale*self._d[p] + + def alpha(self, p): + """ + Lazily compute max(d(p), d(p+1)). + """ + return max(self.d(p), self.d(p+1)) + +def _compute_cost_div_m(m, p, norm_info): + """ + A helper function for computing bounds. + + This is equation (3.10). + It measures cost in terms of the number of required matrix products. + + Parameters + ---------- + m : int + A valid key of _theta. + p : int + A matrix power. + norm_info : LazyOperatorNormInfo + Information about 1-norms of related operators. + + Returns + ------- + cost_div_m : int + Required number of matrix products divided by m. + + """ + return int(np.ceil(norm_info.alpha(p) / _theta[m])) + + +def _compute_p_max(m_max): + """ + Compute the largest positive integer p such that p*(p-1) <= m_max + 1. + + Do this in a slightly dumb way, but safe and not too slow. + + Parameters + ---------- + m_max : int + A count related to bounds. + + """ + sqrt_m_max = np.sqrt(m_max) + p_low = int(np.floor(sqrt_m_max)) + p_high = int(np.ceil(sqrt_m_max + 1)) + return max(p for p in range(p_low, p_high+1) if p*(p-1) <= m_max + 1) + + +def _fragment_3_1(norm_info, n0, tol, m_max=55, ell=2): + """ + A helper function for the _expm_multiply_* functions. + + Parameters + ---------- + norm_info : LazyOperatorNormInfo + Information about norms of certain linear operators of interest. + n0 : int + Number of columns in the _expm_multiply_* B matrix. + tol : float + Expected to be + :math:`2^{-24}` for single precision or + :math:`2^{-53}` for double precision. + m_max : int + A value related to a bound. + ell : int + The number of columns used in the 1-norm approximation. + This is usually taken to be small, maybe between 1 and 5. + + Returns + ------- + best_m : int + Related to bounds for error control. + best_s : int + Amount of scaling. + + Notes + ----- + This is code fragment (3.1) in Al-Mohy and Higham (2011). + The discussion of default values for m_max and ell + is given between the definitions of equation (3.11) + and the definition of equation (3.12). + + """ + if ell < 1: + raise ValueError('expected ell to be a positive integer') + best_m = None + best_s = None + if _condition_3_13(norm_info.onenorm(), n0, m_max, ell): + for m, theta in _theta.items(): + s = int(np.ceil(norm_info.onenorm() / theta)) + if best_m is None or m * s < best_m * best_s: + best_m = m + best_s = s + else: + # Equation (3.11). + for p in range(2, _compute_p_max(m_max) + 1): + for m in range(p*(p-1)-1, m_max+1): + if m in _theta: + s = _compute_cost_div_m(m, p, norm_info) + if best_m is None or m * s < best_m * best_s: + best_m = m + best_s = s + best_s = max(best_s, 1) + return best_m, best_s + + +def _condition_3_13(A_1_norm, n0, m_max, ell): + """ + A helper function for the _expm_multiply_* functions. + + Parameters + ---------- + A_1_norm : float + The precomputed 1-norm of A. + n0 : int + Number of columns in the _expm_multiply_* B matrix. + m_max : int + A value related to a bound. + ell : int + The number of columns used in the 1-norm approximation. + This is usually taken to be small, maybe between 1 and 5. + + Returns + ------- + value : bool + Indicates whether or not the condition has been met. + + Notes + ----- + This is condition (3.13) in Al-Mohy and Higham (2011). + + """ + + # This is the rhs of equation (3.12). + p_max = _compute_p_max(m_max) + a = 2 * ell * p_max * (p_max + 3) + + # Evaluate the condition (3.13). + b = _theta[m_max] / float(n0 * m_max) + return A_1_norm <= a * b + + +def _expm_multiply_interval(A, B, start=None, stop=None, num=None, + endpoint=None, traceA=None, balance=False, + status_only=False): + """ + Compute the action of the matrix exponential at multiple time points. + + Parameters + ---------- + A : transposable linear operator + The operator whose exponential is of interest. + B : ndarray + The matrix to be multiplied by the matrix exponential of A. + start : scalar, optional + The starting time point of the sequence. + stop : scalar, optional + The end time point of the sequence, unless `endpoint` is set to False. + In that case, the sequence consists of all but the last of ``num + 1`` + evenly spaced time points, so that `stop` is excluded. + Note that the step size changes when `endpoint` is False. + num : int, optional + Number of time points to use. + traceA : scalar, optional + Trace of `A`. If not given the trace is estimated for linear operators, + or calculated exactly for sparse matrices. It is used to precondition + `A`, thus an approximate trace is acceptable + endpoint : bool, optional + If True, `stop` is the last time point. Otherwise, it is not included. + balance : bool + Indicates whether or not to apply balancing. + status_only : bool + A flag that is set to True for some debugging and testing operations. + + Returns + ------- + F : ndarray + :math:`e^{t_k A} B` + status : int + An integer status for testing and debugging. + + Notes + ----- + This is algorithm (5.2) in Al-Mohy and Higham (2011). + + There seems to be a typo, where line 15 of the algorithm should be + moved to line 6.5 (between lines 6 and 7). + + """ + if balance: + raise NotImplementedError + if len(A.shape) != 2 or A.shape[0] != A.shape[1]: + raise ValueError('expected A to be like a square matrix') + if A.shape[1] != B.shape[0]: + raise ValueError('shapes of matrices A {} and B {} are incompatible' + .format(A.shape, B.shape)) + ident = _ident_like(A) + is_linear_operator = isinstance(A, scipy.sparse.linalg.LinearOperator) + n = A.shape[0] + if len(B.shape) == 1: + n0 = 1 + elif len(B.shape) == 2: + n0 = B.shape[1] + else: + raise ValueError('expected B to be like a matrix or a vector') + u_d = 2**-53 + tol = u_d + if traceA is None: + if is_linear_operator: + warn("Trace of LinearOperator not available, it will be estimated." + " Provide `traceA` to ensure performance.", stacklevel=3) + # m3=5 is bit arbitrary choice, a more accurate trace (larger m3) might + # speed up exponential calculation, but trace estimation is also costly + # an educated guess would need to consider the number of time points + traceA = traceest(A, m3=5) if is_linear_operator else _trace(A) + mu = traceA / float(n) + + # Get the linspace samples, attempting to preserve the linspace defaults. + linspace_kwargs = {'retstep': True} + if num is not None: + linspace_kwargs['num'] = num + if endpoint is not None: + linspace_kwargs['endpoint'] = endpoint + samples, step = np.linspace(start, stop, **linspace_kwargs) + + # Convert the linspace output to the notation used by the publication. + nsamples = len(samples) + if nsamples < 2: + raise ValueError('at least two time points are required') + q = nsamples - 1 + h = step + t_0 = samples[0] + t_q = samples[q] + + # Define the output ndarray. + # Use an ndim=3 shape, such that the last two indices + # are the ones that may be involved in level 3 BLAS operations. + X_shape = (nsamples,) + B.shape + X = np.empty(X_shape, dtype=np.result_type(A.dtype, B.dtype, float)) + t = t_q - t_0 + A = A - mu * ident + A_1_norm = onenormest(A) if is_linear_operator else _exact_1_norm(A) + ell = 2 + norm_info = LazyOperatorNormInfo(t*A, A_1_norm=t*A_1_norm, ell=ell) + if t*A_1_norm == 0: + m_star, s = 0, 1 + else: + m_star, s = _fragment_3_1(norm_info, n0, tol, ell=ell) + + # Compute the expm action up to the initial time point. + X[0] = _expm_multiply_simple_core(A, B, t_0, mu, m_star, s) + + # Compute the expm action at the rest of the time points. + if q <= s: + if status_only: + return 0 + else: + return _expm_multiply_interval_core_0(A, X, + h, mu, q, norm_info, tol, ell,n0) + elif not (q % s): + if status_only: + return 1 + else: + return _expm_multiply_interval_core_1(A, X, + h, mu, m_star, s, q, tol) + elif (q % s): + if status_only: + return 2 + else: + return _expm_multiply_interval_core_2(A, X, + h, mu, m_star, s, q, tol) + else: + raise Exception('internal error') + + +def _expm_multiply_interval_core_0(A, X, h, mu, q, norm_info, tol, ell, n0): + """ + A helper function, for the case q <= s. + """ + + # Compute the new values of m_star and s which should be applied + # over intervals of size t/q + if norm_info.onenorm() == 0: + m_star, s = 0, 1 + else: + norm_info.set_scale(1./q) + m_star, s = _fragment_3_1(norm_info, n0, tol, ell=ell) + norm_info.set_scale(1) + + for k in range(q): + X[k+1] = _expm_multiply_simple_core(A, X[k], h, mu, m_star, s) + return X, 0 + + +def _expm_multiply_interval_core_1(A, X, h, mu, m_star, s, q, tol): + """ + A helper function, for the case q > s and q % s == 0. + """ + d = q // s + input_shape = X.shape[1:] + K_shape = (m_star + 1, ) + input_shape + K = np.empty(K_shape, dtype=X.dtype) + for i in range(s): + Z = X[i*d] + K[0] = Z + high_p = 0 + for k in range(1, d+1): + F = K[0] + c1 = _exact_inf_norm(F) + for p in range(1, m_star+1): + if p > high_p: + K[p] = h * A.dot(K[p-1]) / float(p) + coeff = float(pow(k, p)) + F = F + coeff * K[p] + inf_norm_K_p_1 = _exact_inf_norm(K[p]) + c2 = coeff * inf_norm_K_p_1 + if c1 + c2 <= tol * _exact_inf_norm(F): + break + c1 = c2 + X[k + i*d] = np.exp(k*h*mu) * F + return X, 1 + + +def _expm_multiply_interval_core_2(A, X, h, mu, m_star, s, q, tol): + """ + A helper function, for the case q > s and q % s > 0. + """ + d = q // s + j = q // d + r = q - d * j + input_shape = X.shape[1:] + K_shape = (m_star + 1, ) + input_shape + K = np.empty(K_shape, dtype=X.dtype) + for i in range(j + 1): + Z = X[i*d] + K[0] = Z + high_p = 0 + if i < j: + effective_d = d + else: + effective_d = r + for k in range(1, effective_d+1): + F = K[0] + c1 = _exact_inf_norm(F) + for p in range(1, m_star+1): + if p == high_p + 1: + K[p] = h * A.dot(K[p-1]) / float(p) + high_p = p + coeff = float(pow(k, p)) + F = F + coeff * K[p] + inf_norm_K_p_1 = _exact_inf_norm(K[p]) + c2 = coeff * inf_norm_K_p_1 + if c1 + c2 <= tol * _exact_inf_norm(F): + break + c1 = c2 + X[k + i*d] = np.exp(k*h*mu) * F + return X, 2 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_interface.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..86a4e62add97e8e1e1b565aaf6b9fa13521b385d --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_interface.py @@ -0,0 +1,896 @@ +"""Abstract linear algebra library. + +This module defines a class hierarchy that implements a kind of "lazy" +matrix representation, called the ``LinearOperator``. It can be used to do +linear algebra with extremely large sparse or structured matrices, without +representing those explicitly in memory. Such matrices can be added, +multiplied, transposed, etc. + +As a motivating example, suppose you want have a matrix where almost all of +the elements have the value one. The standard sparse matrix representation +skips the storage of zeros, but not ones. By contrast, a LinearOperator is +able to represent such matrices efficiently. First, we need a compact way to +represent an all-ones matrix:: + + >>> import numpy as np + >>> from scipy.sparse.linalg._interface import LinearOperator + >>> class Ones(LinearOperator): + ... def __init__(self, shape): + ... super().__init__(dtype=None, shape=shape) + ... def _matvec(self, x): + ... return np.repeat(x.sum(), self.shape[0]) + +Instances of this class emulate ``np.ones(shape)``, but using a constant +amount of storage, independent of ``shape``. The ``_matvec`` method specifies +how this linear operator multiplies with (operates on) a vector. We can now +add this operator to a sparse matrix that stores only offsets from one:: + + >>> from scipy.sparse.linalg._interface import aslinearoperator + >>> from scipy.sparse import csr_matrix + >>> offsets = csr_matrix([[1, 0, 2], [0, -1, 0], [0, 0, 3]]) + >>> A = aslinearoperator(offsets) + Ones(offsets.shape) + >>> A.dot([1, 2, 3]) + array([13, 4, 15]) + +The result is the same as that given by its dense, explicitly-stored +counterpart:: + + >>> (np.ones(A.shape, A.dtype) + offsets.toarray()).dot([1, 2, 3]) + array([13, 4, 15]) + +Several algorithms in the ``scipy.sparse`` library are able to operate on +``LinearOperator`` instances. +""" + +import warnings + +import numpy as np + +from scipy.sparse import issparse +from scipy.sparse._sputils import isshape, isintlike, asmatrix, is_pydata_spmatrix + +__all__ = ['LinearOperator', 'aslinearoperator'] + + +class LinearOperator: + """Common interface for performing matrix vector products + + Many iterative methods (e.g. cg, gmres) do not need to know the + individual entries of a matrix to solve a linear system A*x=b. + Such solvers only require the computation of matrix vector + products, A*v where v is a dense vector. This class serves as + an abstract interface between iterative solvers and matrix-like + objects. + + To construct a concrete LinearOperator, either pass appropriate + callables to the constructor of this class, or subclass it. + + A subclass must implement either one of the methods ``_matvec`` + and ``_matmat``, and the attributes/properties ``shape`` (pair of + integers) and ``dtype`` (may be None). It may call the ``__init__`` + on this class to have these attributes validated. Implementing + ``_matvec`` automatically implements ``_matmat`` (using a naive + algorithm) and vice-versa. + + Optionally, a subclass may implement ``_rmatvec`` or ``_adjoint`` + to implement the Hermitian adjoint (conjugate transpose). As with + ``_matvec`` and ``_matmat``, implementing either ``_rmatvec`` or + ``_adjoint`` implements the other automatically. Implementing + ``_adjoint`` is preferable; ``_rmatvec`` is mostly there for + backwards compatibility. + + Parameters + ---------- + shape : tuple + Matrix dimensions (M, N). + matvec : callable f(v) + Returns returns A * v. + rmatvec : callable f(v) + Returns A^H * v, where A^H is the conjugate transpose of A. + matmat : callable f(V) + Returns A * V, where V is a dense matrix with dimensions (N, K). + dtype : dtype + Data type of the matrix. + rmatmat : callable f(V) + Returns A^H * V, where V is a dense matrix with dimensions (M, K). + + Attributes + ---------- + args : tuple + For linear operators describing products etc. of other linear + operators, the operands of the binary operation. + ndim : int + Number of dimensions (this is always 2) + + See Also + -------- + aslinearoperator : Construct LinearOperators + + Notes + ----- + The user-defined matvec() function must properly handle the case + where v has shape (N,) as well as the (N,1) case. The shape of + the return type is handled internally by LinearOperator. + + LinearOperator instances can also be multiplied, added with each + other and exponentiated, all lazily: the result of these operations + is always a new, composite LinearOperator, that defers linear + operations to the original operators and combines the results. + + More details regarding how to subclass a LinearOperator and several + examples of concrete LinearOperator instances can be found in the + external project `PyLops `_. + + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg import LinearOperator + >>> def mv(v): + ... return np.array([2*v[0], 3*v[1]]) + ... + >>> A = LinearOperator((2,2), matvec=mv) + >>> A + <2x2 _CustomLinearOperator with dtype=float64> + >>> A.matvec(np.ones(2)) + array([ 2., 3.]) + >>> A * np.ones(2) + array([ 2., 3.]) + + """ + + ndim = 2 + # Necessary for right matmul with numpy arrays. + __array_ufunc__ = None + + def __new__(cls, *args, **kwargs): + if cls is LinearOperator: + # Operate as _CustomLinearOperator factory. + return super().__new__(_CustomLinearOperator) + else: + obj = super().__new__(cls) + + if (type(obj)._matvec == LinearOperator._matvec + and type(obj)._matmat == LinearOperator._matmat): + warnings.warn("LinearOperator subclass should implement" + " at least one of _matvec and _matmat.", + category=RuntimeWarning, stacklevel=2) + + return obj + + def __init__(self, dtype, shape): + """Initialize this LinearOperator. + + To be called by subclasses. ``dtype`` may be None; ``shape`` should + be convertible to a length-2 tuple. + """ + if dtype is not None: + dtype = np.dtype(dtype) + + shape = tuple(shape) + if not isshape(shape): + raise ValueError(f"invalid shape {shape!r} (must be 2-d)") + + self.dtype = dtype + self.shape = shape + + def _init_dtype(self): + """Called from subclasses at the end of the __init__ routine. + """ + if self.dtype is None: + v = np.zeros(self.shape[-1]) + self.dtype = np.asarray(self.matvec(v)).dtype + + def _matmat(self, X): + """Default matrix-matrix multiplication handler. + + Falls back on the user-defined _matvec method, so defining that will + define matrix multiplication (though in a very suboptimal way). + """ + + return np.hstack([self.matvec(col.reshape(-1,1)) for col in X.T]) + + def _matvec(self, x): + """Default matrix-vector multiplication handler. + + If self is a linear operator of shape (M, N), then this method will + be called on a shape (N,) or (N, 1) ndarray, and should return a + shape (M,) or (M, 1) ndarray. + + This default implementation falls back on _matmat, so defining that + will define matrix-vector multiplication as well. + """ + return self.matmat(x.reshape(-1, 1)) + + def matvec(self, x): + """Matrix-vector multiplication. + + Performs the operation y=A*x where A is an MxN linear + operator and x is a column vector or 1-d array. + + Parameters + ---------- + x : {matrix, ndarray} + An array with shape (N,) or (N,1). + + Returns + ------- + y : {matrix, ndarray} + A matrix or ndarray with shape (M,) or (M,1) depending + on the type and shape of the x argument. + + Notes + ----- + This matvec wraps the user-specified matvec routine or overridden + _matvec method to ensure that y has the correct shape and type. + + """ + + x = np.asanyarray(x) + + M,N = self.shape + + if x.shape != (N,) and x.shape != (N,1): + raise ValueError('dimension mismatch') + + y = self._matvec(x) + + if isinstance(x, np.matrix): + y = asmatrix(y) + else: + y = np.asarray(y) + + if x.ndim == 1: + y = y.reshape(M) + elif x.ndim == 2: + y = y.reshape(M,1) + else: + raise ValueError('invalid shape returned by user-defined matvec()') + + return y + + def rmatvec(self, x): + """Adjoint matrix-vector multiplication. + + Performs the operation y = A^H * x where A is an MxN linear + operator and x is a column vector or 1-d array. + + Parameters + ---------- + x : {matrix, ndarray} + An array with shape (M,) or (M,1). + + Returns + ------- + y : {matrix, ndarray} + A matrix or ndarray with shape (N,) or (N,1) depending + on the type and shape of the x argument. + + Notes + ----- + This rmatvec wraps the user-specified rmatvec routine or overridden + _rmatvec method to ensure that y has the correct shape and type. + + """ + + x = np.asanyarray(x) + + M,N = self.shape + + if x.shape != (M,) and x.shape != (M,1): + raise ValueError('dimension mismatch') + + y = self._rmatvec(x) + + if isinstance(x, np.matrix): + y = asmatrix(y) + else: + y = np.asarray(y) + + if x.ndim == 1: + y = y.reshape(N) + elif x.ndim == 2: + y = y.reshape(N,1) + else: + raise ValueError('invalid shape returned by user-defined rmatvec()') + + return y + + def _rmatvec(self, x): + """Default implementation of _rmatvec; defers to adjoint.""" + if type(self)._adjoint == LinearOperator._adjoint: + # _adjoint not overridden, prevent infinite recursion + raise NotImplementedError + else: + return self.H.matvec(x) + + def matmat(self, X): + """Matrix-matrix multiplication. + + Performs the operation y=A*X where A is an MxN linear + operator and X dense N*K matrix or ndarray. + + Parameters + ---------- + X : {matrix, ndarray} + An array with shape (N,K). + + Returns + ------- + Y : {matrix, ndarray} + A matrix or ndarray with shape (M,K) depending on + the type of the X argument. + + Notes + ----- + This matmat wraps any user-specified matmat routine or overridden + _matmat method to ensure that y has the correct type. + + """ + if not (issparse(X) or is_pydata_spmatrix(X)): + X = np.asanyarray(X) + + if X.ndim != 2: + raise ValueError(f'expected 2-d ndarray or matrix, not {X.ndim}-d') + + if X.shape[0] != self.shape[1]: + raise ValueError(f'dimension mismatch: {self.shape}, {X.shape}') + + try: + Y = self._matmat(X) + except Exception as e: + if issparse(X) or is_pydata_spmatrix(X): + raise TypeError( + "Unable to multiply a LinearOperator with a sparse matrix." + " Wrap the matrix in aslinearoperator first." + ) from e + raise + + if isinstance(Y, np.matrix): + Y = asmatrix(Y) + + return Y + + def rmatmat(self, X): + """Adjoint matrix-matrix multiplication. + + Performs the operation y = A^H * x where A is an MxN linear + operator and x is a column vector or 1-d array, or 2-d array. + The default implementation defers to the adjoint. + + Parameters + ---------- + X : {matrix, ndarray} + A matrix or 2D array. + + Returns + ------- + Y : {matrix, ndarray} + A matrix or 2D array depending on the type of the input. + + Notes + ----- + This rmatmat wraps the user-specified rmatmat routine. + + """ + if not (issparse(X) or is_pydata_spmatrix(X)): + X = np.asanyarray(X) + + if X.ndim != 2: + raise ValueError('expected 2-d ndarray or matrix, not %d-d' + % X.ndim) + + if X.shape[0] != self.shape[0]: + raise ValueError(f'dimension mismatch: {self.shape}, {X.shape}') + + try: + Y = self._rmatmat(X) + except Exception as e: + if issparse(X) or is_pydata_spmatrix(X): + raise TypeError( + "Unable to multiply a LinearOperator with a sparse matrix." + " Wrap the matrix in aslinearoperator() first." + ) from e + raise + + if isinstance(Y, np.matrix): + Y = asmatrix(Y) + return Y + + def _rmatmat(self, X): + """Default implementation of _rmatmat defers to rmatvec or adjoint.""" + if type(self)._adjoint == LinearOperator._adjoint: + return np.hstack([self.rmatvec(col.reshape(-1, 1)) for col in X.T]) + else: + return self.H.matmat(X) + + def __call__(self, x): + return self*x + + def __mul__(self, x): + return self.dot(x) + + def __truediv__(self, other): + if not np.isscalar(other): + raise ValueError("Can only divide a linear operator by a scalar.") + + return _ScaledLinearOperator(self, 1.0/other) + + def dot(self, x): + """Matrix-matrix or matrix-vector multiplication. + + Parameters + ---------- + x : array_like + 1-d or 2-d array, representing a vector or matrix. + + Returns + ------- + Ax : array + 1-d or 2-d array (depending on the shape of x) that represents + the result of applying this linear operator on x. + + """ + if isinstance(x, LinearOperator): + return _ProductLinearOperator(self, x) + elif np.isscalar(x): + return _ScaledLinearOperator(self, x) + else: + if not issparse(x) and not is_pydata_spmatrix(x): + # Sparse matrices shouldn't be converted to numpy arrays. + x = np.asarray(x) + + if x.ndim == 1 or x.ndim == 2 and x.shape[1] == 1: + return self.matvec(x) + elif x.ndim == 2: + return self.matmat(x) + else: + raise ValueError('expected 1-d or 2-d array or matrix, got %r' + % x) + + def __matmul__(self, other): + if np.isscalar(other): + raise ValueError("Scalar operands are not allowed, " + "use '*' instead") + return self.__mul__(other) + + def __rmatmul__(self, other): + if np.isscalar(other): + raise ValueError("Scalar operands are not allowed, " + "use '*' instead") + return self.__rmul__(other) + + def __rmul__(self, x): + if np.isscalar(x): + return _ScaledLinearOperator(self, x) + else: + return self._rdot(x) + + def _rdot(self, x): + """Matrix-matrix or matrix-vector multiplication from the right. + + Parameters + ---------- + x : array_like + 1-d or 2-d array, representing a vector or matrix. + + Returns + ------- + xA : array + 1-d or 2-d array (depending on the shape of x) that represents + the result of applying this linear operator on x from the right. + + Notes + ----- + This is copied from dot to implement right multiplication. + """ + if isinstance(x, LinearOperator): + return _ProductLinearOperator(x, self) + elif np.isscalar(x): + return _ScaledLinearOperator(self, x) + else: + if not issparse(x) and not is_pydata_spmatrix(x): + # Sparse matrices shouldn't be converted to numpy arrays. + x = np.asarray(x) + + # We use transpose instead of rmatvec/rmatmat to avoid + # unnecessary complex conjugation if possible. + if x.ndim == 1 or x.ndim == 2 and x.shape[0] == 1: + return self.T.matvec(x.T).T + elif x.ndim == 2: + return self.T.matmat(x.T).T + else: + raise ValueError('expected 1-d or 2-d array or matrix, got %r' + % x) + + def __pow__(self, p): + if np.isscalar(p): + return _PowerLinearOperator(self, p) + else: + return NotImplemented + + def __add__(self, x): + if isinstance(x, LinearOperator): + return _SumLinearOperator(self, x) + else: + return NotImplemented + + def __neg__(self): + return _ScaledLinearOperator(self, -1) + + def __sub__(self, x): + return self.__add__(-x) + + def __repr__(self): + M,N = self.shape + if self.dtype is None: + dt = 'unspecified dtype' + else: + dt = 'dtype=' + str(self.dtype) + + return '<%dx%d %s with %s>' % (M, N, self.__class__.__name__, dt) + + def adjoint(self): + """Hermitian adjoint. + + Returns the Hermitian adjoint of self, aka the Hermitian + conjugate or Hermitian transpose. For a complex matrix, the + Hermitian adjoint is equal to the conjugate transpose. + + Can be abbreviated self.H instead of self.adjoint(). + + Returns + ------- + A_H : LinearOperator + Hermitian adjoint of self. + """ + return self._adjoint() + + H = property(adjoint) + + def transpose(self): + """Transpose this linear operator. + + Returns a LinearOperator that represents the transpose of this one. + Can be abbreviated self.T instead of self.transpose(). + """ + return self._transpose() + + T = property(transpose) + + def _adjoint(self): + """Default implementation of _adjoint; defers to rmatvec.""" + return _AdjointLinearOperator(self) + + def _transpose(self): + """ Default implementation of _transpose; defers to rmatvec + conj""" + return _TransposedLinearOperator(self) + + +class _CustomLinearOperator(LinearOperator): + """Linear operator defined in terms of user-specified operations.""" + + def __init__(self, shape, matvec, rmatvec=None, matmat=None, + dtype=None, rmatmat=None): + super().__init__(dtype, shape) + + self.args = () + + self.__matvec_impl = matvec + self.__rmatvec_impl = rmatvec + self.__rmatmat_impl = rmatmat + self.__matmat_impl = matmat + + self._init_dtype() + + def _matmat(self, X): + if self.__matmat_impl is not None: + return self.__matmat_impl(X) + else: + return super()._matmat(X) + + def _matvec(self, x): + return self.__matvec_impl(x) + + def _rmatvec(self, x): + func = self.__rmatvec_impl + if func is None: + raise NotImplementedError("rmatvec is not defined") + return self.__rmatvec_impl(x) + + def _rmatmat(self, X): + if self.__rmatmat_impl is not None: + return self.__rmatmat_impl(X) + else: + return super()._rmatmat(X) + + def _adjoint(self): + return _CustomLinearOperator(shape=(self.shape[1], self.shape[0]), + matvec=self.__rmatvec_impl, + rmatvec=self.__matvec_impl, + matmat=self.__rmatmat_impl, + rmatmat=self.__matmat_impl, + dtype=self.dtype) + + +class _AdjointLinearOperator(LinearOperator): + """Adjoint of arbitrary Linear Operator""" + + def __init__(self, A): + shape = (A.shape[1], A.shape[0]) + super().__init__(dtype=A.dtype, shape=shape) + self.A = A + self.args = (A,) + + def _matvec(self, x): + return self.A._rmatvec(x) + + def _rmatvec(self, x): + return self.A._matvec(x) + + def _matmat(self, x): + return self.A._rmatmat(x) + + def _rmatmat(self, x): + return self.A._matmat(x) + +class _TransposedLinearOperator(LinearOperator): + """Transposition of arbitrary Linear Operator""" + + def __init__(self, A): + shape = (A.shape[1], A.shape[0]) + super().__init__(dtype=A.dtype, shape=shape) + self.A = A + self.args = (A,) + + def _matvec(self, x): + # NB. np.conj works also on sparse matrices + return np.conj(self.A._rmatvec(np.conj(x))) + + def _rmatvec(self, x): + return np.conj(self.A._matvec(np.conj(x))) + + def _matmat(self, x): + # NB. np.conj works also on sparse matrices + return np.conj(self.A._rmatmat(np.conj(x))) + + def _rmatmat(self, x): + return np.conj(self.A._matmat(np.conj(x))) + +def _get_dtype(operators, dtypes=None): + if dtypes is None: + dtypes = [] + for obj in operators: + if obj is not None and hasattr(obj, 'dtype'): + dtypes.append(obj.dtype) + return np.result_type(*dtypes) + + +class _SumLinearOperator(LinearOperator): + def __init__(self, A, B): + if not isinstance(A, LinearOperator) or \ + not isinstance(B, LinearOperator): + raise ValueError('both operands have to be a LinearOperator') + if A.shape != B.shape: + raise ValueError(f'cannot add {A} and {B}: shape mismatch') + self.args = (A, B) + super().__init__(_get_dtype([A, B]), A.shape) + + def _matvec(self, x): + return self.args[0].matvec(x) + self.args[1].matvec(x) + + def _rmatvec(self, x): + return self.args[0].rmatvec(x) + self.args[1].rmatvec(x) + + def _rmatmat(self, x): + return self.args[0].rmatmat(x) + self.args[1].rmatmat(x) + + def _matmat(self, x): + return self.args[0].matmat(x) + self.args[1].matmat(x) + + def _adjoint(self): + A, B = self.args + return A.H + B.H + + +class _ProductLinearOperator(LinearOperator): + def __init__(self, A, B): + if not isinstance(A, LinearOperator) or \ + not isinstance(B, LinearOperator): + raise ValueError('both operands have to be a LinearOperator') + if A.shape[1] != B.shape[0]: + raise ValueError(f'cannot multiply {A} and {B}: shape mismatch') + super().__init__(_get_dtype([A, B]), + (A.shape[0], B.shape[1])) + self.args = (A, B) + + def _matvec(self, x): + return self.args[0].matvec(self.args[1].matvec(x)) + + def _rmatvec(self, x): + return self.args[1].rmatvec(self.args[0].rmatvec(x)) + + def _rmatmat(self, x): + return self.args[1].rmatmat(self.args[0].rmatmat(x)) + + def _matmat(self, x): + return self.args[0].matmat(self.args[1].matmat(x)) + + def _adjoint(self): + A, B = self.args + return B.H * A.H + + +class _ScaledLinearOperator(LinearOperator): + def __init__(self, A, alpha): + if not isinstance(A, LinearOperator): + raise ValueError('LinearOperator expected as A') + if not np.isscalar(alpha): + raise ValueError('scalar expected as alpha') + if isinstance(A, _ScaledLinearOperator): + A, alpha_original = A.args + # Avoid in-place multiplication so that we don't accidentally mutate + # the original prefactor. + alpha = alpha * alpha_original + + dtype = _get_dtype([A], [type(alpha)]) + super().__init__(dtype, A.shape) + self.args = (A, alpha) + + def _matvec(self, x): + return self.args[1] * self.args[0].matvec(x) + + def _rmatvec(self, x): + return np.conj(self.args[1]) * self.args[0].rmatvec(x) + + def _rmatmat(self, x): + return np.conj(self.args[1]) * self.args[0].rmatmat(x) + + def _matmat(self, x): + return self.args[1] * self.args[0].matmat(x) + + def _adjoint(self): + A, alpha = self.args + return A.H * np.conj(alpha) + + +class _PowerLinearOperator(LinearOperator): + def __init__(self, A, p): + if not isinstance(A, LinearOperator): + raise ValueError('LinearOperator expected as A') + if A.shape[0] != A.shape[1]: + raise ValueError('square LinearOperator expected, got %r' % A) + if not isintlike(p) or p < 0: + raise ValueError('non-negative integer expected as p') + + super().__init__(_get_dtype([A]), A.shape) + self.args = (A, p) + + def _power(self, fun, x): + res = np.array(x, copy=True) + for i in range(self.args[1]): + res = fun(res) + return res + + def _matvec(self, x): + return self._power(self.args[0].matvec, x) + + def _rmatvec(self, x): + return self._power(self.args[0].rmatvec, x) + + def _rmatmat(self, x): + return self._power(self.args[0].rmatmat, x) + + def _matmat(self, x): + return self._power(self.args[0].matmat, x) + + def _adjoint(self): + A, p = self.args + return A.H ** p + + +class MatrixLinearOperator(LinearOperator): + def __init__(self, A): + super().__init__(A.dtype, A.shape) + self.A = A + self.__adj = None + self.args = (A,) + + def _matmat(self, X): + return self.A.dot(X) + + def _adjoint(self): + if self.__adj is None: + self.__adj = _AdjointMatrixOperator(self) + return self.__adj + +class _AdjointMatrixOperator(MatrixLinearOperator): + def __init__(self, adjoint): + self.A = adjoint.A.T.conj() + self.__adjoint = adjoint + self.args = (adjoint,) + self.shape = adjoint.shape[1], adjoint.shape[0] + + @property + def dtype(self): + return self.__adjoint.dtype + + def _adjoint(self): + return self.__adjoint + + +class IdentityOperator(LinearOperator): + def __init__(self, shape, dtype=None): + super().__init__(dtype, shape) + + def _matvec(self, x): + return x + + def _rmatvec(self, x): + return x + + def _rmatmat(self, x): + return x + + def _matmat(self, x): + return x + + def _adjoint(self): + return self + + +def aslinearoperator(A): + """Return A as a LinearOperator. + + 'A' may be any of the following types: + - ndarray + - matrix + - sparse matrix (e.g. csr_matrix, lil_matrix, etc.) + - LinearOperator + - An object with .shape and .matvec attributes + + See the LinearOperator documentation for additional information. + + Notes + ----- + If 'A' has no .dtype attribute, the data type is determined by calling + :func:`LinearOperator.matvec()` - set the .dtype attribute to prevent this + call upon the linear operator creation. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg import aslinearoperator + >>> M = np.array([[1,2,3],[4,5,6]], dtype=np.int32) + >>> aslinearoperator(M) + <2x3 MatrixLinearOperator with dtype=int32> + """ + if isinstance(A, LinearOperator): + return A + + elif isinstance(A, np.ndarray) or isinstance(A, np.matrix): + if A.ndim > 2: + raise ValueError('array must have ndim <= 2') + A = np.atleast_2d(np.asarray(A)) + return MatrixLinearOperator(A) + + elif issparse(A) or is_pydata_spmatrix(A): + return MatrixLinearOperator(A) + + else: + if hasattr(A, 'shape') and hasattr(A, 'matvec'): + rmatvec = None + rmatmat = None + dtype = None + + if hasattr(A, 'rmatvec'): + rmatvec = A.rmatvec + if hasattr(A, 'rmatmat'): + rmatmat = A.rmatmat + if hasattr(A, 'dtype'): + dtype = A.dtype + return LinearOperator(A.shape, A.matvec, rmatvec=rmatvec, + rmatmat=rmatmat, dtype=dtype) + + else: + raise TypeError('type not understood') diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..9c17f953c0b1938324e3b553709f0a17b626df3a --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/__init__.py @@ -0,0 +1,20 @@ +"Iterative Solvers for Sparse Linear Systems" + +#from info import __doc__ +from .iterative import * +from .minres import minres +from .lgmres import lgmres +from .lsqr import lsqr +from .lsmr import lsmr +from ._gcrotmk import gcrotmk +from .tfqmr import tfqmr + +__all__ = [ + 'bicg', 'bicgstab', 'cg', 'cgs', 'gcrotmk', 'gmres', + 'lgmres', 'lsmr', 'lsqr', + 'minres', 'qmr', 'tfqmr' +] + +from scipy._lib._testutils import PytestTester +test = PytestTester(__name__) +del PytestTester diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/_gcrotmk.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/_gcrotmk.py new file mode 100644 index 0000000000000000000000000000000000000000..3f4bbf9adbc973bd66bfdef6ef8f61299cd1f057 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/_gcrotmk.py @@ -0,0 +1,514 @@ +# Copyright (C) 2015, Pauli Virtanen +# Distributed under the same license as SciPy. + +import numpy as np +from numpy.linalg import LinAlgError +from scipy.linalg import (get_blas_funcs, qr, solve, svd, qr_insert, lstsq) +from .iterative import _get_atol_rtol +from scipy.sparse.linalg._isolve.utils import make_system +from scipy._lib.deprecation import _NoValue, _deprecate_positional_args + + +__all__ = ['gcrotmk'] + + +def _fgmres(matvec, v0, m, atol, lpsolve=None, rpsolve=None, cs=(), outer_v=(), + prepend_outer_v=False): + """ + FGMRES Arnoldi process, with optional projection or augmentation + + Parameters + ---------- + matvec : callable + Operation A*x + v0 : ndarray + Initial vector, normalized to nrm2(v0) == 1 + m : int + Number of GMRES rounds + atol : float + Absolute tolerance for early exit + lpsolve : callable + Left preconditioner L + rpsolve : callable + Right preconditioner R + cs : list of (ndarray, ndarray) + Columns of matrices C and U in GCROT + outer_v : list of ndarrays + Augmentation vectors in LGMRES + prepend_outer_v : bool, optional + Whether augmentation vectors come before or after + Krylov iterates + + Raises + ------ + LinAlgError + If nans encountered + + Returns + ------- + Q, R : ndarray + QR decomposition of the upper Hessenberg H=QR + B : ndarray + Projections corresponding to matrix C + vs : list of ndarray + Columns of matrix V + zs : list of ndarray + Columns of matrix Z + y : ndarray + Solution to ||H y - e_1||_2 = min! + res : float + The final (preconditioned) residual norm + + """ + + if lpsolve is None: + def lpsolve(x): + return x + if rpsolve is None: + def rpsolve(x): + return x + + axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'], (v0,)) + + vs = [v0] + zs = [] + y = None + res = np.nan + + m = m + len(outer_v) + + # Orthogonal projection coefficients + B = np.zeros((len(cs), m), dtype=v0.dtype) + + # H is stored in QR factorized form + Q = np.ones((1, 1), dtype=v0.dtype) + R = np.zeros((1, 0), dtype=v0.dtype) + + eps = np.finfo(v0.dtype).eps + + breakdown = False + + # FGMRES Arnoldi process + for j in range(m): + # L A Z = C B + V H + + if prepend_outer_v and j < len(outer_v): + z, w = outer_v[j] + elif prepend_outer_v and j == len(outer_v): + z = rpsolve(v0) + w = None + elif not prepend_outer_v and j >= m - len(outer_v): + z, w = outer_v[j - (m - len(outer_v))] + else: + z = rpsolve(vs[-1]) + w = None + + if w is None: + w = lpsolve(matvec(z)) + else: + # w is clobbered below + w = w.copy() + + w_norm = nrm2(w) + + # GCROT projection: L A -> (1 - C C^H) L A + # i.e. orthogonalize against C + for i, c in enumerate(cs): + alpha = dot(c, w) + B[i,j] = alpha + w = axpy(c, w, c.shape[0], -alpha) # w -= alpha*c + + # Orthogonalize against V + hcur = np.zeros(j+2, dtype=Q.dtype) + for i, v in enumerate(vs): + alpha = dot(v, w) + hcur[i] = alpha + w = axpy(v, w, v.shape[0], -alpha) # w -= alpha*v + hcur[i+1] = nrm2(w) + + with np.errstate(over='ignore', divide='ignore'): + # Careful with denormals + alpha = 1/hcur[-1] + + if np.isfinite(alpha): + w = scal(alpha, w) + + if not (hcur[-1] > eps * w_norm): + # w essentially in the span of previous vectors, + # or we have nans. Bail out after updating the QR + # solution. + breakdown = True + + vs.append(w) + zs.append(z) + + # Arnoldi LSQ problem + + # Add new column to H=Q@R, padding other columns with zeros + Q2 = np.zeros((j+2, j+2), dtype=Q.dtype, order='F') + Q2[:j+1,:j+1] = Q + Q2[j+1,j+1] = 1 + + R2 = np.zeros((j+2, j), dtype=R.dtype, order='F') + R2[:j+1,:] = R + + Q, R = qr_insert(Q2, R2, hcur, j, which='col', + overwrite_qru=True, check_finite=False) + + # Transformed least squares problem + # || Q R y - inner_res_0 * e_1 ||_2 = min! + # Since R = [R'; 0], solution is y = inner_res_0 (R')^{-1} (Q^H)[:j,0] + + # Residual is immediately known + res = abs(Q[0,-1]) + + # Check for termination + if res < atol or breakdown: + break + + if not np.isfinite(R[j,j]): + # nans encountered, bail out + raise LinAlgError() + + # -- Get the LSQ problem solution + + # The problem is triangular, but the condition number may be + # bad (or in case of breakdown the last diagonal entry may be + # zero), so use lstsq instead of trtrs. + y, _, _, _, = lstsq(R[:j+1,:j+1], Q[0,:j+1].conj()) + + B = B[:,:j+1] + + return Q, R, B, vs, zs, y, res + + +@_deprecate_positional_args(version="1.14.0") +def gcrotmk(A, b, x0=None, *, tol=_NoValue, maxiter=1000, M=None, callback=None, + m=20, k=None, CU=None, discard_C=False, truncate='oldest', + atol=None, rtol=1e-5): + """ + Solve a matrix equation using flexible GCROT(m,k) algorithm. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real or complex N-by-N matrix of the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution. + rtol, atol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``rtol=1e-5``, the default for ``atol`` is ``rtol``. + + .. warning:: + + The default value for ``atol`` will be changed to ``0.0`` in + SciPy 1.14.0. + maxiter : int, optional + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M : {sparse matrix, ndarray, LinearOperator}, optional + Preconditioner for A. The preconditioner should approximate the + inverse of A. gcrotmk is a 'flexible' algorithm and the preconditioner + can vary from iteration to iteration. Effective preconditioning + dramatically improves the rate of convergence, which implies that + fewer iterations are needed to reach a given error tolerance. + callback : function, optional + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + m : int, optional + Number of inner FGMRES iterations per each outer iteration. + Default: 20 + k : int, optional + Number of vectors to carry between inner FGMRES iterations. + According to [2]_, good values are around m. + Default: m + CU : list of tuples, optional + List of tuples ``(c, u)`` which contain the columns of the matrices + C and U in the GCROT(m,k) algorithm. For details, see [2]_. + The list given and vectors contained in it are modified in-place. + If not given, start from empty matrices. The ``c`` elements in the + tuples can be ``None``, in which case the vectors are recomputed + via ``c = A u`` on start and orthogonalized as described in [3]_. + discard_C : bool, optional + Discard the C-vectors at the end. Useful if recycling Krylov subspaces + for different linear systems. + truncate : {'oldest', 'smallest'}, optional + Truncation scheme to use. Drop: oldest vectors, or vectors with + smallest singular values using the scheme discussed in [1,2]. + See [2]_ for detailed comparison. + Default: 'oldest' + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `gcrotmk` keyword argument ``tol`` is deprecated in favor of + ``rtol`` and will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The solution found. + info : int + Provides convergence information: + + * 0 : successful exit + * >0 : convergence to tolerance not achieved, number of iterations + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import gcrotmk + >>> R = np.random.randn(5, 5) + >>> A = csc_matrix(R) + >>> b = np.random.randn(5) + >>> x, exit_code = gcrotmk(A, b, atol=1e-5) + >>> print(exit_code) + 0 + >>> np.allclose(A.dot(x), b) + True + + References + ---------- + .. [1] E. de Sturler, ''Truncation strategies for optimal Krylov subspace + methods'', SIAM J. Numer. Anal. 36, 864 (1999). + .. [2] J.E. Hicken and D.W. Zingg, ''A simplified and flexible variant + of GCROT for solving nonsymmetric linear systems'', + SIAM J. Sci. Comput. 32, 172 (2010). + .. [3] M.L. Parks, E. de Sturler, G. Mackey, D.D. Johnson, S. Maiti, + ''Recycling Krylov subspaces for sequences of linear systems'', + SIAM J. Sci. Comput. 28, 1651 (2006). + + """ + A,M,x,b,postprocess = make_system(A,M,x0,b) + + if not np.isfinite(b).all(): + raise ValueError("RHS must contain only finite numbers") + + if truncate not in ('oldest', 'smallest'): + raise ValueError(f"Invalid value for 'truncate': {truncate!r}") + + matvec = A.matvec + psolve = M.matvec + + if CU is None: + CU = [] + + if k is None: + k = m + + axpy, dot, scal = None, None, None + + if x0 is None: + r = b.copy() + else: + r = b - matvec(x) + + axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'], (x, r)) + + b_norm = nrm2(b) + + # we call this to get the right atol/rtol and raise warnings as necessary + atol, rtol = _get_atol_rtol('gcrotmk', b_norm, tol, atol, rtol) + + if b_norm == 0: + x = b + return (postprocess(x), 0) + + if discard_C: + CU[:] = [(None, u) for c, u in CU] + + # Reorthogonalize old vectors + if CU: + # Sort already existing vectors to the front + CU.sort(key=lambda cu: cu[0] is not None) + + # Fill-in missing ones + C = np.empty((A.shape[0], len(CU)), dtype=r.dtype, order='F') + us = [] + j = 0 + while CU: + # More memory-efficient: throw away old vectors as we go + c, u = CU.pop(0) + if c is None: + c = matvec(u) + C[:,j] = c + j += 1 + us.append(u) + + # Orthogonalize + Q, R, P = qr(C, overwrite_a=True, mode='economic', pivoting=True) + del C + + # C := Q + cs = list(Q.T) + + # U := U P R^-1, back-substitution + new_us = [] + for j in range(len(cs)): + u = us[P[j]] + for i in range(j): + u = axpy(us[P[i]], u, u.shape[0], -R[i,j]) + if abs(R[j,j]) < 1e-12 * abs(R[0,0]): + # discard rest of the vectors + break + u = scal(1.0/R[j,j], u) + new_us.append(u) + + # Form the new CU lists + CU[:] = list(zip(cs, new_us))[::-1] + + if CU: + axpy, dot = get_blas_funcs(['axpy', 'dot'], (r,)) + + # Solve first the projection operation with respect to the CU + # vectors. This corresponds to modifying the initial guess to + # be + # + # x' = x + U y + # y = argmin_y || b - A (x + U y) ||^2 + # + # The solution is y = C^H (b - A x) + for c, u in CU: + yc = dot(c, r) + x = axpy(u, x, x.shape[0], yc) + r = axpy(c, r, r.shape[0], -yc) + + # GCROT main iteration + for j_outer in range(maxiter): + # -- callback + if callback is not None: + callback(x) + + beta = nrm2(r) + + # -- check stopping condition + beta_tol = max(atol, rtol * b_norm) + + if beta <= beta_tol and (j_outer > 0 or CU): + # recompute residual to avoid rounding error + r = b - matvec(x) + beta = nrm2(r) + + if beta <= beta_tol: + j_outer = -1 + break + + ml = m + max(k - len(CU), 0) + + cs = [c for c, u in CU] + + try: + Q, R, B, vs, zs, y, pres = _fgmres(matvec, + r/beta, + ml, + rpsolve=psolve, + atol=max(atol, rtol*b_norm)/beta, + cs=cs) + y *= beta + except LinAlgError: + # Floating point over/underflow, non-finite result from + # matmul etc. -- report failure. + break + + # + # At this point, + # + # [A U, A Z] = [C, V] G; G = [ I B ] + # [ 0 H ] + # + # where [C, V] has orthonormal columns, and r = beta v_0. Moreover, + # + # || b - A (x + Z y + U q) ||_2 = || r - C B y - V H y - C q ||_2 = min! + # + # from which y = argmin_y || beta e_1 - H y ||_2, and q = -B y + # + + # + # GCROT(m,k) update + # + + # Define new outer vectors + + # ux := (Z - U B) y + ux = zs[0]*y[0] + for z, yc in zip(zs[1:], y[1:]): + ux = axpy(z, ux, ux.shape[0], yc) # ux += z*yc + by = B.dot(y) + for cu, byc in zip(CU, by): + c, u = cu + ux = axpy(u, ux, ux.shape[0], -byc) # ux -= u*byc + + # cx := V H y + hy = Q.dot(R.dot(y)) + cx = vs[0] * hy[0] + for v, hyc in zip(vs[1:], hy[1:]): + cx = axpy(v, cx, cx.shape[0], hyc) # cx += v*hyc + + # Normalize cx, maintaining cx = A ux + # This new cx is orthogonal to the previous C, by construction + try: + alpha = 1/nrm2(cx) + if not np.isfinite(alpha): + raise FloatingPointError() + except (FloatingPointError, ZeroDivisionError): + # Cannot update, so skip it + continue + + cx = scal(alpha, cx) + ux = scal(alpha, ux) + + # Update residual and solution + gamma = dot(cx, r) + r = axpy(cx, r, r.shape[0], -gamma) # r -= gamma*cx + x = axpy(ux, x, x.shape[0], gamma) # x += gamma*ux + + # Truncate CU + if truncate == 'oldest': + while len(CU) >= k and CU: + del CU[0] + elif truncate == 'smallest': + if len(CU) >= k and CU: + # cf. [1,2] + D = solve(R[:-1,:].T, B.T).T + W, sigma, V = svd(D) + + # C := C W[:,:k-1], U := U W[:,:k-1] + new_CU = [] + for j, w in enumerate(W[:,:k-1].T): + c, u = CU[0] + c = c * w[0] + u = u * w[0] + for cup, wp in zip(CU[1:], w[1:]): + cp, up = cup + c = axpy(cp, c, c.shape[0], wp) + u = axpy(up, u, u.shape[0], wp) + + # Reorthogonalize at the same time; not necessary + # in exact arithmetic, but floating point error + # tends to accumulate here + for cp, up in new_CU: + alpha = dot(cp, c) + c = axpy(cp, c, c.shape[0], -alpha) + u = axpy(up, u, u.shape[0], -alpha) + alpha = nrm2(c) + c = scal(1.0/alpha, c) + u = scal(1.0/alpha, u) + + new_CU.append((c, u)) + CU[:] = new_CU + + # Add new vector to CU + CU.append((cx, ux)) + + # Include the solution vector to the span + CU.append((None, x.copy())) + if discard_C: + CU[:] = [(None, uz) for cz, uz in CU] + + return postprocess(x), j_outer + 1 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/iterative.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/iterative.py new file mode 100644 index 0000000000000000000000000000000000000000..233c3404454ae184455aa09588125ff5eaa3861d --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/iterative.py @@ -0,0 +1,1079 @@ +import warnings +import numpy as np +from scipy.sparse.linalg._interface import LinearOperator +from .utils import make_system +from scipy.linalg import get_lapack_funcs +from scipy._lib.deprecation import _NoValue, _deprecate_positional_args + +__all__ = ['bicg', 'bicgstab', 'cg', 'cgs', 'gmres', 'qmr'] + + +def _get_atol_rtol(name, b_norm, tol=_NoValue, atol=0., rtol=1e-5): + """ + A helper function to handle tolerance deprecations and normalization + """ + if tol is not _NoValue: + msg = (f"'scipy.sparse.linalg.{name}' keyword argument `tol` is " + "deprecated in favor of `rtol` and will be removed in SciPy " + "v1.14.0. Until then, if set, it will override `rtol`.") + warnings.warn(msg, category=DeprecationWarning, stacklevel=4) + rtol = float(tol) if tol is not None else rtol + + if atol == 'legacy': + msg = (f"'scipy.sparse.linalg.{name}' called with `atol='legacy'`. " + "This behavior is deprecated and will result in an error in " + "SciPy v1.14.0. To preserve current behaviour, set `atol=0.0`.") + warnings.warn(msg, category=DeprecationWarning, stacklevel=4) + atol = 0 + + # this branch is only hit from gcrotmk/lgmres/tfqmr + if atol is None: + msg = (f"'scipy.sparse.linalg.{name}' called without specifying " + "`atol`. This behavior is deprecated and will result in an " + "error in SciPy v1.14.0. To preserve current behaviour, set " + "`atol=rtol`, or, to adopt the future default, set `atol=0.0`.") + warnings.warn(msg, category=DeprecationWarning, stacklevel=4) + atol = rtol + + atol = max(float(atol), float(rtol) * float(b_norm)) + + return atol, rtol + + +@_deprecate_positional_args(version="1.14") +def bicg(A, b, x0=None, *, tol=_NoValue, maxiter=None, M=None, callback=None, + atol=0., rtol=1e-5): + """Use BIConjugate Gradient iteration to solve ``Ax = b``. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real or complex N-by-N matrix of the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` and ``A^T x`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution. + rtol, atol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``atol=0.`` and ``rtol=1e-5``. + maxiter : integer + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M : {sparse matrix, ndarray, LinearOperator} + Preconditioner for A. The preconditioner should approximate the + inverse of A. Effective preconditioning dramatically improves the + rate of convergence, which implies that fewer iterations are needed + to reach a given error tolerance. + callback : function + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `bicg` keyword argument ``tol`` is deprecated in favor of ``rtol`` + and will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The converged solution. + info : integer + Provides convergence information: + 0 : successful exit + >0 : convergence to tolerance not achieved, number of iterations + <0 : parameter breakdown + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import bicg + >>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1.]]) + >>> b = np.array([2., 4., -1.]) + >>> x, exitCode = bicg(A, b, atol=1e-5) + >>> print(exitCode) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + + """ + A, M, x, b, postprocess = make_system(A, M, x0, b) + bnrm2 = np.linalg.norm(b) + + atol, _ = _get_atol_rtol('bicg', bnrm2, tol, atol, rtol) + + if bnrm2 == 0: + return postprocess(b), 0 + + n = len(b) + dotprod = np.vdot if np.iscomplexobj(x) else np.dot + + if maxiter is None: + maxiter = n*10 + + matvec, rmatvec = A.matvec, A.rmatvec + psolve, rpsolve = M.matvec, M.rmatvec + + rhotol = np.finfo(x.dtype.char).eps**2 + + # Dummy values to initialize vars, silence linter warnings + rho_prev, p, ptilde = None, None, None + + r = b - matvec(x) if x.any() else b.copy() + rtilde = r.copy() + + for iteration in range(maxiter): + if np.linalg.norm(r) < atol: # Are we done? + return postprocess(x), 0 + + z = psolve(r) + ztilde = rpsolve(rtilde) + # order matters in this dot product + rho_cur = dotprod(rtilde, z) + + if np.abs(rho_cur) < rhotol: # Breakdown case + return postprocess, -10 + + if iteration > 0: + beta = rho_cur / rho_prev + p *= beta + p += z + ptilde *= beta.conj() + ptilde += ztilde + else: # First spin + p = z.copy() + ptilde = ztilde.copy() + + q = matvec(p) + qtilde = rmatvec(ptilde) + rv = dotprod(ptilde, q) + + if rv == 0: + return postprocess(x), -11 + + alpha = rho_cur / rv + x += alpha*p + r -= alpha*q + rtilde -= alpha.conj()*qtilde + rho_prev = rho_cur + + if callback: + callback(x) + + else: # for loop exhausted + # Return incomplete progress + return postprocess(x), maxiter + + +@_deprecate_positional_args(version="1.14") +def bicgstab(A, b, *, x0=None, tol=_NoValue, maxiter=None, M=None, + callback=None, atol=0., rtol=1e-5): + """Use BIConjugate Gradient STABilized iteration to solve ``Ax = b``. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real or complex N-by-N matrix of the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` and ``A^T x`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution. + rtol, atol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``atol=0.`` and ``rtol=1e-5``. + maxiter : integer + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M : {sparse matrix, ndarray, LinearOperator} + Preconditioner for A. The preconditioner should approximate the + inverse of A. Effective preconditioning dramatically improves the + rate of convergence, which implies that fewer iterations are needed + to reach a given error tolerance. + callback : function + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `bicgstab` keyword argument ``tol`` is deprecated in favor of + ``rtol`` and will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The converged solution. + info : integer + Provides convergence information: + 0 : successful exit + >0 : convergence to tolerance not achieved, number of iterations + <0 : parameter breakdown + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import bicgstab + >>> R = np.array([[4, 2, 0, 1], + ... [3, 0, 0, 2], + ... [0, 1, 1, 1], + ... [0, 2, 1, 0]]) + >>> A = csc_matrix(R) + >>> b = np.array([-1, -0.5, -1, 2]) + >>> x, exit_code = bicgstab(A, b, atol=1e-5) + >>> print(exit_code) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + + """ + A, M, x, b, postprocess = make_system(A, M, x0, b) + bnrm2 = np.linalg.norm(b) + + atol, _ = _get_atol_rtol('bicgstab', bnrm2, tol, atol, rtol) + + if bnrm2 == 0: + return postprocess(b), 0 + + n = len(b) + + dotprod = np.vdot if np.iscomplexobj(x) else np.dot + + if maxiter is None: + maxiter = n*10 + + matvec = A.matvec + psolve = M.matvec + + # These values make no sense but coming from original Fortran code + # sqrt might have been meant instead. + rhotol = np.finfo(x.dtype.char).eps**2 + omegatol = rhotol + + # Dummy values to initialize vars, silence linter warnings + rho_prev, omega, alpha, p, v = None, None, None, None, None + + r = b - matvec(x) if x.any() else b.copy() + rtilde = r.copy() + + for iteration in range(maxiter): + if np.linalg.norm(r) < atol: # Are we done? + return postprocess(x), 0 + + rho = dotprod(rtilde, r) + if np.abs(rho) < rhotol: # rho breakdown + return postprocess(x), -10 + + if iteration > 0: + if np.abs(omega) < omegatol: # omega breakdown + return postprocess(x), -11 + + beta = (rho / rho_prev) * (alpha / omega) + p -= omega*v + p *= beta + p += r + else: # First spin + s = np.empty_like(r) + p = r.copy() + + phat = psolve(p) + v = matvec(phat) + rv = dotprod(rtilde, v) + if rv == 0: + return postprocess(x), -11 + alpha = rho / rv + r -= alpha*v + s[:] = r[:] + + if np.linalg.norm(s) < atol: + x += alpha*phat + return postprocess(x), 0 + + shat = psolve(s) + t = matvec(shat) + omega = dotprod(t, s) / dotprod(t, t) + x += alpha*phat + x += omega*shat + r -= omega*t + rho_prev = rho + + if callback: + callback(x) + + else: # for loop exhausted + # Return incomplete progress + return postprocess(x), maxiter + + +@_deprecate_positional_args(version="1.14") +def cg(A, b, x0=None, *, tol=_NoValue, maxiter=None, M=None, callback=None, + atol=0., rtol=1e-5): + """Use Conjugate Gradient iteration to solve ``Ax = b``. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real or complex N-by-N matrix of the linear system. + ``A`` must represent a hermitian, positive definite matrix. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution. + rtol, atol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``atol=0.`` and ``rtol=1e-5``. + maxiter : integer + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M : {sparse matrix, ndarray, LinearOperator} + Preconditioner for A. The preconditioner should approximate the + inverse of A. Effective preconditioning dramatically improves the + rate of convergence, which implies that fewer iterations are needed + to reach a given error tolerance. + callback : function + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `cg` keyword argument ``tol`` is deprecated in favor of ``rtol`` and + will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The converged solution. + info : integer + Provides convergence information: + 0 : successful exit + >0 : convergence to tolerance not achieved, number of iterations + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import cg + >>> P = np.array([[4, 0, 1, 0], + ... [0, 5, 0, 0], + ... [1, 0, 3, 2], + ... [0, 0, 2, 4]]) + >>> A = csc_matrix(P) + >>> b = np.array([-1, -0.5, -1, 2]) + >>> x, exit_code = cg(A, b, atol=1e-5) + >>> print(exit_code) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + + """ + A, M, x, b, postprocess = make_system(A, M, x0, b) + bnrm2 = np.linalg.norm(b) + + atol, _ = _get_atol_rtol('cg', bnrm2, tol, atol, rtol) + + if bnrm2 == 0: + return postprocess(b), 0 + + n = len(b) + + if maxiter is None: + maxiter = n*10 + + dotprod = np.vdot if np.iscomplexobj(x) else np.dot + + matvec = A.matvec + psolve = M.matvec + r = b - matvec(x) if x.any() else b.copy() + + # Dummy value to initialize var, silences warnings + rho_prev, p = None, None + + for iteration in range(maxiter): + if np.linalg.norm(r) < atol: # Are we done? + return postprocess(x), 0 + + z = psolve(r) + rho_cur = dotprod(r, z) + if iteration > 0: + beta = rho_cur / rho_prev + p *= beta + p += z + else: # First spin + p = np.empty_like(r) + p[:] = z[:] + + q = matvec(p) + alpha = rho_cur / dotprod(p, q) + x += alpha*p + r -= alpha*q + rho_prev = rho_cur + + if callback: + callback(x) + + else: # for loop exhausted + # Return incomplete progress + return postprocess(x), maxiter + + +@_deprecate_positional_args(version="1.14") +def cgs(A, b, x0=None, *, tol=_NoValue, maxiter=None, M=None, callback=None, + atol=0., rtol=1e-5): + """Use Conjugate Gradient Squared iteration to solve ``Ax = b``. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real-valued N-by-N matrix of the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution. + rtol, atol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``atol=0.`` and ``rtol=1e-5``. + maxiter : integer + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M : {sparse matrix, ndarray, LinearOperator} + Preconditioner for A. The preconditioner should approximate the + inverse of A. Effective preconditioning dramatically improves the + rate of convergence, which implies that fewer iterations are needed + to reach a given error tolerance. + callback : function + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `cgs` keyword argument ``tol`` is deprecated in favor of ``rtol`` + and will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The converged solution. + info : integer + Provides convergence information: + 0 : successful exit + >0 : convergence to tolerance not achieved, number of iterations + <0 : parameter breakdown + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import cgs + >>> R = np.array([[4, 2, 0, 1], + ... [3, 0, 0, 2], + ... [0, 1, 1, 1], + ... [0, 2, 1, 0]]) + >>> A = csc_matrix(R) + >>> b = np.array([-1, -0.5, -1, 2]) + >>> x, exit_code = cgs(A, b) + >>> print(exit_code) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + + """ + A, M, x, b, postprocess = make_system(A, M, x0, b) + bnrm2 = np.linalg.norm(b) + + atol, _ = _get_atol_rtol('cgs', bnrm2, tol, atol, rtol) + + if bnrm2 == 0: + return postprocess(b), 0 + + n = len(b) + + dotprod = np.vdot if np.iscomplexobj(x) else np.dot + + if maxiter is None: + maxiter = n*10 + + matvec = A.matvec + psolve = M.matvec + + rhotol = np.finfo(x.dtype.char).eps**2 + + r = b - matvec(x) if x.any() else b.copy() + + rtilde = r.copy() + bnorm = np.linalg.norm(b) + if bnorm == 0: + bnorm = 1 + + # Dummy values to initialize vars, silence linter warnings + rho_prev, p, u, q = None, None, None, None + + for iteration in range(maxiter): + rnorm = np.linalg.norm(r) + if rnorm < atol: # Are we done? + return postprocess(x), 0 + + rho_cur = dotprod(rtilde, r) + if np.abs(rho_cur) < rhotol: # Breakdown case + return postprocess, -10 + + if iteration > 0: + beta = rho_cur / rho_prev + + # u = r + beta * q + # p = u + beta * (q + beta * p); + u[:] = r[:] + u += beta*q + + p *= beta + p += q + p *= beta + p += u + + else: # First spin + p = r.copy() + u = r.copy() + q = np.empty_like(r) + + phat = psolve(p) + vhat = matvec(phat) + rv = dotprod(rtilde, vhat) + + if rv == 0: # Dot product breakdown + return postprocess(x), -11 + + alpha = rho_cur / rv + q[:] = u[:] + q -= alpha*vhat + uhat = psolve(u + q) + x += alpha*uhat + + # Due to numerical error build-up the actual residual is computed + # instead of the following two lines that were in the original + # FORTRAN templates, still using a single matvec. + + # qhat = matvec(uhat) + # r -= alpha*qhat + r = b - matvec(x) + + rho_prev = rho_cur + + if callback: + callback(x) + + else: # for loop exhausted + # Return incomplete progress + return postprocess(x), maxiter + + +@_deprecate_positional_args(version="1.14") +def gmres(A, b, x0=None, *, tol=_NoValue, restart=None, maxiter=None, M=None, + callback=None, restrt=_NoValue, atol=0., callback_type=None, + rtol=1e-5): + """ + Use Generalized Minimal RESidual iteration to solve ``Ax = b``. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real or complex N-by-N matrix of the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution (a vector of zeros by default). + atol, rtol : float + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``atol=0.`` and ``rtol=1e-5``. + restart : int, optional + Number of iterations between restarts. Larger values increase + iteration cost, but may be necessary for convergence. + If omitted, ``min(20, n)`` is used. + maxiter : int, optional + Maximum number of iterations (restart cycles). Iteration will stop + after maxiter steps even if the specified tolerance has not been + achieved. See `callback_type`. + M : {sparse matrix, ndarray, LinearOperator} + Inverse of the preconditioner of A. M should approximate the + inverse of A and be easy to solve for (see Notes). Effective + preconditioning dramatically improves the rate of convergence, + which implies that fewer iterations are needed to reach a given + error tolerance. By default, no preconditioner is used. + In this implementation, left preconditioning is used, + and the preconditioned residual is minimized. However, the final + convergence is tested with respect to the ``b - A @ x`` residual. + callback : function + User-supplied function to call after each iteration. It is called + as `callback(args)`, where `args` are selected by `callback_type`. + callback_type : {'x', 'pr_norm', 'legacy'}, optional + Callback function argument requested: + - ``x``: current iterate (ndarray), called on every restart + - ``pr_norm``: relative (preconditioned) residual norm (float), + called on every inner iteration + - ``legacy`` (default): same as ``pr_norm``, but also changes the + meaning of `maxiter` to count inner iterations instead of restart + cycles. + + This keyword has no effect if `callback` is not set. + restrt : int, optional, deprecated + + .. deprecated:: 0.11.0 + `gmres` keyword argument ``restrt`` is deprecated in favor of + ``restart`` and will be removed in SciPy 1.14.0. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `gmres` keyword argument ``tol`` is deprecated in favor of ``rtol`` + and will be removed in SciPy 1.14.0 + + Returns + ------- + x : ndarray + The converged solution. + info : int + Provides convergence information: + 0 : successful exit + >0 : convergence to tolerance not achieved, number of iterations + + See Also + -------- + LinearOperator + + Notes + ----- + A preconditioner, P, is chosen such that P is close to A but easy to solve + for. The preconditioner parameter required by this routine is + ``M = P^-1``. The inverse should preferably not be calculated + explicitly. Rather, use the following template to produce M:: + + # Construct a linear operator that computes P^-1 @ x. + import scipy.sparse.linalg as spla + M_x = lambda x: spla.spsolve(P, x) + M = spla.LinearOperator((n, n), M_x) + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import gmres + >>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float) + >>> b = np.array([2, 4, -1], dtype=float) + >>> x, exitCode = gmres(A, b, atol=1e-5) + >>> print(exitCode) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + """ + + # Handle the deprecation frenzy + if restrt not in (None, _NoValue) and restart: + raise ValueError("Cannot specify both 'restart' and 'restrt'" + " keywords. Also 'rstrt' is deprecated." + " and will be removed in SciPy 1.14.0. Use " + "'restart' instead.") + if restrt is not _NoValue: + msg = ("'gmres' keyword argument 'restrt' is deprecated " + "in favor of 'restart' and will be removed in SciPy" + " 1.14.0. Until then, if set, 'rstrt' will override 'restart'." + ) + warnings.warn(msg, DeprecationWarning, stacklevel=3) + restart = restrt + + if callback is not None and callback_type is None: + # Warn about 'callback_type' semantic changes. + # Probably should be removed only in far future, Scipy 2.0 or so. + msg = ("scipy.sparse.linalg.gmres called without specifying " + "`callback_type`. The default value will be changed in" + " a future release. For compatibility, specify a value " + "for `callback_type` explicitly, e.g., " + "``gmres(..., callback_type='pr_norm')``, or to retain the " + "old behavior ``gmres(..., callback_type='legacy')``" + ) + warnings.warn(msg, category=DeprecationWarning, stacklevel=3) + + if callback_type is None: + callback_type = 'legacy' + + if callback_type not in ('x', 'pr_norm', 'legacy'): + raise ValueError(f"Unknown callback_type: {callback_type!r}") + + if callback is None: + callback_type = None + + A, M, x, b, postprocess = make_system(A, M, x0, b) + matvec = A.matvec + psolve = M.matvec + n = len(b) + bnrm2 = np.linalg.norm(b) + + atol, _ = _get_atol_rtol('gmres', bnrm2, tol, atol, rtol) + + if bnrm2 == 0: + return postprocess(b), 0 + + eps = np.finfo(x.dtype.char).eps + + dotprod = np.vdot if np.iscomplexobj(x) else np.dot + + if maxiter is None: + maxiter = n*10 + + if restart is None: + restart = 20 + restart = min(restart, n) + + Mb_nrm2 = np.linalg.norm(psolve(b)) + + # ==================================================== + # =========== Tolerance control from gh-8400 ========= + # ==================================================== + # Tolerance passed to GMRESREVCOM applies to the inner + # iteration and deals with the left-preconditioned + # residual. + ptol_max_factor = 1. + ptol = Mb_nrm2 * min(ptol_max_factor, atol / bnrm2) + presid = 0. + # ==================================================== + lartg = get_lapack_funcs('lartg', dtype=x.dtype) + + # allocate internal variables + v = np.empty([restart+1, n], dtype=x.dtype) + h = np.zeros([restart, restart+1], dtype=x.dtype) + givens = np.zeros([restart, 2], dtype=x.dtype) + + # legacy iteration count + inner_iter = 0 + + for iteration in range(maxiter): + if iteration == 0: + r = b - matvec(x) if x.any() else b.copy() + if np.linalg.norm(r) < atol: # Are we done? + return postprocess(x), 0 + + v[0, :] = psolve(r) + tmp = np.linalg.norm(v[0, :]) + v[0, :] *= (1 / tmp) + # RHS of the Hessenberg problem + S = np.zeros(restart+1, dtype=x.dtype) + S[0] = tmp + + breakdown = False + for col in range(restart): + av = matvec(v[col, :]) + w = psolve(av) + + # Modified Gram-Schmidt + h0 = np.linalg.norm(w) + for k in range(col+1): + tmp = dotprod(v[k, :], w) + h[col, k] = tmp + w -= tmp*v[k, :] + + h1 = np.linalg.norm(w) + h[col, col + 1] = h1 + v[col + 1, :] = w[:] + + # Exact solution indicator + if h1 <= eps*h0: + h[col, col + 1] = 0 + breakdown = True + else: + v[col + 1, :] *= (1 / h1) + + # apply past Givens rotations to current h column + for k in range(col): + c, s = givens[k, 0], givens[k, 1] + n0, n1 = h[col, [k, k+1]] + h[col, [k, k + 1]] = [c*n0 + s*n1, -s.conj()*n0 + c*n1] + + # get and apply current rotation to h and S + c, s, mag = lartg(h[col, col], h[col, col+1]) + givens[col, :] = [c, s] + h[col, [col, col+1]] = mag, 0 + + # S[col+1] component is always 0 + tmp = -np.conjugate(s)*S[col] + S[[col, col + 1]] = [c*S[col], tmp] + presid = np.abs(tmp) + inner_iter += 1 + + if callback_type in ('legacy', 'pr_norm'): + callback(presid / bnrm2) + # Legacy behavior + if callback_type == 'legacy' and inner_iter == maxiter: + break + if presid <= ptol or breakdown: + break + + # Solve h(col, col) upper triangular system and allow pseudo-solve + # singular cases as in (but without the f2py copies): + # y = trsv(h[:col+1, :col+1].T, S[:col+1]) + + if h[col, col] == 0: + S[col] = 0 + + y = np.zeros([col+1], dtype=x.dtype) + y[:] = S[:col+1] + for k in range(col, 0, -1): + if y[k] != 0: + y[k] /= h[k, k] + tmp = y[k] + y[:k] -= tmp*h[k, :k] + if y[0] != 0: + y[0] /= h[0, 0] + + x += y @ v[:col+1, :] + + r = b - matvec(x) + rnorm = np.linalg.norm(r) + + # Legacy exit + if callback_type == 'legacy' and inner_iter == maxiter: + return postprocess(x), 0 if rnorm <= atol else maxiter + + if callback_type == 'x': + callback(x) + + if rnorm <= atol: + break + elif breakdown: + # Reached breakdown (= exact solution), but the external + # tolerance check failed. Bail out with failure. + break + elif presid <= ptol: + # Inner loop passed but outer didn't + ptol_max_factor = max(eps, 0.25 * ptol_max_factor) + else: + ptol_max_factor = min(1.0, 1.5 * ptol_max_factor) + + ptol = presid * min(ptol_max_factor, atol / rnorm) + + info = 0 if (rnorm <= atol) else maxiter + return postprocess(x), info + + +@_deprecate_positional_args(version="1.14") +def qmr(A, b, x0=None, *, tol=_NoValue, maxiter=None, M1=None, M2=None, + callback=None, atol=0., rtol=1e-5): + """Use Quasi-Minimal Residual iteration to solve ``Ax = b``. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real-valued N-by-N matrix of the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` and ``A^T x`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution. + atol, rtol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``atol=0.`` and ``rtol=1e-5``. + maxiter : integer + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M1 : {sparse matrix, ndarray, LinearOperator} + Left preconditioner for A. + M2 : {sparse matrix, ndarray, LinearOperator} + Right preconditioner for A. Used together with the left + preconditioner M1. The matrix M1@A@M2 should have better + conditioned than A alone. + callback : function + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `qmr` keyword argument ``tol`` is deprecated in favor of ``rtol`` + and will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The converged solution. + info : integer + Provides convergence information: + 0 : successful exit + >0 : convergence to tolerance not achieved, number of iterations + <0 : parameter breakdown + + See Also + -------- + LinearOperator + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import qmr + >>> A = csc_matrix([[3., 2., 0.], [1., -1., 0.], [0., 5., 1.]]) + >>> b = np.array([2., 4., -1.]) + >>> x, exitCode = qmr(A, b, atol=1e-5) + >>> print(exitCode) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + """ + A_ = A + A, M, x, b, postprocess = make_system(A, None, x0, b) + bnrm2 = np.linalg.norm(b) + + atol, _ = _get_atol_rtol('qmr', bnrm2, tol, atol, rtol) + + if bnrm2 == 0: + return postprocess(b), 0 + + if M1 is None and M2 is None: + if hasattr(A_, 'psolve'): + def left_psolve(b): + return A_.psolve(b, 'left') + + def right_psolve(b): + return A_.psolve(b, 'right') + + def left_rpsolve(b): + return A_.rpsolve(b, 'left') + + def right_rpsolve(b): + return A_.rpsolve(b, 'right') + M1 = LinearOperator(A.shape, + matvec=left_psolve, + rmatvec=left_rpsolve) + M2 = LinearOperator(A.shape, + matvec=right_psolve, + rmatvec=right_rpsolve) + else: + def id(b): + return b + M1 = LinearOperator(A.shape, matvec=id, rmatvec=id) + M2 = LinearOperator(A.shape, matvec=id, rmatvec=id) + + n = len(b) + if maxiter is None: + maxiter = n*10 + + dotprod = np.vdot if np.iscomplexobj(x) else np.dot + + rhotol = np.finfo(x.dtype.char).eps + betatol = rhotol + gammatol = rhotol + deltatol = rhotol + epsilontol = rhotol + xitol = rhotol + + r = b - A.matvec(x) if x.any() else b.copy() + + vtilde = r.copy() + y = M1.matvec(vtilde) + rho = np.linalg.norm(y) + wtilde = r.copy() + z = M2.rmatvec(wtilde) + xi = np.linalg.norm(z) + gamma, eta, theta = 1, -1, 0 + v = np.empty_like(vtilde) + w = np.empty_like(wtilde) + + # Dummy values to initialize vars, silence linter warnings + epsilon, q, d, p, s = None, None, None, None, None + + for iteration in range(maxiter): + if np.linalg.norm(r) < atol: # Are we done? + return postprocess(x), 0 + if np.abs(rho) < rhotol: # rho breakdown + return postprocess(x), -10 + if np.abs(xi) < xitol: # xi breakdown + return postprocess(x), -15 + + v[:] = vtilde[:] + v *= (1 / rho) + y *= (1 / rho) + w[:] = wtilde[:] + w *= (1 / xi) + z *= (1 / xi) + delta = dotprod(z, y) + + if np.abs(delta) < deltatol: # delta breakdown + return postprocess(x), -13 + + ytilde = M2.matvec(y) + ztilde = M1.rmatvec(z) + + if iteration > 0: + ytilde -= (xi * delta / epsilon) * p + p[:] = ytilde[:] + ztilde -= (rho * (delta / epsilon).conj()) * q + q[:] = ztilde[:] + else: # First spin + p = ytilde.copy() + q = ztilde.copy() + + ptilde = A.matvec(p) + epsilon = dotprod(q, ptilde) + if np.abs(epsilon) < epsilontol: # epsilon breakdown + return postprocess(x), -14 + + beta = epsilon / delta + if np.abs(beta) < betatol: # beta breakdown + return postprocess(x), -11 + + vtilde[:] = ptilde[:] + vtilde -= beta*v + y = M1.matvec(vtilde) + + rho_prev = rho + rho = np.linalg.norm(y) + wtilde[:] = w[:] + wtilde *= - beta.conj() + wtilde += A.rmatvec(q) + z = M2.rmatvec(wtilde) + xi = np.linalg.norm(z) + gamma_prev = gamma + theta_prev = theta + theta = rho / (gamma_prev * np.abs(beta)) + gamma = 1 / np.sqrt(1 + theta**2) + + if np.abs(gamma) < gammatol: # gamma breakdown + return postprocess(x), -12 + + eta *= -(rho_prev / beta) * (gamma / gamma_prev)**2 + + if iteration > 0: + d *= (theta_prev * gamma) ** 2 + d += eta*p + s *= (theta_prev * gamma) ** 2 + s += eta*ptilde + else: + d = p.copy() + d *= eta + s = ptilde.copy() + s *= eta + + x += d + r -= s + + if callback: + callback(x) + + else: # for loop exhausted + # Return incomplete progress + return postprocess(x), maxiter diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lgmres.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lgmres.py new file mode 100644 index 0000000000000000000000000000000000000000..14771d8bf10e6a77c65430ba1be944df113519b2 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lgmres.py @@ -0,0 +1,242 @@ +# Copyright (C) 2009, Pauli Virtanen +# Distributed under the same license as SciPy. + +import numpy as np +from numpy.linalg import LinAlgError +from scipy.linalg import get_blas_funcs +from .iterative import _get_atol_rtol +from .utils import make_system +from scipy._lib.deprecation import _NoValue, _deprecate_positional_args + +from ._gcrotmk import _fgmres + +__all__ = ['lgmres'] + + +@_deprecate_positional_args(version="1.14.0") +def lgmres(A, b, x0=None, *, tol=_NoValue, maxiter=1000, M=None, callback=None, + inner_m=30, outer_k=3, outer_v=None, store_outer_Av=True, + prepend_outer_v=False, atol=None, rtol=1e-5): + """ + Solve a matrix equation using the LGMRES algorithm. + + The LGMRES algorithm [1]_ [2]_ is designed to avoid some problems + in the convergence in restarted GMRES, and often converges in fewer + iterations. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real or complex N-by-N matrix of the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : ndarray + Starting guess for the solution. + rtol, atol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``rtol=1e-5``, the default for ``atol`` is ``rtol``. + + .. warning:: + + The default value for ``atol`` will be changed to ``0.0`` in + SciPy 1.14.0. + maxiter : int, optional + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M : {sparse matrix, ndarray, LinearOperator}, optional + Preconditioner for A. The preconditioner should approximate the + inverse of A. Effective preconditioning dramatically improves the + rate of convergence, which implies that fewer iterations are needed + to reach a given error tolerance. + callback : function, optional + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + inner_m : int, optional + Number of inner GMRES iterations per each outer iteration. + outer_k : int, optional + Number of vectors to carry between inner GMRES iterations. + According to [1]_, good values are in the range of 1...3. + However, note that if you want to use the additional vectors to + accelerate solving multiple similar problems, larger values may + be beneficial. + outer_v : list of tuples, optional + List containing tuples ``(v, Av)`` of vectors and corresponding + matrix-vector products, used to augment the Krylov subspace, and + carried between inner GMRES iterations. The element ``Av`` can + be `None` if the matrix-vector product should be re-evaluated. + This parameter is modified in-place by `lgmres`, and can be used + to pass "guess" vectors in and out of the algorithm when solving + similar problems. + store_outer_Av : bool, optional + Whether LGMRES should store also A@v in addition to vectors `v` + in the `outer_v` list. Default is True. + prepend_outer_v : bool, optional + Whether to put outer_v augmentation vectors before Krylov iterates. + In standard LGMRES, prepend_outer_v=False. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `lgmres` keyword argument ``tol`` is deprecated in favor of ``rtol`` + and will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The converged solution. + info : int + Provides convergence information: + + - 0 : successful exit + - >0 : convergence to tolerance not achieved, number of iterations + - <0 : illegal input or breakdown + + Notes + ----- + The LGMRES algorithm [1]_ [2]_ is designed to avoid the + slowing of convergence in restarted GMRES, due to alternating + residual vectors. Typically, it often outperforms GMRES(m) of + comparable memory requirements by some measure, or at least is not + much worse. + + Another advantage in this algorithm is that you can supply it with + 'guess' vectors in the `outer_v` argument that augment the Krylov + subspace. If the solution lies close to the span of these vectors, + the algorithm converges faster. This can be useful if several very + similar matrices need to be inverted one after another, such as in + Newton-Krylov iteration where the Jacobian matrix often changes + little in the nonlinear steps. + + References + ---------- + .. [1] A.H. Baker and E.R. Jessup and T. Manteuffel, "A Technique for + Accelerating the Convergence of Restarted GMRES", SIAM J. Matrix + Anal. Appl. 26, 962 (2005). + .. [2] A.H. Baker, "On Improving the Performance of the Linear Solver + restarted GMRES", PhD thesis, University of Colorado (2003). + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import lgmres + >>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float) + >>> b = np.array([2, 4, -1], dtype=float) + >>> x, exitCode = lgmres(A, b, atol=1e-5) + >>> print(exitCode) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + """ + A,M,x,b,postprocess = make_system(A,M,x0,b) + + if not np.isfinite(b).all(): + raise ValueError("RHS must contain only finite numbers") + + matvec = A.matvec + psolve = M.matvec + + if outer_v is None: + outer_v = [] + + axpy, dot, scal = None, None, None + nrm2 = get_blas_funcs('nrm2', [b]) + + b_norm = nrm2(b) + + # we call this to get the right atol/rtol and raise warnings as necessary + atol, rtol = _get_atol_rtol('lgmres', b_norm, tol, atol, rtol) + + if b_norm == 0: + x = b + return (postprocess(x), 0) + + ptol_max_factor = 1.0 + + for k_outer in range(maxiter): + r_outer = matvec(x) - b + + # -- callback + if callback is not None: + callback(x) + + # -- determine input type routines + if axpy is None: + if np.iscomplexobj(r_outer) and not np.iscomplexobj(x): + x = x.astype(r_outer.dtype) + axpy, dot, scal, nrm2 = get_blas_funcs(['axpy', 'dot', 'scal', 'nrm2'], + (x, r_outer)) + + # -- check stopping condition + r_norm = nrm2(r_outer) + if r_norm <= max(atol, rtol * b_norm): + break + + # -- inner LGMRES iteration + v0 = -psolve(r_outer) + inner_res_0 = nrm2(v0) + + if inner_res_0 == 0: + rnorm = nrm2(r_outer) + raise RuntimeError("Preconditioner returned a zero vector; " + "|v| ~ %.1g, |M v| = 0" % rnorm) + + v0 = scal(1.0/inner_res_0, v0) + + ptol = min(ptol_max_factor, max(atol, rtol*b_norm)/r_norm) + + try: + Q, R, B, vs, zs, y, pres = _fgmres(matvec, + v0, + inner_m, + lpsolve=psolve, + atol=ptol, + outer_v=outer_v, + prepend_outer_v=prepend_outer_v) + y *= inner_res_0 + if not np.isfinite(y).all(): + # Overflow etc. in computation. There's no way to + # recover from this, so we have to bail out. + raise LinAlgError() + except LinAlgError: + # Floating point over/underflow, non-finite result from + # matmul etc. -- report failure. + return postprocess(x), k_outer + 1 + + # Inner loop tolerance control + if pres > ptol: + ptol_max_factor = min(1.0, 1.5 * ptol_max_factor) + else: + ptol_max_factor = max(1e-16, 0.25 * ptol_max_factor) + + # -- GMRES terminated: eval solution + dx = zs[0]*y[0] + for w, yc in zip(zs[1:], y[1:]): + dx = axpy(w, dx, dx.shape[0], yc) # dx += w*yc + + # -- Store LGMRES augmentation vectors + nx = nrm2(dx) + if nx > 0: + if store_outer_Av: + q = Q.dot(R.dot(y)) + ax = vs[0]*q[0] + for v, qc in zip(vs[1:], q[1:]): + ax = axpy(v, ax, ax.shape[0], qc) + outer_v.append((dx/nx, ax/nx)) + else: + outer_v.append((dx/nx, None)) + + # -- Retain only a finite number of augmentation vectors + while len(outer_v) > outer_k: + del outer_v[0] + + # -- Apply step + x += dx + else: + # didn't converge ... + return postprocess(x), maxiter + + return postprocess(x), 0 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lsmr.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lsmr.py new file mode 100644 index 0000000000000000000000000000000000000000..eae11203a2657cdde8df8213506d54a00d66ea01 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lsmr.py @@ -0,0 +1,486 @@ +""" +Copyright (C) 2010 David Fong and Michael Saunders + +LSMR uses an iterative method. + +07 Jun 2010: Documentation updated +03 Jun 2010: First release version in Python + +David Chin-lung Fong clfong@stanford.edu +Institute for Computational and Mathematical Engineering +Stanford University + +Michael Saunders saunders@stanford.edu +Systems Optimization Laboratory +Dept of MS&E, Stanford University. + +""" + +__all__ = ['lsmr'] + +from numpy import zeros, inf, atleast_1d, result_type +from numpy.linalg import norm +from math import sqrt +from scipy.sparse.linalg._interface import aslinearoperator + +from scipy.sparse.linalg._isolve.lsqr import _sym_ortho + + +def lsmr(A, b, damp=0.0, atol=1e-6, btol=1e-6, conlim=1e8, + maxiter=None, show=False, x0=None): + """Iterative solver for least-squares problems. + + lsmr solves the system of linear equations ``Ax = b``. If the system + is inconsistent, it solves the least-squares problem ``min ||b - Ax||_2``. + ``A`` is a rectangular matrix of dimension m-by-n, where all cases are + allowed: m = n, m > n, or m < n. ``b`` is a vector of length m. + The matrix A may be dense or sparse (usually sparse). + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + Matrix A in the linear system. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` and ``A^H x`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : array_like, shape (m,) + Vector ``b`` in the linear system. + damp : float + Damping factor for regularized least-squares. `lsmr` solves + the regularized least-squares problem:: + + min ||(b) - ( A )x|| + ||(0) (damp*I) ||_2 + + where damp is a scalar. If damp is None or 0, the system + is solved without regularization. Default is 0. + atol, btol : float, optional + Stopping tolerances. `lsmr` continues iterations until a + certain backward error estimate is smaller than some quantity + depending on atol and btol. Let ``r = b - Ax`` be the + residual vector for the current approximate solution ``x``. + If ``Ax = b`` seems to be consistent, `lsmr` terminates + when ``norm(r) <= atol * norm(A) * norm(x) + btol * norm(b)``. + Otherwise, `lsmr` terminates when ``norm(A^H r) <= + atol * norm(A) * norm(r)``. If both tolerances are 1.0e-6 (default), + the final ``norm(r)`` should be accurate to about 6 + digits. (The final ``x`` will usually have fewer correct digits, + depending on ``cond(A)`` and the size of LAMBDA.) If `atol` + or `btol` is None, a default value of 1.0e-6 will be used. + Ideally, they should be estimates of the relative error in the + entries of ``A`` and ``b`` respectively. For example, if the entries + of ``A`` have 7 correct digits, set ``atol = 1e-7``. This prevents + the algorithm from doing unnecessary work beyond the + uncertainty of the input data. + conlim : float, optional + `lsmr` terminates if an estimate of ``cond(A)`` exceeds + `conlim`. For compatible systems ``Ax = b``, conlim could be + as large as 1.0e+12 (say). For least-squares problems, + `conlim` should be less than 1.0e+8. If `conlim` is None, the + default value is 1e+8. Maximum precision can be obtained by + setting ``atol = btol = conlim = 0``, but the number of + iterations may then be excessive. Default is 1e8. + maxiter : int, optional + `lsmr` terminates if the number of iterations reaches + `maxiter`. The default is ``maxiter = min(m, n)``. For + ill-conditioned systems, a larger value of `maxiter` may be + needed. Default is False. + show : bool, optional + Print iterations logs if ``show=True``. Default is False. + x0 : array_like, shape (n,), optional + Initial guess of ``x``, if None zeros are used. Default is None. + + .. versionadded:: 1.0.0 + + Returns + ------- + x : ndarray of float + Least-square solution returned. + istop : int + istop gives the reason for stopping:: + + istop = 0 means x=0 is a solution. If x0 was given, then x=x0 is a + solution. + = 1 means x is an approximate solution to A@x = B, + according to atol and btol. + = 2 means x approximately solves the least-squares problem + according to atol. + = 3 means COND(A) seems to be greater than CONLIM. + = 4 is the same as 1 with atol = btol = eps (machine + precision) + = 5 is the same as 2 with atol = eps. + = 6 is the same as 3 with CONLIM = 1/eps. + = 7 means ITN reached maxiter before the other stopping + conditions were satisfied. + + itn : int + Number of iterations used. + normr : float + ``norm(b-Ax)`` + normar : float + ``norm(A^H (b - Ax))`` + norma : float + ``norm(A)`` + conda : float + Condition number of A. + normx : float + ``norm(x)`` + + Notes + ----- + + .. versionadded:: 0.11.0 + + References + ---------- + .. [1] D. C.-L. Fong and M. A. Saunders, + "LSMR: An iterative algorithm for sparse least-squares problems", + SIAM J. Sci. Comput., vol. 33, pp. 2950-2971, 2011. + :arxiv:`1006.0758` + .. [2] LSMR Software, https://web.stanford.edu/group/SOL/software/lsmr/ + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import lsmr + >>> A = csc_matrix([[1., 0.], [1., 1.], [0., 1.]], dtype=float) + + The first example has the trivial solution ``[0, 0]`` + + >>> b = np.array([0., 0., 0.], dtype=float) + >>> x, istop, itn, normr = lsmr(A, b)[:4] + >>> istop + 0 + >>> x + array([0., 0.]) + + The stopping code `istop=0` returned indicates that a vector of zeros was + found as a solution. The returned solution `x` indeed contains + ``[0., 0.]``. The next example has a non-trivial solution: + + >>> b = np.array([1., 0., -1.], dtype=float) + >>> x, istop, itn, normr = lsmr(A, b)[:4] + >>> istop + 1 + >>> x + array([ 1., -1.]) + >>> itn + 1 + >>> normr + 4.440892098500627e-16 + + As indicated by `istop=1`, `lsmr` found a solution obeying the tolerance + limits. The given solution ``[1., -1.]`` obviously solves the equation. The + remaining return values include information about the number of iterations + (`itn=1`) and the remaining difference of left and right side of the solved + equation. + The final example demonstrates the behavior in the case where there is no + solution for the equation: + + >>> b = np.array([1., 0.01, -1.], dtype=float) + >>> x, istop, itn, normr = lsmr(A, b)[:4] + >>> istop + 2 + >>> x + array([ 1.00333333, -0.99666667]) + >>> A.dot(x)-b + array([ 0.00333333, -0.00333333, 0.00333333]) + >>> normr + 0.005773502691896255 + + `istop` indicates that the system is inconsistent and thus `x` is rather an + approximate solution to the corresponding least-squares problem. `normr` + contains the minimal distance that was found. + """ + + A = aslinearoperator(A) + b = atleast_1d(b) + if b.ndim > 1: + b = b.squeeze() + + msg = ('The exact solution is x = 0, or x = x0, if x0 was given ', + 'Ax - b is small enough, given atol, btol ', + 'The least-squares solution is good enough, given atol ', + 'The estimate of cond(Abar) has exceeded conlim ', + 'Ax - b is small enough for this machine ', + 'The least-squares solution is good enough for this machine', + 'Cond(Abar) seems to be too large for this machine ', + 'The iteration limit has been reached ') + + hdg1 = ' itn x(1) norm r norm Ar' + hdg2 = ' compatible LS norm A cond A' + pfreq = 20 # print frequency (for repeating the heading) + pcount = 0 # print counter + + m, n = A.shape + + # stores the num of singular values + minDim = min([m, n]) + + if maxiter is None: + maxiter = minDim + + if x0 is None: + dtype = result_type(A, b, float) + else: + dtype = result_type(A, b, x0, float) + + if show: + print(' ') + print('LSMR Least-squares solution of Ax = b\n') + print(f'The matrix A has {m} rows and {n} columns') + print('damp = %20.14e\n' % (damp)) + print(f'atol = {atol:8.2e} conlim = {conlim:8.2e}\n') + print(f'btol = {btol:8.2e} maxiter = {maxiter:8g}\n') + + u = b + normb = norm(b) + if x0 is None: + x = zeros(n, dtype) + beta = normb.copy() + else: + x = atleast_1d(x0.copy()) + u = u - A.matvec(x) + beta = norm(u) + + if beta > 0: + u = (1 / beta) * u + v = A.rmatvec(u) + alpha = norm(v) + else: + v = zeros(n, dtype) + alpha = 0 + + if alpha > 0: + v = (1 / alpha) * v + + # Initialize variables for 1st iteration. + + itn = 0 + zetabar = alpha * beta + alphabar = alpha + rho = 1 + rhobar = 1 + cbar = 1 + sbar = 0 + + h = v.copy() + hbar = zeros(n, dtype) + + # Initialize variables for estimation of ||r||. + + betadd = beta + betad = 0 + rhodold = 1 + tautildeold = 0 + thetatilde = 0 + zeta = 0 + d = 0 + + # Initialize variables for estimation of ||A|| and cond(A) + + normA2 = alpha * alpha + maxrbar = 0 + minrbar = 1e+100 + normA = sqrt(normA2) + condA = 1 + normx = 0 + + # Items for use in stopping rules, normb set earlier + istop = 0 + ctol = 0 + if conlim > 0: + ctol = 1 / conlim + normr = beta + + # Reverse the order here from the original matlab code because + # there was an error on return when arnorm==0 + normar = alpha * beta + if normar == 0: + if show: + print(msg[0]) + return x, istop, itn, normr, normar, normA, condA, normx + + if normb == 0: + x[()] = 0 + return x, istop, itn, normr, normar, normA, condA, normx + + if show: + print(' ') + print(hdg1, hdg2) + test1 = 1 + test2 = alpha / beta + str1 = f'{itn:6g} {x[0]:12.5e}' + str2 = f' {normr:10.3e} {normar:10.3e}' + str3 = f' {test1:8.1e} {test2:8.1e}' + print(''.join([str1, str2, str3])) + + # Main iteration loop. + while itn < maxiter: + itn = itn + 1 + + # Perform the next step of the bidiagonalization to obtain the + # next beta, u, alpha, v. These satisfy the relations + # beta*u = A@v - alpha*u, + # alpha*v = A'@u - beta*v. + + u *= -alpha + u += A.matvec(v) + beta = norm(u) + + if beta > 0: + u *= (1 / beta) + v *= -beta + v += A.rmatvec(u) + alpha = norm(v) + if alpha > 0: + v *= (1 / alpha) + + # At this point, beta = beta_{k+1}, alpha = alpha_{k+1}. + + # Construct rotation Qhat_{k,2k+1}. + + chat, shat, alphahat = _sym_ortho(alphabar, damp) + + # Use a plane rotation (Q_i) to turn B_i to R_i + + rhoold = rho + c, s, rho = _sym_ortho(alphahat, beta) + thetanew = s*alpha + alphabar = c*alpha + + # Use a plane rotation (Qbar_i) to turn R_i^T to R_i^bar + + rhobarold = rhobar + zetaold = zeta + thetabar = sbar * rho + rhotemp = cbar * rho + cbar, sbar, rhobar = _sym_ortho(cbar * rho, thetanew) + zeta = cbar * zetabar + zetabar = - sbar * zetabar + + # Update h, h_hat, x. + + hbar *= - (thetabar * rho / (rhoold * rhobarold)) + hbar += h + x += (zeta / (rho * rhobar)) * hbar + h *= - (thetanew / rho) + h += v + + # Estimate of ||r||. + + # Apply rotation Qhat_{k,2k+1}. + betaacute = chat * betadd + betacheck = -shat * betadd + + # Apply rotation Q_{k,k+1}. + betahat = c * betaacute + betadd = -s * betaacute + + # Apply rotation Qtilde_{k-1}. + # betad = betad_{k-1} here. + + thetatildeold = thetatilde + ctildeold, stildeold, rhotildeold = _sym_ortho(rhodold, thetabar) + thetatilde = stildeold * rhobar + rhodold = ctildeold * rhobar + betad = - stildeold * betad + ctildeold * betahat + + # betad = betad_k here. + # rhodold = rhod_k here. + + tautildeold = (zetaold - thetatildeold * tautildeold) / rhotildeold + taud = (zeta - thetatilde * tautildeold) / rhodold + d = d + betacheck * betacheck + normr = sqrt(d + (betad - taud)**2 + betadd * betadd) + + # Estimate ||A||. + normA2 = normA2 + beta * beta + normA = sqrt(normA2) + normA2 = normA2 + alpha * alpha + + # Estimate cond(A). + maxrbar = max(maxrbar, rhobarold) + if itn > 1: + minrbar = min(minrbar, rhobarold) + condA = max(maxrbar, rhotemp) / min(minrbar, rhotemp) + + # Test for convergence. + + # Compute norms for convergence testing. + normar = abs(zetabar) + normx = norm(x) + + # Now use these norms to estimate certain other quantities, + # some of which will be small near a solution. + + test1 = normr / normb + if (normA * normr) != 0: + test2 = normar / (normA * normr) + else: + test2 = inf + test3 = 1 / condA + t1 = test1 / (1 + normA * normx / normb) + rtol = btol + atol * normA * normx / normb + + # The following tests guard against extremely small values of + # atol, btol or ctol. (The user may have set any or all of + # the parameters atol, btol, conlim to 0.) + # The effect is equivalent to the normAl tests using + # atol = eps, btol = eps, conlim = 1/eps. + + if itn >= maxiter: + istop = 7 + if 1 + test3 <= 1: + istop = 6 + if 1 + test2 <= 1: + istop = 5 + if 1 + t1 <= 1: + istop = 4 + + # Allow for tolerances set by the user. + + if test3 <= ctol: + istop = 3 + if test2 <= atol: + istop = 2 + if test1 <= rtol: + istop = 1 + + # See if it is time to print something. + + if show: + if (n <= 40) or (itn <= 10) or (itn >= maxiter - 10) or \ + (itn % 10 == 0) or (test3 <= 1.1 * ctol) or \ + (test2 <= 1.1 * atol) or (test1 <= 1.1 * rtol) or \ + (istop != 0): + + if pcount >= pfreq: + pcount = 0 + print(' ') + print(hdg1, hdg2) + pcount = pcount + 1 + str1 = f'{itn:6g} {x[0]:12.5e}' + str2 = f' {normr:10.3e} {normar:10.3e}' + str3 = f' {test1:8.1e} {test2:8.1e}' + str4 = f' {normA:8.1e} {condA:8.1e}' + print(''.join([str1, str2, str3, str4])) + + if istop > 0: + break + + # Print the stopping condition. + + if show: + print(' ') + print('LSMR finished') + print(msg[istop]) + print(f'istop ={istop:8g} normr ={normr:8.1e}') + print(f' normA ={normA:8.1e} normAr ={normar:8.1e}') + print(f'itn ={itn:8g} condA ={condA:8.1e}') + print(' normx =%8.1e' % (normx)) + print(str1, str2) + print(str3, str4) + + return x, istop, itn, normr, normar, normA, condA, normx diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lsqr.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lsqr.py new file mode 100644 index 0000000000000000000000000000000000000000..11b5eaee8c108d9e48dd472f29664bd36449d699 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/lsqr.py @@ -0,0 +1,587 @@ +"""Sparse Equations and Least Squares. + +The original Fortran code was written by C. C. Paige and M. A. Saunders as +described in + +C. C. Paige and M. A. Saunders, LSQR: An algorithm for sparse linear +equations and sparse least squares, TOMS 8(1), 43--71 (1982). + +C. C. Paige and M. A. Saunders, Algorithm 583; LSQR: Sparse linear +equations and least-squares problems, TOMS 8(2), 195--209 (1982). + +It is licensed under the following BSD license: + +Copyright (c) 2006, Systems Optimization Laboratory +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + + * Neither the name of Stanford University nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The Fortran code was translated to Python for use in CVXOPT by Jeffery +Kline with contributions by Mridul Aanjaneya and Bob Myhill. + +Adapted for SciPy by Stefan van der Walt. + +""" + +__all__ = ['lsqr'] + +import numpy as np +from math import sqrt +from scipy.sparse.linalg._interface import aslinearoperator + +eps = np.finfo(np.float64).eps + + +def _sym_ortho(a, b): + """ + Stable implementation of Givens rotation. + + Notes + ----- + The routine 'SymOrtho' was added for numerical stability. This is + recommended by S.-C. Choi in [1]_. It removes the unpleasant potential of + ``1/eps`` in some important places (see, for example text following + "Compute the next plane rotation Qk" in minres.py). + + References + ---------- + .. [1] S.-C. Choi, "Iterative Methods for Singular Linear Equations + and Least-Squares Problems", Dissertation, + http://www.stanford.edu/group/SOL/dissertations/sou-cheng-choi-thesis.pdf + + """ + if b == 0: + return np.sign(a), 0, abs(a) + elif a == 0: + return 0, np.sign(b), abs(b) + elif abs(b) > abs(a): + tau = a / b + s = np.sign(b) / sqrt(1 + tau * tau) + c = s * tau + r = b / s + else: + tau = b / a + c = np.sign(a) / sqrt(1+tau*tau) + s = c * tau + r = a / c + return c, s, r + + +def lsqr(A, b, damp=0.0, atol=1e-6, btol=1e-6, conlim=1e8, + iter_lim=None, show=False, calc_var=False, x0=None): + """Find the least-squares solution to a large, sparse, linear system + of equations. + + The function solves ``Ax = b`` or ``min ||Ax - b||^2`` or + ``min ||Ax - b||^2 + d^2 ||x - x0||^2``. + + The matrix A may be square or rectangular (over-determined or + under-determined), and may have any rank. + + :: + + 1. Unsymmetric equations -- solve Ax = b + + 2. Linear least squares -- solve Ax = b + in the least-squares sense + + 3. Damped least squares -- solve ( A )*x = ( b ) + ( damp*I ) ( damp*x0 ) + in the least-squares sense + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + Representation of an m-by-n matrix. + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` and ``A^T x`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : array_like, shape (m,) + Right-hand side vector ``b``. + damp : float + Damping coefficient. Default is 0. + atol, btol : float, optional + Stopping tolerances. `lsqr` continues iterations until a + certain backward error estimate is smaller than some quantity + depending on atol and btol. Let ``r = b - Ax`` be the + residual vector for the current approximate solution ``x``. + If ``Ax = b`` seems to be consistent, `lsqr` terminates + when ``norm(r) <= atol * norm(A) * norm(x) + btol * norm(b)``. + Otherwise, `lsqr` terminates when ``norm(A^H r) <= + atol * norm(A) * norm(r)``. If both tolerances are 1.0e-6 (default), + the final ``norm(r)`` should be accurate to about 6 + digits. (The final ``x`` will usually have fewer correct digits, + depending on ``cond(A)`` and the size of LAMBDA.) If `atol` + or `btol` is None, a default value of 1.0e-6 will be used. + Ideally, they should be estimates of the relative error in the + entries of ``A`` and ``b`` respectively. For example, if the entries + of ``A`` have 7 correct digits, set ``atol = 1e-7``. This prevents + the algorithm from doing unnecessary work beyond the + uncertainty of the input data. + conlim : float, optional + Another stopping tolerance. lsqr terminates if an estimate of + ``cond(A)`` exceeds `conlim`. For compatible systems ``Ax = + b``, `conlim` could be as large as 1.0e+12 (say). For + least-squares problems, conlim should be less than 1.0e+8. + Maximum precision can be obtained by setting ``atol = btol = + conlim = zero``, but the number of iterations may then be + excessive. Default is 1e8. + iter_lim : int, optional + Explicit limitation on number of iterations (for safety). + show : bool, optional + Display an iteration log. Default is False. + calc_var : bool, optional + Whether to estimate diagonals of ``(A'A + damp^2*I)^{-1}``. + x0 : array_like, shape (n,), optional + Initial guess of x, if None zeros are used. Default is None. + + .. versionadded:: 1.0.0 + + Returns + ------- + x : ndarray of float + The final solution. + istop : int + Gives the reason for termination. + 1 means x is an approximate solution to Ax = b. + 2 means x approximately solves the least-squares problem. + itn : int + Iteration number upon termination. + r1norm : float + ``norm(r)``, where ``r = b - Ax``. + r2norm : float + ``sqrt( norm(r)^2 + damp^2 * norm(x - x0)^2 )``. Equal to `r1norm` + if ``damp == 0``. + anorm : float + Estimate of Frobenius norm of ``Abar = [[A]; [damp*I]]``. + acond : float + Estimate of ``cond(Abar)``. + arnorm : float + Estimate of ``norm(A'@r - damp^2*(x - x0))``. + xnorm : float + ``norm(x)`` + var : ndarray of float + If ``calc_var`` is True, estimates all diagonals of + ``(A'A)^{-1}`` (if ``damp == 0``) or more generally ``(A'A + + damp^2*I)^{-1}``. This is well defined if A has full column + rank or ``damp > 0``. (Not sure what var means if ``rank(A) + < n`` and ``damp = 0.``) + + Notes + ----- + LSQR uses an iterative method to approximate the solution. The + number of iterations required to reach a certain accuracy depends + strongly on the scaling of the problem. Poor scaling of the rows + or columns of A should therefore be avoided where possible. + + For example, in problem 1 the solution is unaltered by + row-scaling. If a row of A is very small or large compared to + the other rows of A, the corresponding row of ( A b ) should be + scaled up or down. + + In problems 1 and 2, the solution x is easily recovered + following column-scaling. Unless better information is known, + the nonzero columns of A should be scaled so that they all have + the same Euclidean norm (e.g., 1.0). + + In problem 3, there is no freedom to re-scale if damp is + nonzero. However, the value of damp should be assigned only + after attention has been paid to the scaling of A. + + The parameter damp is intended to help regularize + ill-conditioned systems, by preventing the true solution from + being very large. Another aid to regularization is provided by + the parameter acond, which may be used to terminate iterations + before the computed solution becomes very large. + + If some initial estimate ``x0`` is known and if ``damp == 0``, + one could proceed as follows: + + 1. Compute a residual vector ``r0 = b - A@x0``. + 2. Use LSQR to solve the system ``A@dx = r0``. + 3. Add the correction dx to obtain a final solution ``x = x0 + dx``. + + This requires that ``x0`` be available before and after the call + to LSQR. To judge the benefits, suppose LSQR takes k1 iterations + to solve A@x = b and k2 iterations to solve A@dx = r0. + If x0 is "good", norm(r0) will be smaller than norm(b). + If the same stopping tolerances atol and btol are used for each + system, k1 and k2 will be similar, but the final solution x0 + dx + should be more accurate. The only way to reduce the total work + is to use a larger stopping tolerance for the second system. + If some value btol is suitable for A@x = b, the larger value + btol*norm(b)/norm(r0) should be suitable for A@dx = r0. + + Preconditioning is another way to reduce the number of iterations. + If it is possible to solve a related system ``M@x = b`` + efficiently, where M approximates A in some helpful way (e.g. M - + A has low rank or its elements are small relative to those of A), + LSQR may converge more rapidly on the system ``A@M(inverse)@z = + b``, after which x can be recovered by solving M@x = z. + + If A is symmetric, LSQR should not be used! + + Alternatives are the symmetric conjugate-gradient method (cg) + and/or SYMMLQ. SYMMLQ is an implementation of symmetric cg that + applies to any symmetric A and will converge more rapidly than + LSQR. If A is positive definite, there are other implementations + of symmetric cg that require slightly less work per iteration than + SYMMLQ (but will take the same number of iterations). + + References + ---------- + .. [1] C. C. Paige and M. A. Saunders (1982a). + "LSQR: An algorithm for sparse linear equations and + sparse least squares", ACM TOMS 8(1), 43-71. + .. [2] C. C. Paige and M. A. Saunders (1982b). + "Algorithm 583. LSQR: Sparse linear equations and least + squares problems", ACM TOMS 8(2), 195-209. + .. [3] M. A. Saunders (1995). "Solution of sparse rectangular + systems using LSQR and CRAIG", BIT 35, 588-604. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import lsqr + >>> A = csc_matrix([[1., 0.], [1., 1.], [0., 1.]], dtype=float) + + The first example has the trivial solution ``[0, 0]`` + + >>> b = np.array([0., 0., 0.], dtype=float) + >>> x, istop, itn, normr = lsqr(A, b)[:4] + >>> istop + 0 + >>> x + array([ 0., 0.]) + + The stopping code `istop=0` returned indicates that a vector of zeros was + found as a solution. The returned solution `x` indeed contains + ``[0., 0.]``. The next example has a non-trivial solution: + + >>> b = np.array([1., 0., -1.], dtype=float) + >>> x, istop, itn, r1norm = lsqr(A, b)[:4] + >>> istop + 1 + >>> x + array([ 1., -1.]) + >>> itn + 1 + >>> r1norm + 4.440892098500627e-16 + + As indicated by `istop=1`, `lsqr` found a solution obeying the tolerance + limits. The given solution ``[1., -1.]`` obviously solves the equation. The + remaining return values include information about the number of iterations + (`itn=1`) and the remaining difference of left and right side of the solved + equation. + The final example demonstrates the behavior in the case where there is no + solution for the equation: + + >>> b = np.array([1., 0.01, -1.], dtype=float) + >>> x, istop, itn, r1norm = lsqr(A, b)[:4] + >>> istop + 2 + >>> x + array([ 1.00333333, -0.99666667]) + >>> A.dot(x)-b + array([ 0.00333333, -0.00333333, 0.00333333]) + >>> r1norm + 0.005773502691896255 + + `istop` indicates that the system is inconsistent and thus `x` is rather an + approximate solution to the corresponding least-squares problem. `r1norm` + contains the norm of the minimal residual that was found. + """ + A = aslinearoperator(A) + b = np.atleast_1d(b) + if b.ndim > 1: + b = b.squeeze() + + m, n = A.shape + if iter_lim is None: + iter_lim = 2 * n + var = np.zeros(n) + + msg = ('The exact solution is x = 0 ', + 'Ax - b is small enough, given atol, btol ', + 'The least-squares solution is good enough, given atol ', + 'The estimate of cond(Abar) has exceeded conlim ', + 'Ax - b is small enough for this machine ', + 'The least-squares solution is good enough for this machine', + 'Cond(Abar) seems to be too large for this machine ', + 'The iteration limit has been reached ') + + if show: + print(' ') + print('LSQR Least-squares solution of Ax = b') + str1 = f'The matrix A has {m} rows and {n} columns' + str2 = f'damp = {damp:20.14e} calc_var = {calc_var:8g}' + str3 = f'atol = {atol:8.2e} conlim = {conlim:8.2e}' + str4 = f'btol = {btol:8.2e} iter_lim = {iter_lim:8g}' + print(str1) + print(str2) + print(str3) + print(str4) + + itn = 0 + istop = 0 + ctol = 0 + if conlim > 0: + ctol = 1/conlim + anorm = 0 + acond = 0 + dampsq = damp**2 + ddnorm = 0 + res2 = 0 + xnorm = 0 + xxnorm = 0 + z = 0 + cs2 = -1 + sn2 = 0 + + # Set up the first vectors u and v for the bidiagonalization. + # These satisfy beta*u = b - A@x, alfa*v = A'@u. + u = b + bnorm = np.linalg.norm(b) + + if x0 is None: + x = np.zeros(n) + beta = bnorm.copy() + else: + x = np.asarray(x0) + u = u - A.matvec(x) + beta = np.linalg.norm(u) + + if beta > 0: + u = (1/beta) * u + v = A.rmatvec(u) + alfa = np.linalg.norm(v) + else: + v = x.copy() + alfa = 0 + + if alfa > 0: + v = (1/alfa) * v + w = v.copy() + + rhobar = alfa + phibar = beta + rnorm = beta + r1norm = rnorm + r2norm = rnorm + + # Reverse the order here from the original matlab code because + # there was an error on return when arnorm==0 + arnorm = alfa * beta + if arnorm == 0: + if show: + print(msg[0]) + return x, istop, itn, r1norm, r2norm, anorm, acond, arnorm, xnorm, var + + head1 = ' Itn x[0] r1norm r2norm ' + head2 = ' Compatible LS Norm A Cond A' + + if show: + print(' ') + print(head1, head2) + test1 = 1 + test2 = alfa / beta + str1 = f'{itn:6g} {x[0]:12.5e}' + str2 = f' {r1norm:10.3e} {r2norm:10.3e}' + str3 = f' {test1:8.1e} {test2:8.1e}' + print(str1, str2, str3) + + # Main iteration loop. + while itn < iter_lim: + itn = itn + 1 + # Perform the next step of the bidiagonalization to obtain the + # next beta, u, alfa, v. These satisfy the relations + # beta*u = a@v - alfa*u, + # alfa*v = A'@u - beta*v. + u = A.matvec(v) - alfa * u + beta = np.linalg.norm(u) + + if beta > 0: + u = (1/beta) * u + anorm = sqrt(anorm**2 + alfa**2 + beta**2 + dampsq) + v = A.rmatvec(u) - beta * v + alfa = np.linalg.norm(v) + if alfa > 0: + v = (1 / alfa) * v + + # Use a plane rotation to eliminate the damping parameter. + # This alters the diagonal (rhobar) of the lower-bidiagonal matrix. + if damp > 0: + rhobar1 = sqrt(rhobar**2 + dampsq) + cs1 = rhobar / rhobar1 + sn1 = damp / rhobar1 + psi = sn1 * phibar + phibar = cs1 * phibar + else: + # cs1 = 1 and sn1 = 0 + rhobar1 = rhobar + psi = 0. + + # Use a plane rotation to eliminate the subdiagonal element (beta) + # of the lower-bidiagonal matrix, giving an upper-bidiagonal matrix. + cs, sn, rho = _sym_ortho(rhobar1, beta) + + theta = sn * alfa + rhobar = -cs * alfa + phi = cs * phibar + phibar = sn * phibar + tau = sn * phi + + # Update x and w. + t1 = phi / rho + t2 = -theta / rho + dk = (1 / rho) * w + + x = x + t1 * w + w = v + t2 * w + ddnorm = ddnorm + np.linalg.norm(dk)**2 + + if calc_var: + var = var + dk**2 + + # Use a plane rotation on the right to eliminate the + # super-diagonal element (theta) of the upper-bidiagonal matrix. + # Then use the result to estimate norm(x). + delta = sn2 * rho + gambar = -cs2 * rho + rhs = phi - delta * z + zbar = rhs / gambar + xnorm = sqrt(xxnorm + zbar**2) + gamma = sqrt(gambar**2 + theta**2) + cs2 = gambar / gamma + sn2 = theta / gamma + z = rhs / gamma + xxnorm = xxnorm + z**2 + + # Test for convergence. + # First, estimate the condition of the matrix Abar, + # and the norms of rbar and Abar'rbar. + acond = anorm * sqrt(ddnorm) + res1 = phibar**2 + res2 = res2 + psi**2 + rnorm = sqrt(res1 + res2) + arnorm = alfa * abs(tau) + + # Distinguish between + # r1norm = ||b - Ax|| and + # r2norm = rnorm in current code + # = sqrt(r1norm^2 + damp^2*||x - x0||^2). + # Estimate r1norm from + # r1norm = sqrt(r2norm^2 - damp^2*||x - x0||^2). + # Although there is cancellation, it might be accurate enough. + if damp > 0: + r1sq = rnorm**2 - dampsq * xxnorm + r1norm = sqrt(abs(r1sq)) + if r1sq < 0: + r1norm = -r1norm + else: + r1norm = rnorm + r2norm = rnorm + + # Now use these norms to estimate certain other quantities, + # some of which will be small near a solution. + test1 = rnorm / bnorm + test2 = arnorm / (anorm * rnorm + eps) + test3 = 1 / (acond + eps) + t1 = test1 / (1 + anorm * xnorm / bnorm) + rtol = btol + atol * anorm * xnorm / bnorm + + # The following tests guard against extremely small values of + # atol, btol or ctol. (The user may have set any or all of + # the parameters atol, btol, conlim to 0.) + # The effect is equivalent to the normal tests using + # atol = eps, btol = eps, conlim = 1/eps. + if itn >= iter_lim: + istop = 7 + if 1 + test3 <= 1: + istop = 6 + if 1 + test2 <= 1: + istop = 5 + if 1 + t1 <= 1: + istop = 4 + + # Allow for tolerances set by the user. + if test3 <= ctol: + istop = 3 + if test2 <= atol: + istop = 2 + if test1 <= rtol: + istop = 1 + + if show: + # See if it is time to print something. + prnt = False + if n <= 40: + prnt = True + if itn <= 10: + prnt = True + if itn >= iter_lim-10: + prnt = True + # if itn%10 == 0: prnt = True + if test3 <= 2*ctol: + prnt = True + if test2 <= 10*atol: + prnt = True + if test1 <= 10*rtol: + prnt = True + if istop != 0: + prnt = True + + if prnt: + str1 = f'{itn:6g} {x[0]:12.5e}' + str2 = f' {r1norm:10.3e} {r2norm:10.3e}' + str3 = f' {test1:8.1e} {test2:8.1e}' + str4 = f' {anorm:8.1e} {acond:8.1e}' + print(str1, str2, str3, str4) + + if istop != 0: + break + + # End of iteration loop. + # Print the stopping condition. + if show: + print(' ') + print('LSQR finished') + print(msg[istop]) + print(' ') + str1 = f'istop ={istop:8g} r1norm ={r1norm:8.1e}' + str2 = f'anorm ={anorm:8.1e} arnorm ={arnorm:8.1e}' + str3 = f'itn ={itn:8g} r2norm ={r2norm:8.1e}' + str4 = f'acond ={acond:8.1e} xnorm ={xnorm:8.1e}' + print(str1 + ' ' + str2) + print(str3 + ' ' + str4) + print(' ') + + return x, istop, itn, r1norm, r2norm, anorm, acond, arnorm, xnorm, var diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/minres.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/minres.py new file mode 100644 index 0000000000000000000000000000000000000000..579c6cb8ab2c35e360a3b64e80fb36a59bf92e8a --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/minres.py @@ -0,0 +1,387 @@ +import warnings +from numpy import inner, zeros, inf, finfo +from numpy.linalg import norm +from math import sqrt + +from .utils import make_system +from scipy._lib.deprecation import _NoValue, _deprecate_positional_args + +__all__ = ['minres'] + + +@_deprecate_positional_args(version="1.14.0") +def minres(A, b, x0=None, *, shift=0.0, tol=_NoValue, maxiter=None, + M=None, callback=None, show=False, check=False, rtol=1e-5): + """ + Use MINimum RESidual iteration to solve Ax=b + + MINRES minimizes norm(Ax - b) for a real symmetric matrix A. Unlike + the Conjugate Gradient method, A can be indefinite or singular. + + If shift != 0 then the method solves (A - shift*I)x = b + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real symmetric N-by-N matrix of the linear system + Alternatively, ``A`` can be a linear operator which can + produce ``Ax`` using, e.g., + ``scipy.sparse.linalg.LinearOperator``. + b : ndarray + Right hand side of the linear system. Has shape (N,) or (N,1). + + Returns + ------- + x : ndarray + The converged solution. + info : integer + Provides convergence information: + 0 : successful exit + >0 : convergence to tolerance not achieved, number of iterations + <0 : illegal input or breakdown + + Other Parameters + ---------------- + x0 : ndarray + Starting guess for the solution. + shift : float + Value to apply to the system ``(A - shift * I)x = b``. Default is 0. + rtol : float + Tolerance to achieve. The algorithm terminates when the relative + residual is below ``rtol``. + maxiter : integer + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + M : {sparse matrix, ndarray, LinearOperator} + Preconditioner for A. The preconditioner should approximate the + inverse of A. Effective preconditioning dramatically improves the + rate of convergence, which implies that fewer iterations are needed + to reach a given error tolerance. + callback : function + User-supplied function to call after each iteration. It is called + as callback(xk), where xk is the current solution vector. + show : bool + If ``True``, print out a summary and metrics related to the solution + during iterations. Default is ``False``. + check : bool + If ``True``, run additional input validation to check that `A` and + `M` (if specified) are symmetric. Default is ``False``. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `minres` keyword argument ``tol`` is deprecated in favor of ``rtol`` + and will be removed in SciPy 1.14.0. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import minres + >>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float) + >>> A = A + A.T + >>> b = np.array([2, 4, -1], dtype=float) + >>> x, exitCode = minres(A, b) + >>> print(exitCode) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + + References + ---------- + Solution of sparse indefinite systems of linear equations, + C. C. Paige and M. A. Saunders (1975), + SIAM J. Numer. Anal. 12(4), pp. 617-629. + https://web.stanford.edu/group/SOL/software/minres/ + + This file is a translation of the following MATLAB implementation: + https://web.stanford.edu/group/SOL/software/minres/minres-matlab.zip + + """ + A, M, x, b, postprocess = make_system(A, M, x0, b) + + if tol is not _NoValue: + msg = ("'scipy.sparse.linalg.minres' keyword argument `tol` is " + "deprecated in favor of `rtol` and will be removed in SciPy " + "v1.14. Until then, if set, it will override `rtol`.") + warnings.warn(msg, category=DeprecationWarning, stacklevel=4) + rtol = float(tol) if tol is not None else rtol + + matvec = A.matvec + psolve = M.matvec + + first = 'Enter minres. ' + last = 'Exit minres. ' + + n = A.shape[0] + + if maxiter is None: + maxiter = 5 * n + + msg = [' beta2 = 0. If M = I, b and x are eigenvectors ', # -1 + ' beta1 = 0. The exact solution is x0 ', # 0 + ' A solution to Ax = b was found, given rtol ', # 1 + ' A least-squares solution was found, given rtol ', # 2 + ' Reasonable accuracy achieved, given eps ', # 3 + ' x has converged to an eigenvector ', # 4 + ' acond has exceeded 0.1/eps ', # 5 + ' The iteration limit was reached ', # 6 + ' A does not define a symmetric matrix ', # 7 + ' M does not define a symmetric matrix ', # 8 + ' M does not define a pos-def preconditioner '] # 9 + + if show: + print(first + 'Solution of symmetric Ax = b') + print(first + f'n = {n:3g} shift = {shift:23.14e}') + print(first + f'itnlim = {maxiter:3g} rtol = {rtol:11.2e}') + print() + + istop = 0 + itn = 0 + Anorm = 0 + Acond = 0 + rnorm = 0 + ynorm = 0 + + xtype = x.dtype + + eps = finfo(xtype).eps + + # Set up y and v for the first Lanczos vector v1. + # y = beta1 P' v1, where P = C**(-1). + # v is really P' v1. + + if x0 is None: + r1 = b.copy() + else: + r1 = b - A@x + y = psolve(r1) + + beta1 = inner(r1, y) + + if beta1 < 0: + raise ValueError('indefinite preconditioner') + elif beta1 == 0: + return (postprocess(x), 0) + + bnorm = norm(b) + if bnorm == 0: + x = b + return (postprocess(x), 0) + + beta1 = sqrt(beta1) + + if check: + # are these too strict? + + # see if A is symmetric + w = matvec(y) + r2 = matvec(w) + s = inner(w,w) + t = inner(y,r2) + z = abs(s - t) + epsa = (s + eps) * eps**(1.0/3.0) + if z > epsa: + raise ValueError('non-symmetric matrix') + + # see if M is symmetric + r2 = psolve(y) + s = inner(y,y) + t = inner(r1,r2) + z = abs(s - t) + epsa = (s + eps) * eps**(1.0/3.0) + if z > epsa: + raise ValueError('non-symmetric preconditioner') + + # Initialize other quantities + oldb = 0 + beta = beta1 + dbar = 0 + epsln = 0 + qrnorm = beta1 + phibar = beta1 + rhs1 = beta1 + rhs2 = 0 + tnorm2 = 0 + gmax = 0 + gmin = finfo(xtype).max + cs = -1 + sn = 0 + w = zeros(n, dtype=xtype) + w2 = zeros(n, dtype=xtype) + r2 = r1 + + if show: + print() + print() + print(' Itn x(1) Compatible LS norm(A) cond(A) gbar/|A|') + + while itn < maxiter: + itn += 1 + + s = 1.0/beta + v = s*y + + y = matvec(v) + y = y - shift * v + + if itn >= 2: + y = y - (beta/oldb)*r1 + + alfa = inner(v,y) + y = y - (alfa/beta)*r2 + r1 = r2 + r2 = y + y = psolve(r2) + oldb = beta + beta = inner(r2,y) + if beta < 0: + raise ValueError('non-symmetric matrix') + beta = sqrt(beta) + tnorm2 += alfa**2 + oldb**2 + beta**2 + + if itn == 1: + if beta/beta1 <= 10*eps: + istop = -1 # Terminate later + + # Apply previous rotation Qk-1 to get + # [deltak epslnk+1] = [cs sn][dbark 0 ] + # [gbar k dbar k+1] [sn -cs][alfak betak+1]. + + oldeps = epsln + delta = cs * dbar + sn * alfa # delta1 = 0 deltak + gbar = sn * dbar - cs * alfa # gbar 1 = alfa1 gbar k + epsln = sn * beta # epsln2 = 0 epslnk+1 + dbar = - cs * beta # dbar 2 = beta2 dbar k+1 + root = norm([gbar, dbar]) + Arnorm = phibar * root + + # Compute the next plane rotation Qk + + gamma = norm([gbar, beta]) # gammak + gamma = max(gamma, eps) + cs = gbar / gamma # ck + sn = beta / gamma # sk + phi = cs * phibar # phik + phibar = sn * phibar # phibark+1 + + # Update x. + + denom = 1.0/gamma + w1 = w2 + w2 = w + w = (v - oldeps*w1 - delta*w2) * denom + x = x + phi*w + + # Go round again. + + gmax = max(gmax, gamma) + gmin = min(gmin, gamma) + z = rhs1 / gamma + rhs1 = rhs2 - delta*z + rhs2 = - epsln*z + + # Estimate various norms and test for convergence. + + Anorm = sqrt(tnorm2) + ynorm = norm(x) + epsa = Anorm * eps + epsx = Anorm * ynorm * eps + epsr = Anorm * ynorm * rtol + diag = gbar + + if diag == 0: + diag = epsa + + qrnorm = phibar + rnorm = qrnorm + if ynorm == 0 or Anorm == 0: + test1 = inf + else: + test1 = rnorm / (Anorm*ynorm) # ||r|| / (||A|| ||x||) + if Anorm == 0: + test2 = inf + else: + test2 = root / Anorm # ||Ar|| / (||A|| ||r||) + + # Estimate cond(A). + # In this version we look at the diagonals of R in the + # factorization of the lower Hessenberg matrix, Q @ H = R, + # where H is the tridiagonal matrix from Lanczos with one + # extra row, beta(k+1) e_k^T. + + Acond = gmax/gmin + + # See if any of the stopping criteria are satisfied. + # In rare cases, istop is already -1 from above (Abar = const*I). + + if istop == 0: + t1 = 1 + test1 # These tests work if rtol < eps + t2 = 1 + test2 + if t2 <= 1: + istop = 2 + if t1 <= 1: + istop = 1 + + if itn >= maxiter: + istop = 6 + if Acond >= 0.1/eps: + istop = 4 + if epsx >= beta1: + istop = 3 + # if rnorm <= epsx : istop = 2 + # if rnorm <= epsr : istop = 1 + if test2 <= rtol: + istop = 2 + if test1 <= rtol: + istop = 1 + + # See if it is time to print something. + + prnt = False + if n <= 40: + prnt = True + if itn <= 10: + prnt = True + if itn >= maxiter-10: + prnt = True + if itn % 10 == 0: + prnt = True + if qrnorm <= 10*epsx: + prnt = True + if qrnorm <= 10*epsr: + prnt = True + if Acond <= 1e-2/eps: + prnt = True + if istop != 0: + prnt = True + + if show and prnt: + str1 = f'{itn:6g} {x[0]:12.5e} {test1:10.3e}' + str2 = f' {test2:10.3e}' + str3 = f' {Anorm:8.1e} {Acond:8.1e} {gbar/Anorm:8.1e}' + + print(str1 + str2 + str3) + + if itn % 10 == 0: + print() + + if callback is not None: + callback(x) + + if istop != 0: + break # TODO check this + + if show: + print() + print(last + f' istop = {istop:3g} itn ={itn:5g}') + print(last + f' Anorm = {Anorm:12.4e} Acond = {Acond:12.4e}') + print(last + f' rnorm = {rnorm:12.4e} ynorm = {ynorm:12.4e}') + print(last + f' Arnorm = {Arnorm:12.4e}') + print(last + msg[istop+1]) + + if istop == 6: + info = maxiter + else: + info = 0 + + return (postprocess(x),info) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tests/test_gcrotmk.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tests/test_gcrotmk.py new file mode 100644 index 0000000000000000000000000000000000000000..8e6b43497e9750f753578af1bae9f9d76c9bcfe5 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tests/test_gcrotmk.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +"""Tests for the linalg._isolve.gcrotmk module +""" + +from numpy.testing import (assert_, assert_allclose, assert_equal, + suppress_warnings) + +import numpy as np +from numpy import zeros, array, allclose +from scipy.linalg import norm +from scipy.sparse import csr_matrix, eye, rand + +from scipy.sparse.linalg._interface import LinearOperator +from scipy.sparse.linalg import splu +from scipy.sparse.linalg._isolve import gcrotmk, gmres + + +Am = csr_matrix(array([[-2,1,0,0,0,9], + [1,-2,1,0,5,0], + [0,1,-2,1,0,0], + [0,0,1,-2,1,0], + [0,3,0,1,-2,1], + [1,0,0,0,1,-2]])) +b = array([1,2,3,4,5,6]) +count = [0] + + +def matvec(v): + count[0] += 1 + return Am@v + + +A = LinearOperator(matvec=matvec, shape=Am.shape, dtype=Am.dtype) + + +def do_solve(**kw): + count[0] = 0 + with suppress_warnings() as sup: + sup.filter(DeprecationWarning, ".*called without specifying.*") + x0, flag = gcrotmk(A, b, x0=zeros(A.shape[0]), rtol=1e-14, **kw) + count_0 = count[0] + assert_(allclose(A@x0, b, rtol=1e-12, atol=1e-12), norm(A@x0-b)) + return x0, count_0 + + +class TestGCROTMK: + def test_preconditioner(self): + # Check that preconditioning works + pc = splu(Am.tocsc()) + M = LinearOperator(matvec=pc.solve, shape=A.shape, dtype=A.dtype) + + x0, count_0 = do_solve() + x1, count_1 = do_solve(M=M) + + assert_equal(count_1, 3) + assert_(count_1 < count_0/2) + assert_(allclose(x1, x0, rtol=1e-14)) + + def test_arnoldi(self): + np.random.seed(1) + + A = eye(2000) + rand(2000, 2000, density=5e-4) + b = np.random.rand(2000) + + # The inner arnoldi should be equivalent to gmres + with suppress_warnings() as sup: + sup.filter(DeprecationWarning, ".*called without specifying.*") + x0, flag0 = gcrotmk(A, b, x0=zeros(A.shape[0]), m=15, k=0, maxiter=1) + x1, flag1 = gmres(A, b, x0=zeros(A.shape[0]), restart=15, maxiter=1) + + assert_equal(flag0, 1) + assert_equal(flag1, 1) + assert np.linalg.norm(A.dot(x0) - b) > 1e-3 + + assert_allclose(x0, x1) + + def test_cornercase(self): + np.random.seed(1234) + + # Rounding error may prevent convergence with tol=0 --- ensure + # that the return values in this case are correct, and no + # exceptions are raised + + for n in [3, 5, 10, 100]: + A = 2*eye(n) + + with suppress_warnings() as sup: + sup.filter(DeprecationWarning, ".*called without specifying.*") + b = np.ones(n) + x, info = gcrotmk(A, b, maxiter=10) + assert_equal(info, 0) + assert_allclose(A.dot(x) - b, 0, atol=1e-14) + + x, info = gcrotmk(A, b, rtol=0, maxiter=10) + if info == 0: + assert_allclose(A.dot(x) - b, 0, atol=1e-14) + + b = np.random.rand(n) + x, info = gcrotmk(A, b, maxiter=10) + assert_equal(info, 0) + assert_allclose(A.dot(x) - b, 0, atol=1e-14) + + x, info = gcrotmk(A, b, rtol=0, maxiter=10) + if info == 0: + assert_allclose(A.dot(x) - b, 0, atol=1e-14) + + def test_nans(self): + A = eye(3, format='lil') + A[1,1] = np.nan + b = np.ones(3) + + with suppress_warnings() as sup: + sup.filter(DeprecationWarning, ".*called without specifying.*") + x, info = gcrotmk(A, b, rtol=0, maxiter=10) + assert_equal(info, 1) + + def test_truncate(self): + np.random.seed(1234) + A = np.random.rand(30, 30) + np.eye(30) + b = np.random.rand(30) + + for truncate in ['oldest', 'smallest']: + with suppress_warnings() as sup: + sup.filter(DeprecationWarning, ".*called without specifying.*") + x, info = gcrotmk(A, b, m=10, k=10, truncate=truncate, + rtol=1e-4, maxiter=200) + assert_equal(info, 0) + assert_allclose(A.dot(x) - b, 0, atol=1e-3) + + def test_CU(self): + for discard_C in (True, False): + # Check that C,U behave as expected + CU = [] + x0, count_0 = do_solve(CU=CU, discard_C=discard_C) + assert_(len(CU) > 0) + assert_(len(CU) <= 6) + + if discard_C: + for c, u in CU: + assert_(c is None) + + # should converge immediately + x1, count_1 = do_solve(CU=CU, discard_C=discard_C) + if discard_C: + assert_equal(count_1, 2 + len(CU)) + else: + assert_equal(count_1, 3) + assert_(count_1 <= count_0/2) + assert_allclose(x1, x0, atol=1e-14) + + def test_denormals(self): + # Check that no warnings are emitted if the matrix contains + # numbers for which 1/x has no float representation, and that + # the solver behaves properly. + A = np.array([[1, 2], [3, 4]], dtype=float) + A *= 100 * np.nextafter(0, 1) + + b = np.array([1, 1]) + + with suppress_warnings() as sup: + sup.filter(DeprecationWarning, ".*called without specifying.*") + xp, info = gcrotmk(A, b) + + if info == 0: + assert_allclose(A.dot(xp), b) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tests/test_iterative.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tests/test_iterative.py new file mode 100644 index 0000000000000000000000000000000000000000..d6a44a1264d049655972b9f6451693ab7208f48a --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tests/test_iterative.py @@ -0,0 +1,796 @@ +""" Test functions for the sparse.linalg._isolve module +""" + +import itertools +import platform +import sys +import pytest + +import numpy as np +from numpy.testing import assert_array_equal, assert_allclose +from numpy import zeros, arange, array, ones, eye, iscomplexobj +from numpy.linalg import norm + +from scipy.sparse import spdiags, csr_matrix, kronsum + +from scipy.sparse.linalg import LinearOperator, aslinearoperator +from scipy.sparse.linalg._isolve import (bicg, bicgstab, cg, cgs, + gcrotmk, gmres, lgmres, + minres, qmr, tfqmr) + +# TODO check that method preserve shape and type +# TODO test both preconditioner methods + + +# list of all solvers under test +_SOLVERS = [bicg, bicgstab, cg, cgs, gcrotmk, gmres, lgmres, + minres, qmr, tfqmr] + +pytestmark = [ + # remove this once atol defaults to 0.0 for all methods + pytest.mark.filterwarnings("ignore:.*called without specifying.*"), +] + + +# create parametrized fixture for easy reuse in tests +@pytest.fixture(params=_SOLVERS, scope="session") +def solver(request): + """ + Fixture for all solvers in scipy.sparse.linalg._isolve + """ + return request.param + + +class Case: + def __init__(self, name, A, b=None, skip=None, nonconvergence=None): + self.name = name + self.A = A + if b is None: + self.b = arange(A.shape[0], dtype=float) + else: + self.b = b + if skip is None: + self.skip = [] + else: + self.skip = skip + if nonconvergence is None: + self.nonconvergence = [] + else: + self.nonconvergence = nonconvergence + + +class SingleTest: + def __init__(self, A, b, solver, casename, convergence=True): + self.A = A + self.b = b + self.solver = solver + self.name = casename + '-' + solver.__name__ + self.convergence = convergence + + def __repr__(self): + return f"<{self.name}>" + + +class IterativeParams: + def __init__(self): + sym_solvers = [minres, cg] + posdef_solvers = [cg] + real_solvers = [minres] + + # list of Cases + self.cases = [] + + # Symmetric and Positive Definite + N = 40 + data = ones((3, N)) + data[0, :] = 2 + data[1, :] = -1 + data[2, :] = -1 + Poisson1D = spdiags(data, [0, -1, 1], N, N, format='csr') + self.cases.append(Case("poisson1d", Poisson1D)) + # note: minres fails for single precision + self.cases.append(Case("poisson1d-F", Poisson1D.astype('f'), + skip=[minres])) + + # Symmetric and Negative Definite + self.cases.append(Case("neg-poisson1d", -Poisson1D, + skip=posdef_solvers)) + # note: minres fails for single precision + self.cases.append(Case("neg-poisson1d-F", (-Poisson1D).astype('f'), + skip=posdef_solvers + [minres])) + + # 2-dimensional Poisson equations + Poisson2D = kronsum(Poisson1D, Poisson1D) + # note: minres fails for 2-d poisson problem, + # it will be fixed in the future PR + self.cases.append(Case("poisson2d", Poisson2D, skip=[minres])) + # note: minres fails for single precision + self.cases.append(Case("poisson2d-F", Poisson2D.astype('f'), + skip=[minres])) + + # Symmetric and Indefinite + data = array([[6, -5, 2, 7, -1, 10, 4, -3, -8, 9]], dtype='d') + RandDiag = spdiags(data, [0], 10, 10, format='csr') + self.cases.append(Case("rand-diag", RandDiag, skip=posdef_solvers)) + self.cases.append(Case("rand-diag-F", RandDiag.astype('f'), + skip=posdef_solvers)) + + # Random real-valued + np.random.seed(1234) + data = np.random.rand(4, 4) + self.cases.append(Case("rand", data, + skip=posdef_solvers + sym_solvers)) + self.cases.append(Case("rand-F", data.astype('f'), + skip=posdef_solvers + sym_solvers)) + + # Random symmetric real-valued + np.random.seed(1234) + data = np.random.rand(4, 4) + data = data + data.T + self.cases.append(Case("rand-sym", data, skip=posdef_solvers)) + self.cases.append(Case("rand-sym-F", data.astype('f'), + skip=posdef_solvers)) + + # Random pos-def symmetric real + np.random.seed(1234) + data = np.random.rand(9, 9) + data = np.dot(data.conj(), data.T) + self.cases.append(Case("rand-sym-pd", data)) + # note: minres fails for single precision + self.cases.append(Case("rand-sym-pd-F", data.astype('f'), + skip=[minres])) + + # Random complex-valued + np.random.seed(1234) + data = np.random.rand(4, 4) + 1j * np.random.rand(4, 4) + skip_cmplx = posdef_solvers + sym_solvers + real_solvers + self.cases.append(Case("rand-cmplx", data, skip=skip_cmplx)) + self.cases.append(Case("rand-cmplx-F", data.astype('F'), + skip=skip_cmplx)) + + # Random hermitian complex-valued + np.random.seed(1234) + data = np.random.rand(4, 4) + 1j * np.random.rand(4, 4) + data = data + data.T.conj() + self.cases.append(Case("rand-cmplx-herm", data, + skip=posdef_solvers + real_solvers)) + self.cases.append(Case("rand-cmplx-herm-F", data.astype('F'), + skip=posdef_solvers + real_solvers)) + + # Random pos-def hermitian complex-valued + np.random.seed(1234) + data = np.random.rand(9, 9) + 1j * np.random.rand(9, 9) + data = np.dot(data.conj(), data.T) + self.cases.append(Case("rand-cmplx-sym-pd", data, skip=real_solvers)) + self.cases.append(Case("rand-cmplx-sym-pd-F", data.astype('F'), + skip=real_solvers)) + + # Non-symmetric and Positive Definite + # + # cgs, qmr, bicg and tfqmr fail to converge on this one + # -- algorithmic limitation apparently + data = ones((2, 10)) + data[0, :] = 2 + data[1, :] = -1 + A = spdiags(data, [0, -1], 10, 10, format='csr') + self.cases.append(Case("nonsymposdef", A, + skip=sym_solvers + [cgs, qmr, bicg, tfqmr])) + self.cases.append(Case("nonsymposdef-F", A.astype('F'), + skip=sym_solvers + [cgs, qmr, bicg, tfqmr])) + + # Symmetric, non-pd, hitting cgs/bicg/bicgstab/qmr/tfqmr breakdown + A = np.array([[0, 0, 0, 0, 0, 1, -1, -0, -0, -0, -0], + [0, 0, 0, 0, 0, 2, -0, -1, -0, -0, -0], + [0, 0, 0, 0, 0, 2, -0, -0, -1, -0, -0], + [0, 0, 0, 0, 0, 2, -0, -0, -0, -1, -0], + [0, 0, 0, 0, 0, 1, -0, -0, -0, -0, -1], + [1, 2, 2, 2, 1, 0, -0, -0, -0, -0, -0], + [-1, 0, 0, 0, 0, 0, -1, -0, -0, -0, -0], + [0, -1, 0, 0, 0, 0, -0, -1, -0, -0, -0], + [0, 0, -1, 0, 0, 0, -0, -0, -1, -0, -0], + [0, 0, 0, -1, 0, 0, -0, -0, -0, -1, -0], + [0, 0, 0, 0, -1, 0, -0, -0, -0, -0, -1]], dtype=float) + b = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], dtype=float) + assert (A == A.T).all() + self.cases.append(Case("sym-nonpd", A, b, + skip=posdef_solvers, + nonconvergence=[cgs, bicg, bicgstab, qmr, tfqmr] + ) + ) + + def generate_tests(self): + # generate test cases with skips applied + tests = [] + for case in self.cases: + for solver in _SOLVERS: + if (solver in case.skip): + continue + if solver in case.nonconvergence: + tests += [SingleTest(case.A, case.b, solver, case.name, + convergence=False)] + else: + tests += [SingleTest(case.A, case.b, solver, case.name)] + return tests + + +cases = IterativeParams().generate_tests() + + +@pytest.fixture(params=cases, ids=[x.name for x in cases], scope="module") +def case(request): + """ + Fixture for all cases in IterativeParams + """ + return request.param + + +def test_maxiter(case): + if not case.convergence: + pytest.skip("Solver - Breakdown case, see gh-8829") + A = case.A + rtol = 1e-12 + + b = case.b + x0 = 0 * b + + residuals = [] + + def callback(x): + residuals.append(norm(b - case.A * x)) + + x, info = case.solver(A, b, x0=x0, rtol=rtol, maxiter=1, callback=callback) + + assert len(residuals) == 1 + assert info == 1 + + +def test_convergence(case): + A = case.A + + if A.dtype.char in "dD": + rtol = 1e-8 + else: + rtol = 1e-2 + + b = case.b + x0 = 0 * b + + x, info = case.solver(A, b, x0=x0, rtol=rtol) + + assert_array_equal(x0, 0 * b) # ensure that x0 is not overwritten + if case.convergence: + assert info == 0 + assert norm(A @ x - b) <= norm(b) * rtol + else: + assert info != 0 + assert norm(A @ x - b) <= norm(b) + + +def test_precond_dummy(case): + if not case.convergence: + pytest.skip("Solver - Breakdown case, see gh-8829") + + rtol = 1e-8 + + def identity(b, which=None): + """trivial preconditioner""" + return b + + A = case.A + + M, N = A.shape + # Ensure the diagonal elements of A are non-zero before calculating + # 1.0/A.diagonal() + diagOfA = A.diagonal() + if np.count_nonzero(diagOfA) == len(diagOfA): + spdiags([1.0 / diagOfA], [0], M, N) + + b = case.b + x0 = 0 * b + + precond = LinearOperator(A.shape, identity, rmatvec=identity) + + if case.solver is qmr: + x, info = case.solver(A, b, M1=precond, M2=precond, x0=x0, rtol=rtol) + else: + x, info = case.solver(A, b, M=precond, x0=x0, rtol=rtol) + assert info == 0 + assert norm(A @ x - b) <= norm(b) * rtol + + A = aslinearoperator(A) + A.psolve = identity + A.rpsolve = identity + + x, info = case.solver(A, b, x0=x0, rtol=rtol) + assert info == 0 + assert norm(A @ x - b) <= norm(b) * rtol + + +# Specific test for poisson1d and poisson2d cases +@pytest.mark.parametrize('case', [x for x in IterativeParams().cases + if x.name in ('poisson1d', 'poisson2d')], + ids=['poisson1d', 'poisson2d']) +def test_precond_inverse(case): + for solver in _SOLVERS: + if solver in case.skip or solver is qmr: + continue + + rtol = 1e-8 + + def inverse(b, which=None): + """inverse preconditioner""" + A = case.A + if not isinstance(A, np.ndarray): + A = A.toarray() + return np.linalg.solve(A, b) + + def rinverse(b, which=None): + """inverse preconditioner""" + A = case.A + if not isinstance(A, np.ndarray): + A = A.toarray() + return np.linalg.solve(A.T, b) + + matvec_count = [0] + + def matvec(b): + matvec_count[0] += 1 + return case.A @ b + + def rmatvec(b): + matvec_count[0] += 1 + return case.A.T @ b + + b = case.b + x0 = 0 * b + + A = LinearOperator(case.A.shape, matvec, rmatvec=rmatvec) + precond = LinearOperator(case.A.shape, inverse, rmatvec=rinverse) + + # Solve with preconditioner + matvec_count = [0] + x, info = solver(A, b, M=precond, x0=x0, rtol=rtol) + + assert info == 0 + assert norm(case.A @ x - b) <= norm(b) * rtol + + # Solution should be nearly instant + assert matvec_count[0] <= 3 + + +def test_atol(solver): + # TODO: minres / tfqmr. It didn't historically use absolute tolerances, so + # fixing it is less urgent. + if solver in (minres, tfqmr): + pytest.skip("TODO: Add atol to minres/tfqmr") + + # Historically this is tested as below, all pass but for some reason + # gcrotmk is over-sensitive to difference between random.seed/rng.random + # Hence tol lower bound is changed from -10 to -9 + # np.random.seed(1234) + # A = np.random.rand(10, 10) + # A = A @ A.T + 10 * np.eye(10) + # b = 1e3*np.random.rand(10) + + rng = np.random.default_rng(168441431005389) + A = rng.uniform(size=[10, 10]) + A = A @ A.T + 10*np.eye(10) + b = 1e3 * rng.uniform(size=10) + + b_norm = np.linalg.norm(b) + + tols = np.r_[0, np.logspace(-9, 2, 7), np.inf] + + # Check effect of badly scaled preconditioners + M0 = rng.standard_normal(size=(10, 10)) + M0 = M0 @ M0.T + Ms = [None, 1e-6 * M0, 1e6 * M0] + + for M, rtol, atol in itertools.product(Ms, tols, tols): + if rtol == 0 and atol == 0: + continue + + if solver is qmr: + if M is not None: + M = aslinearoperator(M) + M2 = aslinearoperator(np.eye(10)) + else: + M2 = None + x, info = solver(A, b, M1=M, M2=M2, rtol=rtol, atol=atol) + else: + x, info = solver(A, b, M=M, rtol=rtol, atol=atol) + + assert info == 0 + residual = A @ x - b + err = np.linalg.norm(residual) + atol2 = rtol * b_norm + # Added 1.00025 fudge factor because of `err` exceeding `atol` just + # very slightly on s390x (see gh-17839) + assert err <= 1.00025 * max(atol, atol2) + + +def test_zero_rhs(solver): + rng = np.random.default_rng(1684414984100503) + A = rng.random(size=[10, 10]) + A = A @ A.T + 10 * np.eye(10) + + b = np.zeros(10) + tols = np.r_[np.logspace(-10, 2, 7)] + + for tol in tols: + x, info = solver(A, b, rtol=tol) + assert info == 0 + assert_allclose(x, 0., atol=1e-15) + + x, info = solver(A, b, rtol=tol, x0=ones(10)) + assert info == 0 + assert_allclose(x, 0., atol=tol) + + if solver is not minres: + x, info = solver(A, b, rtol=tol, atol=0, x0=ones(10)) + if info == 0: + assert_allclose(x, 0) + + x, info = solver(A, b, rtol=tol, atol=tol) + assert info == 0 + assert_allclose(x, 0, atol=1e-300) + + x, info = solver(A, b, rtol=tol, atol=0) + assert info == 0 + assert_allclose(x, 0, atol=1e-300) + + +@pytest.mark.xfail(reason="see gh-18697") +def test_maxiter_worsening(solver): + if solver not in (gmres, lgmres, qmr): + # these were skipped from the very beginning, see gh-9201; gh-14160 + pytest.skip("Solver breakdown case") + # Check error does not grow (boundlessly) with increasing maxiter. + # This can occur due to the solvers hitting close to breakdown, + # which they should detect and halt as necessary. + # cf. gh-9100 + if (solver is gmres and platform.machine() == 'aarch64' + and sys.version_info[1] == 9): + pytest.xfail(reason="gh-13019") + if (solver is lgmres and + platform.machine() not in ['x86_64' 'x86', 'aarch64', 'arm64']): + # see gh-17839 + pytest.xfail(reason="fails on at least ppc64le, ppc64 and riscv64") + + # Singular matrix, rhs numerically not in range + A = np.array([[-0.1112795288033378, 0, 0, 0.16127952880333685], + [0, -0.13627952880333782 + 6.283185307179586j, 0, 0], + [0, 0, -0.13627952880333782 - 6.283185307179586j, 0], + [0.1112795288033368, 0j, 0j, -0.16127952880333785]]) + v = np.ones(4) + best_error = np.inf + + # Unable to match the Fortran code tolerance levels with this example + # Original tolerance values + + # slack_tol = 7 if platform.machine() == 'aarch64' else 5 + slack_tol = 9 + + for maxiter in range(1, 20): + x, info = solver(A, v, maxiter=maxiter, rtol=1e-8, atol=0) + + if info == 0: + assert norm(A @ x - v) <= 1e-8 * norm(v) + + error = np.linalg.norm(A @ x - v) + best_error = min(best_error, error) + + # Check with slack + assert error <= slack_tol * best_error + + +def test_x0_working(solver): + # Easy problem + rng = np.random.default_rng(1685363802304750) + n = 10 + A = rng.random(size=[n, n]) + A = A @ A.T + b = rng.random(n) + x0 = rng.random(n) + + if solver is minres: + kw = dict(rtol=1e-6) + else: + kw = dict(atol=0, rtol=1e-6) + + x, info = solver(A, b, **kw) + assert info == 0 + assert norm(A @ x - b) <= 1e-6 * norm(b) + + x, info = solver(A, b, x0=x0, **kw) + assert info == 0 + assert norm(A @ x - b) <= 2e-6*norm(b) + + +def test_x0_equals_Mb(case): + if case.solver is tfqmr: + pytest.skip("Solver does not support x0='Mb'") + A = case.A + b = case.b + x0 = 'Mb' + rtol = 1e-8 + x, info = case.solver(A, b, x0=x0, rtol=rtol) + + assert_array_equal(x0, 'Mb') # ensure that x0 is not overwritten + assert info == 0 + assert norm(A @ x - b) <= rtol * norm(b) + + +@pytest.mark.parametrize('solver', _SOLVERS) +def test_x0_solves_problem_exactly(solver): + # See gh-19948 + mat = np.eye(2) + rhs = np.array([-1., -1.]) + + sol, info = solver(mat, rhs, x0=rhs) + assert_allclose(sol, rhs) + assert info == 0 + + +# Specific tfqmr test +@pytest.mark.parametrize('case', IterativeParams().cases) +def test_show(case, capsys): + def cb(x): + pass + + x, info = tfqmr(case.A, case.b, callback=cb, show=True) + out, err = capsys.readouterr() + + if case.name == "sym-nonpd": + # no logs for some reason + exp = "" + elif case.name in ("nonsymposdef", "nonsymposdef-F"): + # Asymmetric and Positive Definite + exp = "TFQMR: Linear solve not converged due to reach MAXIT iterations" + else: # all other cases + exp = "TFQMR: Linear solve converged due to reach TOL iterations" + + assert out.startswith(exp) + assert err == "" + + +def test_positional_deprecation(solver): + # from test_x0_working + rng = np.random.default_rng(1685363802304750) + n = 10 + A = rng.random(size=[n, n]) + A = A @ A.T + b = rng.random(n) + x0 = rng.random(n) + with pytest.deprecated_call( + # due to the use of the _deprecate_positional_args decorator, it's not possible + # to separate the two warnings (1 for positional use, 1 for `tol` deprecation). + match="use keyword arguments.*|argument `tol` is deprecated.*" + ): + solver(A, b, x0, 1e-5) + + +class TestQMR: + @pytest.mark.filterwarnings('ignore::scipy.sparse.SparseEfficiencyWarning') + def test_leftright_precond(self): + """Check that QMR works with left and right preconditioners""" + + from scipy.sparse.linalg._dsolve import splu + from scipy.sparse.linalg._interface import LinearOperator + + n = 100 + + dat = ones(n) + A = spdiags([-2 * dat, 4 * dat, -dat], [-1, 0, 1], n, n) + b = arange(n, dtype='d') + + L = spdiags([-dat / 2, dat], [-1, 0], n, n) + U = spdiags([4 * dat, -dat], [0, 1], n, n) + L_solver = splu(L) + U_solver = splu(U) + + def L_solve(b): + return L_solver.solve(b) + + def U_solve(b): + return U_solver.solve(b) + + def LT_solve(b): + return L_solver.solve(b, 'T') + + def UT_solve(b): + return U_solver.solve(b, 'T') + + M1 = LinearOperator((n, n), matvec=L_solve, rmatvec=LT_solve) + M2 = LinearOperator((n, n), matvec=U_solve, rmatvec=UT_solve) + + rtol = 1e-8 + x, info = qmr(A, b, rtol=rtol, maxiter=15, M1=M1, M2=M2) + + assert info == 0 + assert norm(A @ x - b) <= rtol * norm(b) + + +class TestGMRES: + def test_basic(self): + A = np.vander(np.arange(10) + 1)[:, ::-1] + b = np.zeros(10) + b[0] = 1 + + x_gm, err = gmres(A, b, restart=5, maxiter=1) + + assert_allclose(x_gm[0], 0.359, rtol=1e-2) + + def test_callback(self): + + def store_residual(r, rvec): + rvec[rvec.nonzero()[0].max() + 1] = r + + # Define, A,b + A = csr_matrix(array([[-2, 1, 0, 0, 0, 0], + [1, -2, 1, 0, 0, 0], + [0, 1, -2, 1, 0, 0], + [0, 0, 1, -2, 1, 0], + [0, 0, 0, 1, -2, 1], + [0, 0, 0, 0, 1, -2]])) + b = ones((A.shape[0],)) + maxiter = 1 + rvec = zeros(maxiter + 1) + rvec[0] = 1.0 + + def callback(r): + return store_residual(r, rvec) + + x, flag = gmres(A, b, x0=zeros(A.shape[0]), rtol=1e-16, + maxiter=maxiter, callback=callback) + + # Expected output from SciPy 1.0.0 + assert_allclose(rvec, array([1.0, 0.81649658092772603]), rtol=1e-10) + + # Test preconditioned callback + M = 1e-3 * np.eye(A.shape[0]) + rvec = zeros(maxiter + 1) + rvec[0] = 1.0 + x, flag = gmres(A, b, M=M, rtol=1e-16, maxiter=maxiter, + callback=callback) + + # Expected output from SciPy 1.0.0 + # (callback has preconditioned residual!) + assert_allclose(rvec, array([1.0, 1e-3 * 0.81649658092772603]), + rtol=1e-10) + + def test_abi(self): + # Check we don't segfault on gmres with complex argument + A = eye(2) + b = ones(2) + r_x, r_info = gmres(A, b) + r_x = r_x.astype(complex) + x, info = gmres(A.astype(complex), b.astype(complex)) + + assert iscomplexobj(x) + assert_allclose(r_x, x) + assert r_info == info + + def test_atol_legacy(self): + + A = eye(2) + b = ones(2) + x, info = gmres(A, b, rtol=1e-5) + assert np.linalg.norm(A @ x - b) <= 1e-5 * np.linalg.norm(b) + assert_allclose(x, b, atol=0, rtol=1e-8) + + rndm = np.random.RandomState(12345) + A = rndm.rand(30, 30) + b = 1e-6 * ones(30) + x, info = gmres(A, b, rtol=1e-7, restart=20) + assert np.linalg.norm(A @ x - b) > 1e-7 + + A = eye(2) + b = 1e-10 * ones(2) + x, info = gmres(A, b, rtol=1e-8, atol=0) + assert np.linalg.norm(A @ x - b) <= 1e-8 * np.linalg.norm(b) + + def test_defective_precond_breakdown(self): + # Breakdown due to defective preconditioner + M = np.eye(3) + M[2, 2] = 0 + + b = np.array([0, 1, 1]) + x = np.array([1, 0, 0]) + A = np.diag([2, 3, 4]) + + x, info = gmres(A, b, x0=x, M=M, rtol=1e-15, atol=0) + + # Should not return nans, nor terminate with false success + assert not np.isnan(x).any() + if info == 0: + assert np.linalg.norm(A @ x - b) <= 1e-15 * np.linalg.norm(b) + + # The solution should be OK outside null space of M + assert_allclose(M @ (A @ x), M @ b) + + def test_defective_matrix_breakdown(self): + # Breakdown due to defective matrix + A = np.array([[0, 1, 0], [1, 0, 0], [0, 0, 0]]) + b = np.array([1, 0, 1]) + rtol = 1e-8 + x, info = gmres(A, b, rtol=rtol, atol=0) + + # Should not return nans, nor terminate with false success + assert not np.isnan(x).any() + if info == 0: + assert np.linalg.norm(A @ x - b) <= rtol * np.linalg.norm(b) + + # The solution should be OK outside null space of A + assert_allclose(A @ (A @ x), A @ b) + + def test_callback_type(self): + # The legacy callback type changes meaning of 'maxiter' + np.random.seed(1) + A = np.random.rand(20, 20) + b = np.random.rand(20) + + cb_count = [0] + + def pr_norm_cb(r): + cb_count[0] += 1 + assert isinstance(r, float) + + def x_cb(x): + cb_count[0] += 1 + assert isinstance(x, np.ndarray) + + # 2 iterations is not enough to solve the problem + cb_count = [0] + x, info = gmres(A, b, rtol=1e-6, atol=0, callback=pr_norm_cb, + maxiter=2, restart=50) + assert info == 2 + assert cb_count[0] == 2 + + # With `callback_type` specified, no warning should be raised + cb_count = [0] + x, info = gmres(A, b, rtol=1e-6, atol=0, callback=pr_norm_cb, + maxiter=2, restart=50, callback_type='legacy') + assert info == 2 + assert cb_count[0] == 2 + + # 2 restart cycles is enough to solve the problem + cb_count = [0] + x, info = gmres(A, b, rtol=1e-6, atol=0, callback=pr_norm_cb, + maxiter=2, restart=50, callback_type='pr_norm') + assert info == 0 + assert cb_count[0] > 2 + + # 2 restart cycles is enough to solve the problem + cb_count = [0] + x, info = gmres(A, b, rtol=1e-6, atol=0, callback=x_cb, maxiter=2, + restart=50, callback_type='x') + assert info == 0 + assert cb_count[0] == 1 + + def test_callback_x_monotonic(self): + # Check that callback_type='x' gives monotonic norm decrease + np.random.seed(1) + A = np.random.rand(20, 20) + np.eye(20) + b = np.random.rand(20) + + prev_r = [np.inf] + count = [0] + + def x_cb(x): + r = np.linalg.norm(A @ x - b) + assert r <= prev_r[0] + prev_r[0] = r + count[0] += 1 + + x, info = gmres(A, b, rtol=1e-6, atol=0, callback=x_cb, maxiter=20, + restart=10, callback_type='x') + assert info == 20 + assert count[0] == 20 + + def test_restrt_dep(self): + with pytest.warns( + DeprecationWarning, + match="'gmres' keyword argument 'restrt'" + ): + gmres(np.array([1]), np.array([1]), restrt=10) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tfqmr.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tfqmr.py new file mode 100644 index 0000000000000000000000000000000000000000..3f9a33e0a874706f46b95c766c6cd636dde725f8 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/tfqmr.py @@ -0,0 +1,191 @@ +import numpy as np +from .iterative import _get_atol_rtol +from .utils import make_system +from scipy._lib.deprecation import _NoValue, _deprecate_positional_args + + +__all__ = ['tfqmr'] + + +@_deprecate_positional_args(version="1.14.0") +def tfqmr(A, b, x0=None, *, tol=_NoValue, maxiter=None, M=None, + callback=None, atol=None, rtol=1e-5, show=False): + """ + Use Transpose-Free Quasi-Minimal Residual iteration to solve ``Ax = b``. + + Parameters + ---------- + A : {sparse matrix, ndarray, LinearOperator} + The real or complex N-by-N matrix of the linear system. + Alternatively, `A` can be a linear operator which can + produce ``Ax`` using, e.g., + `scipy.sparse.linalg.LinearOperator`. + b : {ndarray} + Right hand side of the linear system. Has shape (N,) or (N,1). + x0 : {ndarray} + Starting guess for the solution. + rtol, atol : float, optional + Parameters for the convergence test. For convergence, + ``norm(b - A @ x) <= max(rtol*norm(b), atol)`` should be satisfied. + The default is ``rtol=1e-5``, the default for ``atol`` is ``rtol``. + + .. warning:: + + The default value for ``atol`` will be changed to ``0.0`` in + SciPy 1.14.0. + maxiter : int, optional + Maximum number of iterations. Iteration will stop after maxiter + steps even if the specified tolerance has not been achieved. + Default is ``min(10000, ndofs * 10)``, where ``ndofs = A.shape[0]``. + M : {sparse matrix, ndarray, LinearOperator} + Inverse of the preconditioner of A. M should approximate the + inverse of A and be easy to solve for (see Notes). Effective + preconditioning dramatically improves the rate of convergence, + which implies that fewer iterations are needed to reach a given + error tolerance. By default, no preconditioner is used. + callback : function, optional + User-supplied function to call after each iteration. It is called + as `callback(xk)`, where `xk` is the current solution vector. + show : bool, optional + Specify ``show = True`` to show the convergence, ``show = False`` is + to close the output of the convergence. + Default is `False`. + tol : float, optional, deprecated + + .. deprecated:: 1.12.0 + `tfqmr` keyword argument ``tol`` is deprecated in favor of ``rtol`` + and will be removed in SciPy 1.14.0. + + Returns + ------- + x : ndarray + The converged solution. + info : int + Provides convergence information: + + - 0 : successful exit + - >0 : convergence to tolerance not achieved, number of iterations + - <0 : illegal input or breakdown + + Notes + ----- + The Transpose-Free QMR algorithm is derived from the CGS algorithm. + However, unlike CGS, the convergence curves for the TFQMR method is + smoothed by computing a quasi minimization of the residual norm. The + implementation supports left preconditioner, and the "residual norm" + to compute in convergence criterion is actually an upper bound on the + actual residual norm ``||b - Axk||``. + + References + ---------- + .. [1] R. W. Freund, A Transpose-Free Quasi-Minimal Residual Algorithm for + Non-Hermitian Linear Systems, SIAM J. Sci. Comput., 14(2), 470-482, + 1993. + .. [2] Y. Saad, Iterative Methods for Sparse Linear Systems, 2nd edition, + SIAM, Philadelphia, 2003. + .. [3] C. T. Kelley, Iterative Methods for Linear and Nonlinear Equations, + number 16 in Frontiers in Applied Mathematics, SIAM, Philadelphia, + 1995. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import tfqmr + >>> A = csc_matrix([[3, 2, 0], [1, -1, 0], [0, 5, 1]], dtype=float) + >>> b = np.array([2, 4, -1], dtype=float) + >>> x, exitCode = tfqmr(A, b) + >>> print(exitCode) # 0 indicates successful convergence + 0 + >>> np.allclose(A.dot(x), b) + True + """ + + # Check data type + dtype = A.dtype + if np.issubdtype(dtype, np.int64): + dtype = float + A = A.astype(dtype) + if np.issubdtype(b.dtype, np.int64): + b = b.astype(dtype) + + A, M, x, b, postprocess = make_system(A, M, x0, b) + + # Check if the R.H.S is a zero vector + if np.linalg.norm(b) == 0.: + x = b.copy() + return (postprocess(x), 0) + + ndofs = A.shape[0] + if maxiter is None: + maxiter = min(10000, ndofs * 10) + + if x0 is None: + r = b.copy() + else: + r = b - A.matvec(x) + u = r + w = r.copy() + # Take rstar as b - Ax0, that is rstar := r = b - Ax0 mathematically + rstar = r + v = M.matvec(A.matvec(r)) + uhat = v + d = theta = eta = 0. + # at this point we know rstar == r, so rho is always real + rho = np.inner(rstar.conjugate(), r).real + rhoLast = rho + r0norm = np.sqrt(rho) + tau = r0norm + if r0norm == 0: + return (postprocess(x), 0) + + # we call this to get the right atol and raise warnings as necessary + atol, _ = _get_atol_rtol('tfqmr', r0norm, tol, atol, rtol) + + for iter in range(maxiter): + even = iter % 2 == 0 + if (even): + vtrstar = np.inner(rstar.conjugate(), v) + # Check breakdown + if vtrstar == 0.: + return (postprocess(x), -1) + alpha = rho / vtrstar + uNext = u - alpha * v # [1]-(5.6) + w -= alpha * uhat # [1]-(5.8) + d = u + (theta**2 / alpha) * eta * d # [1]-(5.5) + # [1]-(5.2) + theta = np.linalg.norm(w) / tau + c = np.sqrt(1. / (1 + theta**2)) + tau *= theta * c + # Calculate step and direction [1]-(5.4) + eta = (c**2) * alpha + z = M.matvec(d) + x += eta * z + + if callback is not None: + callback(x) + + # Convergence criterion + if tau * np.sqrt(iter+1) < atol: + if (show): + print("TFQMR: Linear solve converged due to reach TOL " + f"iterations {iter+1}") + return (postprocess(x), 0) + + if (not even): + # [1]-(5.7) + rho = np.inner(rstar.conjugate(), w) + beta = rho / rhoLast + u = w + beta * u + v = beta * uhat + (beta**2) * v + uhat = M.matvec(A.matvec(u)) + v += uhat + else: + uhat = M.matvec(A.matvec(uNext)) + u = uNext + rhoLast = rho + + if (show): + print("TFQMR: Linear solve not converged due to reach MAXIT " + f"iterations {iter+1}") + return (postprocess(x), maxiter) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/utils.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5da7679972191eca7585419095d02df32550f6f7 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_isolve/utils.py @@ -0,0 +1,127 @@ +__docformat__ = "restructuredtext en" + +__all__ = [] + + +from numpy import asanyarray, asarray, array, zeros + +from scipy.sparse.linalg._interface import aslinearoperator, LinearOperator, \ + IdentityOperator + +_coerce_rules = {('f','f'):'f', ('f','d'):'d', ('f','F'):'F', + ('f','D'):'D', ('d','f'):'d', ('d','d'):'d', + ('d','F'):'D', ('d','D'):'D', ('F','f'):'F', + ('F','d'):'D', ('F','F'):'F', ('F','D'):'D', + ('D','f'):'D', ('D','d'):'D', ('D','F'):'D', + ('D','D'):'D'} + + +def coerce(x,y): + if x not in 'fdFD': + x = 'd' + if y not in 'fdFD': + y = 'd' + return _coerce_rules[x,y] + + +def id(x): + return x + + +def make_system(A, M, x0, b): + """Make a linear system Ax=b + + Parameters + ---------- + A : LinearOperator + sparse or dense matrix (or any valid input to aslinearoperator) + M : {LinearOperator, Nones} + preconditioner + sparse or dense matrix (or any valid input to aslinearoperator) + x0 : {array_like, str, None} + initial guess to iterative method. + ``x0 = 'Mb'`` means using the nonzero initial guess ``M @ b``. + Default is `None`, which means using the zero initial guess. + b : array_like + right hand side + + Returns + ------- + (A, M, x, b, postprocess) + A : LinearOperator + matrix of the linear system + M : LinearOperator + preconditioner + x : rank 1 ndarray + initial guess + b : rank 1 ndarray + right hand side + postprocess : function + converts the solution vector to the appropriate + type and dimensions (e.g. (N,1) matrix) + + """ + A_ = A + A = aslinearoperator(A) + + if A.shape[0] != A.shape[1]: + raise ValueError(f'expected square matrix, but got shape={(A.shape,)}') + + N = A.shape[0] + + b = asanyarray(b) + + if not (b.shape == (N,1) or b.shape == (N,)): + raise ValueError(f'shapes of A {A.shape} and b {b.shape} are ' + 'incompatible') + + if b.dtype.char not in 'fdFD': + b = b.astype('d') # upcast non-FP types to double + + def postprocess(x): + return x + + if hasattr(A,'dtype'): + xtype = A.dtype.char + else: + xtype = A.matvec(b).dtype.char + xtype = coerce(xtype, b.dtype.char) + + b = asarray(b,dtype=xtype) # make b the same type as x + b = b.ravel() + + # process preconditioner + if M is None: + if hasattr(A_,'psolve'): + psolve = A_.psolve + else: + psolve = id + if hasattr(A_,'rpsolve'): + rpsolve = A_.rpsolve + else: + rpsolve = id + if psolve is id and rpsolve is id: + M = IdentityOperator(shape=A.shape, dtype=A.dtype) + else: + M = LinearOperator(A.shape, matvec=psolve, rmatvec=rpsolve, + dtype=A.dtype) + else: + M = aslinearoperator(M) + if A.shape != M.shape: + raise ValueError('matrix and preconditioner have different shapes') + + # set initial guess + if x0 is None: + x = zeros(N, dtype=xtype) + elif isinstance(x0, str): + if x0 == 'Mb': # use nonzero initial guess ``M @ b`` + bCopy = b.copy() + x = M.matvec(bCopy) + else: + x = array(x0, dtype=xtype) + if not (x.shape == (N, 1) or x.shape == (N,)): + raise ValueError(f'shapes of A {A.shape} and ' + f'x0 {x.shape} are incompatible') + x = x.ravel() + + return A, M, x, b, postprocess diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_matfuncs.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_matfuncs.py new file mode 100644 index 0000000000000000000000000000000000000000..4da553a6214a7c0c772a7316f8f388784024b004 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_matfuncs.py @@ -0,0 +1,940 @@ +""" +Sparse matrix functions +""" + +# +# Authors: Travis Oliphant, March 2002 +# Anthony Scopatz, August 2012 (Sparse Updates) +# Jake Vanderplas, August 2012 (Sparse Updates) +# + +__all__ = ['expm', 'inv', 'matrix_power'] + +import numpy as np +from scipy.linalg._basic import solve, solve_triangular + +from scipy.sparse._base import issparse +from scipy.sparse.linalg import spsolve +from scipy.sparse._sputils import is_pydata_spmatrix, isintlike + +import scipy.sparse +import scipy.sparse.linalg +from scipy.sparse.linalg._interface import LinearOperator +from scipy.sparse._construct import eye + +from ._expm_multiply import _ident_like, _exact_1_norm as _onenorm + + +UPPER_TRIANGULAR = 'upper_triangular' + + +def inv(A): + """ + Compute the inverse of a sparse matrix + + Parameters + ---------- + A : (M, M) sparse matrix + square matrix to be inverted + + Returns + ------- + Ainv : (M, M) sparse matrix + inverse of `A` + + Notes + ----- + This computes the sparse inverse of `A`. If the inverse of `A` is expected + to be non-sparse, it will likely be faster to convert `A` to dense and use + `scipy.linalg.inv`. + + Examples + -------- + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import inv + >>> A = csc_matrix([[1., 0.], [1., 2.]]) + >>> Ainv = inv(A) + >>> Ainv + <2x2 sparse matrix of type '' + with 3 stored elements in Compressed Sparse Column format> + >>> A.dot(Ainv) + <2x2 sparse matrix of type '' + with 2 stored elements in Compressed Sparse Column format> + >>> A.dot(Ainv).toarray() + array([[ 1., 0.], + [ 0., 1.]]) + + .. versionadded:: 0.12.0 + + """ + # Check input + if not (scipy.sparse.issparse(A) or is_pydata_spmatrix(A)): + raise TypeError('Input must be a sparse matrix') + + # Use sparse direct solver to solve "AX = I" accurately + I = _ident_like(A) + Ainv = spsolve(A, I) + return Ainv + + +def _onenorm_matrix_power_nnm(A, p): + """ + Compute the 1-norm of a non-negative integer power of a non-negative matrix. + + Parameters + ---------- + A : a square ndarray or matrix or sparse matrix + Input matrix with non-negative entries. + p : non-negative integer + The power to which the matrix is to be raised. + + Returns + ------- + out : float + The 1-norm of the matrix power p of A. + + """ + # Check input + if int(p) != p or p < 0: + raise ValueError('expected non-negative integer p') + p = int(p) + if len(A.shape) != 2 or A.shape[0] != A.shape[1]: + raise ValueError('expected A to be like a square matrix') + + # Explicitly make a column vector so that this works when A is a + # numpy matrix (in addition to ndarray and sparse matrix). + v = np.ones((A.shape[0], 1), dtype=float) + M = A.T + for i in range(p): + v = M.dot(v) + return np.max(v) + + +def _is_upper_triangular(A): + # This function could possibly be of wider interest. + if issparse(A): + lower_part = scipy.sparse.tril(A, -1) + # Check structural upper triangularity, + # then coincidental upper triangularity if needed. + return lower_part.nnz == 0 or lower_part.count_nonzero() == 0 + elif is_pydata_spmatrix(A): + import sparse + lower_part = sparse.tril(A, -1) + return lower_part.nnz == 0 + else: + return not np.tril(A, -1).any() + + +def _smart_matrix_product(A, B, alpha=None, structure=None): + """ + A matrix product that knows about sparse and structured matrices. + + Parameters + ---------- + A : 2d ndarray + First matrix. + B : 2d ndarray + Second matrix. + alpha : float + The matrix product will be scaled by this constant. + structure : str, optional + A string describing the structure of both matrices `A` and `B`. + Only `upper_triangular` is currently supported. + + Returns + ------- + M : 2d ndarray + Matrix product of A and B. + + """ + if len(A.shape) != 2: + raise ValueError('expected A to be a rectangular matrix') + if len(B.shape) != 2: + raise ValueError('expected B to be a rectangular matrix') + f = None + if structure == UPPER_TRIANGULAR: + if (not issparse(A) and not issparse(B) + and not is_pydata_spmatrix(A) and not is_pydata_spmatrix(B)): + f, = scipy.linalg.get_blas_funcs(('trmm',), (A, B)) + if f is not None: + if alpha is None: + alpha = 1. + out = f(alpha, A, B) + else: + if alpha is None: + out = A.dot(B) + else: + out = alpha * A.dot(B) + return out + + +class MatrixPowerOperator(LinearOperator): + + def __init__(self, A, p, structure=None): + if A.ndim != 2 or A.shape[0] != A.shape[1]: + raise ValueError('expected A to be like a square matrix') + if p < 0: + raise ValueError('expected p to be a non-negative integer') + self._A = A + self._p = p + self._structure = structure + self.dtype = A.dtype + self.ndim = A.ndim + self.shape = A.shape + + def _matvec(self, x): + for i in range(self._p): + x = self._A.dot(x) + return x + + def _rmatvec(self, x): + A_T = self._A.T + x = x.ravel() + for i in range(self._p): + x = A_T.dot(x) + return x + + def _matmat(self, X): + for i in range(self._p): + X = _smart_matrix_product(self._A, X, structure=self._structure) + return X + + @property + def T(self): + return MatrixPowerOperator(self._A.T, self._p) + + +class ProductOperator(LinearOperator): + """ + For now, this is limited to products of multiple square matrices. + """ + + def __init__(self, *args, **kwargs): + self._structure = kwargs.get('structure', None) + for A in args: + if len(A.shape) != 2 or A.shape[0] != A.shape[1]: + raise ValueError( + 'For now, the ProductOperator implementation is ' + 'limited to the product of multiple square matrices.') + if args: + n = args[0].shape[0] + for A in args: + for d in A.shape: + if d != n: + raise ValueError( + 'The square matrices of the ProductOperator ' + 'must all have the same shape.') + self.shape = (n, n) + self.ndim = len(self.shape) + self.dtype = np.result_type(*[x.dtype for x in args]) + self._operator_sequence = args + + def _matvec(self, x): + for A in reversed(self._operator_sequence): + x = A.dot(x) + return x + + def _rmatvec(self, x): + x = x.ravel() + for A in self._operator_sequence: + x = A.T.dot(x) + return x + + def _matmat(self, X): + for A in reversed(self._operator_sequence): + X = _smart_matrix_product(A, X, structure=self._structure) + return X + + @property + def T(self): + T_args = [A.T for A in reversed(self._operator_sequence)] + return ProductOperator(*T_args) + + +def _onenormest_matrix_power(A, p, + t=2, itmax=5, compute_v=False, compute_w=False, structure=None): + """ + Efficiently estimate the 1-norm of A^p. + + Parameters + ---------- + A : ndarray + Matrix whose 1-norm of a power is to be computed. + p : int + Non-negative integer power. + t : int, optional + A positive parameter controlling the tradeoff between + accuracy versus time and memory usage. + Larger values take longer and use more memory + but give more accurate output. + itmax : int, optional + Use at most this many iterations. + compute_v : bool, optional + Request a norm-maximizing linear operator input vector if True. + compute_w : bool, optional + Request a norm-maximizing linear operator output vector if True. + + Returns + ------- + est : float + An underestimate of the 1-norm of the sparse matrix. + v : ndarray, optional + The vector such that ||Av||_1 == est*||v||_1. + It can be thought of as an input to the linear operator + that gives an output with particularly large norm. + w : ndarray, optional + The vector Av which has relatively large 1-norm. + It can be thought of as an output of the linear operator + that is relatively large in norm compared to the input. + + """ + return scipy.sparse.linalg.onenormest( + MatrixPowerOperator(A, p, structure=structure)) + + +def _onenormest_product(operator_seq, + t=2, itmax=5, compute_v=False, compute_w=False, structure=None): + """ + Efficiently estimate the 1-norm of the matrix product of the args. + + Parameters + ---------- + operator_seq : linear operator sequence + Matrices whose 1-norm of product is to be computed. + t : int, optional + A positive parameter controlling the tradeoff between + accuracy versus time and memory usage. + Larger values take longer and use more memory + but give more accurate output. + itmax : int, optional + Use at most this many iterations. + compute_v : bool, optional + Request a norm-maximizing linear operator input vector if True. + compute_w : bool, optional + Request a norm-maximizing linear operator output vector if True. + structure : str, optional + A string describing the structure of all operators. + Only `upper_triangular` is currently supported. + + Returns + ------- + est : float + An underestimate of the 1-norm of the sparse matrix. + v : ndarray, optional + The vector such that ||Av||_1 == est*||v||_1. + It can be thought of as an input to the linear operator + that gives an output with particularly large norm. + w : ndarray, optional + The vector Av which has relatively large 1-norm. + It can be thought of as an output of the linear operator + that is relatively large in norm compared to the input. + + """ + return scipy.sparse.linalg.onenormest( + ProductOperator(*operator_seq, structure=structure)) + + +class _ExpmPadeHelper: + """ + Help lazily evaluate a matrix exponential. + + The idea is to not do more work than we need for high expm precision, + so we lazily compute matrix powers and store or precompute + other properties of the matrix. + + """ + + def __init__(self, A, structure=None, use_exact_onenorm=False): + """ + Initialize the object. + + Parameters + ---------- + A : a dense or sparse square numpy matrix or ndarray + The matrix to be exponentiated. + structure : str, optional + A string describing the structure of matrix `A`. + Only `upper_triangular` is currently supported. + use_exact_onenorm : bool, optional + If True then only the exact one-norm of matrix powers and products + will be used. Otherwise, the one-norm of powers and products + may initially be estimated. + """ + self.A = A + self._A2 = None + self._A4 = None + self._A6 = None + self._A8 = None + self._A10 = None + self._d4_exact = None + self._d6_exact = None + self._d8_exact = None + self._d10_exact = None + self._d4_approx = None + self._d6_approx = None + self._d8_approx = None + self._d10_approx = None + self.ident = _ident_like(A) + self.structure = structure + self.use_exact_onenorm = use_exact_onenorm + + @property + def A2(self): + if self._A2 is None: + self._A2 = _smart_matrix_product( + self.A, self.A, structure=self.structure) + return self._A2 + + @property + def A4(self): + if self._A4 is None: + self._A4 = _smart_matrix_product( + self.A2, self.A2, structure=self.structure) + return self._A4 + + @property + def A6(self): + if self._A6 is None: + self._A6 = _smart_matrix_product( + self.A4, self.A2, structure=self.structure) + return self._A6 + + @property + def A8(self): + if self._A8 is None: + self._A8 = _smart_matrix_product( + self.A6, self.A2, structure=self.structure) + return self._A8 + + @property + def A10(self): + if self._A10 is None: + self._A10 = _smart_matrix_product( + self.A4, self.A6, structure=self.structure) + return self._A10 + + @property + def d4_tight(self): + if self._d4_exact is None: + self._d4_exact = _onenorm(self.A4)**(1/4.) + return self._d4_exact + + @property + def d6_tight(self): + if self._d6_exact is None: + self._d6_exact = _onenorm(self.A6)**(1/6.) + return self._d6_exact + + @property + def d8_tight(self): + if self._d8_exact is None: + self._d8_exact = _onenorm(self.A8)**(1/8.) + return self._d8_exact + + @property + def d10_tight(self): + if self._d10_exact is None: + self._d10_exact = _onenorm(self.A10)**(1/10.) + return self._d10_exact + + @property + def d4_loose(self): + if self.use_exact_onenorm: + return self.d4_tight + if self._d4_exact is not None: + return self._d4_exact + else: + if self._d4_approx is None: + self._d4_approx = _onenormest_matrix_power(self.A2, 2, + structure=self.structure)**(1/4.) + return self._d4_approx + + @property + def d6_loose(self): + if self.use_exact_onenorm: + return self.d6_tight + if self._d6_exact is not None: + return self._d6_exact + else: + if self._d6_approx is None: + self._d6_approx = _onenormest_matrix_power(self.A2, 3, + structure=self.structure)**(1/6.) + return self._d6_approx + + @property + def d8_loose(self): + if self.use_exact_onenorm: + return self.d8_tight + if self._d8_exact is not None: + return self._d8_exact + else: + if self._d8_approx is None: + self._d8_approx = _onenormest_matrix_power(self.A4, 2, + structure=self.structure)**(1/8.) + return self._d8_approx + + @property + def d10_loose(self): + if self.use_exact_onenorm: + return self.d10_tight + if self._d10_exact is not None: + return self._d10_exact + else: + if self._d10_approx is None: + self._d10_approx = _onenormest_product((self.A4, self.A6), + structure=self.structure)**(1/10.) + return self._d10_approx + + def pade3(self): + b = (120., 60., 12., 1.) + U = _smart_matrix_product(self.A, + b[3]*self.A2 + b[1]*self.ident, + structure=self.structure) + V = b[2]*self.A2 + b[0]*self.ident + return U, V + + def pade5(self): + b = (30240., 15120., 3360., 420., 30., 1.) + U = _smart_matrix_product(self.A, + b[5]*self.A4 + b[3]*self.A2 + b[1]*self.ident, + structure=self.structure) + V = b[4]*self.A4 + b[2]*self.A2 + b[0]*self.ident + return U, V + + def pade7(self): + b = (17297280., 8648640., 1995840., 277200., 25200., 1512., 56., 1.) + U = _smart_matrix_product(self.A, + b[7]*self.A6 + b[5]*self.A4 + b[3]*self.A2 + b[1]*self.ident, + structure=self.structure) + V = b[6]*self.A6 + b[4]*self.A4 + b[2]*self.A2 + b[0]*self.ident + return U, V + + def pade9(self): + b = (17643225600., 8821612800., 2075673600., 302702400., 30270240., + 2162160., 110880., 3960., 90., 1.) + U = _smart_matrix_product(self.A, + (b[9]*self.A8 + b[7]*self.A6 + b[5]*self.A4 + + b[3]*self.A2 + b[1]*self.ident), + structure=self.structure) + V = (b[8]*self.A8 + b[6]*self.A6 + b[4]*self.A4 + + b[2]*self.A2 + b[0]*self.ident) + return U, V + + def pade13_scaled(self, s): + b = (64764752532480000., 32382376266240000., 7771770303897600., + 1187353796428800., 129060195264000., 10559470521600., + 670442572800., 33522128640., 1323241920., 40840800., 960960., + 16380., 182., 1.) + B = self.A * 2**-s + B2 = self.A2 * 2**(-2*s) + B4 = self.A4 * 2**(-4*s) + B6 = self.A6 * 2**(-6*s) + U2 = _smart_matrix_product(B6, + b[13]*B6 + b[11]*B4 + b[9]*B2, + structure=self.structure) + U = _smart_matrix_product(B, + (U2 + b[7]*B6 + b[5]*B4 + + b[3]*B2 + b[1]*self.ident), + structure=self.structure) + V2 = _smart_matrix_product(B6, + b[12]*B6 + b[10]*B4 + b[8]*B2, + structure=self.structure) + V = V2 + b[6]*B6 + b[4]*B4 + b[2]*B2 + b[0]*self.ident + return U, V + + +def expm(A): + """ + Compute the matrix exponential using Pade approximation. + + Parameters + ---------- + A : (M,M) array_like or sparse matrix + 2D Array or Matrix (sparse or dense) to be exponentiated + + Returns + ------- + expA : (M,M) ndarray + Matrix exponential of `A` + + Notes + ----- + This is algorithm (6.1) which is a simplification of algorithm (5.1). + + .. versionadded:: 0.12.0 + + References + ---------- + .. [1] Awad H. Al-Mohy and Nicholas J. Higham (2009) + "A New Scaling and Squaring Algorithm for the Matrix Exponential." + SIAM Journal on Matrix Analysis and Applications. + 31 (3). pp. 970-989. ISSN 1095-7162 + + Examples + -------- + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import expm + >>> A = csc_matrix([[1, 0, 0], [0, 2, 0], [0, 0, 3]]) + >>> A.toarray() + array([[1, 0, 0], + [0, 2, 0], + [0, 0, 3]], dtype=int64) + >>> Aexp = expm(A) + >>> Aexp + <3x3 sparse matrix of type '' + with 3 stored elements in Compressed Sparse Column format> + >>> Aexp.toarray() + array([[ 2.71828183, 0. , 0. ], + [ 0. , 7.3890561 , 0. ], + [ 0. , 0. , 20.08553692]]) + """ + return _expm(A, use_exact_onenorm='auto') + + +def _expm(A, use_exact_onenorm): + # Core of expm, separated to allow testing exact and approximate + # algorithms. + + # Avoid indiscriminate asarray() to allow sparse or other strange arrays. + if isinstance(A, (list, tuple, np.matrix)): + A = np.asarray(A) + if len(A.shape) != 2 or A.shape[0] != A.shape[1]: + raise ValueError('expected a square matrix') + + # gracefully handle size-0 input, + # carefully handling sparse scenario + if A.shape == (0, 0): + out = np.zeros([0, 0], dtype=A.dtype) + if issparse(A) or is_pydata_spmatrix(A): + return A.__class__(out) + return out + + # Trivial case + if A.shape == (1, 1): + out = [[np.exp(A[0, 0])]] + + # Avoid indiscriminate casting to ndarray to + # allow for sparse or other strange arrays + if issparse(A) or is_pydata_spmatrix(A): + return A.__class__(out) + + return np.array(out) + + # Ensure input is of float type, to avoid integer overflows etc. + if ((isinstance(A, np.ndarray) or issparse(A) or is_pydata_spmatrix(A)) + and not np.issubdtype(A.dtype, np.inexact)): + A = A.astype(float) + + # Detect upper triangularity. + structure = UPPER_TRIANGULAR if _is_upper_triangular(A) else None + + if use_exact_onenorm == "auto": + # Hardcode a matrix order threshold for exact vs. estimated one-norms. + use_exact_onenorm = A.shape[0] < 200 + + # Track functions of A to help compute the matrix exponential. + h = _ExpmPadeHelper( + A, structure=structure, use_exact_onenorm=use_exact_onenorm) + + # Try Pade order 3. + eta_1 = max(h.d4_loose, h.d6_loose) + if eta_1 < 1.495585217958292e-002 and _ell(h.A, 3) == 0: + U, V = h.pade3() + return _solve_P_Q(U, V, structure=structure) + + # Try Pade order 5. + eta_2 = max(h.d4_tight, h.d6_loose) + if eta_2 < 2.539398330063230e-001 and _ell(h.A, 5) == 0: + U, V = h.pade5() + return _solve_P_Q(U, V, structure=structure) + + # Try Pade orders 7 and 9. + eta_3 = max(h.d6_tight, h.d8_loose) + if eta_3 < 9.504178996162932e-001 and _ell(h.A, 7) == 0: + U, V = h.pade7() + return _solve_P_Q(U, V, structure=structure) + if eta_3 < 2.097847961257068e+000 and _ell(h.A, 9) == 0: + U, V = h.pade9() + return _solve_P_Q(U, V, structure=structure) + + # Use Pade order 13. + eta_4 = max(h.d8_loose, h.d10_loose) + eta_5 = min(eta_3, eta_4) + theta_13 = 4.25 + + # Choose smallest s>=0 such that 2**(-s) eta_5 <= theta_13 + if eta_5 == 0: + # Nilpotent special case + s = 0 + else: + s = max(int(np.ceil(np.log2(eta_5 / theta_13))), 0) + s = s + _ell(2**-s * h.A, 13) + U, V = h.pade13_scaled(s) + X = _solve_P_Q(U, V, structure=structure) + if structure == UPPER_TRIANGULAR: + # Invoke Code Fragment 2.1. + X = _fragment_2_1(X, h.A, s) + else: + # X = r_13(A)^(2^s) by repeated squaring. + for i in range(s): + X = X.dot(X) + return X + + +def _solve_P_Q(U, V, structure=None): + """ + A helper function for expm_2009. + + Parameters + ---------- + U : ndarray + Pade numerator. + V : ndarray + Pade denominator. + structure : str, optional + A string describing the structure of both matrices `U` and `V`. + Only `upper_triangular` is currently supported. + + Notes + ----- + The `structure` argument is inspired by similar args + for theano and cvxopt functions. + + """ + P = U + V + Q = -U + V + if issparse(U) or is_pydata_spmatrix(U): + return spsolve(Q, P) + elif structure is None: + return solve(Q, P) + elif structure == UPPER_TRIANGULAR: + return solve_triangular(Q, P) + else: + raise ValueError('unsupported matrix structure: ' + str(structure)) + + +def _exp_sinch(a, x): + """ + Stably evaluate exp(a)*sinh(x)/x + + Notes + ----- + The strategy of falling back to a sixth order Taylor expansion + was suggested by the Spallation Neutron Source docs + which was found on the internet by google search. + http://www.ornl.gov/~t6p/resources/xal/javadoc/gov/sns/tools/math/ElementaryFunction.html + The details of the cutoff point and the Horner-like evaluation + was picked without reference to anything in particular. + + Note that sinch is not currently implemented in scipy.special, + whereas the "engineer's" definition of sinc is implemented. + The implementation of sinc involves a scaling factor of pi + that distinguishes it from the "mathematician's" version of sinc. + + """ + + # If x is small then use sixth order Taylor expansion. + # How small is small? I am using the point where the relative error + # of the approximation is less than 1e-14. + # If x is large then directly evaluate sinh(x) / x. + if abs(x) < 0.0135: + x2 = x*x + return np.exp(a) * (1 + (x2/6.)*(1 + (x2/20.)*(1 + (x2/42.)))) + else: + return (np.exp(a + x) - np.exp(a - x)) / (2*x) + + +def _eq_10_42(lam_1, lam_2, t_12): + """ + Equation (10.42) of Functions of Matrices: Theory and Computation. + + Notes + ----- + This is a helper function for _fragment_2_1 of expm_2009. + Equation (10.42) is on page 251 in the section on Schur algorithms. + In particular, section 10.4.3 explains the Schur-Parlett algorithm. + expm([[lam_1, t_12], [0, lam_1]) + = + [[exp(lam_1), t_12*exp((lam_1 + lam_2)/2)*sinch((lam_1 - lam_2)/2)], + [0, exp(lam_2)] + """ + + # The plain formula t_12 * (exp(lam_2) - exp(lam_2)) / (lam_2 - lam_1) + # apparently suffers from cancellation, according to Higham's textbook. + # A nice implementation of sinch, defined as sinh(x)/x, + # will apparently work around the cancellation. + a = 0.5 * (lam_1 + lam_2) + b = 0.5 * (lam_1 - lam_2) + return t_12 * _exp_sinch(a, b) + + +def _fragment_2_1(X, T, s): + """ + A helper function for expm_2009. + + Notes + ----- + The argument X is modified in-place, but this modification is not the same + as the returned value of the function. + This function also takes pains to do things in ways that are compatible + with sparse matrices, for example by avoiding fancy indexing + and by using methods of the matrices whenever possible instead of + using functions of the numpy or scipy libraries themselves. + + """ + # Form X = r_m(2^-s T) + # Replace diag(X) by exp(2^-s diag(T)). + n = X.shape[0] + diag_T = np.ravel(T.diagonal().copy()) + + # Replace diag(X) by exp(2^-s diag(T)). + scale = 2 ** -s + exp_diag = np.exp(scale * diag_T) + for k in range(n): + X[k, k] = exp_diag[k] + + for i in range(s-1, -1, -1): + X = X.dot(X) + + # Replace diag(X) by exp(2^-i diag(T)). + scale = 2 ** -i + exp_diag = np.exp(scale * diag_T) + for k in range(n): + X[k, k] = exp_diag[k] + + # Replace (first) superdiagonal of X by explicit formula + # for superdiagonal of exp(2^-i T) from Eq (10.42) of + # the author's 2008 textbook + # Functions of Matrices: Theory and Computation. + for k in range(n-1): + lam_1 = scale * diag_T[k] + lam_2 = scale * diag_T[k+1] + t_12 = scale * T[k, k+1] + value = _eq_10_42(lam_1, lam_2, t_12) + X[k, k+1] = value + + # Return the updated X matrix. + return X + + +def _ell(A, m): + """ + A helper function for expm_2009. + + Parameters + ---------- + A : linear operator + A linear operator whose norm of power we care about. + m : int + The power of the linear operator + + Returns + ------- + value : int + A value related to a bound. + + """ + if len(A.shape) != 2 or A.shape[0] != A.shape[1]: + raise ValueError('expected A to be like a square matrix') + + # The c_i are explained in (2.2) and (2.6) of the 2005 expm paper. + # They are coefficients of terms of a generating function series expansion. + c_i = {3: 100800., + 5: 10059033600., + 7: 4487938430976000., + 9: 5914384781877411840000., + 13: 113250775606021113483283660800000000. + } + abs_c_recip = c_i[m] + + # This is explained after Eq. (1.2) of the 2009 expm paper. + # It is the "unit roundoff" of IEEE double precision arithmetic. + u = 2**-53 + + # Compute the one-norm of matrix power p of abs(A). + A_abs_onenorm = _onenorm_matrix_power_nnm(abs(A), 2*m + 1) + + # Treat zero norm as a special case. + if not A_abs_onenorm: + return 0 + + alpha = A_abs_onenorm / (_onenorm(A) * abs_c_recip) + log2_alpha_div_u = np.log2(alpha/u) + value = int(np.ceil(log2_alpha_div_u / (2 * m))) + return max(value, 0) + +def matrix_power(A, power): + """ + Raise a square matrix to the integer power, `power`. + + For non-negative integers, ``A**power`` is computed using repeated + matrix multiplications. Negative integers are not supported. + + Parameters + ---------- + A : (M, M) square sparse array or matrix + sparse array that will be raised to power `power` + power : int + Exponent used to raise sparse array `A` + + Returns + ------- + A**power : (M, M) sparse array or matrix + The output matrix will be the same shape as A, and will preserve + the class of A, but the format of the output may be changed. + + Notes + ----- + This uses a recursive implementation of the matrix power. For computing + the matrix power using a reasonably large `power`, this may be less efficient + than computing the product directly, using A @ A @ ... @ A. + This is contingent upon the number of nonzero entries in the matrix. + + .. versionadded:: 1.12.0 + + Examples + -------- + >>> from scipy import sparse + >>> A = sparse.csc_array([[0,1,0],[1,0,1],[0,1,0]]) + >>> A.todense() + array([[0, 1, 0], + [1, 0, 1], + [0, 1, 0]]) + >>> (A @ A).todense() + array([[1, 0, 1], + [0, 2, 0], + [1, 0, 1]]) + >>> A2 = sparse.linalg.matrix_power(A, 2) + >>> A2.todense() + array([[1, 0, 1], + [0, 2, 0], + [1, 0, 1]]) + >>> A4 = sparse.linalg.matrix_power(A, 4) + >>> A4.todense() + array([[2, 0, 2], + [0, 4, 0], + [2, 0, 2]]) + + """ + M, N = A.shape + if M != N: + raise TypeError('sparse matrix is not square') + + if isintlike(power): + power = int(power) + if power < 0: + raise ValueError('exponent must be >= 0') + + if power == 0: + return eye(M, dtype=A.dtype) + + if power == 1: + return A.copy() + + tmp = matrix_power(A, power // 2) + if power % 2: + return A @ tmp @ tmp + else: + return tmp @ tmp + else: + raise ValueError("exponent must be an integer") diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_norm.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_norm.py new file mode 100644 index 0000000000000000000000000000000000000000..a81059548b86f894e539cd443674ee2d0a564c5e --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_norm.py @@ -0,0 +1,193 @@ +"""Sparse matrix norms. + +""" +import numpy as np +from scipy.sparse import issparse +from scipy.sparse.linalg import svds +import scipy.sparse as sp + +from numpy import sqrt, abs + +__all__ = ['norm'] + + +def _sparse_frobenius_norm(x): + data = sp._sputils._todata(x) + return np.linalg.norm(data) + + +def norm(x, ord=None, axis=None): + """ + Norm of a sparse matrix + + This function is able to return one of seven different matrix norms, + depending on the value of the ``ord`` parameter. + + Parameters + ---------- + x : a sparse matrix + Input sparse matrix. + ord : {non-zero int, inf, -inf, 'fro'}, optional + Order of the norm (see table under ``Notes``). inf means numpy's + `inf` object. + axis : {int, 2-tuple of ints, None}, optional + If `axis` is an integer, it specifies the axis of `x` along which to + compute the vector norms. If `axis` is a 2-tuple, it specifies the + axes that hold 2-D matrices, and the matrix norms of these matrices + are computed. If `axis` is None then either a vector norm (when `x` + is 1-D) or a matrix norm (when `x` is 2-D) is returned. + + Returns + ------- + n : float or ndarray + + Notes + ----- + Some of the ord are not implemented because some associated functions like, + _multi_svd_norm, are not yet available for sparse matrix. + + This docstring is modified based on numpy.linalg.norm. + https://github.com/numpy/numpy/blob/main/numpy/linalg/linalg.py + + The following norms can be calculated: + + ===== ============================ + ord norm for sparse matrices + ===== ============================ + None Frobenius norm + 'fro' Frobenius norm + inf max(sum(abs(x), axis=1)) + -inf min(sum(abs(x), axis=1)) + 0 abs(x).sum(axis=axis) + 1 max(sum(abs(x), axis=0)) + -1 min(sum(abs(x), axis=0)) + 2 Spectral norm (the largest singular value) + -2 Not implemented + other Not implemented + ===== ============================ + + The Frobenius norm is given by [1]_: + + :math:`||A||_F = [\\sum_{i,j} abs(a_{i,j})^2]^{1/2}` + + References + ---------- + .. [1] G. H. Golub and C. F. Van Loan, *Matrix Computations*, + Baltimore, MD, Johns Hopkins University Press, 1985, pg. 15 + + Examples + -------- + >>> from scipy.sparse import * + >>> import numpy as np + >>> from scipy.sparse.linalg import norm + >>> a = np.arange(9) - 4 + >>> a + array([-4, -3, -2, -1, 0, 1, 2, 3, 4]) + >>> b = a.reshape((3, 3)) + >>> b + array([[-4, -3, -2], + [-1, 0, 1], + [ 2, 3, 4]]) + + >>> b = csr_matrix(b) + >>> norm(b) + 7.745966692414834 + >>> norm(b, 'fro') + 7.745966692414834 + >>> norm(b, np.inf) + 9 + >>> norm(b, -np.inf) + 2 + >>> norm(b, 1) + 7 + >>> norm(b, -1) + 6 + + The matrix 2-norm or the spectral norm is the largest singular + value, computed approximately and with limitations. + + >>> b = diags([-1, 1], [0, 1], shape=(9, 10)) + >>> norm(b, 2) + 1.9753... + """ + if not issparse(x): + raise TypeError("input is not sparse. use numpy.linalg.norm") + + # Check the default case first and handle it immediately. + if axis is None and ord in (None, 'fro', 'f'): + return _sparse_frobenius_norm(x) + + # Some norms require functions that are not implemented for all types. + x = x.tocsr() + + if axis is None: + axis = (0, 1) + elif not isinstance(axis, tuple): + msg = "'axis' must be None, an integer or a tuple of integers" + try: + int_axis = int(axis) + except TypeError as e: + raise TypeError(msg) from e + if axis != int_axis: + raise TypeError(msg) + axis = (int_axis,) + + nd = 2 + if len(axis) == 2: + row_axis, col_axis = axis + if not (-nd <= row_axis < nd and -nd <= col_axis < nd): + message = f'Invalid axis {axis!r} for an array with shape {x.shape!r}' + raise ValueError(message) + if row_axis % nd == col_axis % nd: + raise ValueError('Duplicate axes given.') + if ord == 2: + # Only solver="lobpcg" supports all numpy dtypes + _, s, _ = svds(x, k=1, solver="lobpcg") + return s[0] + elif ord == -2: + raise NotImplementedError + #return _multi_svd_norm(x, row_axis, col_axis, amin) + elif ord == 1: + return abs(x).sum(axis=row_axis).max(axis=col_axis)[0,0] + elif ord == np.inf: + return abs(x).sum(axis=col_axis).max(axis=row_axis)[0,0] + elif ord == -1: + return abs(x).sum(axis=row_axis).min(axis=col_axis)[0,0] + elif ord == -np.inf: + return abs(x).sum(axis=col_axis).min(axis=row_axis)[0,0] + elif ord in (None, 'f', 'fro'): + # The axis order does not matter for this norm. + return _sparse_frobenius_norm(x) + else: + raise ValueError("Invalid norm order for matrices.") + elif len(axis) == 1: + a, = axis + if not (-nd <= a < nd): + message = f'Invalid axis {axis!r} for an array with shape {x.shape!r}' + raise ValueError(message) + if ord == np.inf: + M = abs(x).max(axis=a) + elif ord == -np.inf: + M = abs(x).min(axis=a) + elif ord == 0: + # Zero norm + M = (x != 0).sum(axis=a) + elif ord == 1: + # special case for speedup + M = abs(x).sum(axis=a) + elif ord in (2, None): + M = sqrt(abs(x).power(2).sum(axis=a)) + else: + try: + ord + 1 + except TypeError as e: + raise ValueError('Invalid norm order for vectors.') from e + M = np.power(abs(x).power(ord).sum(axis=a), 1 / ord) + if hasattr(M, 'toarray'): + return M.toarray().ravel() + elif hasattr(M, 'A'): + return M.A.ravel() + else: + return M.ravel() + else: + raise ValueError("Improper number of dimensions to norm.") diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_onenormest.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_onenormest.py new file mode 100644 index 0000000000000000000000000000000000000000..708fbc625a13ea394fd348322ba4b4218d1e4c18 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_onenormest.py @@ -0,0 +1,467 @@ +"""Sparse block 1-norm estimator. +""" + +import numpy as np +from scipy.sparse.linalg import aslinearoperator + + +__all__ = ['onenormest'] + + +def onenormest(A, t=2, itmax=5, compute_v=False, compute_w=False): + """ + Compute a lower bound of the 1-norm of a sparse matrix. + + Parameters + ---------- + A : ndarray or other linear operator + A linear operator that can be transposed and that can + produce matrix products. + t : int, optional + A positive parameter controlling the tradeoff between + accuracy versus time and memory usage. + Larger values take longer and use more memory + but give more accurate output. + itmax : int, optional + Use at most this many iterations. + compute_v : bool, optional + Request a norm-maximizing linear operator input vector if True. + compute_w : bool, optional + Request a norm-maximizing linear operator output vector if True. + + Returns + ------- + est : float + An underestimate of the 1-norm of the sparse matrix. + v : ndarray, optional + The vector such that ||Av||_1 == est*||v||_1. + It can be thought of as an input to the linear operator + that gives an output with particularly large norm. + w : ndarray, optional + The vector Av which has relatively large 1-norm. + It can be thought of as an output of the linear operator + that is relatively large in norm compared to the input. + + Notes + ----- + This is algorithm 2.4 of [1]. + + In [2] it is described as follows. + "This algorithm typically requires the evaluation of + about 4t matrix-vector products and almost invariably + produces a norm estimate (which is, in fact, a lower + bound on the norm) correct to within a factor 3." + + .. versionadded:: 0.13.0 + + References + ---------- + .. [1] Nicholas J. Higham and Francoise Tisseur (2000), + "A Block Algorithm for Matrix 1-Norm Estimation, + with an Application to 1-Norm Pseudospectra." + SIAM J. Matrix Anal. Appl. Vol. 21, No. 4, pp. 1185-1201. + + .. [2] Awad H. Al-Mohy and Nicholas J. Higham (2009), + "A new scaling and squaring algorithm for the matrix exponential." + SIAM J. Matrix Anal. Appl. Vol. 31, No. 3, pp. 970-989. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse import csc_matrix + >>> from scipy.sparse.linalg import onenormest + >>> A = csc_matrix([[1., 0., 0.], [5., 8., 2.], [0., -1., 0.]], dtype=float) + >>> A.toarray() + array([[ 1., 0., 0.], + [ 5., 8., 2.], + [ 0., -1., 0.]]) + >>> onenormest(A) + 9.0 + >>> np.linalg.norm(A.toarray(), ord=1) + 9.0 + """ + + # Check the input. + A = aslinearoperator(A) + if A.shape[0] != A.shape[1]: + raise ValueError('expected the operator to act like a square matrix') + + # If the operator size is small compared to t, + # then it is easier to compute the exact norm. + # Otherwise estimate the norm. + n = A.shape[1] + if t >= n: + A_explicit = np.asarray(aslinearoperator(A).matmat(np.identity(n))) + if A_explicit.shape != (n, n): + raise Exception('internal error: ', + 'unexpected shape ' + str(A_explicit.shape)) + col_abs_sums = abs(A_explicit).sum(axis=0) + if col_abs_sums.shape != (n, ): + raise Exception('internal error: ', + 'unexpected shape ' + str(col_abs_sums.shape)) + argmax_j = np.argmax(col_abs_sums) + v = elementary_vector(n, argmax_j) + w = A_explicit[:, argmax_j] + est = col_abs_sums[argmax_j] + else: + est, v, w, nmults, nresamples = _onenormest_core(A, A.H, t, itmax) + + # Report the norm estimate along with some certificates of the estimate. + if compute_v or compute_w: + result = (est,) + if compute_v: + result += (v,) + if compute_w: + result += (w,) + return result + else: + return est + + +def _blocked_elementwise(func): + """ + Decorator for an elementwise function, to apply it blockwise along + first dimension, to avoid excessive memory usage in temporaries. + """ + block_size = 2**20 + + def wrapper(x): + if x.shape[0] < block_size: + return func(x) + else: + y0 = func(x[:block_size]) + y = np.zeros((x.shape[0],) + y0.shape[1:], dtype=y0.dtype) + y[:block_size] = y0 + del y0 + for j in range(block_size, x.shape[0], block_size): + y[j:j+block_size] = func(x[j:j+block_size]) + return y + return wrapper + + +@_blocked_elementwise +def sign_round_up(X): + """ + This should do the right thing for both real and complex matrices. + + From Higham and Tisseur: + "Everything in this section remains valid for complex matrices + provided that sign(A) is redefined as the matrix (aij / |aij|) + (and sign(0) = 1) transposes are replaced by conjugate transposes." + + """ + Y = X.copy() + Y[Y == 0] = 1 + Y /= np.abs(Y) + return Y + + +@_blocked_elementwise +def _max_abs_axis1(X): + return np.max(np.abs(X), axis=1) + + +def _sum_abs_axis0(X): + block_size = 2**20 + r = None + for j in range(0, X.shape[0], block_size): + y = np.sum(np.abs(X[j:j+block_size]), axis=0) + if r is None: + r = y + else: + r += y + return r + + +def elementary_vector(n, i): + v = np.zeros(n, dtype=float) + v[i] = 1 + return v + + +def vectors_are_parallel(v, w): + # Columns are considered parallel when they are equal or negative. + # Entries are required to be in {-1, 1}, + # which guarantees that the magnitudes of the vectors are identical. + if v.ndim != 1 or v.shape != w.shape: + raise ValueError('expected conformant vectors with entries in {-1,1}') + n = v.shape[0] + return np.dot(v, w) == n + + +def every_col_of_X_is_parallel_to_a_col_of_Y(X, Y): + for v in X.T: + if not any(vectors_are_parallel(v, w) for w in Y.T): + return False + return True + + +def column_needs_resampling(i, X, Y=None): + # column i of X needs resampling if either + # it is parallel to a previous column of X or + # it is parallel to a column of Y + n, t = X.shape + v = X[:, i] + if any(vectors_are_parallel(v, X[:, j]) for j in range(i)): + return True + if Y is not None: + if any(vectors_are_parallel(v, w) for w in Y.T): + return True + return False + + +def resample_column(i, X): + X[:, i] = np.random.randint(0, 2, size=X.shape[0])*2 - 1 + + +def less_than_or_close(a, b): + return np.allclose(a, b) or (a < b) + + +def _algorithm_2_2(A, AT, t): + """ + This is Algorithm 2.2. + + Parameters + ---------- + A : ndarray or other linear operator + A linear operator that can produce matrix products. + AT : ndarray or other linear operator + The transpose of A. + t : int, optional + A positive parameter controlling the tradeoff between + accuracy versus time and memory usage. + + Returns + ------- + g : sequence + A non-negative decreasing vector + such that g[j] is a lower bound for the 1-norm + of the column of A of jth largest 1-norm. + The first entry of this vector is therefore a lower bound + on the 1-norm of the linear operator A. + This sequence has length t. + ind : sequence + The ith entry of ind is the index of the column A whose 1-norm + is given by g[i]. + This sequence of indices has length t, and its entries are + chosen from range(n), possibly with repetition, + where n is the order of the operator A. + + Notes + ----- + This algorithm is mainly for testing. + It uses the 'ind' array in a way that is similar to + its usage in algorithm 2.4. This algorithm 2.2 may be easier to test, + so it gives a chance of uncovering bugs related to indexing + which could have propagated less noticeably to algorithm 2.4. + + """ + A_linear_operator = aslinearoperator(A) + AT_linear_operator = aslinearoperator(AT) + n = A_linear_operator.shape[0] + + # Initialize the X block with columns of unit 1-norm. + X = np.ones((n, t)) + if t > 1: + X[:, 1:] = np.random.randint(0, 2, size=(n, t-1))*2 - 1 + X /= float(n) + + # Iteratively improve the lower bounds. + # Track extra things, to assert invariants for debugging. + g_prev = None + h_prev = None + k = 1 + ind = range(t) + while True: + Y = np.asarray(A_linear_operator.matmat(X)) + g = _sum_abs_axis0(Y) + best_j = np.argmax(g) + g.sort() + g = g[::-1] + S = sign_round_up(Y) + Z = np.asarray(AT_linear_operator.matmat(S)) + h = _max_abs_axis1(Z) + + # If this algorithm runs for fewer than two iterations, + # then its return values do not have the properties indicated + # in the description of the algorithm. + # In particular, the entries of g are not 1-norms of any + # column of A until the second iteration. + # Therefore we will require the algorithm to run for at least + # two iterations, even though this requirement is not stated + # in the description of the algorithm. + if k >= 2: + if less_than_or_close(max(h), np.dot(Z[:, best_j], X[:, best_j])): + break + ind = np.argsort(h)[::-1][:t] + h = h[ind] + for j in range(t): + X[:, j] = elementary_vector(n, ind[j]) + + # Check invariant (2.2). + if k >= 2: + if not less_than_or_close(g_prev[0], h_prev[0]): + raise Exception('invariant (2.2) is violated') + if not less_than_or_close(h_prev[0], g[0]): + raise Exception('invariant (2.2) is violated') + + # Check invariant (2.3). + if k >= 3: + for j in range(t): + if not less_than_or_close(g[j], g_prev[j]): + raise Exception('invariant (2.3) is violated') + + # Update for the next iteration. + g_prev = g + h_prev = h + k += 1 + + # Return the lower bounds and the corresponding column indices. + return g, ind + + +def _onenormest_core(A, AT, t, itmax): + """ + Compute a lower bound of the 1-norm of a sparse matrix. + + Parameters + ---------- + A : ndarray or other linear operator + A linear operator that can produce matrix products. + AT : ndarray or other linear operator + The transpose of A. + t : int, optional + A positive parameter controlling the tradeoff between + accuracy versus time and memory usage. + itmax : int, optional + Use at most this many iterations. + + Returns + ------- + est : float + An underestimate of the 1-norm of the sparse matrix. + v : ndarray, optional + The vector such that ||Av||_1 == est*||v||_1. + It can be thought of as an input to the linear operator + that gives an output with particularly large norm. + w : ndarray, optional + The vector Av which has relatively large 1-norm. + It can be thought of as an output of the linear operator + that is relatively large in norm compared to the input. + nmults : int, optional + The number of matrix products that were computed. + nresamples : int, optional + The number of times a parallel column was observed, + necessitating a re-randomization of the column. + + Notes + ----- + This is algorithm 2.4. + + """ + # This function is a more or less direct translation + # of Algorithm 2.4 from the Higham and Tisseur (2000) paper. + A_linear_operator = aslinearoperator(A) + AT_linear_operator = aslinearoperator(AT) + if itmax < 2: + raise ValueError('at least two iterations are required') + if t < 1: + raise ValueError('at least one column is required') + n = A.shape[0] + if t >= n: + raise ValueError('t should be smaller than the order of A') + # Track the number of big*small matrix multiplications + # and the number of resamplings. + nmults = 0 + nresamples = 0 + # "We now explain our choice of starting matrix. We take the first + # column of X to be the vector of 1s [...] This has the advantage that + # for a matrix with nonnegative elements the algorithm converges + # with an exact estimate on the second iteration, and such matrices + # arise in applications [...]" + X = np.ones((n, t), dtype=float) + # "The remaining columns are chosen as rand{-1,1}, + # with a check for and correction of parallel columns, + # exactly as for S in the body of the algorithm." + if t > 1: + for i in range(1, t): + # These are technically initial samples, not resamples, + # so the resampling count is not incremented. + resample_column(i, X) + for i in range(t): + while column_needs_resampling(i, X): + resample_column(i, X) + nresamples += 1 + # "Choose starting matrix X with columns of unit 1-norm." + X /= float(n) + # "indices of used unit vectors e_j" + ind_hist = np.zeros(0, dtype=np.intp) + est_old = 0 + S = np.zeros((n, t), dtype=float) + k = 1 + ind = None + while True: + Y = np.asarray(A_linear_operator.matmat(X)) + nmults += 1 + mags = _sum_abs_axis0(Y) + est = np.max(mags) + best_j = np.argmax(mags) + if est > est_old or k == 2: + if k >= 2: + ind_best = ind[best_j] + w = Y[:, best_j] + # (1) + if k >= 2 and est <= est_old: + est = est_old + break + est_old = est + S_old = S + if k > itmax: + break + S = sign_round_up(Y) + del Y + # (2) + if every_col_of_X_is_parallel_to_a_col_of_Y(S, S_old): + break + if t > 1: + # "Ensure that no column of S is parallel to another column of S + # or to a column of S_old by replacing columns of S by rand{-1,1}." + for i in range(t): + while column_needs_resampling(i, S, S_old): + resample_column(i, S) + nresamples += 1 + del S_old + # (3) + Z = np.asarray(AT_linear_operator.matmat(S)) + nmults += 1 + h = _max_abs_axis1(Z) + del Z + # (4) + if k >= 2 and max(h) == h[ind_best]: + break + # "Sort h so that h_first >= ... >= h_last + # and re-order ind correspondingly." + # + # Later on, we will need at most t+len(ind_hist) largest + # entries, so drop the rest + ind = np.argsort(h)[::-1][:t+len(ind_hist)].copy() + del h + if t > 1: + # (5) + # Break if the most promising t vectors have been visited already. + if np.isin(ind[:t], ind_hist).all(): + break + # Put the most promising unvisited vectors at the front of the list + # and put the visited vectors at the end of the list. + # Preserve the order of the indices induced by the ordering of h. + seen = np.isin(ind, ind_hist) + ind = np.concatenate((ind[~seen], ind[seen])) + for j in range(t): + X[:, j] = elementary_vector(n, ind[j]) + + new_ind = ind[:t][~np.isin(ind[:t], ind_hist)] + ind_hist = np.concatenate((ind_hist, new_ind)) + k += 1 + v = elementary_vector(n, ind_best) + return est, v, w, nmults, nresamples diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_special_sparse_arrays.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_special_sparse_arrays.py new file mode 100644 index 0000000000000000000000000000000000000000..650cea5afe6f70b1e6537c3dcc3710ffb6f47167 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_special_sparse_arrays.py @@ -0,0 +1,948 @@ +import numpy as np +from scipy.sparse.linalg import LinearOperator +from scipy.sparse import kron, eye, dia_array + +__all__ = ['LaplacianNd'] +# Sakurai and Mikota classes are intended for tests and benchmarks +# and explicitly not included in the public API of this module. + + +class LaplacianNd(LinearOperator): + """ + The grid Laplacian in ``N`` dimensions and its eigenvalues/eigenvectors. + + Construct Laplacian on a uniform rectangular grid in `N` dimensions + and output its eigenvalues and eigenvectors. + The Laplacian ``L`` is square, negative definite, real symmetric array + with signed integer entries and zeros otherwise. + + Parameters + ---------- + grid_shape : tuple + A tuple of integers of length ``N`` (corresponding to the dimension of + the Lapacian), where each entry gives the size of that dimension. The + Laplacian matrix is square of the size ``np.prod(grid_shape)``. + boundary_conditions : {'neumann', 'dirichlet', 'periodic'}, optional + The type of the boundary conditions on the boundaries of the grid. + Valid values are ``'dirichlet'`` or ``'neumann'``(default) or + ``'periodic'``. + dtype : dtype + Numerical type of the array. Default is ``np.int8``. + + Methods + ------- + toarray() + Construct a dense array from Laplacian data + tosparse() + Construct a sparse array from Laplacian data + eigenvalues(m=None) + Construct a 1D array of `m` largest (smallest in absolute value) + eigenvalues of the Laplacian matrix in ascending order. + eigenvectors(m=None): + Construct the array with columns made of `m` eigenvectors (``float``) + of the ``Nd`` Laplacian corresponding to the `m` ordered eigenvalues. + + .. versionadded:: 1.12.0 + + Notes + ----- + Compared to the MATLAB/Octave implementation [1] of 1-, 2-, and 3-D + Laplacian, this code allows the arbitrary N-D case and the matrix-free + callable option, but is currently limited to pure Dirichlet, Neumann or + Periodic boundary conditions only. + + The Laplacian matrix of a graph (`scipy.sparse.csgraph.laplacian`) of a + rectangular grid corresponds to the negative Laplacian with the Neumann + conditions, i.e., ``boundary_conditions = 'neumann'``. + + All eigenvalues and eigenvectors of the discrete Laplacian operator for + an ``N``-dimensional regular grid of shape `grid_shape` with the grid + step size ``h=1`` are analytically known [2]. + + References + ---------- + .. [1] https://github.com/lobpcg/blopex/blob/master/blopex_\ +tools/matlab/laplacian/laplacian.m + .. [2] "Eigenvalues and eigenvectors of the second derivative", Wikipedia + https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors_\ +of_the_second_derivative + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg import LaplacianNd + >>> from scipy.sparse import diags, csgraph + >>> from scipy.linalg import eigvalsh + + The one-dimensional Laplacian demonstrated below for pure Neumann boundary + conditions on a regular grid with ``n=6`` grid points is exactly the + negative graph Laplacian for the undirected linear graph with ``n`` + vertices using the sparse adjacency matrix ``G`` represented by the + famous tri-diagonal matrix: + + >>> n = 6 + >>> G = diags(np.ones(n - 1), 1, format='csr') + >>> Lf = csgraph.laplacian(G, symmetrized=True, form='function') + >>> grid_shape = (n, ) + >>> lap = LaplacianNd(grid_shape, boundary_conditions='neumann') + >>> np.array_equal(lap.matmat(np.eye(n)), -Lf(np.eye(n))) + True + + Since all matrix entries of the Laplacian are integers, ``'int8'`` is + the default dtype for storing matrix representations. + + >>> lap.tosparse() + <6x6 sparse array of type '' + with 16 stored elements (3 diagonals) in DIAgonal format> + >>> lap.toarray() + array([[-1, 1, 0, 0, 0, 0], + [ 1, -2, 1, 0, 0, 0], + [ 0, 1, -2, 1, 0, 0], + [ 0, 0, 1, -2, 1, 0], + [ 0, 0, 0, 1, -2, 1], + [ 0, 0, 0, 0, 1, -1]], dtype=int8) + >>> np.array_equal(lap.matmat(np.eye(n)), lap.toarray()) + True + >>> np.array_equal(lap.tosparse().toarray(), lap.toarray()) + True + + Any number of extreme eigenvalues and/or eigenvectors can be computed. + + >>> lap = LaplacianNd(grid_shape, boundary_conditions='periodic') + >>> lap.eigenvalues() + array([-4., -3., -3., -1., -1., 0.]) + >>> lap.eigenvalues()[-2:] + array([-1., 0.]) + >>> lap.eigenvalues(2) + array([-1., 0.]) + >>> lap.eigenvectors(1) + array([[0.40824829], + [0.40824829], + [0.40824829], + [0.40824829], + [0.40824829], + [0.40824829]]) + >>> lap.eigenvectors(2) + array([[ 0.5 , 0.40824829], + [ 0. , 0.40824829], + [-0.5 , 0.40824829], + [-0.5 , 0.40824829], + [ 0. , 0.40824829], + [ 0.5 , 0.40824829]]) + >>> lap.eigenvectors() + array([[ 0.40824829, 0.28867513, 0.28867513, 0.5 , 0.5 , + 0.40824829], + [-0.40824829, -0.57735027, -0.57735027, 0. , 0. , + 0.40824829], + [ 0.40824829, 0.28867513, 0.28867513, -0.5 , -0.5 , + 0.40824829], + [-0.40824829, 0.28867513, 0.28867513, -0.5 , -0.5 , + 0.40824829], + [ 0.40824829, -0.57735027, -0.57735027, 0. , 0. , + 0.40824829], + [-0.40824829, 0.28867513, 0.28867513, 0.5 , 0.5 , + 0.40824829]]) + + The two-dimensional Laplacian is illustrated on a regular grid with + ``grid_shape = (2, 3)`` points in each dimension. + + >>> grid_shape = (2, 3) + >>> n = np.prod(grid_shape) + + Numeration of grid points is as follows: + + >>> np.arange(n).reshape(grid_shape + (-1,)) + array([[[0], + [1], + [2]], + + [[3], + [4], + [5]]]) + + Each of the boundary conditions ``'dirichlet'``, ``'periodic'``, and + ``'neumann'`` is illustrated separately; with ``'dirichlet'`` + + >>> lap = LaplacianNd(grid_shape, boundary_conditions='dirichlet') + >>> lap.tosparse() + <6x6 sparse array of type '' + with 20 stored elements in Compressed Sparse Row format> + >>> lap.toarray() + array([[-4, 1, 0, 1, 0, 0], + [ 1, -4, 1, 0, 1, 0], + [ 0, 1, -4, 0, 0, 1], + [ 1, 0, 0, -4, 1, 0], + [ 0, 1, 0, 1, -4, 1], + [ 0, 0, 1, 0, 1, -4]], dtype=int8) + >>> np.array_equal(lap.matmat(np.eye(n)), lap.toarray()) + True + >>> np.array_equal(lap.tosparse().toarray(), lap.toarray()) + True + >>> lap.eigenvalues() + array([-6.41421356, -5. , -4.41421356, -3.58578644, -3. , + -1.58578644]) + >>> eigvals = eigvalsh(lap.toarray().astype(np.float64)) + >>> np.allclose(lap.eigenvalues(), eigvals) + True + >>> np.allclose(lap.toarray() @ lap.eigenvectors(), + ... lap.eigenvectors() @ np.diag(lap.eigenvalues())) + True + + with ``'periodic'`` + + >>> lap = LaplacianNd(grid_shape, boundary_conditions='periodic') + >>> lap.tosparse() + <6x6 sparse array of type '' + with 24 stored elements in Compressed Sparse Row format> + >>> lap.toarray() + array([[-4, 1, 1, 2, 0, 0], + [ 1, -4, 1, 0, 2, 0], + [ 1, 1, -4, 0, 0, 2], + [ 2, 0, 0, -4, 1, 1], + [ 0, 2, 0, 1, -4, 1], + [ 0, 0, 2, 1, 1, -4]], dtype=int8) + >>> np.array_equal(lap.matmat(np.eye(n)), lap.toarray()) + True + >>> np.array_equal(lap.tosparse().toarray(), lap.toarray()) + True + >>> lap.eigenvalues() + array([-7., -7., -4., -3., -3., 0.]) + >>> eigvals = eigvalsh(lap.toarray().astype(np.float64)) + >>> np.allclose(lap.eigenvalues(), eigvals) + True + >>> np.allclose(lap.toarray() @ lap.eigenvectors(), + ... lap.eigenvectors() @ np.diag(lap.eigenvalues())) + True + + and with ``'neumann'`` + + >>> lap = LaplacianNd(grid_shape, boundary_conditions='neumann') + >>> lap.tosparse() + <6x6 sparse array of type '' + with 20 stored elements in Compressed Sparse Row format> + >>> lap.toarray() + array([[-2, 1, 0, 1, 0, 0], + [ 1, -3, 1, 0, 1, 0], + [ 0, 1, -2, 0, 0, 1], + [ 1, 0, 0, -2, 1, 0], + [ 0, 1, 0, 1, -3, 1], + [ 0, 0, 1, 0, 1, -2]]) + >>> np.array_equal(lap.matmat(np.eye(n)), lap.toarray()) + True + >>> np.array_equal(lap.tosparse().toarray(), lap.toarray()) + True + >>> lap.eigenvalues() + array([-5., -3., -3., -2., -1., 0.]) + >>> eigvals = eigvalsh(lap.toarray().astype(np.float64)) + >>> np.allclose(lap.eigenvalues(), eigvals) + True + >>> np.allclose(lap.toarray() @ lap.eigenvectors(), + ... lap.eigenvectors() @ np.diag(lap.eigenvalues())) + True + + """ + + def __init__(self, grid_shape, *, + boundary_conditions='neumann', + dtype=np.int8): + + if boundary_conditions not in ('dirichlet', 'neumann', 'periodic'): + raise ValueError( + f"Unknown value {boundary_conditions!r} is given for " + "'boundary_conditions' parameter. The valid options are " + "'dirichlet', 'periodic', and 'neumann' (default)." + ) + + self.grid_shape = grid_shape + self.boundary_conditions = boundary_conditions + # LaplacianNd folds all dimensions in `grid_shape` into a single one + N = np.prod(grid_shape) + super().__init__(dtype=dtype, shape=(N, N)) + + def _eigenvalue_ordering(self, m): + """Compute `m` largest eigenvalues in each of the ``N`` directions, + i.e., up to ``m * N`` total, order them and return `m` largest. + """ + grid_shape = self.grid_shape + if m is None: + indices = np.indices(grid_shape) + Leig = np.zeros(grid_shape) + else: + grid_shape_min = min(grid_shape, + tuple(np.ones_like(grid_shape) * m)) + indices = np.indices(grid_shape_min) + Leig = np.zeros(grid_shape_min) + + for j, n in zip(indices, grid_shape): + if self.boundary_conditions == 'dirichlet': + Leig += -4 * np.sin(np.pi * (j + 1) / (2 * (n + 1))) ** 2 + elif self.boundary_conditions == 'neumann': + Leig += -4 * np.sin(np.pi * j / (2 * n)) ** 2 + else: # boundary_conditions == 'periodic' + Leig += -4 * np.sin(np.pi * np.floor((j + 1) / 2) / n) ** 2 + + Leig_ravel = Leig.ravel() + ind = np.argsort(Leig_ravel) + eigenvalues = Leig_ravel[ind] + if m is not None: + eigenvalues = eigenvalues[-m:] + ind = ind[-m:] + + return eigenvalues, ind + + def eigenvalues(self, m=None): + """Return the requested number of eigenvalues. + + Parameters + ---------- + m : int, optional + The positive number of smallest eigenvalues to return. + If not provided, then all eigenvalues will be returned. + + Returns + ------- + eigenvalues : float array + The requested `m` smallest or all eigenvalues, in ascending order. + """ + eigenvalues, _ = self._eigenvalue_ordering(m) + return eigenvalues + + def _ev1d(self, j, n): + """Return 1 eigenvector in 1d with index `j` + and number of grid points `n` where ``j < n``. + """ + if self.boundary_conditions == 'dirichlet': + i = np.pi * (np.arange(n) + 1) / (n + 1) + ev = np.sqrt(2. / (n + 1.)) * np.sin(i * (j + 1)) + elif self.boundary_conditions == 'neumann': + i = np.pi * (np.arange(n) + 0.5) / n + ev = np.sqrt((1. if j == 0 else 2.) / n) * np.cos(i * j) + else: # boundary_conditions == 'periodic' + if j == 0: + ev = np.sqrt(1. / n) * np.ones(n) + elif j + 1 == n and n % 2 == 0: + ev = np.sqrt(1. / n) * np.tile([1, -1], n//2) + else: + i = 2. * np.pi * (np.arange(n) + 0.5) / n + ev = np.sqrt(2. / n) * np.cos(i * np.floor((j + 1) / 2)) + # make small values exact zeros correcting round-off errors + # due to symmetry of eigenvectors the exact 0. is correct + ev[np.abs(ev) < np.finfo(np.float64).eps] = 0. + return ev + + def _one_eve(self, k): + """Return 1 eigenvector in Nd with multi-index `j` + as a tensor product of the corresponding 1d eigenvectors. + """ + phi = [self._ev1d(j, n) for j, n in zip(k, self.grid_shape)] + result = phi[0] + for phi in phi[1:]: + result = np.tensordot(result, phi, axes=0) + return np.asarray(result).ravel() + + def eigenvectors(self, m=None): + """Return the requested number of eigenvectors for ordered eigenvalues. + + Parameters + ---------- + m : int, optional + The positive number of eigenvectors to return. If not provided, + then all eigenvectors will be returned. + + Returns + ------- + eigenvectors : float array + An array with columns made of the requested `m` or all eigenvectors. + The columns are ordered according to the `m` ordered eigenvalues. + """ + _, ind = self._eigenvalue_ordering(m) + if m is None: + grid_shape_min = self.grid_shape + else: + grid_shape_min = min(self.grid_shape, + tuple(np.ones_like(self.grid_shape) * m)) + + N_indices = np.unravel_index(ind, grid_shape_min) + N_indices = [tuple(x) for x in zip(*N_indices)] + eigenvectors_list = [self._one_eve(k) for k in N_indices] + return np.column_stack(eigenvectors_list) + + def toarray(self): + """ + Converts the Laplacian data to a dense array. + + Returns + ------- + L : ndarray + The shape is ``(N, N)`` where ``N = np.prod(grid_shape)``. + + """ + grid_shape = self.grid_shape + n = np.prod(grid_shape) + L = np.zeros([n, n], dtype=np.int8) + # Scratch arrays + L_i = np.empty_like(L) + Ltemp = np.empty_like(L) + + for ind, dim in enumerate(grid_shape): + # Start zeroing out L_i + L_i[:] = 0 + # Allocate the top left corner with the kernel of L_i + # Einsum returns writable view of arrays + np.einsum("ii->i", L_i[:dim, :dim])[:] = -2 + np.einsum("ii->i", L_i[: dim - 1, 1:dim])[:] = 1 + np.einsum("ii->i", L_i[1:dim, : dim - 1])[:] = 1 + + if self.boundary_conditions == 'neumann': + L_i[0, 0] = -1 + L_i[dim - 1, dim - 1] = -1 + elif self.boundary_conditions == 'periodic': + if dim > 1: + L_i[0, dim - 1] += 1 + L_i[dim - 1, 0] += 1 + else: + L_i[0, 0] += 1 + + # kron is too slow for large matrices hence the next two tricks + # 1- kron(eye, mat) is block_diag(mat, mat, ...) + # 2- kron(mat, eye) can be performed by 4d stride trick + + # 1- + new_dim = dim + # for block_diag we tile the top left portion on the diagonal + if ind > 0: + tiles = np.prod(grid_shape[:ind]) + for j in range(1, tiles): + L_i[j*dim:(j+1)*dim, j*dim:(j+1)*dim] = L_i[:dim, :dim] + new_dim += dim + # 2- + # we need the keep L_i, but reset the array + Ltemp[:new_dim, :new_dim] = L_i[:new_dim, :new_dim] + tiles = int(np.prod(grid_shape[ind+1:])) + # Zero out the top left, the rest is already 0 + L_i[:new_dim, :new_dim] = 0 + idx = [x for x in range(tiles)] + L_i.reshape( + (new_dim, tiles, + new_dim, tiles) + )[:, idx, :, idx] = Ltemp[:new_dim, :new_dim] + + L += L_i + + return L.astype(self.dtype) + + def tosparse(self): + """ + Constructs a sparse array from the Laplacian data. The returned sparse + array format is dependent on the selected boundary conditions. + + Returns + ------- + L : scipy.sparse.sparray + The shape is ``(N, N)`` where ``N = np.prod(grid_shape)``. + + """ + N = len(self.grid_shape) + p = np.prod(self.grid_shape) + L = dia_array((p, p), dtype=np.int8) + + for i in range(N): + dim = self.grid_shape[i] + data = np.ones([3, dim], dtype=np.int8) + data[1, :] *= -2 + + if self.boundary_conditions == 'neumann': + data[1, 0] = -1 + data[1, -1] = -1 + + L_i = dia_array((data, [-1, 0, 1]), shape=(dim, dim), + dtype=np.int8 + ) + + if self.boundary_conditions == 'periodic': + t = dia_array((dim, dim), dtype=np.int8) + t.setdiag([1], k=-dim+1) + t.setdiag([1], k=dim-1) + L_i += t + + for j in range(i): + L_i = kron(eye(self.grid_shape[j], dtype=np.int8), L_i) + for j in range(i + 1, N): + L_i = kron(L_i, eye(self.grid_shape[j], dtype=np.int8)) + L += L_i + return L.astype(self.dtype) + + def _matvec(self, x): + grid_shape = self.grid_shape + N = len(grid_shape) + X = x.reshape(grid_shape + (-1,)) + Y = -2 * N * X + for i in range(N): + Y += np.roll(X, 1, axis=i) + Y += np.roll(X, -1, axis=i) + if self.boundary_conditions in ('neumann', 'dirichlet'): + Y[(slice(None),)*i + (0,) + (slice(None),)*(N-i-1) + ] -= np.roll(X, 1, axis=i)[ + (slice(None),) * i + (0,) + (slice(None),) * (N-i-1) + ] + Y[ + (slice(None),) * i + (-1,) + (slice(None),) * (N-i-1) + ] -= np.roll(X, -1, axis=i)[ + (slice(None),) * i + (-1,) + (slice(None),) * (N-i-1) + ] + + if self.boundary_conditions == 'neumann': + Y[ + (slice(None),) * i + (0,) + (slice(None),) * (N-i-1) + ] += np.roll(X, 0, axis=i)[ + (slice(None),) * i + (0,) + (slice(None),) * (N-i-1) + ] + Y[ + (slice(None),) * i + (-1,) + (slice(None),) * (N-i-1) + ] += np.roll(X, 0, axis=i)[ + (slice(None),) * i + (-1,) + (slice(None),) * (N-i-1) + ] + + return Y.reshape(-1, X.shape[-1]) + + def _matmat(self, x): + return self._matvec(x) + + def _adjoint(self): + return self + + def _transpose(self): + return self + + +class Sakurai(LinearOperator): + """ + Construct a Sakurai matrix in various formats and its eigenvalues. + + Constructs the "Sakurai" matrix motivated by reference [1]_: + square real symmetric positive definite and 5-diagonal + with the main digonal ``[5, 6, 6, ..., 6, 6, 5], the ``+1`` and ``-1`` + diagonals filled with ``-4``, and the ``+2`` and ``-2`` diagonals + made of ``1``. Its eigenvalues are analytically known to be + ``16. * np.power(np.cos(0.5 * k * np.pi / (n + 1)), 4)``. + The matrix gets ill-conditioned with its size growing. + It is useful for testing and benchmarking sparse eigenvalue solvers + especially those taking advantage of its banded 5-diagonal structure. + See the notes below for details. + + Parameters + ---------- + n : int + The size of the matrix. + dtype : dtype + Numerical type of the array. Default is ``np.int8``. + + Methods + ------- + toarray() + Construct a dense array from Laplacian data + tosparse() + Construct a sparse array from Laplacian data + tobanded() + The Sakurai matrix in the format for banded symmetric matrices, + i.e., (3, n) ndarray with 3 upper diagonals + placing the main diagonal at the bottom. + eigenvalues + All eigenvalues of the Sakurai matrix ordered ascending. + + Notes + ----- + Reference [1]_ introduces a generalized eigenproblem for the matrix pair + `A` and `B` where `A` is the identity so we turn it into an eigenproblem + just for the matrix `B` that this function outputs in various formats + together with its eigenvalues. + + .. versionadded:: 1.12.0 + + References + ---------- + .. [1] T. Sakurai, H. Tadano, Y. Inadomi, and U. Nagashima, + "A moment-based method for large-scale generalized + eigenvalue problems", + Appl. Num. Anal. Comp. Math. Vol. 1 No. 2 (2004). + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg._special_sparse_arrays import Sakurai + >>> from scipy.linalg import eig_banded + >>> n = 6 + >>> sak = Sakurai(n) + + Since all matrix entries are small integers, ``'int8'`` is + the default dtype for storing matrix representations. + + >>> sak.toarray() + array([[ 5, -4, 1, 0, 0, 0], + [-4, 6, -4, 1, 0, 0], + [ 1, -4, 6, -4, 1, 0], + [ 0, 1, -4, 6, -4, 1], + [ 0, 0, 1, -4, 6, -4], + [ 0, 0, 0, 1, -4, 5]], dtype=int8) + >>> sak.tobanded() + array([[ 1, 1, 1, 1, 1, 1], + [-4, -4, -4, -4, -4, -4], + [ 5, 6, 6, 6, 6, 5]], dtype=int8) + >>> sak.tosparse() + <6x6 sparse matrix of type '' + with 24 stored elements (5 diagonals) in DIAgonal format> + >>> np.array_equal(sak.dot(np.eye(n)), sak.tosparse().toarray()) + True + >>> sak.eigenvalues() + array([0.03922866, 0.56703972, 2.41789479, 5.97822974, + 10.54287655, 14.45473055]) + >>> sak.eigenvalues(2) + array([0.03922866, 0.56703972]) + + The banded form can be used in scipy functions for banded matrices, e.g., + + >>> e = eig_banded(sak.tobanded(), eigvals_only=True) + >>> np.allclose(sak.eigenvalues, e, atol= n * n * n * np.finfo(float).eps) + True + + """ + def __init__(self, n, dtype=np.int8): + self.n = n + self.dtype = dtype + shape = (n, n) + super().__init__(dtype, shape) + + def eigenvalues(self, m=None): + """Return the requested number of eigenvalues. + + Parameters + ---------- + m : int, optional + The positive number of smallest eigenvalues to return. + If not provided, then all eigenvalues will be returned. + + Returns + ------- + eigenvalues : `np.float64` array + The requested `m` smallest or all eigenvalues, in ascending order. + """ + if m is None: + m = self.n + k = np.arange(self.n + 1 -m, self.n + 1) + return np.flip(16. * np.power(np.cos(0.5 * k * np.pi / (self.n + 1)), 4)) + + def tobanded(self): + """ + Construct the Sakurai matrix as a banded array. + """ + d0 = np.r_[5, 6 * np.ones(self.n - 2, dtype=self.dtype), 5] + d1 = -4 * np.ones(self.n, dtype=self.dtype) + d2 = np.ones(self.n, dtype=self.dtype) + return np.array([d2, d1, d0]).astype(self.dtype) + + def tosparse(self): + """ + Construct the Sakurai matrix is a sparse format. + """ + from scipy.sparse import spdiags + d = self.tobanded() + # the banded format has the main diagonal at the bottom + # `spdiags` has no `dtype` parameter so inherits dtype from banded + return spdiags([d[0], d[1], d[2], d[1], d[0]], [-2, -1, 0, 1, 2], + self.n, self.n) + + def toarray(self): + return self.tosparse().toarray() + + def _matvec(self, x): + """ + Construct matrix-free callable banded-matrix-vector multiplication by + the Sakurai matrix without constructing or storing the matrix itself + using the knowledge of its entries and the 5-diagonal format. + """ + x = x.reshape(self.n, -1) + result_dtype = np.promote_types(x.dtype, self.dtype) + sx = np.zeros_like(x, dtype=result_dtype) + sx[0, :] = 5 * x[0, :] - 4 * x[1, :] + x[2, :] + sx[-1, :] = 5 * x[-1, :] - 4 * x[-2, :] + x[-3, :] + sx[1: -1, :] = (6 * x[1: -1, :] - 4 * (x[:-2, :] + x[2:, :]) + + np.pad(x[:-3, :], ((1, 0), (0, 0))) + + np.pad(x[3:, :], ((0, 1), (0, 0)))) + return sx + + def _matmat(self, x): + """ + Construct matrix-free callable matrix-matrix multiplication by + the Sakurai matrix without constructing or storing the matrix itself + by reusing the ``_matvec(x)`` that supports both 1D and 2D arrays ``x``. + """ + return self._matvec(x) + + def _adjoint(self): + return self + + def _transpose(self): + return self + + +class MikotaM(LinearOperator): + """ + Construct a mass matrix in various formats of Mikota pair. + + The mass matrix `M` is square real diagonal + positive definite with entries that are reciprocal to integers. + + Parameters + ---------- + shape : tuple of int + The shape of the matrix. + dtype : dtype + Numerical type of the array. Default is ``np.float64``. + + Methods + ------- + toarray() + Construct a dense array from Mikota data + tosparse() + Construct a sparse array from Mikota data + tobanded() + The format for banded symmetric matrices, + i.e., (1, n) ndarray with the main diagonal. + """ + def __init__(self, shape, dtype=np.float64): + self.shape = shape + self.dtype = dtype + super().__init__(dtype, shape) + + def _diag(self): + # The matrix is constructed from its diagonal 1 / [1, ..., N+1]; + # compute in a function to avoid duplicated code & storage footprint + return (1. / np.arange(1, self.shape[0] + 1)).astype(self.dtype) + + def tobanded(self): + return self._diag() + + def tosparse(self): + from scipy.sparse import diags + return diags([self._diag()], [0], shape=self.shape, dtype=self.dtype) + + def toarray(self): + return np.diag(self._diag()).astype(self.dtype) + + def _matvec(self, x): + """ + Construct matrix-free callable banded-matrix-vector multiplication by + the Mikota mass matrix without constructing or storing the matrix itself + using the knowledge of its entries and the diagonal format. + """ + x = x.reshape(self.shape[0], -1) + return self._diag()[:, np.newaxis] * x + + def _matmat(self, x): + """ + Construct matrix-free callable matrix-matrix multiplication by + the Mikota mass matrix without constructing or storing the matrix itself + by reusing the ``_matvec(x)`` that supports both 1D and 2D arrays ``x``. + """ + return self._matvec(x) + + def _adjoint(self): + return self + + def _transpose(self): + return self + + +class MikotaK(LinearOperator): + """ + Construct a stiffness matrix in various formats of Mikota pair. + + The stiffness matrix `K` is square real tri-diagonal symmetric + positive definite with integer entries. + + Parameters + ---------- + shape : tuple of int + The shape of the matrix. + dtype : dtype + Numerical type of the array. Default is ``np.int32``. + + Methods + ------- + toarray() + Construct a dense array from Mikota data + tosparse() + Construct a sparse array from Mikota data + tobanded() + The format for banded symmetric matrices, + i.e., (2, n) ndarray with 2 upper diagonals + placing the main diagonal at the bottom. + """ + def __init__(self, shape, dtype=np.int32): + self.shape = shape + self.dtype = dtype + super().__init__(dtype, shape) + # The matrix is constructed from its diagonals; + # we precompute these to avoid duplicating the computation + n = shape[0] + self._diag0 = np.arange(2 * n - 1, 0, -2, dtype=self.dtype) + self._diag1 = - np.arange(n - 1, 0, -1, dtype=self.dtype) + + def tobanded(self): + return np.array([np.pad(self._diag1, (1, 0), 'constant'), self._diag0]) + + def tosparse(self): + from scipy.sparse import diags + return diags([self._diag1, self._diag0, self._diag1], [-1, 0, 1], + shape=self.shape, dtype=self.dtype) + + def toarray(self): + return self.tosparse().toarray() + + def _matvec(self, x): + """ + Construct matrix-free callable banded-matrix-vector multiplication by + the Mikota stiffness matrix without constructing or storing the matrix + itself using the knowledge of its entries and the 3-diagonal format. + """ + x = x.reshape(self.shape[0], -1) + result_dtype = np.promote_types(x.dtype, self.dtype) + kx = np.zeros_like(x, dtype=result_dtype) + d1 = self._diag1 + d0 = self._diag0 + kx[0, :] = d0[0] * x[0, :] + d1[0] * x[1, :] + kx[-1, :] = d1[-1] * x[-2, :] + d0[-1] * x[-1, :] + kx[1: -1, :] = (d1[:-1, None] * x[: -2, :] + + d0[1: -1, None] * x[1: -1, :] + + d1[1:, None] * x[2:, :]) + return kx + + def _matmat(self, x): + """ + Construct matrix-free callable matrix-matrix multiplication by + the Stiffness mass matrix without constructing or storing the matrix itself + by reusing the ``_matvec(x)`` that supports both 1D and 2D arrays ``x``. + """ + return self._matvec(x) + + def _adjoint(self): + return self + + def _transpose(self): + return self + + +class MikotaPair: + """ + Construct the Mikota pair of matrices in various formats and + eigenvalues of the generalized eigenproblem with them. + + The Mikota pair of matrices [1, 2]_ models a vibration problem + of a linear mass-spring system with the ends attached where + the stiffness of the springs and the masses increase along + the system length such that vibration frequencies are subsequent + integers 1, 2, ..., `n` where `n` is the number of the masses. Thus, + eigenvalues of the generalized eigenvalue problem for + the matrix pair `K` and `M` where `K` is he system stiffness matrix + and `M` is the system mass matrix are the squares of the integers, + i.e., 1, 4, 9, ..., ``n * n``. + + The stiffness matrix `K` is square real tri-diagonal symmetric + positive definite. The mass matrix `M` is diagonal with diagonal + entries 1, 1/2, 1/3, ...., ``1/n``. Both matrices get + ill-conditioned with `n` growing. + + Parameters + ---------- + n : int + The size of the matrices of the Mikota pair. + dtype : dtype + Numerical type of the array. Default is ``np.float64``. + + Attributes + ---------- + eigenvalues : 1D ndarray, ``np.uint64`` + All eigenvalues of the Mikota pair ordered ascending. + + Methods + ------- + MikotaK() + A `LinearOperator` custom object for the stiffness matrix. + MikotaM() + A `LinearOperator` custom object for the mass matrix. + + .. versionadded:: 1.12.0 + + References + ---------- + .. [1] J. Mikota, "Frequency tuning of chain structure multibody oscillators + to place the natural frequencies at omega1 and N-1 integer multiples + omega2,..., omegaN", Z. Angew. Math. Mech. 81 (2001), S2, S201-S202. + Appl. Num. Anal. Comp. Math. Vol. 1 No. 2 (2004). + .. [2] Peter C. Muller and Metin Gurgoze, + "Natural frequencies of a multi-degree-of-freedom vibration system", + Proc. Appl. Math. Mech. 6, 319-320 (2006). + http://dx.doi.org/10.1002/pamm.200610141. + + Examples + -------- + >>> import numpy as np + >>> from scipy.sparse.linalg._special_sparse_arrays import MikotaPair + >>> n = 6 + >>> mik = MikotaPair(n) + >>> mik_k = mik.k + >>> mik_m = mik.m + >>> mik_k.toarray() + array([[11., -5., 0., 0., 0., 0.], + [-5., 9., -4., 0., 0., 0.], + [ 0., -4., 7., -3., 0., 0.], + [ 0., 0., -3., 5., -2., 0.], + [ 0., 0., 0., -2., 3., -1.], + [ 0., 0., 0., 0., -1., 1.]]) + >>> mik_k.tobanded() + array([[ 0., -5., -4., -3., -2., -1.], + [11., 9., 7., 5., 3., 1.]]) + >>> mik_m.tobanded() + array([1. , 0.5 , 0.33333333, 0.25 , 0.2 , + 0.16666667]) + >>> mik_k.tosparse() + <6x6 sparse matrix of type '' + with 16 stored elements (3 diagonals) in DIAgonal format> + >>> mik_m.tosparse() + <6x6 sparse matrix of type '' + with 6 stored elements (1 diagonals) in DIAgonal format> + >>> np.array_equal(mik_k(np.eye(n)), mik_k.toarray()) + True + >>> np.array_equal(mik_m(np.eye(n)), mik_m.toarray()) + True + >>> mik.eigenvalues() + array([ 1, 4, 9, 16, 25, 36]) + >>> mik.eigenvalues(2) + array([ 1, 4]) + + """ + def __init__(self, n, dtype=np.float64): + self.n = n + self.dtype = dtype + self.shape = (n, n) + self.m = MikotaM(self.shape, self.dtype) + self.k = MikotaK(self.shape, self.dtype) + + def eigenvalues(self, m=None): + """Return the requested number of eigenvalues. + + Parameters + ---------- + m : int, optional + The positive number of smallest eigenvalues to return. + If not provided, then all eigenvalues will be returned. + + Returns + ------- + eigenvalues : `np.uint64` array + The requested `m` smallest or all eigenvalues, in ascending order. + """ + if m is None: + m = self.n + arange_plus1 = np.arange(1, m + 1, dtype=np.uint64) + return arange_plus1 * arange_plus1 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/_svdp.py b/.venv/Lib/site-packages/scipy/sparse/linalg/_svdp.py new file mode 100644 index 0000000000000000000000000000000000000000..3919e154b717abc4af210bcb0302588457686cef --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/_svdp.py @@ -0,0 +1,315 @@ +""" +Python wrapper for PROPACK +-------------------------- + +PROPACK is a collection of Fortran routines for iterative computation +of partial SVDs of large matrices or linear operators. + +Based on BSD licensed pypropack project: + http://github.com/jakevdp/pypropack + Author: Jake Vanderplas + +PROPACK source is BSD licensed, and available at + http://soi.stanford.edu/~rmunk/PROPACK/ +""" + +__all__ = ['_svdp'] + +import numpy as np + +from scipy._lib._util import check_random_state +from scipy.sparse.linalg import aslinearoperator +from scipy.linalg import LinAlgError + +from ._propack import _spropack # type: ignore[attr-defined] +from ._propack import _dpropack # type: ignore[attr-defined] +from ._propack import _cpropack # type: ignore[attr-defined] +from ._propack import _zpropack # type: ignore[attr-defined] + + +_lansvd_dict = { + 'f': _spropack.slansvd, + 'd': _dpropack.dlansvd, + 'F': _cpropack.clansvd, + 'D': _zpropack.zlansvd, +} + + +_lansvd_irl_dict = { + 'f': _spropack.slansvd_irl, + 'd': _dpropack.dlansvd_irl, + 'F': _cpropack.clansvd_irl, + 'D': _zpropack.zlansvd_irl, +} + +_which_converter = { + 'LM': 'L', + 'SM': 'S', +} + + +class _AProd: + """ + Wrapper class for linear operator + + The call signature of the __call__ method matches the callback of + the PROPACK routines. + """ + def __init__(self, A): + try: + self.A = aslinearoperator(A) + except TypeError: + self.A = aslinearoperator(np.asarray(A)) + + def __call__(self, transa, m, n, x, y, sparm, iparm): + if transa == 'n': + y[:] = self.A.matvec(x) + else: + y[:] = self.A.rmatvec(x) + + @property + def shape(self): + return self.A.shape + + @property + def dtype(self): + try: + return self.A.dtype + except AttributeError: + return self.A.matvec(np.zeros(self.A.shape[1])).dtype + + +def _svdp(A, k, which='LM', irl_mode=True, kmax=None, + compute_u=True, compute_v=True, v0=None, full_output=False, tol=0, + delta=None, eta=None, anorm=0, cgs=False, elr=True, + min_relgap=0.002, shifts=None, maxiter=None, random_state=None): + """ + Compute the singular value decomposition of a linear operator using PROPACK + + Parameters + ---------- + A : array_like, sparse matrix, or LinearOperator + Operator for which SVD will be computed. If `A` is a LinearOperator + object, it must define both ``matvec`` and ``rmatvec`` methods. + k : int + Number of singular values/vectors to compute + which : {"LM", "SM"} + Which singular triplets to compute: + - 'LM': compute triplets corresponding to the `k` largest singular + values + - 'SM': compute triplets corresponding to the `k` smallest singular + values + `which='SM'` requires `irl_mode=True`. Computes largest singular + values by default. + irl_mode : bool, optional + If `True`, then compute SVD using IRL (implicitly restarted Lanczos) + mode. Default is `True`. + kmax : int, optional + Maximal number of iterations / maximal dimension of the Krylov + subspace. Default is ``10 * k``. + compute_u : bool, optional + If `True` (default) then compute left singular vectors, `u`. + compute_v : bool, optional + If `True` (default) then compute right singular vectors, `v`. + tol : float, optional + The desired relative accuracy for computed singular values. + If not specified, it will be set based on machine precision. + v0 : array_like, optional + Starting vector for iterations: must be of length ``A.shape[0]``. + If not specified, PROPACK will generate a starting vector. + full_output : bool, optional + If `True`, then return sigma_bound. Default is `False`. + delta : float, optional + Level of orthogonality to maintain between Lanczos vectors. + Default is set based on machine precision. + eta : float, optional + Orthogonality cutoff. During reorthogonalization, vectors with + component larger than `eta` along the Lanczos vector will be purged. + Default is set based on machine precision. + anorm : float, optional + Estimate of ``||A||``. Default is `0`. + cgs : bool, optional + If `True`, reorthogonalization is done using classical Gram-Schmidt. + If `False` (default), it is done using modified Gram-Schmidt. + elr : bool, optional + If `True` (default), then extended local orthogonality is enforced + when obtaining singular vectors. + min_relgap : float, optional + The smallest relative gap allowed between any shift in IRL mode. + Default is `0.001`. Accessed only if ``irl_mode=True``. + shifts : int, optional + Number of shifts per restart in IRL mode. Default is determined + to satisfy ``k <= min(kmax-shifts, m, n)``. Must be + >= 0, but choosing 0 might lead to performance degradation. + Accessed only if ``irl_mode=True``. + maxiter : int, optional + Maximum number of restarts in IRL mode. Default is `1000`. + Accessed only if ``irl_mode=True``. + random_state : {None, int, `numpy.random.Generator`, + `numpy.random.RandomState`}, optional + + Pseudorandom number generator state used to generate resamples. + + If `random_state` is ``None`` (or `np.random`), the + `numpy.random.RandomState` singleton is used. + If `random_state` is an int, a new ``RandomState`` instance is used, + seeded with `random_state`. + If `random_state` is already a ``Generator`` or ``RandomState`` + instance then that instance is used. + + Returns + ------- + u : ndarray + The `k` largest (``which="LM"``) or smallest (``which="SM"``) left + singular vectors, ``shape == (A.shape[0], 3)``, returned only if + ``compute_u=True``. + sigma : ndarray + The top `k` singular values, ``shape == (k,)`` + vt : ndarray + The `k` largest (``which="LM"``) or smallest (``which="SM"``) right + singular vectors, ``shape == (3, A.shape[1])``, returned only if + ``compute_v=True``. + sigma_bound : ndarray + the error bounds on the singular values sigma, returned only if + ``full_output=True``. + + """ + random_state = check_random_state(random_state) + + which = which.upper() + if which not in {'LM', 'SM'}: + raise ValueError("`which` must be either 'LM' or 'SM'") + if not irl_mode and which == 'SM': + raise ValueError("`which`='SM' requires irl_mode=True") + + aprod = _AProd(A) + typ = aprod.dtype.char + + try: + lansvd_irl = _lansvd_irl_dict[typ] + lansvd = _lansvd_dict[typ] + except KeyError: + # work with non-supported types using native system precision + if np.iscomplexobj(np.empty(0, dtype=typ)): + typ = np.dtype(complex).char + else: + typ = np.dtype(float).char + lansvd_irl = _lansvd_irl_dict[typ] + lansvd = _lansvd_dict[typ] + + m, n = aprod.shape + if (k < 1) or (k > min(m, n)): + raise ValueError("k must be positive and not greater than m or n") + + if kmax is None: + kmax = 10*k + if maxiter is None: + maxiter = 1000 + + # guard against unnecessarily large kmax + kmax = min(m + 1, n + 1, kmax) + if kmax < k: + raise ValueError( + "kmax must be greater than or equal to k, " + f"but kmax ({kmax}) < k ({k})") + + # convert python args to fortran args + jobu = 'y' if compute_u else 'n' + jobv = 'y' if compute_v else 'n' + + # these will be the output arrays + u = np.zeros((m, kmax + 1), order='F', dtype=typ) + v = np.zeros((n, kmax), order='F', dtype=typ) + + # Specify the starting vector. if v0 is all zero, PROPACK will generate + # a random starting vector: the random seed cannot be controlled in that + # case, so we'll instead use numpy to generate a random vector + if v0 is None: + u[:, 0] = random_state.uniform(size=m) + if np.iscomplexobj(np.empty(0, dtype=typ)): # complex type + u[:, 0] += 1j * random_state.uniform(size=m) + else: + try: + u[:, 0] = v0 + except ValueError: + raise ValueError(f"v0 must be of length {m}") + + # process options for the fit + if delta is None: + delta = np.sqrt(np.finfo(typ).eps) + if eta is None: + eta = np.finfo(typ).eps ** 0.75 + + if irl_mode: + doption = np.array((delta, eta, anorm, min_relgap), dtype=typ.lower()) + + # validate or find default shifts + if shifts is None: + shifts = kmax - k + if k > min(kmax - shifts, m, n): + raise ValueError('shifts must satisfy ' + 'k <= min(kmax-shifts, m, n)!') + elif shifts < 0: + raise ValueError('shifts must be >= 0!') + + else: + doption = np.array((delta, eta, anorm), dtype=typ.lower()) + + ioption = np.array((int(bool(cgs)), int(bool(elr))), dtype='i') + + # If computing `u` or `v` (left and right singular vectors, + # respectively), `blocksize` controls how large a fraction of the + # work is done via fast BLAS level 3 operations. A larger blocksize + # may lead to faster computation at the expense of greater memory + # consumption. `blocksize` must be ``>= 1``. Choosing blocksize + # of 16, but docs don't specify; it's almost surely a + # power of 2. + blocksize = 16 + + # Determine lwork & liwork: + # the required lengths are specified in the PROPACK documentation + if compute_u or compute_v: + lwork = m + n + 9*kmax + 5*kmax*kmax + 4 + max( + 3*kmax*kmax + 4*kmax + 4, + blocksize*max(m, n)) + liwork = 8*kmax + else: + lwork = m + n + 9*kmax + 2*kmax*kmax + 4 + max(m + n, 4*kmax + 4) + liwork = 2*kmax + 1 + work = np.empty(lwork, dtype=typ.lower()) + iwork = np.empty(liwork, dtype=np.int32) + + # dummy arguments: these are passed to aprod, and not used in this wrapper + dparm = np.empty(1, dtype=typ.lower()) + iparm = np.empty(1, dtype=np.int32) + + if typ.isupper(): + # PROPACK documentation is unclear on the required length of zwork. + # Use the same length Julia's wrapper uses + # see https://github.com/JuliaSmoothOptimizers/PROPACK.jl/ + zwork = np.empty(m + n + 32*m, dtype=typ) + works = work, zwork, iwork + else: + works = work, iwork + + if irl_mode: + u, sigma, bnd, v, info = lansvd_irl(_which_converter[which], jobu, + jobv, m, n, shifts, k, maxiter, + aprod, u, v, tol, *works, doption, + ioption, dparm, iparm) + else: + u, sigma, bnd, v, info = lansvd(jobu, jobv, m, n, k, aprod, u, v, tol, + *works, doption, ioption, dparm, iparm) + + if info > 0: + raise LinAlgError( + f"An invariant subspace of dimension {info} was found.") + elif info < 0: + raise LinAlgError( + f"k={k} singular triplets did not converge within " + f"kmax={kmax} iterations") + + # info == 0: The K largest (or smallest) singular triplets were computed + # successfully! + + return u[:, :k], sigma, v[:, :k].conj().T, bnd diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/dsolve.py b/.venv/Lib/site-packages/scipy/sparse/linalg/dsolve.py new file mode 100644 index 0000000000000000000000000000000000000000..7246069a4f43e971a48f66b1e73c47a66796797a --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/dsolve.py @@ -0,0 +1,24 @@ +# This file is not meant for public use and will be removed in SciPy v2.0.0. +# Use the `scipy.sparse.linalg` namespace for importing the functions +# included below. + +from scipy._lib.deprecation import _sub_module_deprecation + + +__all__ = [ # noqa: F822 + 'MatrixRankWarning', 'SuperLU', 'factorized', + 'spilu', 'splu', 'spsolve', + 'spsolve_triangular', 'use_solver', 'linsolve', 'test' +] + +dsolve_modules = ['linsolve'] + + +def __dir__(): + return __all__ + + +def __getattr__(name): + return _sub_module_deprecation(sub_package="sparse.linalg", module="dsolve", + private_modules=["_dsolve"], all=__all__, + attribute=name) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/eigen.py b/.venv/Lib/site-packages/scipy/sparse/linalg/eigen.py new file mode 100644 index 0000000000000000000000000000000000000000..2c07f932d1106d0669afeac830685070709071ce --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/eigen.py @@ -0,0 +1,23 @@ +# This file is not meant for public use and will be removed in SciPy v2.0.0. +# Use the `scipy.sparse.linalg` namespace for importing the functions +# included below. + +from scipy._lib.deprecation import _sub_module_deprecation + + +__all__ = [ # noqa: F822 + 'ArpackError', 'ArpackNoConvergence', 'ArpackError', + 'eigs', 'eigsh', 'lobpcg', 'svds', 'arpack', 'test' +] + +eigen_modules = ['arpack'] + + +def __dir__(): + return __all__ + + +def __getattr__(name): + return _sub_module_deprecation(sub_package="sparse.linalg", module="eigen", + private_modules=["_eigen"], all=__all__, + attribute=name) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/interface.py b/.venv/Lib/site-packages/scipy/sparse/linalg/interface.py new file mode 100644 index 0000000000000000000000000000000000000000..5185b060dc5c6f840a1bb011ca5f92bf86d55c93 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/interface.py @@ -0,0 +1,22 @@ +# This file is not meant for public use and will be removed in SciPy v2.0.0. +# Use the `scipy.sparse.linalg` namespace for importing the functions +# included below. + +from scipy._lib.deprecation import _sub_module_deprecation + + +__all__ = [ # noqa: F822 + 'LinearOperator', 'aslinearoperator', + 'isshape', 'isintlike', 'asmatrix', + 'is_pydata_spmatrix', 'MatrixLinearOperator', 'IdentityOperator' +] + + +def __dir__(): + return __all__ + + +def __getattr__(name): + return _sub_module_deprecation(sub_package="sparse.linalg", module="interface", + private_modules=["_interface"], all=__all__, + attribute=name) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/isolve.py b/.venv/Lib/site-packages/scipy/sparse/linalg/isolve.py new file mode 100644 index 0000000000000000000000000000000000000000..346899bebdc0724440cd280d7211097f6e32aa1a --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/isolve.py @@ -0,0 +1,22 @@ +# This file is not meant for public use and will be removed in SciPy v2.0.0. +# Use the `scipy.sparse.linalg` namespace for importing the functions +# included below. + +from scipy._lib.deprecation import _sub_module_deprecation + + +__all__ = [ # noqa: F822 + 'bicg', 'bicgstab', 'cg', 'cgs', 'gcrotmk', 'gmres', + 'lgmres', 'lsmr', 'lsqr', + 'minres', 'qmr', 'tfqmr', 'utils', 'iterative', 'test' +] + + +def __dir__(): + return __all__ + + +def __getattr__(name): + return _sub_module_deprecation(sub_package="sparse.linalg", module="isolve", + private_modules=["_isolve"], all=__all__, + attribute=name) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/matfuncs.py b/.venv/Lib/site-packages/scipy/sparse/linalg/matfuncs.py new file mode 100644 index 0000000000000000000000000000000000000000..cdda2416d51ed665e6762335f87718cd04a77118 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/matfuncs.py @@ -0,0 +1,22 @@ +# This file is not meant for public use and will be removed in SciPy v2.0.0. +# Use the `scipy.sparse.linalg` namespace for importing the functions +# included below. + +from scipy._lib.deprecation import _sub_module_deprecation + + +__all__ = [ # noqa: F822 + 'expm', 'inv', 'solve', 'solve_triangular', + 'spsolve', 'is_pydata_spmatrix', 'LinearOperator', + 'UPPER_TRIANGULAR', 'MatrixPowerOperator', 'ProductOperator' +] + + +def __dir__(): + return __all__ + + +def __getattr__(name): + return _sub_module_deprecation(sub_package="sparse.linalg", module="matfuncs", + private_modules=["_matfuncs"], all=__all__, + attribute=name) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/__init__.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_expm_multiply.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_expm_multiply.py new file mode 100644 index 0000000000000000000000000000000000000000..85c832a429a5d581ec6f59ecc1a64328c7802e9f --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_expm_multiply.py @@ -0,0 +1,346 @@ +"""Test functions for the sparse.linalg._expm_multiply module.""" +from functools import partial +from itertools import product + +import numpy as np +import pytest +from numpy.testing import (assert_allclose, assert_, assert_equal, + suppress_warnings) +from scipy.sparse import SparseEfficiencyWarning +from scipy.sparse.linalg import aslinearoperator +import scipy.linalg +from scipy.sparse.linalg import expm as sp_expm +from scipy.sparse.linalg._expm_multiply import (_theta, _compute_p_max, + _onenormest_matrix_power, expm_multiply, _expm_multiply_simple, + _expm_multiply_interval) +from scipy._lib._util import np_long + + +IMPRECISE = {np.single, np.csingle} +REAL_DTYPES = {np.intc, np_long, np.longlong, + np.float32, np.float64, np.longdouble} +COMPLEX_DTYPES = {np.complex64, np.complex128, np.clongdouble} +# use sorted list to ensure fixed order of tests +DTYPES = sorted(REAL_DTYPES ^ COMPLEX_DTYPES, key=str) + + +def estimated(func): + """If trace is estimated, it should warn. + + We warn that estimation of trace might impact performance. + All result have to be correct nevertheless! + + """ + def wrapped(*args, **kwds): + with pytest.warns(UserWarning, + match="Trace of LinearOperator not available"): + return func(*args, **kwds) + return wrapped + + +def less_than_or_close(a, b): + return np.allclose(a, b) or (a < b) + + +class TestExpmActionSimple: + """ + These tests do not consider the case of multiple time steps in one call. + """ + + def test_theta_monotonicity(self): + pairs = sorted(_theta.items()) + for (m_a, theta_a), (m_b, theta_b) in zip(pairs[:-1], pairs[1:]): + assert_(theta_a < theta_b) + + def test_p_max_default(self): + m_max = 55 + expected_p_max = 8 + observed_p_max = _compute_p_max(m_max) + assert_equal(observed_p_max, expected_p_max) + + def test_p_max_range(self): + for m_max in range(1, 55+1): + p_max = _compute_p_max(m_max) + assert_(p_max*(p_max - 1) <= m_max + 1) + p_too_big = p_max + 1 + assert_(p_too_big*(p_too_big - 1) > m_max + 1) + + def test_onenormest_matrix_power(self): + np.random.seed(1234) + n = 40 + nsamples = 10 + for i in range(nsamples): + A = scipy.linalg.inv(np.random.randn(n, n)) + for p in range(4): + if not p: + M = np.identity(n) + else: + M = np.dot(M, A) + estimated = _onenormest_matrix_power(A, p) + exact = np.linalg.norm(M, 1) + assert_(less_than_or_close(estimated, exact)) + assert_(less_than_or_close(exact, 3*estimated)) + + def test_expm_multiply(self): + np.random.seed(1234) + n = 40 + k = 3 + nsamples = 10 + for i in range(nsamples): + A = scipy.linalg.inv(np.random.randn(n, n)) + B = np.random.randn(n, k) + observed = expm_multiply(A, B) + expected = np.dot(sp_expm(A), B) + assert_allclose(observed, expected) + observed = estimated(expm_multiply)(aslinearoperator(A), B) + assert_allclose(observed, expected) + traceA = np.trace(A) + observed = expm_multiply(aslinearoperator(A), B, traceA=traceA) + assert_allclose(observed, expected) + + def test_matrix_vector_multiply(self): + np.random.seed(1234) + n = 40 + nsamples = 10 + for i in range(nsamples): + A = scipy.linalg.inv(np.random.randn(n, n)) + v = np.random.randn(n) + observed = expm_multiply(A, v) + expected = np.dot(sp_expm(A), v) + assert_allclose(observed, expected) + observed = estimated(expm_multiply)(aslinearoperator(A), v) + assert_allclose(observed, expected) + + def test_scaled_expm_multiply(self): + np.random.seed(1234) + n = 40 + k = 3 + nsamples = 10 + for i, t in product(range(nsamples), [0.2, 1.0, 1.5]): + with np.errstate(invalid='ignore'): + A = scipy.linalg.inv(np.random.randn(n, n)) + B = np.random.randn(n, k) + observed = _expm_multiply_simple(A, B, t=t) + expected = np.dot(sp_expm(t*A), B) + assert_allclose(observed, expected) + observed = estimated(_expm_multiply_simple)( + aslinearoperator(A), B, t=t + ) + assert_allclose(observed, expected) + + def test_scaled_expm_multiply_single_timepoint(self): + np.random.seed(1234) + t = 0.1 + n = 5 + k = 2 + A = np.random.randn(n, n) + B = np.random.randn(n, k) + observed = _expm_multiply_simple(A, B, t=t) + expected = sp_expm(t*A).dot(B) + assert_allclose(observed, expected) + observed = estimated(_expm_multiply_simple)( + aslinearoperator(A), B, t=t + ) + assert_allclose(observed, expected) + + def test_sparse_expm_multiply(self): + np.random.seed(1234) + n = 40 + k = 3 + nsamples = 10 + for i in range(nsamples): + A = scipy.sparse.rand(n, n, density=0.05) + B = np.random.randn(n, k) + observed = expm_multiply(A, B) + with suppress_warnings() as sup: + sup.filter(SparseEfficiencyWarning, + "splu converted its input to CSC format") + sup.filter(SparseEfficiencyWarning, + "spsolve is more efficient when sparse b is in the" + " CSC matrix format") + expected = sp_expm(A).dot(B) + assert_allclose(observed, expected) + observed = estimated(expm_multiply)(aslinearoperator(A), B) + assert_allclose(observed, expected) + + def test_complex(self): + A = np.array([ + [1j, 1j], + [0, 1j]], dtype=complex) + B = np.array([1j, 1j]) + observed = expm_multiply(A, B) + expected = np.array([ + 1j * np.exp(1j) + 1j * (1j*np.cos(1) - np.sin(1)), + 1j * np.exp(1j)], dtype=complex) + assert_allclose(observed, expected) + observed = estimated(expm_multiply)(aslinearoperator(A), B) + assert_allclose(observed, expected) + + +class TestExpmActionInterval: + + def test_sparse_expm_multiply_interval(self): + np.random.seed(1234) + start = 0.1 + stop = 3.2 + n = 40 + k = 3 + endpoint = True + for num in (14, 13, 2): + A = scipy.sparse.rand(n, n, density=0.05) + B = np.random.randn(n, k) + v = np.random.randn(n) + for target in (B, v): + X = expm_multiply(A, target, start=start, stop=stop, + num=num, endpoint=endpoint) + samples = np.linspace(start=start, stop=stop, + num=num, endpoint=endpoint) + with suppress_warnings() as sup: + sup.filter(SparseEfficiencyWarning, + "splu converted its input to CSC format") + sup.filter(SparseEfficiencyWarning, + "spsolve is more efficient when sparse b is in" + " the CSC matrix format") + for solution, t in zip(X, samples): + assert_allclose(solution, sp_expm(t*A).dot(target)) + + def test_expm_multiply_interval_vector(self): + np.random.seed(1234) + interval = {'start': 0.1, 'stop': 3.2, 'endpoint': True} + for num, n in product([14, 13, 2], [1, 2, 5, 20, 40]): + A = scipy.linalg.inv(np.random.randn(n, n)) + v = np.random.randn(n) + samples = np.linspace(num=num, **interval) + X = expm_multiply(A, v, num=num, **interval) + for solution, t in zip(X, samples): + assert_allclose(solution, sp_expm(t*A).dot(v)) + # test for linear operator with unknown trace -> estimate trace + Xguess = estimated(expm_multiply)(aslinearoperator(A), v, + num=num, **interval) + # test for linear operator with given trace + Xgiven = expm_multiply(aslinearoperator(A), v, num=num, **interval, + traceA=np.trace(A)) + # test robustness for linear operator with wrong trace + Xwrong = expm_multiply(aslinearoperator(A), v, num=num, **interval, + traceA=np.trace(A)*5) + for sol_guess, sol_given, sol_wrong, t in zip(Xguess, Xgiven, + Xwrong, samples): + correct = sp_expm(t*A).dot(v) + assert_allclose(sol_guess, correct) + assert_allclose(sol_given, correct) + assert_allclose(sol_wrong, correct) + + def test_expm_multiply_interval_matrix(self): + np.random.seed(1234) + interval = {'start': 0.1, 'stop': 3.2, 'endpoint': True} + for num, n, k in product([14, 13, 2], [1, 2, 5, 20, 40], [1, 2]): + A = scipy.linalg.inv(np.random.randn(n, n)) + B = np.random.randn(n, k) + samples = np.linspace(num=num, **interval) + X = expm_multiply(A, B, num=num, **interval) + for solution, t in zip(X, samples): + assert_allclose(solution, sp_expm(t*A).dot(B)) + X = estimated(expm_multiply)(aslinearoperator(A), B, num=num, + **interval) + for solution, t in zip(X, samples): + assert_allclose(solution, sp_expm(t*A).dot(B)) + + def test_sparse_expm_multiply_interval_dtypes(self): + # Test A & B int + A = scipy.sparse.diags(np.arange(5),format='csr', dtype=int) + B = np.ones(5, dtype=int) + Aexpm = scipy.sparse.diags(np.exp(np.arange(5)),format='csr') + assert_allclose(expm_multiply(A,B,0,1)[-1], Aexpm.dot(B)) + + # Test A complex, B int + A = scipy.sparse.diags(-1j*np.arange(5),format='csr', dtype=complex) + B = np.ones(5, dtype=int) + Aexpm = scipy.sparse.diags(np.exp(-1j*np.arange(5)),format='csr') + assert_allclose(expm_multiply(A,B,0,1)[-1], Aexpm.dot(B)) + + # Test A int, B complex + A = scipy.sparse.diags(np.arange(5),format='csr', dtype=int) + B = np.full(5, 1j, dtype=complex) + Aexpm = scipy.sparse.diags(np.exp(np.arange(5)),format='csr') + assert_allclose(expm_multiply(A,B,0,1)[-1], Aexpm.dot(B)) + + def test_expm_multiply_interval_status_0(self): + self._help_test_specific_expm_interval_status(0) + + def test_expm_multiply_interval_status_1(self): + self._help_test_specific_expm_interval_status(1) + + def test_expm_multiply_interval_status_2(self): + self._help_test_specific_expm_interval_status(2) + + def _help_test_specific_expm_interval_status(self, target_status): + np.random.seed(1234) + start = 0.1 + stop = 3.2 + num = 13 + endpoint = True + n = 5 + k = 2 + nrepeats = 10 + nsuccesses = 0 + for num in [14, 13, 2] * nrepeats: + A = np.random.randn(n, n) + B = np.random.randn(n, k) + status = _expm_multiply_interval(A, B, + start=start, stop=stop, num=num, endpoint=endpoint, + status_only=True) + if status == target_status: + X, status = _expm_multiply_interval(A, B, + start=start, stop=stop, num=num, endpoint=endpoint, + status_only=False) + assert_equal(X.shape, (num, n, k)) + samples = np.linspace(start=start, stop=stop, + num=num, endpoint=endpoint) + for solution, t in zip(X, samples): + assert_allclose(solution, sp_expm(t*A).dot(B)) + nsuccesses += 1 + if not nsuccesses: + msg = 'failed to find a status-' + str(target_status) + ' interval' + raise Exception(msg) + + +@pytest.mark.parametrize("dtype_a", DTYPES) +@pytest.mark.parametrize("dtype_b", DTYPES) +@pytest.mark.parametrize("b_is_matrix", [False, True]) +def test_expm_multiply_dtype(dtype_a, dtype_b, b_is_matrix): + """Make sure `expm_multiply` handles all numerical dtypes correctly.""" + assert_allclose_ = (partial(assert_allclose, rtol=1.2e-3, atol=1e-5) + if {dtype_a, dtype_b} & IMPRECISE else assert_allclose) + rng = np.random.default_rng(1234) + # test data + n = 7 + b_shape = (n, 3) if b_is_matrix else (n, ) + if dtype_a in REAL_DTYPES: + A = scipy.linalg.inv(rng.random([n, n])).astype(dtype_a) + else: + A = scipy.linalg.inv( + rng.random([n, n]) + 1j*rng.random([n, n]) + ).astype(dtype_a) + if dtype_b in REAL_DTYPES: + B = (2*rng.random(b_shape)).astype(dtype_b) + else: + B = (rng.random(b_shape) + 1j*rng.random(b_shape)).astype(dtype_b) + + # single application + sol_mat = expm_multiply(A, B) + sol_op = estimated(expm_multiply)(aslinearoperator(A), B) + direct_sol = np.dot(sp_expm(A), B) + assert_allclose_(sol_mat, direct_sol) + assert_allclose_(sol_op, direct_sol) + sol_op = expm_multiply(aslinearoperator(A), B, traceA=np.trace(A)) + assert_allclose_(sol_op, direct_sol) + + # for time points + interval = {'start': 0.1, 'stop': 3.2, 'num': 13, 'endpoint': True} + samples = np.linspace(**interval) + X_mat = expm_multiply(A, B, **interval) + X_op = estimated(expm_multiply)(aslinearoperator(A), B, **interval) + for sol_mat, sol_op, t in zip(X_mat, X_op, samples): + direct_sol = sp_expm(t*A).dot(B) + assert_allclose_(sol_mat, direct_sol) + assert_allclose_(sol_op, direct_sol) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_interface.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_interface.py new file mode 100644 index 0000000000000000000000000000000000000000..2a8ae6bce6d429b40bcfc14079e2558b72f96e24 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_interface.py @@ -0,0 +1,481 @@ +"""Test functions for the sparse.linalg._interface module +""" + +from functools import partial +from itertools import product +import operator +from pytest import raises as assert_raises, warns +from numpy.testing import assert_, assert_equal + +import numpy as np +import scipy.sparse as sparse + +import scipy.sparse.linalg._interface as interface +from scipy.sparse._sputils import matrix + + +class TestLinearOperator: + def setup_method(self): + self.A = np.array([[1,2,3], + [4,5,6]]) + self.B = np.array([[1,2], + [3,4], + [5,6]]) + self.C = np.array([[1,2], + [3,4]]) + + def test_matvec(self): + def get_matvecs(A): + return [{ + 'shape': A.shape, + 'matvec': lambda x: np.dot(A, x).reshape(A.shape[0]), + 'rmatvec': lambda x: np.dot(A.T.conj(), + x).reshape(A.shape[1]) + }, + { + 'shape': A.shape, + 'matvec': lambda x: np.dot(A, x), + 'rmatvec': lambda x: np.dot(A.T.conj(), x), + 'rmatmat': lambda x: np.dot(A.T.conj(), x), + 'matmat': lambda x: np.dot(A, x) + }] + + for matvecs in get_matvecs(self.A): + A = interface.LinearOperator(**matvecs) + + assert_(A.args == ()) + + assert_equal(A.matvec(np.array([1,2,3])), [14,32]) + assert_equal(A.matvec(np.array([[1],[2],[3]])), [[14],[32]]) + assert_equal(A * np.array([1,2,3]), [14,32]) + assert_equal(A * np.array([[1],[2],[3]]), [[14],[32]]) + assert_equal(A.dot(np.array([1,2,3])), [14,32]) + assert_equal(A.dot(np.array([[1],[2],[3]])), [[14],[32]]) + + assert_equal(A.matvec(matrix([[1],[2],[3]])), [[14],[32]]) + assert_equal(A * matrix([[1],[2],[3]]), [[14],[32]]) + assert_equal(A.dot(matrix([[1],[2],[3]])), [[14],[32]]) + + assert_equal((2*A)*[1,1,1], [12,30]) + assert_equal((2 * A).rmatvec([1, 1]), [10, 14, 18]) + assert_equal((2*A).H.matvec([1,1]), [10, 14, 18]) + assert_equal((2*A)*[[1],[1],[1]], [[12],[30]]) + assert_equal((2 * A).matmat([[1], [1], [1]]), [[12], [30]]) + assert_equal((A*2)*[1,1,1], [12,30]) + assert_equal((A*2)*[[1],[1],[1]], [[12],[30]]) + assert_equal((2j*A)*[1,1,1], [12j,30j]) + assert_equal((A+A)*[1,1,1], [12, 30]) + assert_equal((A + A).rmatvec([1, 1]), [10, 14, 18]) + assert_equal((A+A).H.matvec([1,1]), [10, 14, 18]) + assert_equal((A+A)*[[1],[1],[1]], [[12], [30]]) + assert_equal((A+A).matmat([[1],[1],[1]]), [[12], [30]]) + assert_equal((-A)*[1,1,1], [-6,-15]) + assert_equal((-A)*[[1],[1],[1]], [[-6],[-15]]) + assert_equal((A-A)*[1,1,1], [0,0]) + assert_equal((A - A) * [[1], [1], [1]], [[0], [0]]) + + X = np.array([[1, 2], [3, 4]]) + # A_asarray = np.array([[1, 2, 3], [4, 5, 6]]) + assert_equal((2 * A).rmatmat(X), np.dot((2 * self.A).T, X)) + assert_equal((A * 2).rmatmat(X), np.dot((self.A * 2).T, X)) + assert_equal((2j * A).rmatmat(X), + np.dot((2j * self.A).T.conj(), X)) + assert_equal((A * 2j).rmatmat(X), + np.dot((self.A * 2j).T.conj(), X)) + assert_equal((A + A).rmatmat(X), + np.dot((self.A + self.A).T, X)) + assert_equal((A + 2j * A).rmatmat(X), + np.dot((self.A + 2j * self.A).T.conj(), X)) + assert_equal((-A).rmatmat(X), np.dot((-self.A).T, X)) + assert_equal((A - A).rmatmat(X), + np.dot((self.A - self.A).T, X)) + assert_equal((2j * A).rmatmat(2j * X), + np.dot((2j * self.A).T.conj(), 2j * X)) + + z = A+A + assert_(len(z.args) == 2 and z.args[0] is A and z.args[1] is A) + z = 2*A + assert_(len(z.args) == 2 and z.args[0] is A and z.args[1] == 2) + + assert_(isinstance(A.matvec([1, 2, 3]), np.ndarray)) + assert_(isinstance(A.matvec(np.array([[1],[2],[3]])), np.ndarray)) + assert_(isinstance(A * np.array([1,2,3]), np.ndarray)) + assert_(isinstance(A * np.array([[1],[2],[3]]), np.ndarray)) + assert_(isinstance(A.dot(np.array([1,2,3])), np.ndarray)) + assert_(isinstance(A.dot(np.array([[1],[2],[3]])), np.ndarray)) + + assert_(isinstance(A.matvec(matrix([[1],[2],[3]])), np.ndarray)) + assert_(isinstance(A * matrix([[1],[2],[3]]), np.ndarray)) + assert_(isinstance(A.dot(matrix([[1],[2],[3]])), np.ndarray)) + + assert_(isinstance(2*A, interface._ScaledLinearOperator)) + assert_(isinstance(2j*A, interface._ScaledLinearOperator)) + assert_(isinstance(A+A, interface._SumLinearOperator)) + assert_(isinstance(-A, interface._ScaledLinearOperator)) + assert_(isinstance(A-A, interface._SumLinearOperator)) + assert_(isinstance(A/2, interface._ScaledLinearOperator)) + assert_(isinstance(A/2j, interface._ScaledLinearOperator)) + assert_(((A * 3) / 3).args[0] is A) # check for simplification + + # Test that prefactor is of _ScaledLinearOperator is not mutated + # when the operator is multiplied by a number + result = A @ np.array([1, 2, 3]) + B = A * 3 + C = A / 5 + assert_equal(A @ np.array([1, 2, 3]), result) + + assert_((2j*A).dtype == np.complex128) + + # Test division by non-scalar + msg = "Can only divide a linear operator by a scalar." + with assert_raises(ValueError, match=msg): + A / np.array([1, 2]) + + assert_raises(ValueError, A.matvec, np.array([1,2])) + assert_raises(ValueError, A.matvec, np.array([1,2,3,4])) + assert_raises(ValueError, A.matvec, np.array([[1],[2]])) + assert_raises(ValueError, A.matvec, np.array([[1],[2],[3],[4]])) + + assert_raises(ValueError, lambda: A*A) + assert_raises(ValueError, lambda: A**2) + + for matvecsA, matvecsB in product(get_matvecs(self.A), + get_matvecs(self.B)): + A = interface.LinearOperator(**matvecsA) + B = interface.LinearOperator(**matvecsB) + # AtimesB = np.array([[22, 28], [49, 64]]) + AtimesB = self.A.dot(self.B) + X = np.array([[1, 2], [3, 4]]) + + assert_equal((A * B).rmatmat(X), np.dot((AtimesB).T, X)) + assert_equal((2j * A * B).rmatmat(X), + np.dot((2j * AtimesB).T.conj(), X)) + + assert_equal((A*B)*[1,1], [50,113]) + assert_equal((A*B)*[[1],[1]], [[50],[113]]) + assert_equal((A*B).matmat([[1],[1]]), [[50],[113]]) + + assert_equal((A * B).rmatvec([1, 1]), [71, 92]) + assert_equal((A * B).H.matvec([1, 1]), [71, 92]) + + assert_(isinstance(A*B, interface._ProductLinearOperator)) + + assert_raises(ValueError, lambda: A+B) + assert_raises(ValueError, lambda: A**2) + + z = A*B + assert_(len(z.args) == 2 and z.args[0] is A and z.args[1] is B) + + for matvecsC in get_matvecs(self.C): + C = interface.LinearOperator(**matvecsC) + X = np.array([[1, 2], [3, 4]]) + + assert_equal(C.rmatmat(X), np.dot((self.C).T, X)) + assert_equal((C**2).rmatmat(X), + np.dot((np.dot(self.C, self.C)).T, X)) + + assert_equal((C**2)*[1,1], [17,37]) + assert_equal((C**2).rmatvec([1, 1]), [22, 32]) + assert_equal((C**2).H.matvec([1, 1]), [22, 32]) + assert_equal((C**2).matmat([[1],[1]]), [[17],[37]]) + + assert_(isinstance(C**2, interface._PowerLinearOperator)) + + def test_matmul(self): + D = {'shape': self.A.shape, + 'matvec': lambda x: np.dot(self.A, x).reshape(self.A.shape[0]), + 'rmatvec': lambda x: np.dot(self.A.T.conj(), + x).reshape(self.A.shape[1]), + 'rmatmat': lambda x: np.dot(self.A.T.conj(), x), + 'matmat': lambda x: np.dot(self.A, x)} + A = interface.LinearOperator(**D) + B = np.array([[1 + 1j, 2, 3], + [4, 5, 6], + [7, 8, 9]]) + b = B[0] + + assert_equal(operator.matmul(A, b), A * b) + assert_equal(operator.matmul(A, b.reshape(-1, 1)), A * b.reshape(-1, 1)) + assert_equal(operator.matmul(A, B), A * B) + assert_equal(operator.matmul(b, A.H), b * A.H) + assert_equal(operator.matmul(b.reshape(1, -1), A.H), b.reshape(1, -1) * A.H) + assert_equal(operator.matmul(B, A.H), B * A.H) + assert_raises(ValueError, operator.matmul, A, 2) + assert_raises(ValueError, operator.matmul, 2, A) + + +class TestAsLinearOperator: + def setup_method(self): + self.cases = [] + + def make_cases(original, dtype): + cases = [] + + cases.append((matrix(original, dtype=dtype), original)) + cases.append((np.array(original, dtype=dtype), original)) + cases.append((sparse.csr_matrix(original, dtype=dtype), original)) + + # Test default implementations of _adjoint and _rmatvec, which + # refer to each other. + def mv(x, dtype): + y = original.dot(x) + if len(x.shape) == 2: + y = y.reshape(-1, 1) + return y + + def rmv(x, dtype): + return original.T.conj().dot(x) + + class BaseMatlike(interface.LinearOperator): + args = () + + def __init__(self, dtype): + self.dtype = np.dtype(dtype) + self.shape = original.shape + + def _matvec(self, x): + return mv(x, self.dtype) + + class HasRmatvec(BaseMatlike): + args = () + + def _rmatvec(self,x): + return rmv(x, self.dtype) + + class HasAdjoint(BaseMatlike): + args = () + + def _adjoint(self): + shape = self.shape[1], self.shape[0] + matvec = partial(rmv, dtype=self.dtype) + rmatvec = partial(mv, dtype=self.dtype) + return interface.LinearOperator(matvec=matvec, + rmatvec=rmatvec, + dtype=self.dtype, + shape=shape) + + class HasRmatmat(HasRmatvec): + def _matmat(self, x): + return original.dot(x) + + def _rmatmat(self, x): + return original.T.conj().dot(x) + + cases.append((HasRmatvec(dtype), original)) + cases.append((HasAdjoint(dtype), original)) + cases.append((HasRmatmat(dtype), original)) + return cases + + original = np.array([[1,2,3], [4,5,6]]) + self.cases += make_cases(original, np.int32) + self.cases += make_cases(original, np.float32) + self.cases += make_cases(original, np.float64) + self.cases += [(interface.aslinearoperator(M).T, A.T) + for M, A in make_cases(original.T, np.float64)] + self.cases += [(interface.aslinearoperator(M).H, A.T.conj()) + for M, A in make_cases(original.T, np.float64)] + + original = np.array([[1, 2j, 3j], [4j, 5j, 6]]) + self.cases += make_cases(original, np.complex128) + self.cases += [(interface.aslinearoperator(M).T, A.T) + for M, A in make_cases(original.T, np.complex128)] + self.cases += [(interface.aslinearoperator(M).H, A.T.conj()) + for M, A in make_cases(original.T, np.complex128)] + + def test_basic(self): + + for M, A_array in self.cases: + A = interface.aslinearoperator(M) + M,N = A.shape + + xs = [np.array([1, 2, 3]), + np.array([[1], [2], [3]])] + ys = [np.array([1, 2]), np.array([[1], [2]])] + + if A.dtype == np.complex128: + xs += [np.array([1, 2j, 3j]), + np.array([[1], [2j], [3j]])] + ys += [np.array([1, 2j]), np.array([[1], [2j]])] + + x2 = np.array([[1, 4], [2, 5], [3, 6]]) + + for x in xs: + assert_equal(A.matvec(x), A_array.dot(x)) + assert_equal(A * x, A_array.dot(x)) + + assert_equal(A.matmat(x2), A_array.dot(x2)) + assert_equal(A * x2, A_array.dot(x2)) + + for y in ys: + assert_equal(A.rmatvec(y), A_array.T.conj().dot(y)) + assert_equal(A.T.matvec(y), A_array.T.dot(y)) + assert_equal(A.H.matvec(y), A_array.T.conj().dot(y)) + + for y in ys: + if y.ndim < 2: + continue + assert_equal(A.rmatmat(y), A_array.T.conj().dot(y)) + assert_equal(A.T.matmat(y), A_array.T.dot(y)) + assert_equal(A.H.matmat(y), A_array.T.conj().dot(y)) + + if hasattr(M,'dtype'): + assert_equal(A.dtype, M.dtype) + + assert_(hasattr(A, 'args')) + + def test_dot(self): + + for M, A_array in self.cases: + A = interface.aslinearoperator(M) + M,N = A.shape + + x0 = np.array([1, 2, 3]) + x1 = np.array([[1], [2], [3]]) + x2 = np.array([[1, 4], [2, 5], [3, 6]]) + + assert_equal(A.dot(x0), A_array.dot(x0)) + assert_equal(A.dot(x1), A_array.dot(x1)) + assert_equal(A.dot(x2), A_array.dot(x2)) + + +def test_repr(): + A = interface.LinearOperator(shape=(1, 1), matvec=lambda x: 1) + repr_A = repr(A) + assert_('unspecified dtype' not in repr_A, repr_A) + + +def test_identity(): + ident = interface.IdentityOperator((3, 3)) + assert_equal(ident * [1, 2, 3], [1, 2, 3]) + assert_equal(ident.dot(np.arange(9).reshape(3, 3)).ravel(), np.arange(9)) + + assert_raises(ValueError, ident.matvec, [1, 2, 3, 4]) + + +def test_attributes(): + A = interface.aslinearoperator(np.arange(16).reshape(4, 4)) + + def always_four_ones(x): + x = np.asarray(x) + assert_(x.shape == (3,) or x.shape == (3, 1)) + return np.ones(4) + + B = interface.LinearOperator(shape=(4, 3), matvec=always_four_ones) + + for op in [A, B, A * B, A.H, A + A, B + B, A**4]: + assert_(hasattr(op, "dtype")) + assert_(hasattr(op, "shape")) + assert_(hasattr(op, "_matvec")) + +def matvec(x): + """ Needed for test_pickle as local functions are not pickleable """ + return np.zeros(3) + +def test_pickle(): + import pickle + + for protocol in range(pickle.HIGHEST_PROTOCOL + 1): + A = interface.LinearOperator((3, 3), matvec) + s = pickle.dumps(A, protocol=protocol) + B = pickle.loads(s) + + for k in A.__dict__: + assert_equal(getattr(A, k), getattr(B, k)) + +def test_inheritance(): + class Empty(interface.LinearOperator): + pass + + with warns(RuntimeWarning, match="should implement at least"): + assert_raises(TypeError, Empty) + + class Identity(interface.LinearOperator): + def __init__(self, n): + super().__init__(dtype=None, shape=(n, n)) + + def _matvec(self, x): + return x + + id3 = Identity(3) + assert_equal(id3.matvec([1, 2, 3]), [1, 2, 3]) + assert_raises(NotImplementedError, id3.rmatvec, [4, 5, 6]) + + class MatmatOnly(interface.LinearOperator): + def __init__(self, A): + super().__init__(A.dtype, A.shape) + self.A = A + + def _matmat(self, x): + return self.A.dot(x) + + mm = MatmatOnly(np.random.randn(5, 3)) + assert_equal(mm.matvec(np.random.randn(3)).shape, (5,)) + +def test_dtypes_of_operator_sum(): + # gh-6078 + + mat_complex = np.random.rand(2,2) + 1j * np.random.rand(2,2) + mat_real = np.random.rand(2,2) + + complex_operator = interface.aslinearoperator(mat_complex) + real_operator = interface.aslinearoperator(mat_real) + + sum_complex = complex_operator + complex_operator + sum_real = real_operator + real_operator + + assert_equal(sum_real.dtype, np.float64) + assert_equal(sum_complex.dtype, np.complex128) + +def test_no_double_init(): + call_count = [0] + + def matvec(v): + call_count[0] += 1 + return v + + # It should call matvec exactly once (in order to determine the + # operator dtype) + interface.LinearOperator((2, 2), matvec=matvec) + assert_equal(call_count[0], 1) + +def test_adjoint_conjugate(): + X = np.array([[1j]]) + A = interface.aslinearoperator(X) + + B = 1j * A + Y = 1j * X + + v = np.array([1]) + + assert_equal(B.dot(v), Y.dot(v)) + assert_equal(B.H.dot(v), Y.T.conj().dot(v)) + +def test_ndim(): + X = np.array([[1]]) + A = interface.aslinearoperator(X) + assert_equal(A.ndim, 2) + +def test_transpose_noconjugate(): + X = np.array([[1j]]) + A = interface.aslinearoperator(X) + + B = 1j * A + Y = 1j * X + + v = np.array([1]) + + assert_equal(B.dot(v), Y.dot(v)) + assert_equal(B.T.dot(v), Y.T.dot(v)) + +def test_sparse_matmat_exception(): + A = interface.LinearOperator((2, 2), matvec=lambda x: x) + B = sparse.identity(2) + msg = "Unable to multiply a LinearOperator with a sparse matrix." + with assert_raises(TypeError, match=msg): + A @ B + with assert_raises(TypeError, match=msg): + B @ A + with assert_raises(ValueError): + A @ np.identity(4) + with assert_raises(ValueError): + np.identity(4) @ A diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_matfuncs.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_matfuncs.py new file mode 100644 index 0000000000000000000000000000000000000000..3e3ba5e18d8b28b8ad45ae1073bcd172c6df22c6 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_matfuncs.py @@ -0,0 +1,598 @@ +# +# Created by: Pearu Peterson, March 2002 +# +""" Test functions for scipy.linalg._matfuncs module + +""" +import math + +import numpy as np +from numpy import array, eye, exp, random +from numpy.testing import ( + assert_allclose, assert_, assert_array_almost_equal, assert_equal, + assert_array_almost_equal_nulp, suppress_warnings) + +from scipy.sparse import csc_matrix, csc_array, SparseEfficiencyWarning +from scipy.sparse._construct import eye as speye +from scipy.sparse.linalg._matfuncs import (expm, _expm, + ProductOperator, MatrixPowerOperator, + _onenorm_matrix_power_nnm, matrix_power) +from scipy.sparse._sputils import matrix +from scipy.linalg import logm +from scipy.special import factorial, binom +import scipy.sparse +import scipy.sparse.linalg + + +def _burkardt_13_power(n, p): + """ + A helper function for testing matrix functions. + + Parameters + ---------- + n : integer greater than 1 + Order of the square matrix to be returned. + p : non-negative integer + Power of the matrix. + + Returns + ------- + out : ndarray representing a square matrix + A Forsythe matrix of order n, raised to the power p. + + """ + # Input validation. + if n != int(n) or n < 2: + raise ValueError('n must be an integer greater than 1') + n = int(n) + if p != int(p) or p < 0: + raise ValueError('p must be a non-negative integer') + p = int(p) + + # Construct the matrix explicitly. + a, b = divmod(p, n) + large = np.power(10.0, -n*a) + small = large * np.power(10.0, -n) + return np.diag([large]*(n-b), b) + np.diag([small]*b, b-n) + + +def test_onenorm_matrix_power_nnm(): + np.random.seed(1234) + for n in range(1, 5): + for p in range(5): + M = np.random.random((n, n)) + Mp = np.linalg.matrix_power(M, p) + observed = _onenorm_matrix_power_nnm(M, p) + expected = np.linalg.norm(Mp, 1) + assert_allclose(observed, expected) + +def test_matrix_power(): + np.random.seed(1234) + row, col = np.random.randint(0, 4, size=(2, 6)) + data = np.random.random(size=(6,)) + Amat = csc_matrix((data, (row, col)), shape=(4, 4)) + A = csc_array((data, (row, col)), shape=(4, 4)) + Adense = A.toarray() + for power in (2, 5, 6): + Apow = matrix_power(A, power).toarray() + Amat_pow = (Amat**power).toarray() + Adense_pow = np.linalg.matrix_power(Adense, power) + assert_allclose(Apow, Adense_pow) + assert_allclose(Apow, Amat_pow) + + +class TestExpM: + def test_zero_ndarray(self): + a = array([[0.,0],[0,0]]) + assert_array_almost_equal(expm(a),[[1,0],[0,1]]) + + def test_zero_sparse(self): + a = csc_matrix([[0.,0],[0,0]]) + assert_array_almost_equal(expm(a).toarray(),[[1,0],[0,1]]) + + def test_zero_matrix(self): + a = matrix([[0.,0],[0,0]]) + assert_array_almost_equal(expm(a),[[1,0],[0,1]]) + + def test_misc_types(self): + A = expm(np.array([[1]])) + assert_allclose(expm(((1,),)), A) + assert_allclose(expm([[1]]), A) + assert_allclose(expm(matrix([[1]])), A) + assert_allclose(expm(np.array([[1]])), A) + assert_allclose(expm(csc_matrix([[1]])).A, A) + B = expm(np.array([[1j]])) + assert_allclose(expm(((1j,),)), B) + assert_allclose(expm([[1j]]), B) + assert_allclose(expm(matrix([[1j]])), B) + assert_allclose(expm(csc_matrix([[1j]])).A, B) + + def test_bidiagonal_sparse(self): + A = csc_matrix([ + [1, 3, 0], + [0, 1, 5], + [0, 0, 2]], dtype=float) + e1 = math.exp(1) + e2 = math.exp(2) + expected = np.array([ + [e1, 3*e1, 15*(e2 - 2*e1)], + [0, e1, 5*(e2 - e1)], + [0, 0, e2]], dtype=float) + observed = expm(A).toarray() + assert_array_almost_equal(observed, expected) + + def test_padecases_dtype_float(self): + for dtype in [np.float32, np.float64]: + for scale in [1e-2, 1e-1, 5e-1, 1, 10]: + A = scale * eye(3, dtype=dtype) + observed = expm(A) + expected = exp(scale, dtype=dtype) * eye(3, dtype=dtype) + assert_array_almost_equal_nulp(observed, expected, nulp=100) + + def test_padecases_dtype_complex(self): + for dtype in [np.complex64, np.complex128]: + for scale in [1e-2, 1e-1, 5e-1, 1, 10]: + A = scale * eye(3, dtype=dtype) + observed = expm(A) + expected = exp(scale, dtype=dtype) * eye(3, dtype=dtype) + assert_array_almost_equal_nulp(observed, expected, nulp=100) + + def test_padecases_dtype_sparse_float(self): + # float32 and complex64 lead to errors in spsolve/UMFpack + dtype = np.float64 + for scale in [1e-2, 1e-1, 5e-1, 1, 10]: + a = scale * speye(3, 3, dtype=dtype, format='csc') + e = exp(scale, dtype=dtype) * eye(3, dtype=dtype) + with suppress_warnings() as sup: + sup.filter( + SparseEfficiencyWarning, + "Changing the sparsity structure of a csc_matrix is expensive." + ) + exact_onenorm = _expm(a, use_exact_onenorm=True).toarray() + inexact_onenorm = _expm(a, use_exact_onenorm=False).toarray() + assert_array_almost_equal_nulp(exact_onenorm, e, nulp=100) + assert_array_almost_equal_nulp(inexact_onenorm, e, nulp=100) + + def test_padecases_dtype_sparse_complex(self): + # float32 and complex64 lead to errors in spsolve/UMFpack + dtype = np.complex128 + for scale in [1e-2, 1e-1, 5e-1, 1, 10]: + a = scale * speye(3, 3, dtype=dtype, format='csc') + e = exp(scale) * eye(3, dtype=dtype) + with suppress_warnings() as sup: + sup.filter( + SparseEfficiencyWarning, + "Changing the sparsity structure of a csc_matrix is expensive." + ) + assert_array_almost_equal_nulp(expm(a).toarray(), e, nulp=100) + + def test_logm_consistency(self): + random.seed(1234) + for dtype in [np.float64, np.complex128]: + for n in range(1, 10): + for scale in [1e-4, 1e-3, 1e-2, 1e-1, 1, 1e1, 1e2]: + # make logm(A) be of a given scale + A = (eye(n) + random.rand(n, n) * scale).astype(dtype) + if np.iscomplexobj(A): + A = A + 1j * random.rand(n, n) * scale + assert_array_almost_equal(expm(logm(A)), A) + + def test_integer_matrix(self): + Q = np.array([ + [-3, 1, 1, 1], + [1, -3, 1, 1], + [1, 1, -3, 1], + [1, 1, 1, -3]]) + assert_allclose(expm(Q), expm(1.0 * Q)) + + def test_integer_matrix_2(self): + # Check for integer overflows + Q = np.array([[-500, 500, 0, 0], + [0, -550, 360, 190], + [0, 630, -630, 0], + [0, 0, 0, 0]], dtype=np.int16) + assert_allclose(expm(Q), expm(1.0 * Q)) + + Q = csc_matrix(Q) + assert_allclose(expm(Q).A, expm(1.0 * Q).A) + + def test_triangularity_perturbation(self): + # Experiment (1) of + # Awad H. Al-Mohy and Nicholas J. Higham (2012) + # Improved Inverse Scaling and Squaring Algorithms + # for the Matrix Logarithm. + A = np.array([ + [3.2346e-1, 3e4, 3e4, 3e4], + [0, 3.0089e-1, 3e4, 3e4], + [0, 0, 3.221e-1, 3e4], + [0, 0, 0, 3.0744e-1]], + dtype=float) + A_logm = np.array([ + [-1.12867982029050462e+00, 9.61418377142025565e+04, + -4.52485573953179264e+09, 2.92496941103871812e+14], + [0.00000000000000000e+00, -1.20101052953082288e+00, + 9.63469687211303099e+04, -4.68104828911105442e+09], + [0.00000000000000000e+00, 0.00000000000000000e+00, + -1.13289322264498393e+00, 9.53249183094775653e+04], + [0.00000000000000000e+00, 0.00000000000000000e+00, + 0.00000000000000000e+00, -1.17947533272554850e+00]], + dtype=float) + assert_allclose(expm(A_logm), A, rtol=1e-4) + + # Perturb the upper triangular matrix by tiny amounts, + # so that it becomes technically not upper triangular. + random.seed(1234) + tiny = 1e-17 + A_logm_perturbed = A_logm.copy() + A_logm_perturbed[1, 0] = tiny + with suppress_warnings() as sup: + sup.filter(RuntimeWarning, "Ill-conditioned.*") + A_expm_logm_perturbed = expm(A_logm_perturbed) + rtol = 1e-4 + atol = 100 * tiny + assert_(not np.allclose(A_expm_logm_perturbed, A, rtol=rtol, atol=atol)) + + def test_burkardt_1(self): + # This matrix is diagonal. + # The calculation of the matrix exponential is simple. + # + # This is the first of a series of matrix exponential tests + # collected by John Burkardt from the following sources. + # + # Alan Laub, + # Review of "Linear System Theory" by Joao Hespanha, + # SIAM Review, + # Volume 52, Number 4, December 2010, pages 779--781. + # + # Cleve Moler and Charles Van Loan, + # Nineteen Dubious Ways to Compute the Exponential of a Matrix, + # Twenty-Five Years Later, + # SIAM Review, + # Volume 45, Number 1, March 2003, pages 3--49. + # + # Cleve Moler, + # Cleve's Corner: A Balancing Act for the Matrix Exponential, + # 23 July 2012. + # + # Robert Ward, + # Numerical computation of the matrix exponential + # with accuracy estimate, + # SIAM Journal on Numerical Analysis, + # Volume 14, Number 4, September 1977, pages 600--610. + exp1 = np.exp(1) + exp2 = np.exp(2) + A = np.array([ + [1, 0], + [0, 2], + ], dtype=float) + desired = np.array([ + [exp1, 0], + [0, exp2], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_2(self): + # This matrix is symmetric. + # The calculation of the matrix exponential is straightforward. + A = np.array([ + [1, 3], + [3, 2], + ], dtype=float) + desired = np.array([ + [39.322809708033859, 46.166301438885753], + [46.166301438885768, 54.711576854329110], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_3(self): + # This example is due to Laub. + # This matrix is ill-suited for the Taylor series approach. + # As powers of A are computed, the entries blow up too quickly. + exp1 = np.exp(1) + exp39 = np.exp(39) + A = np.array([ + [0, 1], + [-39, -40], + ], dtype=float) + desired = np.array([ + [ + 39/(38*exp1) - 1/(38*exp39), + -np.expm1(-38) / (38*exp1)], + [ + 39*np.expm1(-38) / (38*exp1), + -1/(38*exp1) + 39/(38*exp39)], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_4(self): + # This example is due to Moler and Van Loan. + # The example will cause problems for the series summation approach, + # as well as for diagonal Pade approximations. + A = np.array([ + [-49, 24], + [-64, 31], + ], dtype=float) + U = np.array([[3, 1], [4, 2]], dtype=float) + V = np.array([[1, -1/2], [-2, 3/2]], dtype=float) + w = np.array([-17, -1], dtype=float) + desired = np.dot(U * np.exp(w), V) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_5(self): + # This example is due to Moler and Van Loan. + # This matrix is strictly upper triangular + # All powers of A are zero beyond some (low) limit. + # This example will cause problems for Pade approximations. + A = np.array([ + [0, 6, 0, 0], + [0, 0, 6, 0], + [0, 0, 0, 6], + [0, 0, 0, 0], + ], dtype=float) + desired = np.array([ + [1, 6, 18, 36], + [0, 1, 6, 18], + [0, 0, 1, 6], + [0, 0, 0, 1], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_6(self): + # This example is due to Moler and Van Loan. + # This matrix does not have a complete set of eigenvectors. + # That means the eigenvector approach will fail. + exp1 = np.exp(1) + A = np.array([ + [1, 1], + [0, 1], + ], dtype=float) + desired = np.array([ + [exp1, exp1], + [0, exp1], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_7(self): + # This example is due to Moler and Van Loan. + # This matrix is very close to example 5. + # Mathematically, it has a complete set of eigenvectors. + # Numerically, however, the calculation will be suspect. + exp1 = np.exp(1) + eps = np.spacing(1) + A = np.array([ + [1 + eps, 1], + [0, 1 - eps], + ], dtype=float) + desired = np.array([ + [exp1, exp1], + [0, exp1], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_8(self): + # This matrix was an example in Wikipedia. + exp4 = np.exp(4) + exp16 = np.exp(16) + A = np.array([ + [21, 17, 6], + [-5, -1, -6], + [4, 4, 16], + ], dtype=float) + desired = np.array([ + [13*exp16 - exp4, 13*exp16 - 5*exp4, 2*exp16 - 2*exp4], + [-9*exp16 + exp4, -9*exp16 + 5*exp4, -2*exp16 + 2*exp4], + [16*exp16, 16*exp16, 4*exp16], + ], dtype=float) * 0.25 + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_9(self): + # This matrix is due to the NAG Library. + # It is an example for function F01ECF. + A = np.array([ + [1, 2, 2, 2], + [3, 1, 1, 2], + [3, 2, 1, 2], + [3, 3, 3, 1], + ], dtype=float) + desired = np.array([ + [740.7038, 610.8500, 542.2743, 549.1753], + [731.2510, 603.5524, 535.0884, 542.2743], + [823.7630, 679.4257, 603.5524, 610.8500], + [998.4355, 823.7630, 731.2510, 740.7038], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_10(self): + # This is Ward's example #1. + # It is defective and nonderogatory. + A = np.array([ + [4, 2, 0], + [1, 4, 1], + [1, 1, 4], + ], dtype=float) + assert_allclose(sorted(scipy.linalg.eigvals(A)), (3, 3, 6)) + desired = np.array([ + [147.8666224463699, 183.7651386463682, 71.79703239999647], + [127.7810855231823, 183.7651386463682, 91.88256932318415], + [127.7810855231824, 163.6796017231806, 111.9681062463718], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_11(self): + # This is Ward's example #2. + # It is a symmetric matrix. + A = np.array([ + [29.87942128909879, 0.7815750847907159, -2.289519314033932], + [0.7815750847907159, 25.72656945571064, 8.680737820540137], + [-2.289519314033932, 8.680737820540137, 34.39400925519054], + ], dtype=float) + assert_allclose(scipy.linalg.eigvalsh(A), (20, 30, 40)) + desired = np.array([ + [ + 5.496313853692378E+15, + -1.823188097200898E+16, + -3.047577080858001E+16], + [ + -1.823188097200899E+16, + 6.060522870222108E+16, + 1.012918429302482E+17], + [ + -3.047577080858001E+16, + 1.012918429302482E+17, + 1.692944112408493E+17], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_12(self): + # This is Ward's example #3. + # Ward's algorithm has difficulty estimating the accuracy + # of its results. + A = np.array([ + [-131, 19, 18], + [-390, 56, 54], + [-387, 57, 52], + ], dtype=float) + assert_allclose(sorted(scipy.linalg.eigvals(A)), (-20, -2, -1)) + desired = np.array([ + [-1.509644158793135, 0.3678794391096522, 0.1353352811751005], + [-5.632570799891469, 1.471517758499875, 0.4060058435250609], + [-4.934938326088363, 1.103638317328798, 0.5413411267617766], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_burkardt_13(self): + # This is Ward's example #4. + # This is a version of the Forsythe matrix. + # The eigenvector problem is badly conditioned. + # Ward's algorithm has difficulty estimating the accuracy + # of its results for this problem. + # + # Check the construction of one instance of this family of matrices. + A4_actual = _burkardt_13_power(4, 1) + A4_desired = [[0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [1e-4, 0, 0, 0]] + assert_allclose(A4_actual, A4_desired) + # Check the expm for a few instances. + for n in (2, 3, 4, 10): + # Approximate expm using Taylor series. + # This works well for this matrix family + # because each matrix in the summation, + # even before dividing by the factorial, + # is entrywise positive with max entry 10**(-floor(p/n)*n). + k = max(1, int(np.ceil(16/n))) + desired = np.zeros((n, n), dtype=float) + for p in range(n*k): + Ap = _burkardt_13_power(n, p) + assert_equal(np.min(Ap), 0) + assert_allclose(np.max(Ap), np.power(10, -np.floor(p/n)*n)) + desired += Ap / factorial(p) + actual = expm(_burkardt_13_power(n, 1)) + assert_allclose(actual, desired) + + def test_burkardt_14(self): + # This is Moler's example. + # This badly scaled matrix caused problems for MATLAB's expm(). + A = np.array([ + [0, 1e-8, 0], + [-(2e10 + 4e8/6.), -3, 2e10], + [200./3., 0, -200./3.], + ], dtype=float) + desired = np.array([ + [0.446849468283175, 1.54044157383952e-09, 0.462811453558774], + [-5743067.77947947, -0.0152830038686819, -4526542.71278401], + [0.447722977849494, 1.54270484519591e-09, 0.463480648837651], + ], dtype=float) + actual = expm(A) + assert_allclose(actual, desired) + + def test_pascal(self): + # Test pascal triangle. + # Nilpotent exponential, used to trigger a failure (gh-8029) + + for scale in [1.0, 1e-3, 1e-6]: + for n in range(0, 80, 3): + sc = scale ** np.arange(n, -1, -1) + if np.any(sc < 1e-300): + break + + A = np.diag(np.arange(1, n + 1), -1) * scale + B = expm(A) + + got = B + expected = binom(np.arange(n + 1)[:,None], + np.arange(n + 1)[None,:]) * sc[None,:] / sc[:,None] + atol = 1e-13 * abs(expected).max() + assert_allclose(got, expected, atol=atol) + + def test_matrix_input(self): + # Large np.matrix inputs should work, gh-5546 + A = np.zeros((200, 200)) + A[-1,0] = 1 + B0 = expm(A) + with suppress_warnings() as sup: + sup.filter(DeprecationWarning, "the matrix subclass.*") + sup.filter(PendingDeprecationWarning, "the matrix subclass.*") + B = expm(np.matrix(A)) + assert_allclose(B, B0) + + def test_exp_sinch_overflow(self): + # Check overflow in intermediate steps is fixed (gh-11839) + L = np.array([[1.0, -0.5, -0.5, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, -0.5, -0.5, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0, 0.0, -0.5, -0.5], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]]) + + E0 = expm(-L) + E1 = expm(-2**11 * L) + E2 = E0 + for j in range(11): + E2 = E2 @ E2 + + assert_allclose(E1, E2) + + +class TestOperators: + + def test_product_operator(self): + random.seed(1234) + n = 5 + k = 2 + nsamples = 10 + for i in range(nsamples): + A = np.random.randn(n, n) + B = np.random.randn(n, n) + C = np.random.randn(n, n) + D = np.random.randn(n, k) + op = ProductOperator(A, B, C) + assert_allclose(op.matmat(D), A.dot(B).dot(C).dot(D)) + assert_allclose(op.T.matmat(D), (A.dot(B).dot(C)).T.dot(D)) + + def test_matrix_power_operator(self): + random.seed(1234) + n = 5 + k = 2 + p = 3 + nsamples = 10 + for i in range(nsamples): + A = np.random.randn(n, n) + B = np.random.randn(n, k) + op = MatrixPowerOperator(A, p) + assert_allclose(op.matmat(B), np.linalg.matrix_power(A, p).dot(B)) + assert_allclose(op.T.matmat(B), np.linalg.matrix_power(A, p).T.dot(B)) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_norm.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_norm.py new file mode 100644 index 0000000000000000000000000000000000000000..d214bfa1686ec6915881e17b5431c594c02a068f --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_norm.py @@ -0,0 +1,141 @@ +"""Test functions for the sparse.linalg.norm module +""" + +import pytest +import numpy as np +from numpy.linalg import norm as npnorm +from numpy.testing import assert_allclose, assert_equal +from pytest import raises as assert_raises + +import scipy.sparse +from scipy.sparse.linalg import norm as spnorm + + +# https://github.com/scipy/scipy/issues/16031 +def test_sparray_norm(): + row = np.array([0, 0, 1, 1]) + col = np.array([0, 1, 2, 3]) + data = np.array([4, 5, 7, 9]) + test_arr = scipy.sparse.coo_array((data, (row, col)), shape=(2, 4)) + test_mat = scipy.sparse.coo_matrix((data, (row, col)), shape=(2, 4)) + assert_equal(spnorm(test_arr, ord=1, axis=0), np.array([4, 5, 7, 9])) + assert_equal(spnorm(test_mat, ord=1, axis=0), np.array([4, 5, 7, 9])) + assert_equal(spnorm(test_arr, ord=1, axis=1), np.array([9, 16])) + assert_equal(spnorm(test_mat, ord=1, axis=1), np.array([9, 16])) + + +class TestNorm: + def setup_method(self): + a = np.arange(9) - 4 + b = a.reshape((3, 3)) + self.b = scipy.sparse.csr_matrix(b) + + def test_matrix_norm(self): + + # Frobenius norm is the default + assert_allclose(spnorm(self.b), 7.745966692414834) + assert_allclose(spnorm(self.b, 'fro'), 7.745966692414834) + + assert_allclose(spnorm(self.b, np.inf), 9) + assert_allclose(spnorm(self.b, -np.inf), 2) + assert_allclose(spnorm(self.b, 1), 7) + assert_allclose(spnorm(self.b, -1), 6) + # Only floating or complex floating dtype supported by svds. + with pytest.warns(UserWarning, match="The problem size"): + assert_allclose(spnorm(self.b.astype(np.float64), 2), + 7.348469228349534) + + # _multi_svd_norm is not implemented for sparse matrix + assert_raises(NotImplementedError, spnorm, self.b, -2) + + def test_matrix_norm_axis(self): + for m, axis in ((self.b, None), (self.b, (0, 1)), (self.b.T, (1, 0))): + assert_allclose(spnorm(m, axis=axis), 7.745966692414834) + assert_allclose(spnorm(m, 'fro', axis=axis), 7.745966692414834) + assert_allclose(spnorm(m, np.inf, axis=axis), 9) + assert_allclose(spnorm(m, -np.inf, axis=axis), 2) + assert_allclose(spnorm(m, 1, axis=axis), 7) + assert_allclose(spnorm(m, -1, axis=axis), 6) + + def test_vector_norm(self): + v = [4.5825756949558398, 4.2426406871192848, 4.5825756949558398] + for m, a in (self.b, 0), (self.b.T, 1): + for axis in a, (a, ), a-2, (a-2, ): + assert_allclose(spnorm(m, 1, axis=axis), [7, 6, 7]) + assert_allclose(spnorm(m, np.inf, axis=axis), [4, 3, 4]) + assert_allclose(spnorm(m, axis=axis), v) + assert_allclose(spnorm(m, ord=2, axis=axis), v) + assert_allclose(spnorm(m, ord=None, axis=axis), v) + + def test_norm_exceptions(self): + m = self.b + assert_raises(TypeError, spnorm, m, None, 1.5) + assert_raises(TypeError, spnorm, m, None, [2]) + assert_raises(ValueError, spnorm, m, None, ()) + assert_raises(ValueError, spnorm, m, None, (0, 1, 2)) + assert_raises(ValueError, spnorm, m, None, (0, 0)) + assert_raises(ValueError, spnorm, m, None, (0, 2)) + assert_raises(ValueError, spnorm, m, None, (-3, 0)) + assert_raises(ValueError, spnorm, m, None, 2) + assert_raises(ValueError, spnorm, m, None, -3) + assert_raises(ValueError, spnorm, m, 'plate_of_shrimp', 0) + assert_raises(ValueError, spnorm, m, 'plate_of_shrimp', (0, 1)) + + +class TestVsNumpyNorm: + _sparse_types = ( + scipy.sparse.bsr_matrix, + scipy.sparse.coo_matrix, + scipy.sparse.csc_matrix, + scipy.sparse.csr_matrix, + scipy.sparse.dia_matrix, + scipy.sparse.dok_matrix, + scipy.sparse.lil_matrix, + ) + _test_matrices = ( + (np.arange(9) - 4).reshape((3, 3)), + [ + [1, 2, 3], + [-1, 1, 4]], + [ + [1, 0, 3], + [-1, 1, 4j]], + ) + + def test_sparse_matrix_norms(self): + for sparse_type in self._sparse_types: + for M in self._test_matrices: + S = sparse_type(M) + assert_allclose(spnorm(S), npnorm(M)) + assert_allclose(spnorm(S, 'fro'), npnorm(M, 'fro')) + assert_allclose(spnorm(S, np.inf), npnorm(M, np.inf)) + assert_allclose(spnorm(S, -np.inf), npnorm(M, -np.inf)) + assert_allclose(spnorm(S, 1), npnorm(M, 1)) + assert_allclose(spnorm(S, -1), npnorm(M, -1)) + + def test_sparse_matrix_norms_with_axis(self): + for sparse_type in self._sparse_types: + for M in self._test_matrices: + S = sparse_type(M) + for axis in None, (0, 1), (1, 0): + assert_allclose(spnorm(S, axis=axis), npnorm(M, axis=axis)) + for ord in 'fro', np.inf, -np.inf, 1, -1: + assert_allclose(spnorm(S, ord, axis=axis), + npnorm(M, ord, axis=axis)) + # Some numpy matrix norms are allergic to negative axes. + for axis in (-2, -1), (-1, -2), (1, -2): + assert_allclose(spnorm(S, axis=axis), npnorm(M, axis=axis)) + assert_allclose(spnorm(S, 'f', axis=axis), + npnorm(M, 'f', axis=axis)) + assert_allclose(spnorm(S, 'fro', axis=axis), + npnorm(M, 'fro', axis=axis)) + + def test_sparse_vector_norms(self): + for sparse_type in self._sparse_types: + for M in self._test_matrices: + S = sparse_type(M) + for axis in (0, 1, -1, -2, (0, ), (1, ), (-1, ), (-2, )): + assert_allclose(spnorm(S, axis=axis), npnorm(M, axis=axis)) + for ord in None, 2, np.inf, -np.inf, 1, 0.5, 0.42: + assert_allclose(spnorm(S, ord, axis=axis), + npnorm(M, ord, axis=axis)) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_onenormest.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_onenormest.py new file mode 100644 index 0000000000000000000000000000000000000000..09ceead21b8bf59d27db249161875fdc4dd6312e --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_onenormest.py @@ -0,0 +1,252 @@ +"""Test functions for the sparse.linalg._onenormest module +""" + +import numpy as np +from numpy.testing import assert_allclose, assert_equal, assert_ +import pytest +import scipy.linalg +import scipy.sparse.linalg +from scipy.sparse.linalg._onenormest import _onenormest_core, _algorithm_2_2 + + +class MatrixProductOperator(scipy.sparse.linalg.LinearOperator): + """ + This is purely for onenormest testing. + """ + + def __init__(self, A, B): + if A.ndim != 2 or B.ndim != 2: + raise ValueError('expected ndarrays representing matrices') + if A.shape[1] != B.shape[0]: + raise ValueError('incompatible shapes') + self.A = A + self.B = B + self.ndim = 2 + self.shape = (A.shape[0], B.shape[1]) + + def _matvec(self, x): + return np.dot(self.A, np.dot(self.B, x)) + + def _rmatvec(self, x): + return np.dot(np.dot(x, self.A), self.B) + + def _matmat(self, X): + return np.dot(self.A, np.dot(self.B, X)) + + @property + def T(self): + return MatrixProductOperator(self.B.T, self.A.T) + + +class TestOnenormest: + + @pytest.mark.xslow + def test_onenormest_table_3_t_2(self): + # This will take multiple seconds if your computer is slow like mine. + # It is stochastic, so the tolerance could be too strict. + np.random.seed(1234) + t = 2 + n = 100 + itmax = 5 + nsamples = 5000 + observed = [] + expected = [] + nmult_list = [] + nresample_list = [] + for i in range(nsamples): + A = scipy.linalg.inv(np.random.randn(n, n)) + est, v, w, nmults, nresamples = _onenormest_core(A, A.T, t, itmax) + observed.append(est) + expected.append(scipy.linalg.norm(A, 1)) + nmult_list.append(nmults) + nresample_list.append(nresamples) + observed = np.array(observed, dtype=float) + expected = np.array(expected, dtype=float) + relative_errors = np.abs(observed - expected) / expected + + # check the mean underestimation ratio + underestimation_ratio = observed / expected + assert_(0.99 < np.mean(underestimation_ratio) < 1.0) + + # check the max and mean required column resamples + assert_equal(np.max(nresample_list), 2) + assert_(0.05 < np.mean(nresample_list) < 0.2) + + # check the proportion of norms computed exactly correctly + nexact = np.count_nonzero(relative_errors < 1e-14) + proportion_exact = nexact / float(nsamples) + assert_(0.9 < proportion_exact < 0.95) + + # check the average number of matrix*vector multiplications + assert_(3.5 < np.mean(nmult_list) < 4.5) + + @pytest.mark.xslow + def test_onenormest_table_4_t_7(self): + # This will take multiple seconds if your computer is slow like mine. + # It is stochastic, so the tolerance could be too strict. + np.random.seed(1234) + t = 7 + n = 100 + itmax = 5 + nsamples = 5000 + observed = [] + expected = [] + nmult_list = [] + nresample_list = [] + for i in range(nsamples): + A = np.random.randint(-1, 2, size=(n, n)) + est, v, w, nmults, nresamples = _onenormest_core(A, A.T, t, itmax) + observed.append(est) + expected.append(scipy.linalg.norm(A, 1)) + nmult_list.append(nmults) + nresample_list.append(nresamples) + observed = np.array(observed, dtype=float) + expected = np.array(expected, dtype=float) + relative_errors = np.abs(observed - expected) / expected + + # check the mean underestimation ratio + underestimation_ratio = observed / expected + assert_(0.90 < np.mean(underestimation_ratio) < 0.99) + + # check the required column resamples + assert_equal(np.max(nresample_list), 0) + + # check the proportion of norms computed exactly correctly + nexact = np.count_nonzero(relative_errors < 1e-14) + proportion_exact = nexact / float(nsamples) + assert_(0.15 < proportion_exact < 0.25) + + # check the average number of matrix*vector multiplications + assert_(3.5 < np.mean(nmult_list) < 4.5) + + def test_onenormest_table_5_t_1(self): + # "note that there is no randomness and hence only one estimate for t=1" + t = 1 + n = 100 + itmax = 5 + alpha = 1 - 1e-6 + A = -scipy.linalg.inv(np.identity(n) + alpha*np.eye(n, k=1)) + first_col = np.array([1] + [0]*(n-1)) + first_row = np.array([(-alpha)**i for i in range(n)]) + B = -scipy.linalg.toeplitz(first_col, first_row) + assert_allclose(A, B) + est, v, w, nmults, nresamples = _onenormest_core(B, B.T, t, itmax) + exact_value = scipy.linalg.norm(B, 1) + underest_ratio = est / exact_value + assert_allclose(underest_ratio, 0.05, rtol=1e-4) + assert_equal(nmults, 11) + assert_equal(nresamples, 0) + # check the non-underscored version of onenormest + est_plain = scipy.sparse.linalg.onenormest(B, t=t, itmax=itmax) + assert_allclose(est, est_plain) + + @pytest.mark.xslow + def test_onenormest_table_6_t_1(self): + #TODO this test seems to give estimates that match the table, + #TODO even though no attempt has been made to deal with + #TODO complex numbers in the one-norm estimation. + # This will take multiple seconds if your computer is slow like mine. + # It is stochastic, so the tolerance could be too strict. + np.random.seed(1234) + t = 1 + n = 100 + itmax = 5 + nsamples = 5000 + observed = [] + expected = [] + nmult_list = [] + nresample_list = [] + for i in range(nsamples): + A_inv = np.random.rand(n, n) + 1j * np.random.rand(n, n) + A = scipy.linalg.inv(A_inv) + est, v, w, nmults, nresamples = _onenormest_core(A, A.T, t, itmax) + observed.append(est) + expected.append(scipy.linalg.norm(A, 1)) + nmult_list.append(nmults) + nresample_list.append(nresamples) + observed = np.array(observed, dtype=float) + expected = np.array(expected, dtype=float) + relative_errors = np.abs(observed - expected) / expected + + # check the mean underestimation ratio + underestimation_ratio = observed / expected + underestimation_ratio_mean = np.mean(underestimation_ratio) + assert_(0.90 < underestimation_ratio_mean < 0.99) + + # check the required column resamples + max_nresamples = np.max(nresample_list) + assert_equal(max_nresamples, 0) + + # check the proportion of norms computed exactly correctly + nexact = np.count_nonzero(relative_errors < 1e-14) + proportion_exact = nexact / float(nsamples) + assert_(0.7 < proportion_exact < 0.8) + + # check the average number of matrix*vector multiplications + mean_nmult = np.mean(nmult_list) + assert_(4 < mean_nmult < 5) + + def _help_product_norm_slow(self, A, B): + # for profiling + C = np.dot(A, B) + return scipy.linalg.norm(C, 1) + + def _help_product_norm_fast(self, A, B): + # for profiling + t = 2 + itmax = 5 + D = MatrixProductOperator(A, B) + est, v, w, nmults, nresamples = _onenormest_core(D, D.T, t, itmax) + return est + + @pytest.mark.slow + def test_onenormest_linear_operator(self): + # Define a matrix through its product A B. + # Depending on the shapes of A and B, + # it could be easy to multiply this product by a small matrix, + # but it could be annoying to look at all of + # the entries of the product explicitly. + np.random.seed(1234) + n = 6000 + k = 3 + A = np.random.randn(n, k) + B = np.random.randn(k, n) + fast_estimate = self._help_product_norm_fast(A, B) + exact_value = self._help_product_norm_slow(A, B) + assert_(fast_estimate <= exact_value <= 3*fast_estimate, + f'fast: {fast_estimate:g}\nexact:{exact_value:g}') + + def test_returns(self): + np.random.seed(1234) + A = scipy.sparse.rand(50, 50, 0.1) + + s0 = scipy.linalg.norm(A.toarray(), 1) + s1, v = scipy.sparse.linalg.onenormest(A, compute_v=True) + s2, w = scipy.sparse.linalg.onenormest(A, compute_w=True) + s3, v2, w2 = scipy.sparse.linalg.onenormest(A, compute_w=True, compute_v=True) + + assert_allclose(s1, s0, rtol=1e-9) + assert_allclose(np.linalg.norm(A.dot(v), 1), s0*np.linalg.norm(v, 1), rtol=1e-9) + assert_allclose(A.dot(v), w, rtol=1e-9) + + +class TestAlgorithm_2_2: + + def test_randn_inv(self): + np.random.seed(1234) + n = 20 + nsamples = 100 + for i in range(nsamples): + + # Choose integer t uniformly between 1 and 3 inclusive. + t = np.random.randint(1, 4) + + # Choose n uniformly between 10 and 40 inclusive. + n = np.random.randint(10, 41) + + # Sample the inverse of a matrix with random normal entries. + A = scipy.linalg.inv(np.random.randn(n, n)) + + # Compute the 1-norm bounds. + g, ind = _algorithm_2_2(A, A.T, t) + diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_propack.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_propack.py new file mode 100644 index 0000000000000000000000000000000000000000..d7307e5c4cf66dff981a4b475c397f3dc2ac1701 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_propack.py @@ -0,0 +1,165 @@ +import os +import pytest + +import numpy as np +from numpy.testing import assert_allclose +from pytest import raises as assert_raises +from scipy.sparse.linalg._svdp import _svdp +from scipy.sparse import csr_matrix, csc_matrix + + +# dtype_flavour to tolerance +TOLS = { + np.float32: 1e-4, + np.float64: 1e-8, + np.complex64: 1e-4, + np.complex128: 1e-8, +} + + +def is_complex_type(dtype): + return np.dtype(dtype).kind == "c" + + +_dtypes = [] +for dtype_flavour in TOLS.keys(): + marks = [] + if is_complex_type(dtype_flavour): + marks = [pytest.mark.slow] + _dtypes.append(pytest.param(dtype_flavour, marks=marks, + id=dtype_flavour.__name__)) +_dtypes = tuple(_dtypes) # type: ignore[assignment] + + +def generate_matrix(constructor, n, m, f, + dtype=float, rseed=0, **kwargs): + """Generate a random sparse matrix""" + rng = np.random.RandomState(rseed) + if is_complex_type(dtype): + M = (- 5 + 10 * rng.rand(n, m) + - 5j + 10j * rng.rand(n, m)).astype(dtype) + else: + M = (-5 + 10 * rng.rand(n, m)).astype(dtype) + M[M.real > 10 * f - 5] = 0 + return constructor(M, **kwargs) + + +def assert_orthogonal(u1, u2, rtol, atol): + """Check that the first k rows of u1 and u2 are orthogonal""" + A = abs(np.dot(u1.conj().T, u2)) + assert_allclose(A, np.eye(u1.shape[1], u2.shape[1]), rtol=rtol, atol=atol) + + +def check_svdp(n, m, constructor, dtype, k, irl_mode, which, f=0.8): + tol = TOLS[dtype] + + M = generate_matrix(np.asarray, n, m, f, dtype) + Msp = constructor(M) + + u1, sigma1, vt1 = np.linalg.svd(M, full_matrices=False) + u2, sigma2, vt2, _ = _svdp(Msp, k=k, which=which, irl_mode=irl_mode, + tol=tol) + + # check the which + if which.upper() == 'SM': + u1 = np.roll(u1, k, 1) + vt1 = np.roll(vt1, k, 0) + sigma1 = np.roll(sigma1, k) + + # check that singular values agree + assert_allclose(sigma1[:k], sigma2, rtol=tol, atol=tol) + + # check that singular vectors are orthogonal + assert_orthogonal(u1, u2, rtol=tol, atol=tol) + assert_orthogonal(vt1.T, vt2.T, rtol=tol, atol=tol) + + +@pytest.mark.parametrize('ctor', (np.array, csr_matrix, csc_matrix)) +@pytest.mark.parametrize('dtype', _dtypes) +@pytest.mark.parametrize('irl', (True, False)) +@pytest.mark.parametrize('which', ('LM', 'SM')) +def test_svdp(ctor, dtype, irl, which): + np.random.seed(0) + n, m, k = 10, 20, 3 + if which == 'SM' and not irl: + message = "`which`='SM' requires irl_mode=True" + with assert_raises(ValueError, match=message): + check_svdp(n, m, ctor, dtype, k, irl, which) + else: + check_svdp(n, m, ctor, dtype, k, irl, which) + + +@pytest.mark.parametrize('dtype', _dtypes) +@pytest.mark.parametrize('irl', (False, True)) +@pytest.mark.timeout(120) # True, complex64 > 60 s: prerel deps cov 64bit blas +def test_examples(dtype, irl): + # Note: atol for complex64 bumped from 1e-4 to 1e-3 due to test failures + # with BLIS, Netlib, and MKL+AVX512 - see + # https://github.com/conda-forge/scipy-feedstock/pull/198#issuecomment-999180432 + atol = { + np.float32: 1.3e-4, + np.float64: 1e-9, + np.complex64: 1e-3, + np.complex128: 1e-9, + }[dtype] + + path_prefix = os.path.dirname(__file__) + # Test matrices from `illc1850.coord` and `mhd1280b.cua` distributed with + # PROPACK 2.1: http://sun.stanford.edu/~rmunk/PROPACK/ + relative_path = "propack_test_data.npz" + filename = os.path.join(path_prefix, relative_path) + with np.load(filename, allow_pickle=True) as data: + if is_complex_type(dtype): + A = data['A_complex'].item().astype(dtype) + else: + A = data['A_real'].item().astype(dtype) + + k = 200 + u, s, vh, _ = _svdp(A, k, irl_mode=irl, random_state=0) + + # complex example matrix has many repeated singular values, so check only + # beginning non-repeated singular vectors to avoid permutations + sv_check = 27 if is_complex_type(dtype) else k + u = u[:, :sv_check] + vh = vh[:sv_check, :] + s = s[:sv_check] + + # Check orthogonality of singular vectors + assert_allclose(np.eye(u.shape[1]), u.conj().T @ u, atol=atol) + assert_allclose(np.eye(vh.shape[0]), vh @ vh.conj().T, atol=atol) + + # Ensure the norm of the difference between the np.linalg.svd and + # PROPACK reconstructed matrices is small + u3, s3, vh3 = np.linalg.svd(A.todense()) + u3 = u3[:, :sv_check] + s3 = s3[:sv_check] + vh3 = vh3[:sv_check, :] + A3 = u3 @ np.diag(s3) @ vh3 + recon = u @ np.diag(s) @ vh + assert_allclose(np.linalg.norm(A3 - recon), 0, atol=atol) + + +@pytest.mark.parametrize('shifts', (None, -10, 0, 1, 10, 70)) +@pytest.mark.parametrize('dtype', _dtypes[:2]) +def test_shifts(shifts, dtype): + np.random.seed(0) + n, k = 70, 10 + A = np.random.random((n, n)) + if shifts is not None and ((shifts < 0) or (k > min(n-1-shifts, n))): + with pytest.raises(ValueError): + _svdp(A, k, shifts=shifts, kmax=5*k, irl_mode=True) + else: + _svdp(A, k, shifts=shifts, kmax=5*k, irl_mode=True) + + +@pytest.mark.slow +@pytest.mark.xfail() +def test_shifts_accuracy(): + np.random.seed(0) + n, k = 70, 10 + A = np.random.random((n, n)).astype(np.float64) + u1, s1, vt1, _ = _svdp(A, k, shifts=None, which='SM', irl_mode=True) + u2, s2, vt2, _ = _svdp(A, k, shifts=32, which='SM', irl_mode=True) + # shifts <= 32 doesn't agree with shifts > 32 + # Does agree when which='LM' instead of 'SM' + assert_allclose(s1, s2) diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_pydata_sparse.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_pydata_sparse.py new file mode 100644 index 0000000000000000000000000000000000000000..3f08514f104cf5453fd896d110f87d40ae718255 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_pydata_sparse.py @@ -0,0 +1,243 @@ +import pytest + +import numpy as np +import scipy.sparse as sp +import scipy.sparse.linalg as splin + +from numpy.testing import assert_allclose, assert_equal + +try: + import sparse +except Exception: + sparse = None + +pytestmark = pytest.mark.skipif(sparse is None, + reason="pydata/sparse not installed") + + +msg = "pydata/sparse (0.15.1) does not implement necessary operations" + + +sparse_params = (pytest.param("COO"), + pytest.param("DOK", marks=[pytest.mark.xfail(reason=msg)])) + +scipy_sparse_classes = [ + sp.bsr_matrix, + sp.csr_matrix, + sp.coo_matrix, + sp.csc_matrix, + sp.dia_matrix, + sp.dok_matrix +] + + +@pytest.fixture(params=sparse_params) +def sparse_cls(request): + return getattr(sparse, request.param) + + +@pytest.fixture(params=scipy_sparse_classes) +def sp_sparse_cls(request): + return request.param + + +@pytest.fixture +def same_matrix(sparse_cls, sp_sparse_cls): + np.random.seed(1234) + A_dense = np.random.rand(9, 9) + return sp_sparse_cls(A_dense), sparse_cls(A_dense) + + +@pytest.fixture +def matrices(sparse_cls): + np.random.seed(1234) + A_dense = np.random.rand(9, 9) + A_dense = A_dense @ A_dense.T + A_sparse = sparse_cls(A_dense) + b = np.random.rand(9) + return A_dense, A_sparse, b + + +def test_isolve_gmres(matrices): + # Several of the iterative solvers use the same + # isolve.utils.make_system wrapper code, so test just one of them. + A_dense, A_sparse, b = matrices + x, info = splin.gmres(A_sparse, b, atol=1e-15) + assert info == 0 + assert isinstance(x, np.ndarray) + assert_allclose(A_sparse @ x, b) + + +def test_lsmr(matrices): + A_dense, A_sparse, b = matrices + res0 = splin.lsmr(A_dense, b) + res = splin.lsmr(A_sparse, b) + assert_allclose(res[0], res0[0], atol=1e-3) + + +# test issue 17012 +def test_lsmr_output_shape(): + x = splin.lsmr(A=np.ones((10, 1)), b=np.zeros(10), x0=np.ones(1))[0] + assert_equal(x.shape, (1,)) + + +def test_lsqr(matrices): + A_dense, A_sparse, b = matrices + res0 = splin.lsqr(A_dense, b) + res = splin.lsqr(A_sparse, b) + assert_allclose(res[0], res0[0], atol=1e-5) + + +def test_eigs(matrices): + A_dense, A_sparse, v0 = matrices + + M_dense = np.diag(v0**2) + M_sparse = A_sparse.__class__(M_dense) + + w_dense, v_dense = splin.eigs(A_dense, k=3, v0=v0) + w, v = splin.eigs(A_sparse, k=3, v0=v0) + + assert_allclose(w, w_dense) + assert_allclose(v, v_dense) + + for M in [M_sparse, M_dense]: + w_dense, v_dense = splin.eigs(A_dense, M=M_dense, k=3, v0=v0) + w, v = splin.eigs(A_sparse, M=M, k=3, v0=v0) + + assert_allclose(w, w_dense) + assert_allclose(v, v_dense) + + w_dense, v_dense = splin.eigsh(A_dense, M=M_dense, k=3, v0=v0) + w, v = splin.eigsh(A_sparse, M=M, k=3, v0=v0) + + assert_allclose(w, w_dense) + assert_allclose(v, v_dense) + + +def test_svds(matrices): + A_dense, A_sparse, v0 = matrices + + u0, s0, vt0 = splin.svds(A_dense, k=2, v0=v0) + u, s, vt = splin.svds(A_sparse, k=2, v0=v0) + + assert_allclose(s, s0) + assert_allclose(np.abs(u), np.abs(u0)) + assert_allclose(np.abs(vt), np.abs(vt0)) + + +def test_lobpcg(matrices): + A_dense, A_sparse, x = matrices + X = x[:,None] + + w_dense, v_dense = splin.lobpcg(A_dense, X) + w, v = splin.lobpcg(A_sparse, X) + + assert_allclose(w, w_dense) + assert_allclose(v, v_dense) + + +def test_spsolve(matrices): + A_dense, A_sparse, b = matrices + b2 = np.random.rand(len(b), 3) + + x0 = splin.spsolve(sp.csc_matrix(A_dense), b) + x = splin.spsolve(A_sparse, b) + assert isinstance(x, np.ndarray) + assert_allclose(x, x0) + + x0 = splin.spsolve(sp.csc_matrix(A_dense), b) + x = splin.spsolve(A_sparse, b, use_umfpack=True) + assert isinstance(x, np.ndarray) + assert_allclose(x, x0) + + x0 = splin.spsolve(sp.csc_matrix(A_dense), b2) + x = splin.spsolve(A_sparse, b2) + assert isinstance(x, np.ndarray) + assert_allclose(x, x0) + + x0 = splin.spsolve(sp.csc_matrix(A_dense), + sp.csc_matrix(A_dense)) + x = splin.spsolve(A_sparse, A_sparse) + assert isinstance(x, type(A_sparse)) + assert_allclose(x.todense(), x0.todense()) + + +def test_splu(matrices): + A_dense, A_sparse, b = matrices + n = len(b) + sparse_cls = type(A_sparse) + + lu = splin.splu(A_sparse) + + assert isinstance(lu.L, sparse_cls) + assert isinstance(lu.U, sparse_cls) + + _Pr_scipy = sp.csc_matrix((np.ones(n), (lu.perm_r, np.arange(n)))) + _Pc_scipy = sp.csc_matrix((np.ones(n), (np.arange(n), lu.perm_c))) + Pr = sparse_cls.from_scipy_sparse(_Pr_scipy) + Pc = sparse_cls.from_scipy_sparse(_Pc_scipy) + A2 = Pr.T @ lu.L @ lu.U @ Pc.T + + assert_allclose(A2.todense(), A_sparse.todense()) + + z = lu.solve(A_sparse.todense()) + assert_allclose(z, np.eye(n), atol=1e-10) + + +def test_spilu(matrices): + A_dense, A_sparse, b = matrices + sparse_cls = type(A_sparse) + + lu = splin.spilu(A_sparse) + + assert isinstance(lu.L, sparse_cls) + assert isinstance(lu.U, sparse_cls) + + z = lu.solve(A_sparse.todense()) + assert_allclose(z, np.eye(len(b)), atol=1e-3) + + +def test_spsolve_triangular(matrices): + A_dense, A_sparse, b = matrices + A_sparse = sparse.tril(A_sparse) + + x = splin.spsolve_triangular(A_sparse, b) + assert_allclose(A_sparse @ x, b) + + +def test_onenormest(matrices): + A_dense, A_sparse, b = matrices + est0 = splin.onenormest(A_dense) + est = splin.onenormest(A_sparse) + assert_allclose(est, est0) + + +def test_inv(matrices): + A_dense, A_sparse, b = matrices + x0 = splin.inv(sp.csc_matrix(A_dense)) + x = splin.inv(A_sparse) + assert_allclose(x.todense(), x0.todense()) + + +def test_expm(matrices): + A_dense, A_sparse, b = matrices + x0 = splin.expm(sp.csc_matrix(A_dense)) + x = splin.expm(A_sparse) + assert_allclose(x.todense(), x0.todense()) + + +def test_expm_multiply(matrices): + A_dense, A_sparse, b = matrices + x0 = splin.expm_multiply(A_dense, b) + x = splin.expm_multiply(A_sparse, b) + assert_allclose(x, x0) + + +def test_eq(same_matrix): + sp_sparse, pd_sparse = same_matrix + assert (sp_sparse == pd_sparse).all() + + +def test_ne(same_matrix): + sp_sparse, pd_sparse = same_matrix + assert not (sp_sparse != pd_sparse).any() diff --git a/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_special_sparse_arrays.py b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_special_sparse_arrays.py new file mode 100644 index 0000000000000000000000000000000000000000..5a51c93edb80e96d6b96305f35bbbe6f47c91f81 --- /dev/null +++ b/.venv/Lib/site-packages/scipy/sparse/linalg/tests/test_special_sparse_arrays.py @@ -0,0 +1,337 @@ +import pytest +import numpy as np +from numpy.testing import assert_array_equal, assert_allclose + +from scipy.sparse import diags, csgraph +from scipy.linalg import eigh + +from scipy.sparse.linalg import LaplacianNd +from scipy.sparse.linalg._special_sparse_arrays import Sakurai +from scipy.sparse.linalg._special_sparse_arrays import MikotaPair + +INT_DTYPES = [np.int8, np.int16, np.int32, np.int64] +REAL_DTYPES = [np.float32, np.float64] +COMPLEX_DTYPES = [np.complex64, np.complex128] +ALLDTYPES = INT_DTYPES + REAL_DTYPES + COMPLEX_DTYPES + + +class TestLaplacianNd: + """ + LaplacianNd tests + """ + + @pytest.mark.parametrize('bc', ['neumann', 'dirichlet', 'periodic']) + def test_1d_specific_shape(self, bc): + lap = LaplacianNd(grid_shape=(6, ), boundary_conditions=bc) + lapa = lap.toarray() + if bc == 'neumann': + a = np.array( + [ + [-1, 1, 0, 0, 0, 0], + [1, -2, 1, 0, 0, 0], + [0, 1, -2, 1, 0, 0], + [0, 0, 1, -2, 1, 0], + [0, 0, 0, 1, -2, 1], + [0, 0, 0, 0, 1, -1], + ] + ) + elif bc == 'dirichlet': + a = np.array( + [ + [-2, 1, 0, 0, 0, 0], + [1, -2, 1, 0, 0, 0], + [0, 1, -2, 1, 0, 0], + [0, 0, 1, -2, 1, 0], + [0, 0, 0, 1, -2, 1], + [0, 0, 0, 0, 1, -2], + ] + ) + else: + a = np.array( + [ + [-2, 1, 0, 0, 0, 1], + [1, -2, 1, 0, 0, 0], + [0, 1, -2, 1, 0, 0], + [0, 0, 1, -2, 1, 0], + [0, 0, 0, 1, -2, 1], + [1, 0, 0, 0, 1, -2], + ] + ) + assert_array_equal(a, lapa) + + def test_1d_with_graph_laplacian(self): + n = 6 + G = diags(np.ones(n - 1), 1, format='dia') + Lf = csgraph.laplacian(G, symmetrized=True, form='function') + La = csgraph.laplacian(G, symmetrized=True, form='array') + grid_shape = (n,) + bc = 'neumann' + lap = LaplacianNd(grid_shape, boundary_conditions=bc) + assert_array_equal(lap(np.eye(n)), -Lf(np.eye(n))) + assert_array_equal(lap.toarray(), -La.toarray()) + # https://github.com/numpy/numpy/issues/24351 + assert_array_equal(lap.tosparse().toarray(), -La.toarray()) + + @pytest.mark.parametrize('grid_shape', [(6, ), (2, 3), (2, 3, 4)]) + @pytest.mark.parametrize('bc', ['neumann', 'dirichlet', 'periodic']) + def test_eigenvalues(self, grid_shape, bc): + lap = LaplacianNd(grid_shape, boundary_conditions=bc, dtype=np.float64) + L = lap.toarray() + eigvals = eigh(L, eigvals_only=True) + n = np.prod(grid_shape) + eigenvalues = lap.eigenvalues() + dtype = eigenvalues.dtype + atol = n * n * np.finfo(dtype).eps + # test the default ``m = None`` + assert_allclose(eigenvalues, eigvals, atol=atol) + # test every ``m > 0`` + for m in np.arange(1, n + 1): + assert_array_equal(lap.eigenvalues(m), eigenvalues[-m:]) + + @pytest.mark.parametrize('grid_shape', [(6, ), (2, 3), (2, 3, 4)]) + @pytest.mark.parametrize('bc', ['neumann', 'dirichlet', 'periodic']) + def test_eigenvectors(self, grid_shape, bc): + lap = LaplacianNd(grid_shape, boundary_conditions=bc, dtype=np.float64) + n = np.prod(grid_shape) + eigenvalues = lap.eigenvalues() + eigenvectors = lap.eigenvectors() + dtype = eigenvectors.dtype + atol = n * n * max(np.finfo(dtype).eps, np.finfo(np.double).eps) + # test the default ``m = None`` every individual eigenvector + for i in np.arange(n): + r = lap.toarray() @ eigenvectors[:, i] - eigenvectors[:, i] * eigenvalues[i] + assert_allclose(r, np.zeros_like(r), atol=atol) + # test every ``m > 0`` + for m in np.arange(1, n + 1): + e = lap.eigenvalues(m) + ev = lap.eigenvectors(m) + r = lap.toarray() @ ev - ev @ np.diag(e) + assert_allclose(r, np.zeros_like(r), atol=atol) + + @pytest.mark.parametrize('grid_shape', [(6, ), (2, 3), (2, 3, 4)]) + @pytest.mark.parametrize('bc', ['neumann', 'dirichlet', 'periodic']) + def test_toarray_tosparse_consistency(self, grid_shape, bc): + lap = LaplacianNd(grid_shape, boundary_conditions=bc) + n = np.prod(grid_shape) + assert_array_equal(lap.toarray(), lap(np.eye(n))) + assert_array_equal(lap.tosparse().toarray(), lap.toarray()) + + @pytest.mark.parametrize('dtype', ALLDTYPES) + @pytest.mark.parametrize('grid_shape', [(6, ), (2, 3), (2, 3, 4)]) + @pytest.mark.parametrize('bc', ['neumann', 'dirichlet', 'periodic']) + def test_linearoperator_shape_dtype(self, grid_shape, bc, dtype): + lap = LaplacianNd(grid_shape, boundary_conditions=bc, dtype=dtype) + n = np.prod(grid_shape) + assert lap.shape == (n, n) + assert lap.dtype == dtype + assert_array_equal( + LaplacianNd( + grid_shape, boundary_conditions=bc, dtype=dtype + ).toarray(), + LaplacianNd(grid_shape, boundary_conditions=bc) + .toarray() + .astype(dtype), + ) + assert_array_equal( + LaplacianNd(grid_shape, boundary_conditions=bc, dtype=dtype) + .tosparse() + .toarray(), + LaplacianNd(grid_shape, boundary_conditions=bc) + .tosparse() + .toarray() + .astype(dtype), + ) + + @pytest.mark.parametrize('dtype', ALLDTYPES) + @pytest.mark.parametrize('grid_shape', [(6, ), (2, 3), (2, 3, 4)]) + @pytest.mark.parametrize('bc', ['neumann', 'dirichlet', 'periodic']) + def test_dot(self, grid_shape, bc, dtype): + """ Test the dot-product for type preservation and consistency. + """ + lap = LaplacianNd(grid_shape, boundary_conditions=bc) + n = np.prod(grid_shape) + x0 = np.arange(n) + x1 = x0.reshape((-1, 1)) + x2 = np.arange(2 * n).reshape((n, 2)) + input_set = [x0, x1, x2] + for x in input_set: + y = lap.dot(x.astype(dtype)) + assert x.shape == y.shape + assert y.dtype == dtype + if x.ndim == 2: + yy = lap.toarray() @ x.astype(dtype) + assert yy.dtype == dtype + np.array_equal(y, yy) + + def test_boundary_conditions_value_error(self): + with pytest.raises(ValueError, match="Unknown value 'robin'"): + LaplacianNd(grid_shape=(6, ), boundary_conditions='robin') + + +class TestSakurai: + """ + Sakurai tests + """ + + def test_specific_shape(self): + sak = Sakurai(6) + assert_array_equal(sak.toarray(), sak(np.eye(6))) + a = np.array( + [ + [ 5, -4, 1, 0, 0, 0], + [-4, 6, -4, 1, 0, 0], + [ 1, -4, 6, -4, 1, 0], + [ 0, 1, -4, 6, -4, 1], + [ 0, 0, 1, -4, 6, -4], + [ 0, 0, 0, 1, -4, 5] + ] + ) + + np.array_equal(a, sak.toarray()) + np.array_equal(sak.tosparse().toarray(), sak.toarray()) + ab = np.array( + [ + [ 1, 1, 1, 1, 1, 1], + [-4, -4, -4, -4, -4, -4], + [ 5, 6, 6, 6, 6, 5] + ] + ) + np.array_equal(ab, sak.tobanded()) + e = np.array( + [0.03922866, 0.56703972, 2.41789479, 5.97822974, + 10.54287655, 14.45473055] + ) + np.array_equal(e, sak.eigenvalues()) + np.array_equal(e[:2], sak.eigenvalues(2)) + + # `Sakurai` default `dtype` is `np.int8` as its entries are small integers + @pytest.mark.parametrize('dtype', ALLDTYPES) + def test_linearoperator_shape_dtype(self, dtype): + n = 7 + sak = Sakurai(n, dtype=dtype) + assert sak.shape == (n, n) + assert sak.dtype == dtype + assert_array_equal(sak.toarray(), Sakurai(n).toarray().astype(dtype)) + assert_array_equal(sak.tosparse().toarray(), + Sakurai(n).tosparse().toarray().astype(dtype)) + + @pytest.mark.parametrize('dtype', ALLDTYPES) + @pytest.mark.parametrize('argument_dtype', ALLDTYPES) + def test_dot(self, dtype, argument_dtype): + """ Test the dot-product for type preservation and consistency. + """ + result_dtype = np.promote_types(argument_dtype, dtype) + n = 5 + sak = Sakurai(n) + x0 = np.arange(n) + x1 = x0.reshape((-1, 1)) + x2 = np.arange(2 * n).reshape((n, 2)) + input_set = [x0, x1, x2] + for x in input_set: + y = sak.dot(x.astype(argument_dtype)) + assert x.shape == y.shape + assert np.can_cast(y.dtype, result_dtype) + if x.ndim == 2: + ya = sak.toarray() @ x.astype(argument_dtype) + np.array_equal(y, ya) + assert np.can_cast(ya.dtype, result_dtype) + ys = sak.tosparse() @ x.astype(argument_dtype) + np.array_equal(y, ys) + assert np.can_cast(ys.dtype, result_dtype) + +class TestMikotaPair: + """ + MikotaPair tests + """ + # both MikotaPair `LinearOperator`s share the same dtype + # while `MikotaK` `dtype` can be as small as its default `np.int32` + # since its entries are integers, the `MikotaM` involves inverses + # so its smallest still accurate `dtype` is `np.float32` + tested_types = REAL_DTYPES + COMPLEX_DTYPES + + def test_specific_shape(self): + n = 6 + mik = MikotaPair(n) + mik_k = mik.k + mik_m = mik.m + assert_array_equal(mik_k.toarray(), mik_k(np.eye(n))) + assert_array_equal(mik_m.toarray(), mik_m(np.eye(n))) + + k = np.array( + [ + [11, -5, 0, 0, 0, 0], + [-5, 9, -4, 0, 0, 0], + [ 0, -4, 7, -3, 0, 0], + [ 0, 0, -3, 5, -2, 0], + [ 0, 0, 0, -2, 3, -1], + [ 0, 0, 0, 0, -1, 1] + ] + ) + np.array_equal(k, mik_k.toarray()) + np.array_equal(mik_k.tosparse().toarray(), k) + kb = np.array( + [ + [ 0, -5, -4, -3, -2, -1], + [11, 9, 7, 5, 3, 1] + ] + ) + np.array_equal(kb, mik_k.tobanded()) + + minv = np.arange(1, n + 1) + np.array_equal(np.diag(1. / minv), mik_m.toarray()) + np.array_equal(mik_m.tosparse().toarray(), mik_m.toarray()) + np.array_equal(1. / minv, mik_m.tobanded()) + + e = np.array([ 1, 4, 9, 16, 25, 36]) + np.array_equal(e, mik.eigenvalues()) + np.array_equal(e[:2], mik.eigenvalues(2)) + + @pytest.mark.parametrize('dtype', tested_types) + def test_linearoperator_shape_dtype(self, dtype): + n = 7 + mik = MikotaPair(n, dtype=dtype) + mik_k = mik.k + mik_m = mik.m + assert mik_k.shape == (n, n) + assert mik_k.dtype == dtype + assert mik_m.shape == (n, n) + assert mik_m.dtype == dtype + mik_default_dtype = MikotaPair(n) + mikd_k = mik_default_dtype.k + mikd_m = mik_default_dtype.m + assert mikd_k.shape == (n, n) + assert mikd_k.dtype == np.float64 + assert mikd_m.shape == (n, n) + assert mikd_m.dtype == np.float64 + assert_array_equal(mik_k.toarray(), + mikd_k.toarray().astype(dtype)) + assert_array_equal(mik_k.tosparse().toarray(), + mikd_k.tosparse().toarray().astype(dtype)) + + @pytest.mark.parametrize('dtype', tested_types) + @pytest.mark.parametrize('argument_dtype', ALLDTYPES) + def test_dot(self, dtype, argument_dtype): + """ Test the dot-product for type preservation and consistency. + """ + result_dtype = np.promote_types(argument_dtype, dtype) + n = 5 + mik = MikotaPair(n, dtype=dtype) + mik_k = mik.k + mik_m = mik.m + x0 = np.arange(n) + x1 = x0.reshape((-1, 1)) + x2 = np.arange(2 * n).reshape((n, 2)) + lo_set = [mik_k, mik_m] + input_set = [x0, x1, x2] + for lo in lo_set: + for x in input_set: + y = lo.dot(x.astype(argument_dtype)) + assert x.shape == y.shape + assert np.can_cast(y.dtype, result_dtype) + if x.ndim == 2: + ya = lo.toarray() @ x.astype(argument_dtype) + np.array_equal(y, ya) + assert np.can_cast(ya.dtype, result_dtype) + ys = lo.tosparse() @ x.astype(argument_dtype) + np.array_equal(y, ys) + assert np.can_cast(ys.dtype, result_dtype)