# This original code is sourced from:
# https://github.com/SRI-International/QC-App-Oriented-Benchmarks/blob/master/hhl/qiskit/sparse_Ham_sim.py
#
# Original repository link:
# https://github.com/SRI-International/QC-App-Oriented-Benchmarks?tab=Apache-2.0-1-ov-file
#
#
# This program is adapted for use as a benchmark in controlled software testing experiments.
# Modifications made to the original code include:
# - Stucture reorganization:
# - Modify the code in the `QuantumCircuit` class
# - Add type hints for better code readability
# - Remove measurements from subcircuits, as they can be added in the main circuit if needed
# - Fucntions like `_v_gate` and `_w_gate` are modified to take `QuantumCircuit` as input and
# return `QuantumCircuit` with the gate applied. Besides, they are included as methods of the
# main class.
# - Slight adjustments to the code logic for improved clarity and maintainability
# - Input validation:
# - Add input validation to ensure the Hamiltonian matrix `H` is a real-valued, Hermitian,
# 2-sparse matrix with a consistent global XOR structure and uniform coupling strengths.
#
# Copyright [yyyy] [name of copyright owner]
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from math import pi
import numpy as np
from qiskit import QuantumCircuit, QuantumRegister
# ---------- benchmark registration ----------
from ..benchmark_registry import register_benchmark
from pathlib import Path
@register_benchmark(
Path(__file__).stem,
family=Path(__file__).resolve().parent.name,
description="Evolution for sparse Hamiltonian",
class_name="SparseHamiltonian",
source={
"repo": "https://github.com/SRI-International/QC-App-Oriented-Benchmarks?tab=Apache-2.0-1-ov-file",
"file": "hhl/qiskit/sparse_Ham_sim.py",
"language": "Qiskit",
"available_doc": False
},
testability_refactoring=[
"Structure reorganization",
"Input validation" # Ensure $t$ are non-negative
]
)
def create_sparse_hamiltonian_simulation(
H: np.ndarray,
t: float,
controlled_ver: bool
):
return SparseHamiltonian(H=H, t=t, controlled_ver=controlled_ver)
[docs]
class SparseHamiltonian(QuantumCircuit):
r"""
Sparse Hamiltonian Simulation Circuit
"""
def __init__(
self, H: np.ndarray, t: float, controlled_ver: bool, name: str | None = None
):
r"""
Args:
H: the sparse Hamiltonian matrix :math:`H` of size :math:`N \times N`
where :math:`N = 2^n`, and :math:`n` is the number of qubits.
t: Evolution time :math:`t`.
controlled_ver: Whether to build the controlled version of the
Hamiltonian simulation.
name: Optional name for the circuit.
Raises:
ValueError: If ``t`` is negative.
ValueError: If ``H`` does not satisfy the required properties
(real-valued, Hermitian, 2-sparse with a consistent
global XOR structure and uniform coupling strengths).
"""
self._H = H
self._t = t
self._controlled_ver = controlled_ver
num_input_qubits = int(np.log2(len(H)))
# Validate evolution time before building the circuit
if t < 0:
raise ValueError("Evolution time t must be non-negative")
# Validate Hamiltonian matrix before building the circuit
self._validate_hamiltonian()
if controlled_ver:
# input + control + 2 ancilla
super().__init__(num_input_qubits*2 + 2, name=name or "ControlHamSim")
# Build the circuit
self.build_with_control()
else:
# input + control + ancilla
super().__init__(num_input_qubits*2 + 1, name=name or "HamSim")
# Build the circuit
self.build_without_control()
def _validate_hamiltonian(self) -> None:
H = self._H
controlled_ver = self._controlled_ver
# Type check
if not isinstance(H, np.ndarray):
raise TypeError("H must be a numpy.ndarray")
# Shape check
if H.ndim != 2 or H.shape[0] != H.shape[1]:
raise ValueError("H must be a square matrix")
N = H.shape[0]
logN = np.log2(N)
if not logN.is_integer():
raise ValueError("Dimension of H must be a power of 2 (2^n x 2^n)")
# Real-valued and Hermitian check
if np.iscomplexobj(H):
raise ValueError("Complex-valued Hamiltonians are not supported")
if not np.allclose(H, H.T):
raise ValueError("H must be Hermitian (real symmetric)")
# Sparsity and structure checks
off_diag_indices = []
for i in range(N):
nz = np.nonzero(H[i])[0]
nz_off = [j for j in nz if j != i]
if len(nz_off) > 1:
raise ValueError(
f"Row {i} has more than one off-diagonal nonzero entry; "
"H must be 2-sparse"
)
if len(nz_off) == 1:
off_diag_indices.append((i, nz_off[0]))
if not off_diag_indices:
raise ValueError("H has no off-diagonal terms; trivial Hamiltonian")
# Global XOR structure check
i0, j0 = off_diag_indices[0]
k = i0 ^ j0
for i, j in off_diag_indices:
if j != (i ^ k):
raise ValueError(
"Off-diagonal structure is not globally consistent: "
"expected j = i XOR k for all rows"
)
# Uniform coupling strength check
ref_val = H[i0, j0]
for i, j in off_diag_indices:
if not np.isclose(H[i, j], ref_val):
raise ValueError(
"All off-diagonal coupling strengths must be identical"
)
# Diagonal constraints check
diag = np.diag(H)
if not controlled_ver:
if not np.allclose(diag, 0.0):
raise ValueError(
"Non-controlled version ignores diagonal terms; "
"all diagonal entries of H must be zero"
)
[docs]
def V_gate(
self,
qc: QuantumCircuit,
H: np.ndarray,
qreg_a: QuantumRegister,
qreg_b: QuantumRegister
) -> QuantumCircuit:
r"""
Hamiltonian oracle :math:`V\ket{a,b} = \ket{a,b+v(a)}`
Args:
qc : Quantum Circuit
H : Sparse matrix
qreg_a : Quantum register
qreg_b : Quantum register
"""
# index of non-zero off diagonal in first row of H
n = qreg_a.size
N = len(H)
for i in range(1,N):
if H[0,i] != 0:
break
i_bin = np.binary_repr(i, width=n)
for q in range(n):
a, b = qreg_a[q], qreg_b[q]
if i_bin[n-1-q] == '1':
qc.x(a)
qc.cx(a,b)
qc.x(a)
else:
qc.cx(a,b)
return qc
[docs]
def W_gate(self, qc: QuantumCircuit, q0: QuantumRegister, q1: QuantumRegister) -> QuantumCircuit:
r"""
Args:
qc : QuantumCircuit
q0 : QuantumRegister as the first qubit
q1 : QuantumRegister as the second qubit
"""
qc.rz(-pi, q1)
qc.rz(-3 * pi / 2, q0)
qc.ry(-pi / 2, q1)
qc.ry(-pi / 2, q0)
qc.rz(-pi, q1)
qc.rz(-3 * pi / 2, q0)
qc.cx(q0, q1)
qc.rz(-7 * pi / 4, q1)
qc.rx(-pi / 2, q0)
qc.rx(-pi, q1)
qc.rz(-3 * pi / 2, q0)
qc.cx(q0, q1)
qc.ry(-pi / 2, q1)
qc.rx(-pi / 4, q1)
qc.cx(q1, q0)
qc.rz(-3 * pi / 2, q0)
return qc
[docs]
def build_without_control(self):
"""
Non-controlled Hamiltonian simulation circuit, which implements
:math:`e^{-i H t}` on the input register.
"""
# read in number of qubits
H = self._H
t = self._t
N = len(H)
n = int(np.log2(N))
# read in matrix elements
#diag_el = H[0,0]
for j in range(1,N):
if H[0,j] != 0:
off_diag_el = H[0,j]
break
j_bin = np.binary_repr(j, width=n)
sign = (-1)**((j_bin.count('1'))%2)
# create registers
qreg_a = QuantumRegister(n, name='q_a')
# creg = ClassicalRegister(n, name='c')
qreg_b = QuantumRegister(n, name='q_b') # ancilla register
anc_reg = QuantumRegister(1, name='q_anc')
qc = QuantumCircuit(qreg_a, qreg_b, anc_reg)
# apply sparse H oracle gate
qc = self.V_gate(qc, H, qreg_a, qreg_b)
# apply W gate that diagonalizes SWAP operator and Toffoli
for q in range(n):
qc = self.W_gate(qc, qreg_a[q], qreg_b[q])
qc.x(qreg_b[q])
qc.ccx(qreg_a[q], qreg_b[q], anc_reg[0])
# phase
qc.p(sign*2*t*off_diag_el, anc_reg[0])
# uncompute
for q in range(n):
j = n-1-q
qc.ccx(qreg_a[j], qreg_b[j], anc_reg[0])
qc.x(qreg_b[j])
qc = self.W_gate(qc, qreg_a[j], qreg_b[j])
# sparse H oracle gate is its own inverse
qc = self.V_gate(qc, H, qreg_a, qreg_b)
# measure
#qc.barrier()
#qc.measure(qreg_a[0:], creg[0:])
self.compose(qc, inplace=True)
[docs]
def build_with_control(self):
"""
Controlled Hamiltonian simulation circuit
"""
# read in number of qubits
H = self._H
t = self._t
N = len(H)
n = int(np.log2(N))
qreg = QuantumRegister(n)
qreg_b = QuantumRegister(n)
control_reg = QuantumRegister(1)
anc_reg = QuantumRegister(1)
qc = QuantumCircuit(qreg, qreg_b, control_reg, anc_reg)
control = control_reg[0]
anc = anc_reg[0]
# read in matrix elements
diag_el = H[0,0]
for j in range(1,N):
if H[0,j] != 0:
off_diag_el = H[0,j]
break
j_bin = np.binary_repr(j, width=n)
sign = (-1)**((j_bin.count('1'))%2)
# use ancilla for phase kickback to simulate diagonal part of H
qc.x(anc)
qc.cp(-t*(diag_el+sign*off_diag_el), control, anc)
qc.x(anc)
# apply sparse H oracle gate
qc = self.V_gate(qc, H, qreg, qreg_b)
# apply W gate that diagonalizes SWAP operator and Toffoli
for q in range(n):
qc = self.W_gate(qc, qreg[q], qreg_b[q])
qc.x(qreg_b[q])
qc.ccx(qreg[q], qreg_b[q], anc)
# phase
qc.cp((sign*2*t*off_diag_el), control, anc)
# uncompute
for q in range(n):
j = n-1-q
qc.ccx(qreg[j], qreg_b[j], anc)
qc.x(qreg_b[j])
qc = self.W_gate(qc, qreg[j], qreg_b[j])
# sparse H oracle gate is its own inverse
qc = self.V_gate(qc, H, qreg, qreg_b)
self.compose(qc, inplace=True)