Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Setting digital noise in addition to readout noise in Pyq backend functions sample and expectation #610

Merged
merged 19 commits into from
Nov 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 58 additions & 9 deletions qadence/backends/pyqtorch/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
flatten,
invert_endianness,
scale_primitive_blocks_only,
set_noise,
transpile,
)
from qadence.types import BackendName, Endianness, Engine
Expand All @@ -38,6 +39,46 @@
logger = getLogger(__name__)


def set_noise_abstract_to_native(circuit: ConvertedCircuit, config: Configuration) -> None:
RolandMacDoland marked this conversation as resolved.
Show resolved Hide resolved
"""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."""
Expand All @@ -55,13 +96,27 @@ 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)

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 = (
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
)
Expand Down
17 changes: 16 additions & 1 deletion qadence/backends/pyqtorch/convert_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 13 additions & 9 deletions qadence/noise/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
chMoussa marked this conversation as resolved.
Show resolved Hide resolved

# 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))
Expand Down
17 changes: 12 additions & 5 deletions qadence/transpile/noise.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,24 +24,30 @@ 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:
circuit: the circuit or block to parse.
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
36 changes: 36 additions & 0 deletions tests/qadence/test_noise/test_digital_noise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
chMoussa marked this conversation as resolved.
Show resolved Hide resolved
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",
[
Expand Down