Skill Tracer
Skills › Scientific Agent Skills › pennylane › advanced_features.md

advanced_features.md

187 lines · skills/pennylane/references/advanced_features.md at 5f2bc03

Open on GitHub
1# Advanced features
2
3Targets PennyLane 0.45.1. Core/ML blocks are tested; the Catalyst block is explicitly
4illustrative because its native compiler was not executed in this review.
5

Advanced features

Targets PennyLane 0.45.1. Core/ML blocks are tested; the Catalyst block is explicitly illustrative because its native compiler was not executed in this review.

Raw lines 1 to 5
6## Templates with valid shapes and reference states
7
8```python
9import pennylane as qml
10from pennylane import numpy as np
12dev = qml.device("default.qubit", wires=4)
13initial_shape, layer_shape = qml.SimplifiedTwoDesign.shape(n_layers=2, n_wires=4)
14initial = np.zeros(initial_shape, requires_grad=True)
15weights = np.ones(layer_shape, requires_grad=True) * 0.1
16@qml.qnode(dev)
17def two_design(initial, weights):
18 qml.SimplifiedTwoDesign(initial, weights, wires=range(4))
19 return qml.expval(qml.Z(0))
20assert np.isfinite(two_design(initial, weights))
22shape = qml.ParticleConservingU1.shape(n_layers=2, n_wires=4)
23particle_weights = np.ones(shape, requires_grad=True) * 0.1
24@qml.qnode(dev)
25def particle_circuit(weights):
26 qml.ParticleConservingU1(weights, wires=range(4), init_state=np.array([1, 1, 0, 0]))
27 return qml.expval(qml.qchem.particle_number(4))
28assert np.allclose(particle_circuit(particle_weights), 2.0)
29```
31`SimplifiedTwoDesign` takes two differently shaped arrays; slicing a single
32homogeneous tensor is not a valid general construction. `StronglyEntanglingLayers`,
33`BasicEntanglerLayers` and `RandomLayers` each expose their own `shape` convention.
34Random templates have separate structure and parameter seeds. A particle-conserving
35ansatz starting in the vacuum remains there; prepare the required occupation.

Templates with valid shapes and reference states

import pennylane as qml
from pennylane import numpy as np

dev = qml.device("default.qubit", wires=4)
initial_shape, layer_shape = qml.SimplifiedTwoDesign.shape(n_layers=2, n_wires=4)
initial = np.zeros(initial_shape, requires_grad=True)
weights = np.ones(layer_shape, requires_grad=True) * 0.1
@qml.qnode(dev)
def two_design(initial, weights):
    qml.SimplifiedTwoDesign(initial, weights, wires=range(4))
    return qml.expval(qml.Z(0))
assert np.isfinite(two_design(initial, weights))

shape = qml.ParticleConservingU1.shape(n_layers=2, n_wires=4)
particle_weights = np.ones(shape, requires_grad=True) * 0.1
@qml.qnode(dev)
def particle_circuit(weights):
    qml.ParticleConservingU1(weights, wires=range(4), init_state=np.array([1, 1, 0, 0]))
    return qml.expval(qml.qchem.particle_number(4))
assert np.allclose(particle_circuit(particle_weights), 2.0)

SimplifiedTwoDesign takes two differently shaped arrays; slicing a single homogeneous tensor is not a valid general construction. StronglyEntanglingLayers, BasicEntanglerLayers and RandomLayers each expose their own shape convention. Random templates have separate structure and parameter seeds. A particle-conserving ansatz starting in the vacuum remains there; prepare the required occupation.

Raw lines 6 to 36
37## Explicit noise and a physical density matrix
39```python
40import pennylane as qml
41import numpy as np
43dev = qml.device("default.mixed", wires=1)
44def noise_kraus(p):
45 if not 0 <= p <= 1:
46 raise ValueError("p must lie in [0, 1]")
47 return [np.sqrt(1-p)*np.eye(2), np.sqrt(p/3)*qml.matrix(qml.X(0)),
48 np.sqrt(p/3)*qml.matrix(qml.Y(0)), np.sqrt(p/3)*qml.matrix(qml.Z(0))]
50kraus = noise_kraus(0.15)
51assert np.allclose(sum(k.conj().T @ k for k in kraus), np.eye(2))
52@qml.qnode(dev)
53def noisy():
54 qml.Hadamard(0)
55 qml.QubitChannel(kraus, wires=0)
56 qml.AmplitudeDamping(0.1, wires=0)
57 return qml.density_matrix(wires=[0])
58rho = noisy()
59assert np.allclose(rho, rho.conj().T)
60assert np.allclose(np.trace(rho), 1)
61assert np.linalg.eigvalsh(rho).min() >= -1e-10
62```
64`DepolarizingChannel`, `AmplitudeDamping`, `PhaseDamping`, `BitFlip` and `PhaseFlip`
65have specific physical meanings and probability conventions. Match them to the
66experiment. Adding noise once at the circuit end is not noise after every gate.
67Reusable ansatz functions should queue gates, not call nested QNodes. Current noise
68insertion/mitigation transforms live in `qml.noise`, not `qml.transforms`. A noise
69model or mitigation method needs independent calibration and uncertainty checks.

Explicit noise and a physical density matrix

import pennylane as qml
import numpy as np

dev = qml.device("default.mixed", wires=1)
def noise_kraus(p):
    if not 0 <= p <= 1:
        raise ValueError("p must lie in [0, 1]")
    return [np.sqrt(1-p)*np.eye(2), np.sqrt(p/3)*qml.matrix(qml.X(0)),
            np.sqrt(p/3)*qml.matrix(qml.Y(0)), np.sqrt(p/3)*qml.matrix(qml.Z(0))]

kraus = noise_kraus(0.15)
assert np.allclose(sum(k.conj().T @ k for k in kraus), np.eye(2))
@qml.qnode(dev)
def noisy():
    qml.Hadamard(0)
    qml.QubitChannel(kraus, wires=0)
    qml.AmplitudeDamping(0.1, wires=0)
    return qml.density_matrix(wires=[0])
rho = noisy()
assert np.allclose(rho, rho.conj().T)
assert np.allclose(np.trace(rho), 1)
assert np.linalg.eigvalsh(rho).min() >= -1e-10

DepolarizingChannel, AmplitudeDamping, PhaseDamping, BitFlip and PhaseFlip have specific physical meanings and probability conventions. Match them to the experiment. Adding noise once at the circuit end is not noise after every gate. Reusable ansatz functions should queue gates, not call nested QNodes. Current noise insertion/mitigation transforms live in qml.noise, not qml.transforms. A noise model or mitigation method needs independent calibration and uncertainty checks.

Raw lines 37 to 70
71## Pulse evolution: a Hamiltonian must be evolved
73This JAX 0.7.1 example is a **simulated** driven qubit with `hbar=1`, amplitude in
74cycles per time unit and duration in matching time units. The implementation
75converts amplitude to angular frequency with a factor of `2*pi`. It does not
76submit a calibrated hardware pulse.
78```python
79import pennylane as qml
80import jax
81import jax.numpy as jnp
83jax.config.update("jax_enable_x64", True)
84def amplitude(params, time):
85 return params[0] # constant envelope; required callback order is (params, time)
86H_drive = qml.pulse.drive(amplitude, phase=0.0, wires=[0])
87dev = qml.device("default.qubit", wires=1)
88@qml.qnode(dev, interface="jax")
89def pulse_state(params):
90 qml.evolve(H_drive)([params], t=[0.0, 1.0], atol=1e-9, rtol=1e-9)
91 return qml.state()
92psi = pulse_state(jnp.array([0.4]))
93assert jnp.allclose(jnp.abs(psi)**2, jnp.array([jnp.cos(jnp.pi * 0.4)**2, jnp.sin(jnp.pi * 0.4)**2]), atol=1e-7)
94target = jnp.array([0.0, 1.0], dtype=complex)
95def infidelity(params):
96 return 1 - jnp.abs(jnp.vdot(target, pulse_state(params)))**2
97grad = jax.grad(infidelity)(jnp.array([0.4]))
98assert jnp.isfinite(grad).all()
99```
101`qml.pulse.drive(amplitude, phase, wires)` returns a `ParametrizedHamiltonian`.
102Its phase-zero one-wire matrix is `pi * amplitude * X`; verify this convention
103before comparing a pulse area with a rotation angle. It has neither `freq` nor
104`duration` arguments and does not by itself apply a gate.
105Carrier-aware `qml.pulse.transmon_drive` is a different Hamiltonian constructor.
106Use `qml.evolve` for duration/time integration, preserve callback parameter ordering,
107and verify solver tolerance convergence. State fidelity needs a returned state
108vector; taking an inner product of a scalar expectation is not fidelity.

Pulse evolution: a Hamiltonian must be evolved

This JAX 0.7.1 example is a simulated driven qubit with hbar=1, amplitude in cycles per time unit and duration in matching time units. The implementation converts amplitude to angular frequency with a factor of 2*pi. It does not submit a calibrated hardware pulse.

import pennylane as qml
import jax
import jax.numpy as jnp

jax.config.update("jax_enable_x64", True)
def amplitude(params, time):
    return params[0]  # constant envelope; required callback order is (params, time)
H_drive = qml.pulse.drive(amplitude, phase=0.0, wires=[0])
dev = qml.device("default.qubit", wires=1)
@qml.qnode(dev, interface="jax")
def pulse_state(params):
    qml.evolve(H_drive)([params], t=[0.0, 1.0], atol=1e-9, rtol=1e-9)
    return qml.state()
psi = pulse_state(jnp.array([0.4]))
assert jnp.allclose(jnp.abs(psi)**2, jnp.array([jnp.cos(jnp.pi * 0.4)**2, jnp.sin(jnp.pi * 0.4)**2]), atol=1e-7)
target = jnp.array([0.0, 1.0], dtype=complex)
def infidelity(params):
    return 1 - jnp.abs(jnp.vdot(target, pulse_state(params)))**2
grad = jax.grad(infidelity)(jnp.array([0.4]))
assert jnp.isfinite(grad).all()

qml.pulse.drive(amplitude, phase, wires) returns a ParametrizedHamiltonian. Its phase-zero one-wire matrix is pi * amplitude * X; verify this convention before comparing a pulse area with a rotation angle. It has neither freq nor duration arguments and does not by itself apply a gate. Carrier-aware qml.pulse.transmon_drive is a different Hamiltonian constructor. Use qml.evolve for duration/time integration, preserve callback parameter ordering, and verify solver tolerance convergence. State fidelity needs a returned state vector; taking an inner product of a scalar expectation is not fidelity.

Raw lines 71 to 109
110## Catalyst compilation (illustrative, not executed)
112Catalyst 0.15.0 requires PennyLane/Lightning >=0.45 and exactly JAX/JAXlib 0.7.1.
113Install it in a supported platform environment and validate against eager results.
114Use compiler-native differentiation/control flow:
116```python
117# Illustrative: requires the optional native Catalyst compiler.
118import pennylane as qml
119from catalyst import qjit, grad, for_loop
121dev = qml.device("lightning.qubit", wires=1)
122@qjit(capture=False)
123@qml.qnode(dev)
124def compiled(x):
125 @for_loop(0, 3, 1)
126 def layer(i):
127 qml.RY(x, wires=0)
128 layer()
129 return qml.expval(qml.Z(0))
131compiled_gradient = qjit(grad(compiled), capture=False)
132# After installation, compare compiled(0.2) to cos(0.6), and its gradient to -3*sin(0.6).
133```
135Separate first-call compilation from warmed execution when timing. The example uses `capture=False`; with program capture enabled, use the
136PennyLane control-flow forms supported by that mode instead. Explicit loop
137constructs or supported AutoGraph are needed for dynamic classical control. Do not
138wrap Autograd `qml.grad` around an already compiled function and assume portability.

Catalyst compilation (illustrative, not executed)

Catalyst 0.15.0 requires PennyLane/Lightning >=0.45 and exactly JAX/JAXlib 0.7.1. Install it in a supported platform environment and validate against eager results. Use compiler-native differentiation/control flow:

# Illustrative: requires the optional native Catalyst compiler.
import pennylane as qml
from catalyst import qjit, grad, for_loop

dev = qml.device("lightning.qubit", wires=1)
@qjit(capture=False)
@qml.qnode(dev)
def compiled(x):
    @for_loop(0, 3, 1)
    def layer(i):
        qml.RY(x, wires=0)
    layer()
    return qml.expval(qml.Z(0))

compiled_gradient = qjit(grad(compiled), capture=False)
# After installation, compare compiled(0.2) to cos(0.6), and its gradient to -3*sin(0.6).

Separate first-call compilation from warmed execution when timing. The example uses capture=False; with program capture enabled, use the PennyLane control-flow forms supported by that mode instead. Explicit loop constructs or supported AutoGraph are needed for dynamic classical control. Do not wrap Autograd qml.grad around an already compiled function and assume portability.

Raw lines 110 to 139
140## Three-qubit bit-flip code with two syndrome wires
142```python
143import pennylane as qml
144import numpy as np
146dev = qml.device("default.qubit", wires=5)
147@qml.qnode(dev)
148def recover(error_wire):
149 qml.RY(0.7, wires=0)
150 qml.CNOT([0, 1])
151 qml.CNOT([0, 2])
152 if error_wire is not None: # classical circuit-construction choice
153 qml.X(error_wire)
154 qml.CNOT([0, 3]); qml.CNOT([1, 3])
155 s1 = qml.measure(3)
156 qml.CNOT([1, 4]); qml.CNOT([2, 4])
157 s2 = qml.measure(4)
158 qml.cond(s1 & ~s2, qml.X)(wires=0)
159 qml.cond(s1 & s2, qml.X)(wires=1)
160 qml.cond(~s1 & s2, qml.X)(wires=2)
161 qml.CNOT([0, 2]); qml.CNOT([0, 1])
162 return qml.density_matrix(wires=[0])
163reference = recover(None)
164for wire in range(3):
165 assert np.allclose(recover(wire), reference)
166```
168This corrects a single bit flip under ideal gates/measurements, not phase errors,
169multiple errors or a fault-tolerant hardware implementation. Allocate all five
170wires. Validate the recovered density matrix, not only one expectation value.

Three-qubit bit-flip code with two syndrome wires

import pennylane as qml
import numpy as np

dev = qml.device("default.qubit", wires=5)
@qml.qnode(dev)
def recover(error_wire):
    qml.RY(0.7, wires=0)
    qml.CNOT([0, 1])
    qml.CNOT([0, 2])
    if error_wire is not None:  # classical circuit-construction choice
        qml.X(error_wire)
    qml.CNOT([0, 3]); qml.CNOT([1, 3])
    s1 = qml.measure(3)
    qml.CNOT([1, 4]); qml.CNOT([2, 4])
    s2 = qml.measure(4)
    qml.cond(s1 & ~s2, qml.X)(wires=0)
    qml.cond(s1 & s2, qml.X)(wires=1)
    qml.cond(~s1 & s2, qml.X)(wires=2)
    qml.CNOT([0, 2]); qml.CNOT([0, 1])
    return qml.density_matrix(wires=[0])
reference = recover(None)
for wire in range(3):
    assert np.allclose(recover(wire), reference)

This corrects a single bit flip under ideal gates/measurements, not phase errors, multiple errors or a fault-tolerant hardware implementation. Allocate all five wires. Validate the recovered density matrix, not only one expectation value.

Raw lines 140 to 171
172## Resource and performance accounting
174Use current `qml.specs(...).resources` at a declared transform level. Simulation
175memory formulas count the state only; gradients and intermediate buffers add cost.
176Do not turn a guessed per-gate constant into a hardware runtime prediction.
177Measure representative inputs, compilation overhead, shot count and device queue
178latency separately. Record circuit splitting, gradient evaluations and shot budgets.

Resource and performance accounting

Use current qml.specs(...).resources at a declared transform level. Simulation memory formulas count the state only; gradients and intermediate buffers add cost. Do not turn a guessed per-gate constant into a hardware runtime prediction. Measure representative inputs, compilation overhead, shot count and device queue latency separately. Record circuit splitting, gradient evaluations and shot budgets.

Raw lines 172 to 179
180## Sources
182- [Templates](https://docs.pennylane.ai/en/stable/code/qml_templates.html)
183- [pulse.drive and frequency conversion source](https://github.com/PennyLaneAI/pennylane/blob/v0.45.1/pennylane/pulse/hardware_hamiltonian.py)
184- [ParametrizedEvolution](https://docs.pennylane.ai/en/stable/code/api/pennylane.pulse.ParametrizedEvolution.html)
185- [Noise module](https://docs.pennylane.ai/en/stable/code/qml_noise.html)
186- [Catalyst gradients](https://docs.pennylane.ai/projects/catalyst/en/stable/code/api/catalyst.grad.html)
187- [Catalyst control-flow source](https://github.com/PennyLaneAI/catalyst/blob/v0.15.0/frontend/catalyst/api_extensions/control_flow.py)