Closed glassnotes closed 3 months ago
Side note: it is possible to apply MCMs within transforms, and perform operations controlled on their outputs. However the specific goal here is to be able to access and return samples of their outcomes.
For instance, achieving something like this:
@qml.dynamic_one_shot
@qml.qnode(dev)
def apply_mcm():
qml.Hadamard(wires=0)
mcm = qml.measure(wires=0, reset=True)
return qml.sample(mcm)
with a transform structured like so:
@qml.transform
def add_mcm(tape):
new_operations = []
measurements = []
with qml.QueuingManager.stop_recording():
for op in tape.operations:
new_operations.append(op)
mcm = qml.measure(wires=0, reset=True)
new_operations.append(mcm)
new_tape = type(tape)(new_operations, [qml.sample(mcm)])
def null_postprocessing(results,):
return results[0]
return ([new_tape], null_postprocessing)
Hi @glassnotes , this is a consequence of the way we have defined qml.measure
. It is a bit unintuitive in the sense that it doesn't actually return an operator, but rather an abstract representation of the measured value (a MeasurementValue
object), and the mid-circuit measurement operator gets silently queued. MeasurementValue
does hold a reference to the original mid-circuit measurement, so you can fix the transform this way:
@qml.transform
def add_mcm(tape):
new_operations = []
measurements = []
with qml.QueuingManager.stop_recording():
for op in tape.operations:
new_operations.append(op)
# Applies a mid-circuit measurement and adds to tape operations
mcm = qml.measure(wires=0, reset=True)
new_operations.append(mcm.measurements[0])
new_tape = type(tape)(new_operations, tape.measurements)
def null_postprocessing(results,):
return results[0]
return ([new_tape], null_postprocessing)
Feel free to reach out if this resolves/does not resolve the issue and I can investigate further 😄
Want to quickly add a follow up. In the above example, you can use mcm
as the conditional argument for qml.cond
, or for collecting mid-circuit measurement statistics (qml.sample(mcm)
, etc.).
Thanks, @mudit2812 !
MeasurementValue does hold a reference to the original mid-circuit measurement, so you can fix the transform this way:
Yes, this fixes the output. The measurement doesn't show up in the list of operations in the tape (which would be helpful for debugging and determining if they are applied correctly).
Want to quickly add a follow up. In the above example, you can use mcm as the conditional argument for qml.cond, or for collecting mid-circuit measurement statistics (qml.sample(mcm), etc.).
I'm not sure I follow here. Even with the updated transform, using
new_tape = type(tape)(new_operations, [qml.sample(mcm.measurements)])
or even
new_tape = type(tape)(new_operations, [qml.sample(mcm.measurements[0])])
does not work for actually returning the sampled values. Another thing I tried was to return the samples as an the output of the post-processing function (but this just returns the measurement object itself, and can't be executed).
@glassnotes glad to hear that this is working.
The measurement doesn't show up in the list of operations in the tape (which would be helpful for debugging and determining if they are applied correctly).
This should not be the case. Working example at the bottom.
new_tape = type(tape)(new_operations, [qml.sample(mcm.measurements)])
Sorry if it wasn't clear, for collecting the mcm
samples (or any other measurement), you should use qml.sample(mcm)
rather than qml.sample(mcm.measurements)
or qml.sample(mcm.measurements[0])
. Working example at the bottom
@qml.transform
def add_mcm(tape):
new_operations = []
new_measurements = []
with qml.QueuingManager.stop_recording():
for op in tape.operations:
new_operations.append(op)
# Applies a mid-circuit measurement and adds to tape operations
mcm = qml.measure(wires=0, reset=True)
new_operations.append(mcm.measurements[0])
# Collects samples on the mid-circuit measurement
new_measurements.append(qml.sample(mcm))
new_tape = type(tape)(new_operations, new_measurements)
def null_postprocessing(
results,
):
return results[0]
return ([new_tape], null_postprocessing)
>>> tape = qml.tape.QuantumScript([qml.RX(1.0, 0), qml.RX(2.0, 0), qml.RX(3.0, 0)], [qml.expval(qml.Z(0))])
>>> [new_tape], _ = add_mcm(tape)
>>> print(new_tape.operations)
[RX(1.0, wires=[0]), measure(wires=[0]), RX(2.0, wires=[0]), measure(wires=[0]), RX(3.0, wires=[0]), measure(wires=[0])]
>>> print(new_tape.measurements)
[sample(MeasurementValue(wires=[0])), sample(MeasurementValue(wires=[0])), sample(MeasurementValue(wires=[0]))]
@mudit2812 great - that solves it. But I did have to do something very specific. Your example with a tape gave me the idea. It seems that it needs to be applied as a quantum function transform rather than a QNode transform. So the following will work:
@qml.dynamic_one_shot
@qml.qnode(dev)
@add_mcm
def apply_mcm():
qml.Hadamard(wires=0)
return qml.probs(wires=0)
>>> apply_mcm()
tensor(1, requires_grad=True)
>>> qml.draw(apply_mcm)()
0: ──H──┤↗│ │0⟩─┤
╚═══════╡ Sample[MCM]
But applying it as a QNode transform, i.e.,
@qml.dynamic_one_shot
@add_mcm
@qml.qnode(dev)
def apply_mcm():
qml.Hadamard(wires=0)
return qml.probs(wires=0)
throws a DeviceError
:
---------------------------------------------------------------------------
DeviceError Traceback (most recent call last)
Cell In[13], line 1
----> 1 apply_mcm()
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/qnode.py:1164](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/qnode.py#line=1163), in QNode.__call__(self, *args, **kwargs)
1162 if qml.capture.enabled():
1163 return qml.capture.qnode_call(self, *args, **kwargs)
-> 1164 return self._impl_call(*args, **kwargs)
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/qnode.py:1150](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/qnode.py#line=1149), in QNode._impl_call(self, *args, **kwargs)
1147 self._update_gradient_fn(shots=override_shots, tape=self._tape)
1149 try:
-> 1150 res = self._execution_component(args, kwargs, override_shots=override_shots)
1151 finally:
1152 if old_interface == "auto":
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/qnode.py:1103](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/qnode.py#line=1102), in QNode._execution_component(self, args, kwargs, override_shots)
1100 _prune_dynamic_transform(full_transform_program, inner_transform_program)
1102 # pylint: disable=unexpected-keyword-arg
-> 1103 res = qml.execute(
1104 (self._tape,),
1105 device=self.device,
1106 gradient_fn=self.gradient_fn,
1107 interface=self.interface,
1108 transform_program=full_transform_program,
1109 inner_transform=inner_transform_program,
1110 config=config,
1111 gradient_kwargs=self.gradient_kwargs,
1112 override_shots=override_shots,
1113 **self.execute_kwargs,
1114 )
1115 res = res[0]
1117 # convert result to the interface in case the qfunc has no parameters
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/execution.py:835](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/execution.py#line=834), in execute(tapes, device, gradient_fn, interface, transform_program, inner_transform, config, grad_on_execution, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform, device_vjp, mcm_config)
827 ml_boundary_execute = _get_ml_boundary_execute(
828 interface,
829 _grad_on_execution,
830 config.use_device_jacobian_product,
831 differentiable=max_diff > 1,
832 )
834 if interface in jpc_interfaces:
--> 835 results = ml_boundary_execute(tapes, execute_fn, jpc, device=device)
836 else:
837 results = ml_boundary_execute(
838 tapes, device, execute_fn, gradient_fn, gradient_kwargs, _n=1, max_diff=max_diff
839 )
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/interfaces/autograd.py:147](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/interfaces/autograd.py#line=146), in autograd_execute(tapes, execute_fn, jpc, device)
142 # pylint misidentifies autograd.builtins as a dict
143 # pylint: disable=no-member
144 parameters = autograd.builtins.tuple(
145 [autograd.builtins.list(t.get_parameters()) for t in tapes]
146 )
--> 147 return _execute(parameters, tuple(tapes), execute_fn, jpc)
File [~/.conda/envs/qecc/lib/python3.12/site-packages/autograd/tracer.py:48](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/autograd/tracer.py#line=47), in primitive.<locals>.f_wrapped(*args, **kwargs)
46 return new_box(ans, trace, node)
47 else:
---> 48 return f_raw(*args, **kwargs)
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/interfaces/autograd.py:168](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/interfaces/autograd.py#line=167), in _execute(parameters, tapes, execute_fn, jpc)
150 @autograd.extend.primitive
151 def _execute(
152 parameters,
(...)
155 jpc,
156 ): # pylint: disable=unused-argument
157 """Autodifferentiable wrapper around a way of executing tapes.
158
159 Args:
(...)
166
167 """
--> 168 return execute_fn(tapes)
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/execution.py:309](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/workflow/execution.py#line=308), in _make_inner_execute.<locals>.inner_execute(tapes, **_)
306 if cache is not None:
307 transform_program.add_transform(_cache_transform, cache=cache)
--> 309 transformed_tapes, transform_post_processing = transform_program(tapes)
311 # TODO: Apply expand_fn() as transform.
312 if expand_fn:
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/transforms/core/transform_program.py:515](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/transforms/core/transform_program.py#line=514), in TransformProgram.__call__(self, tapes)
513 if self._argnums is not None and self._argnums[i] is not None:
514 tape.trainable_params = self._argnums[i][j]
--> 515 new_tapes, fn = transform(tape, *targs, **tkwargs)
516 execution_tapes.extend(new_tapes)
518 fns.append(fn)
File [~/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/devices/preprocess.py:490](http://localhost:8888/home/olivia/.conda/envs/qecc/lib/python3.12/site-packages/pennylane/devices/preprocess.py#line=489), in validate_measurements(tape, analytic_measurements, sample_measurements, name)
488 for m in chain(snapshot_measurements, tape.measurements):
489 if not analytic_measurements(m):
--> 490 raise DeviceError(
491 f"Measurement {m} not accepted for analytic simulation on {name}."
492 )
494 return (tape,), null_postprocessing
DeviceError: Measurement sample(MeasurementValue(wires=[1])) not accepted for analytic simulation on default.qubit.
@glassnotes I think that is because you're executing a tape with qml.sample
and the transformed tape does not copy shots from the original tape. The new tape should be created as new_tape = type(tape)(new_operations, new_measurements, trainable_params=tape.trainable_params, shots=tape.shots)
You're right! Thanks so much for your help. Feel free to close this issue (or at the very least, the bug tag can be removed). I am not sure how common a use-case this is, but could be something for the docs?
@glassnotes great! Yes, we've discussed potential strategies to make this more intuitive. For now I'm closing this issue 😄
Expected behavior
When a mid-circuit measurement is applied in a transform, the measurement is correctly queued to the tape.
Actual behavior
An error is thrown, due to measurement processes not having the same properties as regular operations.
Additional information
This is both a bug report and a feature request. There are specific workflows where this, as well as the ability to sample and return the values of mid-circuit measurements, would be really useful.
Source code
Tracebacks
System information
Existing GitHub issues