Skip to content

Commit

Permalink
feat: implements the PUCCSD ansatz
Browse files Browse the repository at this point in the history
  • Loading branch information
mrossinek committed Sep 4, 2023
1 parent 9b48d54 commit 78f0a6e
Show file tree
Hide file tree
Showing 6 changed files with 300 additions and 8 deletions.
3 changes: 3 additions & 0 deletions qiskit_nature/second_q/circuit/library/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
UCC
UCCSD
PUCCD
PUCCSD
SUCCD
CHC
UVCC
Expand All @@ -73,6 +74,7 @@
UCC,
UCCSD,
PUCCD,
PUCCSD,
SUCCD,
CHC,
UVCC,
Expand All @@ -87,6 +89,7 @@
"UCC",
"UCCSD",
"PUCCD",
"PUCCSD",
"SUCCD",
"HartreeFock",
"CHC",
Expand Down
2 changes: 2 additions & 0 deletions qiskit_nature/second_q/circuit/library/ansatzes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from .chc import CHC
from .puccd import PUCCD
from .puccsd import PUCCSD
from .succd import SUCCD
from .ucc import UCC
from .uccsd import UCCSD
Expand All @@ -22,6 +23,7 @@
__all__ = [
"CHC",
"PUCCD",
"PUCCSD",
"SUCCD",
"UCC",
"UCCSD",
Expand Down
172 changes: 172 additions & 0 deletions qiskit_nature/second_q/circuit/library/ansatzes/puccsd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""
The spin-adapted paired-UCC ansatz.
"""

from __future__ import annotations

import logging
from typing import Sequence, cast
from collections import defaultdict

from qiskit.circuit import QuantumCircuit
from qiskit_nature.second_q.mappers import QubitConverter, QubitMapper
from qiskit_nature.second_q.operators import FermionicOp

from .ucc import UCC
from .utils.fermionic_excitation_generator import (
generate_fermionic_excitations,
get_alpha_excitations,
)

logger = logging.getLogger(__name__)


class PUCCSD(UCC):
"""The spin-adapted paired-UCC Ansatz.
This ansatz (by default) contains paired single and double excitations. This ensures that not
only the number of particles but also the spin is preserved. [1]
Note, that this ansatz will produce a generalized operator pool (``generalized=True``).
This is a convenience subclass of the UCC ansatz. For more information refer to :class:`UCC`.
References:
[1] `arXiv:2207.00085 <https://arxiv.org/abs/2207.00085>`_
"""

def __init__(
self,
num_spatial_orbitals: int | None = None,
num_particles: tuple[int, int] | None = None,
qubit_mapper: QubitConverter | QubitMapper | None = None,
*,
reps: int = 1,
initial_state: QuantumCircuit | None = None,
) -> None:
# pylint: disable=unused-argument
"""
Args:
num_spatial_orbitals: The number of spatial orbitals.
num_particles: The tuple of the number of alpha- and beta-spin particles.
qubit_mapper: The :class:`~qiskit_nature.second_q.mappers.QubitMapper` or
:class:`~qiskit_nature.second_q.mappers.QubitConverter` instance (use of the latter
is deprecated) which takes care of mapping to a qubit operator.
reps: The number of times to repeat the evolved operators.
initial_state: A ``QuantumCircuit`` object to prepend to the circuit.
"""
self._excitations_dict: dict[
tuple[tuple[int, ...], tuple[int, ...]], list[tuple[tuple[int, ...], tuple[int, ...]]]
] | None = None
super().__init__(
num_spatial_orbitals=num_spatial_orbitals,
num_particles=num_particles,
excitations=self.generate_excitations,
qubit_mapper=qubit_mapper,
alpha_spin=True,
beta_spin=True,
max_spin_excitation=None,
generalized=True,
include_imaginary=False,
reps=reps,
initial_state=initial_state,
)

def generate_excitations(
self, num_spatial_orbitals: int, num_particles: tuple[int, int]
) -> list[tuple[tuple[int, ...], tuple[int, ...]]]:
"""Generates the excitations for the PUCCSD Ansatz.
Args:
num_spatial_orbitals: the number of spatial orbitals.
num_particles: the number of alpha and beta electrons. Note, these must be identical for
this class.
Returns:
The list of excitations encoded as tuples of tuples. Each tuple in the list is a pair of
tuples. The first tuple contains the occupied spin orbital indices whereas the second
one contains the indices of the unoccupied spin orbitals.
"""
excitations: list[tuple[tuple[int, ...], tuple[int, ...]]] = []
excitations.extend(
generate_fermionic_excitations(
1,
num_spatial_orbitals,
num_particles,
alpha_spin=True,
beta_spin=False,
generalized=True,
)
)

num_electrons = num_particles[0]
beta_index_shift = num_spatial_orbitals

# generate alpha-spin orbital indices for occupied and unoccupied ones
alpha_excitations = get_alpha_excitations(
num_spatial_orbitals, num_electrons, generalized=True
)
logger.debug("Generated list of single alpha excitations: %s", alpha_excitations)

for alpha_exc in alpha_excitations:
# create the beta-spin excitation by shifting into the upper block-spin orbital indices
beta_exc = (
alpha_exc[0] + beta_index_shift,
alpha_exc[1] + beta_index_shift,
)
# add the excitation tuple
occ: tuple[int, ...]
unocc: tuple[int, ...]
occ, unocc = zip(alpha_exc, beta_exc)
exc_tuple = (occ, unocc)
excitations.append(exc_tuple)
logger.debug("Added the excitation: %s", exc_tuple)

return excitations

def _build_fermionic_excitation_ops(self, excitations: Sequence) -> list[FermionicOp]:
"""Builds all possible excitation operators with the given number of excitations for the
specified number of particles distributed in the number of orbitals.
Args:
excitations: the list of excitations.
Returns:
The list of excitation operators in the second quantized formalism.
"""
operators: list[FermionicOp] = []
self._excitations_dict = defaultdict(list)
beta_index_shift = self.num_spatial_orbitals

# Reform the excitations list to a dictionary. Each items in the dictionary
# corresponds to a parameter.
for exc in excitations:
if len(exc[0]) == 1:
# single excitation
self._excitations_dict[exc].append(exc)
self._excitations_dict[exc].append(
((exc[0][0] + beta_index_shift,), (exc[1][0] + beta_index_shift,))
)
elif len(exc[0]) == 2:
# double excitation
self._excitations_dict[exc].append(exc)

for exc_list in self._excitations_dict.values():
sum_ops = cast(FermionicOp, sum(super()._build_fermionic_excitation_ops(exc_list)))
operators.append(sum_ops)

return operators
17 changes: 9 additions & 8 deletions qiskit_nature/second_q/circuit/library/ansatzes/ucc.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,14 +414,15 @@ def _check_ucc_configuration(self, raise_on_failure: bool = True) -> bool:
)
return False

if any(n >= self.num_spatial_orbitals for n in self.num_particles):
if raise_on_failure:
raise ValueError(
f"The number of spatial orbitals {self.num_spatial_orbitals}"
f"must be greater than number of particles of any spin kind "
f"{self.num_particles}."
)
return False
if not self._generalized:
if any(n >= self.num_spatial_orbitals for n in self.num_particles):
if raise_on_failure:
raise ValueError(
f"The number of spatial orbitals {self.num_spatial_orbitals}"
f"must be greater than number of particles of any spin kind "
f"{self.num_particles}."
)
return False

if self.excitations is None:
if raise_on_failure:
Expand Down
20 changes: 20 additions & 0 deletions releasenotes/notes/puccsd-ansatz-7c97be6dca32a873.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
features:
- |
Adds a new convenience subclass of the :class:`.UCC` ansatz. Namely, the
spin-symmetry-adapted ansatz, :class:`.PUCCSD`, which includes single and double
excitations while always pairing the excitations such that both, the number of
particles and the total spin, will be preserved.
You can use it like any of the other :class:`.UCC`-style ansätze, for example:
.. code-block:: python
from qiskit_nature.second_q.circuit.library import PUCCSD
from qiskit_nature.second_q.mappers import JordanWignerMapper
ansatz = PUCCSD(
num_spatial_orbitals=4,
num_particles=(2, 2),
qubit_mapper=JordanWignerMapper(),
)
94 changes: 94 additions & 0 deletions test/second_q/circuit/library/ansatzes/test_puccsd.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Test the PUCCSD Ansatz."""

import unittest
from test import QiskitNatureTestCase
from test.second_q.circuit.library.ansatzes.test_ucc import assert_ucc_like_ansatz

from ddt import data, ddt, unpack

from qiskit_nature.second_q.circuit.library import PUCCSD
from qiskit_nature.second_q.mappers import JordanWignerMapper
from qiskit_nature.second_q.operators import FermionicOp


@ddt
class TestPUCCSD(QiskitNatureTestCase):
"""Tests for the PUCCSD Ansatz."""

@unpack
@data(
(
2,
(1, 1),
[
FermionicOp(
{"+_0 -_1": 1j, "+_1 -_0": -1j, "+_2 -_3": 1j, "+_3 -_2": -1j},
num_spin_orbitals=4,
),
FermionicOp({"+_0 +_2 -_1 -_3": 1j, "+_3 +_1 -_2 -_0": -1j}, num_spin_orbitals=4),
],
),
(
4,
(2, 2),
[
FermionicOp(
{"+_0 -_1": 1j, "+_1 -_0": -1j, "+_4 -_5": 1j, "+_5 -_4": -1j},
num_spin_orbitals=8,
),
FermionicOp(
{"+_0 -_2": 1j, "+_2 -_0": -1j, "+_4 -_6": 1j, "+_6 -_4": -1j},
num_spin_orbitals=8,
),
FermionicOp(
{"+_0 -_3": 1j, "+_3 -_0": -1j, "+_4 -_7": 1j, "+_7 -_4": -1j},
num_spin_orbitals=8,
),
FermionicOp(
{"+_1 -_2": 1j, "+_2 -_1": -1j, "+_5 -_6": 1j, "+_6 -_5": -1j},
num_spin_orbitals=8,
),
FermionicOp(
{"+_1 -_3": 1j, "+_3 -_1": -1j, "+_5 -_7": 1j, "+_7 -_5": -1j},
num_spin_orbitals=8,
),
FermionicOp(
{"+_2 -_3": 1j, "+_3 -_2": -1j, "+_6 -_7": 1j, "+_7 -_6": -1j},
num_spin_orbitals=8,
),
FermionicOp({"+_0 +_4 -_1 -_5": 1j, "+_5 +_1 -_4 -_0": -1j}, num_spin_orbitals=8),
FermionicOp({"+_0 +_4 -_2 -_6": 1j, "+_6 +_2 -_4 -_0": -1j}, num_spin_orbitals=8),
FermionicOp({"+_0 +_4 -_3 -_7": 1j, "+_7 +_3 -_4 -_0": -1j}, num_spin_orbitals=8),
FermionicOp({"+_1 +_5 -_2 -_6": 1j, "+_6 +_2 -_5 -_1": -1j}, num_spin_orbitals=8),
FermionicOp({"+_1 +_5 -_3 -_7": 1j, "+_7 +_3 -_5 -_1": -1j}, num_spin_orbitals=8),
FermionicOp({"+_2 +_6 -_3 -_7": 1j, "+_7 +_3 -_6 -_2": -1j}, num_spin_orbitals=8),
],
),
)
def test_puccd_ansatz(self, num_spatial_orbitals, num_particles, expect):
"""Tests the PUCCSD Ansatz."""
mapper = JordanWignerMapper()

ansatz = PUCCSD(
qubit_mapper=mapper,
num_particles=num_particles,
num_spatial_orbitals=num_spatial_orbitals,
)

assert_ucc_like_ansatz(self, ansatz, num_spatial_orbitals, expect)


if __name__ == "__main__":
unittest.main()

0 comments on commit 78f0a6e

Please sign in to comment.