Controlled circuit mapper

The ControlledCircuitMapper algorithm in QDK/Chemistry converts a UnitaryRepresentation into a controlled quantum circuit. Following QDK/Chemistry’s algorithm design principles, it takes a UnitaryRepresentation as input and produces a Circuit as output. Control and target qubit indices are configured via settings.

Overview

Controlled unitaries — operations of the form \(C\text{-}U\) that apply \(U\) to a target register conditioned on the state of a control qubit — are a building block in many quantum algorithms. Mathematically, for a single control qubit the controlled unitary acts as:

\[C\text{-}U \;=\; |0\rangle\langle 0| \otimes I \;+\; |1\rangle\langle 1| \otimes U\]

That is, the target register is left unchanged when the control is \(|0\rangle\) and \(U\) is applied when the control is \(|1\rangle\).

The ControlledCircuitMapper synthesises these controlled operations from the abstract UnitaryRepresentation representation produced by a HamiltonianUnitaryBuilder. This is a core component of algorithms such as PhaseEstimation, which requires repeated controlled applications \(C\text{-}U^{2^k}\).

The mapper takes inputs:

  1. A UnitaryRepresentation — produced by a HamiltonianUnitaryBuilder

Control and target qubit indices are configured via the mapper’s settings (control_indices and target_indices).

The resulting Circuit implements the controlled unitary and can be executed by a CircuitExecutor.

Using the ControlledCircuitMapper

Note

This algorithm is currently available only in the Python API.

This section demonstrates how to create, configure, and run the circuit mapper.

Input requirements

The ControlledCircuitMapper requires:

UnitaryRepresentation

A UnitaryRepresentation specifying the unitary to be controlled.

Settings

control_indices (list of int): Which qubits serve as controls (default: [0]). target_indices (list of int): Which qubits the unitary acts on (default: auto-filled).

Creating a mapper

from qdk_chemistry.algorithms import create

# Create the default mapper (pauli_sequence)
mapper = create("controlled_circuit_mapper")

Running the mapper

import numpy as np
from qdk_chemistry.algorithms import create
from qdk_chemistry.data import Structure

# 1. Setup molecule
coords = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]])
symbols = ["H", "H"]
structure = Structure(coords, symbols=symbols)

# 2. SCF
scf_solver = create("scf_solver")
E_scf, wfn_scf = scf_solver.run(
    structure, charge=0, spin_multiplicity=1, basis_or_guess="sto-3g"
)

# 3. Hamiltonian and qubit mapping
hamiltonian_constructor = create("hamiltonian_constructor")
hamiltonian = hamiltonian_constructor.run(wfn_scf.get_orbitals())
from qdk_chemistry.data import MajoranaMapping

n_spin_orbitals = 2 * hamiltonian.get_orbitals().get_num_molecular_orbitals()
qubit_mapper = create("qubit_mapper")
qubit_ham = qubit_mapper.run(
    hamiltonian, MajoranaMapping.jordan_wigner(n_spin_orbitals)
)

# 4. Build time evolution unitary
trotter = create("hamiltonian_unitary_builder", "trotter", order=2, time=0.1)
evolution = trotter.run(qubit_ham)

# 5. Create a controlled version and map to a circuit
mapper = create("controlled_circuit_mapper", "pauli_sequence", control_indices=[0])
circuit = mapper.run(evolution)
print("Controlled evolution circuit generated")

Available implementations

You can discover available implementations programmatically:

from qdk_chemistry.algorithms import registry

# List all registered controlled circuit mapper implementations
implementations = registry.available("controlled_circuit_mapper")
print(
    implementations
)  # e.g. ['prepare_select_prepare', 'pauli_sequence', 'cswap_pauli_sequence']

Pauli sequence mapper

Factory name: "pauli_sequence" (default)

Given a time-evolution unitary expressed as a PauliProductFormulaContainer — a sequence of exponentiated Pauli terms \(e^{-i\theta_j P_j}\) — this mapper constructs a controlled version by:

  1. Rotating each Pauli operator \(P_j\) into the Z basis

  2. Entangling the target qubits with a CNOT ladder

  3. Applying a controlled \(R_z(2\theta_j)\) rotation from the control qubit

  4. Uncomputing the basis rotations and entangling operations

Note

The current implementation supports a single control qubit.

Controlled SWAP Pauli sequence mapper

Factory name: "cswap_pauli_sequence"

This mapper avoids controlling every gate. It allocates an internal vacuum register in \(|0\ldots0\rangle\), conditionally swaps it with the system register, applies the uncontrolled evolution to the vacuum register, and uncomputes the swap. When the control is \(|0\rangle\) the evolution hits the vacuum and the system is untouched; when it is \(|1\rangle\) the system is parked in the vacuum register and evolved. For an \(n\)-qubit system, an additional \(n\)-qubit vacuum register and two layers of \(n\) controlled-\(\mathrm{SWAP}\) gates replace a fully controlled evolution circuit. This mapper applies to particle-conserving Hamiltonians only. It relies on \(|0\ldots0\rangle\) staying in its own particle-number sector.

The \(|0\rangle\) branch picks up \(U|0\ldots0\rangle = e^{i\varphi_0}|0\ldots0\rangle\) with \(\varphi_0 = -E_0 t\) and \(E_0 = \langle 0\ldots0|H|0\ldots0\rangle\). Only the diagonal (\(I\)/\(Z\)) terms of the product formula phase the vacuum, so \(\varphi_0\) is known classically. The mapper cancels it with a phase gate \(R_1(\varphi_0) = \mathrm{diag}(1, e^{i\varphi_0})\) on the control, giving a genuine controlled-\(U\) up to a global phase for any \(E_0\).

Why grouping is required

The vacuum must remain an eigenstate of the evolution, which is exactly what particle conservation buys: \(H\) cannot connect \(|0\ldots0\rangle\) to any other occupation number. Leaked amplitude entangles the vacuum register with the control and destroys the control coherence, losing the very phase the algorithm is trying to measure.

For a molecular Hamiltonian every fermionic term (\(a_p^\dagger a_q\) and \(a_p^\dagger a_r^\dagger a_s a_q\)) annihilates the vacuum. A single Pauli string is unitary and cannot, so the cancellation survives Trotterization only when the strings coming from one fermionic term are exponentiated as one contiguous block:

\[U|0\ldots0\rangle = e^{-it\sum_i P_i}|0\ldots0\rangle \approx \prod_i e^{-it P_i}|0\ldots0\rangle = |0\ldots0\rangle .\]

The vacuum-annihilating term grouper (factory name "vacuum_annihilating") produces that ordering. A Pauli factor flips a qubit when it exchanges \(|0\rangle\) and \(|1\rangle\), which \(X\) and \(Y\) do and \(I\) and \(Z\) do not. Terms flipping the same qubits are the only ones that can cancel, so the grouper walks them in order and closes a group as soon as the accumulated amplitude \(\sum_j c_j i^{n_Y^{(j)}}\) vanishes — each emitted group therefore annihilates the vacuum outright. Groups are restricted to one \(Y\)-count parity so that their members commute and can be exponentiated term by term.

The mapper validates its input product formula and raises a ValueError when the ordering is not vacuum preserving, rather than returning a wrong result.

Worked example

Take the two-mode Hamiltonian

\[H = a_0^\dagger a_1 + a_1^\dagger a_0 + a_0^\dagger a_0 = \tfrac12 (XX + YY) + \tfrac12 (I - Z_0), \qquad H|00\rangle = 0 .\]

For one Trotter step at \(\Delta t = \pi/2\), the interleaved ordering XX, Z0, YY, I splits the XX/YY pair and gives

\[U_{\text{interleaved}}|00\rangle = \tfrac{1-i}{2}|00\rangle + \tfrac{-1+i}{2}|11\rangle , \qquad \bigl|\langle 11|U_{\text{interleaved}}|00\rangle\bigr|^2 = \tfrac12 ,\]

so half of the vacuum amplitude leaks. Keeping the partners together instead gives

\[U_{\text{grouped}} = e^{-i\frac{\pi}{4}(I - Z_0)}\, e^{-i\frac{\pi}{4}(XX + YY)}, \qquad U_{\text{grouped}}|00\rangle = |00\rangle .\]

End-to-end usage

import numpy as np
from qdk_chemistry.algorithms import create
from qdk_chemistry.data import MajoranaMapping, Structure

# 1. Molecule and mean-field reference
coords = np.array([[0.0, 0.0, 0.0], [0.0, 0.0, 1.4]])
structure = Structure(coords, symbols=["H", "H"])
E_scf, wfn_scf = create("scf_solver").run(
    structure, charge=0, spin_multiplicity=1, basis_or_guess="sto-3g"
)

# 2. Fermionic Hamiltonian, then a qubit Hamiltonian (core energy excluded,
#    so the all-zero state satisfies H|0...0> = 0)
hamiltonian = create("hamiltonian_constructor").run(wfn_scf.get_orbitals())
n_spin_orbitals = 2 * hamiltonian.get_orbitals().get_num_molecular_orbitals()
qubit_ham = create("qubit_mapper").run(
    hamiltonian, MajoranaMapping.jordan_wigner(n_spin_orbitals)
)

# 3. Group Pauli strings that flip the same qubits. These are the strings coming
#    from the same fermionic term, whose amplitudes cancel on |0...0>. Without
#    this step Trotterization interleaves them and the vacuum leaks.
term_grouper = create("term_grouper", "vacuum_annihilating")
grouped_ham = term_grouper.run(qubit_ham)

# 4. Trotterize. The builder honours the grouping, so each group is exponentiated
#    as one contiguous block and the product formula still fixes |0...0>.
trotter = create("hamiltonian_unitary_builder", "trotter", order=1, time=0.1)
evolution = trotter.run(grouped_ham)

# 5. Control it with the CSWAP sandwich. The mapper validates the ordering and
#    raises if the product formula would leak the vacuum. Qubit 0 is the control
#    ancilla (the convention the QPE circuit builders use) and the remaining
#    n_spin_orbitals qubits are auto-assigned as the system register.
cswap_mapper = create(
    "controlled_circuit_mapper",
    "cswap_pauli_sequence",
    control_indices=[0],
)
circuit = cswap_mapper.run(evolution)
print("Controlled evolution circuit generated via the CSWAP sandwich")

Further reading