@@ -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+
409563def _multikron (matrix_list , backend ):
410564 """Calculates Kronecker product of a list of matrices.
411565
0 commit comments