# (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.
"""Hop gate ansatz."""
from __future__ import annotations
import itertools
from dataclasses import dataclass
from typing import cast
import numpy as np
from ffsim.gates import apply_hop_gate, apply_orbital_rotation
from ffsim.variational.util import (
orbital_rotation_from_parameters,
orbital_rotation_to_parameters,
)
[docs]
@dataclass(frozen=True)
class HopGateAnsatzOperator:
"""A hop gate ansatz operator.
The hop gate ansatz consists of a sequence of `hop gates`_.
Note that this ansatz does not implement any interactions between spin alpha and
spin beta orbitals. It was designed to be used with `entanglement forging`_.
Attributes:
norb (int): The number of spatial orbitals.
interaction_pairs (list[tuple[int, int]]): The orbital pairs to apply the hop
gates to.
thetas (np.ndarray): The rotation angles for the hop gates.
final_orbital_rotation (np.ndarray): An optional final orbital rotation to
append to the ansatz, used to optimize the orbital basis.
.. _hop gates: ffsim.html#ffsim.apply_hop_gate
.. _entanglement forging: https://journals.aps.org/prxquantum/abstract/10.1103/PRXQuantum.3.010309
"""
norb: int
interaction_pairs: list[tuple[int, int]]
thetas: np.ndarray
final_orbital_rotation: np.ndarray | None = None
def _apply_unitary_(
self, vec: np.ndarray, norb: int, nelec: int | tuple[int, int], copy: bool
) -> np.ndarray:
"""Apply the operator to a vector."""
if copy:
vec = vec.copy()
for target_orbs, theta in zip(
itertools.cycle(self.interaction_pairs), self.thetas
):
vec = apply_hop_gate(
vec, theta, target_orbs=target_orbs, norb=norb, nelec=nelec, copy=False
)
if self.final_orbital_rotation is not None:
vec = apply_orbital_rotation(
vec,
mat=self.final_orbital_rotation,
norb=norb,
nelec=nelec,
copy=False,
)
return vec
[docs]
def to_parameters(self) -> np.ndarray:
"""Convert the operator to a real-valued parameter vector."""
num_params = len(self.thetas)
if self.final_orbital_rotation is not None:
num_params += self.norb**2
params = np.zeros(num_params)
params[: len(self.thetas)] = self.thetas
if self.final_orbital_rotation is not None:
params[len(self.thetas) :] = orbital_rotation_to_parameters(
self.final_orbital_rotation
)
return params
[docs]
@staticmethod
def from_parameters(
params: np.ndarray,
norb: int,
interaction_pairs: list[tuple[int, int]],
with_final_orbital_rotation: bool = False,
) -> HopGateAnsatzOperator:
"""Initialize the operator from a real-valued parameter vector.
Args:
params: The real-valued parameter vector.
norb: The number of spatial orbitals.
interaction_pairs: The orbital pairs to apply the hop gates to.
with_final_orbital_rotation: Whether to include a final orbital rotation
in the ansatz operator.
"""
final_orbital_rotation = None
if with_final_orbital_rotation:
final_orbital_rotation = orbital_rotation_from_parameters(
params[-(norb**2) :], norb
)
params = params[: -(norb**2)]
return HopGateAnsatzOperator(
norb=norb,
interaction_pairs=interaction_pairs,
thetas=params,
final_orbital_rotation=final_orbital_rotation,
)
def _approx_eq_(self, other, rtol: float, atol: float) -> bool:
if isinstance(other, HopGateAnsatzOperator):
if self.norb != other.norb:
return False
if self.interaction_pairs != other.interaction_pairs:
return False
if not np.allclose(self.thetas, other.thetas, rtol=rtol, atol=atol):
return False
if (self.final_orbital_rotation is None) != (
other.final_orbital_rotation is None
):
return False
if self.final_orbital_rotation is not None:
return np.allclose(
cast(np.ndarray, self.final_orbital_rotation),
cast(np.ndarray, other.final_orbital_rotation),
rtol=rtol,
atol=atol,
)
return True
return NotImplemented