tensorcircuit-nightly 1.0.2.dev20250108__py3-none-any.whl → 1.4.0.dev20251103__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 (76) hide show
  1. tensorcircuit/__init__.py +18 -2
  2. tensorcircuit/about.py +46 -0
  3. tensorcircuit/abstractcircuit.py +4 -0
  4. tensorcircuit/analogcircuit.py +413 -0
  5. tensorcircuit/applications/layers.py +1 -1
  6. tensorcircuit/applications/van.py +1 -1
  7. tensorcircuit/backends/abstract_backend.py +320 -7
  8. tensorcircuit/backends/cupy_backend.py +3 -1
  9. tensorcircuit/backends/jax_backend.py +102 -4
  10. tensorcircuit/backends/jax_ops.py +110 -1
  11. tensorcircuit/backends/numpy_backend.py +49 -3
  12. tensorcircuit/backends/pytorch_backend.py +92 -3
  13. tensorcircuit/backends/tensorflow_backend.py +102 -3
  14. tensorcircuit/basecircuit.py +157 -98
  15. tensorcircuit/circuit.py +115 -57
  16. tensorcircuit/cloud/local.py +1 -1
  17. tensorcircuit/cloud/quafu_provider.py +1 -1
  18. tensorcircuit/cloud/tencent.py +1 -1
  19. tensorcircuit/compiler/simple_compiler.py +2 -2
  20. tensorcircuit/cons.py +142 -21
  21. tensorcircuit/densitymatrix.py +43 -14
  22. tensorcircuit/experimental.py +387 -129
  23. tensorcircuit/fgs.py +282 -81
  24. tensorcircuit/gates.py +66 -22
  25. tensorcircuit/interfaces/__init__.py +1 -3
  26. tensorcircuit/interfaces/jax.py +189 -0
  27. tensorcircuit/keras.py +3 -3
  28. tensorcircuit/mpscircuit.py +154 -65
  29. tensorcircuit/quantum.py +868 -152
  30. tensorcircuit/quditcircuit.py +733 -0
  31. tensorcircuit/quditgates.py +618 -0
  32. tensorcircuit/results/counts.py +147 -20
  33. tensorcircuit/results/readout_mitigation.py +4 -1
  34. tensorcircuit/shadows.py +1 -1
  35. tensorcircuit/simplify.py +3 -1
  36. tensorcircuit/stabilizercircuit.py +479 -0
  37. tensorcircuit/templates/__init__.py +2 -0
  38. tensorcircuit/templates/blocks.py +2 -2
  39. tensorcircuit/templates/hamiltonians.py +174 -0
  40. tensorcircuit/templates/lattice.py +1789 -0
  41. tensorcircuit/timeevol.py +896 -0
  42. tensorcircuit/translation.py +10 -3
  43. tensorcircuit/utils.py +7 -0
  44. {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info}/METADATA +73 -23
  45. tensorcircuit_nightly-1.4.0.dev20251103.dist-info/RECORD +96 -0
  46. {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info}/WHEEL +1 -1
  47. {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info}/top_level.txt +0 -1
  48. tensorcircuit_nightly-1.0.2.dev20250108.dist-info/RECORD +0 -115
  49. tests/__init__.py +0 -0
  50. tests/conftest.py +0 -67
  51. tests/test_backends.py +0 -1031
  52. tests/test_calibrating.py +0 -149
  53. tests/test_channels.py +0 -365
  54. tests/test_circuit.py +0 -1699
  55. tests/test_cloud.py +0 -219
  56. tests/test_compiler.py +0 -147
  57. tests/test_dmcircuit.py +0 -555
  58. tests/test_ensemble.py +0 -72
  59. tests/test_fgs.py +0 -310
  60. tests/test_gates.py +0 -156
  61. tests/test_interfaces.py +0 -429
  62. tests/test_keras.py +0 -160
  63. tests/test_miscs.py +0 -277
  64. tests/test_mpscircuit.py +0 -341
  65. tests/test_noisemodel.py +0 -156
  66. tests/test_qaoa.py +0 -86
  67. tests/test_qem.py +0 -152
  68. tests/test_quantum.py +0 -526
  69. tests/test_quantum_attr.py +0 -42
  70. tests/test_results.py +0 -347
  71. tests/test_shadows.py +0 -160
  72. tests/test_simplify.py +0 -46
  73. tests/test_templates.py +0 -218
  74. tests/test_torchnn.py +0 -99
  75. tests/test_van.py +0 -102
  76. {tensorcircuit_nightly-1.0.2.dev20250108.dist-info → tensorcircuit_nightly-1.4.0.dev20251103.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,479 @@
1
+ """
2
+ Stabilizer circuit simulator using Stim backend
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional, Sequence
6
+ import numpy as np
7
+ import stim
8
+
9
+ from .abstractcircuit import AbstractCircuit
10
+
11
+ Tensor = Any
12
+
13
+
14
+ class StabilizerCircuit(AbstractCircuit):
15
+ """
16
+ Quantum circuit simulator for stabilizer circuits using Stim backend.
17
+ Supports Clifford operations and measurements.
18
+ """
19
+
20
+ # Add gate sets as class attributes
21
+ clifford_gates = ["h", "x", "y", "z", "cnot", "cz", "swap", "s", "sd"]
22
+ gate_map = {
23
+ "h": "H",
24
+ "x": "X",
25
+ "y": "Y",
26
+ "z": "Z",
27
+ "cnot": "CNOT",
28
+ "cz": "CZ",
29
+ "swap": "SWAP",
30
+ "s": "S",
31
+ "sd": "S_DAG",
32
+ }
33
+
34
+ def __init__(
35
+ self, nqubits: int, inputs: Tensor = None, tableau_inputs: Tensor = None
36
+ ) -> None:
37
+ """
38
+ ``StabilizerCircuit`` class based on stim package
39
+
40
+ :param nqubits: Number of qubits
41
+ :type nqubits: int
42
+ :param inputs: initial state by stabilizers, defaults to None
43
+ :type inputs: Tensor, optional
44
+ :param tableau_inputs: initial state by **inverse** tableau, defaults to None
45
+ :type tableau_inputs: Tensor, optional
46
+ """
47
+ self._nqubits = nqubits
48
+ self._stim_circuit = stim.Circuit()
49
+ self._qir: List[Dict[str, Any]] = []
50
+ self.is_dm = False
51
+ self.inputs = None
52
+ self._extra_qir: List[Dict[str, Any]] = []
53
+ self.current_sim = stim.TableauSimulator()
54
+ if inputs:
55
+ self.current_sim.set_state_from_stabilizers(inputs)
56
+ if tableau_inputs:
57
+ self.current_sim.set_inverse_tableau(tableau_inputs)
58
+
59
+ def apply_general_gate(
60
+ self,
61
+ gate: Any,
62
+ *index: int,
63
+ name: Optional[str] = None,
64
+ **kws: Any,
65
+ ) -> None:
66
+ """
67
+ Apply a Clifford gate to the circuit.
68
+
69
+ :param gate: Gate to apply (must be Clifford)
70
+ :type gate: Any
71
+ :param index: Qubit indices to apply the gate to
72
+ :type index: int
73
+ :param name: Name of the gate operation, defaults to None
74
+ :type name: Optional[str], optional
75
+ :raises ValueError: If non-Clifford gate is applied
76
+ """
77
+ if name is None:
78
+ name = ""
79
+
80
+ # Record gate in QIR
81
+ gate_dict = {
82
+ "gate": gate,
83
+ "index": index,
84
+ "name": name,
85
+ "split": None,
86
+ "mpo": False,
87
+ }
88
+ ir_dict = kws["ir_dict"]
89
+ if ir_dict is not None:
90
+ ir_dict.update(gate_dict)
91
+ else:
92
+ ir_dict = gate_dict
93
+ self._qir.append(ir_dict)
94
+
95
+ # Map TensorCircuit gates to Stim gates
96
+
97
+ if name.lower() in self.gate_map:
98
+ # self._stim_circuit.append(gate_map[name.lower()], list(index))
99
+ gn = self.gate_map[name.lower()]
100
+ instruction = f"{gn} {' '.join(map(str, index))}"
101
+ self._stim_circuit.append_from_stim_program_text(instruction)
102
+ # append is much slower
103
+ # self.current_sim.do(stim.Circuit(instruction))
104
+ getattr(self.current_sim, gn.lower())(*index)
105
+ else:
106
+ raise ValueError(f"Gate {name} is not supported in stabilizer simulation")
107
+
108
+ apply = apply_general_gate
109
+
110
+ def state(self) -> Tensor:
111
+ """
112
+ Return the wavefunction of the circuit.
113
+ Note that the state can have smaller qubit count if no gate is applied on later qubits
114
+ """
115
+ tab = self.current_tableau()
116
+ return tab.to_state_vector(endian="big")
117
+
118
+ def random_gate(self, *index: int, recorded: bool = False) -> None:
119
+ """
120
+ Apply a random Clifford gate to the circuit.
121
+ This operation will not record in qir
122
+
123
+ :param index: Qubit indices to apply the gate to
124
+ :type index: int
125
+ :param recorded: Whether the gate is recorded in ``stim.Circuit``, defaults to False
126
+ :type recorded: bool, optional
127
+ """
128
+ m = len(index)
129
+ t = stim.Tableau.random(m)
130
+ self.current_sim.do_tableau(t, index)
131
+ if recorded:
132
+ self._stim_circuit += t.to_circuit()
133
+
134
+ def tableau_gate(self, *index: int, tableau: Any, recorded: bool = False) -> None:
135
+ """
136
+ Apply a gate indicated by tableau to the circuit.
137
+ This operation will not record in qir
138
+
139
+ :param index: Qubit indices to apply the gate to
140
+ :type index: int
141
+ :param tableau: stim.Tableau representation of the gate
142
+ :type tableau: Any
143
+ :param recorded: Whether the gate is recorded in ``stim.Circuit``, defaults to False
144
+ :type recorded: bool, optional
145
+ """
146
+ self.current_sim.do_tableau(tableau, index)
147
+ if recorded:
148
+ self._stim_circuit += tableau.to_circuit()
149
+
150
+ def measure(self, *index: int, with_prob: bool = False) -> Tensor:
151
+ """
152
+ Measure qubits in the Z basis.
153
+
154
+ :param index: Indices of the qubits to measure.
155
+ :type index: int
156
+ :param with_prob: If True, returns the theoretical probability of the measurement outcome.
157
+ defaults to False
158
+ :type with_prob: bool, optional
159
+ :return: A tensor containing the measurement results.
160
+ If `with_prob` is True, a tuple containing the results and the probability is returned.
161
+ :rtype: Tensor
162
+ """
163
+ # Convert negative indices
164
+
165
+ index = tuple([i for i in index if i >= 0])
166
+
167
+ # Add measurement instructions
168
+ s1 = self.current_simulator().copy()
169
+ # Sample once from the circuit using sampler
170
+
171
+ if with_prob:
172
+ num_random_measurements = 0
173
+ for i in index:
174
+ if s1.peek_z(i) == 0:
175
+ num_random_measurements += 1
176
+ probability = (0.5) ** num_random_measurements
177
+
178
+ m = s1.measure_many(*index)
179
+ if with_prob:
180
+ return m, probability
181
+ return m
182
+
183
+ def cond_measurement(self, index: int) -> Tensor:
184
+ """
185
+ Measure a single qubit in the Z basis and collapse the state.
186
+
187
+ :param index: The index of the qubit to measure.
188
+ :type index: int
189
+ :return: The measurement result (0 or 1).
190
+ :rtype: Tensor
191
+ """
192
+ # Convert negative indices
193
+
194
+ # Add measurement instructions
195
+ self._stim_circuit.append_from_stim_program_text("M " + str(index))
196
+ # self.current_sim = None
197
+ m = self.current_simulator().measure(index)
198
+ # Sample once from the circuit using sampler
199
+
200
+ return m
201
+
202
+ cond_measure = cond_measurement
203
+
204
+ def cond_measure_many(self, *index: int) -> Tensor:
205
+ """
206
+ Measure multiple qubits in the Z basis and collapse the state.
207
+
208
+ :param index: The indices of the qubits to measure.
209
+ :type index: int
210
+ :return: A tensor containing the measurement results.
211
+ :rtype: Tensor
212
+ """
213
+ # Convert negative indices
214
+
215
+ # Add measurement instructions
216
+ self._stim_circuit.append_from_stim_program_text(
217
+ "M " + " ".join(map(str, index))
218
+ )
219
+ # self.current_sim = None
220
+ m = self.current_simulator().measure_many(*index)
221
+ # Sample once from the circuit using sampler
222
+
223
+ return m
224
+
225
+ def sample(
226
+ self,
227
+ batch: Optional[int] = None,
228
+ **kws: Any,
229
+ ) -> Tensor:
230
+ """
231
+ Sample measurements from the circuit.
232
+
233
+ :param batch: Number of samples to take, defaults to None (single sample)
234
+ :type batch: Optional[int], optional
235
+ :return: Measurement results
236
+ :rtype: Tensor
237
+ """
238
+ if batch is None:
239
+ batch = 1
240
+ c = self.current_circuit().copy()
241
+ for i in range(self._nqubits):
242
+ c.append("M", [i])
243
+ sampler = c.compile_sampler()
244
+ samples = sampler.sample(batch)
245
+ return np.array(samples)
246
+
247
+ def expectation_ps( # type: ignore
248
+ self,
249
+ x: Optional[Sequence[int]] = None,
250
+ y: Optional[Sequence[int]] = None,
251
+ z: Optional[Sequence[int]] = None,
252
+ **kws: Any,
253
+ ) -> Any:
254
+ """
255
+ Compute exact expectation value of Pauli string using stim's direct calculation.
256
+
257
+ :param x: Indices for Pauli X measurements
258
+ :type x: Optional[Sequence[int]], optional
259
+ :param y: Indices for Pauli Y measurements
260
+ :type y: Optional[Sequence[int]], optional
261
+ :param z: Indices for Pauli Z measurements
262
+ :type z: Optional[Sequence[int]], optional
263
+ :return: Expectation value
264
+ :rtype: float
265
+ """
266
+ # Build Pauli string representation
267
+ pauli_str = ["I"] * self._nqubits
268
+
269
+ if x:
270
+ for i in x:
271
+ pauli_str[i] = "X"
272
+ if y:
273
+ for i in y:
274
+ pauli_str[i] = "Y"
275
+ if z:
276
+ for i in z:
277
+ pauli_str[i] = "Z"
278
+
279
+ pauli_string = "".join(pauli_str)
280
+ # Calculate expectation using stim's direct method
281
+ expectation = self.current_simulator().peek_observable_expectation(
282
+ stim.PauliString(pauli_string)
283
+ )
284
+ return expectation
285
+
286
+ expps = expectation_ps
287
+
288
+ def sample_expectation_ps(
289
+ self,
290
+ x: Optional[Sequence[int]] = None,
291
+ y: Optional[Sequence[int]] = None,
292
+ z: Optional[Sequence[int]] = None,
293
+ shots: Optional[int] = None,
294
+ **kws: Any,
295
+ ) -> float:
296
+ """
297
+ Compute expectation value of Pauli string measurements.
298
+
299
+ :param x: Indices for Pauli X measurements, defaults to None
300
+ :type x: Optional[Sequence[int]], optional
301
+ :param y: Indices for Pauli Y measurements, defaults to None
302
+ :type y: Optional[Sequence[int]], optional
303
+ :param z: Indices for Pauli Z measurements, defaults to None
304
+ :type z: Optional[Sequence[int]], optional
305
+ :param shots: Number of measurement shots, defaults to None
306
+ :type shots: Optional[int], optional
307
+ :return: Expectation value
308
+ :rtype: float
309
+ """
310
+ if shots is None:
311
+ shots = 1000 # Default number of shots
312
+
313
+ circuit = self._stim_circuit.copy()
314
+
315
+ # Add basis rotations for measurements
316
+ if x:
317
+ for i in x:
318
+ circuit.append("H", [i])
319
+ if y:
320
+ for i in y:
321
+ circuit.append("S_DAG", [i])
322
+ circuit.append("H", [i])
323
+
324
+ # Add measurements
325
+ measured_qubits: List[int] = []
326
+ if x:
327
+ measured_qubits.extend(x)
328
+ if y:
329
+ measured_qubits.extend(y)
330
+ if z:
331
+ measured_qubits.extend(z)
332
+
333
+ for i in measured_qubits:
334
+ circuit.append("M", [i])
335
+
336
+ # Sample and compute expectation using sampler
337
+ sampler = circuit.compile_sampler()
338
+ samples = sampler.sample(shots)
339
+ results = np.array(samples)
340
+
341
+ # Convert from {0,1} to {1,-1}
342
+ results = 1 - 2 * results
343
+
344
+ # Average over shots
345
+ expectation = np.mean(np.prod(results, axis=1))
346
+
347
+ return float(expectation)
348
+
349
+ sexpps = sample_expectation_ps
350
+
351
+ def mid_measurement(self, index: int, keep: int = 0) -> Tensor:
352
+ """
353
+ Perform a mid-measurement operation on a qubit on z direction.
354
+ The post-selection cannot be recorded in ``stim.Circuit``
355
+
356
+ :param index: Index of the qubit to measure
357
+ :type index: int
358
+ :param keep: State of qubits to keep after measurement, defaults to 0 (up)
359
+ :type keep: int, optional
360
+ :return: Result of the mid-measurement operation
361
+ :rtype: Tensor
362
+ """
363
+ if keep not in [0, 1]:
364
+ raise ValueError("keep must be 0 or 1")
365
+
366
+ self.current_sim.postselect_z(index, desired_value=keep)
367
+
368
+ mid_measure = mid_measurement
369
+ post_select = mid_measurement
370
+ post_selection = mid_measurement
371
+
372
+ def depolarizing(self, *index: int, p: float) -> None:
373
+ """
374
+ Apply depolarizing noise to a qubit.
375
+
376
+ :param index: Index of the qubit to apply noise to
377
+ :type index: int
378
+ :param p: Noise parameter (probability of depolarizing)
379
+ :type p: float
380
+ """
381
+ self._stim_circuit.append_from_stim_program_text(
382
+ f"DEPOLARIZE1({p}) {' '.join(map(str, index))}"
383
+ )
384
+ self.current_sim.depolarize1(*index, p=p)
385
+
386
+ def current_simulator(self) -> stim.TableauSimulator:
387
+ """
388
+ Return the current simulator of the circuit.
389
+ """
390
+ return self.current_sim
391
+
392
+ def current_circuit(self) -> stim.Circuit:
393
+ """
394
+ Return the current stim circuit representation of the circuit.
395
+ """
396
+ return self._stim_circuit
397
+
398
+ def current_tableau(self) -> stim.Tableau:
399
+ """
400
+ Return the current tableau of the circuit.
401
+ """
402
+ return self.current_simulator().current_inverse_tableau() ** -1
403
+
404
+ def current_inverse_tableau(self) -> stim.Tableau:
405
+ """
406
+ Return the current inverse tableau of the circuit.
407
+ """
408
+ return self.current_simulator().current_inverse_tableau()
409
+
410
+ def entanglement_entropy(self, cut: Sequence[int]) -> float:
411
+ """
412
+ Calculate the entanglement entropy for a subset of qubits using stabilizer formalism.
413
+
414
+ :param cut: Indices of qubits to calculate entanglement entropy for
415
+ :type cut: Sequence[int]
416
+ :return: Entanglement entropy
417
+ :rtype: float
418
+ """
419
+ # Get stabilizer tableau
420
+ tableau = self.current_tableau()
421
+ N = len(tableau)
422
+
423
+ # Pre-allocate binary matrix with proper dtype
424
+ # binary_matrix = np.zeros((N, 2 * N), dtype=np.int8)
425
+
426
+ # Vectorized conversion of stabilizers to binary matrix
427
+ # z_outputs = np.array([tableau.z_output(k) for k in range(N)])
428
+ # x_part = z_outputs == 1 # X
429
+ # z_part = z_outputs == 3 # Z
430
+ # y_part = z_outputs == 2 # Y
431
+
432
+ # binary_matrix[:, :N] = x_part | y_part
433
+ # binary_matrix[:, N:] = z_part | y_part
434
+
435
+ _, _, z2x, z2z, _, _ = tableau.to_numpy()
436
+ binary_matrix = np.concatenate([z2x, z2z], axis=1)
437
+ # Get reduced matrix for the cut using boolean indexing
438
+ cut_set = set(cut)
439
+ cut_indices = np.array(
440
+ [i for i in range(N) if i in cut_set]
441
+ + [i + N for i in range(N) if i in cut_set]
442
+ )
443
+ reduced_matrix = binary_matrix[:, cut_indices]
444
+
445
+ # Efficient rank calculation using Gaussian elimination
446
+ matrix = reduced_matrix.copy()
447
+ n_rows, n_cols = matrix.shape
448
+ rank = 0
449
+ row = 0
450
+
451
+ for col in range(n_cols):
452
+ # Vectorized pivot finding
453
+ pivot_rows = np.nonzero(matrix[row:, col])[0]
454
+ if len(pivot_rows) > 0:
455
+ pivot_row = pivot_rows[0] + row
456
+
457
+ # Swap rows if necessary
458
+ if pivot_row != row:
459
+ matrix[row], matrix[pivot_row] = (
460
+ matrix[pivot_row].copy(),
461
+ matrix[row].copy(),
462
+ )
463
+
464
+ # Vectorized elimination
465
+ eliminate_mask = matrix[row + 1 :, col] == 1
466
+ matrix[row + 1 :][eliminate_mask] ^= matrix[row]
467
+
468
+ rank += 1
469
+ row += 1
470
+
471
+ if row == n_rows:
472
+ break
473
+
474
+ # Calculate entropy
475
+ return float((rank - len(cut)) * np.log(2))
476
+
477
+
478
+ # Call _meta_apply at module level to register the gates
479
+ StabilizerCircuit._meta_apply()
@@ -5,5 +5,7 @@ from . import dataset
5
5
  from . import graphs
6
6
  from . import measurements
7
7
  from . import conversions
8
+ from . import lattice
9
+ from . import hamiltonians
8
10
 
9
11
  costfunctions = measurements
@@ -91,7 +91,7 @@ def QAOA_block(
91
91
  e2,
92
92
  unitary=G._zz_matrix,
93
93
  theta=paramzz * g[e1][e2].get("weight", 1.0),
94
- **kws
94
+ **kws,
95
95
  )
96
96
  else:
97
97
  i = 0
@@ -157,7 +157,7 @@ def qft(
157
157
  *index: int,
158
158
  do_swaps: bool = True,
159
159
  inverse: bool = False,
160
- insert_barriers: bool = False
160
+ insert_barriers: bool = False,
161
161
  ) -> Circuit:
162
162
  """
163
163
  This function applies quantum fourier transformation (QFT) to the selected circuit lines
@@ -0,0 +1,174 @@
1
+ from typing import Any, List, Tuple, Union
2
+ import numpy as np
3
+ from ..cons import dtypestr, backend
4
+ from ..quantum import PauliStringSum2COO
5
+ from .lattice import AbstractLattice
6
+
7
+
8
+ def _create_empty_sparse_matrix(shape: Tuple[int, int]) -> Any:
9
+ """
10
+ Helper function to create a backend-agnostic empty sparse matrix.
11
+ """
12
+ indices = backend.convert_to_tensor(backend.zeros((0, 2), dtype="int32"))
13
+ values = backend.convert_to_tensor(backend.zeros((0,), dtype=dtypestr)) # type: ignore
14
+ return backend.coo_sparse_matrix(indices=indices, values=values, shape=shape) # type: ignore
15
+
16
+
17
+ def heisenberg_hamiltonian(
18
+ lattice: AbstractLattice,
19
+ j_coupling: Union[float, List[float], Tuple[float, ...]] = 1.0,
20
+ interaction_scope: str = "neighbors",
21
+ ) -> Any:
22
+ r"""
23
+ Generates the sparse matrix of the Heisenberg Hamiltonian for a given lattice.
24
+
25
+ The Heisenberg Hamiltonian is defined as:
26
+ :math:`H = J\sum_{i,j} (X_i X_j + Y_i Y_j + Z_i Z_j)`
27
+ where the sum is over a specified set of interacting pairs {i,j}.
28
+
29
+ :param lattice: An instance of a class derived from AbstractLattice,
30
+ which provides the geometric information of the system.
31
+ :type lattice: AbstractLattice
32
+ :param j_coupling: The coupling constants. Can be a single float for an
33
+ isotropic model (Jx=Jy=Jz) or a list/tuple of 3 floats for an
34
+ anisotropic model (Jx, Jy, Jz). Defaults to 1.0.
35
+ :type j_coupling: Union[float, List[float], Tuple[float, ...]], optional
36
+ :param interaction_scope: Defines the range of interactions.
37
+ - "neighbors": Includes only nearest-neighbor pairs (default).
38
+ - "all": Includes all unique pairs of sites.
39
+ :type interaction_scope: str, optional
40
+ :return: The Hamiltonian as a backend-agnostic sparse matrix.
41
+ :rtype: Any
42
+ """
43
+ num_sites = lattice.num_sites
44
+ if interaction_scope == "neighbors":
45
+ neighbor_pairs = lattice.get_neighbor_pairs(k=1, unique=True)
46
+ elif interaction_scope == "all":
47
+ neighbor_pairs = lattice.get_all_pairs()
48
+ else:
49
+ raise ValueError(
50
+ f"Invalid interaction_scope: '{interaction_scope}'. "
51
+ "Must be 'neighbors' or 'all'."
52
+ )
53
+
54
+ if isinstance(j_coupling, (float, int)):
55
+ js = [float(j_coupling)] * 3
56
+ else:
57
+ if len(j_coupling) != 3:
58
+ raise ValueError("j_coupling must be a float or a list/tuple of 3 floats.")
59
+ js = [float(j) for j in j_coupling]
60
+
61
+ if not neighbor_pairs:
62
+ return _create_empty_sparse_matrix(shape=(2**num_sites, 2**num_sites))
63
+ if num_sites == 0:
64
+ raise ValueError("Cannot generate a Hamiltonian for a lattice with zero sites.")
65
+
66
+ pauli_map = {"X": 1, "Y": 2, "Z": 3}
67
+
68
+ ls: List[List[int]] = []
69
+ weights: List[float] = []
70
+
71
+ pauli_terms = ["X", "Y", "Z"]
72
+ for i, j in neighbor_pairs:
73
+ for idx, pauli_char in enumerate(pauli_terms):
74
+ if abs(js[idx]) > 1e-9:
75
+ string = [0] * num_sites
76
+ string[i] = pauli_map[pauli_char]
77
+ string[j] = pauli_map[pauli_char]
78
+ ls.append(string)
79
+ weights.append(js[idx])
80
+
81
+ hamiltonian_matrix = PauliStringSum2COO(ls, weight=weights, numpy=False)
82
+
83
+ return hamiltonian_matrix
84
+
85
+
86
+ def rydberg_hamiltonian(
87
+ lattice: AbstractLattice, omega: float, delta: float, c6: float
88
+ ) -> Any:
89
+ r"""
90
+ Generates the sparse matrix of the Rydberg atom array Hamiltonian.
91
+
92
+ The Hamiltonian is defined as:
93
+ .. math::
94
+
95
+ H = \sum_i \frac{\Omega}{2} X_i
96
+ - \sum_i \frac{\delta}{2} \bigl(1 - Z_i \bigr)
97
+ + \sum_{i<j} \frac{V_{ij}}{4} \bigl(1 - Z_i \bigr)\bigl(1 - Z_j \bigr)
98
+
99
+ = \sum_i \frac{\Omega}{2} X_i
100
+ + \sum_i \frac{\delta}{2} Z_i
101
+ + \sum_{i<j} \frac{V_{ij}}{4}\,\bigl(Z_i Z_j - Z_i - Z_j \bigr)
102
+
103
+ where :math:`V_{ij} = C6 / |r_i - r_j|^6`.
104
+
105
+ Note: Constant energy offset terms (proportional to the identity operator)
106
+ are ignored in this implementation.
107
+
108
+ :param lattice: An instance of a class derived from AbstractLattice,
109
+ which provides site coordinates and the distance matrix.
110
+ :type lattice: AbstractLattice
111
+ :param omega: The Rabi frequency (Ω) of the driving laser field.
112
+ :type omega: float
113
+ :param delta: The laser detuning (δ).
114
+ :type delta: float
115
+ :param c6: The Van der Waals interaction coefficient (C6).
116
+ :type c6: float
117
+ :return: The Hamiltonian as a backend-agnostic sparse matrix.
118
+ :rtype: Any
119
+ """
120
+ num_sites = lattice.num_sites
121
+ if num_sites == 0:
122
+ raise ValueError("Cannot generate a Hamiltonian for a lattice with zero sites.")
123
+
124
+ pauli_map = {"X": 1, "Y": 2, "Z": 3}
125
+ ls: List[List[int]] = []
126
+ weights: List[float] = []
127
+
128
+ for i in range(num_sites):
129
+ x_string = [0] * num_sites
130
+ x_string[i] = pauli_map["X"]
131
+ ls.append(x_string)
132
+ weights.append(omega / 2.0)
133
+
134
+ z_coefficients = np.zeros(num_sites)
135
+
136
+ for i in range(num_sites):
137
+ z_coefficients[i] += delta / 2.0
138
+
139
+ dist_matrix = lattice.distance_matrix
140
+
141
+ for i in range(num_sites):
142
+ for j in range(i + 1, num_sites):
143
+ distance = dist_matrix[i, j]
144
+
145
+ if distance < 1e-9:
146
+ continue
147
+
148
+ interaction_strength = c6 / (distance**6)
149
+ coefficient = interaction_strength / 4.0
150
+
151
+ zz_string = [0] * num_sites
152
+ zz_string[i] = pauli_map["Z"]
153
+ zz_string[j] = pauli_map["Z"]
154
+ ls.append(zz_string)
155
+ weights.append(coefficient)
156
+
157
+ # The interaction term V_ij * n_i * n_j, when expanded using
158
+ # n_i = (1-Z_i)/2, becomes (V_ij/4)*(I - Z_i - Z_j + Z_i*Z_j).
159
+ # This contributes a positive term (+V_ij/4) to the ZZ interaction,
160
+ # but negative terms (-V_ij/4) to the single-site Z_i and Z_j operators.
161
+
162
+ z_coefficients[i] -= coefficient
163
+ z_coefficients[j] -= coefficient
164
+
165
+ for i in range(num_sites):
166
+ if abs(z_coefficients[i]) > 1e-9:
167
+ z_string = [0] * num_sites
168
+ z_string[i] = pauli_map["Z"]
169
+ ls.append(z_string)
170
+ weights.append(z_coefficients[i]) # type: ignore
171
+
172
+ hamiltonian_matrix = PauliStringSum2COO(ls, weight=weights, numpy=False)
173
+
174
+ return hamiltonian_matrix