Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d9aab00
[TEMPORARY] Make sure that we don't install from testpypi, and instla…
JerryChen97 Jul 28, 2026
f839532
try fixing obs Y
JerryChen97 Jul 28, 2026
3ae3266
try again
JerryChen97 Jul 28, 2026
3d4af7b
use qp.ctrl
JerryChen97 Jul 28, 2026
774c2a3
CY won't decompose now
JerryChen97 Jul 28, 2026
82070c7
oops inf recursion
JerryChen97 Jul 28, 2026
b08b826
xfail this legacy frontend test
JerryChen97 Jul 28, 2026
529f05a
Op2 gates were desappearing???
JerryChen97 Jul 29, 2026
a5dada8
Merge branch 'main' into fix/op2-as-observable
JerryChen97 Jul 29, 2026
9c9ae69
xfail legacy
JerryChen97 Jul 29, 2026
574e7cd
Use to_name
JerryChen97 Jul 29, 2026
841c56c
xfail custom decomps
JerryChen97 Jul 29, 2026
6675a9b
bump
JerryChen97 Jul 29, 2026
6ec1347
fix S expectation
JerryChen97 Jul 29, 2026
fab5e09
This entire block of tests are confusing:
JerryChen97 Jul 30, 2026
4c7a8bf
fix make frontend
JerryChen97 Jul 31, 2026
131bc18
Controlled verify
JerryChen97 Jul 31, 2026
68b820f
does sum work
JerryChen97 Jul 31, 2026
e72d3bc
lit
JerryChen97 Jul 31, 2026
6fd96d4
Merge branch 'main' into fix/op2-as-observable
JerryChen97 Jul 31, 2026
4086bdb
bring back the quantum control test. Just fix them using Op2
JerryChen97 Jul 31, 2026
88d8658
Merge branch 'main' into fix/op2-as-observable
JerryChen97 Aug 2, 2026
f941715
change dep branch to `main`
JerryChen97 Aug 4, 2026
721fc16
Merge branch 'main' into fix/op2-as-observable
JerryChen97 Aug 4, 2026
acf26ce
xfail
JerryChen97 Aug 4, 2026
cdd5a9b
lift adjoint issue to reveal the second (dynamic ctrl values)
JerryChen97 Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .dep-versions
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ enzyme=v0.0.238

# For a custom PL version, update the package version here and at
# 'doc/requirements.txt'
pennylane=0.46.0.dev62
pennylane=0.46.0.dev65

# For a custom LQ/LK version, update the package version here and at
# 'doc/requirements.txt'
Expand Down
18 changes: 15 additions & 3 deletions .github/workflows/check-catalyst.yaml

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To be reverted once the fix done

Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,11 @@ jobs:
python3 -m pip install oqc-qcaas-client
# Install graphviz for testing the mlir-op-graph integration
sudo apt-get install -y graphviz
make frontend

- name: Install Catalyst Frontend
run: |
make frontend \
PENNYLANE_SOURCE=git+https://github.com/PennyLaneAI/pennylane@fix/adjoint2-static-arg-issue

- name: Verify Graphviz installation
run: |
Expand Down Expand Up @@ -606,7 +610,11 @@ jobs:
sudo apt-get install -y libasan6 make
python3 --version | grep ${{ needs.constants.outputs.primary_python_version }}
python3 -m pip install -r requirements.txt
make frontend

- name: Install Catalyst Frontend
run: |
make frontend \
PENNYLANE_SOURCE=git+https://github.com/PennyLaneAI/pennylane@fix/adjoint2-static-arg-issue

- name: Run Python Pytest Tests (backend=lightning.kokkos)
run: |
Expand Down Expand Up @@ -670,7 +678,11 @@ jobs:
sudo apt-get install -y libasan6 make
python3 --version | grep ${{ needs.constants.outputs.primary_python_version }}
python3 -m pip install -r requirements.txt
make frontend

- name: Install Catalyst Frontend
run: |
make frontend \
PENNYLANE_SOURCE=git+https://github.com/PennyLaneAI/pennylane@fix/adjoint2-static-arg-issue

runtime-device-tests:
name: Third-Party Device Tests (C++)
Expand Down
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
PYTHON ?= $(shell which python3)
PENNYLANE_SOURCE ?=
C_COMPILER ?= $(shell which clang)
CXX_COMPILER ?= $(shell which clang++)
BLACKVERSIONMAJOR := $(shell black --version 2> /dev/null | head -n1 | awk '{ print $$2 }' | cut -d. -f1)
Expand Down Expand Up @@ -121,6 +122,9 @@ frontend:
# Uninstall pennylane before updating Catalyst, since pip will not replace two development
# versions of a package with the same version tag (e.g. 0.38-dev0).
$(PYTHON) -m pip uninstall -y pennylane
if [ -n "$(PENNYLANE_SOURCE)" ]; then \
$(PYTHON) -m pip install --no-deps --force-reinstall "$(PENNYLANE_SOURCE)" $(PIP_VERBOSE_FLAG); \
fi
$(PYTHON) -m pip install -e . --extra-index-url https://test.pypi.org/simple $(PIP_VERBOSE_FLAG)
$(PYTHON) -m catalyst.utils.precompile_decomposition_rules
rm -r frontend/pennylane_catalyst.egg-info
Expand Down
2 changes: 1 addition & 1 deletion doc/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ lxml_html_clean
--extra-index-url https://test.pypi.org/simple/
pennylane-lightning-kokkos==0.46.0-dev21
pennylane-lightning==0.46.0-dev21
pennylane==0.46.0.dev62
pennylane==0.46.0.dev65
12 changes: 10 additions & 2 deletions frontend/catalyst/api_extensions/quantum_operators.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@
from jax.core import get_aval
from pennylane import QueuingManager
from pennylane.decomposition.resources import resolve_work_wire_type
from pennylane.operation import Operator
from pennylane.operation import Operator, Operator2
from pennylane.ops.op_math.adjoint import create_adjoint_op
from pennylane.ops.op_math.controlled import create_controlled_op
from pennylane.ops.op_math.controlled import create_controlled_op, create_controlled_op2
from pennylane.tape import QuantumTape

from catalyst.api_extensions.control_flow import cond
Expand Down Expand Up @@ -664,6 +664,14 @@ def __init__(self, target, control, control_values, work_wires, work_wire_type="
def __call__(self, *args, **kwargs):
if self.single_op:
base_op = self.target if self.instantiated else self.target(*args, **kwargs)
if isinstance(base_op, Operator2):
return create_controlled_op2(
base_op,
control_wires=self.control_wires,
control_values=self.control_values,
work_wires=self.work_wires,
work_wire_type=self.work_wire_type,
)
return create_controlled_op(
base_op,
self.control_wires,
Expand Down
15 changes: 12 additions & 3 deletions frontend/catalyst/device/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
BasisState,
CompositeOp,
Controlled,
Controlled2,
ControlledOp,
ControlledOp2,
StatePrep,
SymbolicOp,
)
Expand All @@ -55,6 +57,13 @@
from catalyst.tracing.contexts import EvaluationContext
from catalyst.utils.exceptions import CompileError, DifferentiableCompileError

_CONTROLLED_WRAPPER_TYPES = (
Controlled,
ControlledOp,
Controlled2,
ControlledOp2,
)


def _verify_nested(
operations: List[Operation],
Expand Down Expand Up @@ -142,7 +151,7 @@ def _grad_method_op_checker(op, grad_method):

def _ctrl_op_checker(op, in_control):
# For PL controlled instances we don't recurse via nested tapes, so check the base op here.
if type(op) in (Controlled, ControlledOp):
if type(op) in _CONTROLLED_WRAPPER_TYPES:
if isinstance(op.base, HybridOp):
raise CompileError(
f"Cannot compile PennyLane control of the hybrid op {type(op.base)}."
Expand Down Expand Up @@ -176,7 +185,7 @@ def _inv_op_checker(op, in_inverse):
return in_inverse
# If its a PL Controlled we also want to check its base to catch C(Adjoint(base)).
# PL simplification should mean pure PL operators will not be more nested than this.
if type(op) in (Controlled, ControlledOp):
if type(op) in _CONTROLLED_WRAPPER_TYPES:
_inv_op_checker(op.base, in_inverse)
return in_inverse
# Exclude control flow ops we always know how to invert
Expand All @@ -199,7 +208,7 @@ def _op_checker(op, state):
# is handled in _inv_op_checker and _ctrl_op_checker.
# Specialized control op classes (e.g. CRZ) should be checked directly though, which is why
# we can't use isinstance(op, Controlled).
if type(op) in (Controlled, ControlledOp) or isinstance(op, (Adjoint)):
if type(op) in _CONTROLLED_WRAPPER_TYPES or isinstance(op, Adjoint):
pass
elif not op.name in supported_ops:
raise CompileError(
Expand Down
4 changes: 2 additions & 2 deletions frontend/catalyst/from_plxpr/decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ def interpret_operation(self, op: "qp.operation.Operator"):
"""

self._operations.add(op)
data, struct = jax.tree_util.tree_flatten(op)
return jax.tree_util.tree_unflatten(struct, data)
# PennyLane's base interpreter rebinds reconstructed Operator2 instances into the trace.
return super().interpret_operation(op)

def cleanup(self):
"""Cleanup after interpretation."""
Expand Down
9 changes: 2 additions & 7 deletions frontend/catalyst/from_plxpr/from_plxpr.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

If this works then I just repalce inplace

Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from jax.extend.core import ClosedJaxpr, Jaxpr
from pennylane.capture import PlxprInterpreter, qnode_prim
from pennylane.capture.primitives import transform_prim
from pennylane.decomposition.utils import to_name
from pennylane.transforms import decompose as pl_decompose

from catalyst.device import extract_backend_info
Expand Down Expand Up @@ -557,10 +558,4 @@ def _get_operator_name(op):
Note: Controlled and Adjoint ops aren't supported in `gate_set`
by PennyLane's DecompositionGraph; unit tests were added in PennyLane.
"""
if isinstance(op, str):
return op

# Return NoNameOp if the operator has no _primitive.name attribute.
# This is to avoid errors when we capture the program
# as we deal with such ops later in the decomposition graph.
return getattr(op._primitive, "name", "NoNameOp")
return to_name(op)
24 changes: 20 additions & 4 deletions frontend/catalyst/from_plxpr/qfunc_interpreter.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Does this look safe to you @paul0403 ? This PR is technically still WIP, but it seems that this file specifically blocks other work (porting Z), so I'd like to hear some early-stage feedback

Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,24 @@ def __init__(

super().__init__()

def interpret_operation_eqn(self, eqn):
"""Override to handle Operator2 (operator_p) equations.

For Operator2 ops used as observables (output is not DropVar),
return the operator instance without applying it as a gate.
For Operator2 ops used as gates (output is DropVar), apply the gate
using the Operator2-specific lowering.
For legacy ops, delegate to the parent implementation.
"""
if eqn.primitive is operator_p:
invals = [self.read(invar) for invar in eqn.invars]
with qp.QueuingManager.stop_recording():
op = eqn.primitive.impl(*invals, **eqn.params)
if isinstance(eqn.outvars[0], jax.core.DropVar):
_apply_operator2_gate(self, *invals, **eqn.params)
return op
return super().interpret_operation_eqn(eqn)

def interpret_operation(self, op, is_adjoint=False, control_values=(), control_wires=()):
"""Re-bind a pennylane operation as a catalyst instruction.

Expand Down Expand Up @@ -311,8 +329,7 @@ def _new_hybrid_arg(interp: PLxPRToQuantumJaxprInterpreter, arg) -> list:


# pylint: disable=too-many-arguments
@PLxPRToQuantumJaxprInterpreter.register_primitive(operator_p)
def handle_operator(
def _apply_operator2_gate(
self,
*args,
op_cls,
Expand All @@ -324,7 +341,7 @@ def handle_operator(
n_ctrls,
**kwargs,
):
"""Handle the conversion from plxpr to Catalyst jaxpr for the operator_p primitive."""
"""Apply an Operator2 as a gate instruction using qref_operator_p."""
n_wires = sum(wire_lens)
wire_inputs = args[len(op_cls.dynamic_argnames) : len(op_cls.dynamic_argnames) + n_wires]
if n_ctrls:
Expand Down Expand Up @@ -370,7 +387,6 @@ def handle_operator(
n_ctrls=n_ctrls,
**kwargs,
)
return []


# pylint: disable=unused-argument, too-many-arguments
Expand Down
4 changes: 2 additions & 2 deletions frontend/test/lit/test_meta_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,12 @@ def adjoint_adjoint():


# CHECK-LABEL: @adjoint_ctrl_adjoint
@qjit(target="mlir")
@qjit(target="mlir", capture=True)
@qp.qnode(qp.device("lightning.qubit", wires=2))
def adjoint_ctrl_adjoint():
qp.adjoint(qp.ctrl(qp.adjoint(qp.S(0)), control=1))
return qp.probs()


# CHECK: quantum.custom "S"() %{{[^\s]+}} ctrls(%{{[^\s]+}}) ctrlvals(%{{[^\s]+}}) : !quantum.bit
# CHECK: qref.custom "S"() %{{[^\s]+}} ctrls(%{{[^\s]+}}) ctrlvals(%{{[^\s]+}}) : !qref.bit ctrls !qref.bit
print(adjoint_ctrl_adjoint.mlir)
8 changes: 8 additions & 0 deletions frontend/test/pytest/test_preprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ def circuit(x: float, y: float):

assert np.isclose(circuit(x, y), np.cos(-x) * np.cos(y))

@pytest.mark.xfail(
reason="Legacy preprocessing cannot decompose Operator2 Hadamard subclasses",
raises=CompileError,
)
def test_decomposition_of_cond_circuit(self):
"""Test that unsupported operators nested in Cond are decompsed, and the
resulting circuit has the expected result, obtained analytically"""
Expand Down Expand Up @@ -331,6 +335,10 @@ def ansatz():
expected_res = np.array([x1, x2, x1, x2])
assert np.allclose(expected_res, circuit(phi))

@pytest.mark.xfail(
reason="Legacy preprocessing cannot decompose Operator2 Hadamard subclasses",
raises=CompileError,
)
@pytest.mark.parametrize("reps, angle", [(3, 1.72), (5, 1.6), (10, 0.4)])
def test_decomposition_of_forloop_circuit(self, reps, angle):
"""Test that unsupported operators nested in ForLoop are decompsed, and
Expand Down
23 changes: 12 additions & 11 deletions frontend/test/pytest/test_quantum_control.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I suggest we directly delete this chunk of PL-mimicing tests. What do you think @paul0403 @kipawaa

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think this is needed, since we're testing catalyst.control and catalyst.adjoint rather than pennylane.control and pennylane.adjoint. Maybe I'm missing some context here though?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Then I can cherry pick and keep catalyst.*? Also actually old frontend might be deprecated soon, but yes we still don't have a clear "what to burn" list yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from pennylane import cond
from pennylane import ctrl as PL_ctrl
from pennylane import for_loop, qjit, while_loop
from pennylane.operation import DecompositionUndefinedError, Operation, Operator, Wires
from pennylane.operation import DecompositionUndefinedError, Operation, Operator, Operator2, Wires
from pennylane.ops.op_math.controlled import Controlled
from pennylane.tape import QuantumTape

Expand Down Expand Up @@ -947,20 +947,18 @@ def test_flatten_unflatten(self):
op = C_ctrl(target, control_wires, control_values=control_values, work_wires=work_wires)

data, metadata = op._flatten()
assert data[0] is target
assert len(data) == 1
dynamic_data, wire_data, hybrid_data = data
assert dynamic_data == [list(control_values)]
assert wire_data == [control_wires, work_wires]
assert hybrid_data == [target]

assert len(metadata) == 4
assert metadata[0] == control_wires
assert metadata[1] == control_values
assert metadata[2] == work_wires
assert metadata[3] == work_wire_type
assert metadata == (work_wire_type,)

assert hash(metadata)

new_op = type(op)._unflatten(*op._flatten())
assert qp.equal(op, new_op)
assert new_op._name == "C(S)" # make sure initialization was called
assert new_op.name == "C(S)" # make sure initialization was called

def test_copy(self):
"""Test that a copy of a controlled oeprator can have its parameters updated
Expand Down Expand Up @@ -1529,17 +1527,20 @@ def test_control_on_zero(self):
assert qp.equal(decomp[4], qp.PauliX(2))

@pytest.mark.parametrize(
"base_cls, params, base_wires, ctrl_wires, _, expected",
"base_cls, params, base_wires, ctrl_wires, custom_ctrl_op, expected",
custom_ctrl_op_decomps,
)
def test_control_on_zero_custom_ops(
self, base_cls, params, base_wires, ctrl_wires, _, expected
self, base_cls, params, base_wires, ctrl_wires, custom_ctrl_op, expected
):
"""Tests that custom ops are not converted when wires are control-on-zero."""

base_op = base_cls(*params, wires=base_wires)
op = C_ctrl(base_op, control=ctrl_wires, control_values=[False] * len(ctrl_wires))

if isinstance(base_op, Operator2):
expected = [custom_ctrl_op(*params, wires=ctrl_wires + base_wires)]

decomp = op.decomposition()

i = 0
Expand Down
2 changes: 1 addition & 1 deletion frontend/test/pytest/test_split_non_commuting.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ def circ():
qp.RX(0.3, wires=0)
qp.RY(0.5, wires=1)
qp.RX(0.7, wires=2)
return qp.expval(qp.Z(0) + qp.X(1) + 2 * qp.Y(2))
return qp.expval(qp.sum(qp.Z(0), qp.X(1), qp.s_prod(2, qp.Y(2))))

circ_split = qjit(self.snc_pass(circ), capture=capture_mode)
circ_ref = qjit(circ, capture=capture_mode)
Expand Down
4 changes: 4 additions & 0 deletions frontend/test/pytest/test_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,10 @@ def circuit(x, y, z, obs):

@pytest.mark.parametrize("params", parameters)
@pytest.mark.parametrize("obs", observables)
@pytest.mark.xfail(
reason="Non-capture pipeline incompatible with Operator2 dynamic wires in pytrees",
strict=False,
)
def test_expansion_qnode_no_cache(self, backend, params, obs):
"""Test broadcast expand.

Expand Down
2 changes: 1 addition & 1 deletion mlir/stablehlo
Submodule stablehlo updated 209 files
Loading