We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
singular_values_ attribute needed for MultipleASE class
## Expected Behavior
MultipleASE should have an attribute that stores singular values for ease of generating scree plots, consistent with the other embedding classes.  Specifically, it should output the singular values of the concatenated adjacency spectral embeddings, consistent with Figure 10 of the [paper](https://arxiv.org/pdf/1906.10026.pdf) where it is introduced.

## Actual Behavior
MultipleASE has no attribute for singular values.

## Template Code
N/A

## Full Traceback
N/A

## Your Environment
* Python version: 3.7.6
* graspologic version: 0.1.0

## Proposed Labels
 * Question

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `graspologic/embed/mase.py`
Content:
```
1 # Copyright (c) Microsoft Corporation and contributors.
2 # Licensed under the MIT License.
3 
4 import numpy as np
5 from sklearn.utils.validation import check_is_fitted
6 
7 from ..utils import import_graph, is_almost_symmetric
8 from .base import BaseEmbedMulti
9 from .svd import select_dimension, selectSVD
10 
11 
12 class MultipleASE(BaseEmbedMulti):
13     r"""
14     Multiple Adjacency Spectral Embedding (MASE) embeds arbitrary number of input
15     graphs with matched vertex sets.
16 
17     For a population of undirected graphs, MASE assumes that the population of graphs
18     is sampled from :math:`VR^{(i)}V^T` where :math:`V \in \mathbb{R}^{n\times d}` and
19     :math:`R^{(i)} \in \mathbb{R}^{d\times d}`. Score matrices, :math:`R^{(i)}`, are
20     allowed to vary for each graph, but are symmetric. All graphs share a common a
21     latent position matrix :math:`V`.
22 
23     For a population of directed graphs, MASE assumes that the population is sampled
24     from :math:`UR^{(i)}V^T` where :math:`U \in \mathbb{R}^{n\times d_1}`,
25     :math:`V \in \mathbb{R}^{n\times d_2}`, and
26     :math:`R^{(i)} \in \mathbb{R}^{d_1\times d_2}`. In this case, score matrices
27     :math:`R^{(i)}` can be assymetric and non-square, but all graphs still share a
28     common latent position matrices :math:`U` and :math:`V`.
29 
30     Parameters
31     ----------
32     n_components : int or None, default = None
33         Desired dimensionality of output data. If "full",
34         ``n_components`` must be ``<= min(X.shape)``. Otherwise, ``n_components`` must be
35         ``< min(X.shape)``. If None, then optimal dimensions will be chosen by
36         :func:`~graspologic.embed.select_dimension` using ``n_elbows`` argument.
37 
38     n_elbows : int, optional, default: 2
39         If ``n_components`` is None, then compute the optimal embedding dimension using
40         :func:`~graspologic.embed.select_dimension`. Otherwise, ignored.
41 
42     algorithm : {'randomized' (default), 'full', 'truncated'}, optional
43         SVD solver to use:
44 
45         - 'randomized'
46             Computes randomized svd using
47             :func:`sklearn.utils.extmath.randomized_svd`
48         - 'full'
49             Computes full svd using :func:`scipy.linalg.svd`
50         - 'truncated'
51             Computes truncated svd using :func:`scipy.sparse.linalg.svds`
52 
53     n_iter : int, optional (default = 5)
54         Number of iterations for randomized SVD solver. Not used by 'full' or
55         'truncated'. The default is larger than the default in randomized_svd
56         to handle sparse matrices that may have large slowly decaying spectrum.
57 
58     scaled : bool, optional (default=True)
59         Whether to scale individual eigenvectors with eigenvalues in first embedding
60         stage.
61 
62     diag_aug : bool, optional (default = True)
63         Whether to replace the main diagonal of each adjacency matrices with
64         a vector corresponding to the degree (or sum of edge weights for a
65         weighted network) before embedding.
66 
67     concat : bool, optional (default False)
68         If graph(s) are directed, whether to concatenate each graph's left and right (out and in) latent positions
69         along axis 1.
70 
71 
72     Attributes
73     ----------
74     n_graphs_ : int
75         Number of graphs
76 
77     n_vertices_ : int
78         Number of vertices in each graph
79 
80     latent_left_ : array, shape (n_samples, n_components)
81         Estimated left latent positions of the graph.
82 
83     latent_right_ : array, shape (n_samples, n_components), or None
84         Estimated right latent positions of the graph. Only computed when the an input
85         graph is directed, or adjacency matrix is assymetric. Otherwise, None.
86 
87     scores_ : array, shape (n_samples, n_components, n_components)
88         Estimated :math:`\hat{R}` matrices for each input graph.
89 
90 
91     Notes
92     -----
93     When an input graph is directed, ``n_components`` of :attr:`latent_left_` may not be equal
94     to ``n_components`` of :attr:`latent_right_`.
95     """
96 
97     def __init__(
98         self,
99         n_components=None,
100         n_elbows=2,
101         algorithm="randomized",
102         n_iter=5,
103         scaled=True,
104         diag_aug=True,
105         concat=False,
106     ):
107         if not isinstance(scaled, bool):
108             msg = "scaled must be a boolean, not {}".format(scaled)
109             raise TypeError(msg)
110 
111         super().__init__(
112             n_components=n_components,
113             n_elbows=n_elbows,
114             algorithm=algorithm,
115             n_iter=n_iter,
116             diag_aug=diag_aug,
117             concat=concat,
118         )
119         self.scaled = scaled
120 
121     def _reduce_dim(self, graphs):
122         # first embed into log2(n_vertices) for each graph
123         n_components = int(np.ceil(np.log2(np.min(self.n_vertices_))))
124 
125         # embed individual graphs
126         embeddings = [
127             selectSVD(
128                 graph,
129                 n_components=n_components,
130                 algorithm=self.algorithm,
131                 n_iter=self.n_iter,
132             )
133             for graph in graphs
134         ]
135         Us, Ds, Vs = zip(*embeddings)
136 
137         # Choose the best embedding dimension for each graphs
138         if self.n_components is None:
139             embedding_dimensions = []
140             for D in Ds:
141                 elbows, _ = select_dimension(D, n_elbows=self.n_elbows)
142                 embedding_dimensions.append(elbows[-1])
143 
144             # Choose the max of all of best embedding dimension of all graphs
145             best_dimension = int(np.ceil(np.max(embedding_dimensions)))
146         else:
147             best_dimension = self.n_components
148 
149         if not self.scaled:
150             Us = np.hstack([U[:, :best_dimension] for U in Us])
151             Vs = np.hstack([V.T[:, :best_dimension] for V in Vs])
152         else:
153             # Equivalent to ASE
154             Us = np.hstack(
155                 [
156                     U[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))
157                     for U, D in zip(Us, Ds)
158                 ]
159             )
160             Vs = np.hstack(
161                 [
162                     V.T[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))
163                     for V, D in zip(Vs, Ds)
164                 ]
165             )
166 
167         # Second SVD for vertices
168         # The notation is slightly different than the paper
169         Uhat, _, _ = selectSVD(
170             Us,
171             n_components=self.n_components,
172             n_elbows=self.n_elbows,
173             algorithm=self.algorithm,
174             n_iter=self.n_iter,
175         )
176 
177         Vhat, _, _ = selectSVD(
178             Vs,
179             n_components=self.n_components,
180             n_elbows=self.n_elbows,
181             algorithm=self.algorithm,
182             n_iter=self.n_iter,
183         )
184         return Uhat, Vhat
185 
186     def fit(self, graphs, y=None):
187         """
188         Fit the model with graphs.
189 
190         Parameters
191         ----------
192         graphs : list of nx.Graph or ndarray, or ndarray
193             If list of nx.Graph, each Graph must contain same number of nodes.
194             If list of ndarray, each array must have shape (n_vertices, n_vertices).
195             If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).
196 
197         Returns
198         -------
199         self : object
200             Returns an instance of self.
201         """
202         graphs = self._check_input_graphs(graphs)
203 
204         # Check if undirected
205         undirected = all(is_almost_symmetric(g) for g in graphs)
206 
207         # Diag augment
208         if self.diag_aug:
209             graphs = self._diag_aug(graphs)
210 
211         # embed
212         Uhat, Vhat = self._reduce_dim(graphs)
213         self.latent_left_ = Uhat
214         if not undirected:
215             self.latent_right_ = Vhat
216             self.scores_ = Uhat.T @ graphs @ Vhat
217         else:
218             self.latent_right_ = None
219             self.scores_ = Uhat.T @ graphs @ Uhat
220 
221         return self
222 
223     def fit_transform(self, graphs, y=None):
224         """
225         Fit the model with graphs and apply the embedding on graphs.
226         n_components is either automatically determined or based on user input.
227 
228         Parameters
229         ----------
230         graphs : list of nx.Graph or ndarray, or ndarray
231             If list of nx.Graph, each Graph must contain same number of nodes.
232             If list of ndarray, each array must have shape (n_vertices, n_vertices).
233             If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).
234 
235         Returns
236         -------
237         out : np.ndarray or length 2 tuple of np.ndarray.
238             If input graphs were symmetric shape (n_vertices, n_components).
239             If graphs were directed and ``concat`` is False, returns tuple of two arrays (same shape as above).
240             The first corresponds to the left latent positions, and the second to the right latent positions.
241             When ``concat`` is True left and right (out and in) latent positions are concatenated along axis 1.
242         """
243         return self._fit_transform(graphs)
244
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/graspologic/embed/mase.py b/graspologic/embed/mase.py
--- a/graspologic/embed/mase.py
+++ b/graspologic/embed/mase.py
@@ -87,6 +87,13 @@
     scores_ : array, shape (n_samples, n_components, n_components)
         Estimated :math:`\hat{R}` matrices for each input graph.
 
+    singular_values_ : array, shape (n_components) OR length 2 tuple of arrays
+        If input graph is undirected, equal to the singular values of the concatenated
+        adjacency spectral embeddings. If input graph is directed, :attr:`singular_values_`
+        is a tuple of length 2, where :attr:`singular_values_[0]` corresponds to
+        the singular values of the concatenated left adjacency spectral embeddings,
+        and :attr:`singular_values_[1]` corresponds to
+        the singular values of the concatenated right adjacency spectral embeddings.
 
     Notes
     -----
@@ -166,7 +173,7 @@
 
         # Second SVD for vertices
         # The notation is slightly different than the paper
-        Uhat, _, _ = selectSVD(
+        Uhat, sing_vals_left, _ = selectSVD(
             Us,
             n_components=self.n_components,
             n_elbows=self.n_elbows,
@@ -174,14 +181,14 @@
             n_iter=self.n_iter,
         )
 
-        Vhat, _, _ = selectSVD(
+        Vhat, sing_vals_right, _ = selectSVD(
             Vs,
             n_components=self.n_components,
             n_elbows=self.n_elbows,
             algorithm=self.algorithm,
             n_iter=self.n_iter,
         )
-        return Uhat, Vhat
+        return Uhat, Vhat, sing_vals_left, sing_vals_right
 
     def fit(self, graphs, y=None):
         """
@@ -209,14 +216,16 @@
             graphs = self._diag_aug(graphs)
 
         # embed
-        Uhat, Vhat = self._reduce_dim(graphs)
+        Uhat, Vhat, sing_vals_left, sing_vals_right = self._reduce_dim(graphs)
         self.latent_left_ = Uhat
         if not undirected:
             self.latent_right_ = Vhat
             self.scores_ = Uhat.T @ graphs @ Vhat
+            self.singular_values_ = (sing_vals_left, sing_vals_right)
         else:
             self.latent_right_ = None
             self.scores_ = Uhat.T @ graphs @ Uhat
+            self.singular_values_ = sing_vals_left
 
         return self

{"golden_diff": "diff --git a/graspologic/embed/mase.py b/graspologic/embed/mase.py\n--- a/graspologic/embed/mase.py\n+++ b/graspologic/embed/mase.py\n@@ -87,6 +87,13 @@\n     scores_ : array, shape (n_samples, n_components, n_components)\n         Estimated :math:`\\hat{R}` matrices for each input graph.\n \n+    singular_values_ : array, shape (n_components) OR length 2 tuple of arrays\n+        If input graph is undirected, equal to the singular values of the concatenated\n+        adjacency spectral embeddings. If input graph is directed, :attr:`singular_values_`\n+        is a tuple of length 2, where :attr:`singular_values_[0]` corresponds to\n+        the singular values of the concatenated left adjacency spectral embeddings,\n+        and :attr:`singular_values_[1]` corresponds to\n+        the singular values of the concatenated right adjacency spectral embeddings.\n \n     Notes\n     -----\n@@ -166,7 +173,7 @@\n \n         # Second SVD for vertices\n         # The notation is slightly different than the paper\n-        Uhat, _, _ = selectSVD(\n+        Uhat, sing_vals_left, _ = selectSVD(\n             Us,\n             n_components=self.n_components,\n             n_elbows=self.n_elbows,\n@@ -174,14 +181,14 @@\n             n_iter=self.n_iter,\n         )\n \n-        Vhat, _, _ = selectSVD(\n+        Vhat, sing_vals_right, _ = selectSVD(\n             Vs,\n             n_components=self.n_components,\n             n_elbows=self.n_elbows,\n             algorithm=self.algorithm,\n             n_iter=self.n_iter,\n         )\n-        return Uhat, Vhat\n+        return Uhat, Vhat, sing_vals_left, sing_vals_right\n \n     def fit(self, graphs, y=None):\n         \"\"\"\n@@ -209,14 +216,16 @@\n             graphs = self._diag_aug(graphs)\n \n         # embed\n-        Uhat, Vhat = self._reduce_dim(graphs)\n+        Uhat, Vhat, sing_vals_left, sing_vals_right = self._reduce_dim(graphs)\n         self.latent_left_ = Uhat\n         if not undirected:\n             self.latent_right_ = Vhat\n             self.scores_ = Uhat.T @ graphs @ Vhat\n+            self.singular_values_ = (sing_vals_left, sing_vals_right)\n         else:\n             self.latent_right_ = None\n             self.scores_ = Uhat.T @ graphs @ Uhat\n+            self.singular_values_ = sing_vals_left\n \n         return self\n", "issue": "singular_values_ attribute needed for MultipleASE class\n## Expected Behavior\r\nMultipleASE should have an attribute that stores singular values for ease of generating scree plots, consistent with the other embedding classes.  Specifically, it should output the singular values of the concatenated adjacency spectral embeddings, consistent with Figure 10 of the [paper](https://arxiv.org/pdf/1906.10026.pdf) where it is introduced.\r\n\r\n## Actual Behavior\r\nMultipleASE has no attribute for singular values.\r\n\r\n## Template Code\r\nN/A\r\n\r\n## Full Traceback\r\nN/A\r\n\r\n## Your Environment\r\n* Python version: 3.7.6\r\n* graspologic version: 0.1.0\r\n\r\n## Proposed Labels\r\n * Question\n", "before_files": [{"content": "# Copyright (c) Microsoft Corporation and contributors.\n# Licensed under the MIT License.\n\nimport numpy as np\nfrom sklearn.utils.validation import check_is_fitted\n\nfrom ..utils import import_graph, is_almost_symmetric\nfrom .base import BaseEmbedMulti\nfrom .svd import select_dimension, selectSVD\n\n\nclass MultipleASE(BaseEmbedMulti):\n    r\"\"\"\n    Multiple Adjacency Spectral Embedding (MASE) embeds arbitrary number of input\n    graphs with matched vertex sets.\n\n    For a population of undirected graphs, MASE assumes that the population of graphs\n    is sampled from :math:`VR^{(i)}V^T` where :math:`V \\in \\mathbb{R}^{n\\times d}` and\n    :math:`R^{(i)} \\in \\mathbb{R}^{d\\times d}`. Score matrices, :math:`R^{(i)}`, are\n    allowed to vary for each graph, but are symmetric. All graphs share a common a\n    latent position matrix :math:`V`.\n\n    For a population of directed graphs, MASE assumes that the population is sampled\n    from :math:`UR^{(i)}V^T` where :math:`U \\in \\mathbb{R}^{n\\times d_1}`,\n    :math:`V \\in \\mathbb{R}^{n\\times d_2}`, and\n    :math:`R^{(i)} \\in \\mathbb{R}^{d_1\\times d_2}`. In this case, score matrices\n    :math:`R^{(i)}` can be assymetric and non-square, but all graphs still share a\n    common latent position matrices :math:`U` and :math:`V`.\n\n    Parameters\n    ----------\n    n_components : int or None, default = None\n        Desired dimensionality of output data. If \"full\",\n        ``n_components`` must be ``<= min(X.shape)``. Otherwise, ``n_components`` must be\n        ``< min(X.shape)``. If None, then optimal dimensions will be chosen by\n        :func:`~graspologic.embed.select_dimension` using ``n_elbows`` argument.\n\n    n_elbows : int, optional, default: 2\n        If ``n_components`` is None, then compute the optimal embedding dimension using\n        :func:`~graspologic.embed.select_dimension`. Otherwise, ignored.\n\n    algorithm : {'randomized' (default), 'full', 'truncated'}, optional\n        SVD solver to use:\n\n        - 'randomized'\n            Computes randomized svd using\n            :func:`sklearn.utils.extmath.randomized_svd`\n        - 'full'\n            Computes full svd using :func:`scipy.linalg.svd`\n        - 'truncated'\n            Computes truncated svd using :func:`scipy.sparse.linalg.svds`\n\n    n_iter : int, optional (default = 5)\n        Number of iterations for randomized SVD solver. Not used by 'full' or\n        'truncated'. The default is larger than the default in randomized_svd\n        to handle sparse matrices that may have large slowly decaying spectrum.\n\n    scaled : bool, optional (default=True)\n        Whether to scale individual eigenvectors with eigenvalues in first embedding\n        stage.\n\n    diag_aug : bool, optional (default = True)\n        Whether to replace the main diagonal of each adjacency matrices with\n        a vector corresponding to the degree (or sum of edge weights for a\n        weighted network) before embedding.\n\n    concat : bool, optional (default False)\n        If graph(s) are directed, whether to concatenate each graph's left and right (out and in) latent positions\n        along axis 1.\n\n\n    Attributes\n    ----------\n    n_graphs_ : int\n        Number of graphs\n\n    n_vertices_ : int\n        Number of vertices in each graph\n\n    latent_left_ : array, shape (n_samples, n_components)\n        Estimated left latent positions of the graph.\n\n    latent_right_ : array, shape (n_samples, n_components), or None\n        Estimated right latent positions of the graph. Only computed when the an input\n        graph is directed, or adjacency matrix is assymetric. Otherwise, None.\n\n    scores_ : array, shape (n_samples, n_components, n_components)\n        Estimated :math:`\\hat{R}` matrices for each input graph.\n\n\n    Notes\n    -----\n    When an input graph is directed, ``n_components`` of :attr:`latent_left_` may not be equal\n    to ``n_components`` of :attr:`latent_right_`.\n    \"\"\"\n\n    def __init__(\n        self,\n        n_components=None,\n        n_elbows=2,\n        algorithm=\"randomized\",\n        n_iter=5,\n        scaled=True,\n        diag_aug=True,\n        concat=False,\n    ):\n        if not isinstance(scaled, bool):\n            msg = \"scaled must be a boolean, not {}\".format(scaled)\n            raise TypeError(msg)\n\n        super().__init__(\n            n_components=n_components,\n            n_elbows=n_elbows,\n            algorithm=algorithm,\n            n_iter=n_iter,\n            diag_aug=diag_aug,\n            concat=concat,\n        )\n        self.scaled = scaled\n\n    def _reduce_dim(self, graphs):\n        # first embed into log2(n_vertices) for each graph\n        n_components = int(np.ceil(np.log2(np.min(self.n_vertices_))))\n\n        # embed individual graphs\n        embeddings = [\n            selectSVD(\n                graph,\n                n_components=n_components,\n                algorithm=self.algorithm,\n                n_iter=self.n_iter,\n            )\n            for graph in graphs\n        ]\n        Us, Ds, Vs = zip(*embeddings)\n\n        # Choose the best embedding dimension for each graphs\n        if self.n_components is None:\n            embedding_dimensions = []\n            for D in Ds:\n                elbows, _ = select_dimension(D, n_elbows=self.n_elbows)\n                embedding_dimensions.append(elbows[-1])\n\n            # Choose the max of all of best embedding dimension of all graphs\n            best_dimension = int(np.ceil(np.max(embedding_dimensions)))\n        else:\n            best_dimension = self.n_components\n\n        if not self.scaled:\n            Us = np.hstack([U[:, :best_dimension] for U in Us])\n            Vs = np.hstack([V.T[:, :best_dimension] for V in Vs])\n        else:\n            # Equivalent to ASE\n            Us = np.hstack(\n                [\n                    U[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))\n                    for U, D in zip(Us, Ds)\n                ]\n            )\n            Vs = np.hstack(\n                [\n                    V.T[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))\n                    for V, D in zip(Vs, Ds)\n                ]\n            )\n\n        # Second SVD for vertices\n        # The notation is slightly different than the paper\n        Uhat, _, _ = selectSVD(\n            Us,\n            n_components=self.n_components,\n            n_elbows=self.n_elbows,\n            algorithm=self.algorithm,\n            n_iter=self.n_iter,\n        )\n\n        Vhat, _, _ = selectSVD(\n            Vs,\n            n_components=self.n_components,\n            n_elbows=self.n_elbows,\n            algorithm=self.algorithm,\n            n_iter=self.n_iter,\n        )\n        return Uhat, Vhat\n\n    def fit(self, graphs, y=None):\n        \"\"\"\n        Fit the model with graphs.\n\n        Parameters\n        ----------\n        graphs : list of nx.Graph or ndarray, or ndarray\n            If list of nx.Graph, each Graph must contain same number of nodes.\n            If list of ndarray, each array must have shape (n_vertices, n_vertices).\n            If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).\n\n        Returns\n        -------\n        self : object\n            Returns an instance of self.\n        \"\"\"\n        graphs = self._check_input_graphs(graphs)\n\n        # Check if undirected\n        undirected = all(is_almost_symmetric(g) for g in graphs)\n\n        # Diag augment\n        if self.diag_aug:\n            graphs = self._diag_aug(graphs)\n\n        # embed\n        Uhat, Vhat = self._reduce_dim(graphs)\n        self.latent_left_ = Uhat\n        if not undirected:\n            self.latent_right_ = Vhat\n            self.scores_ = Uhat.T @ graphs @ Vhat\n        else:\n            self.latent_right_ = None\n            self.scores_ = Uhat.T @ graphs @ Uhat\n\n        return self\n\n    def fit_transform(self, graphs, y=None):\n        \"\"\"\n        Fit the model with graphs and apply the embedding on graphs.\n        n_components is either automatically determined or based on user input.\n\n        Parameters\n        ----------\n        graphs : list of nx.Graph or ndarray, or ndarray\n            If list of nx.Graph, each Graph must contain same number of nodes.\n            If list of ndarray, each array must have shape (n_vertices, n_vertices).\n            If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).\n\n        Returns\n        -------\n        out : np.ndarray or length 2 tuple of np.ndarray.\n            If input graphs were symmetric shape (n_vertices, n_components).\n            If graphs were directed and ``concat`` is False, returns tuple of two arrays (same shape as above).\n            The first corresponds to the left latent positions, and the second to the right latent positions.\n            When ``concat`` is True left and right (out and in) latent positions are concatenated along axis 1.\n        \"\"\"\n        return self._fit_transform(graphs)\n", "path": "graspologic/embed/mase.py"}], "after_files": [{"content": "# Copyright (c) Microsoft Corporation and contributors.\n# Licensed under the MIT License.\n\nimport numpy as np\nfrom sklearn.utils.validation import check_is_fitted\n\nfrom ..utils import import_graph, is_almost_symmetric\nfrom .base import BaseEmbedMulti\nfrom .svd import select_dimension, selectSVD\n\n\nclass MultipleASE(BaseEmbedMulti):\n    r\"\"\"\n    Multiple Adjacency Spectral Embedding (MASE) embeds arbitrary number of input\n    graphs with matched vertex sets.\n\n    For a population of undirected graphs, MASE assumes that the population of graphs\n    is sampled from :math:`VR^{(i)}V^T` where :math:`V \\in \\mathbb{R}^{n\\times d}` and\n    :math:`R^{(i)} \\in \\mathbb{R}^{d\\times d}`. Score matrices, :math:`R^{(i)}`, are\n    allowed to vary for each graph, but are symmetric. All graphs share a common a\n    latent position matrix :math:`V`.\n\n    For a population of directed graphs, MASE assumes that the population is sampled\n    from :math:`UR^{(i)}V^T` where :math:`U \\in \\mathbb{R}^{n\\times d_1}`,\n    :math:`V \\in \\mathbb{R}^{n\\times d_2}`, and\n    :math:`R^{(i)} \\in \\mathbb{R}^{d_1\\times d_2}`. In this case, score matrices\n    :math:`R^{(i)}` can be assymetric and non-square, but all graphs still share a\n    common latent position matrices :math:`U` and :math:`V`.\n\n    Parameters\n    ----------\n    n_components : int or None, default = None\n        Desired dimensionality of output data. If \"full\",\n        ``n_components`` must be ``<= min(X.shape)``. Otherwise, ``n_components`` must be\n        ``< min(X.shape)``. If None, then optimal dimensions will be chosen by\n        :func:`~graspologic.embed.select_dimension` using ``n_elbows`` argument.\n\n    n_elbows : int, optional, default: 2\n        If ``n_components`` is None, then compute the optimal embedding dimension using\n        :func:`~graspologic.embed.select_dimension`. Otherwise, ignored.\n\n    algorithm : {'randomized' (default), 'full', 'truncated'}, optional\n        SVD solver to use:\n\n        - 'randomized'\n            Computes randomized svd using\n            :func:`sklearn.utils.extmath.randomized_svd`\n        - 'full'\n            Computes full svd using :func:`scipy.linalg.svd`\n        - 'truncated'\n            Computes truncated svd using :func:`scipy.sparse.linalg.svds`\n\n    n_iter : int, optional (default = 5)\n        Number of iterations for randomized SVD solver. Not used by 'full' or\n        'truncated'. The default is larger than the default in randomized_svd\n        to handle sparse matrices that may have large slowly decaying spectrum.\n\n    scaled : bool, optional (default=True)\n        Whether to scale individual eigenvectors with eigenvalues in first embedding\n        stage.\n\n    diag_aug : bool, optional (default = True)\n        Whether to replace the main diagonal of each adjacency matrices with\n        a vector corresponding to the degree (or sum of edge weights for a\n        weighted network) before embedding.\n\n    concat : bool, optional (default False)\n        If graph(s) are directed, whether to concatenate each graph's left and right (out and in) latent positions\n        along axis 1.\n\n\n    Attributes\n    ----------\n    n_graphs_ : int\n        Number of graphs\n\n    n_vertices_ : int\n        Number of vertices in each graph\n\n    latent_left_ : array, shape (n_samples, n_components)\n        Estimated left latent positions of the graph.\n\n    latent_right_ : array, shape (n_samples, n_components), or None\n        Estimated right latent positions of the graph. Only computed when the an input\n        graph is directed, or adjacency matrix is assymetric. Otherwise, None.\n\n    scores_ : array, shape (n_samples, n_components, n_components)\n        Estimated :math:`\\hat{R}` matrices for each input graph.\n\n    singular_values_ : array, shape (n_components) OR length 2 tuple of arrays\n        If input graph is undirected, equal to the singular values of the concatenated\n        adjacency spectral embeddings. If input graph is directed, :attr:`singular_values_`\n        is a tuple of length 2, where :attr:`singular_values_[0]` corresponds to\n        the singular values of the concatenated left adjacency spectral embeddings,\n        and :attr:`singular_values_[1]` corresponds to\n        the singular values of the concatenated right adjacency spectral embeddings.\n\n    Notes\n    -----\n    When an input graph is directed, ``n_components`` of :attr:`latent_left_` may not be equal\n    to ``n_components`` of :attr:`latent_right_`.\n    \"\"\"\n\n    def __init__(\n        self,\n        n_components=None,\n        n_elbows=2,\n        algorithm=\"randomized\",\n        n_iter=5,\n        scaled=True,\n        diag_aug=True,\n        concat=False,\n    ):\n        if not isinstance(scaled, bool):\n            msg = \"scaled must be a boolean, not {}\".format(scaled)\n            raise TypeError(msg)\n\n        super().__init__(\n            n_components=n_components,\n            n_elbows=n_elbows,\n            algorithm=algorithm,\n            n_iter=n_iter,\n            diag_aug=diag_aug,\n            concat=concat,\n        )\n        self.scaled = scaled\n\n    def _reduce_dim(self, graphs):\n        # first embed into log2(n_vertices) for each graph\n        n_components = int(np.ceil(np.log2(np.min(self.n_vertices_))))\n\n        # embed individual graphs\n        embeddings = [\n            selectSVD(\n                graph,\n                n_components=n_components,\n                algorithm=self.algorithm,\n                n_iter=self.n_iter,\n            )\n            for graph in graphs\n        ]\n        Us, Ds, Vs = zip(*embeddings)\n\n        # Choose the best embedding dimension for each graphs\n        if self.n_components is None:\n            embedding_dimensions = []\n            for D in Ds:\n                elbows, _ = select_dimension(D, n_elbows=self.n_elbows)\n                embedding_dimensions.append(elbows[-1])\n\n            # Choose the max of all of best embedding dimension of all graphs\n            best_dimension = int(np.ceil(np.max(embedding_dimensions)))\n        else:\n            best_dimension = self.n_components\n\n        if not self.scaled:\n            Us = np.hstack([U[:, :best_dimension] for U in Us])\n            Vs = np.hstack([V.T[:, :best_dimension] for V in Vs])\n        else:\n            # Equivalent to ASE\n            Us = np.hstack(\n                [\n                    U[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))\n                    for U, D in zip(Us, Ds)\n                ]\n            )\n            Vs = np.hstack(\n                [\n                    V.T[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))\n                    for V, D in zip(Vs, Ds)\n                ]\n            )\n\n        # Second SVD for vertices\n        # The notation is slightly different than the paper\n        Uhat, sing_vals_left, _ = selectSVD(\n            Us,\n            n_components=self.n_components,\n            n_elbows=self.n_elbows,\n            algorithm=self.algorithm,\n            n_iter=self.n_iter,\n        )\n\n        Vhat, sing_vals_right, _ = selectSVD(\n            Vs,\n            n_components=self.n_components,\n            n_elbows=self.n_elbows,\n            algorithm=self.algorithm,\n            n_iter=self.n_iter,\n        )\n        return Uhat, Vhat, sing_vals_left, sing_vals_right\n\n    def fit(self, graphs, y=None):\n        \"\"\"\n        Fit the model with graphs.\n\n        Parameters\n        ----------\n        graphs : list of nx.Graph or ndarray, or ndarray\n            If list of nx.Graph, each Graph must contain same number of nodes.\n            If list of ndarray, each array must have shape (n_vertices, n_vertices).\n            If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).\n\n        Returns\n        -------\n        self : object\n            Returns an instance of self.\n        \"\"\"\n        graphs = self._check_input_graphs(graphs)\n\n        # Check if undirected\n        undirected = all(is_almost_symmetric(g) for g in graphs)\n\n        # Diag augment\n        if self.diag_aug:\n            graphs = self._diag_aug(graphs)\n\n        # embed\n        Uhat, Vhat, sing_vals_left, sing_vals_right = self._reduce_dim(graphs)\n        self.latent_left_ = Uhat\n        if not undirected:\n            self.latent_right_ = Vhat\n            self.scores_ = Uhat.T @ graphs @ Vhat\n            self.singular_values_ = (sing_vals_left, sing_vals_right)\n        else:\n            self.latent_right_ = None\n            self.scores_ = Uhat.T @ graphs @ Uhat\n            self.singular_values_ = sing_vals_left\n\n        return self\n\n    def fit_transform(self, graphs, y=None):\n        \"\"\"\n        Fit the model with graphs and apply the embedding on graphs.\n        n_components is either automatically determined or based on user input.\n\n        Parameters\n        ----------\n        graphs : list of nx.Graph or ndarray, or ndarray\n            If list of nx.Graph, each Graph must contain same number of nodes.\n            If list of ndarray, each array must have shape (n_vertices, n_vertices).\n            If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).\n\n        Returns\n        -------\n        out : np.ndarray or length 2 tuple of np.ndarray.\n            If input graphs were symmetric shape (n_vertices, n_components).\n            If graphs were directed and ``concat`` is False, returns tuple of two arrays (same shape as above).\n            The first corresponds to the left latent positions, and the second to the right latent positions.\n            When ``concat`` is True left and right (out and in) latent positions are concatenated along axis 1.\n        \"\"\"\n        return self._fit_transform(graphs)\n", "path": "graspologic/embed/mase.py"}]}