Repository: PennyLaneAI/pennylane
Pull request #7839 (2025-07-10 07:30:11)

Update README image paths for PyPI compatibility
**Context:**
Some images in the `README.md` are not rendering properly on [PyPI](https://pypi.org/project/PennyLane/) because PyPI does not support relative image paths.

**Description of the Change:**
This PR updates image references in `README.md` to use absolute URLs pointing to raw GitHub content to ensure images render correctly on both GitHub and PyPI.


**Notes:**
Please note that we had decided to use a relative path for certain images in the past to support GitHub’s dark mode rendering. See https://github.com/PennyLaneAI/pennylane/pull/2341 for more details.




github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.

Patch:
diff --git a/README.md b/README.md
index c2476199f..de5f9792c 100644
--- a/README.md
+++ b/README.md
@@ -84,7 +84,7 @@ Get up and running quickly with PennyLane by following our [quickstart guide](ht
 
 Whether you're exploring quantum machine learning (QML), quantum computing, or quantum chemistry, PennyLane offers a wide range of tools and resources to support your research:
 
-<img src="./doc/_static/readme/research.png" align="right" width="350px">
+<img src="https://raw.githubusercontent.com/PennyLaneAI/pennylane/master/doc/_static/readme/research.png" align="right" width="350px">
 
 ### Key Resources:
 
@@ -106,7 +106,7 @@ quantum device.
 Take a deeper dive into quantum computing by exploring cutting-edge algorithms using PennyLane and quantum hardware. [Explore PennyLane demos](https://pennylane.ai/qml/demonstrations.html).
 
 <a href="https://pennylane.ai/qml/demonstrations">
-  <img src="./doc/_static/readme/demos.png" width="900px">
+  <img src="https://raw.githubusercontent.com/PennyLaneAI/pennylane/master/doc/_static/readme/demos.png" width="900px">
 </a>
 
 If you would like to contribute your own demo, see our [demo submission
diff --git a/doc/releases/changelog-0.42.0.md b/doc/releases/changelog-0.42.0.md
index 5ca556e31..0b91aaa95 100644
--- a/doc/releases/changelog-0.42.0.md
+++ b/doc/releases/changelog-0.42.0.md
@@ -1195,6 +1195,10 @@ may move operations across a `Snapshot`.
 
 <h3>Bug fixes 🐛</h3>
 
+* Fixes a bug where normalization in `qml.StatePrep` with `normalize=True` was skipped if
+  `validate_norm` is set to `False`.
+  [(#7835)](https://github.com/PennyLaneAI/pennylane/pull/7835) 
+
 * Fixes broken support of `qml.matrix` for a `QNode` when using mixed Torch GPU & CPU data for parametric tensors.
   [(#7775)](https://github.com/PennyLaneAI/pennylane/pull/7775) 
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 6d909d7b0..1e0d7119f 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -6,6 +6,22 @@
 
 <h3>Improvements 🛠</h3>
 
+* A compilation pass written with xDSL called `qml.compiler.python_compiler.transforms.MeasurementsFromSamplesPass`
+  has been added for the experimental xDSL Python compiler integration. This pass replaces all
+  terminal measurements in a program with a single :func:`pennylane.sample` measurement, and adds
+  postprocessing instructions to recover the original measurement.
+  [(#7620)](https://github.com/PennyLaneAI/pennylane/pull/7620)
+
+* The `mbqc` xDSL dialect has been added to the Python compiler, which is used to represent
+  measurement-based quantum-computing instructions in the xDSL framework.
+  [(#7815)](https://github.com/PennyLaneAI/pennylane/pull/7815)
+
+<h3>Labs: a place for unified and rapid prototyping of research software 🧪</h3>
+
+* Added state of the art resources for the `ResourceSelectPauliRot` template and the
+  `ResourceQubitUnitary` templates.
+  [(#7786)](https://github.com/PennyLaneAI/pennylane/pull/7786)
+
 <h3>Breaking changes 💔</h3>
 
 <h3>Deprecations 👋</h3>
@@ -24,4 +40,6 @@
 
 This release contains contributions from (in alphabetical order):
 
+Joey Carter,
 Andrija Paurevic,
+Jay Soni,
diff --git a/pennylane/compiler/python_compiler/mbqc_dialect.py b/pennylane/compiler/python_compiler/mbqc_dialect.py
new file mode 100644
index 000000000..418b2dd9e
--- /dev/null
+++ b/pennylane/compiler/python_compiler/mbqc_dialect.py
@@ -0,0 +1,126 @@
+# Copyright 2025 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.
+"""
+This module contains the definition of the MBQC dialect for the Python compiler.
+
+The MBQC dialect is a set of operations and types used to represent measurement-based
+quantum-computing instructions in the xDSL framework.
+
+It was initially generated by xDSL (using the ``xdsl-tblgen`` tool) starting from the
+catalyst/mlir/include/MBQC/IR/MBQCDialect.td file in the catalyst repository.
+"""
+
+from typing import Optional, TypeAlias
+
+from xdsl.dialects.builtin import I32, Float64Type, IntegerAttr, IntegerType
+from xdsl.ir import Dialect, EnumAttribute, Operation, SSAValue
+from xdsl.irdl import (
+    IRDLOperation,
+    irdl_attr_definition,
+    irdl_op_definition,
+    operand_def,
+    opt_prop_def,
+    prop_def,
+    result_def,
+)
+from xdsl.utils.exceptions import VerifyException
+from xdsl.utils.str_enum import StrEnum  # StrEnum is standard in Python>=3.11
+
+from pennylane.compiler.python_compiler.quantum_dialect import QubitType
+
+QubitSSAValue: TypeAlias = SSAValue[QubitType]
+
+
+class MeasurementPlaneEnum(StrEnum):
+    """Enum containing supported measurement-plane attributes"""
+
+    # pylint: disable=too-few-public-methods
+
+    XY = "XY"
+    YZ = "YZ"
+    ZX = "ZX"
+
+
+@irdl_attr_definition
+class MeasurementPlaneAttr(EnumAttribute[MeasurementPlaneEnum]):
+    """Planes in the Bloch sphere representation with support for arbitrary-basis measurements"""
+
+    # pylint: disable=too-few-public-methods
+
+    name = "mbqc.measurement_plane"
+
+
+@irdl_op_definition
+class MeasureInBasisOp(IRDLOperation):
+    """A parametric single-qubit projective measurement in an arbitrary basis."""
+
+    # pylint: disable=too-few-public-methods
+
+    name = "mbqc.measure_in_basis"
+
+    in_qubit = operand_def(QubitType)
+
+    plane = prop_def(MeasurementPlaneAttr)
+
+    angle = operand_def(Float64Type())
+
+    postselect = opt_prop_def(IntegerAttr[I32])
+
+    mres = result_def(IntegerType(1))
+
+    out_qubit = result_def(QubitType)
+
+    assembly_format = """
+            `[` $plane `,` $angle `]` $in_qubit (`postselect` $postselect^)? attr-dict `:` type(results)
+        """
+
+    def __init__(
+        self,
+        in_qubit: QubitSSAValue | Operation,
+        plane: MeasurementPlaneAttr,
+        angle: SSAValue[Float64Type],
+        postselect: Optional[int | IntegerAttr] = None,
+    ):
+        properties = {"plane": plane}
+
+        if isinstance(postselect, int):
+            postselect = IntegerAttr.from_int_and_width(postselect, 32)
+
+        if postselect is not None:
+            properties["postselect"] = postselect
+
+        super().__init__(
+            operands=(in_qubit, angle),
+            properties=properties,
+            result_types=(IntegerType(1), QubitType()),
+        )
+
+    def verify_(self):
+        """Verify operation when rewriting."""
+        if self.postselect is None:
+            return
+
+        if self.postselect.value.data not in [0, 1]:
+            raise VerifyException("'postselect' must be 0 or 1.")
+
+
+MBQCDialect = Dialect(
+    "mbqc",
+    [
+        MeasureInBasisOp,
+    ],
+    [
+        MeasurementPlaneAttr,
+    ],
+)
diff --git a/pennylane/compiler/python_compiler/transforms/__init__.py b/pennylane/compiler/python_compiler/transforms/__init__.py
index ab0d3d2c2..34a2cd545 100644
--- a/pennylane/compiler/python_compiler/transforms/__init__.py
+++ b/pennylane/compiler/python_compiler/transforms/__init__.py
@@ -14,11 +14,14 @@
 """PennyLane-xDSL transformations API."""
 
 from .cancel_inverses import IterativeCancelInversesPass, iterative_cancel_inverses_pass
+from .measurements_from_samples import MeasurementsFromSamplesPass, measurements_from_samples_pass
 from .merge_rotations import MergeRotationsPass, merge_rotations_pass
 
 __all__ = [
     "iterative_cancel_inverses_pass",
     "IterativeCancelInversesPass",
+    "measurements_from_samples_pass",
+    "MeasurementsFromSamplesPass",
     "merge_rotations_pass",
     "MergeRotationsPass",
 ]
diff --git a/pennylane/compiler/python_compiler/transforms/measurements_from_samples.py b/pennylane/compiler/python_compiler/transforms/measurements_from_samples.py
new file mode 100644
index 000000000..5e419536e
--- /dev/null
+++ b/pennylane/compiler/python_compiler/transforms/measurements_from_samples.py
@@ -0,0 +1,660 @@
+# Copyright 2025 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.
+
+"""This module contains the implementation of the measurements_from_samples transform,
+written using xDSL.
+
+Known Limitations
+-----------------
+
+  * Only measurements in the computational basis (or where the observable is a Pauli Z op) are
+    currently supported; for arbitrary observables we require an equivalent compilation pass of the
+    diagonalize_measurements transform.
+  * The compilation pass assumes a static number of shots.
+  * Usage patterns that are not yet supported with program capture are also not supported in the
+    compilation pass. For example, operator arithmetic is not currently supported, such as
+    qml.expval(qml.Y(0) @ qml.X(1)).
+  * qml.counts() is not supported since the return type/shape is different in PennyLane and Catalyst.
+    See https://docs.pennylane.ai/projects/catalyst/en/stable/dev/quick_start.html#measurements
+    for more information.
+"""
+
+from abc import abstractmethod
+from dataclasses import dataclass
+from itertools import islice
+from typing import Optional
+
+import jax
+import jax.numpy as jnp
+from xdsl import context, ir, passes, pattern_rewriter
+from xdsl.dialects import arith, builtin, func, tensor
+from xdsl.pattern_rewriter import PatternRewriter, RewritePattern
+from xdsl.rewriter import InsertPoint
+
+from pennylane.compiler.compiler import CompileError
+from pennylane.compiler.python_compiler import quantum_dialect as quantum
+from pennylane.compiler.python_compiler.jax_utils import xdsl_module
+from pennylane.compiler.python_compiler.transforms.api import compiler_transform
+
+
+@dataclass(frozen=True)
+class MeasurementsFromSamplesPass(passes.ModulePass):
+    """Pass that replaces all terminal measurements in a program with a single
+    :func:`pennylane.sample` measurement, and adds postprocessing instructions to recover the
+    original measurement.
+    """
+
+    name = "measurements-from-samples"
+
+    # pylint: disable=arguments-renamed,no-self-use
+    def apply(self, _ctx: context.Context, module: builtin.ModuleOp) -> None:
+        """Apply the measurements-from-samples pass."""
+        shots = _get_static_shots_value_from_first_device_op(module)
+
+        greedy_applier = pattern_rewriter.GreedyRewritePatternApplier(
+            [
+                ExpvalAndVarPattern(shots),
+                ProbsPattern(shots),
+                CountsPattern(shots),
+                StatePattern(shots),
+            ]
+        )
+        walker = pattern_rewriter.PatternRewriteWalker(greedy_applier, apply_recursively=False)
+        walker.rewrite_module(module)
+
+
+measurements_from_samples_pass = compiler_transform(MeasurementsFromSamplesPass)
+
+
+# pylint: disable=too-few-public-methods
+class MeasurementsFromSamplesPattern(RewritePattern):
+    """Rewrite pattern base class for the ``measurements_from_samples`` transform, which replaces
+    all terminal measurements in a program with a single :func:`pennylane.sample` measurement, and
+    adds postprocessing instructions to recover the original measurement.
+
+    Args:
+        shots (int): The number of shots (e.g. as retrieved from the DeviceInitOp).
+    """
+
+    def __init__(self, shots: int):
+        super().__init__()
+        assert isinstance(
+            shots, int
+        ), f"Expected `shots` to be an integer value but got {type(shots).__name__}"
+        self._shots = shots
+
+    @abstractmethod
+    def match_and_rewrite(self, op: ir.Operation, rewriter: PatternRewriter, /):
+        """Abstract method for measurements-from-samples match-and-rewrite patterns."""
+
+    @classmethod
+    def get_observable_op(cls, op: quantum.ExpvalOp | quantum.VarianceOp) -> quantum.NamedObsOp:
+        """Return the observable op (quantum.NamedObsOp) given as an input operand to `op`.
+
+        We assume that `op` is either a quantum.ExpvalOp or quantum.VarianceOp, but this is not
+        strictly enforced.
+
+        Args:
+            op (quantum.ExpvalOp | quantum.VarianceOp): The op that uses the observable op.
+
+        Returns:
+            quantum.NamedObsOp: The observable op.
+        """
+        observable_op = op.operands[0].owner
+        cls._validate_observable_op(observable_op)
+
+        return observable_op
+
+    @staticmethod
+    def _validate_observable_op(op: quantum.NamedObsOp):
+        """Validate the observable op.
+
+        Assert that the op is a quantum.NamedObsOp and check if it is supported in the current
+        implementation of the measurements-from-samples transform.
+
+        Raises:
+            NotImplementedError: If the observable is anything but a PauliZ quantum.NamedObsOp.
+        """
+        assert isinstance(
+            op, quantum.NamedObsOp
+        ), f"Expected `op` to be a quantum.NamedObsOp, but got {type(op).__name__}"
+
+        if op.type.data != "PauliZ":
+            raise NotImplementedError(
+                f"Observable '{op.type.data}' used as input to measurement operation is not "
+                f"supported for the measurements_from_samples transform; currently only the "
+                f"PauliZ observable is permitted"
+            )
+
+    @staticmethod
+    def insert_compbasis_op(
+        in_qubit: ir.SSAValue, ref_op: ir.Operation, rewriter: PatternRewriter
+    ) -> quantum.ComputationalBasisOp:
+        """Create and insert a computational-basis op (quantum.ComputationalBasisOp).
+
+        The computation-basis op uses `in_qubit` as its input operand. It is inserted *before* the
+        given reference operation, `ref_op`, using the supplied `rewriter`.
+
+        Args:
+            in_qubit (SSAValue): The SSA value used as input to the computational-basis op.
+            ref_op (Operation): The reference op before which the quantum.ComputationalBasisOp is
+                inserted.
+            rewriter (PatternRewriter): The xDSL pattern rewriter.
+
+        Returns:
+            quantum.ComputationalBasisOp: The inserted computation-basis op.
+        """
+        assert isinstance(in_qubit, ir.SSAValue) and isinstance(in_qubit.type, quantum.QubitType), (
+            f"Expected `in_qubit` to be an SSAValue with type quantum.QubitType, but got "
+            f"{type(in_qubit).__name__}"
+        )
+
+        # The input operands are [[qubit, ...], qreg]
+        compbasis_op = quantum.ComputationalBasisOp(
+            operands=[in_qubit, None], result_types=[quantum.ObservableType()]
+        )
+        rewriter.insert_op(compbasis_op, insertion_point=InsertPoint.before(ref_op))
+
+        return compbasis_op
+
+    @staticmethod
+    def insert_sample_op(
+        compbasis_op: quantum.ComputationalBasisOp,
+        shots: int,
+        n_qubits: int,
+        rewriter: PatternRewriter,
+    ) -> quantum.SampleOp:
+        """Create and insert a sample op (quantum.SampleOp).
+
+        The type of the returned samples array is currently restricted to be static, with shape
+        (shots, n_qubits).
+
+        The sample op is inserted after the supplied `compbasis_op`.
+
+        Args:
+            compbasis_op (quantum.ComputationalBasisOp): The computational-basis op used as the
+                input operand to the sample op.
+            shots (int): Number of shots (to set the shape of the sample op returned array).
+            n_qubits (int): Number of qubits (to set the shape of the sample op returned array).
+            rewriter (PatternRewriter): The xDSL pattern rewriter.
+
+        Returns:
+            quantum.SampleOp: The inserted sample op.
+        """
+        assert isinstance(compbasis_op, quantum.ComputationalBasisOp), (
+            f"Expected `compbasis_op` to be a quantum.ComputationalBasisOp, but got "
+            f"{type(compbasis_op).__name__}"
+        )
+
+        sample_op = quantum.SampleOp(
+            operands=[compbasis_op.results[0], None, None],
+            result_types=[builtin.TensorType(builtin.Float64Type(), [shots, n_qubits])],
+        )
+        rewriter.insert_op(sample_op, insertion_point=InsertPoint.after(compbasis_op))
+
+        return sample_op
+
+    @staticmethod
+    def get_postprocessing_func_op_from_block_by_name(
+        block: ir.Block, name: str
+    ) -> func.FuncOp | None:
+        """Return the post-processing FuncOp from the given `block` with the given `name`.
+
+        If the block does not contain a FuncOp with the matching name, returns None.
+
+        Args:
+            block (Block): The xDSL block to search.
+            name (str): The name of the post-processing FuncOp.
+
+        Returns:
+            func.FuncOp: The FuncOp with matching name.
+            None: If no match was found.
+        """
+        for op in block.ops:
+            if isinstance(op, func.FuncOp) and op.sym_name.data == name:
+                return op
+
+        return None
+
+    @classmethod
+    def get_postprocessing_funcs_from_module_and_insert(
+        cls,
+        postprocessing_module: builtin.ModuleOp,
+        matched_op: ir.Operation,
+        name: Optional[str] = None,
+    ) -> func.FuncOp:
+        """Get the post-processing FuncOp from `postprocessing_module` (and any helper functions
+        also contained in `postprocessing_module`) and insert it (them) immediately after the FuncOp
+        (in the same block) that contains `matched_op`.
+
+        The post-processing function recovers the original measurement process result from the
+        samples array. This post-postprocessing function is optionally renamed to `name`, if given.
+
+        Args:
+            postprocessing_module (builtin.ModuleOp): The MLIR module containing the post-processing
+                FuncOp.
+            matched_op (Operation): The reference op, the parent of which is used as the
+                reference point when inserting the post-processing FuncOp. This is usually the op
+                matched in the call to match_and_rewrite().
+            name (str, optional): The name to assign to the post-processing FuncOp, if given.
+
+        Returns:
+            func.FuncOp: The inserted post-processing FuncOp.
+        """
+        parent_func_op = matched_op.parent_op()
+
+        assert isinstance(parent_func_op, func.FuncOp), (
+            f"Expected parent of matched op '{matched_op}' to be a func.FuncOp, but got "
+            f"{type(parent_func_op).__name__}"
+        )
+
+        # This first op in `postprocessing_module` is the "main" post-processing function
+        postprocessing_func_op = postprocessing_module.body.ops.first.clone()
+        assert isinstance(postprocessing_func_op, func.FuncOp), (
+            f"Expected the first operator of `postprocessing_module` to be a func.FuncOp but "
+            f"got {type(postprocessing_func_op).__name__}"
+        )
+
+        # The function name from jax.jit is 'main'; rename it here
+        if name is not None:
+            postprocessing_func_op.sym_name = builtin.StringAttr(data=name)
+
+        parent_block = parent_func_op.parent
+        parent_block.insert_op_after(postprocessing_func_op, parent_func_op)
+
+        # Get and insert helper functions, if any
+        if len(postprocessing_module.body.ops) > 1:
+            prev_op = postprocessing_func_op
+            for _op in islice(postprocessing_module.body.ops, 1, None):
+                helper_op = _op.clone()
+                parent_block.insert_op_after(helper_op, prev_op)
+                prev_op = helper_op
+
+        return postprocessing_func_op
+
+    @staticmethod
+    def insert_constant_int_op(
+        value: int,
+        insert_point: InsertPoint,
+        rewriter: PatternRewriter,
+        value_type: int = 64,
+    ) -> arith.ConstantOp:
+        """Create and insert a constant op with the given integer value.
+
+        The integer value is contained within a rankless, dense tensor.
+
+        Args:
+            value (int): The integer value.
+            insert_point (InsertPoint): The insertion point for the constant op.
+            rewriter (PatternRewriter): The xDSL pattern rewriter.
+            value_type (int, optional): The integer value type (i.e. number of bits). Defaults to 64.
+
+        Returns:
+            arith.ConstantOp: The created constant op.
+        """
+        constant_int_op = arith.ConstantOp(
+            builtin.DenseIntOrFPElementsAttr.from_list(
+                type=builtin.TensorType(builtin.IntegerType(value_type), shape=()), data=(value,)
+            )
+        )
+        rewriter.insert_op(constant_int_op, insertion_point=insert_point)
+
+        return constant_int_op
+
+    @staticmethod
+    def get_n_qubits_from_qreg(qreg: ir.SSAValue):
+        """Get the number of qubits from a qreg SSA value.
+
+        This method walks back through the SSA graph from the qreg until it reaches its root
+        quantum.alloc op, or alloc-like op (with possibly zero or more quantum.insert ops
+        in-between), from which the number of qubits is extracted.
+
+        An op is "alloc-like" if it has an 'nqubits_attr' attribute.
+
+        Args:
+            qreg (SSAValue): The qreg SSA value.
+        """
+        assert isinstance(qreg, ir.SSAValue) and isinstance(qreg.type, quantum.QuregType), (
+            f"Expected `qreg` to be an SSAValue with type quantum.QuregType, but got "
+            f"{type(qreg).__name__}"
+        )
+
+        def _walk_back_to_alloc_op(
+            insert_or_alloc_op: quantum.AllocOp | quantum.InsertOp,
+        ) -> quantum.AllocOp | None:
+            """Recursively walk back from a quantum.insert op to its root quantum.alloc op or
+            alloc-like op.
+
+            Once found, return the quantum.alloc op.
+            """
+            if (
+                isinstance(insert_or_alloc_op, quantum.AllocOp)
+                or insert_or_alloc_op.properties.get("nqubits_attr") is not None
+            ):
+                return insert_or_alloc_op
+
+            if isinstance(insert_or_alloc_op, quantum.InsertOp):
+                return _walk_back_to_alloc_op(insert_or_alloc_op.operands[0].owner)
+
+            return None
+
+        alloc_op = _walk_back_to_alloc_op(qreg.owner)
+        assert alloc_op is not None, "Unable to walk back from qreg to alloc op"
+
+        nqubits_attr = alloc_op.properties.get("nqubits_attr")
+        assert (
+            nqubits_attr is not None
+        ), "Unable to determine number of qubits from alloc op; missing property 'nqubits_attr'"
+
+        n_qubits = nqubits_attr.value.data
+        assert n_qubits is not None, "Unable to determine number of qubits from qreg SSA value"
+
+        return n_qubits
+
+
+class ExpvalAndVarPattern(MeasurementsFromSamplesPattern):
+    """A rewrite pattern for the ``measurements_from_samples`` transform that matches and rewrites
+    ``qml.expval()`` and ``qml.var()`` operations.
+
+    Args:
+        shots (int): The number of shots (e.g. as retrieved from the DeviceInitOp).
+    """
+
+    # pylint: disable=arguments-renamed
+    @pattern_rewriter.op_type_rewrite_pattern
+    def match_and_rewrite(
+        self, matched_op: quantum.ExpvalOp | quantum.VarianceOp, rewriter: PatternRewriter, /
+    ):
+        """Match and rewrite for quantum.ExpvalOp."""
+        observable_op = self.get_observable_op(matched_op)
+        in_qubit = observable_op.operands[0]
+        compbasis_op = self.insert_compbasis_op(in_qubit, observable_op, rewriter)
+        sample_op = self.insert_sample_op(compbasis_op, self._shots, 1, rewriter)
+
+        # Insert the post-processing function into current module or get handle to it if already
+        # inserted
+        match matched_op:
+            case quantum.ExpvalOp():
+                postprocessing_func_name = f"expval_from_samples.tensor.{self._shots}x1xf64"
+                postprocessing_jit_func = _postprocessing_expval
+            case quantum.VarianceOp():
+                postprocessing_func_name = f"var_from_samples.tensor.{self._shots}x1xf64"
+                postprocessing_jit_func = _postprocessing_var
+            case _:
+                assert False, (
+                    f"Expected a quantum.ExpvalOp or quantum.VarianceOp, but got "
+                    f"{type(matched_op).__name__}"
+                )
+
+        postprocessing_func_op = self.get_postprocessing_func_op_from_block_by_name(
+            matched_op.parent_op().parent, postprocessing_func_name
+        )
+
+        if postprocessing_func_op is None:
+            # TODO: Do we have to set the shape of the samples array statically here? Or can the
+            # shape (shots, wire) be dynamic and given as SSA values?
+            # Same goes for the column/wire indices (the second argument).
+            postprocessing_module = postprocessing_jit_func(
+                jax.core.ShapedArray([self._shots, 1], float), 0
+            )
+
+            postprocessing_func_op = self.get_postprocessing_funcs_from_module_and_insert(
+                postprocessing_module, matched_op, postprocessing_func_name
+            )
+
+        # Insert the op that specifies which column in the samples array to access.
+        # TODO: This also assumes MP acts on a single wire (hence we always use column 0 of the
+        # samples here); what if MP acts on multiple wires?
+        column_index_op = self.insert_constant_int_op(
+            0, insert_point=InsertPoint.after(sample_op), rewriter=rewriter
+        )
+
+        # Insert the call to the post-processing function
+        postprocessing_func_call_op = func.CallOp(
+            callee=builtin.FlatSymbolRefAttr(postprocessing_func_op.sym_name),
+            arguments=[sample_op.results[0], column_index_op],
+            return_types=[builtin.TensorType(builtin.Float64Type(), shape=())],
+        )
+
+        # The op to replace is not the expval/var op itself, but the tensor.from_elements op that follows
+        op_to_replace = list(matched_op.results[0].uses)[0].operation
+        assert isinstance(
+            op_to_replace, tensor.FromElementsOp
+        ), f"Expected to replace a tensor.from_elements op, but got {type(op_to_replace).__name__}"
+        rewriter.replace_op(op_to_replace, postprocessing_func_call_op)
+
+        # Finally, erase the expval/var op and its associated observable op
+        rewriter.erase_matched_op()
+        rewriter.erase_op(observable_op)
+
+
+class ProbsPattern(MeasurementsFromSamplesPattern):
+    """A rewrite pattern for the ``measurements_from_samples`` transform that matches and rewrites
+    ``qml.probs()`` operations.
+
+    Args:
+        shots (int): The number of shots (e.g. as retrieved from the DeviceInitOp).
+    """
+
+    # pylint: disable=arguments-renamed
+    @pattern_rewriter.op_type_rewrite_pattern
+    def match_and_rewrite(self, probs_op: quantum.ProbsOp, rewriter: PatternRewriter, /):
+        """Match and rewrite for quantum.ProbsOp."""
+        compbasis_op = probs_op.operands[0].owner
+
+        n_qubits = None
+        if compbasis_op.qreg is not None:
+            n_qubits = self.get_n_qubits_from_qreg(compbasis_op.qreg)
+
+        elif not compbasis_op.qubits == ():
+            n_qubits = len(compbasis_op.qubits)
+
+        assert (
+            n_qubits is not None
+        ), "Unable to determine number of qubits from quantum.compbasis op"
+
+        sample_op = self.insert_sample_op(compbasis_op, self._shots, n_qubits, rewriter)
+
+        # Insert the post-processing function into current module or
+        # get handle to it if already inserted
+        postprocessing_func_name = f"probs_from_samples.tensor.{self._shots}x{n_qubits}xf64"
+
+        postprocessing_func_op = self.get_postprocessing_func_op_from_block_by_name(
+            probs_op.parent_op().parent, postprocessing_func_name
+        )
+
+        if postprocessing_func_op is None:
+            # TODO: Do we have to set the shape of the samples array statically here? Or can the
+            # shape (shots, wire) be dynamic and given as SSA values?
+            # Same goes for the column/wire indices (the second argument).
+            postprocessing_module = _postprocessing_probs(
+                jax.core.ShapedArray([self._shots, n_qubits], float)
+            )
+
+            postprocessing_func_op = self.get_postprocessing_funcs_from_module_and_insert(
+                postprocessing_module, probs_op, postprocessing_func_name
+            )
+
+        # Insert the call to the post-processing function
+        postprocessing_func_call_op = func.CallOp(
+            callee=builtin.FlatSymbolRefAttr(postprocessing_func_op.sym_name),
+            arguments=[sample_op.results[0]],
+            return_types=[builtin.TensorType(builtin.Float64Type(), shape=(2**n_qubits,))],
+        )
+
+        rewriter.replace_matched_op(postprocessing_func_call_op)
+
+
+class CountsPattern(MeasurementsFromSamplesPattern):
+    """A rewrite pattern for the ``measurements_from_samples`` transform that matches and rewrites
+    ``qml.counts()`` operations.
+
+    Currently there is no plan to support ``qml.counts()`` for this transform. It is included for
+    completeness and to notify users that workloads containing ``counts`` measurement processes are
+    not supported with the measurements-from-samples transform.
+
+    Args:
+        shots (int): The number of shots (e.g. as retrieved from the DeviceInitOp).
+    """
+
+    # pylint: disable=arguments-renamed
+    @pattern_rewriter.op_type_rewrite_pattern
+    def match_and_rewrite(self, counts_op: quantum.CountsOp, rewriter: PatternRewriter, /):
+        """Match and rewrite for quantum.CountsOp."""
+        raise NotImplementedError("qml.counts() operations are not supported.")
+
+
+class StatePattern(MeasurementsFromSamplesPattern):
+    """A rewrite pattern for the ``measurements_from_samples`` transform that matches and rewrites
+    ``qml.state()`` operations.
+
+    It is not possible to recover a quantum state from samples; this pattern is included for
+    completeness and to notify users that workloads containing ``state`` measurement processes are
+    not supported with the measurements-from-samples transform.
+
+    Args:
+        shots (int): The number of shots (e.g. as retrieved from the DeviceInitOp).
+    """
+
+    # pylint: disable=arguments-renamed
+    @pattern_rewriter.op_type_rewrite_pattern
+    def match_and_rewrite(self, state_op: quantum.StateOp, rewriter: PatternRewriter, /):
+        """Match and rewrite for quantum.StateOp."""
+        raise NotImplementedError("qml.state() operations are not supported.")
+
+
+def _get_static_shots_value_from_first_device_op(module: builtin.ModuleOp) -> int:
+    """Returns the number of shots as a static (i.e. known at compile time) integer value from the
+    first instance of a device-initialization op (quantum.DeviceInitOp) found in `module`.
+
+    If `module` contains multiple quantum.DeviceInitOp ops, only the number of shots from the
+    *first* instance is used, and the others are ignored.
+
+    This function expects the number of shots to be an SSA value given as an operand to the
+    quantum.DeviceInitOp op. It also assumes that the number of shots is static, retrieving it from
+    the 'value' attribute of its corresponding constant op.
+
+    Args:
+        module (builtin.ModuleOp): The MLIR module containing the quantum.DeviceInitOp.
+
+    Returns:
+        int: The number of shots.
+
+    Raises:
+        CompileError: If `module` does not contain a quantum.DeviceInitOp.
+    """
+    device_op = None
+
+    for op in module.body.walk():
+        if isinstance(op, quantum.DeviceInitOp):
+            device_op = op
+            break
+
+    if device_op is None:
+        raise CompileError(
+            "Cannot get number of shots; the module does not contain a quantum.DeviceInitOp"
+        )
+
+    # The number of shots is passed as an SSA value operand to the DeviceInitOp
+    shots_operand = device_op.shots
+    shots_extract_op = shots_operand.owner
+    assert isinstance(shots_extract_op, tensor.ExtractOp), (
+        f"Expected owner of shots operand to be a tensor.ExtractOp but got "
+        f"{type(shots_extract_op).__name__}"
+    )
+
+    # This should be a stablehlo.ConstantOp that stores the values of `shots`
+    shots_constant_op = shots_extract_op.operands[0].owner
+    shots_value_attribute: builtin.DenseIntOrFPElementsAttr = shots_constant_op.properties.get(
+        "value"
+    )
+    assert (
+        shots_value_attribute is not None
+    ), "Cannot get number of shots; the constant op has no 'value' attribute"
+
+    shots_int_values = shots_value_attribute.get_values()
+    assert len(shots_int_values) == 1, f"Expected a single shots value, got {len(shots_int_values)}"
+
+    return shots_int_values[0]
+
+
+@xdsl_module
+@jax.jit
+def _postprocessing_expval(samples, column):
+    """Post-processing to recover the expectation value from the given `samples` array for each
+    requested `column` in the array.
+
+    This function assumes that the samples are in the computational basis (0s and 1s) and that the
+    observable operand of the expectation value has eigenvalues +1 and -1.
+
+    Args:
+        samples (jax.core.ShapedArray): Array of samples, with shape (shots, wires).
+        column (int, jax.core.ShapedArray): Column index (or indices) of the `samples` array over
+            which the expectation value is computed.
+
+    Returns:
+        jax.core.ShapedArray: The expectation value for each requested column.
+    """
+    return jnp.mean(1.0 - 2.0 * samples[:, column], axis=0)
+
+
+@xdsl_module
+@jax.jit
+def _postprocessing_var(samples, column):
+    """Post-processing to recover the variance from the given `samples` array for each requested
+    `column` in the array.
+
+    This function assumes that the samples are in the computational basis (0s and 1s) and that the
+    observable operand of the variance has eigenvalues +1 and -1.
+
+    Args:
+        samples (jax.core.ShapedArray): Array of samples, with shape (shots, wires).
+        column (int, jax.core.ShapedArray): Column index (or indices) of the `samples` array over
+            which the variance is computed.
+
+    Returns:
+        jax.core.ShapedArray: The variance for each requested column.
+    """
+    return jnp.var(1.0 - 2.0 * samples[:, column], axis=0)
+
+
+@xdsl_module
+@jax.jit
+def _postprocessing_probs(samples):
+    """Post-processing to recover the probability values from the given `samples` array.
+
+    This function assumes that the samples are in the computational basis (0s and 1s).
+
+    Args:
+        samples (jax.core.ShapedArray): Array of samples, with shape (shots, wires).
+    """
+    n_samples = samples.shape[0]
+    n_wires = samples.shape[1]
+
+    # Convert samples from a list of 0, 1 integers to base 10 representation
+    powers_of_two = 2 ** jnp.arange(n_wires)[::-1]
+    indices = samples @ powers_of_two
+    dim = 2**n_wires
+
+    # This block is effectively equivalent to `jnp.bincount(indices.astype(int), length=dim)`.
+    # However, we are currently not able to use jnp.bincount with Catalyst because after lowering,
+    # it contains a stablehlo.scatter op with <indices_are_sorted = false, unique_indices = false>,
+    # which we currently do not support.
+    # If Catalyst PR https://github.com/PennyLaneAI/catalyst/pull/1849 is merged, then we should be
+    # able to use bincount.
+    counts = jnp.zeros(dim, dtype=int)
+    for i in indices.astype(int):
+        counts = counts.at[i].add(1)
+
+    return counts / n_samples
diff --git a/pennylane/labs/resource_estimation/__init__.py b/pennylane/labs/resource_estimation/__init__.py
index 8a702c6be..55bdef8ab 100644
--- a/pennylane/labs/resource_estimation/__init__.py
+++ b/pennylane/labs/resource_estimation/__init__.py
@@ -125,6 +125,8 @@ Templates:
     ~ResourceBasisRotation
     ~ResourceSelect
     ~ResourceQROM
+    ~ResourceSelectPauliRot
+    ~ResourceQubitUnitary
     ~ResourceTrotterCDF
     ~ResourceTrotterTHC
     ~ResourceTrotterVibrational
@@ -212,6 +214,8 @@ from .templates import (
     ResourceBasisRotation,
     ResourceSelect,
     ResourceQROM,
+    ResourceQubitUnitary,
+    ResourceSelectPauliRot,
     ResourceTrotterCDF,
     ResourceTrotterTHC,
     CompactHamiltonian,
diff --git a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py
index df463080d..d8f07c299 100644
--- a/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py
+++ b/pennylane/labs/resource_estimation/ops/op_math/controlled_ops.py
@@ -1422,6 +1422,7 @@ class ResourceCRX(ResourceOperator):
     [(2 x CNOT), (2 x RZ), (2 x Hadamard)]
     """
 
+    resource_keys = {"eps"}
     num_wires = 2
 
     def __init__(self, eps=None, wires=None) -> None:
@@ -1571,6 +1572,7 @@ class ResourceCRY(ResourceOperator):
     [(2 x CNOT), (2 x RY)]
     """
 
+    resource_keys = {"eps"}
     num_wires = 2
 
     def __init__(self, eps=None, wires=None) -> None:
@@ -1718,6 +1720,7 @@ class ResourceCRZ(ResourceOperator):
     [(2 x CNOT), (2 x RZ)]
     """
 
+    resource_keys = {"eps"}
     num_wires = 2
 
     def __init__(self, eps=None, wires=None) -> None:
@@ -1873,6 +1876,7 @@ class ResourceCRot(ResourceOperator):
     [(2 x CNOT), (3 x RZ), (2 x RY)]
     """
 
+    resource_keys = {"eps"}
     num_wires = 2
 
     def __init__(self, eps=None, wires=None) -> None:
@@ -2031,6 +2035,7 @@ class ResourceControlledPhaseShift(ResourceOperator):
     [(2 x CNOT), (3 x RZ)]
     """
 
+    resource_keys = {"eps"}
     num_wires = 2
 
     def __init__(self, eps=None, wires=None) -> None:
diff --git a/pennylane/labs/resource_estimation/resource_tracking.py b/pennylane/labs/resource_estimation/resource_tracking.py
index bb6c6e10f..65bea08c4 100644
--- a/pennylane/labs/resource_estimation/resource_tracking.py
+++ b/pennylane/labs/resource_estimation/resource_tracking.py
@@ -68,7 +68,8 @@ resource_config = {
     "error_rx": 1e-9,
     "error_ry": 1e-9,
     "error_rz": 1e-9,
-    "precision_multiplexer": 1e-9,
+    "precision_select_pauli_rot": 1e-9,
+    "precision_qubit_unitary": 1e-9,
     "precision_qrom_state_prep": 1e-9,
 }
 
diff --git a/pennylane/labs/resource_estimation/templates/__init__.py b/pennylane/labs/resource_estimation/templates/__init__.py
index 48309074f..4ef2b5794 100644
--- a/pennylane/labs/resource_estimation/templates/__init__.py
+++ b/pennylane/labs/resource_estimation/templates/__init__.py
@@ -21,6 +21,8 @@ from .subroutines import (
     ResourceBasisRotation,
     ResourceSelect,
     ResourceQROM,
+    ResourceSelectPauliRot,
+    ResourceQubitUnitary,
 )
 from .trotter import (
     ResourceTrotterCDF,
diff --git a/pennylane/labs/resource_estimation/templates/subroutines.py b/pennylane/labs/resource_estimation/templates/subroutines.py
index 8628fadca..888076129 100644
--- a/pennylane/labs/resource_estimation/templates/subroutines.py
+++ b/pennylane/labs/resource_estimation/templates/subroutines.py
@@ -1131,3 +1131,350 @@ class ResourceQROM(ResourceOperator):
             "clean": clean,
         }
         return CompressedResourceOp(cls, params)
+
+
+class ResourceQubitUnitary(ResourceOperator):
+    r"""Resource class for the QubitUnitary template.
+
+    Args:
+        num_wires (int): the number of qubits the operation acts upon
+        precision (Union[float, None], optional): The precision used when preparing the single qubit
+            rotations used to synthesize the n-qubit unitary.
+        wires (Sequence[int], optional): the wires the operation acts on
+
+    Resources:
+        The resources are defined by combining the two equalities in `Möttönen and Vartiainen
+        (2005), Fig 14 <https://arxiv.org/pdf/quant-ph/0504100>`_ , we can express an :math:`n`
+        qubit unitary as four :math:`n - 1` qubit unitaries and three multiplexed rotations
+        via ( :class:`~.labs.resource_estimation.ResourceSelectPauliRot` ). Specifically, the cost
+        is given by:
+
+        * 1-qubit unitary, the cost is approximated as a single :code:`RZ` rotation.
+
+        * 2-qubit unitary, the cost is approximated as four single qubit rotations and three :code:`CNOT` gates.
+
+        * 3-qubit unitary or more, the cost is given according to the reference above, recursively.
+
+    .. seealso:: :class:`~.QubitUnitary`
+
+    **Example**
+
+    The resources for this operation are computed using:
+
+    >>> qu = plre.ResourceQubitUnitary(num_wires=3)
+    >>> print(plre.estimate_resources(qu, gate_set))
+    --- Resources: ---
+     Total qubits: 3
+     Total gates : 52
+     Qubit breakdown:
+      clean qubits: 0, dirty qubits: 0, algorithmic qubits: 3
+     Gate breakdown:
+      {'RZ': 24, 'CNOT': 24, 'RY': 4}
+    """
+
+    resource_keys = {"num_wires", "precision"}
+
+    def __init__(self, num_wires, precision=None, wires=None):
+        self.num_wires = num_wires
+        self.precision = precision
+        super().__init__(wires=wires)
+
+    @property
+    def resource_params(self) -> dict:
+        r"""Returns a dictionary containing the minimal information needed to compute the resources.
+
+        Returns:
+            dict: A dictionary containing the resource parameters:
+                * num_wires (int): the number of qubits the operation acts upon
+                * precision (Union[float, None], optional): The precision used when preparing the
+                  single qubit rotations used to synthesize the n-qubit unitary.
+        """
+        return {"num_wires": self.num_wires, "precision": self.precision}
+
+    @classmethod
+    def resource_rep(cls, num_wires, precision) -> CompressedResourceOp:
+        r"""Returns a compressed representation containing only the parameters of
+        the Operator that are needed to compute the resources.
+
+        Args:
+            num_wires (int): the number of qubits the operation acts upon
+            precision (Union[float, None], optional): The precision used when preparing the single
+                qubit rotations used to synthesize the n-qubit unitary.
+
+        Returns:
+            CompressedResourceOp: the operator in a compressed representation
+        """
+        params = {"num_wires": num_wires, "precision": precision}
+        return CompressedResourceOp(cls, params)
+
+    @classmethod
+    def default_resource_decomp(cls, num_wires, precision=None, **kwargs) -> list[GateCount]:
+        r"""Returns a list representing the resources of the operator. Each object in the list
+        represents a gate and the number of times it occurs in the circuit.
+
+        Args:
+            num_wires (int): the number of qubits the operation acts upon
+            precision (Union[float, None], optional): The precision used when preparing the single
+                qubit rotations used to synthesize the n-qubit unitary.
+
+        Resources:
+            The resources are defined by combining the two equalities in `Möttönen and Vartiainen
+            (2005), Fig 14 <https://arxiv.org/pdf/quant-ph/0504100>`_, we can express an :math:`n`-
+            qubit unitary as four :math:`n - 1`-qubit unitaries and three multiplexed rotations
+            via (:class:`~.labs.resource_estimation.ResourceSelectPauliRot`). Specifically, the cost
+            is given by:
+
+            * 1-qubit unitary, the cost is approximated as a single :code:`RZ` rotation.
+
+            * 2-qubit unitary, the cost is approximated as four single qubit rotations and three :code:`CNOT` gates.
+
+            * 3-qubit unitary or more, the cost is given according to the reference above, recursively.
+
+        Returns:
+            list[GateCount]: A list of GateCount objects, where each object
+            represents a specific quantum gate and the number of times it appears
+            in the decomposition.
+        """
+        gate_lst = []
+        precision = precision or kwargs["config"]["precision_qubit_unitary"]
+
+        one_qubit_decomp_cost = [GateCount(resource_rep(re.ResourceRZ, {"eps": precision}))]
+        two_qubit_decomp_cost = [
+            GateCount(resource_rep(re.ResourceRZ, {"eps": precision}), 4),
+            GateCount(resource_rep(re.ResourceCNOT), 3),
+        ]
+
+        if num_wires == 1:
+            return one_qubit_decomp_cost
+
+        if num_wires == 2:
+            return two_qubit_decomp_cost
+
+        for gc in two_qubit_decomp_cost:
+            gate_lst.append(4 ** (num_wires - 2) * gc)
+
+        for index in range(2, num_wires):
+            multiplex_z = resource_rep(
+                ResourceSelectPauliRot,
+                {
+                    "num_ctrl_wires": index,
+                    "rotation_axis": "Z",
+                    "precision": precision,
+                },
+            )
+            multiplex_y = resource_rep(
+                ResourceSelectPauliRot,
+                {
+                    "num_ctrl_wires": index,
+                    "rotation_axis": "Y",
+                    "precision": precision,
+                },
+            )
+
+            gate_lst.append(GateCount(multiplex_z, 2 * 4 ** (num_wires - (1 + index))))
+            gate_lst.append(GateCount(multiplex_y, 4 ** (num_wires - (1 + index))))
+
+        return gate_lst
+
+
+class ResourceSelectPauliRot(ResourceOperator):
+    r"""Resource class for the SelectPauliRot gate.
+
+    Args:
+        rotation_axis (str): the rotation axis used in the multiplexer
+        num_ctrl_wires (int): the number of control wires of the multiplexer
+        precision (float): the precision used in the single qubit rotations
+        wires (Sequence[int], optional): the wires the operation acts on
+
+    Resources:
+        The resources are obtained from the construction scheme given in `Möttönen and Vartiainen
+        (2005), Fig 7a <https://arxiv.org/abs/quant-ph/0504100>`_. Specifically, the resources
+        for an :math:`n` qubit unitary are given as :math:`2^{n}` instances of the :code:`CNOT`
+        gate and :math:`2^{n}` instances of the single qubit rotation gate (:code:`RX`,
+        :code:`RY` or :code:`RZ`) depending on the :code:`rotation_axis`.
+
+    .. seealso:: :class:`~.SelectPauliRot`
+
+    **Example**
+
+    The resources for this operation are computed using:
+
+    >>> mltplxr = plre.ResourceSelectPauliRot(
+    ...     rotation_axis = "Y",
+    ...     num_ctrl_wires = 4,
+    ...     precision = 1e-3,
+    ... )
+    >>> print(plre.estimate_resources(mltplxr, plre.StandardGateSet))
+    --- Resources: ---
+     Total qubits: 5
+     Total gates : 32
+     Qubit breakdown:
+      clean qubits: 0, dirty qubits: 0, algorithmic qubits: 5
+     Gate breakdown:
+      {'RY': 16, 'CNOT': 16}
+    """
+
+    resource_keys = {"num_ctrl_wires", "rotation_axis", "precision"}
+
+    def __init__(self, rotation_axis: str, num_ctrl_wires: int, precision=None, wires=None) -> None:
+        if rotation_axis not in ("X", "Y", "Z"):
+            raise ValueError("The `rotation_axis` argument must be one of ('X', 'Y', 'Z')")
+
+        self.num_ctrl_wires = num_ctrl_wires
+        self.rotation_axis = rotation_axis
+        self.precision = precision
+
+        self.num_wires = num_ctrl_wires + 1
+        super().__init__(wires=wires)
+
+    @property
+    def resource_params(self):
+        r"""Returns a dictionary containing the minimal information needed to compute the resources.
+
+        Returns:
+            dict: A dictionary containing the resource parameters:
+            * rotation_axis (str): the rotation axis used in the multiplexer
+            * num_ctrl_wires (int): the number of control wires of the multiplexer
+            * precision (float): the precision used in the single qubit rotations
+        """
+        return {
+            "num_ctrl_wires": self.num_ctrl_wires,
+            "rotation_axis": self.rotation_axis,
+            "precision": self.precision,
+        }
+
+    @classmethod
+    def resource_rep(cls, num_ctrl_wires, rotation_axis, precision=None):
+        r"""Returns a compressed representation containing only the parameters of
+        the Operator that are needed to compute the resources.
+
+        Args:
+            rotation_axis (str): the rotation axis used in the multiplexer
+            num_ctrl_wires (int): the number of control wires of the multiplexer
+            precision (float): the precision used in the single qubit rotations
+
+        Returns:
+            CompressedResourceOp: the operator in a compressed representation
+        """
+        return CompressedResourceOp(
+            cls,
+            {
+                "num_ctrl_wires": num_ctrl_wires,
+                "rotation_axis": rotation_axis,
+                "precision": precision,
+            },
+        )
+
+    @classmethod
+    def default_resource_decomp(cls, num_ctrl_wires, rotation_axis, precision, **kwargs):
+        r"""Returns a list representing the resources of the operator. Each object in the list
+        represents a gate and the number of times it occurs in the circuit.
+
+        Args:
+            rotation_axis (str): the rotation axis used in the multiplexer
+            num_ctrl_wires (int): the number of control wires of the multiplexer
+            precision (float): the precision used in the single qubit rotations
+            wires (Sequence[int], optional): the wires the operation acts on
+
+        Resources:
+            The resources are obtained from the construction scheme given in `Möttönen and Vartiainen
+            (2005), Fig 7a <https://arxiv.org/abs/quant-ph/0504100>`_. Specifically, the resources
+            for an :math:`n` qubit unitary are given as :math:`2^{n}` instances of the :code:`CNOT`
+            gate and :math:`2^{n}` instances of the single qubit rotation gate (:code:`RX`,
+            :code:`RY` or :code:`RZ`) depending on the :code:`rotation_axis`.
+
+        Returns:
+            list[GateCount]: A list of GateCount objects, where each object
+            represents a specific quantum gate and the number of times it appears
+            in the decomposition.
+        """
+        rotation_gate_map = {
+            "X": re.ResourceRX,
+            "Y": re.ResourceRY,
+            "Z": re.ResourceRZ,
+        }
+        precision = precision or kwargs["config"]["precision_select_pauli_rot"]
+
+        gate = resource_rep(rotation_gate_map[rotation_axis], {"eps": precision})
+        cnot = resource_rep(re.ResourceCNOT)
+
+        gate_lst = [
+            GateCount(gate, 2**num_ctrl_wires),
+            GateCount(cnot, 2**num_ctrl_wires),
+        ]
+
+        return gate_lst
+
+    @classmethod
+    def phase_grad_resource_decomp(cls, num_ctrl_wires, rotation_axis, precision, **kwargs):
+        r"""Returns a list representing the resources of the operator. Each object in the list
+        represents a gate and the number of times it occurs in the circuit.
+
+        Args:
+            rotation_axis (str): the rotation axis used in the multiplexer
+            num_ctrl_wires (int): the number of control wires of the multiplexer
+            precision (float): the precision used in the single qubit rotations
+            wires (Sequence[int], optional): the wires the operation acts on
+
+        Resources:
+            The resources are obtained from the construction scheme given in `O'Brien and Sünderhauf
+            (2025), Fig 4 <https://arxiv.org/pdf/2409.07332>`_. Specifically, the resources
+            use two :code:`~.labs.resource_estimation.ResourceQROM`s to digitally load and unload
+            the phase angles up to some precision. These are then applied using a single controlled
+            :code:`~.labs.resource_estimation.ResourceSemiAdder`.
+
+            .. note::
+
+                This method assumes a phase gradient state is prepared on an auxiliary register.
+
+        Returns:
+            list[GateCount]: A list of GateCount objects, where each object
+            represents a specific quantum gate and the number of times it appears
+            in the decomposition.
+        """
+        precision = precision or kwargs["config"]["precision_select_pauli_rot"]
+        num_prec_wires = abs(math.floor(math.log2(precision)))
+        gate_lst = []
+
+        qrom = resource_rep(
+            re.ResourceQROM,
+            {
+                "num_bitstrings": 2**num_ctrl_wires,
+                "num_bit_flips": 2**num_ctrl_wires * num_prec_wires / 2,
+                "size_bitstring": num_prec_wires,
+                "clean": False,
+            },
+        )
+
+        gate_lst.append(AllocWires(num_prec_wires))
+        gate_lst.append(GateCount(qrom))
+        gate_lst.append(
+            GateCount(
+                resource_rep(
+                    re.ResourceControlled,
+                    {
+                        "base_cmpr_op": resource_rep(
+                            re.ResourceSemiAdder,
+                            {"max_register_size": num_prec_wires},
+                        ),
+                        "num_ctrl_wires": 1,
+                        "num_ctrl_values": 0,
+                    },
+                )
+            )
+        )
+        gate_lst.append(GateCount(resource_rep(re.ResourceAdjoint, {"base_cmpr_op": qrom})))
+        gate_lst.append(FreeWires(num_prec_wires))
+
+        h = resource_rep(re.ResourceHadamard)
+        s = resource_rep(re.ResourceS)
+        s_dagg = resource_rep(re.ResourceAdjoint, {"base_cmpr_op": s})
+
+        if rotation_axis == "X":
+            gate_lst.append(GateCount(h, 2))
+        if rotation_axis == "Y":
+            gate_lst.append(GateCount(h, 2))
+            gate_lst.append(GateCount(s))
+            gate_lst.append(GateCount(s_dagg))
+
+        return gate_lst
diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py
index 455697cc4..2b652624d 100644
--- a/pennylane/ops/functions/assert_valid.py
+++ b/pennylane/ops/functions/assert_valid.py
@@ -436,7 +436,17 @@ def _check_wires(op, skip_wire_mapping):
     assert mapped_op.wires == new_wires, "wires must be mappable with map_wires"
 
 
-def assert_valid(op: qml.operation.Operator, **kwargs) -> None:
+# pylint: disable=too-many-arguments
+def assert_valid(
+    op: qml.operation.Operator,
+    *,
+    skip_deepcopy=False,
+    skip_pickle=False,
+    skip_wire_mapping=False,
+    skip_differentiation=False,
+    skip_new_decomp=False,
+    heuristic_resources=False,
+) -> None:
     """Runs basic validation checks on an :class:`~.operation.Operator` to make
     sure it has been correctly defined.
 
@@ -496,20 +506,20 @@ def assert_valid(op: qml.operation.Operator, **kwargs) -> None:
         assert qml.math.allclose(d, p), "data and parameters must match."
 
     if len(op.wires) <= 26:
-        _check_wires(op, kwargs.get("skip_wire_mapping", False))
-    _check_copy(op, kwargs.get("skip_deepcopy", False))
+        _check_wires(op, skip_wire_mapping=skip_wire_mapping)
+    _check_copy(op, skip_deepcopy=skip_deepcopy)
     _check_pytree(op)
-    if not kwargs.get("skip_pickle", False):
+    if not skip_pickle:
         _check_pickle(op)
     _check_bind_new_parameters(op)
-    _check_decomposition(op, kwargs.get("skip_wire_mapping", False))
-    if not kwargs.get("skip_new_decomp", False):
-        _check_decomposition_new(op, heuristic_resources=kwargs.get("heuristic_resources", False))
+    _check_decomposition(op, skip_wire_mapping=skip_wire_mapping)
+    if not skip_new_decomp:
+        _check_decomposition_new(op, heuristic_resources=heuristic_resources)
     _check_matrix(op)
     _check_matrix_matches_decomp(op)
     _check_sparse_matrix(op)
     _check_eigendecomposition(op)
     _check_generator(op)
-    if not kwargs.get("skip_differentiation", False):
+    if not skip_differentiation:
         _check_differentiation(op)
     _check_capture(op)
diff --git a/pennylane/ops/qubit/state_preparation.py b/pennylane/ops/qubit/state_preparation.py
index dc85d2fcd..8d4ac3423 100644
--- a/pennylane/ops/qubit/state_preparation.py
+++ b/pennylane/ops/qubit/state_preparation.py
@@ -217,7 +217,7 @@ add_decomps(BasisState, _basis_state_decomp)
 
 
 class StatePrep(StatePrepBase):
-    r"""StatePrep(state, wires, pad_with = None, normalize = False, validate_norm = True)
+    r"""StatePrep(state, wires, pad_with = None, normalize = False, validate_norm = False)
     Prepare subsystems using a state vector in the computational basis.
 
     **Details:**
@@ -503,7 +503,7 @@ class StatePrep(StatePrepBase):
                 padding = math.convert_like(padding, state)
                 state = math.hstack([state, padding])
 
-        if not validate_norm:
+        if not (validate_norm or normalize):
             return state
 
         # normalize


Test patch:
diff --git a/.github/workflows/interface-unit-tests.yml b/.github/workflows/interface-unit-tests.yml
index c666ac6b7..48393c672 100644
--- a/.github/workflows/interface-unit-tests.yml
+++ b/.github/workflows/interface-unit-tests.yml
@@ -533,7 +533,7 @@ jobs:
       pytest_additional_args: ${{ needs.warnings-as-errors-setup.outputs.pytest_warning_args }}
       pytest_xml_file_path: '${{ inputs.job_name_prefix }}external-libraries-tests (${{ matrix.python-version }})${{ inputs.job_name_suffix }}.xml'
       additional_pip_packages: |
-        pyzx matplotlib stim quimb==1.11.0 mitiq ply optax scipy-openblas32>=0.3.26 qualtran openqasm3 antlr4_python3_runtime xdsl==0.38 filecheck
+        pyzx matplotlib stim quimb==1.11.0 mitiq ply optax scipy-openblas32>=0.3.26 qualtran openqasm3 antlr4_python3_runtime xdsl==0.44 filecheck
         git+https://github.com/PennyLaneAI/pennylane-qiskit.git@master
         ${{ needs.default-dependency-versions.outputs.catalyst-jax-version }}
         ${{ needs.default-dependency-versions.outputs.tensorflow-version }}
diff --git a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py
index 6d47f83c5..ce64fcc85 100644
--- a/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py
+++ b/pennylane/labs/tests/resource_estimation/ops/op_math/test_controlled_ops.py
@@ -655,6 +655,10 @@ class TestResourceCRX:
 
     op = re.ResourceCRX(wires=[0, 1])
 
+    def test_resource_keys(self):
+        """test that the resource keys are correct"""
+        assert self.op.resource_keys == {"eps"}
+
     def test_resources(self):
         """Test that the resources method produces the expected resources."""
 
@@ -724,6 +728,10 @@ class TestResourceCRY:
 
     op = re.ResourceCRY(wires=[0, 1])
 
+    def test_resource_keys(self):
+        """test that the resource keys are correct"""
+        assert self.op.resource_keys == {"eps"}
+
     def test_resources(self):
         """Test that the resources method produces the expected resources."""
 
@@ -791,6 +799,10 @@ class TestResourceCRZ:
 
     op = re.ResourceCRZ(wires=[0, 1])
 
+    def test_resource_keys(self):
+        """test that the resource keys are correct"""
+        assert self.op.resource_keys == {"eps"}
+
     def test_resources(self):
         """Test that the resources method produces the expected resources."""
 
@@ -858,6 +870,10 @@ class TestResourceCRot:
 
     op = re.ResourceCRot(wires=[0, 1])
 
+    def test_resource_keys(self):
+        """test that the resource keys are correct"""
+        assert self.op.resource_keys == {"eps"}
+
     def test_resources(self):
         """Test that the resources method produces the expected resources."""
         expected_resources = [
@@ -923,6 +939,11 @@ class TestResourceCRot:
 class TestResourceControlledPhaseShift:
     """Test ResourceControlledPhaseShift"""
 
+    def test_resource_keys(self):
+        """test that the resource keys are correct"""
+        op = re.ResourceControlledPhaseShift()
+        assert op.resource_keys == {"eps"}
+
     def test_resources(self):
         """Test the resources method"""
 
diff --git a/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py b/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py
index e4c1c30fe..199a5db99 100644
--- a/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py
+++ b/pennylane/labs/tests/resource_estimation/templates/test_subroutines.py
@@ -17,6 +17,7 @@ Tests for quantum algorithmic subroutines resource operators.
 import pytest
 
 import pennylane.labs.resource_estimation as plre
+from pennylane.labs.resource_estimation import AllocWires, FreeWires, GateCount, resource_rep
 
 # pylint: disable=no-self-use,too-many-arguments
 
@@ -42,8 +43,8 @@ class TestResourceOutOfPlaceSquare:
     def test_resources(self, register_size):
         """Test that the resources are correct."""
         expected = [
-            plre.GateCount(plre.resource_rep(plre.ResourceToffoli), (register_size - 1) ** 2),
-            plre.GateCount(plre.resource_rep(plre.ResourceCNOT), register_size),
+            GateCount(resource_rep(plre.ResourceToffoli), (register_size - 1) ** 2),
+            GateCount(resource_rep(plre.ResourceCNOT), register_size),
         ]
         assert (
             plre.ResourceOutOfPlaceSquare.resource_decomp(register_size=register_size) == expected
@@ -71,35 +72,35 @@ class TestResourcePhaseGradient:
             (
                 1,
                 [
-                    plre.GateCount(plre.ResourceHadamard.resource_rep()),
-                    plre.GateCount(plre.ResourceZ.resource_rep()),
+                    GateCount(plre.ResourceHadamard.resource_rep()),
+                    GateCount(plre.ResourceZ.resource_rep()),
                 ],
             ),
             (
                 2,
                 [
-                    plre.GateCount(plre.ResourceHadamard.resource_rep(), 2),
-                    plre.GateCount(plre.ResourceZ.resource_rep()),
-                    plre.GateCount(plre.ResourceS.resource_rep()),
+                    GateCount(plre.ResourceHadamard.resource_rep(), 2),
+                    GateCount(plre.ResourceZ.resource_rep()),
+                    GateCount(plre.ResourceS.resource_rep()),
                 ],
             ),
             (
                 3,
                 [
-                    plre.GateCount(plre.ResourceHadamard.resource_rep(), 3),
-                    plre.GateCount(plre.ResourceZ.resource_rep()),
-                    plre.GateCount(plre.ResourceS.resource_rep()),
-                    plre.GateCount(plre.ResourceT.resource_rep()),
+                    GateCount(plre.ResourceHadamard.resource_rep(), 3),
+                    GateCount(plre.ResourceZ.resource_rep()),
+                    GateCount(plre.ResourceS.resource_rep()),
+                    GateCount(plre.ResourceT.resource_rep()),
                 ],
             ),
             (
                 5,
                 [
-                    plre.GateCount(plre.ResourceHadamard.resource_rep(), 5),
-                    plre.GateCount(plre.ResourceZ.resource_rep()),
-                    plre.GateCount(plre.ResourceS.resource_rep()),
-                    plre.GateCount(plre.ResourceT.resource_rep()),
-                    plre.GateCount(plre.ResourceRZ.resource_rep(), 2),
+                    GateCount(plre.ResourceHadamard.resource_rep(), 5),
+                    GateCount(plre.ResourceZ.resource_rep()),
+                    GateCount(plre.ResourceS.resource_rep()),
+                    GateCount(plre.ResourceT.resource_rep()),
+                    GateCount(plre.ResourceRZ.resource_rep(), 2),
                 ],
             ),
         ),
@@ -137,17 +138,17 @@ class TestResourceOutMultiplier:
         a_register_size = 5
         b_register_size = 3
 
-        toff = plre.resource_rep(plre.ResourceToffoli)
-        l_elbow = plre.resource_rep(plre.ResourceTempAND)
-        r_elbow = plre.resource_rep(plre.ResourceAdjoint, {"base_cmpr_op": l_elbow})
+        toff = resource_rep(plre.ResourceToffoli)
+        l_elbow = resource_rep(plre.ResourceTempAND)
+        r_elbow = resource_rep(plre.ResourceAdjoint, {"base_cmpr_op": l_elbow})
 
         num_elbows = 12
         num_toff = 1
 
         expected = [
-            plre.GateCount(l_elbow, num_elbows),
-            plre.GateCount(r_elbow, num_elbows),
-            plre.GateCount(toff, num_toff),
+            GateCount(l_elbow, num_elbows),
+            GateCount(r_elbow, num_elbows),
+            GateCount(toff, num_toff),
         ]
         assert (
             plre.ResourceOutMultiplier.resource_decomp(a_register_size, b_register_size) == expected
@@ -176,26 +177,26 @@ class TestResourceSemiAdder:
         (
             (
                 1,
-                [plre.GateCount(plre.resource_rep(plre.ResourceCNOT))],
+                [GateCount(resource_rep(plre.ResourceCNOT))],
             ),
             (
                 2,
                 [
-                    plre.GateCount(plre.resource_rep(plre.ResourceCNOT), 2),
-                    plre.GateCount(plre.resource_rep(plre.ResourceX), 2),
-                    plre.GateCount(plre.resource_rep(plre.ResourceToffoli)),
+                    GateCount(resource_rep(plre.ResourceCNOT), 2),
+                    GateCount(resource_rep(plre.ResourceX), 2),
+                    GateCount(resource_rep(plre.ResourceToffoli)),
                 ],
             ),
             (
                 3,
                 [
                     plre.AllocWires(2),
-                    plre.GateCount(plre.resource_rep(plre.ResourceCNOT), 9),
-                    plre.GateCount(plre.resource_rep(plre.ResourceTempAND), 2),
-                    plre.GateCount(
-                        plre.resource_rep(
+                    GateCount(resource_rep(plre.ResourceCNOT), 9),
+                    GateCount(resource_rep(plre.ResourceTempAND), 2),
+                    GateCount(
+                        resource_rep(
                             plre.ResourceAdjoint,
-                            {"base_cmpr_op": plre.resource_rep(plre.ResourceTempAND)},
+                            {"base_cmpr_op": resource_rep(plre.ResourceTempAND)},
                         ),
                         2,
                     ),
@@ -218,11 +219,11 @@ class TestResourceSemiAdder:
 
         expected_res = [
             plre.AllocWires(4),
-            plre.GateCount(plre.resource_rep(plre.ResourceCNOT), 24),
-            plre.GateCount(plre.resource_rep(plre.ResourceTempAND), 8),
-            plre.GateCount(
-                plre.resource_rep(
-                    plre.ResourceAdjoint, {"base_cmpr_op": plre.resource_rep(plre.ResourceTempAND)}
+            GateCount(resource_rep(plre.ResourceCNOT), 24),
+            GateCount(resource_rep(plre.ResourceTempAND), 8),
+            GateCount(
+                resource_rep(
+                    plre.ResourceAdjoint, {"base_cmpr_op": resource_rep(plre.ResourceTempAND)}
                 ),
                 8,
             ),
@@ -250,12 +251,8 @@ class TestResourceBasisRotation:
     def test_resources(self, dim_n):
         """Test that the resources are correct."""
         expected = [
-            plre.GateCount(
-                plre.resource_rep(plre.ResourcePhaseShift), dim_n + (dim_n * (dim_n - 1) // 2)
-            ),
-            plre.GateCount(
-                plre.resource_rep(plre.ResourceSingleExcitation), dim_n * (dim_n - 1) // 2
-            ),
+            GateCount(resource_rep(plre.ResourcePhaseShift), dim_n + (dim_n * (dim_n - 1) // 2)),
+            GateCount(resource_rep(plre.ResourceSingleExcitation), dim_n * (dim_n - 1) // 2),
         ]
         assert plre.ResourceBasisRotation.resource_decomp(dim_n) == expected
 
@@ -286,31 +283,31 @@ class TestResourceSelect:
 
         expected = [
             plre.AllocWires(1),
-            plre.GateCount(
+            GateCount(
                 plre.ResourceControlled.resource_rep(
                     plre.ResourceRX.resource_rep(),
                     1,
                     0,
                 )
             ),
-            plre.GateCount(
+            GateCount(
                 plre.ResourceControlled.resource_rep(
                     plre.ResourceZ.resource_rep(),
                     1,
                     0,
                 )
             ),
-            plre.GateCount(
+            GateCount(
                 plre.ResourceControlled.resource_rep(
                     plre.ResourceCNOT.resource_rep(),
                     1,
                     0,
                 )
             ),
-            plre.GateCount(plre.ResourceX.resource_rep(), 4),
-            plre.GateCount(plre.ResourceCNOT.resource_rep(), 2),
-            plre.GateCount(plre.ResourceTempAND.resource_rep(), 2),
-            plre.GateCount(
+            GateCount(plre.ResourceX.resource_rep(), 4),
+            GateCount(plre.ResourceCNOT.resource_rep(), 2),
+            GateCount(plre.ResourceTempAND.resource_rep(), 2),
+            GateCount(
                 plre.ResourceAdjoint.resource_rep(
                     plre.ResourceTempAND.resource_rep(),
                 ),
@@ -389,18 +386,18 @@ class TestResourceQROM:
                 True,
                 [
                     plre.AllocWires(5),
-                    plre.GateCount(plre.ResourceHadamard.resource_rep(), 6),
-                    plre.GateCount(plre.ResourceX.resource_rep(), 14),
-                    plre.GateCount(plre.ResourceCNOT.resource_rep(), 36),
-                    plre.GateCount(plre.ResourceTempAND.resource_rep(), 6),
-                    plre.GateCount(
+                    GateCount(plre.ResourceHadamard.resource_rep(), 6),
+                    GateCount(plre.ResourceX.resource_rep(), 14),
+                    GateCount(plre.ResourceCNOT.resource_rep(), 36),
+                    GateCount(plre.ResourceTempAND.resource_rep(), 6),
+                    GateCount(
                         plre.ResourceAdjoint.resource_rep(
                             plre.ResourceTempAND.resource_rep(),
                         ),
                         6,
                     ),
                     plre.FreeWires(2),
-                    plre.GateCount(plre.ResourceCSWAP.resource_rep(), 12),
+                    GateCount(plre.ResourceCSWAP.resource_rep(), 12),
                     plre.FreeWires(3),
                 ],
             ),
@@ -412,17 +409,17 @@ class TestResourceQROM:
                 False,
                 [
                     plre.AllocWires(10),
-                    plre.GateCount(plre.ResourceX.resource_rep(), 97),
-                    plre.GateCount(plre.ResourceCNOT.resource_rep(), 98),
-                    plre.GateCount(plre.ResourceTempAND.resource_rep(), 48),
-                    plre.GateCount(
+                    GateCount(plre.ResourceX.resource_rep(), 97),
+                    GateCount(plre.ResourceCNOT.resource_rep(), 98),
+                    GateCount(plre.ResourceTempAND.resource_rep(), 48),
+                    GateCount(
                         plre.ResourceAdjoint.resource_rep(
                             plre.ResourceTempAND.resource_rep(),
                         ),
                         48,
                     ),
                     plre.FreeWires(5),
-                    plre.GateCount(plre.ResourceCSWAP.resource_rep(), 5),
+                    GateCount(plre.ResourceCSWAP.resource_rep(), 5),
                 ],
             ),
             (
@@ -433,18 +430,18 @@ class TestResourceQROM:
                 True,  # AllocWires(3), (4 x Hadamard), (42 x X), (30 x CNOT), (20 x TempAND), (20 x Adjoint(TempAND)), FreeWires(3), (0 x CSWAP), FreeWires(0)
                 [
                     plre.AllocWires(3),
-                    plre.GateCount(plre.ResourceHadamard.resource_rep(), 4),
-                    plre.GateCount(plre.ResourceX.resource_rep(), 42),
-                    plre.GateCount(plre.ResourceCNOT.resource_rep(), 30),
-                    plre.GateCount(plre.ResourceTempAND.resource_rep(), 20),
-                    plre.GateCount(
+                    GateCount(plre.ResourceHadamard.resource_rep(), 4),
+                    GateCount(plre.ResourceX.resource_rep(), 42),
+                    GateCount(plre.ResourceCNOT.resource_rep(), 30),
+                    GateCount(plre.ResourceTempAND.resource_rep(), 20),
+                    GateCount(
                         plre.ResourceAdjoint.resource_rep(
                             plre.ResourceTempAND.resource_rep(),
                         ),
                         20,
                     ),
                     plre.FreeWires(3),
-                    plre.GateCount(plre.ResourceCSWAP.resource_rep(), 0),
+                    GateCount(plre.ResourceCSWAP.resource_rep(), 0),
                     plre.FreeWires(0),
                 ],
             ),
@@ -464,3 +461,352 @@ class TestResourceQROM:
             )
             == expected_res
         )
+
+
+class TestResourceQubitUnitary:
+    """Test the ResourceQubitUnitary template"""
+
+    @pytest.mark.parametrize("eps", (None, 1e-3, 1e-5))
+    @pytest.mark.parametrize("num_wires", (1, 2, 3, 4, 5, 6))
+    def test_resource_params(self, num_wires, eps):
+        """Test that the resource params are correct."""
+        op = (
+            plre.ResourceQubitUnitary(num_wires, eps)
+            if eps
+            else plre.ResourceQubitUnitary(num_wires)
+        )
+        assert op.resource_params == {"num_wires": num_wires, "precision": eps}
+
+    @pytest.mark.parametrize("eps", (None, 1e-3, 1e-5))
+    @pytest.mark.parametrize("num_wires", (1, 2, 3, 4, 5, 6))
+    def test_resource_rep(self, num_wires, eps):
+        """Test that the compressed representation is correct."""
+        expected = plre.CompressedResourceOp(
+            plre.ResourceQubitUnitary, {"num_wires": num_wires, "precision": eps}
+        )
+        assert (
+            plre.ResourceQubitUnitary.resource_rep(num_wires=num_wires, precision=eps) == expected
+        )
+
+    @pytest.mark.parametrize(
+        "num_wires, eps, expected_res",
+        (
+            (
+                1,
+                None,
+                [
+                    GateCount(resource_rep(plre.ResourceRZ, {"eps": 1e-9})),
+                ],
+            ),
+            (
+                2,
+                1e-3,
+                [
+                    GateCount(resource_rep(plre.ResourceRZ, {"eps": 1e-3}), 4),
+                    GateCount(resource_rep(plre.ResourceCNOT), 3),
+                ],
+            ),
+            (
+                5,
+                1e-5,
+                [
+                    GateCount(resource_rep(plre.ResourceRZ, {"eps": 1e-5}), (4**3) * 4),
+                    GateCount(resource_rep(plre.ResourceCNOT), (4**3) * 3),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceSelectPauliRot,
+                            {
+                                "rotation_axis": "Z",
+                                "num_ctrl_wires": 2,
+                                "precision": 1e-5,
+                            },
+                        ),
+                        2 * 4**2,
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceSelectPauliRot,
+                            {
+                                "rotation_axis": "Y",
+                                "num_ctrl_wires": 2,
+                                "precision": 1e-5,
+                            },
+                        ),
+                        4**2,
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceSelectPauliRot,
+                            {
+                                "rotation_axis": "Z",
+                                "num_ctrl_wires": 3,
+                                "precision": 1e-5,
+                            },
+                        ),
+                        2 * 4,
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceSelectPauliRot,
+                            {
+                                "rotation_axis": "Y",
+                                "num_ctrl_wires": 3,
+                                "precision": 1e-5,
+                            },
+                        ),
+                        4,
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceSelectPauliRot,
+                            {
+                                "rotation_axis": "Z",
+                                "num_ctrl_wires": 4,
+                                "precision": 1e-5,
+                            },
+                        ),
+                        2,
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceSelectPauliRot,
+                            {
+                                "rotation_axis": "Y",
+                                "num_ctrl_wires": 4,
+                                "precision": 1e-5,
+                            },
+                        ),
+                        1,
+                    ),
+                ],
+            ),
+        ),
+    )
+    def test_default_resources(self, num_wires, eps, expected_res):
+        """Test that the resources are correct."""
+        if eps is None:
+            config = {"precision_qubit_unitary": 1e-9}
+            assert (
+                plre.ResourceQubitUnitary.resource_decomp(
+                    num_wires=num_wires, precision=eps, config=config
+                )
+                == expected_res
+            )
+        else:
+            assert (
+                plre.ResourceQubitUnitary.resource_decomp(num_wires=num_wires, precision=eps)
+                == expected_res
+            )
+
+
+class TestResourceSelectPauliRot:
+    """Test the ResourceSelectPauliRot template"""
+
+    @pytest.mark.parametrize("precision", (None, 1e-3, 1e-5))
+    @pytest.mark.parametrize("rotation_axis", ("X", "Y", "Z"))
+    @pytest.mark.parametrize("num_ctrl_wires", (1, 2, 3, 4, 5))
+    def test_resource_params(self, num_ctrl_wires, rotation_axis, precision):
+        """Test that the resource params are correct."""
+        op = (
+            plre.ResourceSelectPauliRot(rotation_axis, num_ctrl_wires, precision)
+            if precision
+            else plre.ResourceSelectPauliRot(rotation_axis, num_ctrl_wires)
+        )
+        assert op.resource_params == {
+            "rotation_axis": rotation_axis,
+            "num_ctrl_wires": num_ctrl_wires,
+            "precision": precision,
+        }
+
+    @pytest.mark.parametrize("precision", (None, 1e-3, 1e-5))
+    @pytest.mark.parametrize("rotation_axis", ("X", "Y", "Z"))
+    @pytest.mark.parametrize("num_ctrl_wires", (1, 2, 3, 4, 5))
+    def test_resource_rep(self, num_ctrl_wires, rotation_axis, precision):
+        """Test that the compressed representation is correct."""
+        expected = plre.CompressedResourceOp(
+            plre.ResourceSelectPauliRot,
+            {
+                "rotation_axis": rotation_axis,
+                "num_ctrl_wires": num_ctrl_wires,
+                "precision": precision,
+            },
+        )
+        assert (
+            plre.ResourceSelectPauliRot.resource_rep(num_ctrl_wires, rotation_axis, precision)
+            == expected
+        )
+
+    @pytest.mark.parametrize(
+        "num_ctrl_wires, rotation_axis, precision, expected_res",
+        (
+            (
+                1,
+                "X",
+                None,
+                [
+                    GateCount(resource_rep(plre.ResourceRX, {"eps": 1e-9}), 2),
+                    GateCount(resource_rep(plre.ResourceCNOT), 2),
+                ],
+            ),
+            (
+                2,
+                "Y",
+                1e-3,
+                [
+                    GateCount(resource_rep(plre.ResourceRY, {"eps": 1e-3}), 2**2),
+                    GateCount(resource_rep(plre.ResourceCNOT), 2**2),
+                ],
+            ),
+            (
+                5,
+                "Z",
+                1e-5,
+                [
+                    GateCount(resource_rep(plre.ResourceRZ, {"eps": 1e-5}), 2**5),
+                    GateCount(resource_rep(plre.ResourceCNOT), 2**5),
+                ],
+            ),
+        ),
+    )
+    def test_default_resources(self, num_ctrl_wires, rotation_axis, precision, expected_res):
+        """Test that the resources are correct."""
+        if precision is None:
+            config = {"precision_select_pauli_rot": 1e-9}
+            assert (
+                plre.ResourceSelectPauliRot.resource_decomp(
+                    num_ctrl_wires=num_ctrl_wires,
+                    rotation_axis=rotation_axis,
+                    precision=precision,
+                    config=config,
+                )
+                == expected_res
+            )
+        else:
+            assert (
+                plre.ResourceSelectPauliRot.resource_decomp(
+                    num_ctrl_wires=num_ctrl_wires,
+                    rotation_axis=rotation_axis,
+                    precision=precision,
+                )
+                == expected_res
+            )
+
+    @pytest.mark.parametrize(
+        "num_ctrl_wires, rotation_axis, precision, expected_res",
+        (
+            (
+                1,
+                "X",
+                None,
+                [
+                    AllocWires(30),
+                    GateCount(plre.ResourceQROM.resource_rep(2, 30, 30, False)),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceControlled,
+                            {
+                                "base_cmpr_op": plre.ResourceSemiAdder.resource_rep(30),
+                                "num_ctrl_wires": 1,
+                                "num_ctrl_values": 0,
+                            },
+                        )
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceAdjoint,
+                            {
+                                "base_cmpr_op": plre.ResourceQROM.resource_rep(2, 30, 30, False),
+                            },
+                        )
+                    ),
+                    FreeWires(30),
+                    GateCount(resource_rep(plre.ResourceHadamard), 2),
+                ],
+            ),
+            (
+                2,
+                "Y",
+                1e-3,
+                [
+                    AllocWires(10),
+                    GateCount(plre.ResourceQROM.resource_rep(4, 10, 20, False)),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceControlled,
+                            {
+                                "base_cmpr_op": plre.ResourceSemiAdder.resource_rep(10),
+                                "num_ctrl_wires": 1,
+                                "num_ctrl_values": 0,
+                            },
+                        )
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceAdjoint,
+                            {
+                                "base_cmpr_op": plre.ResourceQROM.resource_rep(4, 10, 20, False),
+                            },
+                        )
+                    ),
+                    FreeWires(10),
+                    GateCount(resource_rep(plre.ResourceHadamard), 2),
+                    GateCount(resource_rep(plre.ResourceS)),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceAdjoint, {"base_cmpr_op": resource_rep(plre.ResourceS)}
+                        )
+                    ),
+                ],
+            ),
+            (
+                5,
+                "Z",
+                1e-5,
+                [
+                    AllocWires(17),
+                    GateCount(plre.ResourceQROM.resource_rep(32, 17, 272, False)),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceControlled,
+                            {
+                                "base_cmpr_op": plre.ResourceSemiAdder.resource_rep(17),
+                                "num_ctrl_wires": 1,
+                                "num_ctrl_values": 0,
+                            },
+                        )
+                    ),
+                    GateCount(
+                        resource_rep(
+                            plre.ResourceAdjoint,
+                            {
+                                "base_cmpr_op": plre.ResourceQROM.resource_rep(32, 17, 272, False),
+                            },
+                        )
+                    ),
+                    FreeWires(17),
+                ],
+            ),
+        ),
+    )
+    def test_phase_gradient_resources(self, num_ctrl_wires, rotation_axis, precision, expected_res):
+        """Test that the resources are correct."""
+        if precision is None:
+            config = {"precision_select_pauli_rot": 1e-9}
+            assert (
+                plre.ResourceSelectPauliRot.phase_grad_resource_decomp(
+                    num_ctrl_wires=num_ctrl_wires,
+                    rotation_axis=rotation_axis,
+                    precision=precision,
+                    config=config,
+                )
+                == expected_res
+            )
+        else:
+            assert (
+                plre.ResourceSelectPauliRot.phase_grad_resource_decomp(
+                    num_ctrl_wires=num_ctrl_wires,
+                    rotation_axis=rotation_axis,
+                    precision=precision,
+                )
+                == expected_res
+            )
diff --git a/tests/ops/qubit/test_state_prep.py b/tests/ops/qubit/test_state_prep.py
index d20ecf68c..7dda42db1 100644
--- a/tests/ops/qubit/test_state_prep.py
+++ b/tests/ops/qubit/test_state_prep.py
@@ -162,14 +162,15 @@ class TestDecomposition:
             (np.array([1, 1j, 1j, 1])),
         ],
     )
-    def test_StatePrep_normalize(self, state):
+    @pytest.mark.parametrize("validate_norm", [True, False])
+    def test_StatePrep_normalize(self, state, validate_norm):
         """Test that StatePrep normalizes the input state correctly."""
 
         wires = (0, 1)
 
         @qml.qnode(qml.device("default.qubit", wires=2))
         def circuit():
-            qml.StatePrep(state, normalize=True, wires=wires, validate_norm=True)
+            qml.StatePrep(state, normalize=True, wires=wires, validate_norm=validate_norm)
             return qml.state()
 
         assert np.allclose(circuit(), state / 2)
diff --git a/tests/python_compiler/test_mbqc_dialect.py b/tests/python_compiler/test_mbqc_dialect.py
new file mode 100644
index 000000000..947a7f3d0
--- /dev/null
+++ b/tests/python_compiler/test_mbqc_dialect.py
@@ -0,0 +1,163 @@
+# Copyright 2025 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.
+
+"""Unit test module for pennylane/compiler/python_compiler/mbqc_dialect.py."""
+
+import pytest
+
+# pylint: disable=wrong-import-position
+
+xdsl = pytest.importorskip("xdsl")
+filecheck = pytest.importorskip("filecheck")
+
+pytestmark = pytest.mark.external
+
+from xdsl.dialects import builtin, test
+from xdsl.utils.exceptions import VerifyException
+
+from pennylane.compiler.python_compiler import mbqc_dialect as mbqc
+from pennylane.compiler.python_compiler.mbqc_dialect import MBQCDialect
+from pennylane.compiler.python_compiler.quantum_dialect import QuantumDialect
+
+all_ops = list(MBQCDialect.operations)
+all_attrs = list(MBQCDialect.attributes)
+
+expected_ops_names = {
+    "MeasureInBasisOp": "mbqc.measure_in_basis",
+}
+
+expected_attrs_names = {
+    "MeasurementPlaneAttr": "mbqc.measurement_plane",
+}
+
+
+def test_mbqc_dialect_name():
+    """Test that the MBQCDialect name is correct."""
+    assert MBQCDialect.name == "mbqc"
+
+
+@pytest.mark.parametrize("op", all_ops)
+def test_all_operations_names(op):
+    """Test that all operations have the expected name."""
+    op_class_name = op.__name__
+    expected_name = expected_ops_names.get(op_class_name)
+    assert expected_name is not None, f"Unexpected operation {op_class_name} found in MBQCDialect"
+    assert op.name == expected_name
+
+
+@pytest.mark.parametrize("attr", all_attrs)
+def test_all_attributes_names(attr):
+    """Test that all attributes have the expected name."""
+    attr_class_name = attr.__name__
+    expected_name = expected_attrs_names.get(attr_class_name)
+    assert expected_name is not None, f"Unexpected attribute {attr_class_name} found in MBQCDialect"
+    assert attr.name == expected_name
+
+
+def test_assembly_format(run_filecheck):
+    """Test the assembly format of the mbqc ops."""
+    program = r"""
+    // CHECK: [[angle:%.+]] = "test.op"() : () -> f64
+    %angle = "test.op"() : () -> f64
+
+    // CHECK: [[qubit:%.+]] = "test.op"() : () -> !quantum.bit
+    %qubit = "test.op"() : () -> !quantum.bit
+
+    // CHECK: [[mres0:%.+]], [[out_qubit0:%.+]] = mbqc.measure_in_basis{{\s*}}[XY, [[angle]]] [[qubit]] : i1, !quantum.bit
+    %mres0, %out_qubit0 = mbqc.measure_in_basis [XY, %angle] %qubit : i1, !quantum.bit
+
+    // CHECK: [[mres1:%.+]], [[out_qubit1:%.+]] = mbqc.measure_in_basis{{\s*}}[YZ, [[angle]]] [[qubit]] : i1, !quantum.bit
+    %mres1, %out_qubit1 = mbqc.measure_in_basis [YZ, %angle] %qubit : i1, !quantum.bit
+
+    // CHECK: [[mres2:%.+]], [[out_qubit2:%.+]] = mbqc.measure_in_basis{{\s*}}[ZX, [[angle]]] [[qubit]] : i1, !quantum.bit
+    %mres2, %out_qubit2 = mbqc.measure_in_basis [ZX, %angle] %qubit : i1, !quantum.bit
+
+    // CHECK: [[mres3:%.+]], [[out_qubit3:%.+]] = mbqc.measure_in_basis{{\s*}}[XY, [[angle]]] [[qubit]] postselect 0 : i1, !quantum.bit
+    %mres3, %out_qubit3 = mbqc.measure_in_basis [XY, %angle] %qubit postselect 0 : i1, !quantum.bit
+
+    // CHECK: [[mres4:%.+]], [[out_qubit4:%.+]] = mbqc.measure_in_basis{{\s*}}[XY, [[angle]]] [[qubit]] postselect 1 : i1, !quantum.bit
+    %mres4, %out_qubit4 = mbqc.measure_in_basis [XY, %angle] %qubit postselect 1 : i1, !quantum.bit
+    """
+
+    ctx = xdsl.context.Context()
+
+    ctx.load_dialect(builtin.Builtin)
+    ctx.load_dialect(test.Test)
+    ctx.load_dialect(QuantumDialect)
+    ctx.load_dialect(MBQCDialect)
+
+    module = xdsl.parser.Parser(ctx, program).parse_module()
+
+    run_filecheck(program, module)
+
+
+class TestMeasureInBasisOp:
+    """Unit tests for the mbqc.measure_in_basis op."""
+
+    @pytest.mark.parametrize("plane,", ["XY", "YZ", "ZX"])
+    @pytest.mark.parametrize("postselect", ["", "postselect 0", "postselect 1"])
+    def test_measure_in_basis_properties(self, plane, postselect):
+        """Test the parsing of the mbqc.measure_in_basis op's properties."""
+        program = rf"""
+        %angle = "test.op"() : () -> f64
+        %qubit = "test.op"() : () -> !quantum.bit
+
+        %mres, %out_qubit = mbqc.measure_in_basis [{plane}, %angle] %qubit {postselect} : i1, !quantum.bit
+        """
+
+        ctx = xdsl.context.Context()
+
+        ctx.load_dialect(builtin.Builtin)
+        ctx.load_dialect(test.Test)
+        ctx.load_dialect(QuantumDialect)
+        ctx.load_dialect(MBQCDialect)
+
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+
+        measure_in_basis_op: mbqc.MeasureInBasisOp = module.ops.last
+        assert isinstance(measure_in_basis_op, mbqc.MeasureInBasisOp)
+
+        assert measure_in_basis_op.properties["plane"].data == plane
+
+        if postselect:
+            assert measure_in_basis_op.properties["postselect"].value.data == int(postselect[-1])
+        else:
+            assert measure_in_basis_op.properties.get("postselect") is None
+
+    @pytest.mark.parametrize("postselect", [-1, 2])
+    def test_invalid_postselect_raises_on_verify(self, postselect):
+        """Test that using an invalid postselect value (a value other than 0 or 1) raises a
+        VerifyException during verification."""
+
+        program = rf"""
+        %angle = "test.op"() : () -> f64
+        %qubit = "test.op"() : () -> !quantum.bit
+
+        %mres, %out_qubit = mbqc.measure_in_basis [XY, %angle] %qubit postselect {postselect} : i1, !quantum.bit
+        """
+
+        ctx = xdsl.context.Context()
+
+        ctx.load_dialect(builtin.Builtin)
+        ctx.load_dialect(test.Test)
+        ctx.load_dialect(QuantumDialect)
+        ctx.load_dialect(MBQCDialect)
+
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+
+        measure_in_basis_op: mbqc.MeasureInBasisOp = module.ops.last
+        assert isinstance(measure_in_basis_op, mbqc.MeasureInBasisOp)
+
+        with pytest.raises(VerifyException, match="'postselect' must be 0 or 1"):
+            measure_in_basis_op.verify_()
diff --git a/tests/python_compiler/transforms/test_xdsl_measurements_from_samples.py b/tests/python_compiler/transforms/test_xdsl_measurements_from_samples.py
new file mode 100644
index 000000000..8cf432b8f
--- /dev/null
+++ b/tests/python_compiler/transforms/test_xdsl_measurements_from_samples.py
@@ -0,0 +1,876 @@
+# Copyright 2025 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.
+
+"""Unit and integration tests for the Python compiler `measurements_from_samples` transform."""
+
+# pylint: disable=wrong-import-position
+
+from functools import partial
+
+import numpy as np
+import pytest
+
+pytestmark = pytest.mark.external
+
+xdsl = pytest.importorskip("xdsl")
+from xdsl.dialects import arith, builtin, func, tensor
+
+catalyst = pytest.importorskip("catalyst")
+from catalyst.passes import xdsl_plugin
+
+import pennylane as qml
+from pennylane.compiler.python_compiler import quantum_dialect as quantum
+from pennylane.compiler.python_compiler.transforms import (
+    MeasurementsFromSamplesPass,
+    measurements_from_samples_pass,
+)
+
+
+@pytest.fixture(name="context_and_pipeline", scope="function")
+def fixture_context_and_pipeline():
+    """A fixture that prepares the context and pipeline for unit tests of the
+    measurements-from-samples pass.
+    """
+    ctx = xdsl.context.Context(allow_unregistered=True)
+    ctx.load_dialect(builtin.Builtin)
+    ctx.load_dialect(func.Func)
+    ctx.load_dialect(tensor.Tensor)
+    ctx.load_dialect(arith.Arith)
+    ctx.load_dialect(quantum.QuantumDialect)
+
+    pipeline = xdsl.passes.PipelinePass((MeasurementsFromSamplesPass(),))
+
+    yield ctx, pipeline
+
+
+class TestMeasurementsFromSamplesPass:
+    """Unit tests for the measurements-from-samples pass."""
+
+    def test_1_wire_expval(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 1-wire circuit terminating with an expval(Z)
+        measurement.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[q0:%.+]] = "test.op"() : () -> !quantum.bit
+                %2 = "test.op"() : () -> !quantum.bit
+
+                // CHECK-NOT: quantum.namedobs
+                %3 = quantum.namedobs %2[PauliZ] : !quantum.obs
+
+                // CHECK: [[obs:%.+]] = quantum.compbasis qubits [[q0]] : !quantum.obs
+                // CHECK: [[samples:%.+]] = quantum.sample [[obs]] : tensor<1x1xf64>
+                // CHECK: [[c0:%.+]] = arith.constant dense<0> : tensor<i64>
+                // CHECK: [[res:%.+]] = func.call @expval_from_samples.tensor.1x1xf64([[samples]], [[c0]]) :
+                // CHECK-SAME: (tensor<1x1xf64>, tensor<i64>) -> tensor<f64>
+                // CHECK-NOT: quantum.expval
+                %4 = quantum.expval %3 : f64
+                %5 = "tensor.from_elements"(%4) : (f64) -> tensor<f64>
+
+                // CHECK: func.return [[res]] : tensor<f64>
+                func.return %5 : tensor<f64>
+            }
+            // CHECK-LABEL: func.func public @expval_from_samples.tensor.1x1xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    def test_1_wire_var(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 1-wire circuit terminating with a var(Z)
+        measurement.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[q0:%.+]] = "test.op"() : () -> !quantum.bit
+                %2 = "test.op"() : () -> !quantum.bit
+
+                // CHECK-NOT: quantum.namedobs
+                %3 = quantum.namedobs %2[PauliZ] : !quantum.obs
+
+                // CHECK: [[obs:%.+]] = quantum.compbasis qubits [[q0]] : !quantum.obs
+                // CHECK: [[samples:%.+]] = quantum.sample [[obs]] : tensor<1x1xf64>
+                // CHECK: [[c0:%.+]] = arith.constant dense<0> : tensor<i64>
+                // CHECK: [[res:%.+]] = func.call @var_from_samples.tensor.1x1xf64([[samples]], [[c0]]) :
+                // CHECK-SAME: (tensor<1x1xf64>, tensor<i64>) -> tensor<f64>
+                // CHECK-NOT: quantum.var
+                %4 = quantum.var %3 : f64
+                %5 = "tensor.from_elements"(%4) : (f64) -> tensor<f64>
+
+                // CHECK: func.return [[res]] : tensor<f64>
+                func.return %5 : tensor<f64>
+            }
+            // CHECK-LABEL: func.func public @var_from_samples.tensor.1x1xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    def test_1_wire_probs(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 1-wire circuit terminating with a probs
+        measurement.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[q0:%.+]] = "test.op"() <{nqubits_attr = 1 : i64}> : () -> !quantum.reg
+                %2 = "test.op"() <{nqubits_attr = 1 : i64}> : () -> !quantum.reg
+
+                // CHECK: [[compbasis:%.+]] = quantum.compbasis qreg [[q0]] : !quantum.obs
+                %3 = quantum.compbasis qreg %2 : !quantum.obs
+
+                // CHECK: [[samples:%.+]] = quantum.sample [[compbasis]] : tensor<1x1xf64>
+                // CHECK: [[res:%.+]] = func.call @probs_from_samples.tensor.1x1xf64([[samples]]) :
+                // CHECK-SAME: (tensor<1x1xf64>) -> tensor<2xf64>
+                // CHECK-NOT: quantum.probs
+                %4 = quantum.probs %3 : tensor<2xf64>
+
+                // CHECK: func.return [[res]] : tensor<2xf64>
+                func.return %4 : tensor<2xf64>
+            }
+            // CHECK-LABEL: func.func public @probs_from_samples.tensor.1x1xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    def test_1_wire_sample(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 1-wire circuit terminating with a sample
+        measurement.
+
+        This pass should be a no-op.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[q0:%.+]] = "test.op"() : () -> !quantum.reg
+                %2 = "test.op"() : () -> !quantum.reg
+
+                // CHECK: [[compbasis:%.+]] = quantum.compbasis qreg [[q0]] : !quantum.obs
+                %3 = quantum.compbasis qreg %2 : !quantum.obs
+
+                // CHECK: [[samples:%.+]] = quantum.sample [[compbasis]] : tensor<1x1xf64>
+                %4 = quantum.sample %3 : tensor<1x1xf64>
+
+                // CHECK: func.return [[samples]] : tensor<1x1xf64>
+                func.return %4 : tensor<1x1xf64>
+            }
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    @pytest.mark.xfail(reason="Counts not supported", strict=True, raises=NotImplementedError)
+    def test_1_wire_counts(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 1-wire circuit terminating with a counts
+        measurement.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[q0:%.+]] = "test.op"() : () -> !quantum.reg
+                %2 = "test.op"() : () -> !quantum.reg
+
+                // CHECK: [[compbasis:%.+]] = quantum.compbasis qreg [[q0]] : !quantum.obs
+                %3 = quantum.compbasis qreg %2 : !quantum.obs
+
+                // CHECK: [[samples:%.+]] = quantum.sample %3 : tensor<1x1xf64>
+                // CHECK: [[eigvals:%.+]], [[counts:%.+]] = func.call @counts_from_samples.tensor.1x1xf64([[samples]]) :
+                // CHECK-SAME: (tensor<1x1xf64>) -> tensor<2xf64>, tensor<2xi64>
+                // CHECK-NOT: quantum.counts
+                %eigvals, %counts = quantum.counts %3 : tensor<2xf64>, tensor<2xi64>
+
+                // CHECK: [[eigvals_converted:%.+]] = {{.*}}stablehlo.convert{{.+}}[[eigvals]] :
+                // CHECK-SAME: (tensor<2xf64>) -> tensor<2xi64>
+                %4 = "stablehlo.convert"(%eigvals) : (tensor<2xf64>) -> tensor<2xi64>
+
+                // CHECK: func.return [[eigvals_converted]], [[counts]] : tensor<1x1xf64>
+                func.return %4, %counts : tensor<2xi64>, tensor<2xi64>
+            }
+            // CHECK-LABEL: func.func public @counts_from_samples.tensor.1x1xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    def test_2_wire_expval(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 2-wire circuit terminating with an expval(Z)
+        measurement on each wire.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[q0:%.+]] = "test.op"() : () -> !quantum.bit
+                %2 = "test.op"() : () -> !quantum.bit
+
+                // CHECK: [[q1:%.+]] = "test.op"() : () -> !quantum.bit
+                %3 = "test.op"() : () -> !quantum.bit
+
+                // CHECK-NOT: quantum.namedobs
+                %4 = quantum.namedobs %2[PauliZ] : !quantum.obs
+                %5 = quantum.namedobs %3[PauliZ] : !quantum.obs
+
+                // CHECK: [[obs0:%.+]] = quantum.compbasis qubits [[q0]] : !quantum.obs
+                // CHECK: [[samples0:%.+]] = quantum.sample [[obs0]] : tensor<1x1xf64>
+                // CHECK: [[c0:%.+]] = arith.constant dense<0> : tensor<i64>
+                // CHECK: [[obs1:%.+]] = quantum.compbasis qubits [[q1]] : !quantum.obs
+                // CHECK: [[samples1:%.+]] = quantum.sample [[obs1]] : tensor<1x1xf64>
+                // CHECK: [[c1:%.+]] = arith.constant dense<0> : tensor<i64>
+                // CHECK: [[res0:%.+]] = func.call @expval_from_samples.tensor.1x1xf64([[samples0]], [[c0]]) :
+                // CHECK-SAME: (tensor<1x1xf64>, tensor<i64>) -> tensor<f64>
+                // CHECK: [[res1:%.+]] = func.call @expval_from_samples.tensor.1x1xf64([[samples1]], [[c1]]) :
+                // CHECK-SAME: (tensor<1x1xf64>, tensor<i64>) -> tensor<f64>
+                // CHECK-NOT: quantum.expval
+                %6 = quantum.expval %4 : f64
+                %7 = "tensor.from_elements"(%6) : (f64) -> tensor<f64>
+                %8 = quantum.expval %5 : f64
+                %9 = "tensor.from_elements"(%8) : (f64) -> tensor<f64>
+
+                // CHECK: func.return [[res0]], [[res1]] : tensor<f64>, tensor<f64>
+                func.return %7, %9 : tensor<f64>, tensor<f64>
+            }
+            // CHECK-LABEL: func.func public @expval_from_samples.tensor.1x1xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    def test_2_wire_var(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 2-wire circuit terminating with a var(Z)
+        measurement on each wire.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[q0:%.+]] = "test.op"() : () -> !quantum.bit
+                %2 = "test.op"() : () -> !quantum.bit
+
+                // CHECK: [[q1:%.+]] = "test.op"() : () -> !quantum.bit
+                %3 = "test.op"() : () -> !quantum.bit
+
+                // CHECK-NOT: quantum.namedobs
+                %4 = quantum.namedobs %2[PauliZ] : !quantum.obs
+                %5 = quantum.namedobs %3[PauliZ] : !quantum.obs
+
+                // CHECK: [[obs0:%.+]] = quantum.compbasis qubits [[q0]] : !quantum.obs
+                // CHECK: [[samples0:%.+]] = quantum.sample [[obs0]] : tensor<1x1xf64>
+                // CHECK: [[c0:%.+]] = arith.constant dense<0> : tensor<i64>
+                // CHECK: [[obs1:%.+]] = quantum.compbasis qubits [[q1]] : !quantum.obs
+                // CHECK: [[samples1:%.+]] = quantum.sample [[obs1]] : tensor<1x1xf64>
+                // CHECK: [[c1:%.+]] = arith.constant dense<0> : tensor<i64>
+                // CHECK: [[res0:%.+]] = func.call @var_from_samples.tensor.1x1xf64([[samples0]], [[c0]]) :
+                // CHECK-SAME: (tensor<1x1xf64>, tensor<i64>) -> tensor<f64>
+                // CHECK: [[res1:%.+]] = func.call @var_from_samples.tensor.1x1xf64([[samples1]], [[c1]]) :
+                // CHECK-SAME: (tensor<1x1xf64>, tensor<i64>) -> tensor<f64>
+                // CHECK-NOT: quantum.var
+                %6 = quantum.var %4 : f64
+                %7 = "tensor.from_elements"(%6) : (f64) -> tensor<f64>
+                %8 = quantum.var %5 : f64
+                %9 = "tensor.from_elements"(%8) : (f64) -> tensor<f64>
+
+                // CHECK: func.return [[res0]], [[res1]] : tensor<f64>, tensor<f64>
+                func.return %7, %9 : tensor<f64>, tensor<f64>
+            }
+            // CHECK-LABEL: func.func public @var_from_samples.tensor.1x1xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    def test_2_wire_probs_global(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 2-wire circuit terminating with a "global"
+        probs measurement (one that implicitly acts on all wires).
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                // CHECK: [[qreg:%.+]] = {{.*}}quantum.alloc{{.*}} : () -> !quantum.reg
+                %2 = "quantum.alloc"() <{nqubits_attr = 2 : i64}> : () -> !quantum.reg
+
+                // CHECK: [[compbasis:%.+]] = quantum.compbasis qreg [[qreg]] : !quantum.obs
+                %3 = quantum.compbasis qreg %2 : !quantum.obs
+
+                // CHECK: [[samples:%.+]] = quantum.sample [[compbasis]] : tensor<1x2xf64>
+                // CHECK: [[res:%.+]] = func.call @probs_from_samples.tensor.1x2xf64([[samples]]) :
+                // CHECK-SAME: (tensor<1x2xf64>) -> tensor<4xf64>
+                // CHECK-NOT: quantum.probs
+                %4 = quantum.probs %3 : tensor<4xf64>
+
+                // CHECK: func.return [[res]] : tensor<4xf64>
+                func.return %4 : tensor<4xf64>
+            }
+            // CHECK-LABEL: func.func public @probs_from_samples.tensor.1x2xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+    def test_2_wire_probs_per_wire(self, context_and_pipeline, run_filecheck):
+        """Test the measurements-from-samples pass on a 2-wire circuit terminating with separate
+        probs measurements per wire.
+        """
+        program = """
+        builtin.module @module_circuit {
+            // CHECK-LABEL: circuit
+            func.func public @circuit() -> (tensor<f64>) {
+                %0 = "stablehlo.constant"() <{value = dense<1> : tensor<i64>}> : () -> tensor<i64>
+                %1 = tensor.extract %0[] : tensor<i64>
+                quantum.device shots(%1) ["", "", ""]
+
+                %2 = "quantum.alloc"() <{nqubits_attr = 2 : i64}> : () -> !quantum.reg
+
+                // CHECK: [[q0:%.+]] = {{.*}}quantum.extract{{.*}}idx_attr = 0{{.*}} -> !quantum.bit
+                %3 = "quantum.extract"(%2) <{idx_attr = 0 : i64}> : (!quantum.reg) -> !quantum.bit
+
+                // CHECK: [[compbasis0:%.+]] = quantum.compbasis qubits [[q0]] : !quantum.obs
+                %4 = quantum.compbasis qubits %3 : !quantum.obs
+
+                // CHECK: [[samples0:%.+]] = quantum.sample [[compbasis0]] : tensor<1x1xf64>
+                // CHECK: [[res0:%.+]] = func.call @probs_from_samples.tensor.1x1xf64([[samples0]]) :
+                // CHECK-SAME: (tensor<1x1xf64>) -> tensor<2xf64>
+                // CHECK-NOT: quantum.probs
+                %5 = quantum.probs %4 : tensor<2xf64>
+
+                // CHECK: [[q1:%.+]] = {{.*}}quantum.extract{{.*}}idx_attr = 1{{.*}} -> !quantum.bit
+                %6 = "quantum.extract"(%2) <{idx_attr = 1 : i64}> : (!quantum.reg) -> !quantum.bit
+
+                // CHECK: [[compbasis1:%.+]] = quantum.compbasis qubits [[q1]] : !quantum.obs
+                %7 = quantum.compbasis qubits %6 : !quantum.obs
+
+                // CHECK: [[samples1:%.+]] = quantum.sample [[compbasis1]] : tensor<1x1xf64>
+                // CHECK: [[res1:%.+]] = func.call @probs_from_samples.tensor.1x1xf64([[samples1]]) :
+                // CHECK-SAME: (tensor<1x1xf64>) -> tensor<2xf64>
+                // CHECK-NOT: quantum.probs
+                %8 = quantum.probs %7 : tensor<2xf64>
+
+                // CHECK: func.return [[res0]], [[res1]] : tensor<2xf64>, tensor<2xf64>
+                func.return %5, %8 : tensor<2xf64>, tensor<2xf64>
+            }
+            // CHECK-LABEL: func.func public @probs_from_samples.tensor.1x1xf64
+        }
+        """
+
+        ctx, pipeline = context_and_pipeline
+        module = xdsl.parser.Parser(ctx, program).parse_module()
+        pipeline.apply(ctx, module)
+
+        run_filecheck(program, module)
+
+
+class TestMeasurementsFromSamplesExecution:
+    """Tests of the execution of simple workloads with the xDSL-based MeasurementsFromSamplesPass
+    transform.
+    """
+
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_op, mp, obs, expected_res",
+        [
+            # PauliZ observables
+            (qml.I, qml.expval, qml.Z, 1.0),
+            (qml.X, qml.expval, qml.Z, -1.0),
+            (qml.I, qml.var, qml.Z, 0.0),
+            (qml.X, qml.var, qml.Z, 0.0),
+            # PauliX observables
+            pytest.param(
+                partial(qml.RY, phi=np.pi / 2),
+                qml.expval,
+                qml.X,
+                1.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+            pytest.param(
+                partial(qml.RY, phi=-np.pi / 2),
+                qml.expval,
+                qml.X,
+                -1.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+            pytest.param(
+                partial(qml.RY, phi=np.pi / 2),
+                qml.var,
+                qml.X,
+                0.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+            pytest.param(
+                partial(qml.RY, phi=-np.pi / 2),
+                qml.var,
+                qml.X,
+                0.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+            # PauliY observables
+            pytest.param(
+                partial(qml.RX, phi=-np.pi / 2),
+                qml.expval,
+                qml.Y,
+                1.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+            pytest.param(
+                partial(qml.RX, phi=np.pi / 2),
+                qml.expval,
+                qml.Y,
+                -1.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+            pytest.param(
+                partial(qml.RX, phi=-np.pi / 2),
+                qml.var,
+                qml.Y,
+                0.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+            pytest.param(
+                partial(qml.RX, phi=np.pi / 2),
+                qml.var,
+                qml.Y,
+                0.0,
+                marks=pytest.mark.xfail(
+                    reason="Only PauliZ-basis measurements supported",
+                    strict=True,
+                    raises=NotImplementedError,
+                ),
+            ),
+        ],
+    )
+    # pylint: disable=too-many-arguments,too-many-positional-arguments
+    def test_exec_1_wire_mp_with_obs(self, shots, initial_op, mp, obs, expected_res):
+        """Test the measurements_from_samples transform on a device with a single wire and terminal
+        measurements that require an observable (i.e. expval and var).
+        """
+
+        dev = qml.device("lightning.qubit", wires=1, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_op(wires=0)
+            return mp(obs(wires=0))
+
+        assert expected_res == circuit_ref(), "Sanity check failed, is expected_res correct?"
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        assert expected_res == circuit_compiled()
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_op, expected_res",
+        [
+            (qml.I, [1.0, 0.0]),
+            (qml.X, [0.0, 1.0]),
+        ],
+    )
+    def test_exec_1_wire_probs(self, shots, initial_op, expected_res):
+        """Test the measurements_from_samples transform on a device with a single wire and terminal
+        probs measurements.
+        """
+
+        dev = qml.device("lightning.qubit", wires=1, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_op(wires=0)
+            return qml.probs(wires=0)
+
+        assert np.array_equal(
+            expected_res, circuit_ref()
+        ), "Sanity check failed, is expected_res correct?"
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        assert np.array_equal(expected_res, circuit_compiled())
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.xfail(
+        reason="Counts not supported in Catalyst with program capture",
+        strict=True,
+        raises=NotImplementedError,
+    )
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_op, expected_res",
+        [
+            (qml.I, {"0": 10, "1": 0}),
+            (qml.X, {"0": 0, "1": 10}),
+        ],
+    )
+    def test_exec_1_wire_counts(self, shots, initial_op, expected_res):
+        """Test the measurements_from_samples transform on a device with a single wire and terminal
+        counts measurements.
+        """
+
+        dev = qml.device("lightning.qubit", wires=1, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_op(wires=0)
+            return qml.counts(wires=0)
+
+        assert np.array_equal(
+            expected_res, circuit_ref()
+        ), "Sanity check failed, is expected_res correct?"
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        assert np.array_equal(expected_res, _counts_catalyst_to_pl(*circuit_compiled()))
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_op, expected_res_base",
+        [
+            (qml.I, 0),
+            (qml.X, 1),
+        ],
+    )
+    def test_exec_1_wire_sample(self, shots, initial_op, expected_res_base):
+        """Test the measurements_from_samples transform on a device with a single wire and terminal
+        sample measurements.
+
+        In this case, the measurements_from_samples pass should effectively be a no-op.
+        """
+        dev = qml.device("lightning.qubit", wires=1, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_op(wires=0)
+            return qml.sample(wires=0)
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        expected_res = expected_res_base * np.ones(shape=(shots, 1), dtype=int)
+
+        assert np.array_equal(expected_res, circuit_compiled())
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_ops, mp, obs, expected_res",
+        [
+            ((qml.I, qml.I), qml.expval, qml.Z, (1.0, 1.0)),
+            ((qml.I, qml.X), qml.expval, qml.Z, (1.0, -1.0)),
+            ((qml.X, qml.I), qml.expval, qml.Z, (-1.0, 1.0)),
+            ((qml.X, qml.X), qml.expval, qml.Z, (-1.0, -1.0)),
+            ((qml.I, qml.I), qml.var, qml.Z, (0.0, 0.0)),
+            ((qml.I, qml.X), qml.var, qml.Z, (0.0, 0.0)),
+            ((qml.X, qml.I), qml.var, qml.Z, (0.0, 0.0)),
+            ((qml.X, qml.X), qml.var, qml.Z, (0.0, 0.0)),
+        ],
+    )
+    # pylint: disable=too-many-arguments,too-many-positional-arguments
+    def test_exec_2_wire_with_obs_separate(self, shots, initial_ops, mp, obs, expected_res):
+        """Test the measurements_from_samples transform on a device with two wires and terminal
+        measurements that require an observable (i.e. expval and var).
+
+        In this test, the terminal measurements are performed separately per wire.
+        """
+
+        dev = qml.device("lightning.qubit", wires=2, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_ops[0](wires=0)
+            initial_ops[1](wires=1)
+            return mp(obs(wires=0)), mp(obs(wires=1))
+
+        assert expected_res == circuit_ref(), "Sanity check failed, is expected_res correct?"
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        assert expected_res == circuit_compiled()
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.xfail(
+        reason="Operator arithmetic not yet supported with capture enabled", strict=True
+    )
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_ops, mp, expected_res",
+        [
+            ((qml.I, qml.I), qml.expval, 1.0),
+            ((qml.I, qml.X), qml.expval, -1.0),
+            ((qml.X, qml.I), qml.expval, -1.0),
+            ((qml.X, qml.X), qml.expval, 1.0),
+            ((qml.I, qml.I), qml.var, 0.0),
+            ((qml.I, qml.X), qml.var, 0.0),
+            ((qml.X, qml.I), qml.var, 0.0),
+            ((qml.X, qml.X), qml.var, 0.0),
+        ],
+    )
+    def test_exec_2_wire_with_obs_combined(self, shots, initial_ops, mp, expected_res):
+        """Test the measurements_from_samples transform on a device with two wires and terminal
+        measurements that require an observable (i.e. expval and var).
+
+        In this test, the terminal measurements are performed on the combination of both wires.
+        """
+
+        dev = qml.device("lightning.qubit", wires=2, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_ops[0](wires=0)
+            initial_ops[1](wires=1)
+            return mp(qml.Z(wires=0) @ qml.Z(wires=1))
+
+        assert expected_res == circuit_ref(), "Sanity check failed, is expected_res correct?"
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        assert expected_res == circuit_compiled()
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_ops, expected_res",
+        [
+            ((qml.I, qml.I), [1.0, 0.0, 0.0, 0.0]),
+            ((qml.I, qml.X), [0.0, 1.0, 0.0, 0.0]),
+            ((qml.X, qml.I), [0.0, 0.0, 1.0, 0.0]),
+            ((qml.X, qml.X), [0.0, 0.0, 0.0, 1.0]),
+        ],
+    )
+    def test_exec_2_wire_probs_global(self, shots, initial_ops, expected_res):
+        """Test the measurements_from_samples transform on a device with two wires and a terminal,
+        "global" probs measurements (one that implicitly acts on all wires).
+        """
+        dev = qml.device("lightning.qubit", wires=2, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_ops[0](wires=0)
+            initial_ops[1](wires=1)
+            return qml.probs()
+
+        assert np.array_equal(
+            expected_res, circuit_ref()
+        ), "Sanity check failed, is expected_res correct?"
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        assert np.array_equal(expected_res, circuit_compiled())
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    @pytest.mark.parametrize("shots", [1, 2])
+    @pytest.mark.parametrize(
+        "initial_ops, expected_res",
+        [
+            ((qml.I, qml.I), ([1.0, 0.0], [1.0, 0.0])),
+            ((qml.I, qml.X), ([1.0, 0.0], [0.0, 1.0])),
+            ((qml.X, qml.I), ([0.0, 1.0], [1.0, 0.0])),
+            ((qml.X, qml.X), ([0.0, 1.0], [0.0, 1.0])),
+        ],
+    )
+    def test_exec_2_wire_probs_per_wire(self, shots, initial_ops, expected_res):
+        """Test the measurements_from_samples transform on a device with two wires and a terminal,
+        "global" probs measurements (one that implicitly acts on all wires).
+        """
+        dev = qml.device("lightning.qubit", wires=2, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit_ref():
+            initial_ops[0](wires=0)
+            initial_ops[1](wires=1)
+            return qml.probs(wires=0), qml.probs(wires=1)
+
+        assert np.array_equal(
+            expected_res, circuit_ref()
+        ), "Sanity check failed, is expected_res correct?"
+
+        circuit_compiled = qml.qjit(
+            measurements_from_samples_pass(circuit_ref),
+            pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()],
+        )
+
+        assert np.array_equal(expected_res, circuit_compiled())
+
+    # -------------------------------------------------------------------------------------------- #
+
+    @pytest.mark.xfail(reason="Dynamic shots not supported")
+    @pytest.mark.usefixtures("enable_disable_plxpr")
+    def test_exec_expval_dynamic_shots(self):
+        """Test the measurements_from_samples transform where the number of shots is dynamic.
+
+        This use case is not currently supported.
+        """
+
+        @qml.qjit(pass_plugins=[xdsl_plugin.getXDSLPluginAbsolutePath()])
+        def workload(shots):
+            dev = qml.device("lightning.qubit", wires=1, shots=shots)
+
+            @measurements_from_samples_pass
+            @qml.qnode(dev)
+            def circuit():
+                return qml.expval(qml.Z(wires=0))
+
+            return circuit()
+
+        result = workload(2)
+        assert result == 1.0
+
+
+def _counts_catalyst_to_pl(basis_states, counts):
+    """Helper function to convert counts in the Catalyst format to the PennyLane format.
+
+    Example:
+
+    >>> basis_states, counts = ([0, 1], [6, 4])
+    >>> _counts_catalyst_to_pl(basis_states, counts)
+    {'0': 6, '1': 4}
+    """
+    return {format(int(state), "01b"): count for state, count in zip(basis_states, counts)}
+
+
+if __name__ == "__main__":
+    pytest.main(["-x", __file__])