Skip to content

Implement a decomposition of PPRs into PPMs #1664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open

Implement a decomposition of PPRs into PPMs #1664

wants to merge 26 commits into from

Conversation

sengthai
Copy link
Contributor

@sengthai sengthai commented Apr 16, 2025

Context:
This PR introduces a new pass that decomposes Pauli Product Rotations (PPR) into Pauli Product Measurements (PPMs) consuming magic states. Two decomposition strategies are supported: inject-magic-state and auto-corrected. Related prior work on qec.ppr began in #1486 #1563 #1580.

Description of the Change:

  • New operation: qec.select.ppm – conditional Pauli product measurement based on a boolean control
  • New operation: qec.prepare – prepares logical qubits in a specified initial state (|0⟩, |1⟩, |+⟩, |-⟩, |Y⟩, |-Y⟩, |m⟩, or |m̅⟩)
  • Extended qec.ppr and qec.ppm to support conditional execution
  • Implemented decomposition strategies: inject-magic-state and auto-corrected.

Example:
Input:

func.func @test_ppr_to_ppm(%q1 : !quantum.bit) {
    %0 = qec.ppr ["Z"](8) %q1 : !quantum.bit
    return
}

Run:

$ quantum-opt --decompose_clifford_ppr='decompose-method=auto-corrected'  test.mlir

Outputs:

module {
  func.func @foo(%arg0: !quantum.bit) {
    %c2_i64 = arith.constant 2 : i64
    %0 = quantum.alloc(%c2_i64) : !quantum.reg
    %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit
    %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit
    %3 = qec.prepare  zero %1 : !quantum.bit
    %4 = qec.prepare  magic %2 : !quantum.bit
    %mres, %out_qubits:2 = qec.ppm ["Z", "Z"] %arg0, %4 : !quantum.bit, !quantum.bit
    %mres_0, %out_qubits_1:2 = qec.ppm ["Z", "Y"] %3, %out_qubits#1 : !quantum.bit, !quantum.bit
    %mres_2, %out_qubits_3 = qec.ppm ["X"] %out_qubits_1#1 : !quantum.bit
    %mres_4, %out_qubits_5 = qec.select.ppm(%mres, ["X"], ["Z"]) %out_qubits_1#0 : !quantum.bit
    %5 = arith.xori %mres_0, %mres_2 : i1
    %6 = qec.ppr ["Z"](2) %out_qubits#0 cond(%5) : !quantum.bit
    quantum.dealloc %0 : !quantum.reg
    return
  }
}

Run:

$ quantum-opt --decompose_clifford_ppr='decompose-method=inject-magic-state'  test.mlir

Outputs:

module {
  func.func @foo(%arg0: !quantum.bit) {
    %c2_i64 = arith.constant 2 : i64
    %0 = quantum.alloc(%c2_i64) : !quantum.reg
    %1 = quantum.extract %0[ 0] : !quantum.reg -> !quantum.bit
    %2 = qec.prepare  magic %1 : !quantum.bit
    %mres, %out_qubits:2 = qec.ppm ["Z", "Z"] %arg0, %2 : !quantum.bit, !quantum.bit
    %3 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit
    %4 = qec.prepare  zero %3 : !quantum.bit
    %mres_0, %out_qubits_1:2 = qec.ppm ["Z", "Y"](-1) %out_qubits#0, %4 cond(%mres) : !quantum.bit, !quantum.bit
    %mres_2, %out_qubits_3 = qec.ppm ["X"] %out_qubits_1#1 cond(%mres) : !quantum.bit
    %5 = arith.xori %mres_0, %mres_2 : i1
    %6 = qec.ppr ["Z"](2) %out_qubits_1#0 cond(%5) : !quantum.bit
    %mres_4, %out_qubits_5 = qec.ppm ["X"] %out_qubits#1 : !quantum.bit
    %7 = qec.ppr ["Z"](2) %6 cond(%mres_4) : !quantum.bit
    quantum.dealloc %0 : !quantum.reg
    return
  }
}

[sc-89168]
[sc-90158]

@sengthai sengthai marked this pull request as ready for review April 24, 2025 12:57
@sengthai sengthai changed the title [WIP] Implement a decomposition of non-Clifford PPRs into PPMs Implement a decomposition of non-Clifford PPRs into PPMs Apr 24, 2025
@sengthai sengthai requested a review from a team April 24, 2025 12:57
@sengthai sengthai added enhancement New feature or request do-not-merge labels Apr 24, 2025
@sengthai

This comment was marked as resolved.

This comment was marked as outdated.

@sengthai sengthai changed the title Implement a decomposition of non-Clifford PPRs into PPMs Implement a decomposition of PPRs into PPMs Apr 29, 2025
@dime10 dime10 self-requested a review May 1, 2025 22:17
Copy link
Contributor

@dime10 dime10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work!! :) So far I'm leaving some comments regarding organization, documentation, naming, etc. I will leave a second review regarding the C++ implementation tomorrow :)

Prepares logical qubits in a specific initial quantum state,
such as |0⟩, |1⟩, |+⟩, |-⟩, |Y⟩, |-Y⟩, |m⟩, or |m̅⟩.

This operation is typically used at the start of a quantum circuit to initialize
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't say this operation is used at the start of a circuit, don't we need this everywhere in order to implement pi/8 rotations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we need this everywhere we decompose pi/8 rotations.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more thing, in Catalyst, we assume that the qubits are prepared in |0>. So if the program starts from |+>, the developer needs to apply the H gate when writing in Pennylane. In QEC, I think the preparation step and applying the gate could be different in terms of performance. So, I'm thinking of letting users define the prepared state as well.

Copy link
Contributor

@dime10 dime10 May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you plan on keeping this operation now that we have fabricate?

If yes, what are the semantics with respect to the input state? Does the prepare op require that the input is in a 0 state?

sengthai and others added 2 commits May 7, 2025 10:45
Co-authored-by: David Ittah <[email protected]>
@@ -2,6 +2,20 @@

<h3>New features since last release</h3>

* A new compilation pass has been added to Catalyst to decompose Pauli product rotations (PPRs) into
Pauli product measurements (PPMs). Non-Clifford (pi/8) rotations require the consumption of a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Pauli product measurements (PPMs). Non-Clifford (pi/8) rotations require the consumption of a
Pauli product measurements (PPMs). Non-Clifford rotations, :math:`\exp(i\sigma_{x, y, y} \tfrac{\pi}{8})`, require the consumption of a

Copy link
Contributor

@dime10 dime10 May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x, y, z?

The expression is also missing an indication that the exponential argument is a product.

Copy link
Contributor

@dime10 dime10 May 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that these rendered forms add that much, unless we say we have to define the term PPR mathematically whenever we use it. It's also harder to read unrendered.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do something to clarify what the "pi/8" means, especially because, in PL, we take the convention of the factor of 1/2 being in the exponent already! I found that to be a pesky blocker when trying to roughly implement some things in the paper in PL.

Copy link
Contributor

@dime10 dime10 May 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about defining PPR once when it is first mentioned? Then you can say Non-Clifford (Θ=pi/8) rotations assuming theta was the variable used in the definition

@@ -2,6 +2,20 @@

<h3>New features since last release</h3>

* A new compilation pass has been added to Catalyst to decompose Pauli product rotations (PPRs) into
Pauli product measurements (PPMs). Non-Clifford (pi/8) rotations require the consumption of a
magic state, while Clifford rotations will not. The methods are implemented as described in
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
magic state, while Clifford rotations will not. The methods are implemented as described in
magic state, while Clifford rotations, :math:`\exp(i\sigma_{x, y, y} \tfrac{\pi}{4})`, will not. The methods are implemented as described in

Comment on lines +509 to +510
PPRs (pi/8) are decomposed first, and then Clifford PPRs (pi/4) are decomposed. Non-Clifford
decomposition can be performed in one of two ways: "clifford-corrected" or "auto-corrected",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See changelog entry suggestion. I'd do the same here!

sengthai and others added 2 commits May 8, 2025 10:55
- Updated docstring
- Renamed variable name
Co-authored-by: Isaac De Vlugt <[email protected]>

Update doc/releases/changelog-dev.md

Co-authored-by: Isaac De Vlugt <[email protected]>
Copy link
Contributor

@dime10 dime10 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @sengthai 🎉 🎉

This is looking great, only a few points we could improve on :)

Comment on lines +229 to +231
#
# commute_ppr
#
Copy link
Contributor

@dime10 dime10 May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well I would avoid these comments, you should use docstrings to document individual tests, and classes to group similar tests together. That's how we organize all our other pytests as well :)

In this case the headers don't really do much anyway since they just repeat the name of the test function.


def test_commute_ppr():
"""
Test commute_ppr
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good docstring will say what you are testing and possibly additional information like what you are expecting (if that isn't obvious).

Prepares logical qubits in a specific initial quantum state,
such as |0⟩, |1⟩, |+⟩, |-⟩, |Y⟩, |-Y⟩, |m⟩, or |m̅⟩.

This operation is typically used at the start of a quantum circuit to initialize
Copy link
Contributor

@dime10 dime10 May 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you plan on keeping this operation now that we have fabricate?

If yes, what are the semantics with respect to the input state? Does the prepare op require that the input is in a 0 state?

Comment on lines +479 to +481
I1:$condition, // The condition qubit
PauliWord:$pauli_product,
PauliWord:$else_pauli_product,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would reorder these slightly, instead of framing it as an if-else with a condition, framing it as a switch (or selector) where the provided switch value simply indexes the provided pauli products makes more sense to me, because:

  • it is easily extensible to more products with a larger integer
  • it fits the select.ppm name better
Suggested change
I1:$condition, // The condition qubit
PauliWord:$pauli_product,
PauliWord:$else_pauli_product,
I1:$switch,
PauliWord:$pauli_product_0,
PauliWord:$pauli_product_1,

Comment on lines +52 to +64
/// ─────┌───┐───────
/// ─────│ P │(π/4)──
/// ─────└───┘───────
///
/// into
///
/// ─────┌───┐────────┌───────┐──
/// ─────|-P |────────| P(π/2)|──
/// ─────| |────────└───╦───┘──
/// | ╠════════════╣
/// | | ┌───┐ ║
/// |0⟩──| Y |─────| X ╠══╝
/// └───┘ └───┘
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow these are nice drawings 😄

rewriter.create<PPRotationOp>(loc, pauliP, PI_DENOMINATOR, outPZQubits, cond.getResult());

rewriter.replaceOp(op, pprPI2.getOutQubits());
return pprPI2;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not destroy the ancilla before returning?

I was thinking it might be helpful to add single qubit allocation/deallocation ops to not have to deal with the register unless necessary, what do you think?

What I've also seen, which would work here, is a destructive measurement (that automatically deallocates the qubit).

namespace catalyst {
namespace qec {

AllocOp buildAllocQreg(Location loc, int64_t size, PatternRewriter &rewriter)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like these should be builders on the operations directly no?

return rewriter.create<ExtractOp>(loc, type, qreg, nullptr, idxAttr);
}

LogicalInitKind getMagicState(QECOpInterface op)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this method be part of the interface then?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally speaking, "utility" modules often hint at insufficient or poorly designed apis. Not that they never have their place, but like in this case, the functionality is often better placed directly with the relevant code constructs (classes, modules, etc).

auto loc = op.getLoc();

// Prepare |0⟩ and |m⟩
auto zero = rewriter.create<FabricateOp>(loc, LogicalInitKind::zero);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar from the other file, it might be nice to distinguish between using a factory vs regular ancilla allocation.

And same comments regarding the deallocation of ancilla qubits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants