Skip to content

Commit ed34736

Browse files
authored
Merge pull request #1748 from qiboteam/gpp
Add new Hamiltonian `hamiltonians.models.GPP`
2 parents d066ac9 + 1ad79d2 commit ed34736

File tree

5 files changed

+229
-3
lines changed

5 files changed

+229
-3
lines changed

doc/source/api-reference/qibo.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1359,6 +1359,14 @@ Heisenberg XXZ
13591359
:member-order: bysource
13601360

13611361

1362+
Graph Partitioning Problem
1363+
^^^^^^^^^^^^^^^^^^^^^^^^^^
1364+
1365+
.. autoclass:: qibo.hamiltonians.GPP
1366+
:members:
1367+
:member-order: bysource
1368+
1369+
13621370
.. note::
13631371
All pre-coded Hamiltonians can be created as
13641372
:class:`qibo.hamiltonians.Hamiltonian` using ``dense=True``

src/qibo/hamiltonians/__init__.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,15 @@
33
SymbolicAdiabaticHamiltonian,
44
)
55
from qibo.hamiltonians.hamiltonians import Hamiltonian, SymbolicHamiltonian
6-
from qibo.hamiltonians.models import LABS, TFIM, XXX, XXZ, Heisenberg, MaxCut, X, Y, Z
6+
from qibo.hamiltonians.models import (
7+
GPP,
8+
LABS,
9+
TFIM,
10+
XXX,
11+
XXZ,
12+
Heisenberg,
13+
MaxCut,
14+
X,
15+
Y,
16+
Z,
17+
)

src/qibo/hamiltonians/models.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,160 @@ def XXZ(nqubits, delta=0.5, dense: bool = True, backend=None):
406406
return Heisenberg(nqubits, [-1, -1, -delta], 0, dense=dense, backend=backend)
407407

408408

409+
def GPP(
410+
adjacency_matrix,
411+
penalty_coeff: Union[float, int] = 0.0,
412+
node_weights=None,
413+
dense: bool = True,
414+
backend=None,
415+
):
416+
"""The Graph Partitioning Problem (GPP) as a quadratic function.
417+
418+
For a (possibly weighted) graph :math:`G = (V, E)` defined by its set :math:`V` of vertices
419+
and set :math:`E` of edges, the GPP is the task of dividing a graph's vertices into :math:`m`
420+
subsets of approximately equal size, such that :math:`\\bigcup_{k=1}^{m} \\, V_{k} = V`,
421+
while minimizing the number of edges that connect vertices in different subsets.
422+
423+
The formulation of the GPP as a quadratic unconstrained binary optimization (QUBO) reduces to
424+
minimizing the following objective function :math:`C(x)`:
425+
426+
.. math::
427+
C(x) = \\sum_{(j,k) \\in E} \\, A_{jk} \\, (x_{j} + x_{k} - 2 \\, x_{j} \\, x_{k})
428+
\\, + \\lambda \\, P(x),
429+
430+
where :math:`x_{j} \\in \\{0, \\, 1\\}` is a binary variable,
431+
432+
.. math::
433+
P(x) = \\left(\\sum_{k} \\, v_{k} \\, x_{k} - \\sum_{k} \\, \\frac{v_{k}}{2}\\right)^{2}
434+
435+
is a term designed to penalize deviations in the total node weight on each partition,
436+
and :math:`\\lambda > 0` is a hyperparameter.
437+
438+
Args:
439+
adjacency_matrix (ndarray): Square symmetric matrix with weigths :math:`A_{jk}`
440+
representing the edges of the graph. For an unweighted graph,
441+
:math:`\\A_{jk} = 1, \\,\\, \\forall \\, j,k`.
442+
dense (bool, optional): If ``True``, creates the Hamiltonian as a
443+
:class:`qibo.core.hamiltonians.Hamiltonian`, otherwise it creates
444+
a :class:`qibo.core.hamiltonians.SymbolicHamiltonian`.
445+
Defaults to ``True``.
446+
backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used
447+
in the execution. If ``None``, it uses the current backend.
448+
Defaults to ``None``.
449+
450+
Returns:
451+
:class:`qibo.hamiltonians.Hamiltonian` or :class:`qibo.hamiltonians.SymbolicHamiltonian`:
452+
GPP Hamiltonian :math:`H_{C}`.
453+
454+
455+
References:
456+
1. W. Aboumrad, D. Zhu, C. Girotto, F.-H. Rouet, J. Jojo, R. Lucas, J. Pathak,
457+
A. Kaushik, and M. Roetteler, *Accelerating large-scale linear algebra using
458+
variational quantum imaginary time evolution*, `arXiv:2503.13128 (2025)
459+
<https://doi.org/10.48550/arXiv.2406.16142>`_.
460+
461+
"""
462+
if len(adjacency_matrix) != len(adjacency_matrix[0]):
463+
raise_error(
464+
ValueError,
465+
"``adjacency_matrix`` must be a square matrix, "
466+
+ f"but it has shape ({len(adjacency_matrix)}, {len(adjacency_matrix[0])}).",
467+
)
468+
469+
if node_weights is not None and len(node_weights) != len(adjacency_matrix):
470+
raise_error(
471+
ValueError,
472+
"``node_weights`` and ``adjacency_matrix`` must have the same dimensions.",
473+
)
474+
475+
backend = _check_backend(backend)
476+
477+
if isinstance(adjacency_matrix, list):
478+
adjacency_matrix = backend.cast(
479+
adjacency_matrix, dtype=type(adjacency_matrix[0][0])
480+
)
481+
482+
if node_weights is not None and isinstance(node_weights, list):
483+
node_weights = backend.cast(node_weights, dtype=type(node_weights[0]))
484+
485+
if penalty_coeff != 0.0 and node_weights is None:
486+
node_weights = backend.ones(len(adjacency_matrix), dtype=backend.int8)
487+
488+
if not dense:
489+
return _gpp_symbolic(adjacency_matrix, penalty_coeff, node_weights, backend)
490+
491+
return _gpp_dense(adjacency_matrix, penalty_coeff, node_weights, backend)
492+
493+
494+
def _gpp_symbolic(adjacency_matrix, penalty_coeff, node_weights, backend):
495+
def term(index: int):
496+
return (
497+
symbols.I(index, backend=backend) - symbols.Z(index, backend=backend)
498+
) / 2
499+
500+
hamiltonian = 0
501+
rows, columns = backend.nonzero(backend.tril(adjacency_matrix, -1))
502+
for ind_j, ind_k in zip(columns, rows):
503+
ind_j, ind_k = int(ind_j), int(ind_k)
504+
x_j = term(ind_j)
505+
x_k = term(ind_k)
506+
hamiltonian += float(adjacency_matrix[ind_j, ind_k]) * (
507+
x_j + x_k - 2 * x_j * x_k
508+
)
509+
510+
if penalty_coeff != 0.0:
511+
penalty = 0
512+
for elem, weight in enumerate(node_weights):
513+
penalty += float(weight) * (
514+
term(elem) - symbols.I(elem, backend=backend) / 2
515+
)
516+
517+
hamiltonian += penalty_coeff * (penalty**2)
518+
519+
return SymbolicHamiltonian(hamiltonian, backend=backend)
520+
521+
522+
def _gpp_dense(
523+
adjacency_matrix,
524+
penalty_coeff,
525+
node_weights,
526+
backend,
527+
):
528+
def term(nqubits, ind_1, ind_2=None):
529+
diag = [id_diag] * nqubits
530+
diag[ind_1] = term_diag
531+
if ind_2 is not None:
532+
diag[ind_2] = term_diag
533+
diag = _multikron(diag, backend)
534+
535+
return diag
536+
537+
nqubits = len(adjacency_matrix)
538+
539+
id_diag = backend.cast([1, 1])
540+
term_diag = backend.cast([0, 1])
541+
542+
hamiltonian = 0
543+
rows, columns = backend.nonzero(backend.tril(adjacency_matrix, -1))
544+
for ind_j, ind_k in zip(columns, rows):
545+
ind_j, ind_k = int(ind_j), int(ind_k)
546+
547+
diag = term(nqubits, ind_j)
548+
diag += term(nqubits, ind_k)
549+
diag -= 2 * term(nqubits, ind_j, ind_k)
550+
551+
hamiltonian += diag
552+
553+
if penalty_coeff != 0.0:
554+
penalty = 0
555+
for elem, weight in enumerate(node_weights):
556+
penalty += weight * (term(nqubits, elem) - 1 / 2)
557+
558+
hamiltonian += penalty_coeff * (penalty**2)
559+
560+
return Hamiltonian(nqubits, backend.diag(hamiltonian), backend=backend)
561+
562+
409563
def _multikron(matrix_list, backend):
410564
"""Calculates Kronecker product of a list of matrices.
411565

src/qibo/models/encodings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ def sparse_encoder(
173173
174174
References:
175175
1. L. Li, and J. Luo,
176-
*Nearly Optimal Circuit Size for Sparse Quantum State Preparation*
176+
*Nearly optimal circuit size for sparse quantum state preparation*
177177
`arXiv:2406.16142 (2024) <https://doi.org/10.48550/arXiv.2406.16142>`_.
178178
179179
2. R. M. S. Farias, T. O. Maciel, G. Camilo, R. Lin, S. Ramos-Calderer, and L. Aolita,

tests/test_hamiltonians_models.py

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Tests methods from `qibo/src/hamiltonians/models.py`."""
22

3+
from functools import reduce
4+
35
import numpy as np
46
import pytest
57

68
from qibo import hamiltonians, matrices, symbols
79
from qibo.hamiltonians import MaxCut, SymbolicHamiltonian
8-
from qibo.hamiltonians.models import LABS, XXX, Heisenberg
10+
from qibo.hamiltonians.models import GPP, LABS, XXX, Heisenberg
911

1012
models_config = [
1113
("X", {"nqubits": 3}, "x_N3.out"),
@@ -111,3 +113,54 @@ def test_xxx(backend, dense):
111113
dense=dense,
112114
backend=backend,
113115
)
116+
117+
118+
@pytest.mark.parametrize("node_weights", [False, True])
119+
@pytest.mark.parametrize("is_list", [False, True])
120+
@pytest.mark.parametrize("dense", [False, True])
121+
@pytest.mark.parametrize("penalty_coeff", [0.0, 2])
122+
@pytest.mark.parametrize("nqubits", [2, 3])
123+
def test_gpp(backend, nqubits, penalty_coeff, dense, is_list, node_weights):
124+
with pytest.raises(ValueError):
125+
GPP(np.random.rand(3, 3), penalty_coeff, np.random.rand(4), backend=backend)
126+
127+
with pytest.raises(ValueError):
128+
GPP(np.random.rand(3, 2), penalty_coeff, np.random.rand(3), backend=backend)
129+
130+
adj_matrix = np.ones((nqubits, nqubits)) - np.diag(np.ones(nqubits))
131+
adj_matrix = (
132+
list(adj_matrix) if is_list else backend.cast(adj_matrix, dtype=np.int8)
133+
)
134+
135+
node_weights = [1] * nqubits if node_weights else None
136+
137+
hamiltonian = GPP(
138+
adj_matrix, penalty_coeff, node_weights, dense=dense, backend=backend
139+
)
140+
141+
term = (backend.matrices.I() - backend.matrices.Z) / 2
142+
base_string = [backend.matrices.I()] * nqubits
143+
rows, columns = backend.nonzero(backend.tril(adj_matrix, -1))
144+
target = 0
145+
for col, row in zip(columns, rows):
146+
term_col = base_string.copy()
147+
term_col[int(col)] = term
148+
term_col = reduce(backend.kron, term_col)
149+
150+
term_row = base_string.copy()
151+
term_row[int(row)] = term
152+
term_row = reduce(backend.kron, term_row)
153+
154+
target += term_row + term_col - 2 * (term_col @ term_row)
155+
156+
if penalty_coeff != 0.0:
157+
penalty = 0
158+
for elem in range(len(adj_matrix)):
159+
term_weight = base_string.copy()
160+
term_weight[elem] = term - backend.matrices.I() / 2
161+
term_weight = reduce(backend.kron, term_weight)
162+
penalty += term_weight
163+
164+
target += penalty_coeff * (penalty**2)
165+
166+
backend.assert_allclose(hamiltonian.matrix, target)

0 commit comments

Comments
 (0)