Skip to content

Commit 8020ccc

Browse files
authored
Merge pull request #1791 from qiboteam/tfim
Add `closed_boundary` argument to `hamiltonians.models.TFIM`
2 parents dff626d + e3d3aff commit 8020ccc

File tree

2 files changed

+146
-47
lines changed

2 files changed

+146
-47
lines changed

src/qibo/hamiltonians/models.py

Lines changed: 114 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
from functools import reduce
2-
from typing import Optional, Union
2+
from typing import Callable, List, Optional, Union
33

44
import numpy as np
5+
from numpy.typing import ArrayLike
56

67
from qibo import symbols
78
from qibo.backends import Backend, _check_backend
89
from qibo.config import raise_error
910
from qibo.hamiltonians.hamiltonians import Hamiltonian, SymbolicHamiltonian
1011

1112

12-
def X(nqubits, dense: bool = True, backend=None):
13+
def X(
14+
nqubits: int, dense: bool = True, backend: Optional[Backend] = None
15+
) -> Hamiltonian | ArrayLike:
1316
"""Non-interacting Pauli-:math:`X` Hamiltonian.
1417
1518
.. math::
@@ -28,15 +31,17 @@ def X(nqubits, dense: bool = True, backend=None):
2831
return _OneBodyPauli(nqubits, symbols.X, dense, backend=backend)
2932

3033

31-
def Y(nqubits, dense: bool = True, backend=None):
34+
def Y(
35+
nqubits: int, dense: bool = True, backend: Optional[Backend] = None
36+
) -> Hamiltonian | ArrayLike:
3237
"""Non-interacting Pauli-:math:`Y` Hamiltonian.
3338
3439
.. math::
3540
H = - \\sum _{k=0}^{N} \\, Y_{k} \\, .
3641
3742
Args:
3843
nqubits (int): number of qubits.
39-
dense (bool): If ``True`` it creates the Hamiltonian as a
44+
dense (bool, optional): If ``True`` it creates the Hamiltonian as a
4045
:class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
4146
a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
4247
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
@@ -46,15 +51,17 @@ def Y(nqubits, dense: bool = True, backend=None):
4651
return _OneBodyPauli(nqubits, symbols.Y, dense, backend=backend)
4752

4853

49-
def Z(nqubits, dense: bool = True, backend=None):
54+
def Z(
55+
nqubits: int, dense: bool = True, backend: Optional[Backend] = None
56+
) -> Hamiltonian | ArrayLike:
5057
"""Non-interacting Pauli-:math:`Z` Hamiltonian.
5158
5259
.. math::
5360
H = - \\sum _{k=0}^{N} \\, Z_{k} \\, .
5461
5562
Args:
5663
nqubits (int): number of qubits.
57-
dense (bool): If ``True`` it creates the Hamiltonian as a
64+
dense (bool, optional): If ``True`` it creates the Hamiltonian as a
5865
:class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
5966
a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
6067
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
@@ -64,11 +71,19 @@ def Z(nqubits, dense: bool = True, backend=None):
6471
return _OneBodyPauli(nqubits, symbols.Z, dense, backend=backend)
6572

6673

67-
def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None):
68-
"""Transverse field Ising model with periodic boundary conditions.
74+
def TFIM(
75+
nqubits: int,
76+
h: float = 0.0,
77+
dense: bool = True,
78+
closed_boundary: bool = True,
79+
backend: Optional[Backend] = None,
80+
):
81+
""":math:`n`-qubit Transverse field Ising model.
6982
7083
.. math::
71-
H = - \\sum _{k=0}^{N} \\, \\left(Z_{k} \\, Z_{k + 1} + h \\, X_{k}\\right) \\, .
84+
H = - \\sum _{k=0}^{n-1} \\, \\left(Z_{k} \\, Z_{k + 1} + h \\, X_{k}\\right) \\, ,
85+
86+
with :math:`Z_{n-1} Z_{n} \\equiv Z_{n-1} Z_{0}` if ``closed_boundary=True``.
7287
7388
Args:
7489
nqubits (int): number of qubits.
@@ -77,38 +92,69 @@ def TFIM(nqubits, h: float = 0.0, dense: bool = True, backend=None):
7792
:class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
7893
a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
7994
Defaults to ``True``.
95+
closed_boundary (bool, optional): If ``True``, returns TFIM with periodic boundary
96+
condition. If ``False``, returns Hamiltonian with open boundaries.
97+
Defaults to ``True``.
98+
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
99+
in the execution. If ``None``, it uses the current backend.
100+
Defaults to ``None``.
80101
"""
81102
if nqubits < 2:
82103
raise_error(ValueError, "Number of qubits must be larger than one.")
83104

84105
backend = _check_backend(backend)
85106

86107
if dense:
87-
condition = lambda i, j: i in {j % nqubits, (j + 1) % nqubits}
88-
ham = -_build_spin_model(nqubits, backend.matrices.Z, condition, backend)
89-
if h != 0:
90-
condition = lambda i, j: i == j % nqubits
91-
ham -= h * _build_spin_model(
92-
nqubits, backend.matrices.X, condition, backend
108+
matrix = backend.zeros((2**nqubits, 2**nqubits), dtype=backend.complex128)
109+
base_string = [backend.matrices.I()] * nqubits
110+
for qubit in range(nqubits - 1):
111+
base_string[qubit] = backend.matrices.Z
112+
base_string[qubit + 1] = backend.matrices.Z
113+
matrix += reduce(backend.kron, base_string)
114+
base_string[qubit] = backend.matrices.I()
115+
base_string[qubit + 1] = backend.matrices.I()
116+
117+
if closed_boundary:
118+
base_string = (
119+
[backend.matrices.Z]
120+
+ [backend.matrices.I()] * (nqubits - 2)
121+
+ [backend.matrices.Z]
93122
)
94-
return Hamiltonian(nqubits, ham, backend=backend)
123+
matrix += reduce(backend.kron, base_string)
124+
125+
if h != 0:
126+
base_string = [backend.matrices.I()] * nqubits
127+
for qubit in range(nqubits):
128+
base_string[qubit] = backend.matrices.X
129+
matrix += h * reduce(backend.kron, base_string)
130+
base_string[qubit] = backend.matrices.I()
131+
132+
matrix *= -1
133+
134+
return Hamiltonian(nqubits, matrix, backend=backend)
95135

96136
term = lambda q1, q2: symbols.Z(q1, backend=backend) * symbols.Z(
97137
q2, backend=backend
98138
) + h * symbols.X(q1, backend=backend)
99-
form = -1 * sum(term(qubit, qubit + 1) for qubit in range(nqubits - 1)) - term(
100-
nqubits - 1, 0
101-
)
139+
140+
form = -1 * sum(term(qubit, qubit + 1) for qubit in range(nqubits - 1))
141+
142+
if closed_boundary:
143+
form -= term(nqubits - 1, 0)
144+
else:
145+
form -= h * symbols.X(nqubits - 1, backend=backend)
146+
102147
ham = SymbolicHamiltonian(form=form, nqubits=nqubits, backend=backend)
148+
103149
return ham
104150

105151

106152
def MaxCut(
107-
nqubits,
153+
nqubits: int,
108154
dense: bool = True,
109-
adj_matrix: Optional[Union[list[list[float]], np.ndarray]] = None,
155+
adj_matrix: Optional[Union[list[list[float]], ArrayLike]] = None,
110156
backend: Optional[Backend] = None,
111-
):
157+
) -> Hamiltonian | ArrayLike:
112158
"""Max Cut Hamiltonian.
113159
114160
.. math::
@@ -152,7 +198,9 @@ def MaxCut(
152198
return ham
153199

154200

155-
def LABS(nqubits: int, dense: bool = True, backend: Optional[Backend] = None):
201+
def LABS(
202+
nqubits: int, dense: bool = True, backend: Optional[Backend] = None
203+
) -> Hamiltonian | ArrayLike:
156204
"""Create Hamiltonian of the Low Autocorrelation Binary Sequences (LABS) problem.
157205
158206
Given an integer :math:`n > 2`, the LABS problem consists of finding a binary sequence
@@ -167,7 +215,7 @@ def LABS(nqubits: int, dense: bool = True, backend: Optional[Backend] = None):
167215
168216
Args:
169217
nqubits (int): Total number of qubits.
170-
dense (bool): If ``True`` it creates the Hamiltonian as a
218+
dense (bool, optional): If ``True`` it creates the Hamiltonian as a
171219
:class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
172220
a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
173221
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
@@ -202,12 +250,12 @@ def LABS(nqubits: int, dense: bool = True, backend: Optional[Backend] = None):
202250

203251

204252
def Heisenberg(
205-
nqubits,
253+
nqubits: int,
206254
coupling_constants: Union[float, int, list, tuple],
207255
external_field_strengths: Union[float, int],
208256
dense: bool = True,
209-
backend=None,
210-
):
257+
backend: Optional[Backend] = None,
258+
) -> Hamiltonian | ArrayLike:
211259
"""Heisenberg model on a :math:`1`-dimensional periodic lattice.
212260
213261
The general :math:`n`-qubit Hamiltonian is given by
@@ -274,8 +322,7 @@ def Heisenberg(
274322

275323
if dense:
276324
condition = lambda i, j: i in {j % nqubits, (j + 1) % nqubits}
277-
matrix = np.zeros((2**nqubits, 2**nqubits))
278-
matrix = backend.cast(matrix, dtype=backend.complex128)
325+
matrix = backend.zeros((2**nqubits, 2**nqubits), dtype=backend.complex128)
279326
for ind, pauli in enumerate(paulis):
280327
double_term = _build_spin_model(
281328
nqubits, pauli(0, backend=backend).matrix, condition, backend
@@ -314,11 +361,11 @@ def term(q1, q2):
314361

315362

316363
def XXX(
317-
nqubits,
364+
nqubits: int,
318365
coupling_constant: Union[float, int] = 1,
319366
external_field_strengths: Union[float, int, list, tuple] = [0.5, 0, 0],
320367
dense: bool = True,
321-
backend=None,
368+
backend: Optional[Backend] = None,
322369
):
323370
"""Heisenberg :math:`\\mathrm{XXX}` model with periodic boundary conditions.
324371
@@ -371,7 +418,12 @@ def XXX(
371418
)
372419

373420

374-
def XXZ(nqubits, delta=0.5, dense: bool = True, backend=None):
421+
def XXZ(
422+
nqubits: int,
423+
delta: Union[float, int] = 0.5,
424+
dense: bool = True,
425+
backend: Optional[Backend] = None,
426+
) -> Hamiltonian | ArrayLike:
375427
"""Heisenberg :math:`\\mathrm{XXZ}` model with periodic boundary conditions.
376428
377429
.. math::
@@ -386,7 +438,7 @@ def XXZ(nqubits, delta=0.5, dense: bool = True, backend=None):
386438
387439
Args:
388440
nqubits (int): number of qubits.
389-
delta (float, optional): coefficient for the :math:`Z` component.
441+
delta (float or int, optional): coefficient for the :math:`Z` component.
390442
Defaults to :math:`0.5`.
391443
dense (bool, optional): If ``True``, creates the Hamiltonian as a
392444
:class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
@@ -407,11 +459,11 @@ def XXZ(nqubits, delta=0.5, dense: bool = True, backend=None):
407459

408460

409461
def GPP(
410-
adjacency_matrix,
462+
adjacency_matrix: ArrayLike,
411463
penalty_coeff: Union[float, int] = 0.0,
412-
node_weights=None,
464+
node_weights: Optional[ArrayLike] = None,
413465
dense: bool = True,
414-
backend=None,
466+
backend: Optional[Backend] = None,
415467
):
416468
"""The Graph Partitioning Problem (GPP) as a quadratic function.
417469
@@ -439,6 +491,11 @@ def GPP(
439491
adjacency_matrix (ndarray): Square symmetric matrix with weigths :math:`A_{jk}`
440492
representing the edges of the graph. For an unweighted graph,
441493
:math:`\\A_{jk} = 1, \\,\\, \\forall \\, j,k`.
494+
penalty_coeff (float or int, optional): hyperparameter :math:`\\lambda` defining the
495+
strength of the penalty term. Defaults to :math:`0.0`.
496+
node_weights (ArrayLike, optional): Weight of the nodes of the graph.
497+
Used when :math:`\\lambda \\neq 0`. If ``None``, all node weights
498+
default to :math:`1`. Defaults to ``None``.
442499
dense (bool, optional): If ``True``, creates the Hamiltonian as a
443500
:class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
444501
a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
@@ -491,7 +548,12 @@ def GPP(
491548
return _gpp_dense(adjacency_matrix, penalty_coeff, node_weights, backend)
492549

493550

494-
def _gpp_symbolic(adjacency_matrix, penalty_coeff, node_weights, backend):
551+
def _gpp_symbolic(
552+
adjacency_matrix: ArrayLike,
553+
penalty_coeff: float | int,
554+
node_weights: ArrayLike,
555+
backend: Backend,
556+
) -> SymbolicHamiltonian:
495557
def term(index: int):
496558
return (
497559
symbols.I(index, backend=backend) - symbols.Z(index, backend=backend)
@@ -520,11 +582,11 @@ def term(index: int):
520582

521583

522584
def _gpp_dense(
523-
adjacency_matrix,
524-
penalty_coeff,
525-
node_weights,
526-
backend,
527-
):
585+
adjacency_matrix: ArrayLike,
586+
penalty_coeff: ArrayLike,
587+
node_weights: ArrayLike,
588+
backend: Backend,
589+
) -> Hamiltonian:
528590
def term(nqubits, ind_1, ind_2=None):
529591
diag = [id_diag] * nqubits
530592
diag[ind_1] = term_diag
@@ -560,7 +622,7 @@ def term(nqubits, ind_1, ind_2=None):
560622
return Hamiltonian(nqubits, backend.diag(hamiltonian), backend=backend)
561623

562624

563-
def _multikron(matrix_list, backend):
625+
def _multikron(matrix_list: List[ArrayLike], backend: Backend) -> ArrayLike:
564626
"""Calculates Kronecker product of a list of matrices.
565627
566628
Args:
@@ -572,7 +634,9 @@ def _multikron(matrix_list, backend):
572634
return reduce(backend.kron, matrix_list)
573635

574636

575-
def _build_spin_model(nqubits, matrix, condition, backend):
637+
def _build_spin_model(
638+
nqubits: int, matrix: ArrayLike, condition: Callable, backend: Backend
639+
) -> ArrayLike:
576640
"""Helper method for building nearest-neighbor spin model Hamiltonians."""
577641
h = sum(
578642
reduce(
@@ -587,7 +651,12 @@ def _build_spin_model(nqubits, matrix, condition, backend):
587651
return h
588652

589653

590-
def _OneBodyPauli(nqubits, operator, dense: bool = True, backend=None):
654+
def _OneBodyPauli(
655+
nqubits: int,
656+
operator: Callable,
657+
dense: bool = True,
658+
backend: Optional[Backend] = None,
659+
) -> Hamiltonian | SymbolicHamiltonian:
591660
"""Helper method for constructing non-interacting
592661
:math:`X`, :math:`Y`, and :math:`Z` Hamiltonians."""
593662
backend = _check_backend(backend)

tests/test_hamiltonians_models.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import pytest
77

88
from qibo import hamiltonians, matrices, symbols
9-
from qibo.hamiltonians import MaxCut, SymbolicHamiltonian
10-
from qibo.hamiltonians.models import GPP, LABS, XXX, Heisenberg
9+
from qibo.hamiltonians import SymbolicHamiltonian
10+
from qibo.hamiltonians.models import GPP, LABS, TFIM, XXX, Heisenberg, MaxCut
1111

1212
models_config = [
1313
("X", {"nqubits": 3}, "x_N3.out"),
@@ -164,3 +164,33 @@ def test_gpp(backend, nqubits, penalty_coeff, dense, is_list, node_weights):
164164
target += penalty_coeff * (penalty**2)
165165

166166
backend.assert_allclose(hamiltonian.matrix, target)
167+
168+
169+
@pytest.mark.parametrize("dense", [False, True])
170+
@pytest.mark.parametrize("closed_boundary", [False, True])
171+
@pytest.mark.parametrize("h", [0.0, 0.5])
172+
def test_tfim_boundary(backend, h, closed_boundary, dense):
173+
nqubits = 3
174+
175+
I = lambda x: symbols.I(x, backend=backend)
176+
X = lambda x: symbols.X(x, backend=backend)
177+
Z = lambda x: symbols.Z(x, backend=backend)
178+
179+
target = Z(0) * Z(1) + Z(1) * Z(2)
180+
if closed_boundary:
181+
target += Z(2) * Z(0)
182+
if h != 0.0:
183+
for qubit in range(nqubits):
184+
target += h * X(qubit)
185+
186+
target *= -1
187+
target = SymbolicHamiltonian(target, nqubits=nqubits, backend=backend)
188+
print(target)
189+
target = backend.real(target.matrix)
190+
191+
hamiltonian = TFIM(
192+
nqubits, h=h, closed_boundary=closed_boundary, dense=dense, backend=backend
193+
)
194+
hamiltonian = backend.real(hamiltonian.matrix)
195+
196+
backend.assert_allclose(hamiltonian, target)

0 commit comments

Comments
 (0)