Source code for qolumbina.programs.hamiltonian.sparse_hamiltonian

# 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)