Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e2dd4f3
feat: add operator2 dispatch for abstractify
andrijapau Jun 17, 2026
5ebff7e
add op type dispatch
andrijapau Jun 17, 2026
b8d6be1
merge base
andrijapau Jun 17, 2026
65b2b58
bring back abstractify
andrijapau Jun 17, 2026
e9211c0
Apply suggestion from @andrijapau
andrijapau Jun 17, 2026
25c74e1
revert single dispatch
andrijapau Jun 17, 2026
feacd53
Merge branch 'feat/abstractify-operator' of github.com:PennyLaneAI/pe…
andrijapau Jun 17, 2026
466205a
cl
andrijapau Jun 17, 2026
fb914fb
improvements
andrijapau Jun 17, 2026
1239b3c
fix
andrijapau Jun 17, 2026
186b2d3
fix fixed_sig type hint
andrijapau Jun 17, 2026
99cadea
whoops
andrijapau Jun 17, 2026
676b178
update changelog
andrijapau Jun 17, 2026
1bd78f6
add tests for equal and hash
andrijapau Jun 17, 2026
c8fbc43
fix hashing
andrijapau Jun 17, 2026
1eb4bf5
Trigger CI
andrijapau Jun 18, 2026
ec9be1e
add tests for equality
andrijapau Jun 18, 2026
ed9efe6
add repr test
andrijapau Jun 18, 2026
1237157
fix wording in equal
andrijapau Jun 18, 2026
31f262a
fix pylint
andrijapau Jun 18, 2026
930d090
whoops, fix
andrijapau Jun 19, 2026
6cdf0ad
rename some vars
andrijapau Jun 19, 2026
a085dea
fix up tests
andrijapau Jun 19, 2026
b884149
Apply suggestion from @andrijapau
andrijapau Jun 19, 2026
6d2ee9c
fix tach and imports
andrijapau Jun 19, 2026
1686cd0
Merge branch 'fix/skip-child-constructor-if-abstract' into feat/abstr…
andrijapau Jun 19, 2026
54c25fa
use abstractify for metaclass
andrijapau Jun 19, 2026
5ef7abe
Update pennylane/core/operator/operator2.py
andrijapau Jun 19, 2026
0da7a60
fix
andrijapau Jun 19, 2026
da00b6a
fix error
andrijapau Jun 19, 2026
b7694c1
whoops cc
andrijapau Jun 19, 2026
78631b3
Update pennylane/core/operator/operator2.py
andrijapau Jun 19, 2026
26711f8
remove dead code
andrijapau Jun 19, 2026
04d0c48
Merge branch 'feat/abstractify-operator' of github.com:PennyLaneAI/pe…
andrijapau Jun 19, 2026
18effe1
Update pennylane/core/operator/operator2.py
andrijapau Jun 22, 2026
d68e4b3
Merge branch 'fix/skip-child-constructor-if-abstract' into feat/abstr…
andrijapau Jun 23, 2026
f314307
fix
andrijapau Jun 23, 2026
de9a717
Trigger CI
andrijapau Jun 23, 2026
b3422f1
Apply suggestion from @andrijapau
andrijapau Jun 23, 2026
cc0ce5a
Merge branch 'fix/skip-child-constructor-if-abstract' into feat/abstr…
andrijapau Jun 23, 2026
73c0436
fix
andrijapau Jun 23, 2026
47a5cc9
fix imports
andrijapau Jun 23, 2026
ae54a88
Merge branch 'fix/skip-child-constructor-if-abstract' into feat/abstr…
andrijapau Jun 23, 2026
1a7e7bb
fix
andrijapau Jun 23, 2026
fcf32ab
fix logic
andrijapau Jun 23, 2026
1d3962c
use new short cuts
andrijapau Jun 23, 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
4 changes: 4 additions & 0 deletions doc/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,10 @@
[(#9685)](https://github.com/PennyLaneAI/pennylane/pull/9685)
[(#9702)](https://github.com/PennyLaneAI/pennylane/pull/9702)

* Added an internal `abstractify` utility function that is able to convert various objects
to their abstract versions.
[(#9694)](https://github.com/PennyLaneAI/pennylane/pull/9694)

* Adds a new `pennylane/core` module.
Moves the abstractions from `pennylane/operation` into `pennylane/core/operator`.
Moves `MeasurementProcess`, `StateMeasurement`, `SampleMeasurement`, `MeasurementTransform`,
Expand Down
2 changes: 2 additions & 0 deletions pennylane/core/operator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
from .channel import Channel
from .cv import CV, CVObservable, CVOperation
from .state_prep import StatePrepBase
from .utils import abstractify

__all__ = [
"Operator",
Expand All @@ -199,4 +200,5 @@
"StatePrepBase",
"Operator1",
"StatePrepBase2",
"abstractify",
]
32 changes: 10 additions & 22 deletions pennylane/core/operator/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@

from pennylane import math
from pennylane.capture import enabled
from pennylane.pytrees import flatten, unflatten
from pennylane.pytrees import flatten
from pennylane.typing import AbstractArray, AbstractWires
from pennylane.wires import Wires

from .utils import abstractify


class _ArgType(Enum):
"""Enum to keep track of an arguments type."""
Expand Down Expand Up @@ -87,9 +89,8 @@ def _canonicalize_abstract_type(val, kind: _ArgType):

match kind:
case _ArgType.WIRES:
# Use Wires object to sanitize inputs
canonical_wires = Wires(val)
return AbstractWires(len(canonical_wires))
# abstractify expects a Wires object for wire-routing, so we sanitize it first
return abstractify(Wires(val))

case _ArgType.DYN:
# A sequence of types is not supported (i.e., [float, float, float])
Expand All @@ -103,26 +104,13 @@ def _canonicalize_abstract_type(val, kind: _ArgType):
"currently supported. Instead, please use the type "
"specifiers found in pennylane.typing."
)
canonical_arr = math.asarray(val)
return AbstractArray(canonical_arr.shape, canonical_arr.dtype)
# Ensure it behaves like a clean array/scalar leaf before abstractifying
return abstractify(math.asarray(val))

case _ArgType.HYBRID:
leaves, structure = flatten(val, is_leaf=lambda x: isinstance(x, Wires))
new_leaves = []
for leaf in leaves:
if isinstance(leaf, (AbstractArray, AbstractWires)):
new_leaves.append(leaf)
elif isinstance(leaf, Wires):
new_leaves.append(AbstractWires(len(leaf)))
elif isinstance(leaf, type) and issubclass(leaf, Number):
new_leaves.append(AbstractArray((), leaf))
# Process arrays
elif hasattr(leaf, "shape") and hasattr(leaf, "dtype"):
new_leaves.append(AbstractArray(leaf.shape, leaf.dtype))
# Process scalars
else:
new_leaves.append(AbstractArray((), type(leaf)))
return unflatten(new_leaves, structure)
# Since abstractify natively handles PyTree recursion and leaves,
# we can pass the entire structure straight through
return abstractify(val)
Comment on lines +92 to +113

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.

Nice


case _: # pragma: no cover
raise ValueError(f"Unknown kind: '{kind}'")
Expand Down
25 changes: 24 additions & 1 deletion pennylane/core/operator/operator2.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

from .base import _UNSET_BATCH_SIZE, Operator, _get_abstract_operator
from .meta import OperatorMeta
from .utils import abstractify

if TYPE_CHECKING:
from pennylane.pauli import PauliSentence
Expand Down Expand Up @@ -917,7 +918,7 @@ def __repr__(self) -> str:
res = value
# Non-hybrid wire arguments
elif key not in self.hybrid_argnames:
res = value.tolist()
res = value.tolist() if isinstance(value, Wires) else value
# Hybrid wire arguments
else:
leaves, tree = flatten(value, is_leaf=_is_wires)
Expand Down Expand Up @@ -1563,6 +1564,8 @@ def _mod_and_round(x, mod_val):
mod_val = None

# We stringify the data because arrays are unhashable
if isinstance(d, AbstractArray):
return str(d)
return str(id(d) if math.is_abstract(d) else _mod_and_round(d, mod_val))


Expand All @@ -1572,6 +1575,26 @@ def _is_hash_leaf(l) -> bool:
return _is_op(l) or _is_wires(l)


@abstractify.register(OperatorMeta)
def _abstractify_operator_type(op_type: type[Operator2]) -> Operator2:
"""Abstractify a subclass of operator."""

if op_type.fixed_sig is not None:
return op_type(*op_type.fixed_sig)

raise TypeError(
f"Operator type '{op_type.__name__}' must define a 'fixed_sig' to be abstractified."
)


@abstractify.register(Operator2)
def _abstractify_operator(op: Operator2) -> Operator2:
"""Abstractify an operator."""
leaves, tree = flatten(op, is_leaf=_is_wires)
abstract_leaves = tuple(abstractify(l) for l in leaves)
return unflatten(abstract_leaves, tree)


class StatePrepBase2(Operator2, is_baseclass=True):
"""An interface for state-prep operations."""

Expand Down
60 changes: 60 additions & 0 deletions pennylane/core/operator/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright 2026 Xanadu Quantum Technologies Inc.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at

# http://www.apache.org/licenses/LICENSE-2.0

# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Utilities for operators."""

from functools import singledispatch
from numbers import Number

from pennylane import math
from pennylane.pytrees import flatten, leaf, unflatten
from pennylane.typing import AbstractArray, AbstractWires
from pennylane.wires import Wires


@singledispatch
def abstractify(val) -> AbstractArray:
"""Convert the provided value into an abstract type."""
leaves, tree = flatten(val, is_leaf=lambda x: isinstance(x, Wires))
if tree != leaf:
abstract_leaves = tuple(abstractify(l) for l in leaves)
return unflatten(abstract_leaves, tree)

if isinstance(val, Number):
return AbstractArray((), type(val))

shape = math.shape(val)
dtype = math.get_dtype_name(val)
return AbstractArray(shape, dtype)


@abstractify.register(type)
def _abstractify_type(val: type) -> AbstractArray:
"""Abstractify a type."""
if issubclass(val, Number):
return AbstractArray((), val)

raise NotImplementedError(f"Cannot abstractify type '{val}'")


@abstractify.register(Wires)
def _abstractify_wires(val: Wires) -> AbstractWires:
"""Abstractify wires."""
return AbstractWires(len(val))


@abstractify.register(AbstractArray | AbstractWires)

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.

Should this go in typing.py? Only issue there is we pick up a dependency for typing to core.operator.

def _abstractify_abstract_type(
val: AbstractArray | AbstractWires,
) -> AbstractArray | AbstractWires:
"""Abstractify an abstract type, i.e., do nothing."""
return val
3 changes: 1 addition & 2 deletions pennylane/math/single_dispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,7 @@ def autograd_get_dtype_name(x):
"""A autograd version of get_dtype_name that can handle array boxes."""
# abstract array comes from PL so is treated as a autograd array
if x.__class__.__name__ == "AbstractArray":
# will always be a numpy dtype
return x.dtype.name
return np.dtype(x.dtype).name

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.

This can be reverted very soon

# this function seems to only get called with x is an arraybox.
return ar.get_dtype_name(x._value)

Expand Down
25 changes: 24 additions & 1 deletion pennylane/ops/functions/equal.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
from pennylane.tape import QuantumScript
from pennylane.templates import SubroutineOp
from pennylane.templates.subroutines import QSVT, ControlledSequence, PrepSelPrep, Select
from pennylane.typing import TensorLike
from pennylane.typing import AbstractArray, AbstractWires, TensorLike

OPERANDS_MISMATCH_ERROR_MESSAGE = "op1 and op2 have different operands because "

Expand Down Expand Up @@ -461,6 +461,18 @@ def _equal_operator2(

def _check_wire_value(wname: str, wval1: Any, wval2: Any):
"""Check for equality of a wire argument of an Operator2 instance."""

is_aw1 = isinstance(wval1, AbstractWires)
is_aw2 = isinstance(wval2, AbstractWires)

if is_aw1 and is_aw2:
if wval1 == wval2:
return True
return f"op1 and op2 have different AbstractWires type specifiers for {wname}: Got {wval1} and {wval2}."

if is_aw1 != is_aw2:
return f"Mismatched wire representations for {wname}. One operator has an abstract wires type specifier while the other has concrete or traced wires. Got {wval1} and {wval2}."

unequal_wires = False
abstract_wires = False

Expand Down Expand Up @@ -498,6 +510,17 @@ def _check_dynamic_value(
atol=1e-9,
):
"""Check for equality of a dynamic argument of an Operator2 instance."""
is_aa1 = isinstance(dval1, AbstractArray)
is_aa2 = isinstance(dval2, AbstractArray)

# Note: A mixed state (is_aa1 != is_aa2) is structurally impossible under normal
# execution because Operator2's metaclass ensures abstract operators are fully abstract
# and so the wires check would fail first
if is_aa1 and is_aa2:
if dval1 == dval2:
return True
return f"op1 and op2 have different AbstractArray type specifiers for {dname}: Got {dval1} and {dval2}."

if math.is_abstract(dval1) or math.is_abstract(dval2):
return (
f"At least one of op1 or op2 has a tracer value for '{dname}'. Abstract "
Expand Down
2 changes: 1 addition & 1 deletion tach.toml
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ path = "pennylane.core.operator"
cannot_depend_on = ["pennylane.ftqc", "pennylane.labs"]

[[interfaces]]
expose = ["Operator", "Operator1", "Operator2", "Operation", "CV", "CVObservable", "CVOperation", "Channel", "StatePrepBase", "StatePrepBase2"]
expose = ["Operator", "Operator1", "Operator2", "Operation", "CV", "CVObservable", "CVOperation", "Channel", "StatePrepBase", "StatePrepBase2", "abstractify"]
from = ["pennylane.core.operator"]

[[modules]]
Expand Down
9 changes: 9 additions & 0 deletions tests/core/operator/test_operator2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,15 @@ def __init__(self, phi, wires):
class TestDunderMethods:
"""Tests for ``Operator2`` dunder methods."""

def test_repr_with_abstract_args(self):
"""Tests that abstract wires properly render."""

op = DynOp(AbstractArray((1, 2), float), AbstractWires(1))
assert (
repr(op)
== "DynOp(phi=AbstractArray((1, 2), float64, weak_type=True), wires=AbstractWires(1))"
)

def test_repr_with_dynamic_args(self):
"""Test that __repr__ includes dynamic parameters if present."""
op = DynOp(0.5, wires=[0, 1])
Expand Down
51 changes: 51 additions & 0 deletions tests/core/operator/test_operator2_equality.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

import pennylane as qp
from pennylane import numpy as pnp
from pennylane.core.operator import Operator2
from pennylane.typing import AbstractArray, AbstractWires

# ---------------------- Tests ----------------------

Expand Down Expand Up @@ -423,6 +425,55 @@ def test_each_group_difference_detected(self, diff, match):
qp.assert_equal(op1, op2)


class TestAbstractOperatorEquality:
"""Tests the equality of abstract operators."""

@pytest.mark.parametrize(
"wires1, wires2, are_equal",
[
(AbstractWires(1), AbstractWires(1), True),
(AbstractWires(1), AbstractWires(2), False),
],
)
def test_abstract_wires(self, wires1, wires2, are_equal):
"""Test that operators with abstract wires are detected correctly."""

# pylint: disable=useless-parent-delegation
class SimpleOp(Operator2):
def __init__(self, wires) -> None:
super().__init__(wires)

op1 = SimpleOp(wires1)
op2 = SimpleOp(wires2)

assert qp.equal(op1, op2) is are_equal

def test_comparing_abstract_and_concrete_wires(self):
"""Assert that an abstract type specifier is not the same as an operator instance."""

# pylint: disable=useless-parent-delegation
class SimpleOp(Operator2):
def __init__(self, wires) -> None:
super().__init__(wires)

op1 = SimpleOp(AbstractWires(1))
op2 = SimpleOp([0])

assert qp.equal(op1, op2) is False

def test_comparing_dynamic_args(self):
"""Tests that abstract dynamic args behave correctly."""

op1 = DynOp(AbstractArray((1, 2), float), 0)
op2 = DynOp(AbstractArray((1, 2), float), 0)
op3 = DynOp(AbstractArray((2, 1), int), 0)
op4 = DynOp(3.14, 0)

assert qp.equal(op1, op2)
assert not qp.equal(op1, op3)
assert not qp.equal(op1, op4)


def _jit_eq_fn(phi, wires, assert_=False):
op1 = DynOp(phi, wires=wires)
op2 = DynOp(phi, wires=wires)
Expand Down
Loading
Loading