bowtie_qgt.bowtieqgt

Bowtie Quantum Geometric Tensor (QGT) computation module.

This module provides efficient computation of the Quantum Geometric Tensor (QGT), energy gradients, and variance for parameterized quantum circuits using the “bowtie” method. The bowtie approach leverages light-cone structures to reduce computational overhead by focusing only on relevant qubits for each parameter and observable term.

The main class, computes: - Quantum Geometric Tensor (QGT) for variational quantum algorithms - Energy gradients with respect to circuit parameters - Observable variance (optional) - Support for both real (VarQITE) and imaginary time evolution gradients

Key Features: - Sparse tensor operations for efficient overlap computation - Parallel statevector simulation using Qiskit Aer - Automatic identification of active qubits per parameter/observable - Phase fixing for improved numerical stability - GPU acceleration support via Qiskit Aer

Example

>>> from qiskit import QuantumCircuit
>>> from qiskit.quantum_info import SparsePauliOp
>>> from bowtie_qgt.bowtieqgt import BowtieQGT
>>>
>>> # Create a parameterized circuit
>>> qc = QuantumCircuit(4)
>>> # ... add parameterized gates ...
>>>
>>> # Define an observable
>>> obs = SparsePauliOp.from_list([("ZIII", 1.0), ("IZII", 1.0)])
>>>
>>> # Initialize BowtieQGT
>>> bowtie = BowtieQGT(qc, obs, phase_fix=True)
>>>
>>> # Compute QGT and energy at parameter values
>>> params = {p: 0.1 for p in qc.parameters}
>>> gen_qgt, energy = bowtie.get_derivatives(params)
>>>
>>> # Extract QGT and gradient
>>> qgt = bowtie.extract_qgt(gen_qgt)
>>> gradient = bowtie.extract_gradient(gen_qgt)
bowtie_qgt.bowtieqgt.zeroth(tensor)[source]

Returns tensor[0,0,0,0,…] of a given ndarray

bowtie_qgt.bowtieqgt.get_slice(AB, BC)[source]

Generate slice indices for projecting ψ_AB onto the overlap region with ψ_BC.

Computes the slice to apply to statevector ψ_AB when computing its overlap with ψ_BC. The Hilbert space is decomposed into three regions: - Region A: qubits where only ψ_AB has support (projected to |0⟩) - Region B: qubits where both ψ_AB and ψ_BC have support (overlap computed) - Region C: qubits where only ψ_BC has support (projected to |0⟩)

For qubits in region B (intersection of AB and BC), the slice uses slice(None) to keep all amplitudes. For qubits in region A (in AB but not in BC), the slice uses index 0 to project onto |0⟩.

Parameters:
  • AB (tuple[int]) – Tuple of active qubit indices for statevector ψ_AB.

  • BC (tuple[int]) – Tuple of active qubit indices for statevector ψ_BC.

Returns:

  • slice(None) for qubits in the overlap region B (qubits in both AB and BC)

  • 0 for qubits in region A (qubits in AB but not in BC)

The list is in reversed order to match tensor dimension ordering.

Return type:

List of slice objects or integers for indexing the ψ_AB tensor

Note

When precomputing slices for overlap ⟨ψ_AB|ψ_BC⟩, both directions must be computed and stored: one slice for ψ_AB and one for ψ_BC, since the overlap is not symmetric when the active qubit sets differ.

Example

>>> # ψ_AB has support on qubits (0, 1, 2), ψ_BC on qubits (1, 2, 3,4)
>>> # Region A = {0}, Region B = {1, 2}, Region C = {3,4}
>>> get_slice((0, 1, 2), (1, 2, 3, 4))
[slice(None, None, None), slice(None, None, None), 0]
>>> # Qubit 0 (region A) → 0, qubits 1,2 (region B) → slice(None)
bowtie_qgt.bowtieqgt.sparse_overlap_tensors(svAB, sliceab, svBC, slicebc)[source]

Compute the overlap ⟨ψ_AB|ψ_BC⟩ between statevectors with different support.

Calculates the inner product between two statevectors by projecting them onto their common support region (region B). The Hilbert space is decomposed as: - Region A: qubits where only ψ_AB has support → projected to |0⟩ in ψ_AB - Region B: qubits where both have support → full overlap computed - Region C: qubits where only ψ_BC has support → projected to |0⟩ in ψ_BC

The slices sliceab and slicebc project each statevector onto region B, setting amplitudes in regions A and C to their |0⟩ components.

Parameters:
  • svAB (ndarray) – Statevector tensor ψ_AB reshaped to [2, 2, …, 2] with dimensions corresponding to its active qubits.

  • sliceab – Slice for projecting ψ_AB onto region B.

  • svBC (ndarray) – Statevector tensor ψ_BC reshaped to [2, 2, …, 2] with dimensions corresponding to its active qubits.

  • slicebc – Slice for projecting ψ_BC onto region B.

Returns:

Complex number representing the overlap ⟨ψ_AB|ψ_BC⟩ computed over the common support region B.

Note

This is the core operation for computing QGT matrix entries. The sparse tensor approach avoids explicitly constructing full statevectors on the entire Hilbert space, significantly reducing memory and computation.

bowtie_qgt.bowtieqgt.tensor_phase_fix(svAB, sliceab, svBC, slicebc, phase_fix)[source]

Compute overlap with optional phase fixing correction.

Calculates the overlap between two statevector tensors with an optional phase correction term. The phase fix removes the contribution from the |0…0⟩ computational basis state to improve numerical stability.

Parameters:
  • svAB – First statevector tensor.

  • sliceab – Slice indices for svAB.

  • svBC – Second statevector tensor.

  • slicebc – Slice indices for svBC.

  • phase_fix (bool) – Boolean or numeric value. If True, applies phase correction by subtracting the product of |0…0⟩ amplitudes.

Returns:

Complex number representing the phase-corrected overlap.

class bowtie_qgt.bowtieqgt.BowtieQGT(qc, obs, phase_fix=True, pbar=0, verbose_init=False, accelerator='CPU', compute_variance=False, VarQITE_gradient=False)[source]

Bases: object

Efficient Quantum Geometric Tensor computation using the bowtie method.

This class computes the generalized QGT matrix that includes: - QGT block: metric tensor for the parameter space - Gradient block: energy derivatives with respect to parameters - Variance block: observable variance (optional)

The bowtie method constructs auxiliary circuits (bowties) for each parameter and observable term, focusing computation only on relevant qubits determined by light-cone analysis. This significantly reduces computational cost for large quantum circuits.

qc

The parameterized quantum circuit.

obs

Observable as a SparsePauliOp for energy/gradient computation.

phase_fix

Whether phase fixing is enabled.

pbar

Progress bar verbosity level (0=off, 1=minimal, 2=detailed).

accelerator

Device for simulation (“CPU” or “GPU”).

VarQITE_gradient

If True, computes real gradients (VarQITE style); if False, computes imaginary gradients.

simulator

Qiskit Aer statevector simulator instance.

bowties

List of all bowtie circuits (parameters + observables).

active_qubits

Tuple of active qubit indices for each bowtie.

non_zero_indices

List of (i,j) index pairs with non-zero overlaps.

slices

Precomputed slice objects for efficient tensor indexing.

Example

>>> qc = QuantumCircuit(4)
>>> # ... build parameterized circuit ...
>>> obs = SparsePauliOp.from_list([("ZIII", 1.0)])
>>> bowtie = BowtieQGT(qc, obs, phase_fix=True, pbar=1)
>>> params = {p: 0.5 for p in qc.parameters}
>>> gen_qgt, energy = bowtie.get_derivatives(params)
__init__(qc, obs, phase_fix=True, pbar=0, verbose_init=False, accelerator='CPU', compute_variance=False, VarQITE_gradient=False)[source]

Initialize the BowtieQGT calculator.

Parameters:
  • qc (QuantumCircuit) – Parameterized quantum circuit to analyze.

  • obs (SparsePauliOp) – Observable as SparsePauliOp for computing energy and gradients.

  • phase_fix (bool) – If True, applies phase correction. Recommended for most cases. Default: True.

  • pbar (int) – Progress bar verbosity level: - 0: No progress bars - 1: Show main computation progress - 2: Show detailed progress including QGT computation Default: 0.

  • verbose_init (bool) – If True, prints distribution statistics of bowtie circuit sizes during initialization. Default: False.

  • accelerator (str) – Simulation device, either “CPU” or “GPU”. GPU requires appropriate Qiskit Aer GPU support. Default: “CPU”.

  • VarQITE_gradient (bool) – If True, computes real-time gradients (VarQITE); if False, computes imaginary-time gradients. Default: False.

Raises:

ValueError – If a parameter is not found in the circuit during bowtie construction.

Note

Initialization performs the following steps: 1. Constructs bowtie circuits for each parameter 2. Constructs bowtie circuits for each observable term 3. Identifies active qubits for each bowtie 4. Transpiles all circuits for the target simulator 5. Precomputes non-zero overlap indices and slice objects

get_derivatives(parameter_dict, tracking_time=False)[source]

Compute the generalized QGT matrix and energy expectation value.

This is the main computation method that evaluates all bowtie circuits at the given parameter values and constructs the generalized QGT matrix. The matrix structure is:

` ┌─────────────┬──────────────┐      QGT        Gradient     (NxN)         (NxM)      ├─────────────┼──────────────┤   Gradient†     Variance     (MxN)         (MxM)      └─────────────┴──────────────┘ `

where N = number of parameters, M = number of observable terms.

Parameters:
  • parameter_dict (dict[Parameter, float]) – Dictionary mapping circuit Parameters to their numerical values. Can be a subset of circuit parameters; unspecified parameters remain symbolic.

  • tracking_time (bool) – If True, returns timing information for profiling. Default: False.

Returns:

tuple: (gen_qgt, energy) where:
  • gen_qgt: Complex numpy array of shape (N+M, N+M) containing the generalized QGT matrix

  • energy: Complex number representing the energy expectation value ⟨ψ|H|ψ⟩

If tracking_time is True:
tuple: ((gen_qgt, energy), (time_simulation, time_qgt)) where:
  • time_simulation: Time in seconds for statevector computation

  • time_qgt: Time in seconds for QGT matrix assembly

Return type:

If tracking_time is False

Note

The generalized QGT is computed as: G_ij = Re[⟨∂_i ψ|∂_j ψ⟩ - ⟨∂_i ψ|ψ⟩⟨ψ|∂_j ψ⟩] / 4

Gradient entries include appropriate factors of -i for imaginary time evolution when VarQITE_gradient=False.

extract_qgt(gen_qgt)[source]

Extract the QGT block from the generalized QGT matrix.

Extracts the upper-left NxN block containing the Quantum Geometric Tensor (metric tensor) for the parameter space.

Parameters:

gen_qgt (ndarray) – Generalized QGT matrix of shape (N+M, N+M) returned by [get_derivatives()](bowtie_qgt/bowtieqgt.py:113).

Returns:

Complex numpy array of shape (N, N) where N is the number of circuit parameters. This is the QGT matrix G_ij = ⟨∂_i ψ|∂_j ψ⟩.

Example

>>> gen_qgt, energy = bowtie.get_derivatives(params)
>>> qgt = bowtie.extract_qgt(gen_qgt)
>>> print(qgt.shape)  # (num_parameters, num_parameters)
extract_gradient(gen_qgt)[source]

Extract the energy gradient from the generalized QGT matrix.

Extracts and processes the gradient block to compute ∂E/∂θ_i for each parameter θ_i. The gradient is computed by summing over observable terms with appropriate normalization and sign conventions.

Parameters:

gen_qgt (ndarray) – Generalized QGT matrix of shape (N+M, N+M) returned by [get_derivatives()](bowtie_qgt/bowtieqgt.py:113).

Returns:

Complex numpy array of shape (N,) containing the gradient components ∂E/∂θ_i. For VarQITE_gradient=True, returns real gradients; for VarQITE_gradient=False, returns imaginary gradients with appropriate sign convention.

Note

The factor of 4 accounts for the 1/4 normalization in the QGT computation. The sign convention depends on whether real or imaginary time evolution is being used.

Example

>>> gen_qgt, energy = bowtie.get_derivatives(params)
>>> gradient = bowtie.extract_gradient(gen_qgt)
>>> print(gradient.shape)  # (num_parameters,)
extract_variance(gen_qgt, energy=None)[source]

Extract the observable variance from the generalized QGT matrix.

Extracts the lower-right MxM block and computes the total variance Var(H) = ⟨H²⟩ - ⟨H⟩². The variance sector contains ⟨H²⟩ (with phase correction when phase_fix=True), so we must subtract ⟨H⟩² (energy²) to obtain the actual variance when the energy parameter is provided.

Parameters:
  • gen_qgt (ndarray) – Generalized QGT matrix of shape (N+M, N+M) returned by [get_derivatives()](bowtie_qgt/bowtieqgt.py:113).

  • energy (float | None) – The expectation value ⟨H⟩ of the observable. When provided, the method returns Var(H) = ⟨H²⟩ - ⟨H⟩². When not provided, returns ⟨H²⟩ (useful for backward compatibility).

Returns:

Complex number representing the variance of the observable when energy is provided, or ⟨H²⟩ when energy is not provided. For Hermitian observables, the variance should be real and non-negative.

Example

>>> # Compute variance (recommended)
>>> gen_qgt, energy = bowtie.get_derivatives(params)
>>> variance = bowtie.extract_variance(gen_qgt, energy).real
>>>
>>> # Backward compatibility: get ⟨H²⟩ without energy
>>> gen_qgt, _ = bowtie.get_derivatives(params)
>>> h2_expectation = bowtie.extract_variance(gen_qgt).real

Main Class

BowtieQGT

class bowtie_qgt.bowtieqgt.BowtieQGT(qc, obs, phase_fix=True, pbar=0, verbose_init=False, accelerator='CPU', compute_variance=False, VarQITE_gradient=False)[source]

Bases: object

Efficient Quantum Geometric Tensor computation using the bowtie method.

This class computes the generalized QGT matrix that includes: - QGT block: metric tensor for the parameter space - Gradient block: energy derivatives with respect to parameters - Variance block: observable variance (optional)

The bowtie method constructs auxiliary circuits (bowties) for each parameter and observable term, focusing computation only on relevant qubits determined by light-cone analysis. This significantly reduces computational cost for large quantum circuits.

qc

The parameterized quantum circuit.

obs

Observable as a SparsePauliOp for energy/gradient computation.

phase_fix

Whether phase fixing is enabled.

pbar

Progress bar verbosity level (0=off, 1=minimal, 2=detailed).

accelerator

Device for simulation (“CPU” or “GPU”).

VarQITE_gradient

If True, computes real gradients (VarQITE style); if False, computes imaginary gradients.

simulator

Qiskit Aer statevector simulator instance.

bowties

List of all bowtie circuits (parameters + observables).

active_qubits

Tuple of active qubit indices for each bowtie.

non_zero_indices

List of (i,j) index pairs with non-zero overlaps.

slices

Precomputed slice objects for efficient tensor indexing.

Example

>>> qc = QuantumCircuit(4)
>>> # ... build parameterized circuit ...
>>> obs = SparsePauliOp.from_list([("ZIII", 1.0)])
>>> bowtie = BowtieQGT(qc, obs, phase_fix=True, pbar=1)
>>> params = {p: 0.5 for p in qc.parameters}
>>> gen_qgt, energy = bowtie.get_derivatives(params)

Methods

get_derivatives

Compute the generalized QGT matrix and energy expectation value.

extract_qgt

Extract the QGT block from the generalized QGT matrix.

extract_gradient

Extract the energy gradient from the generalized QGT matrix.

extract_variance

Extract the observable variance from the generalized QGT matrix.

__init__(qc, obs, phase_fix=True, pbar=0, verbose_init=False, accelerator='CPU', compute_variance=False, VarQITE_gradient=False)[source]

Initialize the BowtieQGT calculator.

Parameters:
  • qc (QuantumCircuit) – Parameterized quantum circuit to analyze.

  • obs (SparsePauliOp) – Observable as SparsePauliOp for computing energy and gradients.

  • phase_fix (bool) – If True, applies phase correction. Recommended for most cases. Default: True.

  • pbar (int) – Progress bar verbosity level: - 0: No progress bars - 1: Show main computation progress - 2: Show detailed progress including QGT computation Default: 0.

  • verbose_init (bool) – If True, prints distribution statistics of bowtie circuit sizes during initialization. Default: False.

  • accelerator (str) – Simulation device, either “CPU” or “GPU”. GPU requires appropriate Qiskit Aer GPU support. Default: “CPU”.

  • VarQITE_gradient (bool) – If True, computes real-time gradients (VarQITE); if False, computes imaginary-time gradients. Default: False.

Raises:

ValueError – If a parameter is not found in the circuit during bowtie construction.

Note

Initialization performs the following steps: 1. Constructs bowtie circuits for each parameter 2. Constructs bowtie circuits for each observable term 3. Identifies active qubits for each bowtie 4. Transpiles all circuits for the target simulator 5. Precomputes non-zero overlap indices and slice objects

get_derivatives(parameter_dict, tracking_time=False)[source]

Compute the generalized QGT matrix and energy expectation value.

This is the main computation method that evaluates all bowtie circuits at the given parameter values and constructs the generalized QGT matrix. The matrix structure is:

` ┌─────────────┬──────────────┐      QGT        Gradient     (NxN)         (NxM)      ├─────────────┼──────────────┤   Gradient†     Variance     (MxN)         (MxM)      └─────────────┴──────────────┘ `

where N = number of parameters, M = number of observable terms.

Parameters:
  • parameter_dict (dict[Parameter, float]) – Dictionary mapping circuit Parameters to their numerical values. Can be a subset of circuit parameters; unspecified parameters remain symbolic.

  • tracking_time (bool) – If True, returns timing information for profiling. Default: False.

Returns:

tuple: (gen_qgt, energy) where:
  • gen_qgt: Complex numpy array of shape (N+M, N+M) containing the generalized QGT matrix

  • energy: Complex number representing the energy expectation value ⟨ψ|H|ψ⟩

If tracking_time is True:
tuple: ((gen_qgt, energy), (time_simulation, time_qgt)) where:
  • time_simulation: Time in seconds for statevector computation

  • time_qgt: Time in seconds for QGT matrix assembly

Return type:

If tracking_time is False

Note

The generalized QGT is computed as: G_ij = Re[⟨∂_i ψ|∂_j ψ⟩ - ⟨∂_i ψ|ψ⟩⟨ψ|∂_j ψ⟩] / 4

Gradient entries include appropriate factors of -i for imaginary time evolution when VarQITE_gradient=False.

extract_qgt(gen_qgt)[source]

Extract the QGT block from the generalized QGT matrix.

Extracts the upper-left NxN block containing the Quantum Geometric Tensor (metric tensor) for the parameter space.

Parameters:

gen_qgt (ndarray) – Generalized QGT matrix of shape (N+M, N+M) returned by [get_derivatives()](bowtie_qgt/bowtieqgt.py:113).

Returns:

Complex numpy array of shape (N, N) where N is the number of circuit parameters. This is the QGT matrix G_ij = ⟨∂_i ψ|∂_j ψ⟩.

Example

>>> gen_qgt, energy = bowtie.get_derivatives(params)
>>> qgt = bowtie.extract_qgt(gen_qgt)
>>> print(qgt.shape)  # (num_parameters, num_parameters)
extract_gradient(gen_qgt)[source]

Extract the energy gradient from the generalized QGT matrix.

Extracts and processes the gradient block to compute ∂E/∂θ_i for each parameter θ_i. The gradient is computed by summing over observable terms with appropriate normalization and sign conventions.

Parameters:

gen_qgt (ndarray) – Generalized QGT matrix of shape (N+M, N+M) returned by [get_derivatives()](bowtie_qgt/bowtieqgt.py:113).

Returns:

Complex numpy array of shape (N,) containing the gradient components ∂E/∂θ_i. For VarQITE_gradient=True, returns real gradients; for VarQITE_gradient=False, returns imaginary gradients with appropriate sign convention.

Note

The factor of 4 accounts for the 1/4 normalization in the QGT computation. The sign convention depends on whether real or imaginary time evolution is being used.

Example

>>> gen_qgt, energy = bowtie.get_derivatives(params)
>>> gradient = bowtie.extract_gradient(gen_qgt)
>>> print(gradient.shape)  # (num_parameters,)
extract_variance(gen_qgt, energy=None)[source]

Extract the observable variance from the generalized QGT matrix.

Extracts the lower-right MxM block and computes the total variance Var(H) = ⟨H²⟩ - ⟨H⟩². The variance sector contains ⟨H²⟩ (with phase correction when phase_fix=True), so we must subtract ⟨H⟩² (energy²) to obtain the actual variance when the energy parameter is provided.

Parameters:
  • gen_qgt (ndarray) – Generalized QGT matrix of shape (N+M, N+M) returned by [get_derivatives()](bowtie_qgt/bowtieqgt.py:113).

  • energy (float | None) – The expectation value ⟨H⟩ of the observable. When provided, the method returns Var(H) = ⟨H²⟩ - ⟨H⟩². When not provided, returns ⟨H²⟩ (useful for backward compatibility).

Returns:

Complex number representing the variance of the observable when energy is provided, or ⟨H²⟩ when energy is not provided. For Hermitian observables, the variance should be real and non-negative.

Example

>>> # Compute variance (recommended)
>>> gen_qgt, energy = bowtie.get_derivatives(params)
>>> variance = bowtie.extract_variance(gen_qgt, energy).real
>>>
>>> # Backward compatibility: get ⟨H²⟩ without energy
>>> gen_qgt, _ = bowtie.get_derivatives(params)
>>> h2_expectation = bowtie.extract_variance(gen_qgt).real

Utility Functions

bowtie_qgt.bowtieqgt.zeroth(tensor)[source]

Returns tensor[0,0,0,0,…] of a given ndarray

bowtie_qgt.bowtieqgt.get_slice(AB, BC)[source]

Generate slice indices for projecting ψ_AB onto the overlap region with ψ_BC.

Computes the slice to apply to statevector ψ_AB when computing its overlap with ψ_BC. The Hilbert space is decomposed into three regions: - Region A: qubits where only ψ_AB has support (projected to |0⟩) - Region B: qubits where both ψ_AB and ψ_BC have support (overlap computed) - Region C: qubits where only ψ_BC has support (projected to |0⟩)

For qubits in region B (intersection of AB and BC), the slice uses slice(None) to keep all amplitudes. For qubits in region A (in AB but not in BC), the slice uses index 0 to project onto |0⟩.

Parameters:
  • AB (tuple[int]) – Tuple of active qubit indices for statevector ψ_AB.

  • BC (tuple[int]) – Tuple of active qubit indices for statevector ψ_BC.

Returns:

  • slice(None) for qubits in the overlap region B (qubits in both AB and BC)

  • 0 for qubits in region A (qubits in AB but not in BC)

The list is in reversed order to match tensor dimension ordering.

Return type:

List of slice objects or integers for indexing the ψ_AB tensor

Note

When precomputing slices for overlap ⟨ψ_AB|ψ_BC⟩, both directions must be computed and stored: one slice for ψ_AB and one for ψ_BC, since the overlap is not symmetric when the active qubit sets differ.

Example

>>> # ψ_AB has support on qubits (0, 1, 2), ψ_BC on qubits (1, 2, 3,4)
>>> # Region A = {0}, Region B = {1, 2}, Region C = {3,4}
>>> get_slice((0, 1, 2), (1, 2, 3, 4))
[slice(None, None, None), slice(None, None, None), 0]
>>> # Qubit 0 (region A) → 0, qubits 1,2 (region B) → slice(None)