We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Floating Point Error in Lattice.py
I tried generating a periodic Kagome lattice using lattice.py:

>>> kagome = nk.graph.Lattice(basis_vectors=[[2.,0.],[1.,np.sqrt(3)]],extent=[2,2],atoms_coord=[[0.,0.],[1./2.,np.sqrt(3)/2.],[1.,0.]])

Only half of the edges appeared:

>>>kagome.n_edges()
12

The bug is related to floating points, some distances are registered as 0.99999... and some are registered as 1.0


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `netket/graph/lattice.py`
Content:
```
1 # Copyright 2020, 2021 The NetKet Authors - All rights reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 #    http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 from .graph import NetworkX
16 from scipy.spatial import cKDTree
17 from scipy.sparse import find, triu
18 import numpy as _np
19 import itertools
20 import networkx as _nx
21 import warnings
22 
23 
24 def get_edges(atoms_positions, cutoff):
25     kdtree = cKDTree(atoms_positions)
26     dist_matrix = kdtree.sparse_distance_matrix(kdtree, cutoff)
27     id1, id2, values = find(triu(dist_matrix))
28     pairs = []
29     min_dists = {}  # keys are nodes, values are min dists
30     for node in _np.unique(_np.concatenate((id1, id2))):
31         min_dist = _np.min(values[(id1 == node) | (id2 == node)])
32         min_dists[node] = min_dist
33     for node in _np.unique(id1):
34         min_dist = _np.min(values[id1 == node])
35         mask = (id1 == node) & (values == min_dist)
36         first = id1[mask]
37         second = id2[mask]
38         for pair in zip(first, second):
39             if min_dist == min_dists[pair[0]] and min_dist == min_dists[pair[1]]:
40                 pairs.append(pair)
41     return pairs
42 
43 
44 def create_points(basis_vectors, extent, atom_coords, pbc):
45     shell_vec = _np.zeros(extent.size, dtype=int)
46     shift_vec = _np.zeros(extent.size, dtype=int)
47     # note: by modifying these, the number of shells can be tuned.
48     shell_vec[pbc] = 2
49     shift_vec[pbc] = 1
50     ranges = tuple([list(range(ex)) for ex in extent + shell_vec])
51     atoms = []
52     cellANDlabel_to_site = {}
53     for s_cell in itertools.product(*ranges):
54         s_coord_cell = _np.asarray(s_cell) - shift_vec
55         if _np.any(s_coord_cell < 0) or _np.any(s_coord_cell > (extent - 1)):
56             inside = False
57         else:
58             inside = True
59         atom_count = len(atoms)
60         for i, atom_coord in enumerate(atom_coords):
61             s_coord_atom = s_coord_cell + atom_coord
62             r_coord_atom = _np.matmul(basis_vectors.T, s_coord_atom)
63             atoms.append(
64                 {
65                     "Label": i,
66                     "cell": s_coord_cell,
67                     "r_coord": r_coord_atom,
68                     "inside": inside,
69                 }
70             )
71             if tuple(s_coord_cell) not in cellANDlabel_to_site.keys():
72                 cellANDlabel_to_site[tuple(s_coord_cell)] = {}
73             cellANDlabel_to_site[tuple(s_coord_cell)][i] = atom_count + i
74     return atoms, cellANDlabel_to_site
75 
76 
77 def get_true_edges(basis_vectors, atoms, cellANDlabel_to_site, extent):
78     atoms_positions = dicts_to_array(atoms, "r_coord")
79     naive_edges = get_edges(
80         atoms_positions, _np.linalg.norm(basis_vectors, axis=1).max()
81     )
82     true_edges = []
83     for node1, node2 in naive_edges:
84         atom1 = atoms[node1]
85         atom2 = atoms[node2]
86         if atom1["inside"] and atom2["inside"]:
87             true_edges.append((node1, node2))
88         elif atom1["inside"] or atom2["inside"]:
89             cell1 = atom1["cell"] % extent
90             cell2 = atom2["cell"] % extent
91             node1 = cellANDlabel_to_site[tuple(cell1)][atom1["Label"]]
92             node2 = cellANDlabel_to_site[tuple(cell2)][atom2["Label"]]
93             edge = (node1, node2)
94             if edge not in true_edges and (node2, node1) not in true_edges:
95                 true_edges.append(edge)
96     return true_edges
97 
98 
99 def dicts_to_array(dicts, key):
100     result = []
101     for d in dicts:
102         result.append(d[key])
103     return _np.asarray(result)
104 
105 
106 class Lattice(NetworkX):
107     """A lattice built translating a unit cell and adding edges between nearest neighbours sites.
108 
109     The unit cell is defined by the ``basis_vectors`` and it can contain an arbitrary number of atoms.
110     Each atom is located at an arbitrary position and is labelled by an integer number,
111     meant to distinguish between the different atoms within the unit cell.
112     Periodic boundary conditions can also be imposed along the desired directions.
113     There are three different ways to refer to the lattice sites. A site can be labelled
114     by a simple integer number (the site index) or by its coordinates (actual position in space).
115     """
116 
117     def __init__(self, basis_vectors, extent, *, pbc: bool = True, atoms_coord=[]):
118         """
119         Constructs a new ``Lattice`` given its side length and the features of the unit cell.
120 
121         Args:
122             basis_vectors: The basis vectors of the unit cell.
123             extent: The number of copies of the unit cell.
124             pbc: If ``True`` then the constructed lattice
125                 will have periodic boundary conditions, otherwise
126                 open boundary conditions are imposed (default=``True``).
127             atoms_coord: The coordinates of different atoms in the unit cell (default=one atom at the origin).
128 
129         Examples:
130             Constructs a rectangular 3X4 lattice with periodic boundary conditions.
131 
132             >>> import netket
133             >>> g=netket.graph.Lattice(basis_vectors=[[1,0],[0,1]],extent=[3,4])
134             >>> print(g.n_nodes)
135             12
136 
137         """
138 
139         self._basis_vectors = _np.asarray(basis_vectors)
140         if self._basis_vectors.ndim != 2:
141             raise ValueError("Every vector must have the same dimension.")
142         if self._basis_vectors.shape[0] != self._basis_vectors.shape[1]:
143             raise ValueError(
144                 "basis_vectors must be a basis for the N-dimensional vector space you chose"
145             )
146 
147         if not atoms_coord:
148             atoms_coord = [_np.zeros(self._basis_vectors.shape[0])]
149         atoms_coord = _np.asarray(atoms_coord)
150         atoms_coord_fractional = _np.asarray(
151             [
152                 _np.matmul(_np.linalg.inv(self._basis_vectors.T), atom_coord)
153                 for atom_coord in atoms_coord
154             ]
155         )
156         if atoms_coord_fractional.min() < 0 or atoms_coord_fractional.max() >= 1:
157             # Maybe there is another way to state this. I want to avoid that there exists the possibility that two atoms from different cells are at the same position:
158             raise ValueError(
159                 "atoms must reside inside their corresponding unit cell, which includes only the 0-faces in fractional coordinates."
160             )
161         uniques = _np.unique(atoms_coord, axis=0)
162         if len(atoms_coord) != uniques.shape[0]:
163             atoms_coord = _np.asarray(uniques)
164             warnings.warn(
165                 f"Some atom positions are not unique. Duplicates were dropped, and now atom positions are {atoms_coord}",
166                 UserWarning,
167             )
168 
169         self._atoms_coord = atoms_coord
170 
171         if isinstance(pbc, bool):
172             self._pbc = [pbc] * self._basis_vectors.shape[1]
173         elif (
174             not isinstance(pbc, list)
175             or len(pbc) != self._basis_vectors.shape[1]
176             or sum([1 for pbci in pbc if isinstance(pbci, bool)])
177             != self._basis_vectors.shape[1]
178         ):
179             raise ValueError(
180                 "pbc must be either a boolean or a list of booleans with the same dimension as the vector space you chose."
181             )
182         else:
183             self._pbc = pbc
184 
185         extent = _np.asarray(extent)
186         self.extent = extent
187 
188         atoms, cellANDlabel_to_site = create_points(
189             self._basis_vectors, extent, atoms_coord_fractional, pbc
190         )
191         edges = get_true_edges(self._basis_vectors, atoms, cellANDlabel_to_site, extent)
192         graph = _nx.MultiGraph(edges)
193 
194         # Rename atoms
195         old_nodes = sorted(set([node for edge in edges for node in edge]))
196         self._atoms = [atoms[old_node] for old_node in old_nodes]
197         self._coord_to_site = {
198             tuple(atom["r_coord"]): new_site
199             for new_site, atom in enumerate(self._atoms)
200         }
201         new_nodes = {old_node: new_node for new_node, old_node in enumerate(old_nodes)}
202         graph = _nx.relabel_nodes(graph, new_nodes)
203 
204         # Order node names
205         nodes = sorted(graph.nodes())
206         edges = list(graph.edges())
207         graph = _nx.MultiGraph()
208         graph.add_nodes_from(nodes)
209         graph.add_edges_from(edges)
210 
211         super().__init__(graph)
212 
213     @property
214     def basis_vectors(self):
215         return self._basis_vectors
216 
217     @property
218     def atoms_coord(self):
219         """
220         Coordinates of atoms in the unit cell.
221         """
222         return self._atoms_coord
223 
224     def atom_label(self, site):
225         return self._atoms[site]["Label"]
226 
227     def site_to_coord(self, site):
228         return self._atoms[site]["r_coord"]
229 
230     def coord_to_site(self, coord):
231         return self._coord_to_site[tuple(coord)]
232 
233     def site_to_vector(self, site):
234         return self._atoms[site]["cell"]
235 
236     def vector_to_coord(self, vector):
237         return _np.matmul(self._basis_vectors, vector)
238 
239     def __repr__(self):
240         return "Lattice(n_nodes={})\n  extent={}\n  basis_vectors={}".format(
241             self.n_nodes, self.extent.tolist(), self.basis_vectors.tolist()
242         )
243
```
--- 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/netket/graph/lattice.py b/netket/graph/lattice.py
--- a/netket/graph/lattice.py
+++ b/netket/graph/lattice.py
@@ -32,11 +32,13 @@
         min_dists[node] = min_dist
     for node in _np.unique(id1):
         min_dist = _np.min(values[id1 == node])
-        mask = (id1 == node) & (values == min_dist)
+        mask = (id1 == node) & (_np.isclose(values, min_dist))
         first = id1[mask]
         second = id2[mask]
         for pair in zip(first, second):
-            if min_dist == min_dists[pair[0]] and min_dist == min_dists[pair[1]]:
+            if _np.isclose(min_dist, min_dists[pair[0]]) and _np.isclose(
+                min_dist, min_dists[pair[1]]
+            ):
                 pairs.append(pair)
     return pairs

{"golden_diff": "diff --git a/netket/graph/lattice.py b/netket/graph/lattice.py\n--- a/netket/graph/lattice.py\n+++ b/netket/graph/lattice.py\n@@ -32,11 +32,13 @@\n         min_dists[node] = min_dist\n     for node in _np.unique(id1):\n         min_dist = _np.min(values[id1 == node])\n-        mask = (id1 == node) & (values == min_dist)\n+        mask = (id1 == node) & (_np.isclose(values, min_dist))\n         first = id1[mask]\n         second = id2[mask]\n         for pair in zip(first, second):\n-            if min_dist == min_dists[pair[0]] and min_dist == min_dists[pair[1]]:\n+            if _np.isclose(min_dist, min_dists[pair[0]]) and _np.isclose(\n+                min_dist, min_dists[pair[1]]\n+            ):\n                 pairs.append(pair)\n     return pairs\n", "issue": "Floating Point Error in Lattice.py\nI tried generating a periodic Kagome lattice using lattice.py:\r\n\r\n>>> kagome = nk.graph.Lattice(basis_vectors=[[2.,0.],[1.,np.sqrt(3)]],extent=[2,2],atoms_coord=[[0.,0.],[1./2.,np.sqrt(3)/2.],[1.,0.]])\r\n\r\nOnly half of the edges appeared:\r\n\r\n>>>kagome.n_edges()\r\n12\r\n\r\nThe bug is related to floating points, some distances are registered as 0.99999... and some are registered as 1.0\r\n\n", "before_files": [{"content": "# Copyright 2020, 2021 The NetKet Authors - All rights reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom .graph import NetworkX\nfrom scipy.spatial import cKDTree\nfrom scipy.sparse import find, triu\nimport numpy as _np\nimport itertools\nimport networkx as _nx\nimport warnings\n\n\ndef get_edges(atoms_positions, cutoff):\n    kdtree = cKDTree(atoms_positions)\n    dist_matrix = kdtree.sparse_distance_matrix(kdtree, cutoff)\n    id1, id2, values = find(triu(dist_matrix))\n    pairs = []\n    min_dists = {}  # keys are nodes, values are min dists\n    for node in _np.unique(_np.concatenate((id1, id2))):\n        min_dist = _np.min(values[(id1 == node) | (id2 == node)])\n        min_dists[node] = min_dist\n    for node in _np.unique(id1):\n        min_dist = _np.min(values[id1 == node])\n        mask = (id1 == node) & (values == min_dist)\n        first = id1[mask]\n        second = id2[mask]\n        for pair in zip(first, second):\n            if min_dist == min_dists[pair[0]] and min_dist == min_dists[pair[1]]:\n                pairs.append(pair)\n    return pairs\n\n\ndef create_points(basis_vectors, extent, atom_coords, pbc):\n    shell_vec = _np.zeros(extent.size, dtype=int)\n    shift_vec = _np.zeros(extent.size, dtype=int)\n    # note: by modifying these, the number of shells can be tuned.\n    shell_vec[pbc] = 2\n    shift_vec[pbc] = 1\n    ranges = tuple([list(range(ex)) for ex in extent + shell_vec])\n    atoms = []\n    cellANDlabel_to_site = {}\n    for s_cell in itertools.product(*ranges):\n        s_coord_cell = _np.asarray(s_cell) - shift_vec\n        if _np.any(s_coord_cell < 0) or _np.any(s_coord_cell > (extent - 1)):\n            inside = False\n        else:\n            inside = True\n        atom_count = len(atoms)\n        for i, atom_coord in enumerate(atom_coords):\n            s_coord_atom = s_coord_cell + atom_coord\n            r_coord_atom = _np.matmul(basis_vectors.T, s_coord_atom)\n            atoms.append(\n                {\n                    \"Label\": i,\n                    \"cell\": s_coord_cell,\n                    \"r_coord\": r_coord_atom,\n                    \"inside\": inside,\n                }\n            )\n            if tuple(s_coord_cell) not in cellANDlabel_to_site.keys():\n                cellANDlabel_to_site[tuple(s_coord_cell)] = {}\n            cellANDlabel_to_site[tuple(s_coord_cell)][i] = atom_count + i\n    return atoms, cellANDlabel_to_site\n\n\ndef get_true_edges(basis_vectors, atoms, cellANDlabel_to_site, extent):\n    atoms_positions = dicts_to_array(atoms, \"r_coord\")\n    naive_edges = get_edges(\n        atoms_positions, _np.linalg.norm(basis_vectors, axis=1).max()\n    )\n    true_edges = []\n    for node1, node2 in naive_edges:\n        atom1 = atoms[node1]\n        atom2 = atoms[node2]\n        if atom1[\"inside\"] and atom2[\"inside\"]:\n            true_edges.append((node1, node2))\n        elif atom1[\"inside\"] or atom2[\"inside\"]:\n            cell1 = atom1[\"cell\"] % extent\n            cell2 = atom2[\"cell\"] % extent\n            node1 = cellANDlabel_to_site[tuple(cell1)][atom1[\"Label\"]]\n            node2 = cellANDlabel_to_site[tuple(cell2)][atom2[\"Label\"]]\n            edge = (node1, node2)\n            if edge not in true_edges and (node2, node1) not in true_edges:\n                true_edges.append(edge)\n    return true_edges\n\n\ndef dicts_to_array(dicts, key):\n    result = []\n    for d in dicts:\n        result.append(d[key])\n    return _np.asarray(result)\n\n\nclass Lattice(NetworkX):\n    \"\"\"A lattice built translating a unit cell and adding edges between nearest neighbours sites.\n\n    The unit cell is defined by the ``basis_vectors`` and it can contain an arbitrary number of atoms.\n    Each atom is located at an arbitrary position and is labelled by an integer number,\n    meant to distinguish between the different atoms within the unit cell.\n    Periodic boundary conditions can also be imposed along the desired directions.\n    There are three different ways to refer to the lattice sites. A site can be labelled\n    by a simple integer number (the site index) or by its coordinates (actual position in space).\n    \"\"\"\n\n    def __init__(self, basis_vectors, extent, *, pbc: bool = True, atoms_coord=[]):\n        \"\"\"\n        Constructs a new ``Lattice`` given its side length and the features of the unit cell.\n\n        Args:\n            basis_vectors: The basis vectors of the unit cell.\n            extent: The number of copies of the unit cell.\n            pbc: If ``True`` then the constructed lattice\n                will have periodic boundary conditions, otherwise\n                open boundary conditions are imposed (default=``True``).\n            atoms_coord: The coordinates of different atoms in the unit cell (default=one atom at the origin).\n\n        Examples:\n            Constructs a rectangular 3X4 lattice with periodic boundary conditions.\n\n            >>> import netket\n            >>> g=netket.graph.Lattice(basis_vectors=[[1,0],[0,1]],extent=[3,4])\n            >>> print(g.n_nodes)\n            12\n\n        \"\"\"\n\n        self._basis_vectors = _np.asarray(basis_vectors)\n        if self._basis_vectors.ndim != 2:\n            raise ValueError(\"Every vector must have the same dimension.\")\n        if self._basis_vectors.shape[0] != self._basis_vectors.shape[1]:\n            raise ValueError(\n                \"basis_vectors must be a basis for the N-dimensional vector space you chose\"\n            )\n\n        if not atoms_coord:\n            atoms_coord = [_np.zeros(self._basis_vectors.shape[0])]\n        atoms_coord = _np.asarray(atoms_coord)\n        atoms_coord_fractional = _np.asarray(\n            [\n                _np.matmul(_np.linalg.inv(self._basis_vectors.T), atom_coord)\n                for atom_coord in atoms_coord\n            ]\n        )\n        if atoms_coord_fractional.min() < 0 or atoms_coord_fractional.max() >= 1:\n            # Maybe there is another way to state this. I want to avoid that there exists the possibility that two atoms from different cells are at the same position:\n            raise ValueError(\n                \"atoms must reside inside their corresponding unit cell, which includes only the 0-faces in fractional coordinates.\"\n            )\n        uniques = _np.unique(atoms_coord, axis=0)\n        if len(atoms_coord) != uniques.shape[0]:\n            atoms_coord = _np.asarray(uniques)\n            warnings.warn(\n                f\"Some atom positions are not unique. Duplicates were dropped, and now atom positions are {atoms_coord}\",\n                UserWarning,\n            )\n\n        self._atoms_coord = atoms_coord\n\n        if isinstance(pbc, bool):\n            self._pbc = [pbc] * self._basis_vectors.shape[1]\n        elif (\n            not isinstance(pbc, list)\n            or len(pbc) != self._basis_vectors.shape[1]\n            or sum([1 for pbci in pbc if isinstance(pbci, bool)])\n            != self._basis_vectors.shape[1]\n        ):\n            raise ValueError(\n                \"pbc must be either a boolean or a list of booleans with the same dimension as the vector space you chose.\"\n            )\n        else:\n            self._pbc = pbc\n\n        extent = _np.asarray(extent)\n        self.extent = extent\n\n        atoms, cellANDlabel_to_site = create_points(\n            self._basis_vectors, extent, atoms_coord_fractional, pbc\n        )\n        edges = get_true_edges(self._basis_vectors, atoms, cellANDlabel_to_site, extent)\n        graph = _nx.MultiGraph(edges)\n\n        # Rename atoms\n        old_nodes = sorted(set([node for edge in edges for node in edge]))\n        self._atoms = [atoms[old_node] for old_node in old_nodes]\n        self._coord_to_site = {\n            tuple(atom[\"r_coord\"]): new_site\n            for new_site, atom in enumerate(self._atoms)\n        }\n        new_nodes = {old_node: new_node for new_node, old_node in enumerate(old_nodes)}\n        graph = _nx.relabel_nodes(graph, new_nodes)\n\n        # Order node names\n        nodes = sorted(graph.nodes())\n        edges = list(graph.edges())\n        graph = _nx.MultiGraph()\n        graph.add_nodes_from(nodes)\n        graph.add_edges_from(edges)\n\n        super().__init__(graph)\n\n    @property\n    def basis_vectors(self):\n        return self._basis_vectors\n\n    @property\n    def atoms_coord(self):\n        \"\"\"\n        Coordinates of atoms in the unit cell.\n        \"\"\"\n        return self._atoms_coord\n\n    def atom_label(self, site):\n        return self._atoms[site][\"Label\"]\n\n    def site_to_coord(self, site):\n        return self._atoms[site][\"r_coord\"]\n\n    def coord_to_site(self, coord):\n        return self._coord_to_site[tuple(coord)]\n\n    def site_to_vector(self, site):\n        return self._atoms[site][\"cell\"]\n\n    def vector_to_coord(self, vector):\n        return _np.matmul(self._basis_vectors, vector)\n\n    def __repr__(self):\n        return \"Lattice(n_nodes={})\\n  extent={}\\n  basis_vectors={}\".format(\n            self.n_nodes, self.extent.tolist(), self.basis_vectors.tolist()\n        )\n", "path": "netket/graph/lattice.py"}], "after_files": [{"content": "# Copyright 2020, 2021 The NetKet Authors - All rights reserved.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#    http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nfrom .graph import NetworkX\nfrom scipy.spatial import cKDTree\nfrom scipy.sparse import find, triu\nimport numpy as _np\nimport itertools\nimport networkx as _nx\nimport warnings\n\n\ndef get_edges(atoms_positions, cutoff):\n    kdtree = cKDTree(atoms_positions)\n    dist_matrix = kdtree.sparse_distance_matrix(kdtree, cutoff)\n    id1, id2, values = find(triu(dist_matrix))\n    pairs = []\n    min_dists = {}  # keys are nodes, values are min dists\n    for node in _np.unique(_np.concatenate((id1, id2))):\n        min_dist = _np.min(values[(id1 == node) | (id2 == node)])\n        min_dists[node] = min_dist\n    for node in _np.unique(id1):\n        min_dist = _np.min(values[id1 == node])\n        mask = (id1 == node) & (_np.isclose(values, min_dist))\n        first = id1[mask]\n        second = id2[mask]\n        for pair in zip(first, second):\n            if _np.isclose(min_dist, min_dists[pair[0]]) and _np.isclose(\n                min_dist, min_dists[pair[1]]\n            ):\n                pairs.append(pair)\n    return pairs\n\n\ndef create_points(basis_vectors, extent, atom_coords, pbc):\n    shell_vec = _np.zeros(extent.size, dtype=int)\n    shift_vec = _np.zeros(extent.size, dtype=int)\n    # note: by modifying these, the number of shells can be tuned.\n    shell_vec[pbc] = 2\n    shift_vec[pbc] = 1\n    ranges = tuple([list(range(ex)) for ex in extent + shell_vec])\n    atoms = []\n    cellANDlabel_to_site = {}\n    for s_cell in itertools.product(*ranges):\n        s_coord_cell = _np.asarray(s_cell) - shift_vec\n        if _np.any(s_coord_cell < 0) or _np.any(s_coord_cell > (extent - 1)):\n            inside = False\n        else:\n            inside = True\n        atom_count = len(atoms)\n        for i, atom_coord in enumerate(atom_coords):\n            s_coord_atom = s_coord_cell + atom_coord\n            r_coord_atom = _np.matmul(basis_vectors.T, s_coord_atom)\n            atoms.append(\n                {\n                    \"Label\": i,\n                    \"cell\": s_coord_cell,\n                    \"r_coord\": r_coord_atom,\n                    \"inside\": inside,\n                }\n            )\n            if tuple(s_coord_cell) not in cellANDlabel_to_site.keys():\n                cellANDlabel_to_site[tuple(s_coord_cell)] = {}\n            cellANDlabel_to_site[tuple(s_coord_cell)][i] = atom_count + i\n    return atoms, cellANDlabel_to_site\n\n\ndef get_true_edges(basis_vectors, atoms, cellANDlabel_to_site, extent):\n    atoms_positions = dicts_to_array(atoms, \"r_coord\")\n    naive_edges = get_edges(\n        atoms_positions, _np.linalg.norm(basis_vectors, axis=1).max()\n    )\n    true_edges = []\n    for node1, node2 in naive_edges:\n        atom1 = atoms[node1]\n        atom2 = atoms[node2]\n        if atom1[\"inside\"] and atom2[\"inside\"]:\n            true_edges.append((node1, node2))\n        elif atom1[\"inside\"] or atom2[\"inside\"]:\n            cell1 = atom1[\"cell\"] % extent\n            cell2 = atom2[\"cell\"] % extent\n            node1 = cellANDlabel_to_site[tuple(cell1)][atom1[\"Label\"]]\n            node2 = cellANDlabel_to_site[tuple(cell2)][atom2[\"Label\"]]\n            edge = (node1, node2)\n            if edge not in true_edges and (node2, node1) not in true_edges:\n                true_edges.append(edge)\n    return true_edges\n\n\ndef dicts_to_array(dicts, key):\n    result = []\n    for d in dicts:\n        result.append(d[key])\n    return _np.asarray(result)\n\n\nclass Lattice(NetworkX):\n    \"\"\"A lattice built translating a unit cell and adding edges between nearest neighbours sites.\n\n    The unit cell is defined by the ``basis_vectors`` and it can contain an arbitrary number of atoms.\n    Each atom is located at an arbitrary position and is labelled by an integer number,\n    meant to distinguish between the different atoms within the unit cell.\n    Periodic boundary conditions can also be imposed along the desired directions.\n    There are three different ways to refer to the lattice sites. A site can be labelled\n    by a simple integer number (the site index) or by its coordinates (actual position in space).\n    \"\"\"\n\n    def __init__(self, basis_vectors, extent, *, pbc: bool = True, atoms_coord=[]):\n        \"\"\"\n        Constructs a new ``Lattice`` given its side length and the features of the unit cell.\n\n        Args:\n            basis_vectors: The basis vectors of the unit cell.\n            extent: The number of copies of the unit cell.\n            pbc: If ``True`` then the constructed lattice\n                will have periodic boundary conditions, otherwise\n                open boundary conditions are imposed (default=``True``).\n            atoms_coord: The coordinates of different atoms in the unit cell (default=one atom at the origin).\n\n        Examples:\n            Constructs a rectangular 3X4 lattice with periodic boundary conditions.\n\n            >>> import netket\n            >>> g=netket.graph.Lattice(basis_vectors=[[1,0],[0,1]],extent=[3,4])\n            >>> print(g.n_nodes)\n            12\n\n        \"\"\"\n\n        self._basis_vectors = _np.asarray(basis_vectors)\n        if self._basis_vectors.ndim != 2:\n            raise ValueError(\"Every vector must have the same dimension.\")\n        if self._basis_vectors.shape[0] != self._basis_vectors.shape[1]:\n            raise ValueError(\n                \"basis_vectors must be a basis for the N-dimensional vector space you chose\"\n            )\n\n        if not atoms_coord:\n            atoms_coord = [_np.zeros(self._basis_vectors.shape[0])]\n        atoms_coord = _np.asarray(atoms_coord)\n        atoms_coord_fractional = _np.asarray(\n            [\n                _np.matmul(_np.linalg.inv(self._basis_vectors.T), atom_coord)\n                for atom_coord in atoms_coord\n            ]\n        )\n        if atoms_coord_fractional.min() < 0 or atoms_coord_fractional.max() >= 1:\n            # Maybe there is another way to state this. I want to avoid that there exists the possibility that two atoms from different cells are at the same position:\n            raise ValueError(\n                \"atoms must reside inside their corresponding unit cell, which includes only the 0-faces in fractional coordinates.\"\n            )\n        uniques = _np.unique(atoms_coord, axis=0)\n        if len(atoms_coord) != uniques.shape[0]:\n            atoms_coord = _np.asarray(uniques)\n            warnings.warn(\n                f\"Some atom positions are not unique. Duplicates were dropped, and now atom positions are {atoms_coord}\",\n                UserWarning,\n            )\n\n        self._atoms_coord = atoms_coord\n\n        if isinstance(pbc, bool):\n            self._pbc = [pbc] * self._basis_vectors.shape[1]\n        elif (\n            not isinstance(pbc, list)\n            or len(pbc) != self._basis_vectors.shape[1]\n            or sum([1 for pbci in pbc if isinstance(pbci, bool)])\n            != self._basis_vectors.shape[1]\n        ):\n            raise ValueError(\n                \"pbc must be either a boolean or a list of booleans with the same dimension as the vector space you chose.\"\n            )\n        else:\n            self._pbc = pbc\n\n        extent = _np.asarray(extent)\n        self.extent = extent\n\n        atoms, cellANDlabel_to_site = create_points(\n            self._basis_vectors, extent, atoms_coord_fractional, pbc\n        )\n        edges = get_true_edges(self._basis_vectors, atoms, cellANDlabel_to_site, extent)\n        graph = _nx.MultiGraph(edges)\n\n        # Rename atoms\n        old_nodes = sorted(set([node for edge in edges for node in edge]))\n        self._atoms = [atoms[old_node] for old_node in old_nodes]\n        self._coord_to_site = {\n            tuple(atom[\"r_coord\"]): new_site\n            for new_site, atom in enumerate(self._atoms)\n        }\n        new_nodes = {old_node: new_node for new_node, old_node in enumerate(old_nodes)}\n        graph = _nx.relabel_nodes(graph, new_nodes)\n\n        # Order node names\n        nodes = sorted(graph.nodes())\n        edges = list(graph.edges())\n        graph = _nx.MultiGraph()\n        graph.add_nodes_from(nodes)\n        graph.add_edges_from(edges)\n\n        super().__init__(graph)\n\n    @property\n    def basis_vectors(self):\n        return self._basis_vectors\n\n    @property\n    def atoms_coord(self):\n        \"\"\"\n        Coordinates of atoms in the unit cell.\n        \"\"\"\n        return self._atoms_coord\n\n    def atom_label(self, site):\n        return self._atoms[site][\"Label\"]\n\n    def site_to_coord(self, site):\n        return self._atoms[site][\"r_coord\"]\n\n    def coord_to_site(self, coord):\n        return self._coord_to_site[tuple(coord)]\n\n    def site_to_vector(self, site):\n        return self._atoms[site][\"cell\"]\n\n    def vector_to_coord(self, vector):\n        return _np.matmul(self._basis_vectors, vector)\n\n    def __repr__(self):\n        return \"Lattice(n_nodes={})\\n  extent={}\\n  basis_vectors={}\".format(\n            self.n_nodes, self.extent.tolist(), self.basis_vectors.tolist()\n        )\n", "path": "netket/graph/lattice.py"}]}