tensorcircuit-nightly 1.3.0.dev20250809__py3-none-any.whl → 1.3.0.dev20250811__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tensorcircuit-nightly might be problematic. Click here for more details.

Files changed (37) hide show
  1. tensorcircuit/__init__.py +1 -1
  2. {tensorcircuit_nightly-1.3.0.dev20250809.dist-info → tensorcircuit_nightly-1.3.0.dev20250811.dist-info}/METADATA +1 -1
  3. {tensorcircuit_nightly-1.3.0.dev20250809.dist-info → tensorcircuit_nightly-1.3.0.dev20250811.dist-info}/RECORD +6 -37
  4. {tensorcircuit_nightly-1.3.0.dev20250809.dist-info → tensorcircuit_nightly-1.3.0.dev20250811.dist-info}/top_level.txt +0 -1
  5. tests/__init__.py +0 -0
  6. tests/conftest.py +0 -67
  7. tests/test_backends.py +0 -1156
  8. tests/test_calibrating.py +0 -149
  9. tests/test_channels.py +0 -409
  10. tests/test_circuit.py +0 -1713
  11. tests/test_cloud.py +0 -219
  12. tests/test_compiler.py +0 -147
  13. tests/test_dmcircuit.py +0 -555
  14. tests/test_ensemble.py +0 -72
  15. tests/test_fgs.py +0 -318
  16. tests/test_gates.py +0 -156
  17. tests/test_hamiltonians.py +0 -159
  18. tests/test_interfaces.py +0 -557
  19. tests/test_keras.py +0 -160
  20. tests/test_lattice.py +0 -1750
  21. tests/test_miscs.py +0 -304
  22. tests/test_mpscircuit.py +0 -341
  23. tests/test_noisemodel.py +0 -156
  24. tests/test_qaoa.py +0 -86
  25. tests/test_qem.py +0 -152
  26. tests/test_quantum.py +0 -549
  27. tests/test_quantum_attr.py +0 -42
  28. tests/test_results.py +0 -379
  29. tests/test_shadows.py +0 -160
  30. tests/test_simplify.py +0 -46
  31. tests/test_stabilizer.py +0 -226
  32. tests/test_templates.py +0 -218
  33. tests/test_timeevol.py +0 -641
  34. tests/test_torchnn.py +0 -99
  35. tests/test_van.py +0 -102
  36. {tensorcircuit_nightly-1.3.0.dev20250809.dist-info → tensorcircuit_nightly-1.3.0.dev20250811.dist-info}/WHEEL +0 -0
  37. {tensorcircuit_nightly-1.3.0.dev20250809.dist-info → tensorcircuit_nightly-1.3.0.dev20250811.dist-info}/licenses/LICENSE +0 -0
tests/test_lattice.py DELETED
@@ -1,1750 +0,0 @@
1
- from unittest.mock import patch
2
- import logging
3
-
4
- # import time
5
-
6
- import matplotlib
7
-
8
- matplotlib.use("Agg")
9
-
10
-
11
- import pytest
12
- import numpy as np
13
-
14
- from tensorcircuit.templates.lattice import (
15
- ChainLattice,
16
- CheckerboardLattice,
17
- CubicLattice,
18
- CustomizeLattice,
19
- DimerizedChainLattice,
20
- HoneycombLattice,
21
- KagomeLattice,
22
- LiebLattice,
23
- RectangularLattice,
24
- SquareLattice,
25
- TriangularLattice,
26
- AbstractLattice,
27
- get_compatible_layers,
28
- )
29
-
30
-
31
- @pytest.fixture
32
- def simple_square_lattice() -> CustomizeLattice:
33
- """
34
- Provides a simple 2x2 square CustomizeLattice instance for neighbor tests.
35
- The sites are indexed as follows:
36
- 2--3
37
- | |
38
- 0--1
39
- """
40
- coords = [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]]
41
- ids = list(range(len(coords)))
42
- lattice = CustomizeLattice(dimensionality=2, identifiers=ids, coordinates=coords)
43
- # Pre-calculate neighbors up to the 2nd shell for use in tests.
44
- lattice._build_neighbors(max_k=2)
45
- return lattice
46
-
47
-
48
- @pytest.fixture
49
- def kagome_lattice_fragment() -> CustomizeLattice:
50
- """
51
- Pytest fixture to provide a standard CustomizeLattice instance.
52
- This represents the Kagome fragment from the project requirements,
53
- making it a reusable object for multiple tests.
54
- """
55
- kag_coords = [
56
- [0.0, 0.0],
57
- [1.0, 0.0],
58
- [0.5, np.sqrt(3) / 2], # Triangle 1
59
- [2, 0],
60
- [1.5, np.sqrt(3) / 2], # Triangle 2 (shifted basis)
61
- [1.0, np.sqrt(3)], # Top site
62
- ]
63
- kag_ids = list(range(len(kag_coords)))
64
- return CustomizeLattice(
65
- dimensionality=2, identifiers=kag_ids, coordinates=kag_coords
66
- )
67
-
68
-
69
- class TestCustomizeLattice:
70
- """
71
- A test class to group all tests related to the CustomizeLattice.
72
- This helps in organizing the test suite.
73
- """
74
-
75
- def test_initialization_and_properties(self, kagome_lattice_fragment):
76
- """
77
- Test case for successful initialization and verification of basic properties.
78
- This test function receives the 'kagome_lattice_fragment' fixture as an argument.
79
- """
80
- # Arrange: The fixture has already prepared the 'lattice' object for us.
81
- lattice = kagome_lattice_fragment
82
-
83
- # Assert: Check if the object's properties match our expectations.
84
- assert lattice.dimensionality == 2
85
- assert lattice.num_sites == 6
86
- assert len(lattice) == 6 # This also tests the __len__ dunder method
87
-
88
- # Verify that coordinates are correctly stored as numpy arrays.
89
- # It's important to use np.testing.assert_array_equal for numpy array comparison.
90
- expected_coord = np.array([0.5, np.sqrt(3) / 2])
91
- np.testing.assert_array_equal(lattice.get_coordinates(2), expected_coord)
92
-
93
- # Verify that the mapping between identifiers and indices is correct.
94
- assert lattice.get_identifier(4) == 4
95
- assert lattice.get_index(4) == 4
96
-
97
- def test_input_validation_mismatched_lengths(self):
98
- """
99
- Tests that a ValueError is raised if identifiers and coordinates
100
- lists have mismatched lengths.
101
- """
102
- # Arrange: Prepare invalid inputs.
103
- coords = [[0.0, 0.0], [1.0, 0.0]] # 2 coordinates
104
- ids = [0, 1, 2] # 3 identifiers
105
-
106
- # Act & Assert: Use pytest.raises as a context manager to ensure
107
- # the specified exception is raised within the 'with' block.
108
- with pytest.raises(
109
- ValueError,
110
- match="Identifiers and coordinates lists must have the same length.",
111
- ):
112
- CustomizeLattice(dimensionality=2, identifiers=ids, coordinates=coords)
113
-
114
- def test_input_validation_wrong_dimension(self):
115
- """
116
- Tests that a ValueError is raised if a coordinate's dimension
117
- does not match the lattice's specified dimensionality.
118
- """
119
- # Arrange: Prepare coordinates with mixed dimensions for a 2D lattice.
120
- coords_wrong_dim = [[0.0, 0.0], [1.0, 0.0, 0.0]] # A mix of 2D and 3D
121
- ids_ok = [0, 1]
122
-
123
- # Act & Assert: Check for the specific error message. The 'r' before the string
124
- # indicates a raw string, which is good practice for regex patterns.
125
- with pytest.raises(
126
- ValueError, match=r"Coordinate at index 1 has shape \(3,\), expected \(2,\)"
127
- ):
128
- CustomizeLattice(
129
- dimensionality=2, identifiers=ids_ok, coordinates=coords_wrong_dim
130
- )
131
-
132
- def test_neighbor_finding(self, simple_square_lattice):
133
- """
134
- Tests the k-th nearest neighbor finding functionality (_build_neighbors
135
- and get_neighbors).
136
- """
137
- # Arrange: The fixture provides the lattice with pre-built neighbors.
138
- lattice = simple_square_lattice
139
-
140
- # --- Assertions for k=1 (Nearest Neighbors) ---
141
- # We use set() for comparison to ignore the order of neighbors.
142
- assert set(lattice.get_neighbors(0, k=1)) == {1, 2}
143
- assert set(lattice.get_neighbors(1, k=1)) == {0, 3}
144
- assert set(lattice.get_neighbors(2, k=1)) == {0, 3}
145
- assert set(lattice.get_neighbors(3, k=1)) == {1, 2}
146
-
147
- # --- Assertions for k=2 (Next-Nearest Neighbors) ---
148
- # These should be the diagonal sites.
149
- assert set(lattice.get_neighbors(0, k=2)) == {3}
150
- assert set(lattice.get_neighbors(1, k=2)) == {2}
151
-
152
- def test_neighbor_pairs(self, simple_square_lattice):
153
- """
154
- Tests the retrieval of unique neighbor pairs (bonds) using
155
- get_neighbor_pairs.
156
- """
157
- # Arrange: Use the same fixture.
158
- lattice = simple_square_lattice
159
-
160
- # --- Test for k=1 (Nearest Neighbor bonds) ---
161
- # Act: Get unique nearest neighbor pairs.
162
- nn_pairs = lattice.get_neighbor_pairs(k=1, unique=True)
163
-
164
- # Assert: The set of pairs should match the expected bonds.
165
- # We convert the list of pairs to a set of tuples for order-independent comparison.
166
- expected_nn_pairs = {(0, 1), (0, 2), (1, 3), (2, 3)}
167
- assert set(map(tuple, nn_pairs)) == expected_nn_pairs
168
-
169
- # --- Test for k=2 (Next-Nearest Neighbor bonds) ---
170
- # Act: Get unique next-nearest neighbor pairs.
171
- nnn_pairs = lattice.get_neighbor_pairs(k=2, unique=True)
172
-
173
- # Assert:
174
- expected_nnn_pairs = {(0, 3), (1, 2)}
175
- assert set(map(tuple, nnn_pairs)) == expected_nnn_pairs
176
-
177
- def test_neighbor_pairs_non_unique(self, simple_square_lattice):
178
- """
179
- Tests get_neighbor_pairs with unique=False to ensure all
180
- directed pairs (bonds) are returned.
181
- """
182
- # Arrange: Use the same 2x2 square lattice fixture.
183
- # 2--3
184
- # | |
185
- # 0--1
186
- lattice = simple_square_lattice
187
-
188
- # Act: Get NON-unique nearest neighbor pairs.
189
- nn_pairs = lattice.get_neighbor_pairs(k=1, unique=False)
190
-
191
- # Assert:
192
- # There are 4 bonds, so we expect 4 * 2 = 8 directed pairs.
193
- assert len(nn_pairs) == 8
194
-
195
- # Your source code sorts the output, so we can compare against a
196
- # sorted list for a precise match.
197
- expected_pairs = sorted(
198
- [(0, 1), (1, 0), (0, 2), (2, 0), (1, 3), (3, 1), (2, 3), (3, 2)]
199
- )
200
-
201
- assert nn_pairs == expected_pairs
202
-
203
- @patch("matplotlib.pyplot.show")
204
- def test_show_method_runs_and_calls_plt_show(
205
- self, mock_show, simple_square_lattice
206
- ):
207
- """
208
- Smoke test for the .show() method.
209
- It verifies that the method runs without raising an exception and that it
210
- triggers a call to matplotlib's show() function.
211
- We use @patch to "mock" the show function, preventing a plot window
212
- from actually appearing during tests.
213
- """
214
- # Arrange: Get the lattice instance from the fixture
215
- lattice = simple_square_lattice
216
-
217
- # Act: Call the .show() method.
218
- # We wrap it in a try...except block to give a more specific error
219
- # if the method fails for any reason.
220
- try:
221
- lattice.show()
222
- except Exception as e:
223
- pytest.fail(f".show() method raised an unexpected exception: {e}")
224
-
225
- # Assert: Check that our mocked matplotlib.pyplot.show was called exactly once.
226
- mock_show.assert_called_once()
227
-
228
- def test_sites_iterator(self, simple_square_lattice):
229
- """
230
- Tests the sites() iterator to ensure it yields all sites correctly.
231
- """
232
- # Arrange
233
- lattice = simple_square_lattice
234
- expected_num_sites = 4
235
-
236
- # Act
237
- # The sites() method returns an iterator, we convert it to a list to check its length.
238
- all_sites = list(lattice.sites())
239
-
240
- # Assert
241
- assert len(all_sites) == expected_num_sites
242
-
243
- # For a more thorough check, verify the content of one of the yielded tuples.
244
- # For the simple_square_lattice fixture, site 3 has identifier 3 and coords [1, 1].
245
- idx, ident, coords = all_sites[3]
246
- assert idx == 3
247
- assert ident == 3
248
- np.testing.assert_array_equal(coords, np.array([1, 1]))
249
-
250
- def test_get_site_info_with_identifier(self, simple_square_lattice):
251
- """
252
- Tests the get_site_info() method using a site identifier instead of an index.
253
- This covers the 'else' branch of the type check in the method.
254
- """
255
- # Arrange
256
- lattice = simple_square_lattice
257
- # In this fixture, the identifier for the site at index 2 is also the integer 2.
258
- identifier_to_test = 2
259
- expected_index = 2
260
- expected_coords = np.array([0, 1])
261
-
262
- # Act
263
- idx, ident, coords = lattice.get_site_info(identifier_to_test)
264
-
265
- # Assert
266
- assert idx == expected_index
267
- assert ident == identifier_to_test
268
- np.testing.assert_array_equal(coords, expected_coords)
269
-
270
- @patch("matplotlib.pyplot.show")
271
- def test_show_method_with_labels(self, mock_show, simple_square_lattice):
272
- """
273
- Tests that the .show() method runs without error when label-related
274
- options are enabled. This covers the logic inside the
275
- 'if show_indices or show_identifiers:' block.
276
- """
277
- # Arrange
278
- lattice = simple_square_lattice
279
-
280
- # Act & Assert
281
- try:
282
- # Call .show() with options to display indices and identifiers.
283
- lattice.show(show_indices=True, show_identifiers=True)
284
- except Exception as e:
285
- pytest.fail(
286
- f".show() with label options raised an unexpected exception: {e}"
287
- )
288
-
289
- # Ensure the plotting function is still called.
290
- mock_show.assert_called_once()
291
-
292
- def test_get_neighbors_logs_info_for_uncached_k(
293
- self, simple_square_lattice, caplog
294
- ):
295
- """
296
- Tests that an INFO message is logged when get_neighbors is called for a 'k'
297
- that has not been pre-calculated, triggering on-demand computation.
298
- """
299
- # Arrange
300
- lattice = simple_square_lattice # This fixture builds neighbors up to k=2
301
- k_to_test = 99 # A value that is clearly not cached
302
- caplog.set_level(logging.INFO) # Ensure INFO logs are captured
303
-
304
- # Act
305
- # This will now trigger the on-demand computation
306
- _ = lattice.get_neighbors(0, k=k_to_test)
307
-
308
- # Assert
309
- # Check that the correct INFO message about on-demand building was logged.
310
- expected_log = (
311
- f"Neighbors for k={k_to_test} not pre-computed. "
312
- f"Building now up to max_k={k_to_test}."
313
- )
314
- assert expected_log in caplog.text
315
-
316
- @patch("matplotlib.pyplot.show")
317
- def test_show_prints_warning_for_uncached_bonds(
318
- self, mock_show, simple_square_lattice, caplog
319
- ):
320
- """
321
- Tests that a warning is printed when .show() is asked to draw a bond layer 'k'
322
- that has not been pre-calculated.
323
- """
324
- # Arrange
325
- lattice = simple_square_lattice # This fixture builds neighbors up to k=2
326
- k_to_test = 99 # A value that is clearly not cached
327
-
328
- # Act
329
- lattice.show(show_bonds_k=k_to_test)
330
-
331
- # Assert
332
- assert (
333
- f"Cannot draw bonds. k={k_to_test} neighbors have not been calculated"
334
- in caplog.text
335
- )
336
-
337
- @patch("matplotlib.pyplot.show")
338
- def test_show_method_for_3d_lattice(self, mock_show):
339
- """
340
- Tests that the .show() method can handle a 3D lattice without
341
- crashing. This covers the 'if self.dimensionality == 3:' branches.
342
- """
343
- # Arrange: Create a simple 2-site lattice in 3D space.
344
- coords_3d = [[0.0, 0.0, 0.0], [1.0, 1.0, 1.0]]
345
- ids_3d = [0, 1]
346
- lattice_3d = CustomizeLattice(
347
- dimensionality=3, identifiers=ids_3d, coordinates=coords_3d
348
- )
349
-
350
- # Assert basic property
351
- assert lattice_3d.dimensionality == 3
352
-
353
- # Act & Assert
354
- # We just need to ensure that calling .show() on a 3D object
355
- # executes the 3D plotting logic without raising an exception.
356
- try:
357
- lattice_3d.show(show_indices=True, show_bonds_k=None)
358
- except Exception as e:
359
- pytest.fail(f".show() for 3D lattice raised an unexpected exception: {e}")
360
-
361
- # Verify that the plotting pipeline was completed.
362
- mock_show.assert_called_once()
363
-
364
- @patch("matplotlib.pyplot.subplots")
365
- def test_show_method_actually_draws_2d_labels(
366
- self, mock_subplots, simple_square_lattice
367
- ):
368
- """
369
- Tests if ax.text is actually called for a 2D lattice when labels are enabled.
370
- """
371
- # Arrange:
372
- # 1. Prepare mock Figure and Axes objects that `matplotlib.pyplot.subplots` will return.
373
- # This allows us to inspect calls to the `ax` object.
374
- mock_fig = matplotlib.figure.Figure()
375
- mock_ax = matplotlib.axes.Axes(mock_fig, [0.0, 0.0, 1.0, 1.0])
376
- mock_subplots.return_value = (mock_fig, mock_ax)
377
-
378
- # 2. Mock the text method on our mock Axes object to monitor its calls.
379
- with patch.object(mock_ax, "text") as mock_text_method:
380
- lattice = simple_square_lattice
381
-
382
- # Act:
383
- # Call the show method. It will now operate on our mock_ax object.
384
- lattice.show(show_indices=True)
385
-
386
- # Assert:
387
- # Check if the ax.text method was called. For a 4-site lattice, it should be called 4 times.
388
- assert mock_text_method.call_count == lattice.num_sites
389
-
390
- def test_custom_irregular_geometry_neighbors(self):
391
- """
392
- Tests neighbor finding on a more complex, non-grid-like custom geometry
393
- to stress-test the distance shell and KDTree logic.
394
- """
395
- # Arrange: A "star-shaped" lattice with a central point,
396
- # an inner shell, and an outer shell.
397
- coords = [
398
- [0.0, 0.0], # Site 0: Center
399
- [1.0, 0.0],
400
- [0.0, 1.0],
401
- [-1.0, 0.0],
402
- [0.0, -1.0], # Sites 1-4: Inner shell (dist=1)
403
- [2.0, 0.0],
404
- [0.0, 2.0],
405
- [-2.0, 0.0],
406
- [0.0, -2.0], # Sites 5-8: Outer shell (dist=2)
407
- ]
408
- ids = list(range(len(coords)))
409
- lattice = CustomizeLattice(
410
- dimensionality=2, identifiers=ids, coordinates=coords
411
- )
412
- lattice._build_neighbors(max_k=3)
413
-
414
- # Assert 1: Neighbors of the central point (0) should be the distinct shells.
415
- assert set(lattice.get_neighbors(0, k=1)) == {1, 2, 3, 4}
416
- # The shell at dist=2.0 (d_sq=4.0) is the 3rd global shell, so we check k=3.
417
- assert set(lattice.get_neighbors(0, k=3)) == {5, 6, 7, 8}
418
-
419
- assert lattice.get_neighbors(0, k=2) == []
420
-
421
- # Assert 2: Neighbors of a point on the inner shell, e.g., site 1 ([1.0, 0.0]).
422
- # Its nearest neighbors (k=1) are the center (0) and the closest point on the outer shell (5).
423
- # Both are at distance 1.0.
424
- assert set(lattice.get_neighbors(1, k=1)) == {0, 5}
425
-
426
- # Its next-nearest neighbors (k=2) are the other two points on the inner shell (2 and 4),
427
- # both at distance sqrt(2).
428
- assert set(lattice.get_neighbors(1, k=2)) == {2, 4}
429
-
430
- def test_customizelattice_max_k_precomputation_and_ondemand(self):
431
- """
432
- A robust test to verify `precompute_neighbors` (max_k) for CustomizeLattice.
433
- This test is designed to FAIL on the buggy code.
434
- """
435
- coords = [
436
- [0.0, 0.0],
437
- [1.0, 0.0],
438
- [0.0, 1.0],
439
- [-1.0, 0.0],
440
- [0.0, -1.0],
441
- [1.0, 1.0],
442
- [-1.0, 1.0],
443
- [-1.0, -1.0],
444
- [1.0, -1.0],
445
- [2.0, 0.0],
446
- [0.0, 2.0],
447
- [-2.0, 0.0],
448
- [0.0, -2.0],
449
- ]
450
- ids = list(range(len(coords)))
451
- k_precompute = 2
452
-
453
- lattice = CustomizeLattice(
454
- dimensionality=2,
455
- identifiers=ids,
456
- coordinates=coords,
457
- precompute_neighbors=k_precompute,
458
- )
459
-
460
- computed_shells = sorted(list(lattice._neighbor_maps.keys()))
461
- expected_shells = list(range(1, k_precompute + 1))
462
-
463
- assert computed_shells == expected_shells, (
464
- f"TEST FAILED for CustomizeLattice with k={k_precompute}. "
465
- f"Expected shells {expected_shells}, but found {computed_shells}."
466
- )
467
-
468
- k_ondemand = 3
469
- _ = lattice.get_neighbors(0, k=k_ondemand)
470
-
471
- computed_shells_after = sorted(list(lattice._neighbor_maps.keys()))
472
- expected_shells_after = list(range(1, k_ondemand + 1))
473
-
474
- assert computed_shells_after == expected_shells_after, (
475
- f"ON-DEMAND TEST FAILED for CustomizeLattice. "
476
- f"Expected shells {expected_shells_after} after demanding k={k_ondemand}, "
477
- f"but found {computed_shells_after}."
478
- )
479
-
480
-
481
- @pytest.fixture
482
- def obc_square_lattice() -> SquareLattice:
483
- """Provides a 3x3 SquareLattice with Open Boundary Conditions."""
484
- return SquareLattice(size=(3, 3), pbc=False)
485
-
486
-
487
- @pytest.fixture
488
- def pbc_square_lattice() -> SquareLattice:
489
- """Provides a 3x3 SquareLattice with Periodic Boundary Conditions."""
490
- return SquareLattice(size=(3, 3), pbc=True)
491
-
492
-
493
- class TestSquareLattice:
494
- """
495
- Groups all tests for the SquareLattice class, which implicitly tests
496
- the core functionality of its parent, TILattice.
497
- """
498
-
499
- def test_initialization_and_properties(self, obc_square_lattice):
500
- """
501
- Tests the basic properties of a SquareLattice instance.
502
- """
503
- lattice = obc_square_lattice
504
- assert lattice.dimensionality == 2
505
- assert lattice.num_sites == 9 # A 3x3 lattice should have 9 sites.
506
- assert len(lattice) == 9
507
-
508
- def test_site_info_and_identifiers(self, obc_square_lattice):
509
- """
510
- Tests that site information (coordinates, identifiers) is correct.
511
- """
512
- lattice = obc_square_lattice
513
- center_idx = lattice.get_index((1, 1, 0))
514
- assert center_idx == 4
515
-
516
- _, ident, coords = lattice.get_site_info(center_idx)
517
- assert ident == (1, 1, 0)
518
- np.testing.assert_array_equal(coords, np.array([1.0, 1.0]))
519
-
520
- corner_idx = 0
521
- _, ident, coords = lattice.get_site_info(corner_idx)
522
- assert ident == (0, 0, 0)
523
- np.testing.assert_array_equal(coords, np.array([0.0, 0.0]))
524
-
525
- def test_neighbors_with_open_boundaries(self, obc_square_lattice):
526
- """
527
- Tests neighbor finding with Open Boundary Conditions (OBC) using specific
528
- neighbor identities.
529
- """
530
- lattice = obc_square_lattice
531
- # Site indices for a 3x3 grid (row-major order):
532
- # 0 1 2
533
- # 3 4 5
534
- # 6 7 8
535
- center_idx = 4 # (1, 1, 0)
536
- corner_idx = 0 # (0, 0, 0)
537
- edge_idx = 3 # (1, 0, 0)
538
-
539
- # Assert center site (4) has neighbors 1, 3, 5, 7
540
- assert set(lattice.get_neighbors(center_idx, k=1)) == {1, 3, 5, 7}
541
- # Assert corner site (0) has neighbors 1, 3
542
- assert set(lattice.get_neighbors(corner_idx, k=1)) == {1, 3}
543
- # Assert edge site (3) has neighbors 0, 4, 6
544
- assert set(lattice.get_neighbors(edge_idx, k=1)) == {0, 4, 6}
545
-
546
- def test_neighbors_with_periodic_boundaries(self, pbc_square_lattice):
547
- """
548
- Tests neighbor finding with Periodic Boundary Conditions (PBC).
549
- """
550
- lattice = pbc_square_lattice
551
- corner_idx = lattice.get_index((0, 0, 0))
552
-
553
- neighbors = lattice.get_neighbors(corner_idx, k=1)
554
- neighbor_idents = {lattice.get_identifier(i) for i in neighbors}
555
- expected_neighbor_idents = {(1, 0, 0), (0, 1, 0), (2, 0, 0), (0, 2, 0)}
556
- assert neighbor_idents == expected_neighbor_idents
557
-
558
- nnn_neighbors = lattice.get_neighbors(corner_idx, k=2)
559
- nnn_neighbor_idents = {lattice.get_identifier(i) for i in nnn_neighbors}
560
- expected_nnn_idents = {(1, 1, 0), (2, 1, 0), (1, 2, 0), (2, 2, 0)}
561
- assert nnn_neighbor_idents == expected_nnn_idents
562
-
563
-
564
- # --- Tests for HoneycombLattice ---
565
-
566
-
567
- @pytest.fixture
568
- def pbc_honeycomb_lattice() -> HoneycombLattice:
569
- """Provides a 2x2 HoneycombLattice with Periodic Boundary Conditions."""
570
- return HoneycombLattice(size=(2, 2), pbc=True)
571
-
572
-
573
- class TestHoneycombLattice:
574
- """
575
- Tests the HoneycombLattice class, focusing on its two-site basis.
576
- """
577
-
578
- def test_initialization_and_properties(self, pbc_honeycomb_lattice):
579
- """
580
- Tests that the total number of sites is correct for a composite lattice.
581
- """
582
- lattice = pbc_honeycomb_lattice
583
- assert lattice.num_sites == 8
584
- assert lattice.num_basis == 2
585
-
586
- def test_honeycomb_neighbors(self, pbc_honeycomb_lattice):
587
- """
588
- Tests that every site in a honeycomb lattice has 3 nearest neighbors.
589
- """
590
- lattice = pbc_honeycomb_lattice
591
- site_a_idx = lattice.get_index((0, 0, 0))
592
- assert len(lattice.get_neighbors(site_a_idx, k=1)) == 3
593
-
594
- site_b_idx = lattice.get_index((0, 0, 1))
595
- assert len(lattice.get_neighbors(site_b_idx, k=1)) == 3
596
-
597
-
598
- # --- Tests for TriangularLattice ---
599
-
600
-
601
- @pytest.fixture
602
- def pbc_triangular_lattice() -> TriangularLattice:
603
- """
604
- Provides a 3x3 TriangularLattice with Periodic Boundary Conditions.
605
- A 3x3 size is used to ensure all 6 nearest neighbors are unique sites.
606
- """
607
- return TriangularLattice(size=(3, 3), pbc=True)
608
-
609
-
610
- class TestTriangularLattice:
611
- """
612
- Tests the TriangularLattice class, focusing on its coordination number.
613
- """
614
-
615
- def test_initialization_and_properties(self, pbc_triangular_lattice):
616
- """
617
- Tests the basic properties of the triangular lattice.
618
- """
619
- lattice = pbc_triangular_lattice
620
- assert lattice.num_sites == 9 # 3 * 3 = 9 sites for a 3x3 grid
621
-
622
- def test_triangular_neighbors(self, pbc_triangular_lattice):
623
- """
624
- Tests that every site in a triangular lattice has 6 nearest neighbors.
625
- """
626
- lattice = pbc_triangular_lattice
627
- site_idx = 0
628
- assert len(lattice.get_neighbors(site_idx, k=1)) == 6
629
-
630
-
631
- # --- Tests for New TILattice Implementations ---
632
-
633
-
634
- class TestRectangularLattice:
635
- """Tests for the 2D RectangularLattice."""
636
-
637
- def test_rectangular_properties_and_neighbors(self):
638
- """Tests neighbor counts for an OBC rectangular lattice."""
639
- lattice = RectangularLattice(size=(3, 4), pbc=False)
640
- assert lattice.num_sites == 12
641
- assert lattice.dimensionality == 2
642
-
643
- # Test neighbor counts for different site types
644
- center_idx = lattice.get_index((1, 1, 0))
645
- corner_idx = lattice.get_index((0, 0, 0))
646
- edge_idx = lattice.get_index((0, 1, 0))
647
-
648
- assert len(lattice.get_neighbors(center_idx, k=1)) == 4
649
- assert len(lattice.get_neighbors(corner_idx, k=1)) == 2
650
- assert len(lattice.get_neighbors(edge_idx, k=1)) == 3
651
-
652
-
653
- class TestTILatticeEdgeCases:
654
- """
655
- A dedicated class for testing the behavior of TILattice and its
656
- subclasses under less common, "edge-case" conditions.
657
- """
658
-
659
- @pytest.fixture
660
- def obc_1d_chain(self) -> ChainLattice:
661
- """
662
- Provides a 5-site 1D chain with Open Boundary Conditions.
663
- """
664
- # 0--1--2--3--4
665
- return ChainLattice(size=(5,), pbc=False)
666
-
667
- def test_1d_chain_properties_and_neighbors(self, obc_1d_chain):
668
- # Arrange
669
- lattice = obc_1d_chain
670
-
671
- # Assert basic properties
672
- assert lattice.num_sites == 5
673
- assert lattice.dimensionality == 1
674
-
675
- # Assert neighbor counts for different positions
676
- # Endpoint (site 0) should have 1 neighbor (site 1)
677
- endpoint_idx = lattice.get_index((0, 0))
678
- assert lattice.get_neighbors(endpoint_idx, k=1) == [1]
679
-
680
- # Middle point (site 2) should have 2 neighbors (sites 1 and 3)
681
- middle_idx = lattice.get_index((2, 0))
682
- assert len(lattice.get_neighbors(middle_idx, k=1)) == 2
683
- assert set(lattice.get_neighbors(middle_idx, k=1)) == {1, 3}
684
-
685
- @pytest.fixture
686
- def nonsquare_lattice(self) -> SquareLattice:
687
- """Provides a non-square 2x3 lattice to test indexing."""
688
- return SquareLattice(size=(2, 3), pbc=False)
689
-
690
- def test_nonsquare_lattice_indexing(self, nonsquare_lattice):
691
- """
692
- Tests site indexing and coordinate generation on a non-square (2x3) lattice.
693
- This ensures the logic correctly handles different dimension lengths.
694
- The lattice sites are indexed row by row:
695
- (0,0) (0,1) (0,2) -> indices 0, 1, 2
696
- (1,0) (1,1) (1,2) -> indices 3, 4, 5
697
- """
698
- # Arrange
699
- lattice = nonsquare_lattice
700
-
701
- # Assert properties
702
- assert lattice.num_sites == 6 # 2 * 3 = 6
703
-
704
- # Act & Assert: Check a non-trivial site, e.g., the last one.
705
- # The identifier for the site in the last row and last column.
706
- ident = (1, 2, 0)
707
- expected_idx = 5
708
- expected_coords = np.array([1.0, 2.0])
709
-
710
- # Get index from identifier
711
- idx = lattice.get_index(ident)
712
- assert idx == expected_idx
713
-
714
- # Get info from index
715
- _, _, coords = lattice.get_site_info(idx)
716
- np.testing.assert_array_equal(coords, expected_coords)
717
-
718
- @patch("matplotlib.pyplot.show")
719
- def test_show_method_for_1d_lattice(self, mock_show, obc_1d_chain):
720
- """
721
- Tests that the .show() method can handle a 1D lattice (chain)
722
- without crashing. This covers the 'if self.dimensionality == 1:' branches.
723
- """
724
- # Arrange
725
- lattice_1d = obc_1d_chain
726
-
727
- # Assert basic property
728
- assert lattice_1d.num_sites == 5
729
-
730
- # Act & Assert
731
- try:
732
- # Call .show() on the 1D lattice to execute the 1D plotting logic.
733
- lattice_1d.show(show_indices=True)
734
- except Exception as e:
735
- pytest.fail(f".show() for 1D lattice raised an unexpected exception: {e}")
736
-
737
- # Verify that the plotting pipeline was completed.
738
- mock_show.assert_called_once()
739
-
740
-
741
- # --- Tests for API Robustness / Negative Cases ---
742
-
743
-
744
- class TestApiRobustness:
745
- """
746
- Groups tests that verify the API's behavior with invalid inputs.
747
- This ensures the lattice classes fail gracefully and predictably.
748
- """
749
-
750
- def test_access_with_out_of_bounds_index(self, simple_square_lattice):
751
- """
752
- Tests that an IndexError is raised when accessing a site index
753
- that is out of the valid range (0 to num_sites-1).
754
- """
755
- # Arrange
756
- lattice = simple_square_lattice # This lattice has 4 sites (indices 0, 1, 2, 3)
757
- invalid_index = 999
758
-
759
- # Act & Assert
760
- # We use pytest.raises to confirm that the expected exception is thrown.
761
- with pytest.raises(IndexError):
762
- lattice.get_coordinates(invalid_index)
763
-
764
- with pytest.raises(IndexError):
765
- lattice.get_identifier(invalid_index)
766
-
767
- with pytest.raises(IndexError):
768
- # get_site_info should also raise IndexError for an invalid index
769
- lattice.get_site_info(invalid_index)
770
-
771
- def test_empty_lattice_handles_gracefully(self, caplog):
772
- """
773
- Tests that an empty lattice initializes correctly and that methods
774
- like .show() and ._build_neighbors() handle the zero-site case
775
- gracefully without crashing.
776
- """
777
- # Arrange: Create an empty CustomizeLattice instance.
778
- empty_lattice = CustomizeLattice(
779
- dimensionality=2, identifiers=[], coordinates=[]
780
- )
781
-
782
- # Assert: Verify basic properties.
783
- assert empty_lattice.num_sites == 0
784
- assert len(empty_lattice) == 0
785
-
786
- # Act & Assert for .show(): Verify it prints the expected message without crashing.
787
- caplog.set_level(logging.INFO)
788
-
789
- empty_lattice.show()
790
- assert "Lattice is empty, nothing to show." in caplog.text
791
-
792
- # Act & Assert for neighbor finding: Verify these calls run without errors.
793
- empty_lattice._build_neighbors()
794
- assert empty_lattice.get_neighbor_pairs(k=1) == []
795
-
796
- def test_single_site_lattice_handles_gracefully(self):
797
- """
798
- Tests that a lattice with a single site correctly handles neighbor
799
- finding (i.e., returns no neighbors).
800
- """
801
- # Arrange: Create a CustomizeLattice with a single site.
802
- single_site_lattice = CustomizeLattice(
803
- dimensionality=2, identifiers=[0], coordinates=[[0.0, 0.0]]
804
- )
805
-
806
- # Assert: Verify basic properties.
807
- assert single_site_lattice.num_sites == 1
808
-
809
- # Act: Attempt to build neighbor relationships.
810
- single_site_lattice._build_neighbors(max_k=1)
811
-
812
- # Assert: The single site should have no neighbors.
813
- assert single_site_lattice.get_neighbors(0, k=1) == []
814
-
815
- def test_access_with_non_existent_identifier(self, simple_square_lattice):
816
- """
817
- Tests that a ValueError is raised when accessing a site
818
- with an identifier that does not exist in the lattice.
819
- """
820
- # Arrange
821
- lattice = simple_square_lattice
822
- invalid_identifier = "non_existent_site"
823
-
824
- # Act & Assert
825
- # Your code raises a ValueError with a specific message. We can even
826
- # use the 'match' parameter to check if the error message is correct.
827
- with pytest.raises(ValueError, match="not found in the lattice"):
828
- lattice.get_index(invalid_identifier)
829
-
830
- with pytest.raises(ValueError, match="not found in the lattice"):
831
- lattice.get_site_info(invalid_identifier)
832
-
833
- def test_show_warning_for_unsupported_dimension(self, caplog):
834
- """
835
- Tests that .show() prints a warning when called on a lattice with a
836
- dimensionality that it does not support for plotting (e.g., 4D).
837
- """
838
- # Arrange: Create a simple lattice with an unsupported dimension.
839
- lattice_4d = CustomizeLattice(
840
- dimensionality=4, identifiers=[0], coordinates=[[0, 0, 0, 0]]
841
- )
842
-
843
- # Act
844
- lattice_4d.show()
845
-
846
- # Assert: Check that the appropriate warning was printed to stdout.
847
- assert "show() is not implemented for 4D lattices." in caplog.text
848
-
849
- def test_disconnected_lattice_neighbor_finding(self):
850
- """
851
- Tests that neighbor finding algorithms work correctly for a lattice
852
- composed of multiple, physically disconnected components.
853
- """
854
- # Arrange: Create a lattice with two disconnected 2x2 squares,
855
- # separated by a large distance.
856
- # Component 1: sites with indices 0, 1, 2, 3
857
- # Component 2: sites with indices 4, 5, 6, 7
858
- coords = [
859
- [0.0, 0.0],
860
- [1.0, 0.0],
861
- [0.0, 1.0],
862
- [1.0, 1.0], # Square 1
863
- [100.0, 0.0],
864
- [101.0, 0.0],
865
- [100.0, 1.0],
866
- [101.0, 1.0], # Square 2
867
- ]
868
- ids = list(range(len(coords)))
869
- lattice = CustomizeLattice(
870
- dimensionality=2, identifiers=ids, coordinates=coords
871
- )
872
- lattice._build_neighbors(max_k=1) # Explicitly build neighbors
873
-
874
- # --- Test 1: get_neighbors() ---
875
- # Act: Get neighbors for a site in the first component.
876
- neighbors_of_site_0 = lattice.get_neighbors(0, k=1)
877
-
878
- # Assert: Its neighbors must only be within the first component.
879
- assert set(neighbors_of_site_0) == {1, 2}
880
-
881
- # --- Test 2: get_neighbor_pairs() ---
882
- # Act: Get all unique bonds for the entire lattice.
883
- all_bonds = lattice.get_neighbor_pairs(k=1, unique=True)
884
-
885
- # Assert: No bond should connect a site from Component 1 to Component 2.
886
- for i, j in all_bonds:
887
- # A bond is valid only if both its sites are in the same component.
888
- # We check this by seeing if their indices fall in the same range.
889
- is_in_first_component = i < 4 and j < 4
890
- is_in_second_component = i >= 4 and j >= 4
891
-
892
- assert is_in_first_component or is_in_second_component, (
893
- f"Found an invalid bond { (i,j) } that incorrectly connects "
894
- "two separate components of the lattice."
895
- )
896
-
897
- def test_lattice_with_duplicate_coordinates(self):
898
- """
899
- Tests a pathological case where multiple sites share the exact same coordinates.
900
- The neighbor-finding logic must still treat them as distinct sites and
901
- correctly identify neighbors based on other non-overlapping sites.
902
- """
903
- # Arrange
904
- # Site 'A' and 'B' are at the same position (0,0).
905
- # Site 'C' is at (1,0), which should be a neighbor to both 'A' and 'B'.
906
- ids = ["A", "B", "C"]
907
- coords = [[0.0, 0.0], [0.0, 0.0], [1.0, 0.0]]
908
-
909
- lattice = CustomizeLattice(
910
- dimensionality=2, identifiers=ids, coordinates=coords
911
- )
912
- lattice._build_neighbors(max_k=1) # Build nearest neighbors
913
-
914
- # Act
915
- idx_A = lattice.get_index("A")
916
- idx_B = lattice.get_index("B")
917
- idx_C = lattice.get_index("C")
918
-
919
- neighbors_A = lattice.get_neighbors(idx_A, k=1)
920
- neighbors_B = lattice.get_neighbors(idx_B, k=1)
921
-
922
- # Assert
923
- # 1. The distance between the overlapping points 'A' and 'B' is 0,
924
- # so they should NOT be considered neighbors of each other.
925
- assert (
926
- idx_B not in neighbors_A
927
- ), "Overlapping sites should not be their own neighbors."
928
- assert (
929
- idx_A not in neighbors_B
930
- ), "Overlapping sites should not be their own neighbors."
931
-
932
- # 2. Both 'A' and 'B' should correctly identify 'C' as their neighbor.
933
- # This is the key test of robustness.
934
- assert neighbors_A == [
935
- idx_C
936
- ], "Site 'A' failed to find its correct neighbor 'C'."
937
- assert neighbors_B == [
938
- idx_C
939
- ], "Site 'B' failed to find its correct neighbor 'C'."
940
-
941
- # 3. Conversely, 'C' should identify both 'A' and 'B' as its neighbors.
942
- neighbors_C = lattice.get_neighbors(idx_C, k=1)
943
- assert set(neighbors_C) == {
944
- idx_A,
945
- idx_B,
946
- }, "Site 'C' failed to find both overlapping neighbors."
947
-
948
- def test_neighbor_shells_with_tiny_separation(self):
949
- """
950
- Tests the numerical stability of neighbor shell identification.
951
- Creates a lattice where the k=1 and k=2 shells are separated by a
952
- distance much smaller than the default tolerance, and verifies that they
953
- are still correctly identified as distinct shells.
954
- """
955
- # Arrange
956
- # Let d1 be the distance to the first neighbor shell.
957
- d1 = 1.0
958
- # Let d2 be the distance to the second shell, which is extremely close to d1.
959
- epsilon = 1e-8 # A tiny separation
960
- d2 = d1 + epsilon
961
-
962
- # Create a 1D lattice with these specific distances.
963
- # Site 0 is origin. Site 1 is at d1. Site 2 is at d2.
964
- ids = [0, 1, 2]
965
- coords = [[0.0], [d1], [d2]]
966
-
967
- # We explicitly use a tolerance LARGER than the separation,
968
- # which SHOULD cause the shells to merge.
969
- lattice_merged = CustomizeLattice(
970
- dimensionality=1, identifiers=ids, coordinates=coords
971
- )
972
- # Use a tolerance that cannot distinguish d1 and d2.
973
- lattice_merged._build_neighbors(max_k=2, tol=1e-7)
974
-
975
- # Now, use a tolerance SMALLER than the separation,
976
- # which SHOULD correctly distinguish the shells.
977
- lattice_distinct = CustomizeLattice(
978
- dimensionality=1, identifiers=ids, coordinates=coords
979
- )
980
- lattice_distinct._build_neighbors(max_k=2, tol=1e-9)
981
-
982
- # Assert for the merged case
983
- # With a large tolerance, site 1 and 2 should both be in the k=1 shell.
984
- merged_neighbors_k1 = lattice_merged.get_neighbors(0, k=1)
985
- assert set(merged_neighbors_k1) == {
986
- 1,
987
- 2,
988
- }, "Shells were not merged with a large tolerance."
989
- # There should be no k=2 shell.
990
- merged_neighbors_k2 = lattice_merged.get_neighbors(0, k=2)
991
- assert (
992
- merged_neighbors_k2 == []
993
- ), "A k=2 shell should not exist when shells are merged."
994
-
995
- # Assert for the distinct case
996
- # With a small tolerance, only site 1 should be in the k=1 shell.
997
- distinct_neighbors_k1 = lattice_distinct.get_neighbors(0, k=1)
998
- assert distinct_neighbors_k1 == [
999
- 1
1000
- ], "k=1 shell is incorrect with a small tolerance."
1001
- # Site 2 should now be in its own k=2 shell.
1002
- distinct_neighbors_k2 = lattice_distinct.get_neighbors(0, k=2)
1003
- assert distinct_neighbors_k2 == [
1004
- 2
1005
- ], "k=2 shell is incorrect with a small tolerance."
1006
-
1007
-
1008
- class TestTILattice:
1009
- """
1010
- A dedicated class for testing the Translationally Invariant Lattice (TILattice)
1011
- and its subclasses like SquareLattice.
1012
- """
1013
-
1014
- def test_init_with_mismatched_shapes_raises_error(self):
1015
- """
1016
- Tests that TILattice raises AssertionError if the 'size' parameter's
1017
- length does not match the dimensionality.
1018
- """
1019
- # Act & Assert:
1020
- # Pass a 'size' tuple with 3 elements to a 2D SquareLattice.
1021
- # This should trigger the AssertionError from the parent TILattice class.
1022
- with pytest.raises(AssertionError, match="Size tuple length mismatch"):
1023
- SquareLattice(size=(2, 2, 2))
1024
-
1025
- def test_init_with_tuple_pbc(self):
1026
- """
1027
- Tests that TILattice correctly handles a tuple input for the 'pbc'
1028
- (periodic boundary conditions) parameter. This covers the 'else' branch.
1029
- """
1030
- # Arrange
1031
- pbc_tuple = (True, False)
1032
-
1033
- # Act
1034
- # Initialize a lattice with a tuple for pbc.
1035
- lattice = SquareLattice(size=(3, 3), pbc=pbc_tuple)
1036
-
1037
- # Assert
1038
- # The public 'pbc' attribute should be identical to the tuple we passed.
1039
- assert lattice.pbc == pbc_tuple
1040
-
1041
- @pytest.mark.parametrize(
1042
- "LatticeClass, init_args, k_precompute",
1043
- [
1044
- (HoneycombLattice, {"size": (4, 5), "pbc": True}, 1),
1045
- (SquareLattice, {"size": (5, 5), "pbc": True}, 2),
1046
- (SquareLattice, {"size": (5, 5), "pbc": False}, 1),
1047
- (KagomeLattice, {"size": (3, 3), "pbc": True}, 1),
1048
- ],
1049
- )
1050
- def test_tilattice_max_k_precomputation_and_ondemand(
1051
- self, LatticeClass, init_args, k_precompute
1052
- ):
1053
- """
1054
- A robust, parameterized test to verify that `precompute_neighbors` (max_k)
1055
- works correctly across various TILattice types and conditions.
1056
- This test is designed to FAIL on the buggy code.
1057
- """
1058
- lattice = LatticeClass(**init_args, precompute_neighbors=k_precompute)
1059
-
1060
- computed_shells = sorted(list(lattice._neighbor_maps.keys()))
1061
- expected_shells = list(range(1, k_precompute + 1))
1062
-
1063
- assert computed_shells == expected_shells, (
1064
- f"TEST FAILED for {LatticeClass.__name__} with k={k_precompute}. "
1065
- f"Expected shells {expected_shells}, but found {computed_shells}."
1066
- )
1067
-
1068
- k_ondemand = k_precompute + 1
1069
-
1070
- _ = lattice.get_neighbors(0, k=k_ondemand)
1071
-
1072
- computed_shells_after = sorted(list(lattice._neighbor_maps.keys()))
1073
- expected_shells_after = list(range(1, k_ondemand + 1))
1074
-
1075
- assert computed_shells_after == expected_shells_after, (
1076
- f"ON-DEMAND TEST FAILED for {LatticeClass.__name__}. "
1077
- f"Expected shells {expected_shells_after} after demanding k={k_ondemand}, "
1078
- f"but found {computed_shells_after}."
1079
- )
1080
-
1081
-
1082
- class TestLongRangeNeighborFinding:
1083
- """
1084
- Tests neighbor finding on larger lattices and for longer-range interactions (large k),
1085
- addressing suggestions from code review.
1086
- """
1087
-
1088
- @pytest.fixture(scope="class")
1089
- def large_pbc_square_lattice(self) -> SquareLattice:
1090
- """
1091
- Provides a single 6x8 SquareLattice with PBC for all tests in this class.
1092
- Using scope="class" makes it more efficient as it's created only once.
1093
- """
1094
- # We choose a non-square size to catch potential bugs with non-uniform dimensions.
1095
- return SquareLattice(size=(7, 9), pbc=True)
1096
-
1097
- def test_neighbor_shell_structure_on_large_lattice(self, large_pbc_square_lattice):
1098
- """
1099
- Tests the coordination number of various neighbor shells (k) on a large
1100
- periodic lattice. In a PBC square lattice, every site is identical, so
1101
- the number of neighbors for each shell k should be the same for all sites.
1102
-
1103
- Shell distances squared and their coordination numbers for a 2D square lattice:
1104
- - k=1: dist_sq=1 (e.g., (1,0)) -> 4 neighbors
1105
- - k=2: dist_sq=2 (e.g., (1,1)) -> 4 neighbors
1106
- - k=3: dist_sq=4 (e.g., (2,0)) -> 4 neighbors
1107
- - k=4: dist_sq=5 (e.g., (2,1)) -> 8 neighbors
1108
- - k=5: dist_sq=8 (e.g., (2,2)) -> 4 neighbors
1109
- - k=6: dist_sq=9 (e.g., (3,0)) -> 4 neighbors
1110
- - k=7: dist_sq=10 (e.g., (3,1)) -> 8 neighbors
1111
- """
1112
- lattice = large_pbc_square_lattice
1113
- # Pick an arbitrary site, e.g., index 0.
1114
- site_idx = 0
1115
-
1116
- # Expected coordination numbers for the first few shells.
1117
- expected_coordinations = {1: 4, 2: 4, 3: 4, 4: 8, 5: 4, 6: 4, 7: 8}
1118
-
1119
- for k, expected_count in expected_coordinations.items():
1120
- neighbors = lattice.get_neighbors(site_idx, k=k)
1121
- assert (
1122
- len(neighbors) == expected_count
1123
- ), f"Failed for k={k}. Expected {expected_count}, got {len(neighbors)}"
1124
-
1125
- def test_requesting_k_beyond_max_possible_shell(self, large_pbc_square_lattice):
1126
- """
1127
- Tests that requesting a neighbor shell 'k' that is larger than any
1128
- possible shell in the finite lattice returns an empty list, and does
1129
- not raise an error.
1130
- """
1131
- lattice = large_pbc_square_lattice
1132
- site_idx = 0
1133
-
1134
- # 1. First, find out the maximum number of shells that *do* exist.
1135
- # We do this by calling _build_neighbors with a very large max_k.
1136
- # This is a bit of "white-box" testing but necessary to find the true max k.
1137
- lattice._build_neighbors(max_k=100)
1138
- max_k_found = len(lattice._neighbor_maps)
1139
-
1140
- # 2. Assert that the last valid shell is not empty.
1141
- last_shell_neighbors = lattice.get_neighbors(site_idx, k=max_k_found)
1142
- assert len(last_shell_neighbors) > 0
1143
-
1144
- # 3. Assert that requesting a shell just beyond the last valid one returns empty.
1145
- # This is the core of the test.
1146
- non_existent_shell_neighbors = lattice.get_neighbors(
1147
- site_idx, k=max_k_found + 1
1148
- )
1149
- assert non_existent_shell_neighbors == []
1150
-
1151
- @patch("matplotlib.pyplot.subplots")
1152
- def test_show_method_with_custom_bond_kwargs(
1153
- self, mock_subplots, simple_square_lattice
1154
- ):
1155
- """
1156
- Tests that .show() correctly uses the `bond_kwargs` parameter
1157
- to customize the appearance of neighbor bonds.
1158
- """
1159
- # Arrange:
1160
- # 1. Set up mock Figure and Axes objects, similar to other show() tests.
1161
- mock_fig = matplotlib.figure.Figure()
1162
- mock_ax = matplotlib.axes.Axes(mock_fig, [0.0, 0.0, 1.0, 1.0])
1163
- mock_subplots.return_value = (mock_fig, mock_ax)
1164
-
1165
- # 2. Define our custom styles and the expected final styles.
1166
- lattice = simple_square_lattice
1167
- custom_bond_kwargs = {"color": "red", "linestyle": ":", "linewidth": 2}
1168
-
1169
- # The final dictionary should contain the defaults updated by our custom arguments.
1170
- expected_plot_kwargs = {
1171
- "color": "red", # Overridden
1172
- "linestyle": ":", # Overridden
1173
- "linewidth": 2, # A new key
1174
- "alpha": 0.6, # From default
1175
- "zorder": 1, # From default
1176
- }
1177
-
1178
- # 3. We specifically mock the `plot` method on our mock `ax` object.
1179
- with patch.object(mock_ax, "plot") as mock_plot_method:
1180
- # Act:
1181
- # Call the show method with our custom bond styles.
1182
- lattice.show(show_bonds_k=1, bond_kwargs=custom_bond_kwargs)
1183
-
1184
- # Assert:
1185
- # Check that the plot method was called. For a 2x2 square, there are 4 NN bonds.
1186
- assert mock_plot_method.call_count == 4
1187
-
1188
- # Get the keyword arguments from the very first call to plot().
1189
- # Note: call_args is a tuple (positional_args, keyword_args). We need the second element.
1190
- actual_kwargs = mock_plot_method.call_args[1]
1191
-
1192
- # Verify that the keyword arguments used for plotting match our expectations.
1193
- assert actual_kwargs == expected_plot_kwargs
1194
-
1195
- def test_mixed_boundary_conditions(self):
1196
- """
1197
- Tests neighbor finding with mixed PBC (periodic in x, open in y).
1198
- This verifies that the neighbor finding logic correctly handles
1199
- anisotropy in periodic boundary conditions and returns sorted indices.
1200
- """
1201
- # Arrange: Create a 3x3 square lattice, periodic in x, open in y.
1202
- lattice = SquareLattice(size=(3, 3), pbc=(True, False))
1203
-
1204
- # We will test a site on the corner of the open boundary: (0, 0)
1205
- corner_site_idx = lattice.get_index((0, 0, 0))
1206
-
1207
- # --- Test corner site (0, 0, 0), which is index 0 ---
1208
- # Act
1209
- corner_neighbors = lattice.get_neighbors(corner_site_idx, k=1)
1210
-
1211
- # Assert: The expected neighbors are (1,0,0), (2,0,0) [periodic], and (0,1,0)
1212
- # We get their indices and sort them to create the expected output.
1213
- expected_indices = sorted(
1214
- [
1215
- lattice.get_index((1, 0, 0)), # Right neighbor
1216
- lattice.get_index((2, 0, 0)), # "Left" neighbor (wraps around)
1217
- lattice.get_index((0, 1, 0)), # "Up" neighbor
1218
- ]
1219
- )
1220
-
1221
- # The list returned by get_neighbors should be identical to our sorted list.
1222
- assert (
1223
- corner_neighbors == expected_indices
1224
- ), "Failed for corner site with mixed BC."
1225
-
1226
- # --- Test middle site on the edge (1, 0, 0), which is index 1 ---
1227
- edge_site_idx = lattice.get_index((1, 0, 0))
1228
-
1229
- # Act
1230
- edge_neighbors = lattice.get_neighbors(edge_site_idx, k=1)
1231
-
1232
- # Assert
1233
- expected_edge_indices = sorted(
1234
- [
1235
- lattice.get_index((0, 0, 0)), # Left neighbor
1236
- lattice.get_index((2, 0, 0)), # Right neighbor
1237
- lattice.get_index((1, 1, 0)), # "Up" neighbor
1238
- ]
1239
- )
1240
- assert (
1241
- edge_neighbors == expected_edge_indices
1242
- ), "Failed for edge site with mixed BC."
1243
-
1244
-
1245
- class TestAllTILattices:
1246
- """
1247
- A parameterized test class to verify the basic properties and coordination
1248
- numbers for all implemented TILattice subclasses. This avoids code duplication.
1249
- """
1250
-
1251
- # --- Test data in a structured and readable format ---
1252
- # Format:
1253
- # (
1254
- # LatticeClass, # The lattice class to test
1255
- # {"size": ..., ...}, # Arguments for the constructor
1256
- # expected_num_sites, # Expected total number of sites
1257
- # expected_num_basis, # Expected number of sites in the basis
1258
- # {site_repr: count} # Dict of {representative_site: neighbor_count}
1259
- # )
1260
- # For `site_repr`:
1261
- # - For simple lattices (basis=1), it's the integer index of the site.
1262
- # - For composite lattices (basis>1), it's the *basis index* to test.
1263
- lattice_test_cases = [
1264
- # 1D Lattices
1265
- (ChainLattice, {"size": (5,), "pbc": True}, 5, 1, {0: 2, 2: 2}),
1266
- (ChainLattice, {"size": (5,), "pbc": False}, 5, 1, {0: 1, 2: 2}),
1267
- (DimerizedChainLattice, {"size": (3,), "pbc": True}, 6, 2, {0: 2, 1: 2}),
1268
- # 2D Lattices
1269
- (
1270
- RectangularLattice,
1271
- {"size": (3, 4), "pbc": False},
1272
- 12,
1273
- 1,
1274
- {5: 4, 0: 2, 4: 3},
1275
- ), # center, corner, edge
1276
- (HoneycombLattice, {"size": (2, 2), "pbc": True}, 8, 2, {0: 3, 1: 3}),
1277
- (TriangularLattice, {"size": (3, 3), "pbc": True}, 9, 1, {0: 6}),
1278
- (CheckerboardLattice, {"size": (2, 2), "pbc": True}, 8, 2, {0: 4, 1: 4}),
1279
- (KagomeLattice, {"size": (2, 2), "pbc": True}, 12, 3, {0: 4, 1: 4, 2: 4}),
1280
- (LiebLattice, {"size": (2, 2), "pbc": True}, 12, 3, {0: 4, 1: 2, 2: 2}),
1281
- # 3D Lattices
1282
- (CubicLattice, {"size": (3, 3, 3), "pbc": True}, 27, 1, {0: 6, 13: 6}),
1283
- ]
1284
-
1285
- @pytest.mark.parametrize(
1286
- "LatticeClass, init_args, num_sites, num_basis, coordination_numbers",
1287
- lattice_test_cases,
1288
- )
1289
- def test_lattice_properties_and_coordination(
1290
- self,
1291
- LatticeClass,
1292
- init_args,
1293
- num_sites,
1294
- num_basis,
1295
- coordination_numbers,
1296
- ):
1297
- """
1298
- A single, parameterized test to validate all TILattice types.
1299
- """
1300
- # --- Arrange ---
1301
- # Create the lattice instance dynamically from the test data.
1302
- lattice = LatticeClass(**init_args)
1303
-
1304
- # --- Assert: Basic properties ---
1305
- assert lattice.num_sites == num_sites
1306
- assert lattice.num_basis == num_basis
1307
- assert lattice.dimensionality == len(init_args["size"])
1308
-
1309
- # --- Assert: Coordination numbers (nearest neighbors, k=1) ---
1310
- for site_repr, expected_count in coordination_numbers.items():
1311
- # This logic correctly gets the site index to test,
1312
- # whether it's a simple or composite lattice.
1313
- if lattice.num_basis > 1:
1314
- # For composite lattices, site_repr is the basis_index.
1315
- # We find the index of this basis site in the first unit cell.
1316
- uc_coord = (0,) * lattice.dimensionality
1317
- test_site_idx = lattice.get_index(uc_coord + (site_repr,))
1318
- else:
1319
- # For simple lattices, site_repr is the absolute site index.
1320
- test_site_idx = site_repr
1321
-
1322
- neighbors = lattice.get_neighbors(test_site_idx, k=1)
1323
- assert len(neighbors) == expected_count
1324
- if isinstance(LatticeClass, ChainLattice) and not init_args.get("pbc"):
1325
- if test_site_idx == 0:
1326
- assert 1 in neighbors
1327
-
1328
-
1329
- class TestCustomizeLatticeDynamic:
1330
- """Tests the dynamic modification capabilities of CustomizeLattice."""
1331
-
1332
- @pytest.fixture
1333
- def initial_lattice(self) -> CustomizeLattice:
1334
- """Provides a basic 3-site lattice for modification tests."""
1335
- return CustomizeLattice(
1336
- dimensionality=2,
1337
- identifiers=["A", "B", "C"],
1338
- coordinates=[[0, 0], [1, 0], [0, 1]],
1339
- )
1340
-
1341
- def test_from_lattice_conversion(self):
1342
- """Tests creating a CustomizeLattice from a TILattice."""
1343
- # Arrange
1344
- sq_lattice = SquareLattice(size=(2, 2), pbc=False)
1345
-
1346
- # Act
1347
- custom_lattice = CustomizeLattice.from_lattice(sq_lattice)
1348
-
1349
- # Assert
1350
- assert isinstance(custom_lattice, CustomizeLattice)
1351
- assert custom_lattice.num_sites == sq_lattice.num_sites
1352
- assert custom_lattice.dimensionality == sq_lattice.dimensionality
1353
- # Verify a site to be sure
1354
- np.testing.assert_array_equal(
1355
- custom_lattice.get_coordinates(3), sq_lattice.get_coordinates(3)
1356
- )
1357
- assert custom_lattice.get_identifier(3) == sq_lattice.get_identifier(3)
1358
-
1359
- def test_add_sites_successfully(self, initial_lattice):
1360
- """Tests adding new, valid sites to the lattice."""
1361
- # Arrange
1362
- lat = initial_lattice
1363
- assert lat.num_sites == 3
1364
-
1365
- # Act
1366
- lat.add_sites(identifiers=["D", "E"], coordinates=[[1, 1], [2, 2]])
1367
-
1368
- # Assert
1369
- assert lat.num_sites == 5
1370
- assert lat.get_identifier(4) == "E"
1371
- np.testing.assert_array_equal(lat.get_coordinates(3), np.array([1, 1]))
1372
- assert "E" in lat._ident_to_idx
1373
-
1374
- def test_remove_sites_successfully(self, initial_lattice):
1375
- """Tests removing existing sites from the lattice."""
1376
- # Arrange
1377
- lat = initial_lattice
1378
- assert lat.num_sites == 3
1379
-
1380
- # Act
1381
- lat.remove_sites(identifiers=["A", "C"])
1382
-
1383
- # Assert
1384
- assert lat.num_sites == 1
1385
- assert lat.get_identifier(0) == "B" # Site 'B' is now at index 0
1386
- assert "A" not in lat._ident_to_idx
1387
- np.testing.assert_array_equal(lat.get_coordinates(0), np.array([1, 0]))
1388
-
1389
- def test_add_duplicate_identifier_raises_error(self, initial_lattice):
1390
- """Tests that adding a site with an existing identifier fails."""
1391
- with pytest.raises(ValueError, match="Duplicate identifiers found"):
1392
- initial_lattice.add_sites(identifiers=["A"], coordinates=[[9, 9]])
1393
-
1394
- def test_remove_nonexistent_identifier_raises_error(self, initial_lattice):
1395
- """Tests that removing a non-existent site fails."""
1396
- with pytest.raises(ValueError, match="Non-existent identifiers provided"):
1397
- initial_lattice.remove_sites(identifiers=["Z"])
1398
-
1399
- def test_modification_clears_neighbor_cache(self, initial_lattice):
1400
- """
1401
- Tests that add_sites and remove_sites correctly invalidate the
1402
- pre-computed neighbor map.
1403
- """
1404
- # Arrange: Pre-compute neighbors on the initial lattice
1405
- initial_lattice._build_neighbors(max_k=1)
1406
- assert 0 in initial_lattice._neighbor_maps[1] # Check that neighbors exist
1407
-
1408
- # Act 1: Add a site
1409
- initial_lattice.add_sites(identifiers=["D"], coordinates=[[5, 5]])
1410
-
1411
- # Assert 1: The neighbor map should now be empty
1412
- assert not initial_lattice._neighbor_maps
1413
-
1414
- # Arrange 2: Re-compute neighbors and then remove a site
1415
- initial_lattice._build_neighbors(max_k=1)
1416
- assert 0 in initial_lattice._neighbor_maps[1]
1417
-
1418
- # Act 2: Remove a site
1419
- initial_lattice.remove_sites(identifiers=["A"])
1420
-
1421
- # Assert 2: The neighbor map should be empty again
1422
- assert not initial_lattice._neighbor_maps
1423
-
1424
- def test_modification_clears_distance_matrix_cache(self, initial_lattice):
1425
- """
1426
- Tests that add_sites and remove_sites correctly invalidate the
1427
- cached distance matrix and that the recomputed matrix is correct.
1428
- """
1429
- # Arrange 1: Compute, cache, and perform a meaningful check on the original matrix.
1430
- lat = initial_lattice
1431
- original_matrix = lat.distance_matrix
1432
- assert lat._distance_matrix is not None
1433
- assert original_matrix.shape == (3, 3)
1434
- # Meaningful check: distance from 'A'(idx 0) to 'B'(idx 1) should be 1.0
1435
- np.testing.assert_allclose(original_matrix[0, 1], 1.0)
1436
-
1437
- # Act 1: Add a site. This should invalidate the cache.
1438
- lat.add_sites(identifiers=["D"], coordinates=[[1, 1]])
1439
-
1440
- # Assert 1: Check cache is cleared and the new matrix is correct.
1441
- assert lat._distance_matrix is None # Verify cache invalidation
1442
- new_matrix_added = lat.distance_matrix
1443
- assert new_matrix_added.shape == (4, 4)
1444
- # Meaningful check: distance from 'B'(idx 1) to new site 'D'(idx 3) should be 1.0
1445
- # Coords: B=[1,0], D=[1,1]
1446
- np.testing.assert_allclose(new_matrix_added[1, 3], 1.0)
1447
-
1448
- # Act 2: Remove a site. This should also invalidate the cache.
1449
- lat.remove_sites(identifiers=["A"])
1450
-
1451
- # Assert 2: Check cache is cleared again and the final matrix is correct.
1452
- assert lat._distance_matrix is None # Verify cache invalidation
1453
- final_matrix = lat.distance_matrix
1454
- assert final_matrix.shape == (3, 3) # Now has 3 sites again
1455
- # Meaningful check: After removing 'A', the sites are B, C, D.
1456
- # 'B' is now at index 0 (coords [1,0])
1457
- # 'C' is now at index 1 (coords [0,1])
1458
- # 'D' is now at index 2 (coords [1,1])
1459
- # Distance from new 'B' (idx 0) to new 'D' (idx 2) should be 1.0
1460
- np.testing.assert_allclose(final_matrix[0, 2], 1.0)
1461
-
1462
- def test_neighbor_finding_returns_sorted_list(self, simple_square_lattice):
1463
- """
1464
- Ensures that the list of neighbors returned by get_neighbors is always sorted.
1465
- This provides a stricter check than set-based comparisons.
1466
- """
1467
- # Arrange
1468
- lattice = simple_square_lattice
1469
-
1470
- # Act
1471
- # Get neighbors for the central site (index 1 in a 2x2 grid)
1472
- # Expected neighbors are 0, 3.
1473
- neighbors = lattice.get_neighbors(1, k=1)
1474
-
1475
- # Assert
1476
- # We compare directly against a pre-sorted list, not a set.
1477
- # This will fail if the implementation returns [3, 0] instead of [0, 3].
1478
- assert neighbors == [
1479
- 0,
1480
- 3,
1481
- ], "The neighbor list should be sorted in ascending order."
1482
-
1483
-
1484
- class TestDistanceMatrix:
1485
-
1486
- # This is the upgraded, parameterized test.
1487
- @pytest.mark.parametrize(
1488
- # We define test scenarios as tuples:
1489
- # (build_k, check_site_identifier, expected_dist_sq)
1490
- # build_k: The number of neighbor shells to pre-build.
1491
- # check_site_identifier: The identifier of a site whose distance from the origin we will check.
1492
- # expected_dist_sq: The expected squared distance to that site.
1493
- "build_k, check_site_identifier, expected_dist_sq",
1494
- [
1495
- # Scenario 1: The most common case. Build only NN (k=1), but check a NNN (k=2) distance.
1496
- # A buggy cache would fail this.
1497
- (1, (1, 1, 0), 2.0),
1498
- # Scenario 2: Build up to k=2, but check a k=3 distance.
1499
- (2, (2, 0, 0), 4.0),
1500
- # Scenario 3: Build up to k=3, but check a k=4 distance.
1501
- (3, (2, 1, 0), 5.0),
1502
- # Scenario 4: A more complex, higher-order neighbor.
1503
- (5, (3, 1, 0), 10.0),
1504
- ],
1505
- )
1506
- def test_tilattice_full_pbc_distance_matrix_is_correct_regardless_of_build_k(
1507
- self, build_k, check_site_identifier, expected_dist_sq
1508
- ):
1509
- """
1510
- Tests that the distance matrix for a fully periodic TILattice is
1511
- always fully correct, no matter how many neighbor shells were pre-calculated.
1512
- This is a high-strength test designed to catch subtle caching bugs where
1513
- the cached matrix might only contain partial information.
1514
- """
1515
- # Arrange
1516
- # Using a larger, non-square lattice to avoid accidental symmetries
1517
- lat = SquareLattice(size=(7, 9), pbc=True)
1518
-
1519
- # Act
1520
- # Step 1: Pre-build neighbors. This is where a faulty caching
1521
- # mechanism in the source code might be triggered.
1522
- lat._build_neighbors(max_k=build_k)
1523
-
1524
- # Step 2: Access the distance_matrix property. A correct implementation
1525
- # will return a fully valid matrix.
1526
- dist_matrix = lat.distance_matrix
1527
-
1528
- # Assert
1529
- # Find the indices for the sites we want to check.
1530
- origin_idx = lat.get_index((0, 0, 0))
1531
- check_site_idx = lat.get_index(check_site_identifier)
1532
-
1533
- # The core assertion: check the distance.
1534
- actual_dist_sq = dist_matrix[origin_idx, check_site_idx] ** 2
1535
-
1536
- error_message = (
1537
- f"Distance matrix failed when building k={build_k}. "
1538
- f"Checking distance to site {check_site_identifier} (expected sq={expected_dist_sq}) "
1539
- f"but got sq={actual_dist_sq} instead."
1540
- )
1541
-
1542
- np.testing.assert_allclose(
1543
- actual_dist_sq, expected_dist_sq, err_msg=error_message
1544
- )
1545
-
1546
- def test_tilattice_mixed_bc_distance_matrix_is_correct(self):
1547
- """
1548
- Tests that the distance matrix is correctly calculated for a TILattice
1549
- with mixed boundary conditions (e.g., periodic in x, open in y).
1550
- """
1551
- # Arrange
1552
- # pbc=(True, False) means periodic along x-axis, open along y-axis.
1553
- lat = SquareLattice(size=(5, 5), pbc=(True, False))
1554
-
1555
- # Pre-build neighbors to engage the caching logic.
1556
- lat._build_neighbors(max_k=2)
1557
- dist_matrix = lat.distance_matrix
1558
-
1559
- # Assert
1560
- origin_idx = lat.get_index((0, 0, 0))
1561
-
1562
- # 1. Test a distance affected by the periodic boundary (x-direction)
1563
- # The distance between (0,0) and (4,0) should be 1.0 due to PBC wrap-around.
1564
- pbc_neighbor_idx = lat.get_index((4, 0, 0))
1565
- np.testing.assert_allclose(dist_matrix[origin_idx, pbc_neighbor_idx], 1.0)
1566
-
1567
- # 2. Test a distance affected by the open boundary (y-direction)
1568
- # The distance between (0,0) and (0,4) should be 4.0 as there's no wrap-around.
1569
- obc_neighbor_idx = lat.get_index((0, 4, 0))
1570
- np.testing.assert_allclose(dist_matrix[origin_idx, obc_neighbor_idx], 4.0)
1571
-
1572
- # 3. Test a general, off-axis point.
1573
- # Distance from (0,0) to (3,3) with x-pbc. The x-distance is min(3, 5-3=2) = 2.
1574
- # The y-distance is 3. So total distance is sqrt(2^2 + 3^2) = sqrt(13).
1575
- general_neighbor_idx = lat.get_index((3, 3, 0))
1576
- np.testing.assert_allclose(
1577
- dist_matrix[origin_idx, general_neighbor_idx], np.sqrt(13)
1578
- )
1579
-
1580
- # --- This list and the following test are now at the correct indentation level ---
1581
- lattice_instances_for_invariant_test = [
1582
- SquareLattice(size=(4, 4), pbc=True),
1583
- SquareLattice(size=(4, 3), pbc=(True, False)), # Mixed BC, non-square
1584
- HoneycombLattice(size=(3, 3), pbc=True),
1585
- TriangularLattice(size=(4, 4), pbc=False),
1586
- CustomizeLattice(
1587
- dimensionality=2,
1588
- identifiers=list(range(4)),
1589
- coordinates=[[0, 0], [1, 1], [0, 1], [1, 0]],
1590
- ),
1591
- ]
1592
-
1593
- @pytest.mark.parametrize("lattice", lattice_instances_for_invariant_test)
1594
- def test_distance_matrix_invariants_for_all_lattice_types(self, lattice):
1595
- """
1596
- Tests that the distance matrix for any lattice type adheres to
1597
- fundamental mathematical properties (invariants): symmetry, zero diagonal,
1598
- and positive off-diagonal elements.
1599
- """
1600
- # Arrange
1601
- n = lattice.num_sites
1602
- if n < 2:
1603
- pytest.skip("Invariant test requires at least 2 sites.")
1604
-
1605
- # Act
1606
- # We call the property directly, without building neighbors first,
1607
- # to test the on-demand computation path.
1608
- matrix = lattice.distance_matrix
1609
-
1610
- # Assert
1611
- # 1. Symmetry: The matrix must be equal to its transpose.
1612
- np.testing.assert_allclose(
1613
- matrix,
1614
- matrix.T,
1615
- err_msg=f"Distance matrix for {type(lattice).__name__} is not symmetric.",
1616
- )
1617
-
1618
- # 2. Zero Diagonal: All diagonal elements must be zero.
1619
- np.testing.assert_allclose(
1620
- np.diag(matrix),
1621
- np.zeros(n),
1622
- err_msg=f"Diagonal of distance matrix for {type(lattice).__name__} is not zero.",
1623
- )
1624
-
1625
- # 3. Positive Off-diagonal: All non-diagonal elements must be > 0.
1626
- # We create a boolean mask for the off-diagonal elements.
1627
- off_diagonal_mask = ~np.eye(n, dtype=bool)
1628
- assert np.all(
1629
- matrix[off_diagonal_mask] > 1e-9
1630
- ), f"Found non-positive off-diagonal elements in distance matrix for {type(lattice).__name__}."
1631
-
1632
-
1633
- # @pytest.mark.slow
1634
- # class TestPerformance:
1635
- # def test_pbc_implementation_is_not_significantly_slower_than_obc(self):
1636
- # """
1637
- # A performance regression test.
1638
- # It ensures that the specialized implementation for fully periodic
1639
- # lattices (pbc=True) is not substantially slower than the general
1640
- # implementation used for open boundaries (pbc=False).
1641
- # This test will FAIL with the current code, exposing the performance bug.
1642
- # """
1643
- # # Arrange: Use a large-enough lattice to make performance differences apparent
1644
- # size = (30, 30)
1645
- # k = 1
1646
-
1647
- # # Act 1: Measure the execution time of the general (OBC) implementation
1648
- # start_time_obc = time.time()
1649
- # _ = SquareLattice(size=size, pbc=False, precompute_neighbors=k)
1650
- # duration_obc = time.time() - start_time_obc
1651
-
1652
- # # Act 2: Measure the execution time of the specialized (PBC) implementation
1653
- # start_time_pbc = time.time()
1654
- # _ = SquareLattice(size=size, pbc=True, precompute_neighbors=k)
1655
- # duration_pbc = time.time() - start_time_pbc
1656
-
1657
- # print(
1658
- # f"\n[Performance] OBC ({size}): {duration_obc:.4f}s | PBC ({size}): {duration_pbc:.4f}s"
1659
- # )
1660
-
1661
- # # Assert: The PBC implementation should not be drastically slower.
1662
- # # We allow it to be up to 3 times slower to account for minor overheads,
1663
- # # but this will catch the current 10x+ regression.
1664
- # # THIS ASSERTION WILL FAIL with the current buggy code.
1665
- # assert duration_pbc < duration_obc * 5, (
1666
- # "The specialized PBC implementation is significantly slower "
1667
- # "than the general-purpose implementation."
1668
- # )
1669
-
1670
-
1671
- def _validate_layers(bonds, layers) -> None:
1672
- """
1673
- A helper function to scientifically validate the output of get_compatible_layers.
1674
- """
1675
- # MODIFICATION: This function now takes the original bonds list for comparison.
1676
- expected_edges = set(tuple(sorted(b)) for b in bonds)
1677
- actual_edges = set(tuple(sorted(edge)) for layer in layers for edge in layer)
1678
-
1679
- assert (
1680
- expected_edges == actual_edges
1681
- ), "Completeness check failed: The set of all edges in the layers must "
1682
- "exactly match the input bonds."
1683
-
1684
- for i, layer in enumerate(layers):
1685
- qubits_in_layer: set[int] = set()
1686
- for edge in layer:
1687
- q1, q2 = edge
1688
- assert (
1689
- q1 not in qubits_in_layer
1690
- ), f"Compatibility check failed: Qubit {q1} is reused in layer {i}."
1691
- qubits_in_layer.add(q1)
1692
- assert (
1693
- q2 not in qubits_in_layer
1694
- ), f"Compatibility check failed: Qubit {q2} is reused in layer {i}."
1695
- qubits_in_layer.add(q2)
1696
-
1697
-
1698
- @pytest.mark.parametrize(
1699
- "lattice_instance",
1700
- [
1701
- SquareLattice(size=(3, 2), pbc=False),
1702
- SquareLattice(size=(3, 3), pbc=True),
1703
- HoneycombLattice(size=(2, 2), pbc=False),
1704
- ],
1705
- ids=[
1706
- "SquareLattice_3x2_OBC",
1707
- "SquareLattice_3x3_PBC",
1708
- "HoneycombLattice_2x2_OBC",
1709
- ],
1710
- )
1711
- def test_layering_on_various_lattices(lattice_instance: AbstractLattice):
1712
- """Tests gate layering for various standard lattice types."""
1713
- bonds = lattice_instance.get_neighbor_pairs(k=1, unique=True)
1714
- layers = get_compatible_layers(bonds)
1715
-
1716
- assert len(layers) > 0, "Layers should not be empty for non-trivial lattices."
1717
- _validate_layers(bonds, layers)
1718
-
1719
-
1720
- def test_layering_on_1d_chain_pbc():
1721
- """Test layering on a 1D chain with periodic boundaries (a cycle graph)."""
1722
- lattice_even = ChainLattice(size=(6,), pbc=True)
1723
- bonds_even = lattice_even.get_neighbor_pairs(k=1, unique=True)
1724
- layers_even = get_compatible_layers(bonds_even)
1725
- _validate_layers(bonds_even, layers_even)
1726
-
1727
- lattice_odd = ChainLattice(size=(5,), pbc=True)
1728
- bonds_odd = lattice_odd.get_neighbor_pairs(k=1, unique=True)
1729
- layers_odd = get_compatible_layers(bonds_odd)
1730
- assert len(layers_odd) == 3, "A 5-site cycle graph should be 3-colorable."
1731
- _validate_layers(bonds_odd, layers_odd)
1732
-
1733
-
1734
- def test_layering_on_custom_star_graph():
1735
- """Test layering on a custom lattice forming a star graph."""
1736
- star_edges = [(0, 1), (0, 2), (0, 3)]
1737
- layers = get_compatible_layers(star_edges)
1738
- assert len(layers) == 3, "A star graph S_4 requires 3 layers."
1739
- _validate_layers(star_edges, layers)
1740
-
1741
-
1742
- def test_layering_on_edge_cases():
1743
- """Test various edge cases: empty, single-site, and no-edge lattices."""
1744
- layers_empty = get_compatible_layers([])
1745
- assert layers_empty == [], "Layers should be empty for an empty set of bonds."
1746
-
1747
- single_edge = [(0, 1)]
1748
- layers_single = get_compatible_layers(single_edge)
1749
- assert layers_single == [[(0, 1)]]
1750
- _validate_layers(single_edge, layers_single)