We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add tutorial for MASE


--- 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     singular_values_ : array, shape (n_components) OR length 2 tuple of arrays
91         If input graph is undirected, equal to the singular values of the concatenated
92         adjacency spectral embeddings. If input graph is directed, :attr:`singular_values_`
93         is a tuple of length 2, where :attr:`singular_values_[0]` corresponds to
94         the singular values of the concatenated left adjacency spectral embeddings,
95         and :attr:`singular_values_[1]` corresponds to
96         the singular values of the concatenated right adjacency spectral embeddings.
97 
98     Notes
99     -----
100     When an input graph is directed, ``n_components`` of :attr:`latent_left_` may not be equal
101     to ``n_components`` of :attr:`latent_right_`.
102     """
103 
104     def __init__(
105         self,
106         n_components=None,
107         n_elbows=2,
108         algorithm="randomized",
109         n_iter=5,
110         scaled=True,
111         diag_aug=True,
112         concat=False,
113     ):
114         if not isinstance(scaled, bool):
115             msg = "scaled must be a boolean, not {}".format(scaled)
116             raise TypeError(msg)
117 
118         super().__init__(
119             n_components=n_components,
120             n_elbows=n_elbows,
121             algorithm=algorithm,
122             n_iter=n_iter,
123             diag_aug=diag_aug,
124             concat=concat,
125         )
126         self.scaled = scaled
127 
128     def _reduce_dim(self, graphs):
129         # first embed into log2(n_vertices) for each graph
130         n_components = int(np.ceil(np.log2(np.min(self.n_vertices_))))
131 
132         # embed individual graphs
133         embeddings = [
134             selectSVD(
135                 graph,
136                 n_components=n_components,
137                 algorithm=self.algorithm,
138                 n_iter=self.n_iter,
139             )
140             for graph in graphs
141         ]
142         Us, Ds, Vs = zip(*embeddings)
143 
144         # Choose the best embedding dimension for each graphs
145         if self.n_components is None:
146             embedding_dimensions = []
147             for D in Ds:
148                 elbows, _ = select_dimension(D, n_elbows=self.n_elbows)
149                 embedding_dimensions.append(elbows[-1])
150 
151             # Choose the max of all of best embedding dimension of all graphs
152             best_dimension = int(np.ceil(np.max(embedding_dimensions)))
153         else:
154             best_dimension = self.n_components
155 
156         if not self.scaled:
157             Us = np.hstack([U[:, :best_dimension] for U in Us])
158             Vs = np.hstack([V.T[:, :best_dimension] for V in Vs])
159         else:
160             # Equivalent to ASE
161             Us = np.hstack(
162                 [
163                     U[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))
164                     for U, D in zip(Us, Ds)
165                 ]
166             )
167             Vs = np.hstack(
168                 [
169                     V.T[:, :best_dimension] @ np.diag(np.sqrt(D[:best_dimension]))
170                     for V, D in zip(Vs, Ds)
171                 ]
172             )
173 
174         # Second SVD for vertices
175         # The notation is slightly different than the paper
176         Uhat, sing_vals_left, _ = selectSVD(
177             Us,
178             n_components=self.n_components,
179             n_elbows=self.n_elbows,
180             algorithm=self.algorithm,
181             n_iter=self.n_iter,
182         )
183 
184         Vhat, sing_vals_right, _ = selectSVD(
185             Vs,
186             n_components=self.n_components,
187             n_elbows=self.n_elbows,
188             algorithm=self.algorithm,
189             n_iter=self.n_iter,
190         )
191         return Uhat, Vhat, sing_vals_left, sing_vals_right
192 
193     def fit(self, graphs, y=None):
194         """
195         Fit the model with graphs.
196 
197         Parameters
198         ----------
199         graphs : list of nx.Graph or ndarray, or ndarray
200             If list of nx.Graph, each Graph must contain same number of nodes.
201             If list of ndarray, each array must have shape (n_vertices, n_vertices).
202             If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).
203 
204         Returns
205         -------
206         self : object
207             Returns an instance of self.
208         """
209         graphs = self._check_input_graphs(graphs)
210 
211         # Check if undirected
212         undirected = all(is_almost_symmetric(g) for g in graphs)
213 
214         # Diag augment
215         if self.diag_aug:
216             graphs = self._diag_aug(graphs)
217 
218         # embed
219         Uhat, Vhat, sing_vals_left, sing_vals_right = self._reduce_dim(graphs)
220         self.latent_left_ = Uhat
221         if not undirected:
222             self.latent_right_ = Vhat
223             self.scores_ = Uhat.T @ graphs @ Vhat
224             self.singular_values_ = (sing_vals_left, sing_vals_right)
225         else:
226             self.latent_right_ = None
227             self.scores_ = Uhat.T @ graphs @ Uhat
228             self.singular_values_ = sing_vals_left
229 
230         return self
231 
232     def fit_transform(self, graphs, y=None):
233         """
234         Fit the model with graphs and apply the embedding on graphs.
235         n_components is either automatically determined or based on user input.
236 
237         Parameters
238         ----------
239         graphs : list of nx.Graph or ndarray, or ndarray
240             If list of nx.Graph, each Graph must contain same number of nodes.
241             If list of ndarray, each array must have shape (n_vertices, n_vertices).
242             If ndarray, then array must have shape (n_graphs, n_vertices, n_vertices).
243 
244         Returns
245         -------
246         out : np.ndarray or length 2 tuple of np.ndarray.
247             If input graphs were symmetric shape (n_vertices, n_components).
248             If graphs were directed and ``concat`` is False, returns tuple of two arrays (same shape as above).
249             The first corresponds to the left latent positions, and the second to the right latent positions.
250             When ``concat`` is True left and right (out and in) latent positions are concatenated along axis 1.
251         """
252         return self._fit_transform(graphs)
253
```
--- 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
@@ -27,6 +27,8 @@
     :math:`R^{(i)}` can be assymetric and non-square, but all graphs still share a
     common latent position matrices :math:`U` and :math:`V`.
 
+    Read more in the :ref:`tutorials <embed_tutorials>`
+
     Parameters
     ----------
     n_components : int or None, default = None

{"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@@ -27,6 +27,8 @@\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+    Read more in the :ref:`tutorials <embed_tutorials>`\n+\n     Parameters\n     ----------\n     n_components : int or None, default = None\n", "issue": "Add tutorial for MASE\n\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    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"}], "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    Read more in the :ref:`tutorials <embed_tutorials>`\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"}]}