diff --git a/qadence/backends/pyqtorch/backend.py b/qadence/backends/pyqtorch/backend.py index 31aeaa87..529c2c6a 100644 --- a/qadence/backends/pyqtorch/backend.py +++ b/qadence/backends/pyqtorch/backend.py @@ -28,6 +28,7 @@ flatten, invert_endianness, scale_primitive_blocks_only, + set_noise, transpile, ) from qadence.types import BackendName, Endianness, Engine @@ -38,6 +39,46 @@ logger = getLogger(__name__) +def set_noise_abstract_to_native(circuit: ConvertedCircuit, config: Configuration) -> None: + """Set noise in native blocks from the abstract ones with noise. + + Args: + circuit (ConvertedCircuit): Input converted circuit. + """ + ops = convert_block(circuit.abstract.block, n_qubits=circuit.native.n_qubits, config=config) + circuit.native = pyq.QuantumCircuit(circuit.native.n_qubits, ops, circuit.native.readout_noise) + + +def set_readout_noise(circuit: ConvertedCircuit, noise: NoiseHandler) -> None: + """Set readout noise in place in native. + + Args: + circuit (ConvertedCircuit): Input converted circuit. + noise (NoiseHandler | None): Noise. + """ + readout = convert_readout_noise(circuit.abstract.n_qubits, noise) + if readout: + circuit.native.readout_noise = readout + + +def set_block_and_readout_noises( + circuit: ConvertedCircuit, noise: NoiseHandler | None, config: Configuration +) -> None: + """Add noise on blocks and readout on circuit. + + We first start by adding noise to the abstract blocks. Then we do a conversion to their + native representation. Finally, we add readout. + + Args: + circuit (ConvertedCircuit): Input circuit. + noise (NoiseHandler | None): Noise to add. + """ + if noise: + set_noise(circuit, noise) + set_noise_abstract_to_native(circuit, config) + set_readout_noise(circuit, noise) + + @dataclass(frozen=True, eq=True) class Backend(BackendInterface): """PyQTorch backend.""" @@ -55,6 +96,17 @@ class Backend(BackendInterface): logger.debug("Initialised") def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit: + """Return the converted circuit. + + Note that to get a representation with noise, noise + should be passed within the config. + + Args: + circuit (QuantumCircuit): Original circuit + + Returns: + ConvertedCircuit: ConvertedCircuit instance for backend. + """ passes = self.config.transpilation_passes if passes is None: passes = default_passes(self.config) @@ -62,6 +114,9 @@ def circuit(self, circuit: QuantumCircuit) -> ConvertedCircuit: original_circ = circuit if len(passes) > 0: circuit = transpile(*passes)(circuit) + # Setting noise in the circuit. + if self.config.noise: + set_noise(circuit, self.config.noise) ops = convert_block(circuit.block, n_qubits=circuit.n_qubits, config=self.config) readout_noise = ( @@ -124,9 +179,7 @@ def _batched_expectation( noise: NoiseHandler | None = None, endianness: Endianness = Endianness.BIG, ) -> Tensor: - if noise and circuit.native.readout_noise is None: - readout = convert_readout_noise(circuit.abstract.n_qubits, noise) - circuit.native.readout_noise = readout + set_block_and_readout_noises(circuit, noise, self.config) state = self.run( circuit, param_values=param_values, @@ -164,9 +217,7 @@ def _looped_expectation( "Define your initial state with `batch_size=1`" ) - if noise and circuit.native.readout_noise is None: - readout = convert_readout_noise(circuit.abstract.n_qubits, noise) - circuit.native.readout_noise = readout + set_block_and_readout_noises(circuit, noise, self.config) list_expvals = [] observables = observable if isinstance(observable, list) else [observable] @@ -222,9 +273,7 @@ def sample( elif state is not None and pyqify_state: n_qubits = circuit.abstract.n_qubits state = pyqify(state, n_qubits) if pyqify_state else state - if noise and circuit.native.readout_noise is None: - readout = convert_readout_noise(circuit.abstract.n_qubits, noise) - circuit.native.readout_noise = readout + set_block_and_readout_noises(circuit, noise, self.config) samples: list[Counter] = circuit.native.sample( state=state, values=param_values, n_shots=n_shots ) diff --git a/qadence/backends/pyqtorch/convert_ops.py b/qadence/backends/pyqtorch/convert_ops.py index 362cb564..099da813 100644 --- a/qadence/backends/pyqtorch/convert_ops.py +++ b/qadence/backends/pyqtorch/convert_ops.py @@ -175,8 +175,23 @@ def fn(x: str | ConcretizedCallable, y: str | ConcretizedCallable) -> Callable: def convert_block( - block: AbstractBlock, n_qubits: int = None, config: Configuration = None + block: AbstractBlock, + n_qubits: int = None, + config: Configuration = None, ) -> Sequence[Module | Tensor | str | sympy.Expr]: + """Convert block to native Pyqtorch representation. + + Args: + block (AbstractBlock): Block to convert. + n_qubits (int, optional): Number of qubits. Defaults to None. + config (Configuration, optional): Backend configuration instance. Defaults to None. + + Raises: + NotImplementedError: For non supported blocks. + + Returns: + Sequence[Module | Tensor | str | sympy.Expr]: List of native operations. + """ if isinstance(block, (Tensor, str, sympy.Expr)): # case for hamevo generators if isinstance(block, Tensor): block = block.permute(1, 2, 0) # put batch size in the back diff --git a/qadence/noise/protocols.py b/qadence/noise/protocols.py index 99fc1154..3c68b358 100644 --- a/qadence/noise/protocols.py +++ b/qadence/noise/protocols.py @@ -148,16 +148,20 @@ def _from_dict(cls, d: dict | None) -> NoiseHandler | None: def list(cls) -> list: return list(filter(lambda el: not el.startswith("__"), dir(cls))) - def filter(self, protocol: NoiseEnum | str) -> NoiseHandler | None: - is_protocol: list = [p == protocol or isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type] - return ( - NoiseHandler( - list(compress(self.protocol, is_protocol)), - list(compress(self.options, is_protocol)), + def filter(self, protocol: NoiseEnum) -> NoiseHandler | None: + protocol_matches: list = list() + if protocol == NoiseProtocol.READOUT: + protocol_matches = [p == protocol for p in self.protocol] + else: + protocol_matches = [isinstance(p, protocol) for p in self.protocol] # type: ignore[arg-type] + + # if we have at least a match + if True in protocol_matches: + return NoiseHandler( + list(compress(self.protocol, protocol_matches)), + list(compress(self.options, protocol_matches)), ) - if len(is_protocol) > 0 - else None - ) + return None def bitflip(self, *args: Any, **kwargs: Any) -> NoiseHandler: self.append(NoiseHandler(NoiseProtocol.DIGITAL.BITFLIP, *args, **kwargs)) diff --git a/qadence/transpile/noise.py b/qadence/transpile/noise.py index 0a0c51ff..c8a5ea9d 100644 --- a/qadence/transpile/noise.py +++ b/qadence/transpile/noise.py @@ -1,5 +1,6 @@ from __future__ import annotations +from qadence.backend import ConvertedCircuit from qadence.blocks.abstract import AbstractBlock from qadence.circuit import QuantumCircuit from qadence.noise.protocols import NoiseHandler @@ -23,13 +24,15 @@ def _set_noise( def set_noise( - circuit: QuantumCircuit | AbstractBlock, + circuit: QuantumCircuit | AbstractBlock | ConvertedCircuit, noise: NoiseHandler | None, target_class: AbstractBlock | None = None, ) -> QuantumCircuit | AbstractBlock: """ Parses a `QuantumCircuit` or `CompositeBlock` to add noise to specific gates. + If `circuit` is a `ConvertedCircuit`, this is done within `circuit.abstract`. + Changes the input in place. Arguments: @@ -37,10 +40,14 @@ def set_noise( noise: the NoiseHandler protocol to change to, or `None` to remove the noise. target_class: optional class to selectively add noise to. """ - is_circuit_input = isinstance(circuit, QuantumCircuit) - - input_block: AbstractBlock = circuit.block if is_circuit_input else circuit # type: ignore + input_block: AbstractBlock + if isinstance(circuit, ConvertedCircuit): + input_block = circuit.abstract.block + elif isinstance(circuit, QuantumCircuit): + input_block = circuit.block + else: + input_block = circuit - output_block = apply_fn_to_blocks(input_block, _set_noise, noise, target_class) + apply_fn_to_blocks(input_block, _set_noise, noise, target_class) return circuit diff --git a/tests/qadence/test_noise/test_digital_noise.py b/tests/qadence/test_noise/test_digital_noise.py index 7c5a9355..d65409c7 100644 --- a/tests/qadence/test_noise/test_digital_noise.py +++ b/tests/qadence/test_noise/test_digital_noise.py @@ -92,6 +92,42 @@ def test_run_digital(noisy_config: NoiseProtocol | list[NoiseProtocol]) -> None: assert torch.allclose(noisy_output, native_output) +@pytest.mark.parametrize( + "noisy_config", + [ + NoiseProtocol.DIGITAL.BITFLIP, + [NoiseProtocol.DIGITAL.BITFLIP, NoiseProtocol.DIGITAL.PHASEFLIP], + ], +) +def test_expectation_digital_noise(noisy_config: NoiseProtocol | list[NoiseProtocol]) -> None: + block = kron(H(0), Z(1)) + circuit = QuantumCircuit(2, block) + observable = hamiltonian_factory(circuit.n_qubits, detuning=Z) + noise = NoiseHandler(noisy_config, {"error_probability": 0.1}) + backend = backend_factory(backend=BackendName.PYQTORCH, diff_mode=DiffMode.AD) + + # Construct a quantum model. + model = QuantumModel(circuit=circuit, observable=observable) + noiseless_expectation = model.expectation(values={}) + + (pyqtorch_circ, pyqtorch_obs, embed, params) = backend.convert(circuit, observable) + native_noisy_expectation = backend.expectation( + pyqtorch_circ, pyqtorch_obs, embed(params, {}), noise=noise + ) + assert not torch.allclose(noiseless_expectation, native_noisy_expectation) + + noisy_model = QuantumModel(circuit=circuit, observable=observable, noise=noise) + noisy_model_expectation = noisy_model.expectation(values={}) + assert torch.allclose(noisy_model_expectation, native_noisy_expectation) + + (pyqtorch_circ, pyqtorch_obs, embed, params) = backend.convert(circuit, observable) + noisy_converted_model_expectation = backend.expectation( + pyqtorch_circ, pyqtorch_obs, embed(params, {}) + ) + + assert torch.allclose(noisy_converted_model_expectation, native_noisy_expectation) + + @pytest.mark.parametrize( "noise_config", [