from qiskit.circuit.library.blueprintcircuit import BlueprintCircuit
from qiskit.exceptions import QiskitError
from qiskit.circuit import QuantumCircuit, Parameter, QuantumRegister
from qiskit_nature.second_q.mappers import QubitMapper
from typing import Optional
import numpy as np
[docs]
class GateFabric(BlueprintCircuit):
"""Gate Fabric ansatz implementation for Qiskit."""
def __init__(
self,
num_spatial_orbitals: int,
num_particles: tuple,
qubit_mapper: QubitMapper,
initial_state: Optional[QuantumCircuit] = None,
num_layers: int = 1,
include_pi: bool = False,
name: str | None = "GateFabric",
) -> None:
"""
Args:
num_spatial_orbitals: Number of spatial orbitals
num_particles: Tuple of (alpha, beta) electrons
qubit_mapper: Mapping of fermionic to qubit operators
initial_state: Initial state circuit (should be provided externally)
num_layers: Number of Gate Fabric layers
include_pi: Whether to include pi rotations in the ansatz
"""
super().__init__(name=name)
[docs]
self._num_qubits = 2 * num_spatial_orbitals
[docs]
self._num_spatial_orbitals = num_spatial_orbitals
[docs]
self._num_particles = num_particles
[docs]
self._qubit_mapper = qubit_mapper
[docs]
self._num_layers = num_layers
[docs]
self._include_pi = include_pi
if self.num_qubits == 0:
self.qregs = []
else:
self.qregs = [QuantumRegister(self.num_qubits, name="q")]
# Use provided initial state, default to an empty circuit
[docs]
self._initial_state = initial_state if initial_state is not None else QuantumCircuit(self.num_qubits)
@property
[docs]
def qubit_mapper(self) -> QubitMapper | None:
"""The qubit operator mapper."""
return self._qubit_mapper
@qubit_mapper.setter
def qubit_mapper(self, mapper: QubitMapper | None) -> None:
"""Sets the qubit operator mapper."""
self._invalidate()
self._qubit_mapper = mapper
@property
[docs]
def num_qubits(self) -> int:
"""The number of qubits."""
return self._num_qubits
@num_qubits.setter
def num_qubits(self, n: int) -> None:
"""Sets the number of qubits."""
self._invalidate()
self._num_qubits = n
@property
[docs]
def num_spatial_orbitals(self) -> int:
"""The number of spatial orbitals."""
return self._num_spatial_orbitals
@num_spatial_orbitals.setter
def num_spatial_orbitals(self, n: int) -> None:
"""Sets the number of spatial orbitals."""
self._invalidate()
self._num_spatial_orbitals = n
@property
[docs]
def num_particles(self) -> tuple[int, int]:
"""The number of particles."""
return self._num_particles
@num_particles.setter
def num_particles(self, n: tuple[int, int]) -> None:
"""Sets the number of particles."""
self._invalidate()
self._num_particles = n
[docs]
def _check_configuration(self, raise_on_failure: bool = True) -> bool:
"""Check if the configuration of the NLocal class is valid.
Args:
raise_on_failure: Whether to raise on failure.
Returns:
True, if the configuration is valid and the circuit can be constructed. Otherwise
an ValueError is raised.
Raises:
ValueError: If the numbr fo qubit is not set.
ValueError: If the number of spatial orbitals is lower than the number of particles
"""
valid = True
if self.num_qubits is None:
valid = False
if raise_on_failure:
raise ValueError("No number of qubits specified.")
# check no needed parameters are None
if self._num_spatial_orbitals < self._num_particles[0] or self._num_spatial_orbitals < self._num_particles[1]:
valid = False
if raise_on_failure :
raise ValueError("Number of spatial orbitals inferior to number of particles.")
return valid
[docs]
def _build(self) -> None:
"""Builds the Gate Fabric circuit
"""
if self._is_built:
return
super()._build()
# Create circuit
circuit = QuantumCircuit(self.num_qubits, name=self.name)
# Apply initial state
circuit.compose(self._initial_state.copy(), inplace=True, copy=False)
# Create parameters
parameters = []
for l in range(self._num_layers):
layer_params = []
for g in range(self.num_qubits // 2 - 1):
theta = Parameter(f'θ_{l}_{g}')
phi = Parameter(f'φ_{l}_{g}')
layer_params.append([theta, phi])
parameters.append(layer_params)
# Apply Gate Fabric layers
for layer in range(self._num_layers):
self._gate_fabric_layer(circuit, parameters[layer], range(self.num_qubits))
# append to self
try:
block = circuit.to_gate()
except QiskitError:
block = circuit.to_instruction()
self.append(block, range(self.num_qubits), copy=False)
[docs]
def _gate_fabric_layer(self, circuit: QuantumCircuit, parameters: list, wires: list) -> None:
"""Implements a single layer of Gate Fabric"""
n_gates = len(wires) // 2 - 1
for i in range(n_gates):
qubits = wires[i : i + 4]
if self._include_pi:
self._orbital_rotation( np.pi, qubits)
self._double_excitation(circuit, parameters[i][0], qubits)
self._orbital_rotation(circuit, parameters[i][1], qubits)
[docs]
def _orbital_rotation(self, circuit: QuantumCircuit, theta: Parameter, qubits: list) -> None:
"""Implements the orbital rotation gate"""
circuit.ry(theta / 2, qubits[1])
circuit.cx(qubits[1], qubits[0])
circuit.ry(-theta / 2, qubits[1])
circuit.cx(qubits[1], qubits[0])
circuit.ry(theta / 2, qubits[3])
circuit.cx(qubits[3], qubits[2])
circuit.ry(-theta / 2, qubits[3])
circuit.cx(qubits[3], qubits[2])
[docs]
def _double_excitation(self, circuit: QuantumCircuit, phi: Parameter, qubits: list) -> None:
"""Implements the double excitation gate"""
circuit.cx(qubits[3], qubits[2])
circuit.cx(qubits[2], qubits[1])
circuit.cx(qubits[1], qubits[0])
circuit.rz(phi, qubits[0])
circuit.cx(qubits[1], qubits[0])
circuit.cx(qubits[2], qubits[1])
circuit.cx(qubits[3], qubits[2])