Release 0.12.0¶
New features since last release
A new compilation pass called
ppm_compilation()has been added to Catalyst to transform Clifford+T gates into Pauli Product Measurements (PPMs) using just one transform, allowing for exploring representations of programs in a new paradigm in logical quantum compilation. (#1750)Based on arXiv:1808.02892, this new compilation pass simplifies circuit transformations and optimizations by combining multiple sub-passes into a single compilation pass, where Clifford+T gates are compiled down to Pauli product rotations (PPRs, \(\exp(-iP_{\{x, y, z\}} \theta)\)) and PPMs:
to_ppr(): converts Clifford+T gates into PPRs.commute_ppr(): commutes PPRs past non-Clifford PPRs.merge_ppr_ppm(): merges Clifford PPRs into PPMs.ppr_to_ppm(): decomposes both non-Clifford PPRs (\(\theta = \tfrac{\pi}{8}\)), consuming a magic state in the process, and Clifford PPRs (\(\theta = \tfrac{\pi}{4}\)) into PPMs. (#1664)
import pennylane as qml from catalyst.passes import ppm_compilation pipeline = [("pipe", ["enforce-runtime-invariants-pipeline"])] @qml.qjit(pipelines=pipeline, target="mlir") @ppm_compilation(decompose_method="clifford-corrected", avoid_y_measure=True, max_pauli_size=2) @qml.qnode(qml.device("null.qubit", wires=2)) def circuit(): qml.CNOT([0, 1]) qml.CNOT([1, 0]) qml.adjoint(qml.T)(0) qml.T(1) return catalyst.measure(0), catalyst.measure(1)
>>> print(circuit.mlir_opt) ... %m, %out:3 = qec.ppm ["Z", "Z", "Z"] %1, %2, %4 : !quantum.bit, !quantum.bit, !quantum.bit %m_0, %out_1:2 = qec.ppm ["Z", "Y"] %3, %out#2 : !quantum.bit, !quantum.bit %m_2, %out_3 = qec.ppm ["X"] %out_1#1 : !quantum.bit %m_4, %out_5 = qec.select.ppm(%m, ["X"], ["Z"]) %out_1#0 : !quantum.bit %5 = arith.xori %m_0, %m_2 : i1 %6:2 = qec.ppr ["Z", "Z"](2) %out#0, %out#1 cond(%5) : !quantum.bit, !quantum.bit quantum.dealloc_qb %out_5 : !quantum.bit quantum.dealloc_qb %out_3 : !quantum.bit %7 = quantum.alloc_qb : !quantum.bit %8 = qec.fabricate magic_conj : !quantum.bit %m_6, %out_7:2 = qec.ppm ["Z", "Z"] %6#1, %8 : !quantum.bit, !quantum.bit %m_8, %out_9:2 = qec.ppm ["Z", "Y"] %7, %out_7#1 : !quantum.bit, !quantum.bit %m_10, %out_11 = qec.ppm ["X"] %out_9#1 : !quantum.bit %m_12, %out_13 = qec.select.ppm(%m_6, ["X"], ["Z"]) %out_9#0 : !quantum.bit %9 = arith.xori %m_8, %m_10 : i1 %10 = qec.ppr ["Z"](2) %out_7#0 cond(%9) : !quantum.bit quantum.dealloc_qb %out_13 : !quantum.bit quantum.dealloc_qb %out_11 : !quantum.bit %m_14, %out_15:2 = qec.ppm ["Z", "Z"] %6#0, %10 : !quantum.bit, !quantum.bit %from_elements = tensor.from_elements %m_14 : tensor<i1> %m_16, %out_17 = qec.ppm ["Z"] %out_15#1 : !quantum.bit ...
A new function called
get_ppm_specs()has been added for acquiring statistics after PPM compilation. (#1794)After compiling a workflow with any combination of
to_ppr(),commute_ppr(),merge_ppr_ppm(),ppr_to_ppm(), orppm_compilation(), useget_ppm_specs()to track useful statistics of the compiled workflow, including:num_pi4_gates: number of Clifford PPRsnum_pi8_gates: number of non-Clifford PPRsnum_pi2_gates: number of classical PPRsmax_weight_pi4: maximum weight of Clifford PPRsmax_weight_pi8: maximum weight of non-Clifford PPRsmax_weight_pi2: maximum weight of classical PPRsnum_logical_qubits: number of logical qubitsnum_of_ppm: number of PPMs
from catalyst.passes import get_ppm_specs, to_ppr, merge_ppr_ppm, commute_ppr pipe = [("pipe", ["enforce-runtime-invariants-pipeline"])] @qjit(pipelines=pipe, target="mlir", autograph=True) def test_convert_clifford_to_ppr_workflow(): device = qml.device("lightning.qubit", wires=2) @merge_ppr_ppm @commute_ppr(max_pauli_size=2) @to_ppr @qml.qnode(device) def f(): qml.CNOT([0, 2]) qml.T(0) return measure(0), measure(1) @merge_ppr_ppm(max_pauli_size=1) @commute_ppr @to_ppr @qml.qnode(device) def g(): qml.CNOT([0, 2]) qml.T(0) qml.T(1) qml.CNOT([0, 1]) for i in range(10): qml.Hadamard(0) return measure(0), measure(1) return f(), g()
>>> ppm_specs = get_ppm_specs(test_convert_clifford_to_ppr_workflow) >>> print(ppm_specs) { 'f_0': {'max_weight_pi8': 1, 'num_logical_qubits': 2, 'num_of_ppm': 2, 'num_pi8_gates': 1}, 'g_0': {'max_weight_pi4': 2, 'max_weight_pi8': 1, 'num_logical_qubits': 2, 'num_of_ppm': 2, 'num_pi4_gates': 36, 'num_pi8_gates': 2} }
Catalyst now supports
qml.Snapshot, which captures quantum states at any point in a circuit. (#1741)For example, the code below is capturing two snapshot’d states, all within a qjit’d circuit:
NUM_QUBITS = 2 dev = qml.device("lightning.qubit", wires=NUM_QUBITS) @qjit @qml.qnode(dev) def circuit(): wires = list(range(NUM_QUBITS)) qml.Snapshot("Initial state") for wire in wires: qml.Hadamard(wires=wire) qml.Snapshot("After applying Hadamard gates") return qml.probs() results = circuit() snapshots, *results = circuit() >>> print(snapshots) [Array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex128), Array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex128)] >>> print(results) Array([0.25, 0.25, 0.25, 0.25], dtype=float64)
>>> print(results) ([Array([1.+0.j, 0.+0.j, 0.+0.j, 0.+0.j], dtype=complex128), Array([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], dtype=complex128)], Array([0.25, 0.25, 0.25, 0.25], dtype=float64))
Catalyst now supports automatic qubit management, meaning that the number of wires does not need to be specified during device initialization. (#1788)
@qjit def workflow(): dev = qml.device("lightning.qubit") # no wires here! @qml.qnode(dev) def circuit(): qml.PauliX(wires=2) return qml.probs() return circuit() print(workflow())
[0. 1. 0. 0. 0. 0. 0. 0.]While this feature adds a lot of convenience, it may also reduce performance on devices where reallocating resources can be expensive, such as statevector simulators.
Two new peephole-optimization compilation passes called
disentangle_cnot()anddisentangle_swap()have been added. Each compilation pass replacesSWAPorCNOTinstructions with other equivalent elementary gates. (#1823)As an example,
disentangle_cnot()applied to the circuit below will replace theCNOTgate with anXgate.dev = qml.device("lightning.qubit", wires=2) @qml.qjit(keep_intermediate=True) @catalyst.passes.disentangle_cnot @qml.qnode(dev) def circuit(): # first qubit in |1> qml.X(0) # second qubit in |0> # current state : |10> qml.CNOT([0,1]) # state after CNOT : |11> return qml.state()
>>> from catalyst.debug import get_compilation_stage >>> print(get_compilation_stage(circuit, stage="QuantumCompilationPass")) ... %out_qubits = quantum.custom "PauliX"() %1 : !quantum.bit %2 = quantum.extract %0[ 1] : !quantum.reg -> !quantum.bit %out_qubits_0 = quantum.custom "PauliX"() %2 : !quantum.bit ...
Improvements 🛠
The
qml.measureoperation for mid-circuit measurements can now be used in qjit-compiled circuits with program capture enabled. (#1766)Note that the simulation behaviour of mid-circuit measurements can differ between PennyLane and Catalyst, depending on the chosen
mcm_method. Please see the Functionality differences from PennyLane section in the sharp bits and debugging tips page for additional information.The behaviour of measurement processes executed on
null.qubitwith qjit is now more consistent with their behaviour onnull.qubitwithout qjit. (#1598)Previously, measurement processes like
qml.sample,qml.counts,qml.probs, etc., returned values from uninitialized memory when executed onnull.qubitwith qjit. This change ensures that measurement processes onnull.qubitalways return the value 0 or the result corresponding to the ‘0’ state, depending on the context.The package name of the Catalyst distribution has been updated to be consistent with PyPA standards, from
PennyLane-Catalysttopennylane_catalyst. This change is not expected to affect users as tools in the Python ecosystem (e.g.pip) already handle both versions through normalization. (#1817)The
commute_ppr()andmerge_ppr_ppm()passes now accept an optionalmax_pauli_sizeargument, which limits the size of the Pauli strings generated by the passes through commutation or absorption rules. (#1719)The
to_ppr()pass is now more efficient by adding support for the direct conversion of Pauli gates (qml.X,qml.Y,qml.Z), the adjoint ofqml.Sgate, and the adjoint of theqml.Tgate. (#1738)The
keep_intermediateargument in theqjitdecorator now accepts a new value that allows for saving intermediate files after each pass. The updated possible options for this argument are:Falseor0orNone: No intermediate files are kept.Trueor1or"pipeline": Intermediate files are saved after each pipeline.2or"pass": Intermediate files are saved after each pass.
The default value is
False. (#1791)The
static_argnumskeyword argument in theqjitdecorator is now compatible with PennyLane program capture enabled (qml.capture.enable). (#1810)Catalyst is compatible with the new
qml.set_shotstransform introduced in PennyLane v0.42. (#1784)null.qubitcan now support an optionaltrack_resourceskeyword argument, which allows it to record which gates are executed. (#1619)import json import glob dev = qml.device("null.qubit", wires=2, track_resources=True) @qml.qjit @qml.qnode(dev) def circuit(): for _ in range(5): qml.H(0) qml.CNOT([0, 1]) return qml.probs() circuit() pattern = "./__pennylane_resources_data_*" filepath = glob.glob(pattern)[0] with open(filepath) as f: resources = json.loads(f.read())
>>> print(resources) {'num_qubits': 2, 'num_gates': 6, 'gate_types': {'CNOT': 1, 'Hadamard': 5}}
Breaking changes 💔
Support for Mac x86 has been removed. This includes Macs running on Intel processors. (#1716)
This is because JAX has also dropped support for it since 0.5.0, with the rationale being that such machines are becoming increasingly scarce.
If support for Mac x86 platforms is still desired, please install Catalyst v0.11.0, PennyLane v0.41.0, PennyLane-Lightning v0.41.0, and JAX v0.4.28.
(Device Developers Only) The
QuantumDeviceinterface in the Catalyst Runtime plugin system has been modified, which requires recompiling plugins for binary compatibility. (#1680)As announced in the 0.10.0 release, the
shotsargument has been removed from theSampleandCountsmethods in the interface, since it unnecessarily duplicated this information. Additionally,shotswill no longer be supplied by Catalyst through thekwargsparameter of the device constructor. The shot value must now be obtained through theSetDeviceShotsmethod.Further, the documentation for the interface has been overhauled and now describes the expected behaviour of each method in detail. A quality of life improvement is that optional methods are now clearly marked as such and also come with a default implementation in the base class, so device plugins need only override the methods they wish to support.
Finally, the
PrintStateand theOne/Zeroutility functions have been removed, since they did not serve a convincing purpose.(Frontend Developers Only) Some Catalyst primitives for JAX have been renamed, and the qubit deallocation primitive has been split into deallocation and a separate device release primitive. (#1720)
qunitary_pis nowunitary_p(unchanged)qmeasure_pis nowmeasure_p(unchanged)qdevice_pis nowdevice_init_p(unchanged)qdealloc_pno longer releases the device, thus it can be used at any point of a quantum execution scopedevice_release_pis a new primitive that must be used to mark the end of a quantum execution scope, which will release the quantum device
Catalyst has removed the
experimental_capturekeyword from theqjitdecorator in favour of unified behaviour with PennyLane. (#1657)Instead of enabling program capture with Catalyst via
qjit(experimental_capture=True), program capture can be enabled via the global toggleqml.capture.enable():import pennylane as qml from catalyst import qjit dev = qml.device("lightning.qubit", wires=2) qml.capture.enable() @qjit @qml.qnode(dev) def circuit(x): qml.Hadamard(0) qml.CNOT([0, 1]) return qml.expval(qml.Z(0)) circuit(0.1)
Disabling program capture can be done with
qml.capture.disable().The
ppr_to_ppmpass functionality has been moved to a new pass calledmerge_ppr_ppm. Theppr_to_ppmfunctionality now handles direct decomposition of PPRs into PPMs. (#1688)The version of JAX used by Catalyst has been updated to v0.6.0. (#1652) (#1729)
Several internal changes were made for this update.
LAPACK kernels are updated to adhere to the new JAX lowering rules for external functions. (#1685)
The trace stack is removed and replaced with a tracing context manager. (#1662)
A new
debug_infoargument is added toJaxpr, themake_jaxprfunctions, andjax.extend.linear_util.wrap_init. (#1670) (#1671) (#1681)
The version of LLVM, mlir-hlo, and Enzyme used by Catalyst has been updated to track those in JAX v0.6.0. (#1752)
The LLVM version has been updated to commit a8513158. The mlir-hlo version has been updated to commit e30c22d1. The Enzyme version has been updated to v0.0.180.
(Device developers only) Device parameters which are forwarded by the Catalyst runtime to plugin devices as a string may not contain nested dictionaries. Previously, these would be parsed incorrectly, and instead will now raise an error. (#1843) (#1846)
Deprecations 👋
Python 3.10 is now deprecated and will not be supported in Catalyst v0.13. Please upgrade to a newer Python version.
Bug fixes 🐛
Fixed Boolean arguments/results not working with the debugging functions
debug.get_cmainanddebug.compile_executable. (#1687)Fixed AutoGraph fallback for valid iteration targets with constant data but no length, for example
itertools.product(range(2), repeat=2). (#1665)Catalyst now correctly supports
qml.StatePrep()andqml.BasisState()operations in the experimental PennyLane program capture pipeline. (#1631)make allnow correctly compiles the standalone plugin with the same compiler used to compile LLVM and MLIR. (#1768)Stacked Python decorators for built-in Catalyst passes are now applied in the correct order. (#1798)
MLIR plugins can now be specified via lists and tuples, not just sets. (#1812)
Fixed the conversion of PLxPR to JAXPR with quantum primitives when using control flow. (#1809)
Fixed a bug in the internal simplification of qubit chains in the compiler, which manifested in certain transformations like
cancel_inversesand led to incorrect results. (#1840)Fixes the conversion of PLxPR to JAXPR with quantum primitives when using dynamic wires. (#1842)
Internal changes ⚙️
The clang-format and clang-tidy versions used by Catalyst have been updated to v20. (#1721)
The Sphinx version has been updated to v8.1. (#1734)
Integration with PennyLane’s experimental Python compiler based on xDSL has been added. This allows developers and users to write xDSL transformations that can be used with Catalyst. (#1715)
An xDSL MLIR plugin has been added to denote whether to use xDSL to execute compilation passes. (#1707)
The function
dataclass.replaceis now used to updateExecutionConfigandMCMConfigrather than mutating properties. (#1814)A function has been added that allows developers to register an equivalent MLIR transform for a given PLxPR transform. (#1705)
Overriding the
num_wiresproperty ofHybridOpis no longer happening when the operator can exist onAnyWires. This allows the deprecation ofWiresEnumin PennyLane. (#1667) (#1676)Catalyst now includes an experimental
mbqcdialect for representing measurement-based quantum-computing protocols in MLIR. (#1663) (#1679)The Catalyst Runtime C-API now includes a stub for the experimental
mbqc.measure_in_basisoperation,__catalyst__mbqc__measure_in_basis(), allowing for mock execution of MBQC workloads containing parameterized arbitrary-basis measurements. (#1674)This runtime stub is currently for mock execution only and should be treated as a placeholder operation. Internally, it functions just as a computational-basis measurement instruction.
Support for quantum subroutines was added. This feature is expected to improve compilation times for large quantum programs. (#1774) (#1828)
PennyLane’s arbitrary-basis measurement operations, such as
qml.ftqc.measure_arbitrary_basis, are now qjit-compatible with PennyLane program capture enabled. (#1645) (#1710)The utility function
EnsureFunctionDeclarationhas been refactored into theUtilsof the Catalyst dialect instead of being duplicated in each individual dialect. (#1683)The assembly format for some MLIR operations now includes
adjoint. (#1695)Improved the definition of
YieldOpin the quantum dialect by removingAnyTypeOf. (#1696)The assembly format of
MeasureOpin theQuantumdialect andMeasureInBasisOpin theMBQCdialect now contains thepostselectattribute. (#1732)The bufferization of custom Catalyst dialects has been migrated to the new one-shot bufferization interface in MLIR. The new MLIR bufferization interface is required by JAX v0.4.29 or higher. (#1027) (#1686) (#1708) (#1740) (#1751) (#1769)
The redundant
OptionalAttrhas been removed from theadjointargument in theQuantumOps.tdTableGen file. (#1746)ValueRangehas been replaced withTypeRangefor creatingCustomOpinIonsDecompositionPatterns.cppto match the build constructors. (#1749)The unused helper function
genArgMapFunctionin the--lower-gradientspass has been removed. (#1753)Base components of
QFuncPLxPRInterpreterhave been moved into a base class calledSubroutineInterpreter. This is intended to reduce code duplication. (#1787)An argument (
openapl_file_name) has been added to theOQDDeviceconstructor to specify the name of the output OpenAPL file. (#1763)The OQD device TOML file has been modified to only include gates that are decomposable to the OQD device target gate set. (#1763)
The
quantum-to-ionpass has been renamed togates-to-pulses. (#1818)The runtime CAPI function
__catalyst__rt__num_qubitsnow has a corresponding JAX primitivenum_qubits_pand quantum dialect operationNumQubitsOp. (#1793)For measurements whose shapes depend on the number of qubits, they now properly retrieve the number of qubits through this new operation when it is dynamic.
The PPR/PPM pass names have been renamed from snake-case to kebab-case in MLIR to align with MLIR conventions. Class names and tests were updated accordingly. Example:
--to_ppris now--to-ppr. (#1802)A new internal python module called
catalyst.from_plxprhas been created to better organize the code for plxpr integration. (#1813)A new
from_plxpr.QregManagerhas been created to handle converting plxpr wire index semantics into catalyst qubit value semantics. (#1813)
Documentation 📝
The header (logo+title) images in the README and in the overview on ReadTheDocs have been updated, reflecting that Catalyst is now beyond beta 🎉! (#1718)
The API section in the documentation has been simplified. The Catalyst ‘Runtime Device Interface’ page has been updated to point directly to the documented
QuantumDevicestruct, and the ‘QIR C-API’ page has been removed due to limited utility. (#1739)
Contributors ✍️
This release contains contributions from (in alphabetical order):
Runor Agbaire, Joey Carter, Isaac De Vlugt, Sengthai Heng, David Ittah, Tzung-Han Juang, Christina Lee, Mehrdad Malekmohammadi, Anton Naim Ibrahim, Erick Ochoa Lopez, Ritu Thombre, Raul Torres, Paul Haochen Wang, Jake Zaia.