Skip to content

Extend Pauli rotations to accept complex angles #594

@TysonRayJones

Description

@TysonRayJones

Summary

Implement applyNonUnitaryPauliGadget() which extends applyPauliGadget() to accept a complex angle parameter. This requires merely updating several signatures in operations.cpp and localiser.cpp.

Context

The existing function applyPauliGadget() accepts a real scalar $\theta=$ angle and a PauliStr $\hat{\sigma}=$ str (a tensor product of Pauli operators), and simulates the unitary operation

$$ \hat{U}(\theta) = \exp\left( -\mathrm{i} \frac{ \theta}{2} \hat{\sigma} \right), \,\,\,\,\,\,\,\,\, \theta \in \mathbb{R} $$

This is a multi-qubit generalisation of a rotation operator and appears in many quantum circuits, like Trotter simulations.

Consider the non-unitary produced by substituting the real $\theta$ with an imaginary scalar, which removes the imaginary unit from the exponent:

$$ \hat{V}(\theta) = \hat{U}(- \theta \mathrm{i}) = \exp\left( - \frac{ \theta}{2} \hat{\sigma} \right), \,\,\,\,\,\,\,\,\, \theta \in \mathbb{R}. $$

Despite being non-physical, such an operation turns out to be very useful in a classical simulator. It appears in Trotter circuits for "imaginary time evolution" which when effected upon a state, drives the system toward the ground-state.

A slightly more general variant of the operation would permit the input parameter to be any complex number.

$$ \hat{W}(\phi) = \exp\left( -\mathrm{i} \frac{\phi}{2} \hat{\sigma} \right), \,\,\,\,\,\,\,\,\, \phi \in \mathbb{C} $$

A function called applyNonUnitaryPauliGadget() which accepts a complex $\phi$ above is ergo a valuable facility, and can be leveraged by a hypothetical complex-angle generalisation of applyTrotterizedPauliStrSumGadget() to (among other things) simulate imaginary-time evolution.

The existing applyPauliGadget() function has controlled-variants like applyMultiStateControlledPauliGadget. Such variants are not needed nor useful for the new applyNonUnitaryPauliGadget().

Details

In QuEST, a qreal is a precision-agnostic alias for a real scalar (like double) while qcomp is a similar alias for a complex number (like std::complex<double>). This challenge involves retaining the existing function

void applyPauliGadget(Qureg qureg, PauliStr str, qreal angle);

while defining a new function

void applyNonUnitaryPauliGadget(Qureg qureg, PauliStr str, qcomp angle);

to simulate $\hat{W}(\phi)$ above, where $\phi=$ angle.

Fortunately, this requires no new simulation code! The existing applyPauliGadget() function eventually invokes the localiser.cpp function localiser_statevec_anyCtrlPauliGadget():

void localiser_statevec_anyCtrlPauliGadget(Qureg qureg, vector<int> ctrls, vector<int> ctrlStates, PauliStr str, qreal phase) {
// when str=IZ, we must use the above bespoke algorithm
if (!paulis_containsXOrY(str)) {
localiser_statevec_anyCtrlPhaseGadget(qureg, ctrls, ctrlStates, paulis_getInds(str), phase);
return;
}
qcomp ampFac = std::cos(phase);
qcomp pairAmpFac = std::sin(phase) * 1_i;
anyCtrlPauliTensorOrGadget(qureg, ctrls, ctrlStates, str, ampFac, pairAmpFac);
}

Setting aside the str=IZ edgecase, this merely expands exp(i phase) with Euler's formula and passes the two terms to the internal localiser function:

void anyCtrlPauliTensorOrGadget(..., qcomp ampFac, qcomp pairAmpFac);

The terms (ampFac and pairAmpFac) are eventually multiplied directly upon the quantum amplitudes (like here). Since ampFac and pairAmpFac are already of type qcomp, we can trivially change the type of parameter phase in localiser_statevec_anyCtrlPauliGadget from qreal to qcomp to support complex phases, like $\phi$ above. So too we can the simple functions involved in the str=IZ edgecase. Any code passing a qreal phase to these functions can instead pass qcomp(phase,0). Easy! 🎉

The actual applyNonUnitaryPauliGadget() function contents (similar to applyMultiStateControlledPauliGadget()) can be copied from below:

void applyNonUnitaryPauliGadget(Qureg qureg, PauliStr str, qcomp angle) {
    validate_quregFields(qureg, __func__);
    validate_pauliStrTargets(qureg, str, __func__);

    qcomp phase = util_getPhaseFromGateAngle(angle);
    localiser_statevec_anyCtrlPauliGadget(qureg, {}, {}, str, phase);

    if (!qureg.isDensityMatrix)
        return;

    // conj(e^i(a)XZ) = e^(-i conj(a)XZ) but conj(Y)=-Y, so odd-Y undoes phase negation
    phase = std::conj(phase) * (paulis_hasOddNumY(str) ? 1 : -1);
    str = paulis_getShiftedPauliStr(str, qureg.numQubits);
    localiser_statevec_anyCtrlPauliGadget(qureg, {}, {}, str, phase);
}

where the utilities.cpp function util_getPhaseFromGateAngle has been given a qcomp overload.

Testing

Implementing the unit tests for this function is beyond the scope of the unitaryHACK challenge. Instead, one can locally run the script

#include "quest.h"

int main() {
    initQuESTEnv();

    Qureg qureg = createQureg(3);
    PauliStr str = getInlinePauliStr("XYZ", {0,1,2});
    qcomp angle = getQcomp(.4, .8);

    initPlusState(qureg);
    applyNonUnitaryPauliGadget(qureg, str, angle);

    qreal norm = calcTotalProb(qureg);
    reportScalar("norm", norm);

    finalizeQuESTEnv();
    return 0;
}

which should output 1.33743 (in lieu of 1.0).

Such a test can be automatically run by QuEST's CI across varying operating systems, compilers and floating-point precisions by including the above file in PR within /examples/automated/ (saved as apply_no_unitary_pauli_gadget.c). See an explanation here.

Bonus

Once applyNonUnitaryPauliGadget() is implemented, it will be trivial to also generalise applyTrotterizedPauliStrSumGadget() to applyTrotterizedNonUnitaryPauliStrSumGadget() which accepts a complex angle.

Metadata

Metadata

Assignees

Labels

enhancementunitaryhack2025Issues associated with challenges in unitaryHACK 2025

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions