PennyLaneAI / pennylane

PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
https://pennylane.ai
Apache License 2.0
2.27k stars 585 forks source link

[BUG] WireError when computing quantum fisher info of mixed state #6059

Open joeybarreto opened 1 month ago

joeybarreto commented 1 month ago

Expected behavior

Execution without failure (i.e. the QFIM is returned)

Actual behavior

WireError: Did not find some of the wires <Wires = [2]> on device with wires <Wires = [0, 1]>.

Additional information

Removing the second layer of $RX$ gates removes the error, so something is going wrong when noise channels are applied mid-circuit as opposed to at the end of the circuit. This is a minimum working example of the error, after some troubleshooting (I would imagine even with 1 qubit it would still error).

Source code

import pennylane as qml
from pennylane import numpy as np

def circuit(params):
    qml.RX(params[0], wires=0)
    qml.RX(params[1], wires=1)

    qml.AmplitudeDamping(0.1, wires=0)
    qml.AmplitudeDamping(0.1, wires=1)

    qml.RX(params[0], wires=0)
    qml.RX(params[1], wires=1)        

    return qml.expval(qml.PauliZ(0))

dev = qml.device("default.mixed", wires=2)
parameters = 2*np.pi*np.random.random(size=2, requires_grad=True)
qml.qinfo.transforms.quantum_fisher(qml.QNode(circuit, dev))(parameters)

Tracebacks

---------------------------------------------------------------------------
WireError                                 Traceback (most recent call last)
File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/_device.py:384, in Device.map_wires(self, wires)
    383 try:
--> 384     mapped_wires = wires.map(self.wire_map)
    385 except WireError as e:

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/wires.py:293, in Wires.map(self, wire_map)
    292     if w not in wire_map:
--> 293         raise WireError(f"No mapping for wire label {w} specified in wire map {wire_map}.")
    295 new_wires = [wire_map[w] for w in self]

WireError: No mapping for wire label 2 specified in wire map OrderedDict({0: 0, 1: 1}).

The above exception was the direct cause of the following exception:

WireError                                 Traceback (most recent call last)
Cell In[62], line 15
     13 dev = qml.device("default.mixed", wires=2)
     14 parameters = 2*np.pi*np.random.random(size=2, requires_grad=True)
---> 15 qml.qinfo.transforms.quantum_fisher(qml.QNode(circuit, dev))(parameters)

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/workflow/qnode.py:1098, in QNode.__call__(self, *args, **kwargs)
   1095 self._update_gradient_fn(shots=override_shots, tape=self._tape)
   1097 try:
-> 1098     res = self._execution_component(args, kwargs, override_shots=override_shots)
   1099 finally:
   1100     if old_interface == "auto":

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/workflow/qnode.py:1052, in QNode._execution_component(self, args, kwargs, override_shots)
   1049 full_transform_program.prune_dynamic_transform()
   1051 # pylint: disable=unexpected-keyword-arg
-> 1052 res = qml.execute(
   1053     (self._tape,),
   1054     device=self.device,
   1055     gradient_fn=self.gradient_fn,
   1056     interface=self.interface,
   1057     transform_program=full_transform_program,
   1058     config=config,
   1059     gradient_kwargs=self.gradient_kwargs,
   1060     override_shots=override_shots,
   1061     **self.execute_kwargs,
   1062 )
   1063 res = res[0]
   1065 # convert result to the interface in case the qfunc has no parameters

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/workflow/execution.py:612, in execute(tapes, device, gradient_fn, interface, transform_program, config, grad_on_execution, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform, device_vjp)
    609         return program_post_processing(program_pre_processing(results))
    611 if transform_program.is_informative:
--> 612     return post_processing(tapes)
    614 # Exiting early if we do not need to deal with an interface boundary
    615 if no_interface_boundary_required:

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/workflow/execution.py:609, in execute.<locals>.post_processing(results)
    608 def post_processing(results):
--> 609     return program_post_processing(program_pre_processing(results))

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/transforms/core/transform_program.py:88, in _apply_postprocessing_stack(results, postprocessing_stack)
     65 """Applies the postprocessing and cotransform postprocessing functions in a Last-In-First-Out LIFO manner.
     66 
     67 Args:
   (...)
     85 
     86 """
     87 for postprocessing in reversed(postprocessing_stack):
---> 88     results = postprocessing(results)
     89 return results

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/transforms/core/transform_program.py:58, in _batch_postprocessing(results, individual_fns, slices)
     32 def _batch_postprocessing(
     33     results: ResultBatch, individual_fns: List[PostProcessingFn], slices: List[slice]
     34 ) -> ResultBatch:
     35     """Broadcast individual post processing functions onto their respective tapes.
     36 
     37     Args:
   (...)
     56 
     57     """
---> 58     return tuple(fn(results[sl]) for fn, sl in zip(individual_fns, slices))

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/transforms/core/transform_program.py:58, in <genexpr>(.0)
     32 def _batch_postprocessing(
     33     results: ResultBatch, individual_fns: List[PostProcessingFn], slices: List[slice]
     34 ) -> ResultBatch:
     35     """Broadcast individual post processing functions onto their respective tapes.
     36 
     37     Args:
   (...)
     56 
     57     """
---> 58     return tuple(fn(results[sl]) for fn, sl in zip(individual_fns, slices))

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/qinfo/transforms.py:807, in quantum_fisher.<locals>.processing_fn_multiply(res)
    806 def processing_fn_multiply(res):
--> 807     res = qml.execute(res, device=device)
    808     return 4 * processing_fn(res)

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/workflow/execution.py:616, in execute(tapes, device, gradient_fn, interface, transform_program, config, grad_on_execution, gradient_kwargs, cache, cachesize, max_diff, override_shots, expand_fn, max_expansion, device_batch_transform, device_vjp)
    614 # Exiting early if we do not need to deal with an interface boundary
    615 if no_interface_boundary_required:
--> 616     results = inner_execute(tapes)
    617     return post_processing(results)
    619 _grad_on_execution = False

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/workflow/execution.py:297, in _make_inner_execute.<locals>.inner_execute(tapes, **_)
    294 transformed_tapes, transform_post_processing = transform_program(tapes)
    296 if transformed_tapes:
--> 297     results = device_execution(transformed_tapes)
    298 else:
    299     results = ()

File ~/miniconda3/envs/lanl/lib/python3.12/contextlib.py:81, in ContextDecorator.__call__.<locals>.inner(*args, **kwds)
     78 @wraps(func)
     79 def inner(*args, **kwds):
     80     with self._recreate_cm():
---> 81         return func(*args, **kwds)

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/_qubit_device.py:490, in QubitDevice.batch_execute(self, circuits)
    485 for circuit in circuits:
    486     # we need to reset the device here, else it will
    487     # not start the next computation in the zero state
    488     self.reset()
--> 490     res = self.execute(circuit)
    491     results.append(res)
    493 if self.tracker.active:

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/devices/default_mixed.py:767, in DefaultMixed.execute(self, circuit, **kwargs)
    765         wires_list.append(m.wires)
    766     self.measured_wires = qml.wires.Wires.all_wires(wires_list)
--> 767 return super().execute(circuit, **kwargs)

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/_qubit_device.py:281, in QubitDevice.execute(self, circuit, **kwargs)
    279     kwargs["mid_measurements"] = {}
    280 # apply all circuit operations
--> 281 self.apply(
    282     circuit.operations,
    283     rotations=self._get_diagonalizing_gates(circuit),
    284     **kwargs,
    285 )
    286 if has_mcm:
    287     mid_measurements = kwargs["mid_measurements"]

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/devices/default_mixed.py:781, in DefaultMixed.apply(self, operations, rotations, **kwargs)
    775         raise DeviceError(
    776             f"Operation {operation.name} cannot be used after other Operations have already been applied "
    777             f"on a {self.short_name} device."
    778         )
    780 for operation in operations:
--> 781     self._apply_operation(operation)
    783 # store the pre-rotated state
    784 self._pre_rotated_state = self._state

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/devices/default_mixed.py:711, in DefaultMixed._apply_operation(self, operation)
    709     self._apply_channel_tensordot(matrices, wires)
    710 else:
--> 711     self._apply_channel(matrices, wires)

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/devices/default_mixed.py:329, in DefaultMixed._apply_channel(self, kraus, wires)
    321 def _apply_channel(self, kraus, wires):
    322     r"""Apply a quantum channel specified by a list of Kraus operators to subsystems of the
    323     quantum state. For a unitary gate, there is a single Kraus operator.
    324 
   (...)
    327         wires (Wires): target wires
    328     """
--> 329     channel_wires = self.map_wires(wires)
    330     rho_dim = 2 * self.num_wires
    331     num_ch_wires = len(channel_wires)

File ~/miniconda3/envs/lanl/lib/python3.12/site-packages/pennylane/_device.py:386, in Device.map_wires(self, wires)
    384     mapped_wires = wires.map(self.wire_map)
    385 except WireError as e:
--> 386     raise WireError(
    387         f"Did not find some of the wires {wires} on device with wires {self.wires}."
    388     ) from e
    390 return mapped_wires

WireError: Did not find some of the wires <Wires = [2]> on device with wires <Wires = [0, 1]>.

System information

Name: PennyLane
Version: 0.36.0
Summary: PennyLane is a cross-platform Python library for quantum computing, quantum machine learning, and quantum chemistry. Train a quantum computer the same way as a neural network.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Users/joey/miniconda3/envs/lanl/lib/python3.12/site-packages
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing-extensions
Required-by: PennyLane_Lightning

Platform info:           macOS-14.4.1-arm64-arm-64bit
Python version:          3.12.3
Numpy version:           1.26.4
Scipy version:           1.13.1
Installed devices:
- default.clifford (PennyLane-0.36.0)
- default.gaussian (PennyLane-0.36.0)
- default.mixed (PennyLane-0.36.0)
- default.qubit (PennyLane-0.36.0)
- default.qubit.autograd (PennyLane-0.36.0)
- default.qubit.jax (PennyLane-0.36.0)
- default.qubit.legacy (PennyLane-0.36.0)
- default.qubit.tf (PennyLane-0.36.0)
- default.qubit.torch (PennyLane-0.36.0)
- default.qutrit (PennyLane-0.36.0)
- default.qutrit.mixed (PennyLane-0.36.0)
- null.qubit (PennyLane-0.36.0)
- lightning.qubit (PennyLane_Lightning-0.36.0)

Existing GitHub issues

CatalinaAlbornoz commented 1 month ago

Hi @joeybarreto , thank you for reporting this! We're looking into it and will get back to you soon.

dwierichs commented 1 month ago

Hi @joeybarreto,

tl;dr: Simplest fix: Add a wire to your device (see below for working code based on your MWE)

I'll outline the function structure here to make the bug understandable:

  1. quantum_fisher uses the functions adjoint_metric_tensor and metric_tensor under the hood. However, adjoint_metric_tensor currently is only supported on some devices, which unfortunately does not include DefaultMixed.
  2. The other function, metric_tensor uses two techniques to obtain the QFI, one for the block diagonal, where the blocks of parameters are defined by commuting sets of gates, and one for the block off-diagonal.
  3. The block diagonal technique only requires running a part of the total circuit together with measuring come covariances (as detailed in the paper Quantum Natural Gradient). The block off-diagonal requires Hadamard tests, which in turn require an auxiliary wire.

Now the structure for your code is as follows: adjoint_metric_tensor does not work because of the device choice. For a single layer of parametrized/differentiable operations, only the covariance-based method needs to be used (the QFI only has one block, so it is entirely block diagonal). For a second layer, the Hadamard test is needed, starting to cause the bug you report. Finally, the bug itself is caused by the device not having an auxiliary wire available, which is needed for Hadamard tests. You have a number of options here:

  1. Add a wire to your device. This is clearly the simplest fix, but it -- well -- requires another wire.
  2. Only use the block diagonal approximation of the QFI, by passing approx="block_diag" to quantum_fisher. This will not require the additional wire, but it is an approximation only (note that it also is much cheaper in terms of circuit executions).
  3. Change device. This is a non-option because you want to simulate noise, and I'm not aware of a mixed-state simulator that supports adjoint_metric_tensor.

I hope this helps! Let me know if you still encounter problems!

And here is the working example based on your MWE:

import pennylane as qml
from pennylane import numpy as np

def circuit(params):
    qml.RX(params[0], wires=0)
    qml.RX(params[1], wires=1)

    qml.AmplitudeDamping(0.1, wires=0)
    qml.AmplitudeDamping(0.1, wires=1)

    qml.RX(params[0], wires=0)
    qml.RX(params[1], wires=1)        

    return qml.expval(qml.PauliZ(0))

dev = qml.device("default.mixed", wires=3) # <<< Increased wire count to accomodate aux wire
parameters = 2*np.pi*np.random.random(size=2, requires_grad=True)
qml.qinfo.transforms.quantum_fisher(qml.QNode(circuit, dev))(parameters)

Alternatively, using the approximate QFI:

dev = qml.device("default.mixed", wires=2) # <<< No aux wire this time
parameters = 2*np.pi*np.random.random(size=2, requires_grad=True)
qml.qinfo.transforms.quantum_fisher(qml.QNode(circuit, dev), approx="block-diag")(parameters) # <<< Pick approximation