From 1ff915bc27bbcb778cf77757c047e831f8657f6c Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 16 Oct 2023 17:26:35 +0200 Subject: [PATCH 01/78] clifford sampling --- src/qibo/models/error_mitigation.py | 177 ++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index f0d333203d..c17a845655 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -1,5 +1,7 @@ """Error Mitigation Methods.""" +from itertools import product + import numpy as np from scipy.optimize import curve_fit @@ -595,3 +597,178 @@ def apply_randomized_readout_mitigation( results[j]._frequencies = freq_sum return results + + +def sample_clifford_training_circuit( + circuit, + backend=None, +): + """Samples a training circuit for CDR by susbtituting some of the non-Clifford gates. + + Args: + circuit (:class:`qibo.models.Circuit`): circuit to sample from, + decomposed in ``RX(pi/2)``, ``X``, ``CNOT`` and ``RZ`` gates. + replacement_gates (list, optional): candidates for the substitution of the + non-Clifford gates. The ``list`` should be composed by ``tuples`` of the + form (``gates.XYZ``, ``kwargs``). For example, phase gates are used by default: + ``list((RZ, {'theta':0}), (RZ, {'theta':pi/2}), (RZ, {'theta':pi}), (RZ, {'theta':3*pi/2}))``. + sigma (float, optional): standard devation of the Gaussian distribution used for sampling. + backend (:class:`qibo.backends.abstract.Backend`, optional): Calculation engine. + + Returns: + :class:`qibo.models.Circuit`: The sampled circuit. + """ + from qibo.quantum_info import random_clifford + + if backend is None: # pragma: no cover + backend = GlobalBackend() + + # Find all the non-Clifford gates + gates_to_replace = [] + for i, gate in enumerate(circuit.queue): + if gate.clifford is False and isinstance(gate, gates.M) is False: + gates_to_replace.append([i, gate]) + gates_to_replace = np.array(gates_to_replace, dtype=object) + + if len(gates_to_replace) == 0: + raise_error(ValueError, "No non-Clifford gate found, no circuit sampled.") + + # Build the training circuit by substituting the sampled gates + sampled_circuit = circuit.__class__(**circuit.init_kwargs) + + for i, gate in enumerate(circuit.queue): + if isinstance(gate, gates.M): + gate_rand = gates.Unitary( + random_clifford(1, backend=backend, return_circuit=False), + gate.qubits[0], + ) + gate_rand.clifford = True + + sampled_circuit.add(gate_rand) + sampled_circuit.add(gate) + + else: + if i in gates_to_replace[:, 0]: + gate = gates.Unitary( + random_clifford(1, backend=backend, return_circuit=False), + gate.qubits[0], + ) + gate.clifford = True + sampled_circuit.add(gate) + + return sampled_circuit + + +def escircuit(circuit, obs, backend=None): + from qibo.quantum_info import comp_basis_to_pauli, random_clifford, vectorization + + if backend is None: # pragma: no cover + backend = GlobalBackend() + + circ_cliff = sample_clifford_training_circuit(circuit, backend=backend) + + c_unitary = circ_cliff.unitary(backend=backend) + nqubits = circ_cliff.nqubits + U_c2p = comp_basis_to_pauli(nqubits, backend=backend) + obs_liouville = vectorization( + np.transpose(np.conjugate(c_unitary)) @ obs.matrix @ c_unitary, order="row" + ) + obs_pauli_liouville = U_c2p @ obs_liouville + index = np.where(abs(obs_pauli_liouville) >= 1e-5)[0][0] + obs1 = list(product(["I", "X", "Y", "Z"], repeat=nqubits))[index] + + paulis = { + "I": gates.I(0).matrix(), + "X": gates.X(0).matrix(), + "Y": gates.Y(0).matrix(), + "Z": gates.Z(0).matrix(), + } + + adjust_gates = [] + for i in range(nqubits): + obs_i = paulis[obs1[i]] + R = paulis["I"] + while np.any(abs(obs_i - paulis["Z"]) > 1e-5) and np.any( + abs(obs_i - paulis["I"]) > 1e-5 + ): + R = random_clifford(1, backend=backend, return_circuit=False) + obs_i = np.conjugate(np.transpose(R)) @ paulis[obs1[i]] @ R + + adjust_gate = gates.Unitary(R, i) + adjust_gate.clifford = True + adjust_gates.append(adjust_gate) + + circ_cliff1 = circ_cliff.__class__(**circ_cliff.init_kwargs) + + for gate in adjust_gates: + circ_cliff1.add(gate) + + for gate in circ_cliff.queue: + circ_cliff1.add(gate) + + return circ_cliff1.fuse(max_qubits=1), circ_cliff, adjust_gates + + +def mit_obs( + circuit, + observable, + noise_model, + nshots=int(1e4), + n_training_samples=10, + backend=None, +): + if backend is None: # pragma: no cover + backend = GlobalBackend() + + training_circs = [ + escircuit(circuit, observable, backend=backend)[0] + for _ in range(n_training_samples) + ] + + data = {"exact": {"-1": [], "1": []}, "noisy": {"-1": [], "1": []}} + + a_list = {"-1": [], "1": []} + for c in training_circs: + exp = c(nshots=nshots).expectation_from_samples(observable) + # state = c().state() + # exp = obs.expectation(state) + if noise_model is not None and backend.name != "qibolab": + c = noise_model.apply(c) + c.density_matrix = True + circuit_result = backend.execute_circuit(c, nshots=nshots) + exp_noisy = circuit_result.expectation_from_samples(observable) + # state = c_noisy().state() + # exp_noisy = obs.expectation(state) + + if exp > 0: + data["exact"]["1"].append(exp) + data["noisy"]["1"].append(exp_noisy) + a_list["1"].append(1 - exp_noisy / exp) + else: + data["exact"]["-1"].append(exp) + data["noisy"]["-1"].append(exp_noisy) + a_list["-1"].append(1 - exp_noisy / exp) + + a_std_1 = np.std(a_list["1"]) + a_std_m1 = np.std(a_list["-1"]) + a = (np.sum(a_list["1"]) + np.sum(a_list["-1"])) / n_training_samples + a_std = np.sqrt( + (a_std_1**2 * len(a_list["1"]) + a_std_m1**2 * len(a_list["-1"])) + / n_training_samples + ) + + if noise_model is not None and backend.name != "qibolab": + circuit = noise_model.apply(circuit) + circuit.density_matrix = True + circuit_result = backend.execute_circuit(circuit, nshots=nshots) + exp_noisy = circuit_result.expectation_from_samples(observable) + # exp_noisy = obs.expectation(c_noisy().state()) + exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) + exp_mit_std = ( + a_std + * abs(exp_noisy) + * abs((1 - a) ** 2 - a_std**2) + / ((1 - a) ** 2 + a_std**2) ** 2 + ) + + return exp_mit, exp_mit_std, a, a_std, a_list, data From 65630298e69ce2871d4daef76ff927ddcde91f6b Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 16 Oct 2023 22:29:02 +0200 Subject: [PATCH 02/78] output compatible with cdr --- src/qibo/models/error_mitigation.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index c17a845655..6af4c09334 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -655,11 +655,10 @@ def sample_clifford_training_circuit( ) gate.clifford = True sampled_circuit.add(gate) - return sampled_circuit -def escircuit(circuit, obs, backend=None): +def escircuit(circuit, obs, fuse=True, backend=None): from qibo.quantum_info import comp_basis_to_pauli, random_clifford, vectorization if backend is None: # pragma: no cover @@ -706,7 +705,10 @@ def escircuit(circuit, obs, backend=None): for gate in circ_cliff.queue: circ_cliff1.add(gate) - return circ_cliff1.fuse(max_qubits=1), circ_cliff, adjust_gates + if fuse: + circ_cliff1 = circ_cliff1.fuse(max_qubits=1) + + return circ_cliff1, circ_cliff, adjust_gates def mit_obs( @@ -715,6 +717,7 @@ def mit_obs( noise_model, nshots=int(1e4), n_training_samples=10, + full_output=False, backend=None, ): if backend is None: # pragma: no cover @@ -725,7 +728,7 @@ def mit_obs( for _ in range(n_training_samples) ] - data = {"exact": {"-1": [], "1": []}, "noisy": {"-1": [], "1": []}} + data = {"noise-free": {"-1": [], "1": []}, "noisy": {"-1": [], "1": []}} a_list = {"-1": [], "1": []} for c in training_circs: @@ -741,11 +744,11 @@ def mit_obs( # exp_noisy = obs.expectation(state) if exp > 0: - data["exact"]["1"].append(exp) + data["noise-free"]["1"].append(exp) data["noisy"]["1"].append(exp_noisy) a_list["1"].append(1 - exp_noisy / exp) else: - data["exact"]["-1"].append(exp) + data["noise-free"]["-1"].append(exp) data["noisy"]["-1"].append(exp_noisy) a_list["-1"].append(1 - exp_noisy / exp) @@ -771,4 +774,7 @@ def mit_obs( / ((1 - a) ** 2 + a_std**2) ** 2 ) - return exp_mit, exp_mit_std, a, a_std, a_list, data + if full_output: + return exp_mit, exp_mit_std, a, a_std, a_list, data + + return exp_mit From 40251cab3e87af887af08473c2cead4605b4fb84 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Tue, 17 Oct 2023 13:03:24 +0200 Subject: [PATCH 03/78] do not fuse I gates --- src/qibo/gates/special.py | 3 ++- src/qibo/models/circuit.py | 2 +- src/qibo/models/error_mitigation.py | 3 +-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/qibo/gates/special.py b/src/qibo/gates/special.py index 236f19d72d..8968e82ba2 100644 --- a/src/qibo/gates/special.py +++ b/src/qibo/gates/special.py @@ -1,5 +1,6 @@ from qibo.backends import GlobalBackend from qibo.gates.abstract import SpecialGate +from qibo.gates.gates import I from qibo.gates.measurements import M @@ -54,7 +55,7 @@ def __init__(self, *q): def from_gate(cls, gate): fgate = cls(*gate.qubits) fgate.append(gate) - if isinstance(gate, (M, SpecialGate)): + if isinstance(gate, (M, SpecialGate, I)): # special gates do not participate in fusion fgate.marked = True return fgate diff --git a/src/qibo/models/circuit.py b/src/qibo/models/circuit.py index 3e5d132b1f..f333736bd4 100644 --- a/src/qibo/models/circuit.py +++ b/src/qibo/models/circuit.py @@ -74,7 +74,7 @@ def from_fused(self): queue.append(gate.gates[0]) else: queue.append(gate) - elif isinstance(gate.gates[0], (gates.SpecialGate, gates.M)): + elif isinstance(gate.gates[0], (gates.SpecialGate, gates.M, gates.I)): # special gates are marked by default so we need # to add them manually queue.append(gate.gates[0]) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 6af4c09334..b2b06f8925 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -737,7 +737,6 @@ def mit_obs( # exp = obs.expectation(state) if noise_model is not None and backend.name != "qibolab": c = noise_model.apply(c) - c.density_matrix = True circuit_result = backend.execute_circuit(c, nshots=nshots) exp_noisy = circuit_result.expectation_from_samples(observable) # state = c_noisy().state() @@ -760,9 +759,9 @@ def mit_obs( / n_training_samples ) + circuit.fuse(max_qubits=1) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) - circuit.density_matrix = True circuit_result = backend.execute_circuit(circuit, nshots=nshots) exp_noisy = circuit_result.expectation_from_samples(observable) # exp_noisy = obs.expectation(c_noisy().state()) From 5a261e487b6698f4d7d383ff2b6bd5b268b637de Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Tue, 17 Oct 2023 13:39:22 +0200 Subject: [PATCH 04/78] fix fuse --- src/qibo/models/error_mitigation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index b2b06f8925..558e3f3a62 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -759,7 +759,7 @@ def mit_obs( / n_training_samples ) - circuit.fuse(max_qubits=1) + circuit = circuit.fuse(max_qubits=1) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) circuit_result = backend.execute_circuit(circuit, nshots=nshots) From 204920796e579be4fcc569f7aa46d4a8b96c72bb Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 20 Oct 2023 15:06:45 +0200 Subject: [PATCH 05/78] transpiling circuit --- src/qibo/models/error_mitigation.py | 35 +++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 558e3f3a62..a20480ea03 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -658,6 +658,25 @@ def sample_clifford_training_circuit( return sampled_circuit +def transpile_circ(circuit, qubit_map, backend): + if backend.name == "qibolab": + new_c = circuit.__class__(5) + for gate in circuit.queue: + qubits = [qubit_map[j] for j in gate.qubits] + if isinstance(gate, gates.M): + new_gate = gates.M(*tuple(qubits), **gate.init_kwargs) + new_gate.result = gate.result + new_c.add(new_gate) + elif isinstance(gate, gates.I): + new_c.add(gate.__class__(*tuple(qubits), **gate.init_kwargs)) + else: + matrix = gate.matrix() + new_c.add(gates.Unitary(matrix, *tuple(qubits), **gate.init_kwargs)) + return new_c + else: + return circuit + + def escircuit(circuit, obs, fuse=True, backend=None): from qibo.quantum_info import comp_basis_to_pauli, random_clifford, vectorization @@ -714,7 +733,8 @@ def escircuit(circuit, obs, fuse=True, backend=None): def mit_obs( circuit, observable, - noise_model, + qubit_map=None, + noise_model=None, nshots=int(1e4), n_training_samples=10, full_output=False, @@ -723,6 +743,9 @@ def mit_obs( if backend is None: # pragma: no cover backend = GlobalBackend() + if qubit_map is None: + qubit_map = list(range(circuit.nqubits)) + training_circs = [ escircuit(circuit, observable, backend=backend)[0] for _ in range(n_training_samples) @@ -732,13 +755,16 @@ def mit_obs( a_list = {"-1": [], "1": []} for c in training_circs: - exp = c(nshots=nshots).expectation_from_samples(observable) + circuit_result = c(nshots=nshots) + exp = observable.expectation_from_samples(circuit_result.frequencies()) # state = c().state() # exp = obs.expectation(state) if noise_model is not None and backend.name != "qibolab": c = noise_model.apply(c) + + c = transpile_circ(c, qubit_map, backend) circuit_result = backend.execute_circuit(c, nshots=nshots) - exp_noisy = circuit_result.expectation_from_samples(observable) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # state = c_noisy().state() # exp_noisy = obs.expectation(state) @@ -762,8 +788,9 @@ def mit_obs( circuit = circuit.fuse(max_qubits=1) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) + circuit = transpile_circ(circuit, qubit_map, backend) circuit_result = backend.execute_circuit(circuit, nshots=nshots) - exp_noisy = circuit_result.expectation_from_samples(observable) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # exp_noisy = obs.expectation(c_noisy().state()) exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) exp_mit_std = ( From cdf56471454df17e939ae786117f4755debf4e29 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 20 Oct 2023 15:07:45 +0200 Subject: [PATCH 06/78] fix --- src/qibo/models/error_mitigation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index a20480ea03..738aedb22a 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -660,7 +660,7 @@ def sample_clifford_training_circuit( def transpile_circ(circuit, qubit_map, backend): if backend.name == "qibolab": - new_c = circuit.__class__(5) + new_c = circuit.__class__(backend.platform.nqubits) for gate in circuit.queue: qubits = [qubit_map[j] for j in gate.qubits] if isinstance(gate, gates.M): From c1fc36bc59b971531265123d5850999196093774 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Fri, 20 Oct 2023 21:26:11 +0400 Subject: [PATCH 07/78] fix transpiling --- src/qibo/models/error_mitigation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 738aedb22a..a77903c20e 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -659,6 +659,7 @@ def sample_clifford_training_circuit( def transpile_circ(circuit, qubit_map, backend): + from qibolab.transpilers.unitary_decompositions import u3_decomposition if backend.name == "qibolab": new_c = circuit.__class__(backend.platform.nqubits) for gate in circuit.queue: @@ -671,7 +672,7 @@ def transpile_circ(circuit, qubit_map, backend): new_c.add(gate.__class__(*tuple(qubits), **gate.init_kwargs)) else: matrix = gate.matrix() - new_c.add(gates.Unitary(matrix, *tuple(qubits), **gate.init_kwargs)) + new_c.add(gates.U3(qubits[0], *u3_decomposition(matrix))) return new_c else: return circuit From 6c49e7123137ce137df7c58dfae8c9d9cd29e8cb Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 20 Oct 2023 20:56:58 +0200 Subject: [PATCH 08/78] fix identities --- src/qibo/models/error_mitigation.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index a77903c20e..dad2870686 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -637,7 +637,7 @@ def sample_clifford_training_circuit( sampled_circuit = circuit.__class__(**circuit.init_kwargs) for i, gate in enumerate(circuit.queue): - if isinstance(gate, gates.M): + if gate.name == "id_end": # isinstance(gate, gates.M): gate_rand = gates.Unitary( random_clifford(1, backend=backend, return_circuit=False), gate.qubits[0], @@ -660,6 +660,7 @@ def sample_clifford_training_circuit( def transpile_circ(circuit, qubit_map, backend): from qibolab.transpilers.unitary_decompositions import u3_decomposition + if backend.name == "qibolab": new_c = circuit.__class__(backend.platform.nqubits) for gate in circuit.queue: @@ -719,11 +720,19 @@ def escircuit(circuit, obs, fuse=True, backend=None): circ_cliff1 = circ_cliff.__class__(**circ_cliff.init_kwargs) - for gate in adjust_gates: - circ_cliff1.add(gate) + # for gate in adjust_gates: + # circ_cliff1.add(gate) + # for gate in circ_cliff.queue: + # circ_cliff1.add(gate) + j = 0 for gate in circ_cliff.queue: - circ_cliff1.add(gate) + if gate.name == "id_init": + circ_cliff1.add(gate) + circ_cliff1.add(adjust_gates[j]) + j += 1 + else: + circ_cliff1.add(gate) if fuse: circ_cliff1 = circ_cliff1.fuse(max_qubits=1) @@ -764,6 +773,7 @@ def mit_obs( c = noise_model.apply(c) c = transpile_circ(c, qubit_map, backend) + print(c.draw()) circuit_result = backend.execute_circuit(c, nshots=nshots) exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # state = c_noisy().state() @@ -787,6 +797,7 @@ def mit_obs( ) circuit = circuit.fuse(max_qubits=1) + print(circuit.draw()) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) circuit = transpile_circ(circuit, qubit_map, backend) From b1b3971841bddf84aa77b106bba006adefb20227 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Fri, 20 Oct 2023 23:47:32 +0400 Subject: [PATCH 09/78] remove prints --- src/qibo/models/error_mitigation.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index dad2870686..db208c6837 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -773,7 +773,6 @@ def mit_obs( c = noise_model.apply(c) c = transpile_circ(c, qubit_map, backend) - print(c.draw()) circuit_result = backend.execute_circuit(c, nshots=nshots) exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # state = c_noisy().state() @@ -797,7 +796,6 @@ def mit_obs( ) circuit = circuit.fuse(max_qubits=1) - print(circuit.draw()) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) circuit = transpile_circ(circuit, qubit_map, backend) From 57db2ec77919bf3ed2b2e78f8d23e296b9a033a7 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Sun, 22 Oct 2023 21:27:17 +0400 Subject: [PATCH 10/78] add minus for iqm5q --- src/qibo/models/error_mitigation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index db208c6837..6385381a1a 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -774,7 +774,7 @@ def mit_obs( c = transpile_circ(c, qubit_map, backend) circuit_result = backend.execute_circuit(c, nshots=nshots) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) #Add minus for iqm # state = c_noisy().state() # exp_noisy = obs.expectation(state) @@ -800,7 +800,7 @@ def mit_obs( circuit = noise_model.apply(circuit) circuit = transpile_circ(circuit, qubit_map, backend) circuit_result = backend.execute_circuit(circuit, nshots=nshots) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # Add - for iqm # exp_noisy = obs.expectation(c_noisy().state()) exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) exp_mit_std = ( From 332d88b78169259908bd90877ed58033a3b77833 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Wed, 25 Oct 2023 20:31:02 +0400 Subject: [PATCH 11/78] readout --- src/qibo/models/error_mitigation.py | 45 ++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 6385381a1a..84d5bf502c 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -463,7 +463,7 @@ def vnCDR( return mit_val -def calibration_matrix(nqubits, noise_model=None, nshots: int = 1000, backend=None): +def calibration_matrix(nqubits, qubit_map, noise_model=None, nshots: int = 1000, backend=None): """Computes the calibration matrix for readout mitigation. Args: @@ -485,7 +485,7 @@ def calibration_matrix(nqubits, noise_model=None, nshots: int = 1000, backend=No backend = GlobalBackend() matrix = np.zeros((2**nqubits, 2**nqubits)) - + from qibo.config import log for i in range(2**nqubits): state = format(i, f"0{nqubits}b") @@ -495,17 +495,21 @@ def calibration_matrix(nqubits, noise_model=None, nshots: int = 1000, backend=No circuit.add(gates.X(q)) circuit.add(gates.M(*range(nqubits))) + if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) - + circuit = transpile_circ(circuit, qubit_map, backend) freq = backend.execute_circuit(circuit, nshots=nshots).frequencies() - column = np.zeros(2**nqubits) for key in freq.keys(): f = freq[key] / nshots column[int(key, 2)] = f matrix[:, i] = column + # iqm + + # matrix[:, [0, 1]] = matrix[:, [1, 0]] + return np.linalg.inv(matrix) @@ -520,12 +524,12 @@ def apply_readout_mitigation(state, calibration_matrix): Returns: :class:`qibo.states.CircuitResult`: the input state with the updated frequencies. """ - freq = np.zeros(2**state.nqubits) + from qibo.config import log + freq = np.zeros(2**len(state.measurements)) for k, v in state.frequencies().items(): freq[int(k, 2)] = v freq = freq.reshape(-1, 1) - for i, val in enumerate(calibration_matrix @ freq): state._frequencies[i] = float(val) @@ -685,8 +689,9 @@ def escircuit(circuit, obs, fuse=True, backend=None): if backend is None: # pragma: no cover backend = GlobalBackend() + sign = -1 + #while sign == -1: circ_cliff = sample_clifford_training_circuit(circuit, backend=backend) - c_unitary = circ_cliff.unitary(backend=backend) nqubits = circ_cliff.nqubits U_c2p = comp_basis_to_pauli(nqubits, backend=backend) @@ -695,6 +700,8 @@ def escircuit(circuit, obs, fuse=True, backend=None): ) obs_pauli_liouville = U_c2p @ obs_liouville index = np.where(abs(obs_pauli_liouville) >= 1e-5)[0][0] + sign = np.sign(obs_pauli_liouville[index]) + obs1 = list(product(["I", "X", "Y", "Z"], repeat=nqubits))[index] paulis = { @@ -743,6 +750,7 @@ def escircuit(circuit, obs, fuse=True, backend=None): def mit_obs( circuit, observable, + readout = {}, qubit_map=None, noise_model=None, nshots=int(1e4), @@ -762,7 +770,7 @@ def mit_obs( ] data = {"noise-free": {"-1": [], "1": []}, "noisy": {"-1": [], "1": []}} - + from qibo.config import log a_list = {"-1": [], "1": []} for c in training_circs: circuit_result = c(nshots=nshots) @@ -774,10 +782,17 @@ def mit_obs( c = transpile_circ(c, qubit_map, backend) circuit_result = backend.execute_circuit(c, nshots=nshots) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) #Add minus for iqm + + if "calibration_matrix" in readout.keys(): + circuit_result = apply_readout_mitigation( + circuit_result, readout["calibration_matrix"] + ) + + log.info(c.draw()) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) #Add minus for iqm # state = c_noisy().state() # exp_noisy = obs.expectation(state) - + log.info(str(exp)+' '+str(exp_noisy)) if exp > 0: data["noise-free"]["1"].append(exp) data["noisy"]["1"].append(exp_noisy) @@ -795,12 +810,20 @@ def mit_obs( / n_training_samples ) + # a = np.mean(a_list["1"]) + # a_std = a_std_1 + circuit = circuit.fuse(max_qubits=1) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) circuit = transpile_circ(circuit, qubit_map, backend) + log.info(circuit.draw()) circuit_result = backend.execute_circuit(circuit, nshots=nshots) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # Add - for iqm + if "calibration_matrix" in readout.keys(): + circuit_result = apply_readout_mitigation( + circuit_result, readout["calibration_matrix"] + ) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # Add - for iqm # exp_noisy = obs.expectation(c_noisy().state()) exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) exp_mit_std = ( From e24014652929d374c0c166f95f8f8a55371007d7 Mon Sep 17 00:00:00 2001 From: Alejandro Date: Mon, 30 Oct 2023 17:00:16 +0400 Subject: [PATCH 12/78] bayesian unfolding readout --- src/qibo/models/error_mitigation.py | 40 +++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 84d5bf502c..39c3caf721 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -463,7 +463,14 @@ def vnCDR( return mit_val -def calibration_matrix(nqubits, qubit_map, noise_model=None, nshots: int = 1000, backend=None): +def ibu(prob, mat, iters=10): + t = np.ones((len(prob),1))/len(prob) + for k in range(iters): + t = t*(np.transpose(mat)@(prob/(mat@t))) + + return t + +def calibration_matrix(nqubits, qubit_map, inv=False, noise_model=None, nshots: int = 1000, backend=None): """Computes the calibration matrix for readout mitigation. Args: @@ -508,12 +515,16 @@ def calibration_matrix(nqubits, qubit_map, noise_model=None, nshots: int = 1000, # iqm - # matrix[:, [0, 1]] = matrix[:, [1, 0]] + #matrix[[0, 1], :] = matrix[[1, 0], :] + - return np.linalg.inv(matrix) + if inv == True: + matrix = np.linalg.inv(matrix) + return matrix -def apply_readout_mitigation(state, calibration_matrix): + +def apply_readout_mitigation(state, calibration_matrix, inv=False): """Updates the frequencies of the input state with the mitigated ones obtained with ``calibration_matrix * state.frequencies()``. @@ -530,8 +541,17 @@ def apply_readout_mitigation(state, calibration_matrix): freq[int(k, 2)] = v freq = freq.reshape(-1, 1) - for i, val in enumerate(calibration_matrix @ freq): - state._frequencies[i] = float(val) + + if inv: + for i, val in enumerate(calibration_matrix @ freq): + state._frequencies[i] = float(val) + else: + prob_mit = ibu(freq/np.sum(freq),calibration_matrix) + freq_mit = np.round(prob_mit*np.sum(freq),0) + freq_mit = (freq_mit/np.sum(freq_mit))*np.sum(freq) + + for i, val in enumerate(freq_mit): + state._frequencies[i] = float(freq_mit[i]) return state @@ -785,7 +805,7 @@ def mit_obs( if "calibration_matrix" in readout.keys(): circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"] + circuit_result, readout["calibration_matrix"], readout["inv"] ) log.info(c.draw()) @@ -821,9 +841,9 @@ def mit_obs( circuit_result = backend.execute_circuit(circuit, nshots=nshots) if "calibration_matrix" in readout.keys(): circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"] + circuit_result, readout["calibration_matrix"], readout["inv"] ) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # Add - for iqm + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # Add - for iqm # exp_noisy = obs.expectation(c_noisy().state()) exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) exp_mit_std = ( @@ -832,7 +852,7 @@ def mit_obs( * abs((1 - a) ** 2 - a_std**2) / ((1 - a) ** 2 + a_std**2) ** 2 ) - + log.info(str(exp_mit)+' '+str(exp_noisy)) if full_output: return exp_mit, exp_mit_std, a, a_std, a_list, data From 81f44fe3dbf20ae53b85e77897ddff2f5a8d8c20 Mon Sep 17 00:00:00 2001 From: MatteoRobbiati Date: Mon, 20 Nov 2023 16:38:06 +0100 Subject: [PATCH 13/78] remove logs --- src/qibo/models/error_mitigation.py | 45 ++++++++-------- src/qibo/models/variational.py | 79 +++++++---------------------- 2 files changed, 40 insertions(+), 84 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 39c3caf721..b5fb1657b9 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -464,13 +464,16 @@ def vnCDR( def ibu(prob, mat, iters=10): - t = np.ones((len(prob),1))/len(prob) + t = np.ones((len(prob), 1)) / len(prob) for k in range(iters): - t = t*(np.transpose(mat)@(prob/(mat@t))) + t = t * (np.transpose(mat) @ (prob / (mat @ t))) return t -def calibration_matrix(nqubits, qubit_map, inv=False, noise_model=None, nshots: int = 1000, backend=None): + +def calibration_matrix( + nqubits, qubit_map, inv=False, noise_model=None, nshots: int = 1000, backend=None +): """Computes the calibration matrix for readout mitigation. Args: @@ -493,6 +496,7 @@ def calibration_matrix(nqubits, qubit_map, inv=False, noise_model=None, nshots: matrix = np.zeros((2**nqubits, 2**nqubits)) from qibo.config import log + for i in range(2**nqubits): state = format(i, f"0{nqubits}b") @@ -502,7 +506,6 @@ def calibration_matrix(nqubits, qubit_map, inv=False, noise_model=None, nshots: circuit.add(gates.X(q)) circuit.add(gates.M(*range(nqubits))) - if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) circuit = transpile_circ(circuit, qubit_map, backend) @@ -513,10 +516,9 @@ def calibration_matrix(nqubits, qubit_map, inv=False, noise_model=None, nshots: column[int(key, 2)] = f matrix[:, i] = column - # iqm - - #matrix[[0, 1], :] = matrix[[1, 0], :] + # iqm + # matrix[[0, 1], :] = matrix[[1, 0], :] if inv == True: matrix = np.linalg.inv(matrix) @@ -536,7 +538,8 @@ def apply_readout_mitigation(state, calibration_matrix, inv=False): :class:`qibo.states.CircuitResult`: the input state with the updated frequencies. """ from qibo.config import log - freq = np.zeros(2**len(state.measurements)) + + freq = np.zeros(2 ** len(state.measurements)) for k, v in state.frequencies().items(): freq[int(k, 2)] = v @@ -546,9 +549,9 @@ def apply_readout_mitigation(state, calibration_matrix, inv=False): for i, val in enumerate(calibration_matrix @ freq): state._frequencies[i] = float(val) else: - prob_mit = ibu(freq/np.sum(freq),calibration_matrix) - freq_mit = np.round(prob_mit*np.sum(freq),0) - freq_mit = (freq_mit/np.sum(freq_mit))*np.sum(freq) + prob_mit = ibu(freq / np.sum(freq), calibration_matrix) + freq_mit = np.round(prob_mit * np.sum(freq), 0) + freq_mit = (freq_mit / np.sum(freq_mit)) * np.sum(freq) for i, val in enumerate(freq_mit): state._frequencies[i] = float(freq_mit[i]) @@ -710,7 +713,7 @@ def escircuit(circuit, obs, fuse=True, backend=None): backend = GlobalBackend() sign = -1 - #while sign == -1: + # while sign == -1: circ_cliff = sample_clifford_training_circuit(circuit, backend=backend) c_unitary = circ_cliff.unitary(backend=backend) nqubits = circ_cliff.nqubits @@ -720,7 +723,7 @@ def escircuit(circuit, obs, fuse=True, backend=None): ) obs_pauli_liouville = U_c2p @ obs_liouville index = np.where(abs(obs_pauli_liouville) >= 1e-5)[0][0] - sign = np.sign(obs_pauli_liouville[index]) + sign = np.sign(obs_pauli_liouville[index]) obs1 = list(product(["I", "X", "Y", "Z"], repeat=nqubits))[index] @@ -770,7 +773,7 @@ def escircuit(circuit, obs, fuse=True, backend=None): def mit_obs( circuit, observable, - readout = {}, + readout={}, qubit_map=None, noise_model=None, nshots=int(1e4), @@ -791,6 +794,7 @@ def mit_obs( data = {"noise-free": {"-1": [], "1": []}, "noisy": {"-1": [], "1": []}} from qibo.config import log + a_list = {"-1": [], "1": []} for c in training_circs: circuit_result = c(nshots=nshots) @@ -808,11 +812,8 @@ def mit_obs( circuit_result, readout["calibration_matrix"], readout["inv"] ) - log.info(c.draw()) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) #Add minus for iqm - # state = c_noisy().state() - # exp_noisy = obs.expectation(state) - log.info(str(exp)+' '+str(exp_noisy)) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) + if exp > 0: data["noise-free"]["1"].append(exp) data["noisy"]["1"].append(exp_noisy) @@ -837,13 +838,14 @@ def mit_obs( if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) circuit = transpile_circ(circuit, qubit_map, backend) - log.info(circuit.draw()) circuit_result = backend.execute_circuit(circuit, nshots=nshots) if "calibration_matrix" in readout.keys(): circuit_result = apply_readout_mitigation( circuit_result, readout["calibration_matrix"], readout["inv"] ) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) # Add - for iqm + exp_noisy = observable.expectation_from_samples( + circuit_result.frequencies() + ) # Add - for iqm # exp_noisy = obs.expectation(c_noisy().state()) exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) exp_mit_std = ( @@ -852,7 +854,6 @@ def mit_obs( * abs((1 - a) ** 2 - a_std**2) / ((1 - a) ** 2 + a_std**2) ** 2 ) - log.info(str(exp_mit)+' '+str(exp_noisy)) if full_output: return exp_mit, exp_mit_std, a, a_std, a_list, data diff --git a/src/qibo/models/variational.py b/src/qibo/models/variational.py index 7172bf07d8..c9cfefd938 100644 --- a/src/qibo/models/variational.py +++ b/src/qibo/models/variational.py @@ -36,45 +36,23 @@ def __init__(self, circuit, hamiltonian): def minimize( self, - initial_state, - method="Powell", - jac=None, - hess=None, - hessp=None, - bounds=None, - constraints=(), - tol=None, - callback=None, - options=None, - compile=False, - processes=None, + initial_parameters, + optimizer=None, + optimizer_options={}, ): """Search for parameters which minimizes the hamiltonian expectation. Args: - initial_state (array): a initial guess for the parameters of the + initial_parameters (array or list): a initial guess for the parameters of the variational circuit. - method (str): the desired minimization method. - See :meth:`qibo.optimizers.optimize` for available optimization - methods. - jac (dict): Method for computing the gradient vector for scipy optimizers. - hess (dict): Method for computing the hessian matrix for scipy optimizers. - hessp (callable): Hessian of objective function times an arbitrary - vector for scipy optimizers. - bounds (sequence or Bounds): Bounds on variables for scipy optimizers. - constraints (dict): Constraints definition for scipy optimizers. - tol (float): Tolerance of termination for scipy optimizers. - callback (callable): Called after each iteration for scipy optimizers. - options (dict): a dictionary with options for the different optimizers. - compile (bool): whether the TensorFlow graph should be compiled. - processes (int): number of processes when using the paralle BFGS method. + optimizer (qibo.optimizers.Optimizer): Qibo optimizer which is used to + minimize the energy [default is `ScipyMinimizer(method="Powell")`]. + optimizer_options (dict): extra options for the optimizers. Return: - The final expectation value. - The corresponding best parameters. - The optimization result object. For scipy methods it returns - the ``OptimizeResult``, for ``'cma'`` the ``CMAEvolutionStrategy.result``, - and for ``'sgd'`` the options used during the optimization. + output of `qibo.optimizers.Optimizer` classes: [best function value, + best set of trainable parameters, complete result object according + to specific optimizer configuration]. """ def _loss(params, circuit, hamiltonian): @@ -83,38 +61,15 @@ def _loss(params, circuit, hamiltonian): final_state = result.state() return hamiltonian.expectation(final_state) - if compile: - loss = self.hamiltonian.backend.compile(_loss) - else: - loss = _loss - - if method == "cma": - # TODO: check if we can use this shortcut - # dtype = getattr(self.hamiltonian.backend.np, self.hamiltonian.backend._dtypes.get('DTYPE')) - dtype = self.hamiltonian.backend.np.float64 - loss = lambda p, c, h: dtype(_loss(p, c, h)) - elif method != "sgd": - loss = lambda p, c, h: self.hamiltonian.backend.to_numpy(_loss(p, c, h)) - - result, parameters, extra = self.optimizers.optimize( - loss, - initial_state, + opt = optimizer( + initial_parameters=initial_parameters, + loss=_loss, args=(self.circuit, self.hamiltonian), - method=method, - jac=jac, - hess=hess, - hessp=hessp, - bounds=bounds, - constraints=constraints, - tol=tol, - callback=callback, - options=options, - compile=compile, - processes=processes, - backend=self.hamiltonian.backend, + **optimizer_options, ) - self.circuit.set_parameters(parameters) - return result, parameters, extra + + best_f, best_p, results = opt.fit() + return best_f, best_p, results class AAVQE: From 1aed5d4a1468ad2258fe5f07cef0025b77bb0e30 Mon Sep 17 00:00:00 2001 From: MatteoRobbiati Date: Mon, 20 Nov 2023 17:14:36 +0100 Subject: [PATCH 14/78] rm comments and add preliminary docstrings --- src/qibo/models/error_mitigation.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index b5fb1657b9..ceff367882 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -464,6 +464,7 @@ def vnCDR( def ibu(prob, mat, iters=10): + """Iterative Bayesian Unfolding.""" t = np.ones((len(prob), 1)) / len(prob) for k in range(iters): t = t * (np.transpose(mat) @ (prob / (mat @ t))) @@ -516,10 +517,6 @@ def calibration_matrix( column[int(key, 2)] = f matrix[:, i] = column - # iqm - - # matrix[[0, 1], :] = matrix[[1, 0], :] - if inv == True: matrix = np.linalg.inv(matrix) @@ -707,6 +704,10 @@ def transpile_circ(circuit, qubit_map, backend): def escircuit(circuit, obs, fuse=True, backend=None): + """ + Error sensitive circuit algorithm. + Implement what proposed in https://arxiv.org/abs/2112.06255. + """ from qibo.quantum_info import comp_basis_to_pauli, random_clifford, vectorization if backend is None: # pragma: no cover @@ -750,11 +751,6 @@ def escircuit(circuit, obs, fuse=True, backend=None): circ_cliff1 = circ_cliff.__class__(**circ_cliff.init_kwargs) - # for gate in adjust_gates: - # circ_cliff1.add(gate) - - # for gate in circ_cliff.queue: - # circ_cliff1.add(gate) j = 0 for gate in circ_cliff.queue: if gate.name == "id_init": @@ -770,7 +766,7 @@ def escircuit(circuit, obs, fuse=True, backend=None): return circ_cliff1, circ_cliff, adjust_gates -def mit_obs( +def ICS( circuit, observable, readout={}, @@ -781,6 +777,10 @@ def mit_obs( full_output=False, backend=None, ): + """ + Compute the important Clifford Sampling methos proposed in + https://arxiv.org/abs/2112.06255. + """ if backend is None: # pragma: no cover backend = GlobalBackend() @@ -831,9 +831,6 @@ def mit_obs( / n_training_samples ) - # a = np.mean(a_list["1"]) - # a_std = a_std_1 - circuit = circuit.fuse(max_qubits=1) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) @@ -843,10 +840,7 @@ def mit_obs( circuit_result = apply_readout_mitigation( circuit_result, readout["calibration_matrix"], readout["inv"] ) - exp_noisy = observable.expectation_from_samples( - circuit_result.frequencies() - ) # Add - for iqm - # exp_noisy = obs.expectation(c_noisy().state()) + exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) exp_mit_std = ( a_std From 5a5f11229c996bddeda8969d265f6cff7f437f95 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 5 Dec 2023 10:25:59 +0400 Subject: [PATCH 15/78] restrict qubits function --- src/qibo/transpiler/router.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 8bea586444..191087ffaf 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -34,6 +34,29 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): ) +def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): + """Restrict the connectivity to selected qubits. + + Args: + connectivity (:class:`networkx.Graph`): chip connectivity. + qubits (list): list of physical qubits to be used. + """ + if set(qubits).issubset(connectivity.nodes): + raise_error( + ConnectivityError, "Some qubits are not in the original connectivity." + ) + new_connectivity = nx.Graph() + new_connectivity.add_nodes_from(qubits) + new_edges = [] + for edge in connectivity.edges: + if edge[0] in qubits and edge[1] in qubits: + new_edges.append(edge) + new_connectivity.add_edges_from(new_edges) + if not nx.is_connected(new_connectivity): + raise_error(ConnectivityError, "The new connectivity graph is not connected.") + return new_connectivity + + # TODO: make this class work with CircuitMap class ShortestPaths(Router): """A class to perform initial qubit mapping and connectivity matching. @@ -716,8 +739,16 @@ def _create_dag(gates_qubits_pairs): return _remove_redundant_connections(dag) -def _remove_redundant_connections(dag: nx.Graph): - """Remove redundant connection from a DAG unsing transitive reduction.""" +def _remove_redundant_connections(dag: nx.DiGraph): + """Helper method for :func:`_create_dag`. + Remove redundant connection from a DAG using transitive reduction. + + Args: + dag (:class:`networkx.DiGraph`): dag to be reduced. + + Returns: + (:class:`networkx.DiGraph`): reduced dag. + """ new_dag = nx.DiGraph() new_dag.add_nodes_from(range(dag.number_of_nodes())) transitive_reduction = nx.transitive_reduction(dag) From 9899a22f0f5d986f1ce456148444b16c0df9a7db Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 5 Dec 2023 11:28:00 +0400 Subject: [PATCH 16/78] added tests --- src/qibo/transpiler/router.py | 5 ++++- tests/test_transpiler_router.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 191087ffaf..f9ff7dc28a 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -40,8 +40,11 @@ def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): Args: connectivity (:class:`networkx.Graph`): chip connectivity. qubits (list): list of physical qubits to be used. + + Returns: + (:class:`networkx.Graph`): restricted connectivity. """ - if set(qubits).issubset(connectivity.nodes): + if not set(qubits).issubset(set(connectivity.nodes)): raise_error( ConnectivityError, "Some qubits are not in the original connectivity." ) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 621f7f69e3..ea4ca73275 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -14,6 +14,7 @@ ShortestPaths, _find_gates_qubits_pairs, assert_connectivity, + restrict_connectivity_qubits, ) @@ -79,6 +80,24 @@ def matched_circuit(): return circuit +def test_restrict_qubits_error_no_subset(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 2, 6]) + assert "Some qubits are not in the original connectivity." in str(excinfo.value) + + +def test_restrict_qubits_error_not_connected(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 3]) + assert "The new connectivity graph is not connected." in str(excinfo.value) + + +def test_restrict_qubits(): + new_connectivity = restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]) + assert list(new_connectivity.nodes) == [1, 2, 3] + assert list(new_connectivity.edges) == [(1, 2), (2, 3)] + + def test_assert_connectivity(): assert_connectivity(star_connectivity(), matched_circuit()) From 6ada47cfc3e56042eaff1990133a62e4e7343e41 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 8 Dec 2023 16:03:55 +0400 Subject: [PATCH 17/78] restricted topology for placer --- src/qibo/transpiler/pipeline.py | 51 ++++++++++++--- src/qibo/transpiler/placer.py | 74 ++++++++++++++------- src/qibo/transpiler/router.py | 29 +-------- tests/test_transpiler_pipeline.py | 32 +++++++-- tests/test_transpiler_placer.py | 105 +++++++++++++++++++++++++++++- tests/test_transpiler_router.py | 19 ------ 6 files changed, 222 insertions(+), 88 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index b88da8da8f..96e4be76d6 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -131,40 +131,71 @@ def assert_transpiling( ) +def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): + """Restrict the connectivity to selected qubits. + + Args: + connectivity (:class:`networkx.Graph`): chip connectivity. + qubits (list): list of physical qubits to be used. + + Returns: + (:class:`networkx.Graph`): restricted connectivity. + """ + if not set(qubits).issubset(set(connectivity.nodes)): + raise_error( + ConnectivityError, "Some qubits are not in the original connectivity." + ) + new_connectivity = nx.Graph() + new_connectivity.add_nodes_from(qubits) + new_edges = [] + for edge in connectivity.edges: + if edge[0] in qubits and edge[1] in qubits: + new_edges.append(edge) + new_connectivity.add_edges_from(new_edges) + if not nx.is_connected(new_connectivity): + raise_error(ConnectivityError, "The new connectivity graph is not connected.") + return new_connectivity + + class Passes: """Define a transpiler pipeline consisting of smaller transpiler steps that are applied sequentially: Args: passes (list): list of passes to be applied sequentially, - if None default transpiler will be used, it requires hardware connectivity. - connectivity (nx.Graph): hardware qubit connectivity. + if None default transpiler will be used. + connectivity (nx.Graph): physical qubits connectivity. + native_gates (NativeGates): native gates. + on_qubits (list): list of physical qubits to be used. If "None" all qubits are used. """ def __init__( self, - passes: list = None, - connectivity: nx.Graph = None, + passes: list, + connectivity: nx.Graph, native_gates: NativeGates = NativeGates.default(), + on_qubits: list = None, ): + if on_qubits is not None: + connectivity = restrict_connectivity_qubits(connectivity, on_qubits) + self.connectivity = connectivity self.native_gates = native_gates if passes is None: - self.passes = self.default(connectivity) + self.passes = self.default() else: self.passes = passes - self.connectivity = connectivity - def default(self, connectivity: nx.Graph): + def default(self): """Return the default transpiler pipeline for the required hardware connectivity.""" - if not isinstance(connectivity, nx.Graph): + if not isinstance(self.connectivity, nx.Graph): raise_error( TranspilerPipelineError, "Define the hardware chip connectivity to use default transpiler", ) default_passes = [] # preprocessing - default_passes.append(Preprocessing(connectivity=connectivity)) + default_passes.append(Preprocessing(connectivity=self.connectivity)) # default placer pass - default_passes.append(Trivial(connectivity=connectivity)) + default_passes.append(Trivial(connectivity=self.connectivity)) # default router pass default_passes.append(StarConnectivity()) # default unroller pass diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index aa33536445..f74a1bbf76 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -11,14 +11,18 @@ from qibo.transpiler.router import _find_gates_qubits_pairs -def assert_placement(circuit: Circuit, layout: dict) -> bool: +def assert_placement( + circuit: Circuit, layout: dict, connectivity: nx.graph = None +) -> bool: """Check if layout is in the correct form and matches the number of qubits of the circuit. Args: circuit (:class:`qibo.models.circuit.Circuit`): Circuit model to check. layout (dict): physical to logical qubit mapping. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the + layout applied to a subset of qubits in the original connectivity graph. """ - assert_mapping_consistency(layout) + assert_mapping_consistency(layout=layout, connectivity=connectivity) if circuit.nqubits > len(layout): raise_error( PlacementError, @@ -31,16 +35,20 @@ def assert_placement(circuit: Circuit, layout: dict) -> bool: ) -def assert_mapping_consistency(layout: dict): +def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): """Check if layout is in the correct form. Args: layout (dict): physical to logical qubit mapping. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the + layout applied to a subset of qubits in the original connectivity graph. """ values = sorted(layout.values()) - keys = list(layout) - ref_keys = ["q" + str(i) for i in range(len(keys))] - if keys != ref_keys: + if connectivity is not None: + ref_keys = ["q" + str(i) for i in list(connectivity.nodes)] + else: + ref_keys = ["q" + str(i) for i in range(len(values))] + if list(layout.keys()) != ref_keys: raise_error( PlacementError, "Some physical qubits in the layout may be missing or duplicated.", @@ -81,12 +89,20 @@ def __call__(self, circuit: Circuit): "The number of nodes of the connectivity graph must match " + "the number of qubits in the circuit", ) - return dict( - zip( - list("q" + str(i) for i in range(circuit.nqubits)), - range(circuit.nqubits), + trivial_layout = dict( + zip( + ["q" + str(i) for i in list(self.connectivity.nodes())], + range(circuit.nqubits), + ) ) - ) + else: + trivial_layout = dict( + zip( + ["q" + str(i) for i in range(circuit.nqubits)], + range(circuit.nqubits), + ) + ) + return trivial_layout class Custom(Placer): @@ -99,6 +115,8 @@ class Custom(Placer): to assign the physical qubits :math:`\\{0, 1, 2\\}` to the logical qubits :math:`[1, 2, 0]`. connectivity (networkx.Graph, optional): chip connectivity. + This argument is necessary if the layout applied to a subset of + qubits of the original connectivity graph. """ def __init__(self, map: Union[list, dict], connectivity: nx.Graph = None): @@ -117,15 +135,20 @@ def __call__(self, circuit=None): if isinstance(self.map, dict): pass elif isinstance(self.map, list): - self.map = dict( - zip(list("q" + str(i) for i in range(len(self.map))), self.map) - ) + if self.connectivity is not None: + self.map = dict( + zip(["q" + str(i) for i in self.connectivity.nodes()], self.map) + ) + else: + self.map = dict( + zip(["q" + str(i) for i in range(len(self.map))], self.map) + ) else: raise_error(TypeError, "Use dict or list to define mapping.") if circuit is not None: - assert_placement(circuit, self.map) + assert_placement(circuit, self.map, connectivity=self.connectivity) else: - assert_mapping_consistency(self.map) + assert_mapping_consistency(self.map, connectivity=self.connectivity) return self.map @@ -145,6 +168,7 @@ def __init__(self, connectivity: nx.Graph): def __call__(self, circuit: Circuit): """Find the initial layout of the given circuit using subgraph isomorphism. + Circuit must contain at least two two-qubit gates to implement subgraph placement. Args: circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. @@ -159,7 +183,7 @@ def __call__(self, circuit: Circuit): "Circuit must contain at least two two-qubit gates to implement subgraph placement.", ) circuit_subgraph = nx.Graph() - circuit_subgraph.add_nodes_from(range(self.connectivity.number_of_nodes())) + circuit_subgraph.add_nodes_from(list(range(circuit.nqubits))) matcher = nx.algorithms.isomorphism.GraphMatcher( self.connectivity, circuit_subgraph ) @@ -180,7 +204,11 @@ def __call__(self, circuit: Circuit): or i == len(gates_qubits_pairs) - 1 ): break - return {"q" + str(i): result.mapping[i] for i in range(len(result.mapping))} + + sorted_result = dict(sorted(result.mapping.items())) + return dict( + zip(["q" + str(i) for i in sorted_result.keys()], sorted_result.values()) + ) class Random(Placer): @@ -210,23 +238,21 @@ def __call__(self, circuit): gates_qubits_pairs = _find_gates_qubits_pairs(circuit) nodes = self.connectivity.number_of_nodes() keys = list(self.connectivity.nodes()) + dict_keys = ["q" + str(i) for i in keys] final_mapping = dict(zip(keys, range(nodes))) final_graph = nx.relabel_nodes(self.connectivity, final_mapping) - final_mapping = { - "q" + str(i): final_mapping[i] for i in range(len(final_mapping)) - } final_cost = self._cost(final_graph, gates_qubits_pairs) for _ in range(self.samples): mapping = dict(zip(keys, random.sample(range(nodes), nodes))) graph = nx.relabel_nodes(self.connectivity, mapping) cost = self._cost(graph, gates_qubits_pairs) if cost == 0: - return {"q" + str(i): mapping[i] for i in range(len(mapping))} + return dict(zip(dict_keys, list(mapping.values()))) if cost < final_cost: final_graph = graph - final_mapping = {"q" + str(i): mapping[i] for i in range(len(mapping))} + final_mapping = {dict_keys[i]: mapping[i] for i in range(len(mapping))} final_cost = cost - return final_mapping + return dict(zip(dict_keys, list(final_mapping.values()))) def _cost(self, graph: nx.Graph, gates_qubits_pairs: list): """ diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index f9ff7dc28a..e3180d84bf 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -34,32 +34,6 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): ) -def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): - """Restrict the connectivity to selected qubits. - - Args: - connectivity (:class:`networkx.Graph`): chip connectivity. - qubits (list): list of physical qubits to be used. - - Returns: - (:class:`networkx.Graph`): restricted connectivity. - """ - if not set(qubits).issubset(set(connectivity.nodes)): - raise_error( - ConnectivityError, "Some qubits are not in the original connectivity." - ) - new_connectivity = nx.Graph() - new_connectivity.add_nodes_from(qubits) - new_edges = [] - for edge in connectivity.edges: - if edge[0] in qubits and edge[1] in qubits: - new_edges.append(edge) - new_connectivity.add_edges_from(new_edges) - if not nx.is_connected(new_connectivity): - raise_error(ConnectivityError, "The new connectivity graph is not connected.") - return new_connectivity - - # TODO: make this class work with CircuitMap class ShortestPaths(Router): """A class to perform initial qubit mapping and connectivity matching. @@ -101,6 +75,7 @@ def __call__(self, circuit: Circuit, initial_layout: dict): (:class:`qibo.models.circuit.Circuit`, dict): circut mapped to hardware topology, and final qubit mapping. """ self._mapping = initial_layout + dict_keys = list(initial_layout.keys()) init_qubit_map = np.asarray(list(initial_layout.values())) self._initial_checks(circuit.nqubits) self._gates_qubits_pairs = _find_gates_qubits_pairs(circuit) @@ -114,7 +89,7 @@ def __call__(self, circuit: Circuit, initial_layout: dict): self._transpiler_step(circuit) hardware_mapped_circuit = self._remap_circuit(np.argsort(init_qubit_map)) final_mapping = { - "q" + str(j): self._swap_map[j] + dict_keys[j]: self._swap_map[j] for j in range(self._graph.number_of_nodes()) } diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index 1c72b3eeaa..bc82956485 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -6,10 +6,12 @@ from qibo.models import Circuit from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.pipeline import ( + ConnectivityError, Passes, TranspilerPipelineError, assert_circuit_equivalence, assert_transpiling, + restrict_connectivity_qubits, ) from qibo.transpiler.placer import Random, ReverseTraversal, Trivial from qibo.transpiler.router import ShortestPaths @@ -68,10 +70,28 @@ def star_connectivity(): return chip +def test_restrict_qubits_error_no_subset(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 2, 6]) + assert "Some qubits are not in the original connectivity." in str(excinfo.value) + + +def test_restrict_qubits_error_not_connected(): + with pytest.raises(ConnectivityError) as excinfo: + restrict_connectivity_qubits(star_connectivity(), [1, 3]) + assert "The new connectivity graph is not connected." in str(excinfo.value) + + +def test_restrict_qubits(): + new_connectivity = restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]) + assert list(new_connectivity.nodes) == [1, 2, 3] + assert list(new_connectivity.edges) == [(1, 2), (2, 3)] + + @pytest.mark.parametrize("ngates", [5, 10, 50]) def test_pipeline_default(ngates): circ = generate_random_circuit(nqubits=5, ngates=ngates) - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) transpiled_circ, final_layout = default_transpiler(circ) initial_layout = default_transpiler.get_initial_layout() assert_transpiling( @@ -127,11 +147,11 @@ def test_assert_circuit_equivalence_wrong_nqubits(): def test_error_connectivity(): with pytest.raises(TranspilerPipelineError): - default_transpiler = Passes() + default_transpiler = Passes(passes=None, connectivity=None) def test_is_satisfied(): - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) circuit = Circuit(5) circuit.add(gates.CZ(0, 2)) circuit.add(gates.Z(0)) @@ -139,7 +159,7 @@ def test_is_satisfied(): def test_is_satisfied_false_decomposition(): - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) circuit = Circuit(5) circuit.add(gates.CZ(0, 2)) circuit.add(gates.X(0)) @@ -147,7 +167,7 @@ def test_is_satisfied_false_decomposition(): def test_is_satisfied_false_connectivity(): - default_transpiler = Passes(connectivity=star_connectivity()) + default_transpiler = Passes(passes=None, connectivity=star_connectivity()) circuit = Circuit(5) circuit.add(gates.CZ(0, 1)) circuit.add(gates.Z(0)) @@ -241,7 +261,7 @@ def test_custom_passes_no_placer(): def test_custom_passes_wrong_pass(): custom_passes = [0] - custom_pipeline = Passes(passes=custom_passes) + custom_pipeline = Passes(passes=custom_passes, connectivity=None) circ = generate_random_circuit(nqubits=5, ngates=5) with pytest.raises(TranspilerPipelineError): transpiled_circ, final_layout = custom_pipeline(circ) diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index 748d469f0b..ae34bcec1e 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -3,6 +3,7 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler.pipeline import restrict_connectivity_qubits from qibo.transpiler.placer import ( Custom, PlacementError, @@ -53,7 +54,7 @@ def test_assert_placement_false(qubits, layout): assert_placement(circuit, layout) -def test_mapping_consistency_true(): +def test_mapping_consistency(): layout = {"q0": 0, "q1": 2, "q2": 1, "q3": 4, "q4": 3} assert_mapping_consistency(layout) @@ -65,11 +66,32 @@ def test_mapping_consistency_true(): {"q0": 0, "q1": 2, "q0": 1, "q3": 4, "q4": 3}, ], ) -def test_mapping_consistency_false(layout): +def test_mapping_consistency_error(layout): with pytest.raises(PlacementError): assert_mapping_consistency(layout) +def test_mapping_consistency_restricted(): + layout = {"q0": 0, "q2": 1} + connectivity = star_connectivity() + restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) + assert_mapping_consistency(layout, restricted_connecyivity) + + +@pytest.mark.parametrize( + "layout", + [ + {"q0": 0, "q2": 2}, + {"q0": 0, "q1": 1}, + ], +) +def test_mapping_consistency_restricted_error(layout): + connectivity = star_connectivity() + restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) + with pytest.raises(PlacementError): + assert_mapping_consistency(layout, restricted_connecyivity) + + def test_trivial(): circuit = Circuit(5) connectivity = star_connectivity() @@ -79,6 +101,18 @@ def test_trivial(): assert_placement(circuit, layout) +def test_trivial_restricted(): + circuit = Circuit(2) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) + placer = Trivial(connectivity=restricted_connectivity) + layout = placer(circuit) + assert layout == {"q0": 0, "q2": 1} + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + def test_trivial_error(): circuit = Circuit(4) connectivity = star_connectivity() @@ -102,6 +136,19 @@ def test_custom(custom_layout, give_circuit): assert layout == {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0} +@pytest.mark.parametrize("custom_layout", [[1, 0], {"q0": 1, "q2": 0}]) +def test_custom_restricted(custom_layout): + circuit = Circuit(2) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) + placer = Custom(connectivity=restricted_connectivity, map=custom_layout) + layout = placer(circuit) + assert layout == {"q0": 1, "q2": 0} + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + def test_custom_error_circuit(): circuit = Circuit(3) custom_layout = [4, 3, 2, 1, 0] @@ -165,6 +212,23 @@ def test_subgraph_error(): layout = placer(circuit) +def test_subgraph_restricted(): + circuit = Circuit(4) + circuit.add(gates.CNOT(0, 3)) + circuit.add(gates.CNOT(0, 1)) + circuit.add(gates.CNOT(3, 2)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.CNOT(3, 1)) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4]) + placer = Subgraph(connectivity=restricted_connectivity) + layout = placer(circuit) + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + @pytest.mark.parametrize("reps", [1, 10, 100]) def test_random(reps): connectivity = star_connectivity() @@ -182,6 +246,23 @@ def test_random_perfect(): assert_placement(star_circuit(), layout) +def test_random_restricted(): + circuit = Circuit(4) + circuit.add(gates.CNOT(1, 3)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(3, 2)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.CNOT(3, 1)) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4]) + placer = Random(connectivity=restricted_connectivity, samples=100) + layout = placer(circuit) + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) + + @pytest.mark.parametrize("gates", [None, 5, 13]) def test_reverse_traversal(gates): circuit = star_circuit() @@ -199,3 +280,23 @@ def test_reverse_traversal_no_gates(): circuit = Circuit(5) with pytest.raises(ValueError): layout = placer(circuit) + + +def test_reverse_traversal_restricted(): + circuit = Circuit(4) + circuit.add(gates.CNOT(1, 3)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(3, 2)) + circuit.add(gates.CNOT(2, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.CNOT(3, 1)) + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3, 4]) + routing = ShortestPaths(connectivity=restricted_connectivity) + placer = ReverseTraversal( + connectivity=restricted_connectivity, routing_algorithm=routing, depth=5 + ) + layout = placer(circuit) + assert_placement( + circuit=circuit, layout=layout, connectivity=restricted_connectivity + ) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index ea4ca73275..621f7f69e3 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -14,7 +14,6 @@ ShortestPaths, _find_gates_qubits_pairs, assert_connectivity, - restrict_connectivity_qubits, ) @@ -80,24 +79,6 @@ def matched_circuit(): return circuit -def test_restrict_qubits_error_no_subset(): - with pytest.raises(ConnectivityError) as excinfo: - restrict_connectivity_qubits(star_connectivity(), [1, 2, 6]) - assert "Some qubits are not in the original connectivity." in str(excinfo.value) - - -def test_restrict_qubits_error_not_connected(): - with pytest.raises(ConnectivityError) as excinfo: - restrict_connectivity_qubits(star_connectivity(), [1, 3]) - assert "The new connectivity graph is not connected." in str(excinfo.value) - - -def test_restrict_qubits(): - new_connectivity = restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]) - assert list(new_connectivity.nodes) == [1, 2, 3] - assert list(new_connectivity.edges) == [(1, 2), (2, 3)] - - def test_assert_connectivity(): assert_connectivity(star_connectivity(), matched_circuit()) From c238ad9ea44ffcf3eefef2bbb82238bc358092d8 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:46:31 +0100 Subject: [PATCH 18/78] get_gammas --- src/qibo/models/__init__.py | 2 +- src/qibo/models/error_mitigation.py | 37 ++++++++++++++++------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/qibo/models/__init__.py b/src/qibo/models/__init__.py index 0adff068c3..f0d07da334 100644 --- a/src/qibo/models/__init__.py +++ b/src/qibo/models/__init__.py @@ -5,7 +5,7 @@ ZNE, get_gammas, get_noisy_circuit, - sample_training_circuit, + sample_training_circuit_cdr, vnCDR, ) from qibo.models.evolution import AdiabaticEvolution, StateEvolution diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index ceff367882..832dca5a25 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -10,42 +10,45 @@ from qibo.config import raise_error -def get_gammas(c, solve: bool = True): +def get_gammas(noise_levels, analytical: bool = True): """Standalone function to compute the ZNE coefficients given the noise levels. Args: - c (numpy.ndarray): array containing the different noise levels. + noise_levels (numpy.ndarray): array containing the different noise levels. Note that in the CNOT insertion paradigm this corresponds to the number of CNOT pairs to be inserted. The canonical ZNE noise levels are obtained as ``2 * c + 1``. - solve (bool, optional): If ``True``, computes the coeffients by solving the + analytical (bool, optional): if ``True``, computes the coeffients by solving the linear system. If ``False``, use the analytical solution valid for the CNOT insertion method. Default is ``True``. Returns: - numpy.ndarray: The computed coefficients. + numpy.ndarray: the computed coefficients. """ - if solve: - c = 2 * c + 1 - a = np.array([c**i for i in range(len(c))]) - b = np.zeros(len(c)) - b[0] = 1 - gammas = np.linalg.solve(a, b) + if analytical: + noise_levels = 2 * noise_levels + 1 + a_matrix = np.array([noise_levels**i for i in range(len(noise_levels))]) + b_vector = np.zeros(len(noise_levels)) + b_vector[0] = 1 + zne_coefficients = np.linalg.solve(a_matrix, b_vector) else: - cmax = c[-1] - gammas = np.array( + max_noise_level = noise_levels[-1] + zne_coefficients = np.array( [ 1 - / (2 ** (2 * cmax) * np.math.factorial(i)) + / (2 ** (2 * max_noise_level) * np.math.factorial(i)) * (-1) ** i / (1 + 2 * i) - * np.math.factorial(1 + 2 * cmax) - / (np.math.factorial(cmax) * np.math.factorial(cmax - i)) - for i in c + * np.math.factorial(1 + 2 * max_noise_level) + / ( + np.math.factorial(max_noise_level) + * np.math.factorial(max_noise_level - i) + ) + for i in noise_levels ] ) - return gammas + return zne_coefficients def get_noisy_circuit(circuit, num_insertions: int, insertion_gate: str = "CNOT"): From b57184ef32ede094ccd45a7b907671dbc0329596 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:47:46 +0100 Subject: [PATCH 19/78] get_noisy_circuit --- src/qibo/models/error_mitigation.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 832dca5a25..7f5ca9f1e2 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -62,7 +62,7 @@ def get_noisy_circuit(circuit, num_insertions: int, insertion_gate: str = "CNOT" Default is ``"CNOT"``. Returns: - :class:`qibo.models.Circuit`: The circuit with the inserted CNOT pairs. + :class:`qibo.models.Circuit`: circuit with the inserted gate pairs. """ if insertion_gate not in ("CNOT", "RX"): # pragma: no cover raise_error( @@ -83,16 +83,17 @@ def get_noisy_circuit(circuit, num_insertions: int, insertion_gate: str = "CNOT" for gate in circuit.queue: noisy_circuit.add(gate) + if isinstance(gate, i_gate): if insertion_gate == "CNOT": control = gate.control_qubits[0] target = gate.target_qubits[0] - for i in range(num_insertions): + for _ in range(num_insertions): noisy_circuit.add(gates.CNOT(control, target)) noisy_circuit.add(gates.CNOT(control, target)) elif gate.init_kwargs["theta"] == theta: qubit = gate.qubits[0] - for i in range(num_insertions): + for _ in range(num_insertions): noisy_circuit.add(gates.RX(qubit, theta=theta)) noisy_circuit.add(gates.RX(qubit, theta=-theta)) From 7fc65603e864eb1c7c25e08936936b5fbb892c09 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:48:30 +0100 Subject: [PATCH 20/78] zero noise extrapolation --- src/qibo/models/error_mitigation.py | 36 +++++++++++++++++------------ 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 7f5ca9f1e2..c899ac0745 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -123,26 +123,32 @@ def ZNE( noise_levels (numpy.ndarray): Sequence of noise levels. noise_model (:class:`qibo.noise.NoiseModel`, optional): Noise model applied to simulate noisy computation. - nshots (int, optional): Number of shots. + nshots (int, optional): Number of shots. Defauylts to 10000. solve_for_gammas (bool, optional): If ``True``, explicitly solve the - equations to obtain the ``gamma`` coefficients. + equations to obtain the ``gamma`` coefficients. Default is ``False``. insertion_gate (str, optional): gate to be used in the insertion. If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``. - Default is ``"CNOT"``. - readout (dict, optional): It has the structure - {'calibration_matrix': `numpy.ndarray`, 'ncircuits': `int`}. - If passed, the calibration matrix or the randomized method is - used to mitigate readout errors. - backend (:class:`qibo.backends.abstract.Backend`, optional): Calculation engine. + Defaults to ``"CNOT"``. + readout (dict, optional): A dictionary that may contain the following keys: + - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + If provided, the corresponding readout error mitigation method is used. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Returns: numpy.ndarray: Estimate of the expected value of ``observable`` in the noise free condition. - """ + Reference: + 1. K. Temme, S. Bravyi et al, *Error mitigation for short-depth quantum circuits*. + `arXiv:1612.02058 [quant-ph] `_. + """ if backend is None: # pragma: no cover backend = GlobalBackend() - expected_val = [] + expected_values = [] for num_insertions in noise_levels: noisy_circuit = get_noisy_circuit( circuit, num_insertions, insertion_gate=insertion_gate @@ -156,17 +162,17 @@ def ZNE( noisy_circuit = noise_model.apply(noisy_circuit) circuit_result = backend.execute_circuit(noisy_circuit, nshots=nshots) if "calibration_matrix" in readout.keys() is not None: - circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"] + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, readout["calibration_matrix"], readout["ibu_iters"] ) val = circuit_result.expectation_from_samples(observable) if "ncircuits" in readout.keys(): val /= circuit_result_cal.expectation_from_samples(observable) - expected_val.append(val) + expected_values.append(val) - gamma = get_gammas(noise_levels, solve=solve_for_gammas) + gamma = get_gammas(noise_levels, analytical=solve_for_gammas) - return np.sum(gamma * expected_val) + return np.sum(gamma * expected_values) def sample_training_circuit( From cf792bffd172d625adb3d878db850f42866a7986 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:49:46 +0100 Subject: [PATCH 21/78] clifford data regression --- src/qibo/models/error_mitigation.py | 84 ++++++++++++++--------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index c899ac0745..65ab5bf1dc 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -175,7 +175,7 @@ def ZNE( return np.sum(gamma * expected_values) -def sample_training_circuit( +def sample_training_circuit_cdr( circuit, replacement_gates: list = None, sigma: float = 0.5, @@ -191,7 +191,9 @@ def sample_training_circuit( form (``gates.XYZ``, ``kwargs``). For example, phase gates are used by default: ``list((RZ, {'theta':0}), (RZ, {'theta':pi/2}), (RZ, {'theta':pi}), (RZ, {'theta':3*pi/2}))``. sigma (float, optional): standard devation of the Gaussian distribution used for sampling. - backend (:class:`qibo.backends.abstract.Backend`, optional): Calculation engine. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Returns: :class:`qibo.models.Circuit`: The sampled circuit. @@ -202,18 +204,15 @@ def sample_training_circuit( if replacement_gates is None: replacement_gates = [(gates.RZ, {"theta": n * np.pi / 2}) for n in range(4)] - # Find all the non-Clifford RZ gates gates_to_replace = [] for i, gate in enumerate(circuit.queue): if isinstance(gate, gates.RZ): if gate.init_kwargs["theta"] % (np.pi / 2) != 0.0: gates_to_replace.append((i, gate)) - if len(gates_to_replace) == 0: + if not gates_to_replace: raise_error(ValueError, "No non-Clifford RZ gate found, no circuit sampled.") - # For each RZ gate build the possible candidates and - # compute the frobenius distance to the candidates replacement, distance = [], [] for _, gate in gates_to_replace: rep_gates = np.array( @@ -231,31 +230,28 @@ def sample_training_circuit( ) distance = np.vstack(distance) - # Compute the scores prob = np.exp(-(distance**2) / sigma**2) - # Sample which of the RZ found to substitute + index = np.random.choice( range(len(gates_to_replace)), size=min(int(len(gates_to_replace) / 2), 50), replace=False, p=prob.sum(-1) / prob.sum(), ) + gates_to_replace = np.array([gates_to_replace[i] for i in index]) prob = [prob[i] for i in index] - # Sample which replacement gate to substitute with + replacement = np.array([replacement[i] for i in index]) replacement = [ replacement[i][np.random.choice(range(len(p)), size=1, p=p / p.sum())[0]] for i, p in enumerate(prob) ] replacement = {i[0]: g for i, g in zip(gates_to_replace, replacement)} - # Build the training circuit by substituting the sampled gates + sampled_circuit = circuit.__class__(**circuit.init_kwargs) for i, gate in enumerate(circuit.queue): - if i in replacement.keys(): - sampled_circuit.add(replacement[i]) - else: - sampled_circuit.add(gate) + sampled_circuit.add(replacement.get(i, gate)) return sampled_circuit @@ -279,57 +275,62 @@ def CDR( observable (numpy.ndarray): observable to be measured. noise_model (:class:`qibo.noise.NoiseModel`): noise model used for simulating noisy computation. - nshots (int, optional): number of shots. + nshots (int, optional): number of shots. Defaults 10000. model (callable, optional): model used for fitting. This should be a callable function object ``f(x, *params)``, taking as input the predictor variable and the parameters. Default is a simple linear model ``f(x,a,b) := a*x + b``. - n_training_samples (int, optional): number of training circuits to sample. + n_training_samples (int, optional): number of training circuits to sample. Defaults to 100. full_output (bool, optional): if ``True``, this function returns additional - information: ``val``, ``optimal_params``, ``train_val``. - readout (dict, optional): It has the structure - {'calibration_matrix': `numpy.ndarray`, 'ncircuits': `int`}. - If passed, the calibration matrix or the randomized method is - used to mitigate readout errors. - backend (:class:`qibo.backends.abstract.Backend`, optional): calculation engine. + information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``. + readout (dict, optional): A dictionary that may contain the following keys: + - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + If provided, the corresponding readout error mitigation method is used. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Returns: mit_val (float): Mitigated expectation value of `observable`. val (float): Noisy expectation value of `observable`. optimal_params (list): Optimal values for `params`. train_val (dict): Contains the noise-free and noisy expectation values obtained with the training circuits. - """ - # Set backend + Reference: + 1. P. Czarnik, A. Arrasmith et al, *Error mitigation with Clifford quantum-circuit data*. + `arXiv:2005.10189 [quant-ph] `_. + """ if backend is None: # pragma: no cover backend = GlobalBackend() - # Sample the training set + training_circuits = [ - sample_training_circuit(circuit) for n in range(n_training_samples) + sample_training_circuit_cdr(circuit) for _ in range(n_training_samples) ] - # Run the sampled circuits + train_val = {"noise-free": [], "noisy": []} - for c in training_circuits: - val = c(nshots=nshots).expectation_from_samples(observable) + for circ in training_circuits: + val = circ(nshots=nshots).expectation_from_samples(observable) train_val["noise-free"].append(val) if "ncircuits" in readout.keys(): circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( - c, noise_model, nshots, readout["ncircuits"], backend + circ, noise_model, nshots, readout["ncircuits"], backend ) else: if noise_model is not None and backend.name != "qibolab": - c = noise_model.apply(c) - circuit_result = backend.execute_circuit(c, nshots=nshots) + circ = noise_model.apply(circ) + circuit_result = backend.execute_circuit(circ, nshots=nshots) if "calibration_matrix" in readout.keys() is not None: - circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"] + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, readout["calibration_matrix"], readout["ibu_iters"] ) val = circuit_result.expectation_from_samples(observable) if "ncircuits" in readout.keys(): val /= circuit_result_cal.expectation_from_samples(observable) train_val["noisy"].append(val) - # Fit the model + optimal_params = curve_fit(model, train_val["noisy"], train_val["noise-free"])[0] - # Run the input circuit + if "ncircuits" in readout.keys(): circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( circuit, noise_model, nshots, readout["ncircuits"], backend @@ -339,19 +340,18 @@ def CDR( circuit = noise_model.apply(circuit) circuit_result = backend.execute_circuit(circuit, nshots=nshots) if "calibration_matrix" in readout.keys() is not None: - circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"] + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, readout["calibration_matrix"], readout["ibu_iters"] ) val = circuit_result.expectation_from_samples(observable) if "ncircuits" in readout.keys(): val /= circuit_result_cal.expectation_from_samples(observable) mit_val = model(val, *optimal_params) - # Return data - if full_output == True: + if full_output is True: return mit_val, val, optimal_params, train_val - else: - return mit_val + + return mit_val def vnCDR( From d4414acf1e136ce6bbc905777c6a476f7649f9b4 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:50:12 +0100 Subject: [PATCH 22/78] vnCDR --- src/qibo/models/error_mitigation.py | 45 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 65ab5bf1dc..eb533a00aa 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -385,12 +385,15 @@ def vnCDR( If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``. Default is ``"CNOT"``. full_output (bool, optional): if ``True``, this function returns additional - information: ``val``, ``optimal_params``, ``train_val``. - readout (dict, optional): It has the structure - {'calibration_matrix': `numpy.ndarray`, 'ncircuits': `int`}. - If passed, the calibration matrix or the randomized method is - used to mitigate readout errors. - backend (:class:`qibo.backends.abstract.Backend`, optional): calculation engine. + information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``. + readout (dict, optional): A dictionary that may contain the following keys: + - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + If provided, the corresponding readout error mitigation method is used. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Returns: mit_val (float): Mitigated expectation value of `observable`. @@ -398,24 +401,24 @@ def vnCDR( optimal_params (list): Optimal values for `params`. train_val (dict): Contains the noise-free and noisy expectation values obtained with the training circuits. - """ - # Set backend + Reference: + 1. A. Lowe, MH. Gordon et al, *Unified approach to data-driven quantum error mitigation*. + `arXiv:2011.01157 [quant-ph] `_. + """ if backend is None: # pragma: no cover backend = GlobalBackend() - # Sample the training circuits training_circuits = [ - sample_training_circuit(circuit) for n in range(n_training_samples) + sample_training_circuit_cdr(circuit) for _ in range(n_training_samples) ] train_val = {"noise-free": [], "noisy": []} - # Add the different noise levels and run the circuits - for c in training_circuits: - val = c(nshots=nshots).expectation_from_samples(observable) + for circ in training_circuits: + val = circ(nshots=nshots).expectation_from_samples(observable) train_val["noise-free"].append(val) for level in noise_levels: - noisy_c = get_noisy_circuit(c, level, insertion_gate=insertion_gate) + noisy_c = get_noisy_circuit(circ, level, insertion_gate=insertion_gate) if "ncircuits" in readout.keys(): ( circuit_result, @@ -428,22 +431,19 @@ def vnCDR( noisy_c = noise_model.apply(noisy_c) circuit_result = backend.execute_circuit(noisy_c, nshots=nshots) if "calibration_matrix" in readout.keys(): - circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"] + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, readout["calibration_matrix"], readout["ibu_iters"] ) val = circuit_result.expectation_from_samples(observable) if "ncircuits" in readout.keys(): val /= circuit_result_cal.expectation_from_samples(observable) train_val["noisy"].append(val) - # Repeat noise-free values for each noise level noisy_array = np.array(train_val["noisy"]).reshape(-1, len(noise_levels)) - # Fit the model params = np.random.rand(len(noise_levels)) optimal_params = curve_fit(model, noisy_array.T, train_val["noise-free"], p0=params) - # Run the input circuit val = [] for level in noise_levels: noisy_c = get_noisy_circuit(circuit, level, insertion_gate=insertion_gate) @@ -456,8 +456,8 @@ def vnCDR( noisy_c = noise_model.apply(noisy_c) circuit_result = backend.execute_circuit(noisy_c, nshots=nshots) if "calibration_matrix" in readout.keys(): - circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"] + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, readout["calibration_matrix"], readout["ibu_iters"] ) expval = circuit_result.expectation_from_samples(observable) if "ncircuits" in readout.keys(): @@ -466,8 +466,7 @@ def vnCDR( mit_val = model(np.array(val).reshape(-1, 1), *optimal_params[0])[0] - # Return data - if full_output == True: + if full_output is True: return mit_val, val, optimal_params, train_val return mit_val From 72e5ef8e4aaef95837e1a742a7fbd3403774e028 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:51:57 +0100 Subject: [PATCH 23/78] readout mitigation --- src/qibo/models/error_mitigation.py | 148 +++++++++++++++++----------- 1 file changed, 90 insertions(+), 58 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index eb533a00aa..a202d8ef95 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -472,17 +472,35 @@ def vnCDR( return mit_val -def ibu(prob, mat, iters=10): - """Iterative Bayesian Unfolding.""" - t = np.ones((len(prob), 1)) / len(prob) - for k in range(iters): - t = t * (np.transpose(mat) @ (prob / (mat @ t))) +def iterative_bayesian_unfolding(probabilities, response_matrix, iterations=10): + """ + Iterative Bayesian Unfolding (IBU) method for readout mitigation. + + Args: + probabilities (numpy.ndarray): the input probabilities to be unfolded. + response_matrix (numpy.ndarray): the response matrix. + iterations (int, optional): the number of iterations to perform. Defaults to 10. + + Returns: + numpy.ndarray: the unfolded probabilities. - return t + Reference: + 1. S. Srinivasan, B. Pokharel et al, *Scalable Measurement Error Mitigation via Iterative Bayesian Unfolding*. + `arXiv:2210.12284 [quant-ph] `_. + """ + unfolded_probabilities = np.ones((len(probabilities), 1)) / len(probabilities) + for _ in range(iterations): + unfolded_probabilities = unfolded_probabilities * ( + np.transpose(response_matrix) + @ (probabilities / (response_matrix @ unfolded_probabilities)) + ) -def calibration_matrix( - nqubits, qubit_map, inv=False, noise_model=None, nshots: int = 1000, backend=None + return unfolded_probabilities + + +def get_calibration_matrix( + nqubits, qubit_map, noise_model=None, nshots: int = 10000, backend=None ): """Computes the calibration matrix for readout mitigation. @@ -491,76 +509,82 @@ def calibration_matrix( noise_model (:class:`qibo.noise.NoiseModel`, optional): noise model used for simulating noisy computation. This matrix can be used to mitigate the effect of `qibo.noise.ReadoutError`. - nshots (int, optional): number of shots. - backend (:class:`qibo.backends.abstract.Backend`, optional): calculation engine. + nshots (int, optional): number of shots. Defaults to 10000. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Returns: numpy.ndarray : The computed (`nqubits`, `nqubits`) calibration matrix for readout mitigation. """ - from qibo import Circuit # pylint: disable=import-outside-toplevel if backend is None: # pragma: no cover backend = GlobalBackend() - matrix = np.zeros((2**nqubits, 2**nqubits)) - from qibo.config import log + calibration_matrix = np.zeros((2**nqubits, 2**nqubits)) for i in range(2**nqubits): - state = format(i, f"0{nqubits}b") + binary_state = format(i, f"0{nqubits}b") circuit = Circuit(nqubits, density_matrix=True) - for q, bit in enumerate(state): + for qubit, bit in enumerate(binary_state): if bit == "1": - circuit.add(gates.X(q)) + circuit.add(gates.X(qubit)) circuit.add(gates.M(*range(nqubits))) if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) circuit = transpile_circ(circuit, qubit_map, backend) - freq = backend.execute_circuit(circuit, nshots=nshots).frequencies() - column = np.zeros(2**nqubits) - for key in freq.keys(): - f = freq[key] / nshots - column[int(key, 2)] = f - matrix[:, i] = column - if inv == True: - matrix = np.linalg.inv(matrix) + frequencies = backend.execute_circuit(circuit, nshots=nshots).frequencies() + + column = np.zeros(2**nqubits) + for key, value in frequencies.items(): + column[int(key, 2)] = value / nshots + calibration_matrix[:, i] = column - return matrix + return calibration_matrix -def apply_readout_mitigation(state, calibration_matrix, inv=False): - """Updates the frequencies of the input state with the mitigated ones obtained with - ``calibration_matrix * state.frequencies()``. +def apply_cal_mat_readout_mitigation(state, calibration_matrix, iterations=None): + """ + Applies readout error mitigation to the given state using the provided calibration matrix. Args: - state (:class:`qibo.states.CircuitResult`): input state to be updated. - calibration_matrix (numpy.ndarray): calibration matrix for readout mitigation. + state (:class:`qibo.states.CircuitResult`): the input state to be updated. This state should contain the + frequencies that need to be mitigated. + calibration_matrix (numpy.ndarray, optional): the calibration matrix for readout mitigation. + iterations (int, optional): the number of iterations to use for the Iterative Bayesian Unfolding method. + If ``None`` the 'inverse' method is used. Defaults to ``None``. Returns: - :class:`qibo.states.CircuitResult`: the input state with the updated frequencies. + :class:`qibo.states.CircuitResult`: The input state with the updated (mitigated) frequencies. """ - from qibo.config import log + frequencies = np.zeros(2 ** len(state.measurements[0].qubits)) + for key, value in state.frequencies().items(): + frequencies[int(key, 2)] = value - freq = np.zeros(2 ** len(state.measurements)) - for k, v in state.frequencies().items(): - freq[int(k, 2)] = v + frequencies = frequencies.reshape(-1, 1) - freq = freq.reshape(-1, 1) - - if inv: - for i, val in enumerate(calibration_matrix @ freq): - state._frequencies[i] = float(val) + if iterations is None: + calibration_matrix = np.linalg.inv(calibration_matrix) + for i, value in enumerate(calibration_matrix @ frequencies): + state._frequencies[i] = float(value) else: - prob_mit = ibu(freq / np.sum(freq), calibration_matrix) - freq_mit = np.round(prob_mit * np.sum(freq), 0) - freq_mit = (freq_mit / np.sum(freq_mit)) * np.sum(freq) + mitigated_probabilities = iterative_bayesian_unfolding( + frequencies / np.sum(frequencies), calibration_matrix, iterations + ) + mitigated_frequencies = np.round( + mitigated_probabilities * np.sum(frequencies), 0 + ) + mitigated_frequencies = ( + mitigated_frequencies / np.sum(mitigated_frequencies) + ) * np.sum(frequencies) - for i, val in enumerate(freq_mit): - state._frequencies[i] = float(freq_mit[i]) + for i, value in enumerate(mitigated_frequencies): + state._frequencies[i] = float(value) return state @@ -568,22 +592,27 @@ def apply_readout_mitigation(state, calibration_matrix, inv=False): def apply_randomized_readout_mitigation( circuit, noise_model=None, nshots: int = int(1e3), ncircuits: int = 10, backend=None ): - """Implements the readout mitigation method proposed in https://arxiv.org/abs/2012.09738. + """Readout mitigation method that transforms the bias in an expectation value into a measurable multiplicative factor. This factor can be eliminated at the expense of increased sampling complexity for the observable. Args: circuit (:class:`qibo.models.Circuit`): input circuit. noise_model(:class:`qibo.noise.NoiseModel`, optional): noise model used for - simulating noisy computation. This matrix can be used to mitigate the - effects of :class:`qibo.noise.ReadoutError`. - nshots (int, optional): number of shots. + simulating noisy computation. Defaults to ``None``. + nshots (int, optional): number of shots. Defaults to 10000. ncircuits (int, optional): number of randomized circuits. Each of them uses - ``int(nshots / ncircuits)`` shots. - backend (:class:`qibo.backends.abstract.Backend`): calculation engine. + ``int(nshots / ncircuits)`` shots. Defaults to 10. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Return: :class:`qibo.states.CircuitResult`: the state of the input circuit with mitigated frequencies. + + Reference: + 1. Ewout van den Berg, Zlatko K. Minev et al, *Model-free readout-error mitigation for quantum expectation values*. + `arXiv:2012.09738 [quant-ph] `_. """ from qibo import Circuit # pylint: disable=import-outside-toplevel from qibo.quantum_info import ( # pylint: disable=import-outside-toplevel @@ -593,7 +622,7 @@ def apply_randomized_readout_mitigation( if backend is None: # pragma: no cover backend = GlobalBackend() - qubits = circuit.queue[-1].qubits + meas_qubits = circuit.measurements[0].qubits nshots_r = int(nshots / ncircuits) freq = np.zeros((ncircuits, 2), object) for k in range(ncircuits): @@ -601,19 +630,22 @@ def apply_randomized_readout_mitigation( circuit_c.queue.pop() cal_circuit = Circuit(circuit.nqubits, density_matrix=True) - x_gate = random_pauli(len(qubits), 1, subset=["I", "X"]).queue + x_gate = random_pauli(circuit.nqubits, 1, subset=["I", "X"]).queue error_map = {} - for gate in x_gate: + for j, gate in enumerate(x_gate): if gate.name == "x": - error_map[gate.qubits[0]] = 1 + if gate.qubits[0] in meas_qubits: + error_map[gate.qubits[0]] = 1 + else: + x_gate.queue[j] = gates.I(gate.qubits[0]) circuits = [circuit_c, cal_circuit] results = [] freqs = [] for circ in circuits: circ.add(x_gate) - circ.add(gates.M(*qubits)) + circ.add(gates.M(*meas_qubits)) if noise_model is not None and backend.name != "qibolab": circ = noise_model.apply(circ) result = backend.execute_circuit(circ, nshots=nshots_r) @@ -625,8 +657,8 @@ def apply_randomized_readout_mitigation( for j in range(2): results[j].nshots = nshots freq_sum = freq[0, j] - for f in freq[1::, j]: - freq_sum += f + for frs in freq[1::, j]: + freq_sum += frs results[j]._frequencies = freq_sum return results From aa7efb1b6814f80e25b9fdc8b890d03e91535d03 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:52:38 +0100 Subject: [PATCH 24/78] importance clifford sampling --- src/qibo/models/error_mitigation.py | 280 ++++++++++++++++------------ 1 file changed, 161 insertions(+), 119 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index a202d8ef95..60f6a67433 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -678,31 +678,33 @@ def sample_clifford_training_circuit( form (``gates.XYZ``, ``kwargs``). For example, phase gates are used by default: ``list((RZ, {'theta':0}), (RZ, {'theta':pi/2}), (RZ, {'theta':pi}), (RZ, {'theta':3*pi/2}))``. sigma (float, optional): standard devation of the Gaussian distribution used for sampling. - backend (:class:`qibo.backends.abstract.Backend`, optional): Calculation engine. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. Returns: :class:`qibo.models.Circuit`: The sampled circuit. """ - from qibo.quantum_info import random_clifford + from qibo.quantum_info import ( # pylint: disable=import-outside-toplevel + random_clifford, + ) if backend is None: # pragma: no cover backend = GlobalBackend() - # Find all the non-Clifford gates - gates_to_replace = [] - for i, gate in enumerate(circuit.queue): - if gate.clifford is False and isinstance(gate, gates.M) is False: - gates_to_replace.append([i, gate]) - gates_to_replace = np.array(gates_to_replace, dtype=object) + non_clifford_gates = [ + (i, gate) + for i, gate in enumerate(circuit.queue) + if not gate.clifford and not isinstance(gate, gates.M) + ] - if len(gates_to_replace) == 0: + if not non_clifford_gates: raise_error(ValueError, "No non-Clifford gate found, no circuit sampled.") - # Build the training circuit by substituting the sampled gates sampled_circuit = circuit.__class__(**circuit.init_kwargs) for i, gate in enumerate(circuit.queue): - if gate.name == "id_end": # isinstance(gate, gates.M): + if isinstance(gate, gates.M): gate_rand = gates.Unitary( random_clifford(1, backend=backend, return_circuit=False), gate.qubits[0], @@ -713,98 +715,99 @@ def sample_clifford_training_circuit( sampled_circuit.add(gate) else: - if i in gates_to_replace[:, 0]: + if i in [index for index, _ in non_clifford_gates]: gate = gates.Unitary( random_clifford(1, backend=backend, return_circuit=False), gate.qubits[0], ) gate.clifford = True sampled_circuit.add(gate) + return sampled_circuit -def transpile_circ(circuit, qubit_map, backend): - from qibolab.transpilers.unitary_decompositions import u3_decomposition +def error_sensitive_circuit(circuit, observable, fuse=True, backend=None): + """ + Generates a Clifford circuit that preserves the same circuit frame as the input circuit, and stabilizes the specified Pauli observable. - if backend.name == "qibolab": - new_c = circuit.__class__(backend.platform.nqubits) - for gate in circuit.queue: - qubits = [qubit_map[j] for j in gate.qubits] - if isinstance(gate, gates.M): - new_gate = gates.M(*tuple(qubits), **gate.init_kwargs) - new_gate.result = gate.result - new_c.add(new_gate) - elif isinstance(gate, gates.I): - new_c.add(gate.__class__(*tuple(qubits), **gate.init_kwargs)) - else: - matrix = gate.matrix() - new_c.add(gates.U3(qubits[0], *u3_decomposition(matrix))) - return new_c - else: - return circuit + Args: + circuit (:class:`qibo.models.Circuit`): input circuit. + observable (numpy.ndarray): Pauli observable to be measured. + fuse (bool, optional): if True, fuse the single qubits gates in the circuit. + backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used + in the execution. if ``None``, it uses :class:`qibo.backends.GlobalBackend`. + Defaults to ``None``. + Returns: + :class:`qibo.models.Circuit`: the error sensitive circuit. + :class:`qibo.models.Circuit`: the sampled Clifford circuit. + list: the list of adjustment gates. -def escircuit(circuit, obs, fuse=True, backend=None): - """ - Error sensitive circuit algorithm. - Implement what proposed in https://arxiv.org/abs/2112.06255. + Reference: + 1. Dayue Qin, Yanzhu Chen et al, *Error statistics and scalability of quantum error mitigation formulas*. + `arXiv:2112.06255 [quant-ph] `_. """ - from qibo.quantum_info import comp_basis_to_pauli, random_clifford, vectorization + from qibo.quantum_info import ( # pylint: disable=import-outside-toplevel + comp_basis_to_pauli, + random_clifford, + vectorization, + ) if backend is None: # pragma: no cover backend = GlobalBackend() - sign = -1 - # while sign == -1: - circ_cliff = sample_clifford_training_circuit(circuit, backend=backend) - c_unitary = circ_cliff.unitary(backend=backend) - nqubits = circ_cliff.nqubits - U_c2p = comp_basis_to_pauli(nqubits, backend=backend) - obs_liouville = vectorization( - np.transpose(np.conjugate(c_unitary)) @ obs.matrix @ c_unitary, order="row" + sampled_circuit = sample_clifford_training_circuit(circuit, backend=backend) + unitary_matrix = sampled_circuit.unitary(backend=backend) + num_qubits = sampled_circuit.nqubits + + comp_to_pauli = comp_basis_to_pauli(num_qubits, backend=backend) + observable_liouville = vectorization( + np.transpose(np.conjugate(unitary_matrix)) @ observable.matrix @ unitary_matrix, + order="row", + backend=backend, ) - obs_pauli_liouville = U_c2p @ obs_liouville - index = np.where(abs(obs_pauli_liouville) >= 1e-5)[0][0] - sign = np.sign(obs_pauli_liouville[index]) + observable_pauli_liouville = comp_to_pauli @ observable_liouville + + index = int(np.where(abs(observable_pauli_liouville) >= 1e-5)[0][0]) - obs1 = list(product(["I", "X", "Y", "Z"], repeat=nqubits))[index] + observable_pauli = list(product(["I", "X", "Y", "Z"], repeat=num_qubits))[index] - paulis = { - "I": gates.I(0).matrix(), - "X": gates.X(0).matrix(), - "Y": gates.Y(0).matrix(), - "Z": gates.Z(0).matrix(), + pauli_gates = { + "I": backend.cast(gates.I(0).matrix(backend=backend)), + "X": backend.cast(gates.X(0).matrix(backend=backend)), + "Y": backend.cast(gates.Y(0).matrix(backend=backend)), + "Z": backend.cast(gates.Z(0).matrix(backend=backend)), } - adjust_gates = [] - for i in range(nqubits): - obs_i = paulis[obs1[i]] - R = paulis["I"] - while np.any(abs(obs_i - paulis["Z"]) > 1e-5) and np.any( - abs(obs_i - paulis["I"]) > 1e-5 + adjustment_gates = [] + for i in range(num_qubits): + observable_i = pauli_gates[observable_pauli[i]] + random_init = pauli_gates["I"] + while np.any(abs(observable_i - pauli_gates["Z"]) > 1e-5) and np.any( + abs(observable_i - pauli_gates["I"]) > 1e-5 ): - R = random_clifford(1, backend=backend, return_circuit=False) - obs_i = np.conjugate(np.transpose(R)) @ paulis[obs1[i]] @ R + random_init = random_clifford(1, backend=backend, return_circuit=False) + observable_i = ( + np.conjugate(np.transpose(random_init)) + @ pauli_gates[observable_pauli[i]] + @ random_init + ) - adjust_gate = gates.Unitary(R, i) - adjust_gate.clifford = True - adjust_gates.append(adjust_gate) + adjustment_gate = gates.Unitary(random_init, i) + adjustment_gate.clifford = True + adjustment_gates.append(adjustment_gate) - circ_cliff1 = circ_cliff.__class__(**circ_cliff.init_kwargs) + sensitive_circuit = sampled_circuit.__class__(**sampled_circuit.init_kwargs) - j = 0 - for gate in circ_cliff.queue: - if gate.name == "id_init": - circ_cliff1.add(gate) - circ_cliff1.add(adjust_gates[j]) - j += 1 - else: - circ_cliff1.add(gate) + for gate in adjustment_gates: + sensitive_circuit.add(gate) + for gate in sampled_circuit.queue: + sensitive_circuit.add(gate) if fuse: - circ_cliff1 = circ_cliff1.fuse(max_qubits=1) + sensitive_circuit = sensitive_circuit.fuse(max_qubits=1) - return circ_cliff1, circ_cliff, adjust_gates + return sensitive_circuit, sampled_circuit, adjustment_gates def ICS( @@ -819,8 +822,37 @@ def ICS( backend=None, ): """ - Compute the important Clifford Sampling methos proposed in - https://arxiv.org/abs/2112.06255. + Computes the Important Clifford Sampling method. + + Args: + circuit (:class:`qibo.models.Circuit`): input circuit. + observable (numpy.ndarray): the observable to be measured. + readout (dict, optional): A dictionary that may contain the following keys: + - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + If ``None`` the 'inverse' method is used. + If provided, the corresponding readout error mitigation method is used. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. + noise_model (qibo.models.noise.Noise, optional): the noise model to be applied. Defaults to ``None``. + nshots (int, optional): the number of shots for the circuit execution. Defaults to 10000. + n_training_samples (int, optional): the number of training samples. Defaults to 10. + full_output (bool, optional): if ``True``, this function returns additional + information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``. + backend (qibo.backends.abstract.Backend, optional): the backend to be used in the execution. + If None, it uses the global backend. Defaults to ``None``. + + Returns: + mitigated_expectation (float): the mitigated expectated value. + mitigated_expectation_std (float): the standard deviation of the mitigated expectated value. + dep_param (float): the depolarizing parameter. + dep_param_std (float): the standard deviation of the depolarizing parameter. + lambda_list (list): the list of the depolarizing parameters. + data (dict): the data dictionary containing the noise-free and noisy expectation values obtained with the training circuits. + + Reference: + 1. Dayue Qin, Yanzhu Chen et al, *Error statistics and scalability of quantum error mitigation formulas*. + `arXiv:2112.06255 [quant-ph] `_. """ if backend is None: # pragma: no cover backend = GlobalBackend() @@ -828,68 +860,78 @@ def ICS( if qubit_map is None: qubit_map = list(range(circuit.nqubits)) - training_circs = [ - escircuit(circuit, observable, backend=backend)[0] + training_circuits = [ + error_sensitive_circuit(circuit, observable, backend=backend)[0] for _ in range(n_training_samples) ] - data = {"noise-free": {"-1": [], "1": []}, "noisy": {"-1": [], "1": []}} - from qibo.config import log + data = {"noise-free": [], "noisy": []} + lambda_list = [] + + for training_circuit in training_circuits: + circuit_result = training_circuit(nshots=nshots) + expectation = observable.expectation_from_samples(circuit_result.frequencies()) - a_list = {"-1": [], "1": []} - for c in training_circs: - circuit_result = c(nshots=nshots) - exp = observable.expectation_from_samples(circuit_result.frequencies()) - # state = c().state() - # exp = obs.expectation(state) if noise_model is not None and backend.name != "qibolab": - c = noise_model.apply(c) + training_circuit = noise_model.apply(training_circuit) - c = transpile_circ(c, qubit_map, backend) - circuit_result = backend.execute_circuit(c, nshots=nshots) + training_circuit = transpile_circ(training_circuit, qubit_map, backend) + circuit_result = backend.execute_circuit(training_circuit, nshots=nshots) if "calibration_matrix" in readout.keys(): - circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["inv"] + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, + readout["calibration_matrix"], + readout["ibu_iters"], ) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) + noisy_expectation = observable.expectation_from_samples( + circuit_result.frequencies() + ) - if exp > 0: - data["noise-free"]["1"].append(exp) - data["noisy"]["1"].append(exp_noisy) - a_list["1"].append(1 - exp_noisy / exp) - else: - data["noise-free"]["-1"].append(exp) - data["noisy"]["-1"].append(exp_noisy) - a_list["-1"].append(1 - exp_noisy / exp) - - a_std_1 = np.std(a_list["1"]) - a_std_m1 = np.std(a_list["-1"]) - a = (np.sum(a_list["1"]) + np.sum(a_list["-1"])) / n_training_samples - a_std = np.sqrt( - (a_std_1**2 * len(a_list["1"]) + a_std_m1**2 * len(a_list["-1"])) - / n_training_samples - ) + data["noise-free"].append(expectation) + data["noisy"].append(noisy_expectation) + lambda_list.append(1 - noisy_expectation / expectation) + + dep_param = np.mean(lambda_list) + dep_param_std = np.std(lambda_list) circuit = circuit.fuse(max_qubits=1) + if noise_model is not None and backend.name != "qibolab": circuit = noise_model.apply(circuit) + circuit = transpile_circ(circuit, qubit_map, backend) circuit_result = backend.execute_circuit(circuit, nshots=nshots) + if "calibration_matrix" in readout.keys(): - circuit_result = apply_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["inv"] + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, readout["calibration_matrix"], readout["ibu_iters"] ) - exp_noisy = observable.expectation_from_samples(circuit_result.frequencies()) - exp_mit = (1 - a) * exp_noisy / ((1 - a) ** 2 + a_std**2) - exp_mit_std = ( - a_std - * abs(exp_noisy) - * abs((1 - a) ** 2 - a_std**2) - / ((1 - a) ** 2 + a_std**2) ** 2 + + noisy_expectation = observable.expectation_from_samples( + circuit_result.frequencies() + ) + mitigated_expectation = ( + (1 - dep_param) + * noisy_expectation + / ((1 - dep_param) ** 2 + dep_param_std**2) ) + mitigated_expectation_std = ( + dep_param_std + * abs(noisy_expectation) + * abs((1 - dep_param) ** 2 - dep_param_std**2) + / ((1 - dep_param) ** 2 + dep_param_std**2) ** 2 + ) + if full_output: - return exp_mit, exp_mit_std, a, a_std, a_list, data + return ( + mitigated_expectation, + mitigated_expectation_std, + dep_param, + dep_param_std, + lambda_list, + data, + ) - return exp_mit + return mitigated_expectation From 626540b92a83e9009501e9061c0175a4336210ae Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:53:47 +0100 Subject: [PATCH 25/78] transpile --- src/qibo/models/error_mitigation.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 60f6a67433..1996bb69f7 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -935,3 +935,26 @@ def ICS( ) return mitigated_expectation + + +def transpile_circ(circuit, qubit_map, backend): + from qibolab.transpilers.unitary_decompositions import u3_decomposition + + if backend.name == "qibolab": + if qubit_map is None: + qubit_map = list(range(circuit.nqubits)) + new_c = circuit.__class__(backend.platform.nqubits) + for gate in circuit.queue: + qubits = [qubit_map[j] for j in gate.qubits] + if isinstance(gate, gates.M): + new_gate = gates.M(*tuple(qubits), **gate.init_kwargs) + new_gate.result = gate.result + new_c.add(new_gate) + elif isinstance(gate, gates.I): + new_c.add(gate.__class__(*tuple(qubits), **gate.init_kwargs)) + else: + matrix = gate.matrix() + new_c.add(gates.U3(qubits[0], *u3_decomposition(matrix))) + return new_c + + return circuit From 6b13d0f5892b522f7ddd03fc625bc972edab048d Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 01:54:22 +0100 Subject: [PATCH 26/78] updating tests --- tests/test_models_error_mitigation.py | 130 +++++++++++++++++++------- 1 file changed, 95 insertions(+), 35 deletions(-) diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index d602d0ae4c..0d19979e60 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -2,14 +2,17 @@ import pytest from qibo import Circuit, gates +from qibo.backends import construct_backend from qibo.hamiltonians import SymbolicHamiltonian from qibo.models.error_mitigation import ( CDR, + ICS, ZNE, + apply_cal_mat_readout_mitigation, apply_randomized_readout_mitigation, - apply_readout_mitigation, - calibration_matrix, - sample_training_circuit, + get_calibration_matrix, + sample_clifford_training_circuit, + sample_training_circuit_cdr, vnCDR, ) from qibo.noise import DepolarizingError, NoiseModel, ReadoutError @@ -22,6 +25,7 @@ def get_noise_model(error, gate, cal_matrix=[False, None]): noise.add(error, gate) if cal_matrix[0]: noise.add(ReadoutError(probabilities=cal_matrix[1]), gate=gates.M) + return noise @@ -43,10 +47,9 @@ def get_circuit(nqubits): c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2)) c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) c.add(gates.M(*range(nqubits))) - return c + return c -from qibo.backends import construct_backend backend = construct_backend("numpy") # # Generate random calibration matrices @@ -66,7 +69,7 @@ def get_circuit(nqubits): 3, get_noise_model(DepolarizingError(0.1), gates.CNOT), "CNOT", - {"calibration_matrix": cal_matrix_3q}, + {"calibration_matrix": cal_matrix_3q, "ibu_iters": None}, ), ( 3, @@ -79,7 +82,13 @@ def get_circuit(nqubits): 1, get_noise_model(DepolarizingError(0.3), gates.RX), "RX", - {"calibration_matrix": cal_matrix_1q}, + {"calibration_matrix": cal_matrix_1q, "ibu_iters": None}, + ), + ( + 1, + get_noise_model(DepolarizingError(0.3), gates.RX), + "RX", + {"calibration_matrix": cal_matrix_1q, "ibu_iters": 10}, ), (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", {"ncircuits": 2}), ], @@ -116,6 +125,7 @@ def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): readout=readout, backend=backend, ) + assert np.abs(exact - estimate) <= np.abs(exact - noisy) @@ -127,7 +137,11 @@ def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), ( get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), - {"calibration_matrix": cal_matrix_3q}, + {"calibration_matrix": cal_matrix_3q, "ibu_iters": None}, + ), + ( + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), + {"calibration_matrix": cal_matrix_3q, "ibu_iters": 10}, ), ( get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), @@ -167,6 +181,7 @@ def test_cdr(backend, nqubits, noise, full_output, readout): ) if full_output: estimate = estimate[0] + assert np.abs(exact - estimate) <= np.abs(exact - noisy) @@ -189,32 +204,22 @@ def test_sample_training_circuit(nqubits): c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2)) c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) c.add(gates.M(q) for q in range(nqubits)) + + for j in range(len(c.queue)): + c.queue[j].clifford = True with pytest.raises(ValueError): - sample_training_circuit(c) + sample_training_circuit_cdr(c) + sample_clifford_training_circuit(c) @pytest.mark.parametrize( "nqubits,noise,insertion_gate,readout", [ - (3, get_noise_model(DepolarizingError(0.1), gates.CNOT), "CNOT", {}), - ( - 3, - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), - "CNOT", - {"calibration_matrix": cal_matrix_3q}, - ), - ( - 3, - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), - "CNOT", - {"ncircuits": 2}, - ), - (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", {}), ( 1, get_noise_model(DepolarizingError(0.1), gates.RX, [True, cal_matrix_1q]), "RX", - {"calibration_matrix": cal_matrix_1q}, + {"calibration_matrix": cal_matrix_1q, "ibu_iters": 10}, ), ( 1, @@ -242,12 +247,6 @@ def test_vncdr(backend, nqubits, noise, full_output, insertion_gate, readout): # Noise-free expected value exact = obs.expectation(backend.execute_circuit(c).state()) # Noisy expected value without mitigation - if "calibration_matrix" in readout.keys() or "ncircuits" in readout.keys(): - if nqubits == 1: - p = cal_matrix_1q - elif nqubits == 3: - p = cal_matrix_3q - # noise.add(ReadoutError(probabilities=p),gate=gates.M) state = backend.execute_circuit(noise.apply(c), nshots=10000) noisy = state.expectation_from_samples(obs) # Mitigated expected value @@ -265,12 +264,14 @@ def test_vncdr(backend, nqubits, noise, full_output, insertion_gate, readout): ) if full_output: estimate = estimate[0] + assert np.abs(exact - estimate) <= np.abs(exact - noisy) @pytest.mark.parametrize("nqubits", [3]) -@pytest.mark.parametrize("method", ["cal_matrix", "temme"]) -def test_readout_mitigation(backend, nqubits, method): +@pytest.mark.parametrize("method", ["cal_matrix", "randomized"]) +@pytest.mark.parametrize("ibu_iters", [None, 10]) +def test_readout_mitigation(backend, nqubits, method, ibu_iters): if backend.name == "tensorflow": import tensorflow as tf @@ -283,7 +284,9 @@ def test_readout_mitigation(backend, nqubits, method): noise = NoiseModel() noise.add(ReadoutError(probabilities=p), gate=gates.M) if method == "cal_matrix": - calibration = calibration_matrix(nqubits, noise, nshots=nshots, backend=backend) + calibration = get_calibration_matrix( + nqubits, None, noise, nshots=nshots, backend=backend + ) # Define the observable obs = np.prod([Z(i) for i in range(nqubits)]) obs = SymbolicHamiltonian(obs, backend=backend) @@ -295,9 +298,9 @@ def test_readout_mitigation(backend, nqubits, method): state = backend.execute_circuit(noise.apply(c), nshots=nshots) noisy_val = state.expectation_from_samples(obs) if method == "cal_matrix": - mit_state = apply_readout_mitigation(state, calibration) + mit_state = apply_cal_mat_readout_mitigation(state, calibration, ibu_iters) mit_val = mit_state.expectation_from_samples(obs) - elif method == "temme": + elif method == "randomized": ncircuits = 10 result, result_cal = apply_randomized_readout_mitigation( c, noise, nshots, ncircuits, backend @@ -305,4 +308,61 @@ def test_readout_mitigation(backend, nqubits, method): mit_val = result.expectation_from_samples( obs ) / result_cal.expectation_from_samples(obs) + assert np.abs(true_val - mit_val) <= np.abs(true_val - noisy_val) + + +@pytest.mark.parametrize("nqubits", [3]) +@pytest.mark.parametrize("full_output", [False, True]) +@pytest.mark.parametrize( + "noise,readout", + [ + (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), + ( + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), + {"calibration_matrix": cal_matrix_3q, "ibu_iters": None}, + ), + ( + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), + {"calibration_matrix": cal_matrix_3q, "ibu_iters": 10}, + ), + ( + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), + {"ncircuits": 2}, + ), + ], +) +def test_ics(backend, nqubits, noise, full_output, readout): + if backend.name == "tensorflow": + import tensorflow as tf + + tf.config.threading.set_inter_op_parallelism_threads = 1 + tf.config.threading.set_intra_op_parallelism_threads = 1 + else: + backend.set_threads(1) + """Test that ICS reduces the noise.""" + # Define the circuit + c = get_circuit(nqubits) + # Define the observable + obs = np.prod([Z(i) for i in range(nqubits)]) + obs = SymbolicHamiltonian(obs, backend=backend) + # Noise-free expected value + exact = obs.expectation(backend.execute_circuit(c).state()) + # Noisy expected value without mitigation + state = backend.execute_circuit(noise.apply(c), nshots=10000) + noisy = state.expectation_from_samples(obs) + # Mitigated expected value + estimate = ICS( + circuit=c, + observable=obs, + noise_model=noise, + nshots=10000, + n_training_samples=20, + full_output=full_output, + readout=readout, + backend=backend, + ) + if full_output: + estimate = estimate[0] + + assert np.abs(exact - estimate) <= np.abs(exact - noisy) From ea56f406bcab9c234eaf462a679c1195234502d1 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 02:22:48 +0100 Subject: [PATCH 27/78] fix docstring --- src/qibo/models/error_mitigation.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index c7e3b0c654..0226e1914b 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -553,14 +553,14 @@ def apply_cal_mat_readout_mitigation(state, calibration_matrix, iterations=None) Applies readout error mitigation to the given state using the provided calibration matrix. Args: - state (:class:`qibo.states.CircuitResult`): the input state to be updated. This state should contain the + state (:class:`qibo.measurements.CircuitResult`): the input state to be updated. This state should contain the frequencies that need to be mitigated. calibration_matrix (numpy.ndarray, optional): the calibration matrix for readout mitigation. iterations (int, optional): the number of iterations to use for the Iterative Bayesian Unfolding method. If ``None`` the 'inverse' method is used. Defaults to ``None``. Returns: - :class:`qibo.states.CircuitResult`: The input state with the updated (mitigated) frequencies. + :class:`qibo.measurements.CircuitResult`: The input state with the updated (mitigated) frequencies. """ frequencies = np.zeros(2 ** len(state.measurements[0].qubits)) for key, value in state.frequencies().items(): @@ -938,7 +938,7 @@ def ICS( def transpile_circ(circuit, qubit_map, backend): - from qibolab.transpilers.unitary_decompositions import u3_decomposition + from qibo.transpiler.unitary_decompositions import u3_decomposition if backend.name == "qibolab": if qubit_map is None: From 1dd96c78f6acd93ba7e2e163816d092da3f46a07 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 02:29:07 +0100 Subject: [PATCH 28/78] fix variational --- src/qibo/models/variational.py | 79 ++++++++++++++++++++++++++-------- 1 file changed, 62 insertions(+), 17 deletions(-) diff --git a/src/qibo/models/variational.py b/src/qibo/models/variational.py index 46bc417536..d031e58ae0 100644 --- a/src/qibo/models/variational.py +++ b/src/qibo/models/variational.py @@ -40,23 +40,45 @@ def __init__(self, circuit, hamiltonian): def minimize( self, - initial_parameters, - optimizer=None, - optimizer_options={}, + initial_state, + method="Powell", + jac=None, + hess=None, + hessp=None, + bounds=None, + constraints=(), + tol=None, + callback=None, + options=None, + compile=False, + processes=None, ): """Search for parameters which minimizes the hamiltonian expectation. Args: - initial_parameters (array or list): a initial guess for the parameters of the + initial_state (array): a initial guess for the parameters of the variational circuit. - optimizer (qibo.optimizers.Optimizer): Qibo optimizer which is used to - minimize the energy [default is `ScipyMinimizer(method="Powell")`]. - optimizer_options (dict): extra options for the optimizers. + method (str): the desired minimization method. + See :meth:`qibo.optimizers.optimize` for available optimization + methods. + jac (dict): Method for computing the gradient vector for scipy optimizers. + hess (dict): Method for computing the hessian matrix for scipy optimizers. + hessp (callable): Hessian of objective function times an arbitrary + vector for scipy optimizers. + bounds (sequence or Bounds): Bounds on variables for scipy optimizers. + constraints (dict): Constraints definition for scipy optimizers. + tol (float): Tolerance of termination for scipy optimizers. + callback (callable): Called after each iteration for scipy optimizers. + options (dict): a dictionary with options for the different optimizers. + compile (bool): whether the TensorFlow graph should be compiled. + processes (int): number of processes when using the paralle BFGS method. Return: - output of `qibo.optimizers.Optimizer` classes: [best function value, - best set of trainable parameters, complete result object according - to specific optimizer configuration]. + The final expectation value. + The corresponding best parameters. + The optimization result object. For scipy methods it returns + the ``OptimizeResult``, for ``'cma'`` the ``CMAEvolutionStrategy.result``, + and for ``'sgd'`` the options used during the optimization. """ def _loss(params, circuit, hamiltonian): @@ -65,15 +87,38 @@ def _loss(params, circuit, hamiltonian): final_state = result.state() return hamiltonian.expectation(final_state) - opt = optimizer( - initial_parameters=initial_parameters, - loss=_loss, + if compile: + loss = self.hamiltonian.backend.compile(_loss) + else: + loss = _loss + + if method == "cma": + # TODO: check if we can use this shortcut + # dtype = getattr(self.hamiltonian.backend.np, self.hamiltonian.backend._dtypes.get('DTYPE')) + dtype = self.hamiltonian.backend.np.float64 + loss = lambda p, c, h: dtype(_loss(p, c, h)) + elif method != "sgd": + loss = lambda p, c, h: self.hamiltonian.backend.to_numpy(_loss(p, c, h)) + + result, parameters, extra = self.optimizers.optimize( + loss, + initial_state, args=(self.circuit, self.hamiltonian), - **optimizer_options, + method=method, + jac=jac, + hess=hess, + hessp=hessp, + bounds=bounds, + constraints=constraints, + tol=tol, + callback=callback, + options=options, + compile=compile, + processes=processes, + backend=self.hamiltonian.backend, ) - - best_f, best_p, results = opt.fit() - return best_f, best_p, results + self.circuit.set_parameters(parameters) + return result, parameters, extra def energy_fluctuation(self, state): """ From 60c7299f92c93021478cab0cc6069d8d8c575b3e Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 02:35:06 +0100 Subject: [PATCH 29/78] update init --- src/qibo/models/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/qibo/models/__init__.py b/src/qibo/models/__init__.py index 7093858901..cf55bf7d52 100644 --- a/src/qibo/models/__init__.py +++ b/src/qibo/models/__init__.py @@ -1,14 +1,7 @@ from qibo.models import hep, tsp from qibo.models.circuit import Circuit from qibo.models.encodings import unary_encoder -from qibo.models.error_mitigation import ( - CDR, - ZNE, - get_gammas, - get_noisy_circuit, - sample_training_circuit_cdr, - vnCDR, -) +from qibo.models.error_mitigation import CDR, ICS, ZNE, vnCDR from qibo.models.evolution import AdiabaticEvolution, StateEvolution from qibo.models.grover import Grover from qibo.models.qft import QFT From 452661e387844c1b78cc2fa243a03da011b7d9ed Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Mon, 11 Dec 2023 02:44:30 +0100 Subject: [PATCH 30/78] update pre-commit --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a8aa2dea52..2d641a95aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - id: check-yaml - id: debug-statements - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.11.0 hooks: - id: black - repo: https://github.com/pycqa/isort @@ -22,7 +22,7 @@ repos: hooks: - id: pyupgrade - repo: https://github.com/hadialqattan/pycln - rev: v2.2.2 + rev: v2.4.0 hooks: - id: pycln args: [--config=pyproject.toml] From d58778685bd4b4579b920f36aee135b793f1b70f Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Mon, 11 Dec 2023 18:46:20 +0400 Subject: [PATCH 31/78] refactor shortest paths --- src/qibo/transpiler/placer.py | 24 +- src/qibo/transpiler/router.py | 653 +++++++++++++---------------- src/qibo/transpiler/test_new_ss.py | 24 ++ tests/test_transpiler_router.py | 68 ++- 4 files changed, 388 insertions(+), 381 deletions(-) create mode 100644 src/qibo/transpiler/test_new_ss.py diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index f74a1bbf76..fb687a38b2 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -8,7 +8,6 @@ from qibo.models import Circuit from qibo.transpiler.abstract import Placer, Router from qibo.transpiler.exceptions import PlacementError -from qibo.transpiler.router import _find_gates_qubits_pairs def assert_placement( @@ -60,6 +59,29 @@ def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): ) +def _find_gates_qubits_pairs(circuit: Circuit): + """Helper method for :meth:`qibo.transpiler.placer`. + Translate qibo circuit into a list of pairs of qubits to be used by the router and placer. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. + + Returns: + gates_qubits_pairs (list): list containing pairs of qubits targeted by two qubits gates. + """ + gates_qubits_pairs = [] + for gate in circuit.queue: + if isinstance(gate, gates.M): + pass + elif len(gate.qubits) == 2: + gates_qubits_pairs.append(sorted(gate.qubits)) + elif len(gate.qubits) >= 3: + raise_error( + ValueError, "Gates targeting more than 2 qubits are not supported" + ) + return gates_qubits_pairs + + class Trivial(Placer): """Place qubits according to the following notation: diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index e3180d84bf..42f4905bdd 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -22,7 +22,15 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): circuit (:class:`qibo.models.circuit.Circuit`): circuit model to check. connectivity (:class:`networkx.Graph`): chip connectivity. """ - + if list(connectivity.nodes) != list(range(connectivity.number_of_nodes())): + node_mapping = {node: i for i, node in enumerate(connectivity.nodes)} + new_connectivity = nx.Graph() + for new_name in node_mapping.values(): + new_connectivity.add_node(new_name) + new_connectivity.add_edges_from( + [(node_mapping[u], node_mapping[v]) for u, v in connectivity.edges] + ) + connectivity = new_connectivity for gate in circuit.queue: if len(gate.qubits) > 2 and not isinstance(gate, gates.M): raise_error(ConnectivityError, f"{gate.name} acts on more than two qubits.") @@ -34,339 +42,6 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): ) -# TODO: make this class work with CircuitMap -class ShortestPaths(Router): - """A class to perform initial qubit mapping and connectivity matching. - - Args: - connectivity (:class:`networkx.Graph`): chip connectivity. - sampling_split (float, optional): fraction of paths tested - (between :math:`0` and :math:`1`). Defaults to :math:`1.0`. - verbose (bool, optional): If ``True``, print info messages. Defaults to ``False``. - """ - - def __init__( - self, connectivity: nx.Graph, sampling_split: float = 1.0, verbose: bool = False - ): - self.connectivity = connectivity - self.sampling_split = sampling_split - self.verbose = verbose - - self.initial_layout = None - self._added_swaps = 0 - self.final_map = None - self._gates_qubits_pairs = None - self._mapping = None - self._swap_map = None - self._added_swaps_list = [] - self._graph = None - self._qubit_map = None - self._transpiled_circuit = None - self._circuit_position = 0 - - def __call__(self, circuit: Circuit, initial_layout: dict): - """Circuit connectivity matching. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be matched to hardware connectivity. - initial_layout (dict): initial physical-to-logical qubit mapping - - Returns: - (:class:`qibo.models.circuit.Circuit`, dict): circut mapped to hardware topology, and final qubit mapping. - """ - self._mapping = initial_layout - dict_keys = list(initial_layout.keys()) - init_qubit_map = np.asarray(list(initial_layout.values())) - self._initial_checks(circuit.nqubits) - self._gates_qubits_pairs = _find_gates_qubits_pairs(circuit) - self._mapping = dict(zip(range(len(initial_layout)), initial_layout.values())) - self._graph = nx.relabel_nodes(self.connectivity, self._mapping) - self._qubit_map = np.sort(init_qubit_map) - self._swap_map = deepcopy(init_qubit_map) - self._first_transpiler_step(circuit) - - while len(self._gates_qubits_pairs) != 0: - self._transpiler_step(circuit) - hardware_mapped_circuit = self._remap_circuit(np.argsort(init_qubit_map)) - final_mapping = { - dict_keys[j]: self._swap_map[j] - for j in range(self._graph.number_of_nodes()) - } - - return hardware_mapped_circuit, final_mapping - - @property - def added_swaps(self): - """Number of added swaps during transpiling.""" - return self._added_swaps - - @property - def sampling_split(self): - """Fraction of possible shortest paths to be analyzed.""" - return self._sampling_split - - @sampling_split.setter - def sampling_split(self, sampling_split: float): - """Set the sampling split, the fraction of possible shortest paths to be analyzed. - - Args: - sampling_split (float): define fraction of shortest path tested. - """ - - if 0.0 < sampling_split <= 1.0: - self._sampling_split = sampling_split - else: - raise_error(ValueError, "Sampling_split must be in (0:1].") - - def _transpiler_step(self, circuit: Circuit): - """Transpilation step. Find new mapping, add swap gates and apply gates that can be run with this configuration. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - """ - len_before_step = len(self._gates_qubits_pairs) - path, meeting_point = self._relocate() - self._add_swaps(path, meeting_point) - self._update_qubit_map() - self._add_gates(circuit, len_before_step - len(self._gates_qubits_pairs)) - - def _first_transpiler_step(self, circuit: Circuit): - """First transpilation step. Apply gates that can be run with the initial qubit mapping. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - """ - self._circuit_position = 0 - self._added_swaps = 0 - self._added_swaps_list = [] - len_2q_circuit = len(self._gates_qubits_pairs) - self._gates_qubits_pairs = self._reduce(self._graph) - self._add_gates(circuit, len_2q_circuit - len(self._gates_qubits_pairs)) - - def _reduce(self, graph: nx.Graph): - """Reduces the circuit by deleting two-qubit gates if it can be applied on the current configuration. - - Args: - graph (:class:`networkx.Graph`): current hardware qubit mapping. - - Returns: - (list): reduced circuit. - """ - new_circuit = self._gates_qubits_pairs.copy() - while ( - new_circuit != [] - and (new_circuit[0][0], new_circuit[0][1]) in graph.edges() - ): - del new_circuit[0] - return new_circuit - - def _map_list(self, path: list): - """Return all possible walks of qubits, or a fraction, for a given path. - - Args: - path (list): path to move qubits. - - Returns: - (list, list): all possible walks of qubits, or a fraction of them based on self.sampling_split, for a given path, and qubit meeting point for each path. - """ - path_ends = [path[0], path[-1]] - path_middle = path[1:-1] - mapping_list = [] - meeting_point_list = [] - test_paths = range(len(path) - 1) - if self.sampling_split != 1.0: - test_paths = np.random.choice( - test_paths, - size=int(np.ceil(len(test_paths) * self.sampling_split)), - replace=False, - ) - for i in test_paths: - values = path_middle[:i] + path_ends + path_middle[i:] - mapping = dict(zip(path, values)) - mapping_list.append(mapping) - meeting_point_list.append(i) - - return mapping_list, meeting_point_list - - def _relocate(self): - """Greedy algorithm to decide which path to take, and how qubits should walk. - - Returns: - (list, int): best path to move qubits and qubit meeting point in the path. - """ - nodes = self._graph.number_of_nodes() - circuit = self._reduce(self._graph) - final_circuit = circuit - keys = list(range(nodes)) - final_graph = self._graph - final_mapping = dict(zip(keys, keys)) - # Consider all shortest paths - path_list = [ - p - for p in nx.all_shortest_paths( - self._graph, source=circuit[0][0], target=circuit[0][1] - ) - ] - self._added_swaps += len(path_list[0]) - 2 - # Here test all paths - for path in path_list: - # map_list uses self.sampling_split - list_, meeting_point_list = self._map_list(path) - for j, mapping in enumerate(list_): - new_graph = nx.relabel_nodes(self._graph, mapping) - new_circuit = self._reduce(new_graph) - # Greedy looking for the optimal path and the optimal walk on this path - if len(new_circuit) < len(final_circuit): - final_graph = new_graph - final_circuit = new_circuit - final_mapping = mapping - final_path = path - meeting_point = meeting_point_list[j] - self._graph = final_graph - self._mapping = final_mapping - self._gates_qubits_pairs = final_circuit - - return final_path, meeting_point - - def _initial_checks(self, qubits: int): - """Initializes the transpiled circuit and check if it can be mapped to the defined connectivity. - - Args: - qubits (int): number of qubits in the circuit to be transpiled. - """ - nodes = self.connectivity.number_of_nodes() - if qubits > nodes: - raise_error( - ValueError, - "There are not enough physical qubits in the hardware to map the circuit.", - ) - if qubits == nodes: - new_circuit = Circuit(nodes) - else: - if self.verbose: - log.info( - "You are using more physical qubits than required by the circuit, some ancillary qubits will be added to the circuit." - ) - new_circuit = Circuit(nodes) - self._transpiled_circuit = new_circuit - - def _add_gates(self, circuit: Circuit, matched_gates: int): - """Adds one and two qubit gates to transpiled circuit until connectivity is matched. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - matched_gates (int): number of two-qubit gates that - can be applied with the current qubit mapping. - """ - index = 0 - while self._circuit_position < len(circuit.queue): - gate = circuit.queue[self._circuit_position] - if isinstance(gate, gates.M): - measured_qubits = gate.qubits - self._transpiled_circuit.add( - gate.on_qubits( - { - measured_qubits[i]: self._qubit_map[measured_qubits[i]] - for i in range(len(measured_qubits)) - } - ) - ) - self._circuit_position += 1 - elif len(gate.qubits) == 1: - self._transpiled_circuit.add( - gate.on_qubits({gate.qubits[0]: self._qubit_map[gate.qubits[0]]}) - ) - self._circuit_position += 1 - else: - index += 1 - if index == matched_gates + 1: - break - self._transpiled_circuit.add( - gate.on_qubits( - { - gate.qubits[0]: self._qubit_map[gate.qubits[0]], - gate.qubits[1]: self._qubit_map[gate.qubits[1]], - } - ) - ) - self._circuit_position += 1 - - def _add_swaps(self, path: list, meeting_point: int): - """Adds swaps to the transpiled circuit to move qubits. - - Args: - path (list): path to move qubits. - meeting_point (int): qubit meeting point in the path. - """ - forward = path[0 : meeting_point + 1] - backward = list(reversed(path[meeting_point + 1 :])) - if len(forward) > 1: - for f1, f2 in zip(forward[:-1], forward[1:]): - gate = gates.SWAP(self._qubit_map[f1], self._qubit_map[f2]) - self._transpiled_circuit.add(gate) - self._added_swaps_list.append(gate) - - if len(backward) > 1: - for b1, b2 in zip(backward[:-1], backward[1:]): - gate = gates.SWAP(self._qubit_map[b1], self._qubit_map[b2]) - self._transpiled_circuit.add(gate) - self._added_swaps_list.append(gate) - - def _update_swap_map(self, swap: tuple): - """Updates the qubit swap map.""" - temp = self._swap_map[swap[0]] - self._swap_map[swap[0]] = self._swap_map[swap[1]] - self._swap_map[swap[1]] = temp - - def _update_qubit_map(self): - """Update the qubit mapping after adding swaps.""" - old_mapping = self._qubit_map.copy() - for key, value in self._mapping.items(): - self._qubit_map[value] = old_mapping[key] - - def _remap_circuit(self, qubit_map): - """Map logical to physical qubits in a circuit. - - Args: - qubit_map (ndarray): new qubit mapping. - - Returns: - (:class:`qibo.models.circuit.Circuit`): transpiled circuit mapped with initial qubit mapping. - """ - new_circuit = Circuit(self._transpiled_circuit.nqubits) - for gate in self._transpiled_circuit.queue: - new_circuit.add(gate.on_qubits({q: qubit_map[q] for q in gate.qubits})) - if gate in self._added_swaps_list: - self._update_swap_map( - tuple(qubit_map[gate.qubits[i]] for i in range(2)) - ) - return new_circuit - - -def _find_gates_qubits_pairs(circuit: Circuit): - """Helper method for :meth:`qibo.transpiler.router.ShortestPaths`. - Translate qibo circuit into a list of pairs of qubits to be used by the router and placer. - - Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. - - Returns: - (list): list containing qubits targeted by two qubit gates. - """ - translated_circuit = [] - for gate in circuit.queue: - if isinstance(gate, gates.M): - pass - elif len(gate.qubits) == 2: - translated_circuit.append(sorted(gate.qubits)) - elif len(gate.qubits) >= 3: - raise_error( - ValueError, "Gates targeting more than 2 qubits are not supported" - ) - - return translated_circuit - - class CircuitMap: """Class to keep track of the circuit and physical-logical mapping during routing, this class also implements the initial two qubit blocks decomposition. @@ -383,6 +58,7 @@ def __init__(self, initial_layout: dict, circuit: Circuit, blocks=None): else: self.circuit_blocks = CircuitBlocks(circuit, index_names=True) self.initial_layout = initial_layout + self._graph_qubits_names = [int(key[1:]) for key in initial_layout.keys()] self._circuit_logical = list(range(len(initial_layout))) self._physical_logical = list(initial_layout.values()) self._routed_blocks = CircuitBlocks(Circuit(circuit.nqubits)) @@ -400,7 +76,9 @@ def execute_block(self, block: Block): """Executes a block by removing it from the circuit representation and adding it to the routed circuit. """ - self._routed_blocks.add_block(block.on_qubits(self.get_physical_qubits(block))) + self._routed_blocks.add_block( + block.on_qubits(self.get_physical_qubits(block, index=True)) + ) self.circuit_blocks.remove_block(block) def routed_circuit(self, circuit_kwargs=None): @@ -424,7 +102,7 @@ def update(self, swap: tuple): and add the SWAP gate to the routed blocks, the swap is represented by a tuple containing the logical qubits to be swapped. """ - physical_swap = self.logical_to_physical(swap) + physical_swap = self.logical_to_physical(swap, index=True) self._routed_blocks.add_block( Block(qubits=physical_swap, gates=[gates.SWAP(*physical_swap)]) ) @@ -438,14 +116,25 @@ def get_logical_qubits(self, block: Block): """Returns the current logical qubits where a block is acting""" return self.circuit_to_logical(block.qubits) - def get_physical_qubits(self, block: Block or int): - """Returns the physical qubits where a block is acting.""" + def get_physical_qubits(self, block: Block or int, index=False): + """Returns the physical qubits where a block is acting. + If index is True the qubits are returned as indices of the connectivity nodes. + """ if isinstance(block, int): block = self.circuit_blocks.search_by_index(block) - return self.logical_to_physical(self.get_logical_qubits(block)) + return self.logical_to_physical(self.get_logical_qubits(block), index=index) - def logical_to_physical(self, logical_qubits: tuple): - """Returns the physical qubits associated to the logical qubits.""" + def logical_to_physical(self, logical_qubits: tuple, index=False): + """Returns the physical qubits associated to the logical qubits. + If index is True the qubits are returned as indices of the connectivity nodes. + """ + if not index: + return tuple( + self._graph_qubits_names[ + self._physical_logical.index(logical_qubits[i]) + ] + for i in range(2) + ) return tuple(self._physical_logical.index(logical_qubits[i]) for i in range(2)) def circuit_to_logical(self, circuit_qubits: tuple): @@ -454,7 +143,250 @@ def circuit_to_logical(self, circuit_qubits: tuple): def circuit_to_physical(self, circuit_qubit: int): """Returns the current physical qubit associated to an initial circuit qubit.""" - return self._physical_logical.index(self._circuit_logical[circuit_qubit]) + return self._graph_qubits_names[ + self._physical_logical.index(self._circuit_logical[circuit_qubit]) + ] + + def physical_to_logical(self, physical_qubit: int): + """Returns the current logical qubit associated to a physical qubit (connectivity graph node).""" + physical_qubit_index = self._graph_qubits_names.index(physical_qubit) + return self._physical_logical[physical_qubit_index] + + +class ShortestPaths(Router): + """A class to perform initial qubit mapping and connectivity matching. + + Args: + connectivity (:class:`networkx.Graph`): chip connectivity. + sampling_split (float, optional): fraction of paths tested + (between :math:`0` and :math:`1`). Defaults to :math:`1.0`. + seed (int): seed for the random number generator. + """ + + def __init__(self, connectivity: nx.Graph, seed=42): + self.connectivity = connectivity + self._front_layer = None + self.circuit = None + self._dag = None + self._final_measurements = None + random.seed(seed) + + @property + def added_swaps(self): + """Number of SWAP gates added to the circuit during routing.""" + return self.circuit._swaps + + def __call__(self, circuit: Circuit, initial_layout: dict): + """Circuit connectivity matching. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be matched to hardware connectivity. + initial_layout (dict): initial physical-to-logical qubit mapping + + Returns: + (:class:`qibo.models.circuit.Circuit`, dict): circut mapped to hardware topology, and final physical-to-logical qubit mapping. + """ + self._preprocessing(circuit=circuit, initial_layout=initial_layout) + while self._dag.number_of_nodes() != 0: + print("new step, front layer:", self._front_layer) + execute_block_list = self._check_execution() + print("executable gates:", execute_block_list) + if execute_block_list is not None: + self._execute_blocks(execute_block_list) + else: + self._find_new_mapping() + + routed_circuit = self.circuit.routed_circuit(circuit_kwargs=circuit.init_kwargs) + if self._final_measurements is not None: + routed_circuit = self._append_final_measurements( + routed_circuit=routed_circuit + ) + + return routed_circuit, self.circuit.final_layout() + + def _find_new_mapping(self): + """Find new qubit mapping. The mapping is found by looking for the shortest path.""" + candidates_evaluation = [] + for candidate in self._candidates(): + cost = self._compute_cost(candidate) + candidates_evaluation.append((candidate, cost)) + print("candidate evaluation:", (candidate, cost)) + best_cost = min(candidate[1] for candidate in candidates_evaluation) + best_candidates = [ + candidate[0] + for candidate in candidates_evaluation + if candidate[1] == best_cost + ] + print("best cost:", best_cost) + print("best candidates:", best_candidates) + best_candidate = random.choice(best_candidates) + print("best candidate:", best_candidate) + self._add_swaps(best_candidate, self.circuit) + + def _candidates(self): + """Return all possible shortest paths, + a list contains the new mapping and a second list contains the path meeting point. + """ + target_qubits = self.circuit.get_physical_qubits(self._front_layer[0]) + path_list = list( + nx.all_shortest_paths( + self.connectivity, source=target_qubits[0], target=target_qubits[1] + ) + ) + all_candidates = [] + for path in path_list: + for meeting_point in range(len(path) - 1): + all_candidates.append((path, meeting_point)) + return all_candidates + + @staticmethod + def _add_swaps(candidate: tuple, circuitmap: CircuitMap): + """Adds swaps to the circuit to move qubits. + + Args: + candidate (tuple): contains path to move qubits and qubit meeting point in the path. + circuitmap (CircuitMap): representation of the circuit. + """ + path = candidate[0] + meeting_point = candidate[1] + forward = path[0 : meeting_point + 1] + backward = list(reversed(path[meeting_point + 1 :])) + if len(forward) > 1: + for f1, f2 in zip(forward[:-1], forward[1:]): + circuitmap.update( + ( + circuitmap.physical_to_logical(f1), + circuitmap.physical_to_logical(f2), + ) + ) + if len(backward) > 1: + for b1, b2 in zip(backward[:-1], backward[1:]): + circuitmap.update( + ( + circuitmap.physical_to_logical(b1), + circuitmap.physical_to_logical(b2), + ) + ) + + def _compute_cost(self, candidate): + """Greedy algorithm to decide which path to take, and how qubits should walk. + + Returns: + (list, int): best path to move qubits and qubit meeting point in the path. + """ + temporary_circuit = CircuitMap( + initial_layout=self.circuit.initial_layout, + circuit=Circuit(len(self.circuit.initial_layout)), + blocks=self.circuit.circuit_blocks, + ) + temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical)) + self._add_swaps(candidate, temporary_circuit) + temporary_dag = deepcopy(self._dag) + successive_executed_gates = 0 + while temporary_dag.number_of_nodes() != 0: + for layer, nodes in enumerate(nx.topological_generations(temporary_dag)): + for node in nodes: + temporary_dag.nodes[node]["layer"] = layer + temporary_front_layer = [ + node[0] for node in temporary_dag.nodes(data="layer") if node[1] == 0 + ] + all_executed = True + for block in temporary_front_layer: + print("check temp block:", block) + if ( + temporary_circuit.get_physical_qubits(block) + in self.connectivity.edges + or not temporary_circuit.circuit_blocks.search_by_index( + block + ).entangled + ): + successive_executed_gates += 1 + temporary_circuit.execute_block( + temporary_circuit.circuit_blocks.search_by_index(block) + ) + temporary_dag.remove_node(block) + print("executed") + else: + all_executed = False + if not all_executed: + break + print("cost:", successive_executed_gates) + return -successive_executed_gates + + def _check_execution(self): + """Check if some blocks in the front layer can be executed in the current configuration. + + Returns: + (list): executable blocks if there are, ``None`` otherwise. + """ + executable_blocks = [] + for block in self._front_layer: + if ( + self.circuit.get_physical_qubits(block) in self.connectivity.edges + or not self.circuit.circuit_blocks.search_by_index(block).entangled + ): + executable_blocks.append(block) + if len(executable_blocks) == 0: + return None + return executable_blocks + + def _execute_blocks(self, blocklist: list): + """Execute a list of blocks: + -Remove the correspondent nodes from the dag and circuit representation. + -Add the executed blocks to the routed circuit. + -Update the dag layers and front layer. + """ + for block_id in blocklist: + block = self.circuit.circuit_blocks.search_by_index(block_id) + self.circuit.execute_block(block) + self._dag.remove_node(block_id) + self._update_front_layer() + + def _update_front_layer(self): + """Update the front layer of the dag.""" + for layer, nodes in enumerate(nx.topological_generations(self._dag)): + for node in nodes: + self._dag.nodes[node]["layer"] = layer + self._front_layer = [ + node[0] for node in self._dag.nodes(data="layer") if node[1] == 0 + ] + + def _preprocessing(self, circuit: Circuit, initial_layout: dict): + """The following objects will be initialised: + - circuit: class to represent circuit and to perform logical-physical qubit mapping. + - _final_measurements: measurement gates at the end of the circuit. + - _front_layer: list containing the blocks to be executed. + """ + copied_circuit = _copy_circuit(circuit) + self._final_measurements = self._detach_final_measurements(copied_circuit) + self.circuit = CircuitMap(initial_layout, copied_circuit) + self._dag = _create_dag(self.circuit.blocks_qubits_pairs()) + self._update_front_layer() + + def _detach_final_measurements(self, circuit: Circuit): + """Detach measurement gates at the end of the circuit for separate handling.""" + final_measurements = [] + for gate in circuit.queue[::-1]: + if isinstance(gate, gates.M): + final_measurements.append(gate) + circuit.queue.remove(gate) + else: + break + if not final_measurements: + return None + return final_measurements[::-1] + + def _append_final_measurements(self, routed_circuit: Circuit): + """Append the final measurment gates on the correct qubits conserving the measurement register.""" + for measurement in self._final_measurements: + original_qubits = measurement.qubits + routed_qubits = ( + self.circuit.circuit_to_physical(qubit) for qubit in original_qubits + ) + routed_circuit.add( + measurement.on_qubits(dict(zip(original_qubits, routed_qubits))) + ) + return routed_circuit class Sabre(Router): @@ -536,9 +468,9 @@ def _preprocessing(self, circuit: Circuit, initial_layout: dict): - _delta_register: list containing the special weigh added to qubits to prevent overlapping swaps. """ - copy_circuit = self._copy_circuit(circuit) - self._final_measurements = self._detach_final_measurements(copy_circuit) - self.circuit = CircuitMap(initial_layout, copy_circuit) + copied_circuit = _copy_circuit(circuit) + self._final_measurements = self._detach_final_measurements(copied_circuit) + self.circuit = CircuitMap(initial_layout, copied_circuit) self._dist_matrix = nx.floyd_warshall_numpy(self.connectivity) self._dag = _create_dag(self.circuit.blocks_qubits_pairs()) self._memory_map = [] @@ -546,15 +478,6 @@ def _preprocessing(self, circuit: Circuit, initial_layout: dict): self._update_front_layer() self._delta_register = [1.0 for _ in range(circuit.nqubits)] - @staticmethod - def _copy_circuit(circuit: Circuit): - """Return a copy of the circuit to avoid altering the original circuit. - This copy conserves the registers of the measurement gates.""" - new_circuit = Circuit(circuit.nqubits) - for gate in circuit.queue: - new_circuit.add(gate) - return new_circuit - def _detach_final_measurements(self, circuit: Circuit): """Detach measurement gates at the end of the circuit for separate handling.""" final_measurements = [] @@ -605,7 +528,7 @@ def _find_new_mapping(self): key for key, value in candidates_evaluation.items() if value == best_cost ] best_candidate = random.choice(best_candidates) - for qubit in self.circuit.logical_to_physical(best_candidate): + for qubit in self.circuit.logical_to_physical(best_candidate, index=True): self._delta_register[qubit] += self.delta self.circuit.update(best_candidate) @@ -626,7 +549,7 @@ def _compute_cost(self, candidate): layer_gates = self._get_dag_layer(layer) avg_layer_distance = 0.0 for gate in layer_gates: - qubits = temporary_circuit.get_physical_qubits(gate) + qubits = temporary_circuit.get_physical_qubits(gate, index=True) avg_layer_distance += ( max(self._delta_register[i] for i in qubits) * (self._dist_matrix[qubits[0], qubits[1]] - 1.0) @@ -647,8 +570,8 @@ def _swap_candidates(self): candidate = tuple( sorted( ( - self.circuit._physical_logical[qubit], - self.circuit._physical_logical[connected], + self.circuit.physical_to_logical(qubit), + self.circuit.physical_to_logical(connected), ) ) ) @@ -657,7 +580,7 @@ def _swap_candidates(self): return candidates def _check_execution(self): - """Check if some gatesblocks in the front layer can be executed in the current configuration. + """Check if some blocks in the front layer can be executed in the current configuration. Returns: (list): executable blocks if there are, ``None`` otherwise. @@ -690,6 +613,18 @@ def _execute_blocks(self, blocklist: list): self._delta_register = [1.0 for _ in self._delta_register] +def _copy_circuit(circuit: Circuit): + """Helper method for :meth:`qibo.transpiler.router`. + + Return a copy of the circuit to avoid altering the original circuit. + This copy conserves the registers of the measurement gates. + """ + new_circuit = Circuit(circuit.nqubits) + for gate in circuit.queue: + new_circuit.add(gate) + return new_circuit + + def _create_dag(gates_qubits_pairs): """Helper method for :meth:`qibo.transpiler.router.Sabre`. Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. diff --git a/src/qibo/transpiler/test_new_ss.py b/src/qibo/transpiler/test_new_ss.py new file mode 100644 index 0000000000..67b1c7ca0f --- /dev/null +++ b/src/qibo/transpiler/test_new_ss.py @@ -0,0 +1,24 @@ +import networkx as nx + +from qibo import Circuit, gates +from qibo.transpiler.router import ShortestPaths + + +def star_connectivity(): + Q = [i for i in range(5)] + chip = nx.Graph() + chip.add_nodes_from(Q) + graph_list = [(Q[i], Q[2]) for i in range(5) if i != 2] + chip.add_edges_from(graph_list) + return chip + + +circuit = Circuit(5) +circuit.add(gates.CNOT(1, 3)) +circuit.add(gates.CNOT(2, 1)) +circuit.add(gates.CNOT(4, 1)) +initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4} +transpiler = ShortestPaths(connectivity=star_connectivity()) +routed_circ, final_layout = transpiler(circuit, initial_layout) + +print(routed_circ.draw()) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 621f7f69e3..1c5dbdbc57 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -5,14 +5,16 @@ from qibo import gates from qibo.models import Circuit from qibo.transpiler.optimizer import Preprocessing -from qibo.transpiler.pipeline import assert_circuit_equivalence +from qibo.transpiler.pipeline import ( + assert_circuit_equivalence, + restrict_connectivity_qubits, +) from qibo.transpiler.placer import Custom, Random, Subgraph, Trivial, assert_placement from qibo.transpiler.router import ( CircuitMap, ConnectivityError, Sabre, ShortestPaths, - _find_gates_qubits_pairs, assert_connectivity, ) @@ -105,22 +107,6 @@ def test_split_setter(split): ) -def test_insufficient_qubits(): - circuit = generate_random_circuit(10, 20) - placer = Trivial() - initial_layout = placer(circuit) - transpiler = ShortestPaths(connectivity=star_connectivity()) - with pytest.raises(ValueError): - transpiler(circuit, initial_layout) - - -def test_find_pairs_error(): - circuit = Circuit(3) - circuit.add(gates.TOFFOLI(0, 1, 2)) - with pytest.raises(ValueError): - _find_gates_qubits_pairs(circuit) - - @pytest.mark.parametrize("gates", [5, 25]) @pytest.mark.parametrize("qubits", [3, 5]) @pytest.mark.parametrize("placer", [Trivial, Random]) @@ -130,9 +116,7 @@ def test_random_circuits_5q(gates, qubits, placer, connectivity, split): placer = placer(connectivity=connectivity) layout_circ = Circuit(5) initial_layout = placer(layout_circ) - transpiler = ShortestPaths( - connectivity=connectivity, verbose=True, sampling_split=split - ) + transpiler = ShortestPaths(connectivity=connectivity, sampling_split=split) circuit = generate_random_circuit(nqubits=qubits, ngates=gates) transpiled_circuit, final_qubit_map = transpiler(circuit, initial_layout) assert transpiler.added_swaps >= 0 @@ -341,3 +325,45 @@ def test_sabre_intermediate_measurements(): routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) circuit_result = routed_circ.execute(nshots=100) assert routed_circ.queue[3].result is measurement.result + + +def test_sabre_restrict_qubits(): + circ = Circuit(3) + circ.add(gates.CZ(0, 1)) + circ.add(gates.CZ(0, 2)) + circ.add(gates.CZ(2, 1)) + initial_layout = {"q0": 0, "q2": 2, "q3": 1} + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) + router = Sabre(connectivity=restricted_connectivity) + routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) + assert_circuit_equivalence( + original_circuit=circ, + transpiled_circuit=routed_circ, + final_map=final_layout, + initial_map=initial_layout, + ) + assert_connectivity(restricted_connectivity, routed_circ) + print(final_layout) + print(routed_circ.draw()) + assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) + + +def test_shortest_paths_restrict_qubits(): + circ = Circuit(3) + circ.add(gates.CZ(0, 1)) + circ.add(gates.CZ(0, 2)) + circ.add(gates.CZ(2, 1)) + initial_layout = {"q0": 0, "q2": 2, "q3": 1} + connectivity = star_connectivity() + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) + router = ShortestPaths(connectivity=restricted_connectivity) + routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) + assert_circuit_equivalence( + original_circuit=circ, + transpiled_circuit=routed_circ, + final_map=final_layout, + initial_map=initial_layout, + ) + assert_connectivity(restricted_connectivity, routed_circ) + assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) From 1e87f5aa77c9e064f46b4559facf103306f15b96 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 12 Dec 2023 14:45:49 +0400 Subject: [PATCH 32/78] completed new shortest paths --- src/qibo/transpiler/router.py | 2 +- src/qibo/transpiler/test_new_ss.py | 2 ++ tests/test_transpiler_router.py | 16 +++------------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 42f4905bdd..03462b53a4 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -277,7 +277,7 @@ def _compute_cost(self, candidate): temporary_circuit = CircuitMap( initial_layout=self.circuit.initial_layout, circuit=Circuit(len(self.circuit.initial_layout)), - blocks=self.circuit.circuit_blocks, + blocks=deepcopy(self.circuit.circuit_blocks), ) temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical)) self._add_swaps(candidate, temporary_circuit) diff --git a/src/qibo/transpiler/test_new_ss.py b/src/qibo/transpiler/test_new_ss.py index 67b1c7ca0f..e878c3f214 100644 --- a/src/qibo/transpiler/test_new_ss.py +++ b/src/qibo/transpiler/test_new_ss.py @@ -17,6 +17,8 @@ def star_connectivity(): circuit.add(gates.CNOT(1, 3)) circuit.add(gates.CNOT(2, 1)) circuit.add(gates.CNOT(4, 1)) +circuit.add(gates.CNOT(4, 2)) +circuit.add(gates.CNOT(4, 3)) initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4} transpiler = ShortestPaths(connectivity=star_connectivity()) routed_circ, final_layout = transpiler(circuit, initial_layout) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 1c5dbdbc57..f20d970608 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -99,25 +99,15 @@ def test_assert_connectivity_3q(): assert_connectivity(star_connectivity(), circuit) -@pytest.mark.parametrize("split", [2.0, -1.0]) -def test_split_setter(split): - with pytest.raises(ValueError): - transpiler = ShortestPaths( - connectivity=star_connectivity(), sampling_split=split - ) - - @pytest.mark.parametrize("gates", [5, 25]) -@pytest.mark.parametrize("qubits", [3, 5]) @pytest.mark.parametrize("placer", [Trivial, Random]) @pytest.mark.parametrize("connectivity", [star_connectivity(), grid_connectivity()]) -@pytest.mark.parametrize("split", [1.0, 0.5]) -def test_random_circuits_5q(gates, qubits, placer, connectivity, split): +def test_random_circuits_5q(gates, placer, connectivity): placer = placer(connectivity=connectivity) layout_circ = Circuit(5) initial_layout = placer(layout_circ) - transpiler = ShortestPaths(connectivity=connectivity, sampling_split=split) - circuit = generate_random_circuit(nqubits=qubits, ngates=gates) + transpiler = ShortestPaths(connectivity=connectivity) + circuit = generate_random_circuit(nqubits=5, ngates=gates) transpiled_circuit, final_qubit_map = transpiler(circuit, initial_layout) assert transpiler.added_swaps >= 0 assert_connectivity(connectivity, transpiled_circuit) From d2d6abdd1733b217c275f52f5abb355cc8a4935c Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Tue, 12 Dec 2023 14:54:36 +0400 Subject: [PATCH 33/78] remove prints --- src/qibo/transpiler/placer.py | 2 +- src/qibo/transpiler/router.py | 10 +--------- src/qibo/transpiler/test_new_ss.py | 26 -------------------------- tests/test_transpiler_router.py | 2 -- 4 files changed, 2 insertions(+), 38 deletions(-) delete mode 100644 src/qibo/transpiler/test_new_ss.py diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index fb687a38b2..97bfc09a56 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -11,7 +11,7 @@ def assert_placement( - circuit: Circuit, layout: dict, connectivity: nx.graph = None + circuit: Circuit, layout: dict, connectivity: nx.Graph = None ) -> bool: """Check if layout is in the correct form and matches the number of qubits of the circuit. diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 03462b53a4..dd9f583130 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -188,9 +188,7 @@ def __call__(self, circuit: Circuit, initial_layout: dict): """ self._preprocessing(circuit=circuit, initial_layout=initial_layout) while self._dag.number_of_nodes() != 0: - print("new step, front layer:", self._front_layer) execute_block_list = self._check_execution() - print("executable gates:", execute_block_list) if execute_block_list is not None: self._execute_blocks(execute_block_list) else: @@ -210,17 +208,13 @@ def _find_new_mapping(self): for candidate in self._candidates(): cost = self._compute_cost(candidate) candidates_evaluation.append((candidate, cost)) - print("candidate evaluation:", (candidate, cost)) best_cost = min(candidate[1] for candidate in candidates_evaluation) best_candidates = [ candidate[0] for candidate in candidates_evaluation if candidate[1] == best_cost ] - print("best cost:", best_cost) - print("best candidates:", best_candidates) best_candidate = random.choice(best_candidates) - print("best candidate:", best_candidate) self._add_swaps(best_candidate, self.circuit) def _candidates(self): @@ -270,6 +264,7 @@ def _add_swaps(candidate: tuple, circuitmap: CircuitMap): def _compute_cost(self, candidate): """Greedy algorithm to decide which path to take, and how qubits should walk. + The cost is computed as minus the number of successive gates that can be executed. Returns: (list, int): best path to move qubits and qubit meeting point in the path. @@ -292,7 +287,6 @@ def _compute_cost(self, candidate): ] all_executed = True for block in temporary_front_layer: - print("check temp block:", block) if ( temporary_circuit.get_physical_qubits(block) in self.connectivity.edges @@ -305,12 +299,10 @@ def _compute_cost(self, candidate): temporary_circuit.circuit_blocks.search_by_index(block) ) temporary_dag.remove_node(block) - print("executed") else: all_executed = False if not all_executed: break - print("cost:", successive_executed_gates) return -successive_executed_gates def _check_execution(self): diff --git a/src/qibo/transpiler/test_new_ss.py b/src/qibo/transpiler/test_new_ss.py deleted file mode 100644 index e878c3f214..0000000000 --- a/src/qibo/transpiler/test_new_ss.py +++ /dev/null @@ -1,26 +0,0 @@ -import networkx as nx - -from qibo import Circuit, gates -from qibo.transpiler.router import ShortestPaths - - -def star_connectivity(): - Q = [i for i in range(5)] - chip = nx.Graph() - chip.add_nodes_from(Q) - graph_list = [(Q[i], Q[2]) for i in range(5) if i != 2] - chip.add_edges_from(graph_list) - return chip - - -circuit = Circuit(5) -circuit.add(gates.CNOT(1, 3)) -circuit.add(gates.CNOT(2, 1)) -circuit.add(gates.CNOT(4, 1)) -circuit.add(gates.CNOT(4, 2)) -circuit.add(gates.CNOT(4, 3)) -initial_layout = {"q0": 0, "q1": 1, "q2": 2, "q3": 3, "q4": 4} -transpiler = ShortestPaths(connectivity=star_connectivity()) -routed_circ, final_layout = transpiler(circuit, initial_layout) - -print(routed_circ.draw()) diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index f20d970608..bee84e47b6 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -334,8 +334,6 @@ def test_sabre_restrict_qubits(): initial_map=initial_layout, ) assert_connectivity(restricted_connectivity, routed_circ) - print(final_layout) - print(routed_circ.draw()) assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) From 9b31bfa047910dfc878e8ebd0cd664210d228506 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Wed, 13 Dec 2023 14:39:38 +0400 Subject: [PATCH 34/78] fix coverage and tests --- src/qibo/transpiler/pipeline.py | 11 ++++- src/qibo/transpiler/placer.py | 2 +- src/qibo/transpiler/star_connectivity.py | 2 + tests/test_transpiler_pipeline.py | 55 ++++++++++++++++++++++-- tests/test_transpiler_placer.py | 25 ++++++++++- 5 files changed, 87 insertions(+), 8 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index 96e4be76d6..289429e40f 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -120,8 +120,12 @@ def assert_transpiling( if original_circuit.nqubits != transpiled_circuit.nqubits: qubit_matcher = Preprocessing(connectivity=connectivity) original_circuit = qubit_matcher(circuit=original_circuit) - assert_placement(circuit=original_circuit, layout=initial_layout) - assert_placement(circuit=transpiled_circuit, layout=final_layout) + assert_placement( + circuit=original_circuit, layout=initial_layout, connectivity=connectivity + ) + assert_placement( + circuit=transpiled_circuit, layout=final_layout, connectivity=connectivity + ) if check_circuit_equivalence: assert_circuit_equivalence( original_circuit=original_circuit, @@ -207,8 +211,10 @@ def __call__(self, circuit): final_layout = None for transpiler_pass in self.passes: if isinstance(transpiler_pass, Optimizer): + transpiler_pass.connectivity = self.connectivity circuit = transpiler_pass(circuit) elif isinstance(transpiler_pass, Placer): + transpiler_pass.connectivity = self.connectivity if self.initial_layout == None: self.initial_layout = transpiler_pass(circuit) else: @@ -217,6 +223,7 @@ def __call__(self, circuit): "You are defining more than one placer pass.", ) elif isinstance(transpiler_pass, Router): + transpiler_pass.connectivity = self.connectivity if self.initial_layout is not None: circuit, final_layout = transpiler_pass( circuit, self.initial_layout diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index 97bfc09a56..d47bc30da0 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -272,7 +272,7 @@ def __call__(self, circuit): return dict(zip(dict_keys, list(mapping.values()))) if cost < final_cost: final_graph = graph - final_mapping = {dict_keys[i]: mapping[i] for i in range(len(mapping))} + final_mapping = mapping final_cost = cost return dict(zip(dict_keys, list(final_mapping.values()))) diff --git a/src/qibo/transpiler/star_connectivity.py b/src/qibo/transpiler/star_connectivity.py index 3439211be9..693df030c1 100644 --- a/src/qibo/transpiler/star_connectivity.py +++ b/src/qibo/transpiler/star_connectivity.py @@ -3,6 +3,7 @@ from qibo.transpiler.router import ConnectivityError +# TODO: split into routing plus placer steps class StarConnectivity(Router): """Transforms an arbitrary circuit to one that can be executed on hardware. @@ -23,6 +24,7 @@ class StarConnectivity(Router): def __init__(self, connectivity=None, middle_qubit: int = 2): self.middle_qubit = middle_qubit + self.connectivity = connectivity def __call__(self, circuit: Circuit, initial_layout=None): """Apply the transpiler transformation on a given circuit. diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index bc82956485..feeaba4e84 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -14,7 +14,7 @@ restrict_connectivity_qubits, ) from qibo.transpiler.placer import Random, ReverseTraversal, Trivial -from qibo.transpiler.router import ShortestPaths +from qibo.transpiler.router import Sabre, ShortestPaths from qibo.transpiler.unroller import NativeGates, Unroller @@ -174,13 +174,24 @@ def test_is_satisfied_false_connectivity(): assert not default_transpiler.is_satisfied(circuit) +@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) +@pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) @pytest.mark.parametrize( "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] ) -def test_custom_passes(circ): +def test_custom_passes(circ, placer, routing, reps): custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) - custom_passes.append(Random(connectivity=star_connectivity())) + if placer == ReverseTraversal: + custom_passes.append( + placer( + connectivity=star_connectivity(), + routing_algorithm=routing(connectivity=star_connectivity()), + ) + ) + else: + custom_passes.append(placer(connectivity=star_connectivity())) custom_passes.append(ShortestPaths(connectivity=star_connectivity())) custom_passes.append(Unroller(native_gates=NativeGates.default())) custom_pipeline = Passes( @@ -200,6 +211,44 @@ def test_custom_passes(circ): ) +@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) +@pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) +def test_custom_passes_restict(reps, placer, routing): + circ = generate_random_circuit(nqubits=3, ngates=20) + custom_passes = [] + custom_passes.append(Preprocessing(connectivity=star_connectivity())) + if placer == ReverseTraversal: + custom_passes.append( + placer( + connectivity=star_connectivity(), + routing_algorithm=routing(connectivity=star_connectivity()), + ) + ) + else: + custom_passes.append(placer(connectivity=star_connectivity())) + custom_passes.append(routing(connectivity=star_connectivity())) + custom_passes.append(Unroller(native_gates=NativeGates.default())) + custom_pipeline = Passes( + custom_passes, + connectivity=star_connectivity(), + native_gates=NativeGates.default(), + on_qubits=[1, 2, 3], + ) + transpiled_circ, final_layout = custom_pipeline(circ) + initial_layout = custom_pipeline.get_initial_layout() + print(initial_layout) + print(final_layout) + assert_transpiling( + original_circuit=circ, + transpiled_circuit=transpiled_circ, + connectivity=restrict_connectivity_qubits(star_connectivity(), [1, 2, 3]), + initial_layout=initial_layout, + final_layout=final_layout, + native_gates=NativeGates.default(), + ) + + @pytest.mark.parametrize( "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] ) diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index ae34bcec1e..c76e341981 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -11,6 +11,7 @@ ReverseTraversal, Subgraph, Trivial, + _find_gates_qubits_pairs, assert_mapping_consistency, assert_placement, ) @@ -92,6 +93,22 @@ def test_mapping_consistency_restricted_error(layout): assert_mapping_consistency(layout, restricted_connecyivity) +def test_gates_qubits_pairs(): + circuit = Circuit(5) + circuit.add(gates.CNOT(0, 1)) + circuit.add(gates.CNOT(1, 2)) + circuit.add(gates.M(1, 2)) + gates_qubits_pairs = _find_gates_qubits_pairs(circuit) + assert gates_qubits_pairs == [[0, 1], [1, 2]] + + +def test_gates_qubits_pairs_error(): + circuit = Circuit(5) + circuit.add(gates.TOFFOLI(0, 1, 2)) + with pytest.raises(ValueError): + gates_qubits_pairs = _find_gates_qubits_pairs(circuit) + + def test_trivial(): circuit = Circuit(5) connectivity = star_connectivity() @@ -125,12 +142,16 @@ def test_trivial_error(): "custom_layout", [[4, 3, 2, 1, 0], {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0}] ) @pytest.mark.parametrize("give_circuit", [True, False]) -def test_custom(custom_layout, give_circuit): +@pytest.mark.parametrize("give_connectivity", [True, False]) +def test_custom(custom_layout, give_circuit, give_connectivity): if give_circuit: circuit = Circuit(5) else: circuit = None - connectivity = star_connectivity() + if give_connectivity: + connectivity = star_connectivity() + else: + connectivity = None placer = Custom(connectivity=connectivity, map=custom_layout) layout = placer(circuit) assert layout == {"q0": 4, "q1": 3, "q2": 2, "q3": 1, "q4": 0} From 44e2560631f4e90ebf068d9da3e9aa7dd05aa28b Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Thu, 14 Dec 2023 12:34:56 +0400 Subject: [PATCH 35/78] small correction in unroller --- src/qibo/transpiler/unroller.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/qibo/transpiler/unroller.py b/src/qibo/transpiler/unroller.py index ef5f0c8d30..1358056600 100644 --- a/src/qibo/transpiler/unroller.py +++ b/src/qibo/transpiler/unroller.py @@ -11,9 +11,8 @@ class NativeGates(Flag): """Define native gates supported by the unroller. A native gate set should contain at least one two-qubit gate (CZ or iSWAP) and at least one single qubit gate (GPI2 or U3). - Gates I, Z, RZ and M are always included in the single qubit native gates set. - - Should have the same names with qibo gates. + Possible values are: + I, Z, RZ, M, GPI2, U3, CZ, iSWAP. """ I = auto() @@ -56,8 +55,6 @@ class Unroller: """Translates a circuit to native gates. Args: - circuit (:class:`qibo.models.circuit.Circuit`): circuit model to translate - into native gates. native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates to use in the transpiled circuit. Returns: @@ -71,6 +68,14 @@ def __init__( self.native_gates = native_gates def __call__(self, circuit: Circuit): + """Decomposes a circuit into native gates. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit model to decompose. + + Returns: + (:class:`qibo.models.circuit.Circuit`): equivalent circuit with native gates. + """ translated_circuit = circuit.__class__(circuit.nqubits) for gate in circuit.queue: translated_circuit.add( From a4b691a7e2030b7105a39897776b0553a07528a9 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Thu, 14 Dec 2023 20:34:53 +0100 Subject: [PATCH 36/78] fix documentation tests --- doc/source/code-examples/advancedexamples.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst index 821b6e1141..4a3fd766f8 100644 --- a/doc/source/code-examples/advancedexamples.rst +++ b/doc/source/code-examples/advancedexamples.rst @@ -1293,17 +1293,17 @@ calibration matrix and use it modify the final state after the circuit execution .. testcode:: - from qibo.models.error_mitigation import apply_readout_mitigation, calibration_matrix + from qibo.models.error_mitigation import apply_cal_mat_readout_mitigation, get_calibration_matrix nshots = 10000 # compute the calibration matrix - calibration = calibration_matrix( + calibration = get_calibration_matrix( nqubits, backend=backend, noise_model=noise, nshots=nshots ) # execute the circuit state = backend.execute_circuit(noise.apply(c), nshots=nshots) # mitigate the readout errors - mit_state = apply_readout_mitigation(state, calibration) + mit_state = apply_cal_mat_readout_mitigation(state, calibration) mit_val = mit_state.expectation_from_samples(obs) print(mit_val) # 0.5945794816381054 From 8fcb3a208b038323f944ea63edfc970eec4a1752 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Thu, 14 Dec 2023 23:17:42 +0100 Subject: [PATCH 37/78] unify readout mitigation --- src/qibo/models/error_mitigation.py | 232 +++++++++++++--------------- 1 file changed, 111 insertions(+), 121 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 0226e1914b..075c1111ab 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -109,6 +109,7 @@ def ZNE( solve_for_gammas=False, insertion_gate="CNOT", readout: dict = {}, + qubit_map=None, backend=None, ): """Runs the Zero Noise Extrapolation method for error mitigation. @@ -153,21 +154,15 @@ def ZNE( noisy_circuit = get_noisy_circuit( circuit, num_insertions, insertion_gate=insertion_gate ) - if "ncircuits" in readout.keys(): - circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( - noisy_circuit, noise_model, nshots, readout["ncircuits"], backend - ) - else: - if noise_model is not None and backend.name != "qibolab": - noisy_circuit = noise_model.apply(noisy_circuit) - circuit_result = backend.execute_circuit(noisy_circuit, nshots=nshots) - if "calibration_matrix" in readout.keys() is not None: - circuit_result = apply_cal_mat_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["ibu_iters"] - ) - val = circuit_result.expectation_from_samples(observable) - if "ncircuits" in readout.keys(): - val /= circuit_result_cal.expectation_from_samples(observable) + val = apply_readout_mitigation( + noisy_circuit, + observable, + noise_model, + nshots, + readout, + qubit_map, + backend=backend, + ) expected_values.append(val) gamma = get_gammas(noise_levels, analytical=solve_for_gammas) @@ -265,6 +260,7 @@ def CDR( n_training_samples: int = 100, full_output: bool = False, readout: dict = {}, + qubit_map=None, backend=None, ): """Runs the Clifford Data Regression error mitigation method. @@ -312,40 +308,16 @@ def CDR( for circ in training_circuits: val = circ(nshots=nshots).expectation_from_samples(observable) train_val["noise-free"].append(val) - if "ncircuits" in readout.keys(): - circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( - circ, noise_model, nshots, readout["ncircuits"], backend - ) - else: - if noise_model is not None and backend.name != "qibolab": - circ = noise_model.apply(circ) - circuit_result = backend.execute_circuit(circ, nshots=nshots) - if "calibration_matrix" in readout.keys() is not None: - circuit_result = apply_cal_mat_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["ibu_iters"] - ) - val = circuit_result.expectation_from_samples(observable) - if "ncircuits" in readout.keys(): - val /= circuit_result_cal.expectation_from_samples(observable) + val = apply_readout_mitigation( + circ, observable, noise_model, nshots, readout, qubit_map, backend=backend + ) train_val["noisy"].append(val) optimal_params = curve_fit(model, train_val["noisy"], train_val["noise-free"])[0] - if "ncircuits" in readout.keys(): - circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( - circuit, noise_model, nshots, readout["ncircuits"], backend - ) - else: - if noise_model is not None and backend.name != "qibolab": - circuit = noise_model.apply(circuit) - circuit_result = backend.execute_circuit(circuit, nshots=nshots) - if "calibration_matrix" in readout.keys() is not None: - circuit_result = apply_cal_mat_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["ibu_iters"] - ) - val = circuit_result.expectation_from_samples(observable) - if "ncircuits" in readout.keys(): - val /= circuit_result_cal.expectation_from_samples(observable) + val = apply_readout_mitigation( + circuit, observable, noise_model, nshots, readout, qubit_map, backend=backend + ) mit_val = model(val, *optimal_params) if full_output is True: @@ -365,6 +337,7 @@ def vnCDR( insertion_gate: str = "CNOT", full_output: bool = False, readout: dict = {}, + qubit_map=None, backend=None, ): """Runs the variable-noise Clifford Data Regression error mitigation method. @@ -419,24 +392,15 @@ def vnCDR( train_val["noise-free"].append(val) for level in noise_levels: noisy_c = get_noisy_circuit(circ, level, insertion_gate=insertion_gate) - if "ncircuits" in readout.keys(): - ( - circuit_result, - circuit_result_cal, - ) = apply_randomized_readout_mitigation( - noisy_c, noise_model, nshots, readout["ncircuits"], backend - ) - else: - if noise_model is not None and backend.name != "qibolab": - noisy_c = noise_model.apply(noisy_c) - circuit_result = backend.execute_circuit(noisy_c, nshots=nshots) - if "calibration_matrix" in readout.keys(): - circuit_result = apply_cal_mat_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["ibu_iters"] - ) - val = circuit_result.expectation_from_samples(observable) - if "ncircuits" in readout.keys(): - val /= circuit_result_cal.expectation_from_samples(observable) + val = apply_readout_mitigation( + noisy_c, + observable, + noise_model, + nshots, + readout, + qubit_map, + backend=backend, + ) train_val["noisy"].append(val) noisy_array = np.array(train_val["noisy"]).reshape(-1, len(noise_levels)) @@ -447,21 +411,15 @@ def vnCDR( val = [] for level in noise_levels: noisy_c = get_noisy_circuit(circuit, level, insertion_gate=insertion_gate) - if "ncircuits" in readout.keys(): - circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( - noisy_c, noise_model, nshots, readout["ncircuits"], backend - ) - else: - if noise_model is not None and backend.name != "qibolab": - noisy_c = noise_model.apply(noisy_c) - circuit_result = backend.execute_circuit(noisy_c, nshots=nshots) - if "calibration_matrix" in readout.keys(): - circuit_result = apply_cal_mat_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["ibu_iters"] - ) - expval = circuit_result.expectation_from_samples(observable) - if "ncircuits" in readout.keys(): - expval /= circuit_result_cal.expectation_from_samples(observable) + expval = apply_readout_mitigation( + noisy_c, + observable, + noise_model, + nshots, + readout, + qubit_map, + backend=backend, + ) val.append(expval) mit_val = model(np.array(val).reshape(-1, 1), *optimal_params[0])[0] @@ -500,7 +458,7 @@ def iterative_bayesian_unfolding(probabilities, response_matrix, iterations=10): def get_calibration_matrix( - nqubits, qubit_map, noise_model=None, nshots: int = 10000, backend=None + nqubits, qubit_map=None, noise_model=None, nshots: int = 10000, backend=None ): """Computes the calibration matrix for readout mitigation. @@ -534,11 +492,11 @@ def get_calibration_matrix( circuit.add(gates.X(qubit)) circuit.add(gates.M(*range(nqubits))) - if noise_model is not None and backend.name != "qibolab": - circuit = noise_model.apply(circuit) - circuit = transpile_circ(circuit, qubit_map, backend) + circuit_result = _circuit_conf( + circuit, qubit_map, noise_model, nshots, backend=backend + ) - frequencies = backend.execute_circuit(circuit, nshots=nshots).frequencies() + frequencies = circuit_result.frequencies() column = np.zeros(2**nqubits) for key, value in frequencies.items(): @@ -590,7 +548,12 @@ def apply_cal_mat_readout_mitigation(state, calibration_matrix, iterations=None) def apply_randomized_readout_mitigation( - circuit, noise_model=None, nshots: int = int(1e3), ncircuits: int = 10, backend=None + circuit, + noise_model=None, + nshots: int = int(1e3), + ncircuits: int = 10, + qubit_map=None, + backend=None, ): """Readout mitigation method that transforms the bias in an expectation value into a measurable multiplicative factor. This factor can be eliminated at the expense of increased sampling complexity for the observable. @@ -646,9 +609,10 @@ def apply_randomized_readout_mitigation( for circ in circuits: circ.add(x_gate) circ.add(gates.M(*meas_qubits)) - if noise_model is not None and backend.name != "qibolab": - circ = noise_model.apply(circ) - result = backend.execute_circuit(circ, nshots=nshots_r) + + result = _circuit_conf( + circ, qubit_map, noise_model, nshots_r, backend=backend + ) result._samples = result.apply_bitflips(error_map) results.append(result) freqs.append(result.frequencies(binary=False)) @@ -664,6 +628,39 @@ def apply_randomized_readout_mitigation( return results +def apply_readout_mitigation( + circuit, + observable, + noise_model=None, + nshots: int = int(1e3), + readout={}, + qubit_map=None, + backend=None, +): + if "ncircuits" in readout: + circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( + circuit, noise_model, nshots, readout["ncircuits"], backend + ) + else: + circuit_result = _circuit_conf( + circuit, qubit_map, noise_model, nshots, backend=backend + ) + + if "calibration_matrix" in readout: + circuit_result = apply_cal_mat_readout_mitigation( + circuit_result, + readout["calibration_matrix"], + readout.get("ibu_iters", None), + ) + + exp_val = circuit_result.expectation_from_samples(observable) + + if "ncircuits" in readout: + exp_val /= circuit_result_cal.expectation_from_samples(observable) + + return exp_val + + def sample_clifford_training_circuit( circuit, backend=None, @@ -726,7 +723,7 @@ def sample_clifford_training_circuit( return sampled_circuit -def error_sensitive_circuit(circuit, observable, fuse=True, backend=None): +def error_sensitive_circuit(circuit, observable, backend=None): """ Generates a Clifford circuit that preserves the same circuit frame as the input circuit, and stabilizes the specified Pauli observable. @@ -804,9 +801,6 @@ def error_sensitive_circuit(circuit, observable, fuse=True, backend=None): for gate in sampled_circuit.queue: sensitive_circuit.add(gate) - if fuse: - sensitive_circuit = sensitive_circuit.fuse(max_qubits=1) - return sensitive_circuit, sampled_circuit, adjustment_gates @@ -872,21 +866,14 @@ def ICS( circuit_result = training_circuit(nshots=nshots) expectation = observable.expectation_from_samples(circuit_result.frequencies()) - if noise_model is not None and backend.name != "qibolab": - training_circuit = noise_model.apply(training_circuit) - - training_circuit = transpile_circ(training_circuit, qubit_map, backend) - circuit_result = backend.execute_circuit(training_circuit, nshots=nshots) - - if "calibration_matrix" in readout.keys(): - circuit_result = apply_cal_mat_readout_mitigation( - circuit_result, - readout["calibration_matrix"], - readout["ibu_iters"], - ) - - noisy_expectation = observable.expectation_from_samples( - circuit_result.frequencies() + noisy_expectation = apply_readout_mitigation( + training_circuit, + observable, + noise_model, + nshots, + readout, + qubit_map, + backend=backend, ) data["noise-free"].append(expectation) @@ -896,22 +883,10 @@ def ICS( dep_param = np.mean(lambda_list) dep_param_std = np.std(lambda_list) - circuit = circuit.fuse(max_qubits=1) - - if noise_model is not None and backend.name != "qibolab": - circuit = noise_model.apply(circuit) - - circuit = transpile_circ(circuit, qubit_map, backend) - circuit_result = backend.execute_circuit(circuit, nshots=nshots) - - if "calibration_matrix" in readout.keys(): - circuit_result = apply_cal_mat_readout_mitigation( - circuit_result, readout["calibration_matrix"], readout["ibu_iters"] - ) - - noisy_expectation = observable.expectation_from_samples( - circuit_result.frequencies() + noisy_expectation = apply_readout_mitigation( + circuit, observable, noise_model, nshots, readout, qubit_map, backend=backend ) + mitigated_expectation = ( (1 - dep_param) * noisy_expectation @@ -958,3 +933,18 @@ def transpile_circ(circuit, qubit_map, backend): return new_c return circuit + + +def _circuit_conf(circuit, qubit_map, noise_model, nshots, backend=None): + from qibo.transpiler.placer import Custom + + if backend.name == "qibolab": + backend.transpiler.passes[1] = Custom( + map=qubit_map, connectivity=backend.platform.topology + ) + elif noise_model is not None: + circuit = noise_model.apply(circuit) + + circuit_result = backend.execute_circuit(circuit, nshots=nshots) + + return circuit_result From e0e8c716bc0a8efe3620f36de51b27c8d6eb715e Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 15 Dec 2023 00:22:31 +0100 Subject: [PATCH 38/78] fix coverage --- src/qibo/models/error_mitigation.py | 56 +++++++++++++++------------ tests/test_models_error_mitigation.py | 19 +++------ 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 075c1111ab..1f66cf2a12 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -597,11 +597,12 @@ def apply_randomized_readout_mitigation( error_map = {} for j, gate in enumerate(x_gate): - if gate.name == "x": - if gate.qubits[0] in meas_qubits: - error_map[gate.qubits[0]] = 1 - else: - x_gate.queue[j] = gates.I(gate.qubits[0]) + if isinstance(gate, gates.X) and gate.qubits[0] in meas_qubits: + error_map[gate.qubits[0]] = 1 + # if gate.qubits[0] in meas_qubits: + # error_map[gate.qubits[0]] = 1 + # else: + # x_gate.queue[j] = gates.I(gate.qubits[0]) circuits = [circuit_c, cal_circuit] results = [] @@ -637,6 +638,9 @@ def apply_readout_mitigation( qubit_map=None, backend=None, ): + if backend is None: # pragma: no cover + backend = GlobalBackend() + if "ncircuits" in readout: circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( circuit, noise_model, nshots, readout["ncircuits"], backend @@ -912,33 +916,35 @@ def ICS( return mitigated_expectation -def transpile_circ(circuit, qubit_map, backend): - from qibo.transpiler.unitary_decompositions import u3_decomposition +# def transpile_circ(circuit, qubit_map, backend): +# from qibo.transpiler.unitary_decompositions import u3_decomposition - if backend.name == "qibolab": - if qubit_map is None: - qubit_map = list(range(circuit.nqubits)) - new_c = circuit.__class__(backend.platform.nqubits) - for gate in circuit.queue: - qubits = [qubit_map[j] for j in gate.qubits] - if isinstance(gate, gates.M): - new_gate = gates.M(*tuple(qubits), **gate.init_kwargs) - new_gate.result = gate.result - new_c.add(new_gate) - elif isinstance(gate, gates.I): - new_c.add(gate.__class__(*tuple(qubits), **gate.init_kwargs)) - else: - matrix = gate.matrix() - new_c.add(gates.U3(qubits[0], *u3_decomposition(matrix))) - return new_c +# if backend.name == "qibolab": +# if qubit_map is None: +# qubit_map = list(range(circuit.nqubits)) +# new_c = circuit.__class__(backend.platform.nqubits) +# for gate in circuit.queue: +# qubits = [qubit_map[j] for j in gate.qubits] +# if isinstance(gate, gates.M): +# new_gate = gates.M(*tuple(qubits), **gate.init_kwargs) +# new_gate.result = gate.result +# new_c.add(new_gate) +# elif isinstance(gate, gates.I): +# new_c.add(gate.__class__(*tuple(qubits), **gate.init_kwargs)) +# else: +# matrix = gate.matrix() +# new_c.add(gates.U3(qubits[0], *u3_decomposition(matrix))) +# return new_c - return circuit +# return circuit def _circuit_conf(circuit, qubit_map, noise_model, nshots, backend=None): from qibo.transpiler.placer import Custom - if backend.name == "qibolab": + if backend is None: # pragma: no cover + backend = GlobalBackend() + elif backend.name == "qibolab": # pragma: no cover backend.transpiler.passes[1] = Custom( map=qubit_map, connectivity=backend.platform.topology ) diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 0d19979e60..78a9a1d830 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -8,8 +8,7 @@ CDR, ICS, ZNE, - apply_cal_mat_readout_mitigation, - apply_randomized_readout_mitigation, + apply_readout_mitigation, get_calibration_matrix, sample_clifford_training_circuit, sample_training_circuit_cdr, @@ -287,6 +286,9 @@ def test_readout_mitigation(backend, nqubits, method, ibu_iters): calibration = get_calibration_matrix( nqubits, None, noise, nshots=nshots, backend=backend ) + readout = {"calibration_matrix": calibration, "ibu_iters": ibu_iters} + elif method == "randomized": + readout = {"ncircuits": 10} # Define the observable obs = np.prod([Z(i) for i in range(nqubits)]) obs = SymbolicHamiltonian(obs, backend=backend) @@ -297,17 +299,8 @@ def test_readout_mitigation(backend, nqubits, method, ibu_iters): # get noisy expected val state = backend.execute_circuit(noise.apply(c), nshots=nshots) noisy_val = state.expectation_from_samples(obs) - if method == "cal_matrix": - mit_state = apply_cal_mat_readout_mitigation(state, calibration, ibu_iters) - mit_val = mit_state.expectation_from_samples(obs) - elif method == "randomized": - ncircuits = 10 - result, result_cal = apply_randomized_readout_mitigation( - c, noise, nshots, ncircuits, backend - ) - mit_val = result.expectation_from_samples( - obs - ) / result_cal.expectation_from_samples(obs) + + mit_val = apply_readout_mitigation(c, obs, noise, nshots, readout, backend=backend) assert np.abs(true_val - mit_val) <= np.abs(true_val - noisy_val) From 1443cb67b45542d49a2a0fdf6d03562795db8d15 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 15 Dec 2023 02:38:55 +0100 Subject: [PATCH 39/78] fix coverage (raise error) --- tests/test_models_error_mitigation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 78a9a1d830..0dc5fd395c 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -208,6 +208,7 @@ def test_sample_training_circuit(nqubits): c.queue[j].clifford = True with pytest.raises(ValueError): sample_training_circuit_cdr(c) + with pytest.raises(ValueError): sample_clifford_training_circuit(c) From f764ed87a2201ad8301911121cf27b511f72aa87 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 19 Dec 2023 11:24:44 +0400 Subject: [PATCH 40/78] initial commit --- src/qibo/models/encodings.py | 56 ++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 9621d80fcd..928b361fe2 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -10,7 +10,7 @@ from qibo.models.circuit import Circuit -def unary_encoder(data): +def unary_encoder(data, architecture: str = "diagonal"): """Creates circuit that performs the unary encoding of ``data``. Given a classical ``data`` array :math:`\\mathbf{x} \\in \\mathbb{R}^{d}` such that @@ -50,10 +50,18 @@ def unary_encoder(data): ValueError, f"len(data) must be a power of 2, but it is {len(data)}." ) + if not isinstance(architecture, str): + raise_error( + TypeError, + f"``architecture`` must be type str, but it is type {type(architecture)}.", + ) + if architecture not in ["diagonal", "semi-diagonal", "tree"]: + raise_error(ValueError, f"``architecture`` {architecture} not found.") + nqubits = len(data) j_max = int(nqubits / 2) - circuit, _ = _generate_rbs_pairs(nqubits) + circuit, _ = _generate_rbs_pairs(nqubits, architecture=architecture) # calculating phases and setting circuit parameters r_array = np.zeros(nqubits - 1, dtype=float) @@ -74,7 +82,9 @@ def unary_encoder(data): return circuit -def unary_encoder_random_gaussian(nqubits: int, seed=None): +def unary_encoder_random_gaussian( + nqubits: int, architecture: str = "parallel", seed=None +): """Creates a circuit that performs the unary encoding of a random Gaussian state. Given :math:`d` qubits, encodes the quantum state @@ -143,7 +153,7 @@ def unary_encoder_random_gaussian(nqubits: int, seed=None): a=0, b=2 * math.pi, seed=local_state ) - circuit, pairs_rbs = _generate_rbs_pairs(nqubits) + circuit, pairs_rbs = _generate_rbs_pairs(nqubits, architecture) phases = [] for depth, row in enumerate(pairs_rbs, 1): @@ -154,27 +164,29 @@ def unary_encoder_random_gaussian(nqubits: int, seed=None): return circuit -def _generate_rbs_pairs(nqubits): +def _generate_rbs_pairs(nqubits: int, architecture: str): """Generating list of indexes representing the RBS connections and creating circuit with all RBS initialised with 0.0 phase.""" - pairs_rbs = [[(0, int(nqubits / 2))]] - indexes = list(np.array(pairs_rbs).flatten()) - for depth in range(2, int(math.log2(nqubits)) + 1): - pairs_rbs_per_depth = [ - [(index, index + int(nqubits / 2**depth)) for index in indexes] + + if architecture == "tree": + pairs_rbs = [[(0, int(nqubits / 2))]] + indexes = list(np.array(pairs_rbs).flatten()) + for depth in range(2, int(math.log2(nqubits)) + 1): + pairs_rbs_per_depth = [ + [(index, index + int(nqubits / 2**depth)) for index in indexes] + ] + pairs_rbs += pairs_rbs_per_depth + indexes = list(np.array(pairs_rbs_per_depth).flatten()) + + pairs_rbs = [ + [(nqubits - 1 - a, nqubits - 1 - b) for a, b in row] for row in pairs_rbs ] - pairs_rbs += pairs_rbs_per_depth - indexes = list(np.array(pairs_rbs_per_depth).flatten()) - - pairs_rbs = [ - [(nqubits - 1 - a, nqubits - 1 - b) for a, b in row] for row in pairs_rbs - ] - - circuit = Circuit(nqubits) - circuit.add(gates.X(nqubits - 1)) - for row in pairs_rbs: - for pair in row: - circuit.add(gates.RBS(*pair, 0.0, trainable=True)) + + circuit = Circuit(nqubits) + circuit.add(gates.X(nqubits - 1)) + for row in pairs_rbs: + for pair in row: + circuit.add(gates.RBS(*pair, 0.0, trainable=True)) return circuit, pairs_rbs From 83d7ba8eb14fd8a77ee8c3a09efb951b5febf0b8 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 19 Dec 2023 14:02:30 +0400 Subject: [PATCH 41/78] diagonal and semi-diagonal --- src/qibo/models/encodings.py | 130 ++++++++++++++++++++++++++--------- 1 file changed, 99 insertions(+), 31 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 928b361fe2..59648021a0 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -10,7 +10,7 @@ from qibo.models.circuit import Circuit -def unary_encoder(data, architecture: str = "diagonal"): +def unary_encoder(data, architecture: str = "tree"): """Creates circuit that performs the unary encoding of ``data``. Given a classical ``data`` array :math:`\\mathbf{x} \\in \\mathbb{R}^{d}` such that @@ -45,10 +45,6 @@ def unary_encoder(data, architecture: str = "diagonal"): TypeError, f"``data`` must be a 1-dimensional array, but it has dimensions {data.shape}.", ) - elif not math.log2(data.shape[0]).is_integer(): - raise_error( - ValueError, f"len(data) must be a power of 2, but it is {len(data)}." - ) if not isinstance(architecture, str): raise_error( @@ -58,33 +54,44 @@ def unary_encoder(data, architecture: str = "diagonal"): if architecture not in ["diagonal", "semi-diagonal", "tree"]: raise_error(ValueError, f"``architecture`` {architecture} not found.") + if architecture == "tree" and not math.log2(data.shape[0]).is_integer(): + raise_error( + ValueError, + "When ``architecture = 'tree'``, len(data) must be a power of 2. " + + f"However, it is {len(data)}.", + ) + nqubits = len(data) - j_max = int(nqubits / 2) - circuit, _ = _generate_rbs_pairs(nqubits, architecture=architecture) + circuit = Circuit(nqubits) + if architecture == "semi-diagonal": + circuit.add(gates.X(int(nqubits / 2))) + else: + circuit.add(gates.X(nqubits - 1)) + circuit_rbs, _ = _generate_rbs_pairs(nqubits, architecture=architecture) + circuit += circuit_rbs # calculating phases and setting circuit parameters - r_array = np.zeros(nqubits - 1, dtype=float) - phases = np.zeros(nqubits - 1, dtype=float) - for j in range(1, j_max + 1): - r_array[j_max + j - 2] = math.sqrt(data[2 * j - 1] ** 2 + data[2 * j - 2] ** 2) - theta = math.acos(data[2 * j - 2] / r_array[j_max + j - 2]) - if data[2 * j - 1] < 0.0: - theta = 2 * math.pi - theta - phases[j_max + j - 2] = theta - - for j in range(j_max - 1, 0, -1): - r_array[j - 1] = math.sqrt(r_array[2 * j] ** 2 + r_array[2 * j - 1] ** 2) - phases[j - 1] = math.acos(r_array[2 * j - 1] / r_array[j - 1]) + phases = _rbs_angles_deterministic(data, nqubits, architecture) + # r_array = np.zeros(nqubits - 1, dtype=float) + # phases = np.zeros(nqubits - 1, dtype=float) + # for j in range(1, j_max + 1): + # r_array[j_max + j - 2] = math.sqrt(data[2 * j - 1] ** 2 + data[2 * j - 2] ** 2) + # theta = math.acos(data[2 * j - 2] / r_array[j_max + j - 2]) + # if data[2 * j - 1] < 0.0: + # theta = 2 * math.pi - theta + # phases[j_max + j - 2] = theta + + # for j in range(j_max - 1, 0, -1): + # r_array[j - 1] = math.sqrt(r_array[2 * j] ** 2 + r_array[2 * j - 1] ** 2) + # phases[j - 1] = math.acos(r_array[2 * j - 1] / r_array[j - 1]) circuit.set_parameters(phases) return circuit -def unary_encoder_random_gaussian( - nqubits: int, architecture: str = "parallel", seed=None -): +def unary_encoder_random_gaussian(nqubits: int, architecture: str = "tree", seed=None): """Creates a circuit that performs the unary encoding of a random Gaussian state. Given :math:`d` qubits, encodes the quantum state @@ -153,7 +160,10 @@ def unary_encoder_random_gaussian( a=0, b=2 * math.pi, seed=local_state ) - circuit, pairs_rbs = _generate_rbs_pairs(nqubits, architecture) + circuit = Circuit(nqubits) + circuit.add(gates.X(nqubits - 1)) + circuit_rbs, pairs_rbs = _generate_rbs_pairs(nqubits, architecture) + circuit += circuit_rbs phases = [] for depth, row in enumerate(pairs_rbs, 1): @@ -168,6 +178,17 @@ def _generate_rbs_pairs(nqubits: int, architecture: str): """Generating list of indexes representing the RBS connections and creating circuit with all RBS initialised with 0.0 phase.""" + if architecture == "diagonal": + pairs_rbs = np.arange(nqubits) + pairs_rbs = [[pair] for pair in zip(pairs_rbs[:-1], pairs_rbs[1:])] + + if architecture == "semi-diagonal": + index = int(nqubits / 2) - 1 + pairs_rbs = [[(index, index + 1)]] + for k in range(index): + target, control = pairs_rbs[k][0][0], pairs_rbs[k][-1][1] + pairs_rbs.append([(target - 1, target), (control, control + 1)]) + if architecture == "tree": pairs_rbs = [[(0, int(nqubits / 2))]] indexes = list(np.array(pairs_rbs).flatten()) @@ -178,19 +199,66 @@ def _generate_rbs_pairs(nqubits: int, architecture: str): pairs_rbs += pairs_rbs_per_depth indexes = list(np.array(pairs_rbs_per_depth).flatten()) - pairs_rbs = [ - [(nqubits - 1 - a, nqubits - 1 - b) for a, b in row] for row in pairs_rbs - ] + pairs_rbs = [ + [(nqubits - 1 - a, nqubits - 1 - b) for a, b in row] for row in pairs_rbs + ] - circuit = Circuit(nqubits) - circuit.add(gates.X(nqubits - 1)) - for row in pairs_rbs: - for pair in row: - circuit.add(gates.RBS(*pair, 0.0, trainable=True)) + circuit = Circuit(nqubits) + for row in pairs_rbs: + for pair in row: + circuit.add(gates.RBS(*pair, 0.0, trainable=True)) return circuit, pairs_rbs +def _rbs_angles_deterministic(data, nqubits: int, architecture: str): + if architecture == "diagonal": + norm = np.linalg.norm(data) + denominator = norm + phases = [] + for k, elem in enumerate(data[:-1], 1): + sin = 1 if k == 1 else math.sin(phases[-1]) + denominator *= sin + phases.append(math.acos(elem / denominator)) + + if architecture == "semi-diagonal": + phi_1 = math.atan(data[0] / data[1]) + phi_last = math.atan(data[-1] / data[-2]) + + phases, phases_reverse = [phi_1], [phi_last] + k = 1 + while len(phases) + len(phases_reverse) < len(data) - 1: + phases.append(math.atan(data[k] / (data[k + 1] * math.cos(phases[k - 1])))) + phases_reverse.append( + math.atan( + data[nqubits - 1 - k] + / (data[nqubits - 1 - k] * math.cos(phases_reverse[-k])) + ) + ) + k += 1 + phases.extend(phases_reverse[::-1][1:]) + + if architecture == "tree": + j_max = int(nqubits / 2) + + r_array = np.zeros(nqubits - 1, dtype=float) + phases = np.zeros(nqubits - 1, dtype=float) + for j in range(1, j_max + 1): + r_array[j_max + j - 2] = math.sqrt( + data[2 * j - 1] ** 2 + data[2 * j - 2] ** 2 + ) + theta = math.acos(data[2 * j - 2] / r_array[j_max + j - 2]) + if data[2 * j - 1] < 0.0: + theta = 2 * math.pi - theta + phases[j_max + j - 2] = theta + + for j in range(j_max - 1, 0, -1): + r_array[j - 1] = math.sqrt(r_array[2 * j] ** 2 + r_array[2 * j - 1] ** 2) + phases[j - 1] = math.acos(r_array[2 * j - 1] / r_array[j - 1]) + + return phases + + class _ProbabilityDistributionGaussianLoader(rv_continuous): """Probability density function for sampling phases of the RBS gates as a function of circuit depth.""" From 0428baed4fb83a1ef65dc4b007895768a0532ca3 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 19 Dec 2023 16:25:54 +0400 Subject: [PATCH 42/78] fix coverage --- src/qibo/models/encodings.py | 67 +++++++++++----------------------- tests/test_models_encodings.py | 29 +++++++++++---- 2 files changed, 43 insertions(+), 53 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 59648021a0..9912a39271 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -51,7 +51,8 @@ def unary_encoder(data, architecture: str = "tree"): TypeError, f"``architecture`` must be type str, but it is type {type(architecture)}.", ) - if architecture not in ["diagonal", "semi-diagonal", "tree"]: + + if architecture not in ["diagonal", "tree"]: raise_error(ValueError, f"``architecture`` {architecture} not found.") if architecture == "tree" and not math.log2(data.shape[0]).is_integer(): @@ -64,28 +65,12 @@ def unary_encoder(data, architecture: str = "tree"): nqubits = len(data) circuit = Circuit(nqubits) - if architecture == "semi-diagonal": - circuit.add(gates.X(int(nqubits / 2))) - else: - circuit.add(gates.X(nqubits - 1)) + circuit.add(gates.X(nqubits - 1)) circuit_rbs, _ = _generate_rbs_pairs(nqubits, architecture=architecture) circuit += circuit_rbs # calculating phases and setting circuit parameters - phases = _rbs_angles_deterministic(data, nqubits, architecture) - # r_array = np.zeros(nqubits - 1, dtype=float) - # phases = np.zeros(nqubits - 1, dtype=float) - # for j in range(1, j_max + 1): - # r_array[j_max + j - 2] = math.sqrt(data[2 * j - 1] ** 2 + data[2 * j - 2] ** 2) - # theta = math.acos(data[2 * j - 2] / r_array[j_max + j - 2]) - # if data[2 * j - 1] < 0.0: - # theta = 2 * math.pi - theta - # phases[j_max + j - 2] = theta - - # for j in range(j_max - 1, 0, -1): - # r_array[j - 1] = math.sqrt(r_array[2 * j] ** 2 + r_array[2 * j - 1] ** 2) - # phases[j - 1] = math.acos(r_array[2 * j - 1] / r_array[j - 1]) - + phases = _generate_rbs_angles(data, nqubits, architecture) circuit.set_parameters(phases) return circuit @@ -136,11 +121,25 @@ def unary_encoder_random_gaussian(nqubits: int, architecture: str = "tree", seed raise_error( TypeError, f"nqubits must be type int, but it is type {type(nqubits)}." ) - elif nqubits <= 0.0: + + if nqubits <= 0.0: raise_error( ValueError, f"nqubits must be a positive integer, but it is {nqubits}." ) - elif not math.log2(nqubits).is_integer(): + + if not isinstance(architecture, str): + raise_error( + TypeError, + f"``architecture`` must be type str, but it is type {type(architecture)}.", + ) + + if architecture != "tree": + raise_error( + NotImplementedError, + f"Currently, this function only accepts ``architecture=='tree'``.", + ) + + if not math.log2(nqubits).is_integer(): raise_error(ValueError, f"nqubits must be a power of 2, but it is {nqubits}.") if ( @@ -182,13 +181,6 @@ def _generate_rbs_pairs(nqubits: int, architecture: str): pairs_rbs = np.arange(nqubits) pairs_rbs = [[pair] for pair in zip(pairs_rbs[:-1], pairs_rbs[1:])] - if architecture == "semi-diagonal": - index = int(nqubits / 2) - 1 - pairs_rbs = [[(index, index + 1)]] - for k in range(index): - target, control = pairs_rbs[k][0][0], pairs_rbs[k][-1][1] - pairs_rbs.append([(target - 1, target), (control, control + 1)]) - if architecture == "tree": pairs_rbs = [[(0, int(nqubits / 2))]] indexes = list(np.array(pairs_rbs).flatten()) @@ -211,7 +203,7 @@ def _generate_rbs_pairs(nqubits: int, architecture: str): return circuit, pairs_rbs -def _rbs_angles_deterministic(data, nqubits: int, architecture: str): +def _generate_rbs_angles(data, nqubits: int, architecture: str): if architecture == "diagonal": norm = np.linalg.norm(data) denominator = norm @@ -221,23 +213,6 @@ def _rbs_angles_deterministic(data, nqubits: int, architecture: str): denominator *= sin phases.append(math.acos(elem / denominator)) - if architecture == "semi-diagonal": - phi_1 = math.atan(data[0] / data[1]) - phi_last = math.atan(data[-1] / data[-2]) - - phases, phases_reverse = [phi_1], [phi_last] - k = 1 - while len(phases) + len(phases_reverse) < len(data) - 1: - phases.append(math.atan(data[k] / (data[k + 1] * math.cos(phases[k - 1])))) - phases_reverse.append( - math.atan( - data[nqubits - 1 - k] - / (data[nqubits - 1 - k] * math.cos(phases_reverse[-k])) - ) - ) - k += 1 - phases.extend(phases_reverse[::-1][1:]) - if architecture == "tree": j_max = int(nqubits / 2) diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index d0c255ae49..33891e0cf8 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -13,28 +13,39 @@ def gaussian(x, a, b, c): return np.exp(a * x**2 + b * x + c) +@pytest.mark.parametrize("architecture", ["tree", "diagonal"]) @pytest.mark.parametrize("nqubits", [2, 4, 8, 16]) -def test_unary_encoder(backend, nqubits): +def test_unary_encoder(backend, nqubits, architecture): sampler = np.random.default_rng(1) with pytest.raises(TypeError): data = sampler.random((nqubits, nqubits)) data = backend.cast(data, dtype=data.dtype) - unary_encoder(data) + unary_encoder(data, architecture=architecture) + with pytest.raises(TypeError): + data = sampler.random(nqubits) + data = backend.cast(data, dtype=data.dtype) + unary_encoder(data, architecture=True) with pytest.raises(ValueError): - data = sampler.random(nqubits + 1) + data = sampler.random(nqubits) data = backend.cast(data, dtype=data.dtype) - unary_encoder(data) + unary_encoder(data, architecture="semi-diagonal") + if architecture == "tree": + with pytest.raises(ValueError): + data = sampler.random(nqubits + 1) + data = backend.cast(data, dtype=data.dtype) + unary_encoder(data, architecture=architecture) # sampling random data in interval [-1, 1] sampler = np.random.default_rng(1) - data = 2 * sampler.random(nqubits) - 1 + # data = 2 * sampler.random(nqubits) - 1 + data = 2 * sampler.random(nqubits) data = backend.cast(data, dtype=data.dtype) - circuit = unary_encoder(data) + circuit = unary_encoder(data, architecture=architecture) state = backend.execute_circuit(circuit).state() indexes = np.flatnonzero(state) - state = state[indexes] + state = np.real(state[indexes]) backend.assert_allclose(state, data / backend.calculate_norm(data, order=2)) @@ -51,6 +62,10 @@ def test_unary_encoder_random_gaussian(backend, nqubits, seed): unary_encoder_random_gaussian(-1, seed=seed) with pytest.raises(ValueError): unary_encoder_random_gaussian(3, seed=seed) + with pytest.raises(ValueError): + unary_encoder_random_gaussian(nqubits, architecture=True, seed=seed) + with pytest.raises(ValueError): + unary_encoder_random_gaussian(nqubits, architecture="diagonal", seed=seed) with pytest.raises(TypeError): unary_encoder_random_gaussian(nqubits, seed="seed") From fd8102175c1a08be2552a3173a2a8bfd5da6a643 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 19 Dec 2023 17:10:00 +0400 Subject: [PATCH 43/78] fix test --- tests/test_models_encodings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index 33891e0cf8..782ad00825 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -62,9 +62,9 @@ def test_unary_encoder_random_gaussian(backend, nqubits, seed): unary_encoder_random_gaussian(-1, seed=seed) with pytest.raises(ValueError): unary_encoder_random_gaussian(3, seed=seed) - with pytest.raises(ValueError): + with pytest.raises(TypeError): unary_encoder_random_gaussian(nqubits, architecture=True, seed=seed) - with pytest.raises(ValueError): + with pytest.raises(NotImplementedError): unary_encoder_random_gaussian(nqubits, architecture="diagonal", seed=seed) with pytest.raises(TypeError): unary_encoder_random_gaussian(nqubits, seed="seed") From d6ce5f69dd9cfd4f5acde336c5cc3086fdafba19 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 19 Dec 2023 18:16:34 +0400 Subject: [PATCH 44/78] fix `diagonal` angles --- src/qibo/models/encodings.py | 15 ++++++++------- tests/test_models_encodings.py | 5 ++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 9912a39271..4ba96316ac 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -65,7 +65,10 @@ def unary_encoder(data, architecture: str = "tree"): nqubits = len(data) circuit = Circuit(nqubits) + # if architecture == "tree": circuit.add(gates.X(nqubits - 1)) + # else: + # circuit.add(gates.X(0)) circuit_rbs, _ = _generate_rbs_pairs(nqubits, architecture=architecture) circuit += circuit_rbs @@ -205,13 +208,11 @@ def _generate_rbs_pairs(nqubits: int, architecture: str): def _generate_rbs_angles(data, nqubits: int, architecture: str): if architecture == "diagonal": - norm = np.linalg.norm(data) - denominator = norm - phases = [] - for k, elem in enumerate(data[:-1], 1): - sin = 1 if k == 1 else math.sin(phases[-1]) - denominator *= sin - phases.append(math.acos(elem / denominator)) + phases = [ + math.atan2(np.linalg.norm(data[k + 1 :]), data[k]) + for k in range(len(data) - 2) + ] + phases.append(math.atan2(data[-1], data[-2])) if architecture == "tree": j_max = int(nqubits / 2) diff --git a/tests/test_models_encodings.py b/tests/test_models_encodings.py index 782ad00825..d51421c198 100644 --- a/tests/test_models_encodings.py +++ b/tests/test_models_encodings.py @@ -14,7 +14,7 @@ def gaussian(x, a, b, c): @pytest.mark.parametrize("architecture", ["tree", "diagonal"]) -@pytest.mark.parametrize("nqubits", [2, 4, 8, 16]) +@pytest.mark.parametrize("nqubits", [8]) def test_unary_encoder(backend, nqubits, architecture): sampler = np.random.default_rng(1) @@ -38,8 +38,7 @@ def test_unary_encoder(backend, nqubits, architecture): # sampling random data in interval [-1, 1] sampler = np.random.default_rng(1) - # data = 2 * sampler.random(nqubits) - 1 - data = 2 * sampler.random(nqubits) + data = 2 * sampler.random(nqubits) - 1 data = backend.cast(data, dtype=data.dtype) circuit = unary_encoder(data, architecture=architecture) From 933f3fe576c4f270a82e016be788e70321db336a Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 19 Dec 2023 18:26:26 +0400 Subject: [PATCH 45/78] update documentation --- src/qibo/models/encodings.py | 38 ++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index 4ba96316ac..d87ce0a3db 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -32,6 +32,10 @@ def unary_encoder(data, architecture: str = "tree"): Args: data (ndarray, optional): :math:`1`-dimensional array of data to be loaded. + architecture(str, optional): circuit architecture used for the unary loader. + If ``diagonal``, uses a ladder-like structure. + If ``tree``, uses a binary-tree-based structure. + Defaults to ``tree``. Returns: :class:`qibo.models.circuit.Circuit`: circuit that loads ``data`` in unary representation. @@ -65,10 +69,7 @@ def unary_encoder(data, architecture: str = "tree"): nqubits = len(data) circuit = Circuit(nqubits) - # if architecture == "tree": circuit.add(gates.X(nqubits - 1)) - # else: - # circuit.add(gates.X(0)) circuit_rbs, _ = _generate_rbs_pairs(nqubits, architecture=architecture) circuit += circuit_rbs @@ -108,6 +109,9 @@ def unary_encoder_random_gaussian(nqubits: int, architecture: str = "tree", seed Args: nqubits (int): number of qubits. + architecture(str, optional): circuit architecture used for the unary loader. + If ``tree``, uses a binary-tree-based structure. + Defaults to ``tree``. seed (int or :class:`numpy.random.Generator`, optional): Either a generator of random numbers or a fixed seed to initialize a generator. If ``None``, initializes a generator with a random seed. Defaults to ``None``. @@ -178,7 +182,20 @@ def unary_encoder_random_gaussian(nqubits: int, architecture: str = "tree", seed def _generate_rbs_pairs(nqubits: int, architecture: str): """Generating list of indexes representing the RBS connections - and creating circuit with all RBS initialised with 0.0 phase.""" + + Creates circuit with all RBS initialised with 0.0 phase. + + Args: + nqubits (int): number of qubits. + architecture(str, optional): circuit architecture used for the unary loader. + If ``diagonal``, uses a ladder-like structure. + If ``tree``, uses a binary-tree-based structure. + Defaults to ``tree``. + + Returns: + (:class:`qibo.models.circuit.Circuit`, list): circuit composed of :class:`qibo.gates.gates.RBS` + and list of indexes of target qubits per depth. + """ if architecture == "diagonal": pairs_rbs = np.arange(nqubits) @@ -207,6 +224,19 @@ def _generate_rbs_pairs(nqubits: int, architecture: str): def _generate_rbs_angles(data, nqubits: int, architecture: str): + """Generating list of angles for RBS gates based on ``architecture``. + + Args: + data (ndarray, optional): :math:`1`-dimensional array of data to be loaded. + nqubits (int): number of qubits. + architecture(str, optional): circuit architecture used for the unary loader. + If ``diagonal``, uses a ladder-like structure. + If ``tree``, uses a binary-tree-based structure. + Defaults to ``tree``. + + Returns: + list: list of phases for RBS gates. + """ if architecture == "diagonal": phases = [ math.atan2(np.linalg.norm(data[k + 1 :]), data[k]) From 150e29a842fe98a658f2aebfde334eee55d4f925 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 09:50:21 +0400 Subject: [PATCH 46/78] improve docstrings `pipeline` --- src/qibo/transpiler/pipeline.py | 48 +++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index 289429e40f..a88c90de10 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -149,15 +149,17 @@ def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): raise_error( ConnectivityError, "Some qubits are not in the original connectivity." ) + new_connectivity = nx.Graph() new_connectivity.add_nodes_from(qubits) - new_edges = [] - for edge in connectivity.edges: - if edge[0] in qubits and edge[1] in qubits: - new_edges.append(edge) + new_edges = [ + edge for edge in connectivity.edges if edge[0] in qubits and edge[1] in qubits + ] new_connectivity.add_edges_from(new_edges) + if not nx.is_connected(new_connectivity): - raise_error(ConnectivityError, "The new connectivity graph is not connected.") + raise_error(ConnectivityError, "New connectivity graph is not connected.") + return new_connectivity @@ -165,17 +167,22 @@ class Passes: """Define a transpiler pipeline consisting of smaller transpiler steps that are applied sequentially: Args: - passes (list): list of passes to be applied sequentially, - if None default transpiler will be used. - connectivity (nx.Graph): physical qubits connectivity. - native_gates (NativeGates): native gates. - on_qubits (list): list of physical qubits to be used. If "None" all qubits are used. + passes (list, optional): list of passes to be applied sequentially. + If ``None``, default transpiler will be used. + Defaults to ``None``. + connectivity (:class:`networkx.Graph`, optional): physical qubits connectivity. + If ``None``, :class:`` is used. + Defaults to ``None``. + native_gates (:class:`qibo.transpiler.unroller.NativeGates`, optional): native gates. + Defaults to :math:`qibo.transpiler.unroller.NativeGates.default`. + on_qubits (list, optional): list of physical qubits to be used. + If "None" all qubits are used. Defaults to ``None``. """ def __init__( self, - passes: list, - connectivity: nx.Graph, + passes: list = None, + connectivity: nx.Graph = None, native_gates: NativeGates = NativeGates.default(), on_qubits: list = None, ): @@ -183,10 +190,7 @@ def __init__( connectivity = restrict_connectivity_qubits(connectivity, on_qubits) self.connectivity = connectivity self.native_gates = native_gates - if passes is None: - self.passes = self.default() - else: - self.passes = passes + self.passes = self.default() if passes is None else passes def default(self): """Return the default transpiler pipeline for the required hardware connectivity.""" @@ -204,6 +208,7 @@ def default(self): default_passes.append(StarConnectivity()) # default unroller pass default_passes.append(Unroller(native_gates=self.native_gates)) + return default_passes def __call__(self, circuit): @@ -239,14 +244,17 @@ def __call__(self, circuit): TranspilerPipelineError, f"Unrecognised transpiler pass: {transpiler_pass}", ) + return circuit, final_layout - def is_satisfied(self, circuit): - """Return True if the circuit respects the hardware connectivity and native gates, False otherwise. + def is_satisfied(self, circuit: Circuit): + """Returns ``True`` if the circuit respects the hardware connectivity and native gates, ``False`` otherwise. Args: - circuit (qibo.models.Circuit): circuit to be checked. - native_gates (NativeGates): two qubit native gates. + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be checked. + + Returns: + (bool): satisfiability condition. """ try: assert_connectivity(circuit=circuit, connectivity=self.connectivity) From 438f95de87950f92cae25cbfcfcfe3d3ac2ece2c Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 09:58:18 +0400 Subject: [PATCH 47/78] improved docstring `unroller` --- src/qibo/transpiler/unroller.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/qibo/transpiler/unroller.py b/src/qibo/transpiler/unroller.py index 1358056600..076f358b5a 100644 --- a/src/qibo/transpiler/unroller.py +++ b/src/qibo/transpiler/unroller.py @@ -8,11 +8,20 @@ class NativeGates(Flag): - """Define native gates supported by the unroller. - A native gate set should contain at least one two-qubit gate (CZ or iSWAP) - and at least one single qubit gate (GPI2 or U3). - Possible values are: - I, Z, RZ, M, GPI2, U3, CZ, iSWAP. + """Define native gates supported by the unroller. A native gate set should contain at least + one two-qubit gate (:class:`qibo.gates.gates.CZ` or :class:`qibo.gates.gates.iSWAP`), + and at least one single-qubit gate + (:class:`qibo.gates.gates.GPI2` or :class:`qibo.gates.gates.U3`). + + Possible gates are: + - :class:`qibo.gates.gates.I` + - :class:`qibo.gates.gates.Z` + - :class:`qibo.gates.gates.RZ` + - :class:`qibo.gates.gates.M` + - :class:`qibo.gates.gates.GPI2` + - :class:`qibo.gates.gates.U3` + - :class:`qibo.gates.gates.CZ` + - :class:`qibo.gates.gates.iSWAP` """ I = auto() @@ -31,7 +40,7 @@ def default(cls): @classmethod def from_gatelist(cls, gatelist: list): - """Create a NativeGates object containing all gates from a gatelist.""" + """Create a NativeGates object containing all gates from a ``gatelist``.""" natives = cls(0) for gate in gatelist: natives |= cls.from_gate(gate) @@ -39,9 +48,8 @@ def from_gatelist(cls, gatelist: list): @classmethod def from_gate(cls, gate: gates.Gate): - """Create a NativeGates object from a gate. - The gate can be either a class:`qibo.gates.Gate` or an instance of this class. - """ + """Create a :class:`qibo.transpiler.unroller.NativeGates` + object from a :class:`qibo.gates.gates.Gate`.""" if isinstance(gate, gates.Gate): return cls.from_gate(gate.__class__) try: @@ -55,7 +63,8 @@ class Unroller: """Translates a circuit to native gates. Args: - native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates to use in the transpiled circuit. + native_gates (:class:`qibo.transpiler.unroller.NativeGates`): native gates to use + in the transpiled circuit. Returns: (:class:`qibo.models.circuit.Circuit`): equivalent circuit with native gates. From 1e5ae8a96de4cab8ae180171b7100562dc1e5fa3 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:00:36 +0400 Subject: [PATCH 48/78] hiding `exceptions` submodule --- src/qibo/transpiler/{exceptions.py => _exceptions.py} | 0 src/qibo/transpiler/blocks.py | 2 +- src/qibo/transpiler/pipeline.py | 2 +- src/qibo/transpiler/placer.py | 2 +- src/qibo/transpiler/router.py | 5 ++--- src/qibo/transpiler/unroller.py | 2 +- 6 files changed, 6 insertions(+), 7 deletions(-) rename src/qibo/transpiler/{exceptions.py => _exceptions.py} (100%) diff --git a/src/qibo/transpiler/exceptions.py b/src/qibo/transpiler/_exceptions.py similarity index 100% rename from src/qibo/transpiler/exceptions.py rename to src/qibo/transpiler/_exceptions.py diff --git a/src/qibo/transpiler/blocks.py b/src/qibo/transpiler/blocks.py index 764213e62f..199979767e 100644 --- a/src/qibo/transpiler/blocks.py +++ b/src/qibo/transpiler/blocks.py @@ -3,7 +3,7 @@ from qibo import Circuit, gates from qibo.config import raise_error from qibo.gates import Gate -from qibo.transpiler.exceptions import BlockingError +from qibo.transpiler._exceptions import BlockingError class Block: diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index a88c90de10..d1bd6fdc6c 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -7,8 +7,8 @@ from qibo.config import raise_error from qibo.models import Circuit from qibo.quantum_info.random_ensembles import random_statevector +from qibo.transpiler._exceptions import TranspilerPipelineError from qibo.transpiler.abstract import Optimizer, Placer, Router -from qibo.transpiler.exceptions import TranspilerPipelineError from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.placer import Trivial, assert_placement from qibo.transpiler.router import ConnectivityError, assert_connectivity diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index d47bc30da0..8cbd09117d 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -6,8 +6,8 @@ from qibo import gates from qibo.config import raise_error from qibo.models import Circuit +from qibo.transpiler._exceptions import PlacementError from qibo.transpiler.abstract import Placer, Router -from qibo.transpiler.exceptions import PlacementError def assert_placement( diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index dd9f583130..3befa6edbe 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -2,14 +2,13 @@ from copy import deepcopy import networkx as nx -import numpy as np from qibo import gates -from qibo.config import log, raise_error +from qibo.config import raise_error from qibo.models import Circuit +from qibo.transpiler._exceptions import ConnectivityError from qibo.transpiler.abstract import Router from qibo.transpiler.blocks import Block, CircuitBlocks -from qibo.transpiler.exceptions import ConnectivityError def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): diff --git a/src/qibo/transpiler/unroller.py b/src/qibo/transpiler/unroller.py index 076f358b5a..c62ae4e273 100644 --- a/src/qibo/transpiler/unroller.py +++ b/src/qibo/transpiler/unroller.py @@ -3,8 +3,8 @@ from qibo import gates from qibo.config import raise_error from qibo.models import Circuit +from qibo.transpiler._exceptions import DecompositionError from qibo.transpiler.decompositions import cz_dec, gpi2_dec, iswap_dec, opt_dec, u3_dec -from qibo.transpiler.exceptions import DecompositionError class NativeGates(Flag): From dc9feeecde5b01d8848db3c94a2267c464be29cb Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:12:38 +0400 Subject: [PATCH 49/78] rewrite imports for exceptions in tests --- tests/test_transpiler_blocks.py | 2 +- tests/test_transpiler_pipeline.py | 3 +-- tests/test_transpiler_placer.py | 2 +- tests/test_transpiler_router.py | 9 ++------- tests/test_transpiler_star_connectivity.py | 2 +- tests/test_transpiler_unroller.py | 2 +- 6 files changed, 7 insertions(+), 13 deletions(-) diff --git a/tests/test_transpiler_blocks.py b/tests/test_transpiler_blocks.py index 48207890bd..af652f72f5 100644 --- a/tests/test_transpiler_blocks.py +++ b/tests/test_transpiler_blocks.py @@ -1,9 +1,9 @@ import pytest from qibo import Circuit, gates +from qibo.transpiler._exceptions import BlockingError from qibo.transpiler.blocks import ( Block, - BlockingError, CircuitBlocks, _check_multi_qubit_measurements, _count_multi_qubit_gates, diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index feeaba4e84..86e84b3b01 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -4,11 +4,10 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import ConnectivityError, TranspilerPipelineError from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.pipeline import ( - ConnectivityError, Passes, - TranspilerPipelineError, assert_circuit_equivalence, assert_transpiling, restrict_connectivity_qubits, diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index c76e341981..09d4a80d2a 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -3,10 +3,10 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import PlacementError from qibo.transpiler.pipeline import restrict_connectivity_qubits from qibo.transpiler.placer import ( Custom, - PlacementError, Random, ReverseTraversal, Subgraph, diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index bee84e47b6..0a2d8422f3 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -4,19 +4,14 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import ConnectivityError from qibo.transpiler.optimizer import Preprocessing from qibo.transpiler.pipeline import ( assert_circuit_equivalence, restrict_connectivity_qubits, ) from qibo.transpiler.placer import Custom, Random, Subgraph, Trivial, assert_placement -from qibo.transpiler.router import ( - CircuitMap, - ConnectivityError, - Sabre, - ShortestPaths, - assert_connectivity, -) +from qibo.transpiler.router import CircuitMap, Sabre, ShortestPaths, assert_connectivity def star_connectivity(): diff --git a/tests/test_transpiler_star_connectivity.py b/tests/test_transpiler_star_connectivity.py index 5a8e9a4b57..2330afd76c 100644 --- a/tests/test_transpiler_star_connectivity.py +++ b/tests/test_transpiler_star_connectivity.py @@ -7,8 +7,8 @@ from qibo.backends import NumpyBackend from qibo.models import Circuit from qibo.quantum_info.random_ensembles import random_unitary +from qibo.transpiler._exceptions import ConnectivityError from qibo.transpiler.pipeline import _transpose_qubits -from qibo.transpiler.router import ConnectivityError from qibo.transpiler.star_connectivity import StarConnectivity diff --git a/tests/test_transpiler_unroller.py b/tests/test_transpiler_unroller.py index 26529c8f2c..89c44ca9c7 100644 --- a/tests/test_transpiler_unroller.py +++ b/tests/test_transpiler_unroller.py @@ -2,8 +2,8 @@ from qibo import gates from qibo.models import Circuit +from qibo.transpiler._exceptions import DecompositionError from qibo.transpiler.unroller import ( - DecompositionError, NativeGates, Unroller, assert_decomposition, From 7316cb76424fe0e724716cd6bd10bc6d10a21fc3 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:28:20 +0400 Subject: [PATCH 50/78] improved docstring `placer` --- src/qibo/transpiler/placer.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/qibo/transpiler/placer.py b/src/qibo/transpiler/placer.py index 8cbd09117d..2dd2ee19f4 100644 --- a/src/qibo/transpiler/placer.py +++ b/src/qibo/transpiler/placer.py @@ -18,8 +18,9 @@ def assert_placement( Args: circuit (:class:`qibo.models.circuit.Circuit`): Circuit model to check. layout (dict): physical to logical qubit mapping. - connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the - layout applied to a subset of qubits in the original connectivity graph. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. + This argument is necessary if the layout applied to a subset of + qubits in the original connectivity graph. Defaults to ``None``. """ assert_mapping_consistency(layout=layout, connectivity=connectivity) if circuit.nqubits > len(layout): @@ -39,8 +40,9 @@ def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): Args: layout (dict): physical to logical qubit mapping. - connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the - layout applied to a subset of qubits in the original connectivity graph. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. + This argument is necessary if the layout applied to a subset of + qubits in the original connectivity graph. Defaults to ``None``. """ values = sorted(layout.values()) if connectivity is not None: @@ -61,13 +63,13 @@ def assert_mapping_consistency(layout: dict, connectivity: nx.Graph = None): def _find_gates_qubits_pairs(circuit: Circuit): """Helper method for :meth:`qibo.transpiler.placer`. - Translate qibo circuit into a list of pairs of qubits to be used by the router and placer. + Translate circuit into a list of pairs of qubits to be used by the router and placer. Args: circuit (:class:`qibo.models.circuit.Circuit`): circuit to be transpiled. Returns: - gates_qubits_pairs (list): list containing pairs of qubits targeted by two qubits gates. + (list): Pairs of qubits targeted by two qubits gates. """ gates_qubits_pairs = [] for gate in circuit.queue: @@ -136,9 +138,9 @@ class Custom(Placer): :math:`{\\textup{"q0"}: 1, \\textup{"q1"}: 2, \\textup{"q2"}:0}` to assign the physical qubits :math:`\\{0, 1, 2\\}` to the logical qubits :math:`[1, 2, 0]`. - connectivity (networkx.Graph, optional): chip connectivity. + connectivity (:class:`networkx.Graph`, optional): chip connectivity. This argument is necessary if the layout applied to a subset of - qubits of the original connectivity graph. + qubits of the original connectivity graph. Defaults to ``None``. """ def __init__(self, map: Union[list, dict], connectivity: nx.Graph = None): @@ -228,6 +230,7 @@ def __call__(self, circuit: Circuit): break sorted_result = dict(sorted(result.mapping.items())) + return dict( zip(["q" + str(i) for i in sorted_result.keys()], sorted_result.values()) ) @@ -261,6 +264,7 @@ def __call__(self, circuit): nodes = self.connectivity.number_of_nodes() keys = list(self.connectivity.nodes()) dict_keys = ["q" + str(i) for i in keys] + final_mapping = dict(zip(keys, range(nodes))) final_graph = nx.relabel_nodes(self.connectivity, final_mapping) final_cost = self._cost(final_graph, gates_qubits_pairs) @@ -274,6 +278,7 @@ def __call__(self, circuit): final_graph = graph final_mapping = mapping final_cost = cost + return dict(zip(dict_keys, list(final_mapping.values()))) def _cost(self, graph: nx.Graph, gates_qubits_pairs: list): @@ -290,6 +295,7 @@ def _cost(self, graph: nx.Graph, gates_qubits_pairs: list): for allowed, gate in enumerate(gates_qubits_pairs): if gate not in graph.edges(): return len(gates_qubits_pairs) - allowed - 1 + return 0 @@ -334,11 +340,11 @@ def __call__(self, circuit: Circuit): Returns: (dict): physical to logical qubit mapping. """ - initial_placer = Trivial(self.connectivity) initial_placement = initial_placer(circuit=circuit) new_circuit = self._assemble_circuit(circuit) final_placement = self._routing_step(initial_placement, new_circuit) + return final_placement def _assemble_circuit(self, circuit: Circuit): @@ -385,6 +391,6 @@ def _routing_step(self, layout: dict, circuit: Circuit): layout (dict): intial qubit layout. circuit (:class:`qibo.models.circuit.Circuit`): circuit to be routed. """ - _, final_mapping = self.routing_algorithm(circuit, layout) + return final_mapping From 2b27317bb8ebe8305d56c807609a3608dfdfe443 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 20 Dec 2023 07:44:47 +0100 Subject: [PATCH 51/78] updating tests --- src/qibo/models/error_mitigation.py | 39 +++----------- tests/test_models_error_mitigation.py | 77 ++++++++++++++++----------- 2 files changed, 54 insertions(+), 62 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 1f66cf2a12..4731d43a2c 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -706,15 +706,14 @@ def sample_clifford_training_circuit( for i, gate in enumerate(circuit.queue): if isinstance(gate, gates.M): - gate_rand = gates.Unitary( - random_clifford(1, backend=backend, return_circuit=False), - gate.qubits[0], - ) - gate_rand.clifford = True - - sampled_circuit.add(gate_rand) + for q in gate.qubits: + gate_rand = gates.Unitary( + random_clifford(1, backend=backend, return_circuit=False), + q, + ) + gate_rand.clifford = True + sampled_circuit.add(gate_rand) sampled_circuit.add(gate) - else: if i in [index for index, _ in non_clifford_gates]: gate = gates.Unitary( @@ -762,6 +761,7 @@ def error_sensitive_circuit(circuit, observable, backend=None): num_qubits = sampled_circuit.nqubits comp_to_pauli = comp_basis_to_pauli(num_qubits, backend=backend) + observable.nqubits = num_qubits observable_liouville = vectorization( np.transpose(np.conjugate(unitary_matrix)) @ observable.matrix @ unitary_matrix, order="row", @@ -916,29 +916,6 @@ def ICS( return mitigated_expectation -# def transpile_circ(circuit, qubit_map, backend): -# from qibo.transpiler.unitary_decompositions import u3_decomposition - -# if backend.name == "qibolab": -# if qubit_map is None: -# qubit_map = list(range(circuit.nqubits)) -# new_c = circuit.__class__(backend.platform.nqubits) -# for gate in circuit.queue: -# qubits = [qubit_map[j] for j in gate.qubits] -# if isinstance(gate, gates.M): -# new_gate = gates.M(*tuple(qubits), **gate.init_kwargs) -# new_gate.result = gate.result -# new_c.add(new_gate) -# elif isinstance(gate, gates.I): -# new_c.add(gate.__class__(*tuple(qubits), **gate.init_kwargs)) -# else: -# matrix = gate.matrix() -# new_c.add(gates.U3(qubits[0], *u3_decomposition(matrix))) -# return new_c - -# return circuit - - def _circuit_conf(circuit, qubit_map, noise_model, nshots, backend=None): from qibo.transpiler.placer import Custom diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 0dc5fd395c..6951b4e6e0 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -28,7 +28,9 @@ def get_noise_model(error, gate, cal_matrix=[False, None]): return noise -def get_circuit(nqubits): +def get_circuit(nqubits, nmeas=None): + if nmeas is None: + nmeas = nqubits # Define the circuit hz = 0.5 hx = 0.5 @@ -45,7 +47,7 @@ def get_circuit(nqubits): c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2)) c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) - c.add(gates.M(*range(nqubits))) + c.add(gates.M(*range(nmeas))) return c @@ -55,8 +57,8 @@ def get_circuit(nqubits): cal_matrix_1q = random_stochastic_matrix( 2, diagonally_dominant=True, seed=2, backend=backend ) -cal_matrix_3q = random_stochastic_matrix( - 8, diagonally_dominant=True, seed=2, backend=backend +cal_matrix_2q = random_stochastic_matrix( + 4, diagonally_dominant=True, seed=2, backend=backend ) @@ -68,7 +70,7 @@ def get_circuit(nqubits): 3, get_noise_model(DepolarizingError(0.1), gates.CNOT), "CNOT", - {"calibration_matrix": cal_matrix_3q, "ibu_iters": None}, + {"calibration_matrix": cal_matrix_2q, "ibu_iters": None}, ), ( 3, @@ -102,13 +104,19 @@ def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): tf.config.threading.set_intra_op_parallelism_threads = 1 else: backend.set_threads(1) + + if nqubits == 1: + nmeas = 1 + else: + nmeas = nqubits - 1 # Define the circuit - c = get_circuit(nqubits) + c = get_circuit(nqubits, nmeas) # Define the observable - obs = np.prod([Z(i) for i in range(nqubits)]) + obs = np.prod([Z(i) for i in range(nmeas)]) + obs_exact = SymbolicHamiltonian(obs, nqubits=nqubits, backend=backend) obs = SymbolicHamiltonian(obs, backend=backend) # Noise-free expected value - exact = obs.expectation(backend.execute_circuit(c).state()) + exact = obs_exact.expectation(backend.execute_circuit(c).state()) # Noisy expected value without mitigation state = backend.execute_circuit(noise.apply(c), nshots=10000) noisy = state.expectation_from_samples(obs) @@ -135,20 +143,21 @@ def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): [ (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), - {"calibration_matrix": cal_matrix_3q, "ibu_iters": None}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), + {"calibration_matrix": cal_matrix_2q, "ibu_iters": None}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), - {"calibration_matrix": cal_matrix_3q, "ibu_iters": 10}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), + {"calibration_matrix": cal_matrix_2q, "ibu_iters": 10}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), {"ncircuits": 2}, ), ], ) def test_cdr(backend, nqubits, noise, full_output, readout): + """Test that CDR reduces the noise.""" if backend.name == "tensorflow": import tensorflow as tf @@ -156,14 +165,19 @@ def test_cdr(backend, nqubits, noise, full_output, readout): tf.config.threading.set_intra_op_parallelism_threads = 1 else: backend.set_threads(1) - """Test that CDR reduces the noise.""" + + if nqubits == 1: + nmeas = 1 + else: + nmeas = nqubits - 1 # Define the circuit - c = get_circuit(nqubits) + c = get_circuit(nqubits, nmeas) # Define the observable - obs = np.prod([Z(i) for i in range(nqubits)]) + obs = np.prod([Z(i) for i in range(nmeas)]) + obs_exact = SymbolicHamiltonian(obs, nqubits=nqubits, backend=backend) obs = SymbolicHamiltonian(obs, backend=backend) # Noise-free expected value - exact = obs.expectation(backend.execute_circuit(c).state()) + exact = obs_exact.expectation(backend.execute_circuit(c).state()) # Noisy expected value without mitigation state = backend.execute_circuit(noise.apply(c), nshots=10000) noisy = state.expectation_from_samples(obs) @@ -268,10 +282,10 @@ def test_vncdr(backend, nqubits, noise, full_output, insertion_gate, readout): assert np.abs(exact - estimate) <= np.abs(exact - noisy) -@pytest.mark.parametrize("nqubits", [3]) +@pytest.mark.parametrize("nqubits,nmeas", [(3, 2)]) @pytest.mark.parametrize("method", ["cal_matrix", "randomized"]) @pytest.mark.parametrize("ibu_iters", [None, 10]) -def test_readout_mitigation(backend, nqubits, method, ibu_iters): +def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): if backend.name == "tensorflow": import tensorflow as tf @@ -280,21 +294,21 @@ def test_readout_mitigation(backend, nqubits, method, ibu_iters): else: backend.set_threads(1) nshots = 10000 - p = random_stochastic_matrix(2**nqubits, diagonally_dominant=True, seed=5) + p = random_stochastic_matrix(2**nmeas, diagonally_dominant=True, seed=5) noise = NoiseModel() noise.add(ReadoutError(probabilities=p), gate=gates.M) if method == "cal_matrix": calibration = get_calibration_matrix( - nqubits, None, noise, nshots=nshots, backend=backend + nmeas, None, noise, nshots=nshots, backend=backend ) readout = {"calibration_matrix": calibration, "ibu_iters": ibu_iters} elif method == "randomized": readout = {"ncircuits": 10} # Define the observable - obs = np.prod([Z(i) for i in range(nqubits)]) + obs = np.prod([Z(i) for i in range(nmeas)]) obs = SymbolicHamiltonian(obs, backend=backend) # get noise free expected val - c = get_circuit(nqubits) + c = get_circuit(nqubits, nmeas) true_state = backend.execute_circuit(c, nshots=nshots) true_val = true_state.expectation_from_samples(obs) # get noisy expected val @@ -313,15 +327,15 @@ def test_readout_mitigation(backend, nqubits, method, ibu_iters): [ (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), - {"calibration_matrix": cal_matrix_3q, "ibu_iters": None}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), + {"calibration_matrix": cal_matrix_2q, "ibu_iters": None}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), - {"calibration_matrix": cal_matrix_3q, "ibu_iters": 10}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), + {"calibration_matrix": cal_matrix_2q, "ibu_iters": 10}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_3q]), + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), {"ncircuits": 2}, ), ], @@ -336,12 +350,13 @@ def test_ics(backend, nqubits, noise, full_output, readout): backend.set_threads(1) """Test that ICS reduces the noise.""" # Define the circuit - c = get_circuit(nqubits) + c = get_circuit(nqubits, nqubits - 1) # Define the observable - obs = np.prod([Z(i) for i in range(nqubits)]) + obs = np.prod([Z(i) for i in range(nqubits - 1)]) + obs_exact = SymbolicHamiltonian(obs, nqubits=nqubits, backend=backend) obs = SymbolicHamiltonian(obs, backend=backend) # Noise-free expected value - exact = obs.expectation(backend.execute_circuit(c).state()) + exact = obs_exact.expectation(backend.execute_circuit(c).state()) # Noisy expected value without mitigation state = backend.execute_circuit(noise.apply(c), nshots=10000) noisy = state.expectation_from_samples(obs) From 209aac6b8ffdc2af7e2815a1ca2e5c5a3caafb63 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 10:58:33 +0400 Subject: [PATCH 52/78] improved docstrings `router` --- src/qibo/transpiler/router.py | 135 ++++++++++++++++++++++++++-------- 1 file changed, 105 insertions(+), 30 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 3befa6edbe..54ff6f3899 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -1,5 +1,6 @@ import random from copy import deepcopy +from typing import Optional, Union import networkx as nx @@ -42,16 +43,24 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): class CircuitMap: - """Class to keep track of the circuit and physical-logical mapping during routing, - this class also implements the initial two qubit blocks decomposition. + """Class that stores the circuit and physical-logical mapping during routing. + + Also implements the initial two-qubit block decompositions. Args: initial_layout (dict): initial logical-to-physical qubit mapping. - circuit (Circuit): circuit to be routed. - blocks (CircuitBlocks): circuit blocks representation, if None the blocks will be computed from the circuit. + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be routed. + blocks (:class:`qibo.transpiler.blocks.CircuitBlocks`, optional): circuit block representation. + If ``None``, the blocks will be computed from the circuit. + Defaults to ``None``. """ - def __init__(self, initial_layout: dict, circuit: Circuit, blocks=None): + def __init__( + self, + initial_layout: dict, + circuit: Circuit, + blocks: Optional[CircuitBlocks] = None, + ): if blocks is not None: self.circuit_blocks = blocks else: @@ -64,7 +73,13 @@ def __init__(self, initial_layout: dict, circuit: Circuit, blocks=None): self._swaps = 0 def set_circuit_logical(self, circuit_logical_map: list): - """Set the current circuit to logical qubit mapping.""" + """Sets the current circuit to logical qubit mapping. + + Method works in-place. + + Args: + circuit_logical_map (list): logical mapping. + """ self._circuit_logical = circuit_logical_map def blocks_qubits_pairs(self): @@ -74,17 +89,25 @@ def blocks_qubits_pairs(self): def execute_block(self, block: Block): """Executes a block by removing it from the circuit representation and adding it to the routed circuit. + + Method works in-place. + + Args: + block (:class:`qibo.transpiler.blocks.Block`): block to be removed. """ self._routed_blocks.add_block( block.on_qubits(self.get_physical_qubits(block, index=True)) ) self.circuit_blocks.remove_block(block) - def routed_circuit(self, circuit_kwargs=None): - """Return the routed circuit. + def routed_circuit(self, circuit_kwargs: Optional[dict] = None): + """Returns the routed circuit. Args: - circuit_kwargs (dict): original circuit init_kwargs. + circuit_kwargs (dict): original circuit ``init_kwargs``. + + Returns: + :class:`qibo.models.circuit.Circuit`: Routed circuit. """ return self._routed_blocks.circuit(circuit_kwargs=circuit_kwargs) @@ -94,12 +117,17 @@ def final_layout(self): "q" + str(self.circuit_to_physical(i)): i for i in range(len(self._circuit_logical)) } + return dict(sorted(unsorted_dict.items())) def update(self, swap: tuple): - """Updates the logical-physical qubit mapping after applying a SWAP - and add the SWAP gate to the routed blocks, the swap is represented by a tuple containing - the logical qubits to be swapped. + """Updates the logical-physical qubit mapping after applying a ``SWAP`` + + Adds the :class:`qibo.gates.gates.SWAP` gate to the routed blocks. + Method works in-place. + + Args: + swap (tuple): tuple containing the logical qubits to be swapped. """ physical_swap = self.logical_to_physical(swap, index=True) self._routed_blocks.add_block( @@ -112,20 +140,43 @@ def update(self, swap: tuple): self._circuit_logical[idx_0], self._circuit_logical[idx_1] = swap[1], swap[0] def get_logical_qubits(self, block: Block): - """Returns the current logical qubits where a block is acting""" + """Returns the current logical qubits where a block is acting on. + + Args: + block (:class:`qibo.transpiler.blocks.Block`): block to be analysed. + + Returns: + (tuple): logical qubits where a block is acting on. + """ return self.circuit_to_logical(block.qubits) - def get_physical_qubits(self, block: Block or int, index=False): - """Returns the physical qubits where a block is acting. - If index is True the qubits are returned as indices of the connectivity nodes. + def get_physical_qubits(self, block: Union[int, Block], index: bool = False): + """Returns the physical qubits where a block is acting on. + + Args: + block (int or :class:`qibo.transpiler.blocks.Block`): block to be analysed. + index (bool, optional): If ``True``, qubits are returned as indices of + the connectivity nodes. Defaults to ``False``. + + Returns: + (tuple): physical qubits where a block is acting on. + """ if isinstance(block, int): block = self.circuit_blocks.search_by_index(block) + return self.logical_to_physical(self.get_logical_qubits(block), index=index) - def logical_to_physical(self, logical_qubits: tuple, index=False): + def logical_to_physical(self, logical_qubits: tuple, index: bool = False): """Returns the physical qubits associated to the logical qubits. - If index is True the qubits are returned as indices of the connectivity nodes. + + Args: + logical_qubits (tuple): physical qubits. + index (bool, optional): If ``True``, qubits are returned as indices of + `the connectivity nodes. Defaults to ``False``. + + Returns: + (tuple): physical qubits associated to the logical qubits. """ if not index: return tuple( @@ -134,21 +185,44 @@ def logical_to_physical(self, logical_qubits: tuple, index=False): ] for i in range(2) ) + return tuple(self._physical_logical.index(logical_qubits[i]) for i in range(2)) def circuit_to_logical(self, circuit_qubits: tuple): - """Returns the current logical qubits associated to the initial circuit qubits.""" + """Returns the current logical qubits associated to the initial circuit qubits. + + Args: + circuit_qubits (tuple): circuit qubits. + + Returns: + (tuple): logical qubits. + """ return tuple(self._circuit_logical[circuit_qubits[i]] for i in range(2)) def circuit_to_physical(self, circuit_qubit: int): - """Returns the current physical qubit associated to an initial circuit qubit.""" + """Returns the current physical qubit associated to an initial circuit qubit. + + Args: + circuit_qubit (int): circuit qubit. + + Returns: + (int): physical qubit. + """ return self._graph_qubits_names[ self._physical_logical.index(self._circuit_logical[circuit_qubit]) ] def physical_to_logical(self, physical_qubit: int): - """Returns the current logical qubit associated to a physical qubit (connectivity graph node).""" + """Returns the current logical qubit associated to a physical qubit (connectivity graph node). + + Args: + physical_qubit (int): physical qubit. + + Returns: + (int): logical qubit. + """ physical_qubit_index = self._graph_qubits_names.index(physical_qubit) + return self._physical_logical[physical_qubit_index] @@ -157,17 +231,18 @@ class ShortestPaths(Router): Args: connectivity (:class:`networkx.Graph`): chip connectivity. - sampling_split (float, optional): fraction of paths tested - (between :math:`0` and :math:`1`). Defaults to :math:`1.0`. - seed (int): seed for the random number generator. + seed (int, optional): seed for the random number generator. + If ``None``, defaults to :math:`42`. Defaults to ``None``. """ - def __init__(self, connectivity: nx.Graph, seed=42): + def __init__(self, connectivity: nx.Graph, seed: Optional[int] = None): self.connectivity = connectivity self._front_layer = None self.circuit = None self._dag = None self._final_measurements = None + if seed is None: + seed = 42 random.seed(seed) @property @@ -387,18 +462,18 @@ def __init__( lookahead: int = 2, decay_lookahead: float = 0.6, delta: float = 0.001, - seed=None, + seed: Optional[int] = None, ): """Routing algorithm proposed in Ref [1]. Args: connectivity (dict): hardware chip connectivity. - lookahead (int): lookahead factor, how many dag layers will be considered in computing the cost. - decay_lookahead (float): value in interval [0,1]. + lookahead (int, optional): lookahead factor, how many dag layers will be considered in computing the cost. + decay_lookahead (float, optional): value in interval [0,1]. How the weight of the distance in the dag layers decays in computing the cost. - delta (float): this parameter defines the number of swaps vs depth trade-off by deciding + delta (float, optional): this parameter defines the number of swaps vs depth trade-off by deciding how the algorithm tends to select non-overlapping SWAPs. - seed (int): seed for the candidate random choice as tiebraker. + seed (int, optional): seed for the candidate random choice as tiebraker. References: 1. G. Li, Y. Ding, and Y. Xie, *Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices*. From 8d2dd1faee9dcb7cd5d02b2c5cf34027ca18aaa5 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 11:01:07 +0400 Subject: [PATCH 53/78] fix test --- tests/test_transpiler_pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index 86e84b3b01..cc01a09b61 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -78,7 +78,7 @@ def test_restrict_qubits_error_no_subset(): def test_restrict_qubits_error_not_connected(): with pytest.raises(ConnectivityError) as excinfo: restrict_connectivity_qubits(star_connectivity(), [1, 3]) - assert "The new connectivity graph is not connected." in str(excinfo.value) + assert "New connectivity graph is not connected." in str(excinfo.value) def test_restrict_qubits(): From 38aae682d7330e7262fb15b98aa9721f25d086c2 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Wed, 20 Dec 2023 11:33:23 +0400 Subject: [PATCH 54/78] improved docstrings `router` --- src/qibo/transpiler/router.py | 174 +++++++++++++++++++++++----------- 1 file changed, 120 insertions(+), 54 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 54ff6f3899..222c4b7198 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -247,7 +247,7 @@ def __init__(self, connectivity: nx.Graph, seed: Optional[int] = None): @property def added_swaps(self): - """Number of SWAP gates added to the circuit during routing.""" + """Returns the number of SWAP gates added to the circuit during routing.""" return self.circuit._swaps def __call__(self, circuit: Circuit, initial_layout: dict): @@ -277,7 +277,10 @@ def __call__(self, circuit: Circuit, initial_layout: dict): return routed_circuit, self.circuit.final_layout() def _find_new_mapping(self): - """Find new qubit mapping. The mapping is found by looking for the shortest path.""" + """Find new qubit mapping. Mapping is found by looking for the shortest path. + + Method works in-place. + """ candidates_evaluation = [] for candidate in self._candidates(): cost = self._compute_cost(candidate) @@ -292,8 +295,8 @@ def _find_new_mapping(self): self._add_swaps(best_candidate, self.circuit) def _candidates(self): - """Return all possible shortest paths, - a list contains the new mapping and a second list contains the path meeting point. + """Returns all possible shortest paths in a ``list`` that contains + the new mapping and a second ``list`` containing the path meeting point. """ target_qubits = self.circuit.get_physical_qubits(self._front_layer[0]) path_list = list( @@ -305,12 +308,15 @@ def _candidates(self): for path in path_list: for meeting_point in range(len(path) - 1): all_candidates.append((path, meeting_point)) + return all_candidates @staticmethod def _add_swaps(candidate: tuple, circuitmap: CircuitMap): """Adds swaps to the circuit to move qubits. + Method works in-place. + Args: candidate (tuple): contains path to move qubits and qubit meeting point in the path. circuitmap (CircuitMap): representation of the circuit. @@ -336,10 +342,14 @@ def _add_swaps(candidate: tuple, circuitmap: CircuitMap): ) ) - def _compute_cost(self, candidate): - """Greedy algorithm to decide which path to take, and how qubits should walk. + def _compute_cost(self, candidate: tuple): + """Greedy algorithm that decides which path to take and how qubits should be walked. + The cost is computed as minus the number of successive gates that can be executed. + Args: + candidate (tuple): contains path to move qubits and qubit meeting point in the path. + Returns: (list, int): best path to move qubits and qubit meeting point in the path. """ @@ -377,10 +387,11 @@ def _compute_cost(self, candidate): all_executed = False if not all_executed: break + return -successive_executed_gates def _check_execution(self): - """Check if some blocks in the front layer can be executed in the current configuration. + """Checks if some blocks in the front layer can be executed in the current configuration. Returns: (list): executable blocks if there are, ``None`` otherwise. @@ -394,13 +405,19 @@ def _check_execution(self): executable_blocks.append(block) if len(executable_blocks) == 0: return None + return executable_blocks def _execute_blocks(self, blocklist: list): - """Execute a list of blocks: - -Remove the correspondent nodes from the dag and circuit representation. - -Add the executed blocks to the routed circuit. - -Update the dag layers and front layer. + """Executes a list of blocks: + -Remove the correspondent nodes from the dag and circuit representation. + -Add the executed blocks to the routed circuit. + -Update the dag layers and front layer. + + Method works in-place. + + Args: + blocklist (list): list of blocks. """ for block_id in blocklist: block = self.circuit.circuit_blocks.search_by_index(block_id) @@ -409,7 +426,10 @@ def _execute_blocks(self, blocklist: list): self._update_front_layer() def _update_front_layer(self): - """Update the front layer of the dag.""" + """Updates the front layer of the dag. + + Method works in-place. + """ for layer, nodes in enumerate(nx.topological_generations(self._dag)): for node in nodes: self._dag.nodes[node]["layer"] = layer @@ -419,18 +439,29 @@ def _update_front_layer(self): def _preprocessing(self, circuit: Circuit, initial_layout: dict): """The following objects will be initialised: - - circuit: class to represent circuit and to perform logical-physical qubit mapping. - - _final_measurements: measurement gates at the end of the circuit. - - _front_layer: list containing the blocks to be executed. + - circuit: class to represent circuit and to perform logical-physical qubit mapping. + - _final_measurements: measurement gates at the end of the circuit. + - _front_layer: list containing the blocks to be executed. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be preprocessed. + initial_layout (dict): initial physical-to-logical qubit mapping. """ - copied_circuit = _copy_circuit(circuit) + copied_circuit = circuit.copy(deep=True) self._final_measurements = self._detach_final_measurements(copied_circuit) self.circuit = CircuitMap(initial_layout, copied_circuit) self._dag = _create_dag(self.circuit.blocks_qubits_pairs()) self._update_front_layer() def _detach_final_measurements(self, circuit: Circuit): - """Detach measurement gates at the end of the circuit for separate handling.""" + """Detaches measurement gates at the end of the circuit for separate handling. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuits to be processed. + + Returns: + (NoneType or list): list of measurements. If no measurements, returns ``None``. + """ final_measurements = [] for gate in circuit.queue[::-1]: if isinstance(gate, gates.M): @@ -440,10 +471,11 @@ def _detach_final_measurements(self, circuit: Circuit): break if not final_measurements: return None + return final_measurements[::-1] def _append_final_measurements(self, routed_circuit: Circuit): - """Append the final measurment gates on the correct qubits conserving the measurement register.""" + """Appends the final measurment gates on the correct qubits conserving the measurement register.""" for measurement in self._final_measurements: original_qubits = measurement.qubits routed_qubits = ( @@ -452,6 +484,7 @@ def _append_final_measurements(self, routed_circuit: Circuit): routed_circuit.add( measurement.on_qubits(dict(zip(original_qubits, routed_qubits))) ) + return routed_circuit @@ -467,13 +500,17 @@ def __init__( """Routing algorithm proposed in Ref [1]. Args: - connectivity (dict): hardware chip connectivity. - lookahead (int, optional): lookahead factor, how many dag layers will be considered in computing the cost. - decay_lookahead (float, optional): value in interval [0,1]. + connectivity (:class:`networkx.Graph`): hardware chip connectivity. + lookahead (int, optional): lookahead factor, how many dag layers will be considered + in computing the cost. Defaults to :math:`2`. + decay_lookahead (float, optional): value in interval :math:`[0, 1]`. How the weight of the distance in the dag layers decays in computing the cost. - delta (float, optional): this parameter defines the number of swaps vs depth trade-off by deciding + Defaults to :math:`0.6`. + delta (float, optional): defines the number of SWAPs vs depth trade-off by deciding how the algorithm tends to select non-overlapping SWAPs. + Defaults to math:`10^{-3}`. seed (int, optional): seed for the candidate random choice as tiebraker. + Defaults to ``None``. References: 1. G. Li, Y. Ding, and Y. Xie, *Tackling the Qubit Mapping Problem for NISQ-Era Quantum Devices*. @@ -520,21 +557,25 @@ def __call__(self, circuit: Circuit, initial_layout: dict): @property def added_swaps(self): - """Number of SWAP gates added to the circuit during routing.""" + """Returns the number of SWAP gates added to the circuit during routing.""" return self.circuit._swaps def _preprocessing(self, circuit: Circuit, initial_layout: dict): """The following objects will be initialised: - - circuit: class to represent circuit and to perform logical-physical qubit mapping. - - _final_measurements: measurement gates at the end of the circuit. - - _dist_matrix: matrix reporting the shortest path lengh between all node pairs. - - _dag: direct acyclic graph of the circuit based on commutativity. - - _memory_map: list to remember previous SWAP moves. - - _front_layer: list containing the blocks to be executed. - - _delta_register: list containing the special weigh added to qubits - to prevent overlapping swaps. + - circuit: class to represent circuit and to perform logical-physical qubit mapping. + - _final_measurements: measurement gates at the end of the circuit. + - _dist_matrix: matrix reporting the shortest path lengh between all node pairs. + - _dag: direct acyclic graph of the circuit based on commutativity. + - _memory_map: list to remember previous SWAP moves. + - _front_layer: list containing the blocks to be executed. + - _delta_register: list containing the special weigh added to qubits + to prevent overlapping swaps. + + Args: + circuit (:class:`qibo.models.circuit.Circuit`): circuit to be preprocessed. + initial_layout (dict): initial physical-to-logical qubit mapping. """ - copied_circuit = _copy_circuit(circuit) + copied_circuit = circuit.copy(deep=True) self._final_measurements = self._detach_final_measurements(copied_circuit) self.circuit = CircuitMap(initial_layout, copied_circuit) self._dist_matrix = nx.floyd_warshall_numpy(self.connectivity) @@ -558,7 +599,14 @@ def _detach_final_measurements(self, circuit: Circuit): return final_measurements[::-1] def _append_final_measurements(self, routed_circuit: Circuit): - """Append the final measurment gates on the correct qubits conserving the measurement register.""" + """Appends final measurment gates on the correct qubits conserving the measurement register. + + Args: + routed_circuit (:class:`qibo.models.circuit.Circuit`): original circuit. + + Returns: + (:class:`qibo.models.circuit.Circuit`) routed circuit. + """ for measurement in self._final_measurements: original_qubits = measurement.qubits routed_qubits = ( @@ -567,16 +615,23 @@ def _append_final_measurements(self, routed_circuit: Circuit): routed_circuit.add( measurement.on_qubits(dict(zip(original_qubits, routed_qubits))) ) + return routed_circuit def _update_dag_layers(self): - """Update dag layers and put them in topological order.""" + """Update dag layers and put them in topological order. + + Method works in-place. + """ for layer, nodes in enumerate(nx.topological_generations(self._dag)): for node in nodes: self._dag.nodes[node]["layer"] = layer def _update_front_layer(self): - """Update the front layer of the dag.""" + """Update the front layer of the dag. + + Method works in-place. + """ self._front_layer = self._get_dag_layer(0) def _get_dag_layer(self, n_layer): @@ -589,16 +644,18 @@ def _find_new_mapping(self): self._memory_map.append(deepcopy(self.circuit._circuit_logical)) for candidate in self._swap_candidates(): candidates_evaluation[candidate] = self._compute_cost(candidate) + best_cost = min(candidates_evaluation.values()) best_candidates = [ key for key, value in candidates_evaluation.items() if value == best_cost ] best_candidate = random.choice(best_candidates) + for qubit in self.circuit.logical_to_physical(best_candidate, index=True): self._delta_register[qubit] += self.delta self.circuit.update(best_candidate) - def _compute_cost(self, candidate): + def _compute_cost(self, candidate: int): """Compute the cost associated to a possible SWAP candidate.""" temporary_circuit = CircuitMap( initial_layout=self.circuit.initial_layout, @@ -607,8 +664,10 @@ def _compute_cost(self, candidate): ) temporary_circuit.set_circuit_logical(deepcopy(self.circuit._circuit_logical)) temporary_circuit.update(candidate) + if temporary_circuit._circuit_logical in self._memory_map: return float("inf") + tot_distance = 0.0 weight = 1.0 for layer in range(self.lookahead + 1): @@ -623,11 +682,17 @@ def _compute_cost(self, candidate): ) tot_distance += weight * avg_layer_distance weight *= self.decay + return tot_distance def _swap_candidates(self): - """Return a list of possible candidate SWAPs (to be applied on logical qubits directly). - The possible candidates are the ones sharing at least one qubit with a block in the front layer. + """Returns a list of possible candidate SWAPs to be applied on logical qubits directly. + + The possible candidates are the ones sharing at least one qubit + with a block in the front layer. + + Returns: + (list): list of candidates. """ candidates = [] for block in self._front_layer: @@ -643,6 +708,7 @@ def _swap_candidates(self): ) if candidate not in candidates: candidates.append(candidate) + return candidates def _check_execution(self): @@ -658,16 +724,23 @@ def _check_execution(self): or not self.circuit.circuit_blocks.search_by_index(block).entangled ): executable_blocks.append(block) + if len(executable_blocks) == 0: return None + return executable_blocks def _execute_blocks(self, blocklist: list): - """Execute a list of blocks: + """Executes a list of blocks: -Remove the correspondent nodes from the dag and circuit representation. -Add the executed blocks to the routed circuit. -Update the dag layers and front layer. -Reset the mapping memory. + + Method works in-place. + + Args: + blocklist (list): list of blocks. """ for block_id in blocklist: block = self.circuit.circuit_blocks.search_by_index(block_id) @@ -679,21 +752,11 @@ def _execute_blocks(self, blocklist: list): self._delta_register = [1.0 for _ in self._delta_register] -def _copy_circuit(circuit: Circuit): - """Helper method for :meth:`qibo.transpiler.router`. - - Return a copy of the circuit to avoid altering the original circuit. - This copy conserves the registers of the measurement gates. - """ - new_circuit = Circuit(circuit.nqubits) - for gate in circuit.queue: - new_circuit.add(gate) - return new_circuit - - -def _create_dag(gates_qubits_pairs): +def _create_dag(gates_qubits_pairs: list): """Helper method for :meth:`qibo.transpiler.router.Sabre`. - Create direct acyclic graph (dag) of the circuit based on two qubit gates commutativity relations. + + Create direct acyclic graph (dag) of the circuit based on two qubit gates + commutativity relations. Args: gates_qubits_pairs (list): list of qubits tuples where gates/blocks acts. @@ -715,11 +778,13 @@ def _create_dag(gates_qubits_pairs): if len(saturated_qubits) >= 2: break dag.add_edges_from(connectivity_list) + return _remove_redundant_connections(dag) def _remove_redundant_connections(dag: nx.DiGraph): - """Helper method for :func:`_create_dag`. + """Helper method for :func:`qibo.transpiler.router._create_dag`. + Remove redundant connection from a DAG using transitive reduction. Args: @@ -732,4 +797,5 @@ def _remove_redundant_connections(dag: nx.DiGraph): new_dag.add_nodes_from(range(dag.number_of_nodes())) transitive_reduction = nx.transitive_reduction(dag) new_dag.add_edges_from(transitive_reduction.edges) + return new_dag From abdd6234e5b250ed85e88478bf525aa43ae31935 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 20 Dec 2023 08:35:54 +0100 Subject: [PATCH 55/78] updating doc strings --- src/qibo/models/error_mitigation.py | 108 +++++++++++++++++--------- tests/test_models_error_mitigation.py | 4 +- 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 4731d43a2c..93792fac49 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -105,7 +105,7 @@ def ZNE( observable, noise_levels, noise_model=None, - nshots=int(1e4), + nshots=10000, solve_for_gammas=False, insertion_gate="CNOT", readout: dict = {}, @@ -120,11 +120,11 @@ def ZNE( Args: circuit (:class:`qibo.models.Circuit`): input circuit. - observable (numpy.ndarray): Observable to measure. + observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): Observable to measure. noise_levels (numpy.ndarray): Sequence of noise levels. noise_model (:class:`qibo.noise.NoiseModel`, optional): Noise model applied to simulate noisy computation. - nshots (int, optional): Number of shots. Defauylts to 10000. + nshots (int, optional): Number of shots. Defaults to :math:`10000`. solve_for_gammas (bool, optional): If ``True``, explicitly solve the equations to obtain the ``gamma`` coefficients. Default is ``False``. insertion_gate (str, optional): gate to be used in the insertion. @@ -133,8 +133,9 @@ def ZNE( readout (dict, optional): A dictionary that may contain the following keys: - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -255,7 +256,7 @@ def CDR( circuit, observable, noise_model, - nshots: int = int(1e4), + nshots: int = 10000, model=lambda x, a, b: a * x + b, n_training_samples: int = 100, full_output: bool = False, @@ -268,10 +269,10 @@ def CDR( Args: circuit (:class:`qibo.models.Circuit`): input circuit decomposed in the primitive gates ``X``, ``CNOT``, ``RX(pi/2)``, ``RZ(theta)``. - observable (numpy.ndarray): observable to be measured. + observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): observable to be measured. noise_model (:class:`qibo.noise.NoiseModel`): noise model used for simulating noisy computation. - nshots (int, optional): number of shots. Defaults 10000. + nshots (int, optional): number of shots. Defaults :math:`10000`. model (callable, optional): model used for fitting. This should be a callable function object ``f(x, *params)``, taking as input the predictor variable and the parameters. Default is a simple linear model ``f(x,a,b) := a*x + b``. @@ -281,8 +282,9 @@ def CDR( readout (dict, optional): A dictionary that may contain the following keys: - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -331,7 +333,7 @@ def vnCDR( observable, noise_levels, noise_model, - nshots: int = int(1e4), + nshots: int = 10000, model=lambda x, *params: (x * np.array(params).reshape(-1, 1)).sum(0), n_training_samples: int = 100, insertion_gate: str = "CNOT", @@ -345,11 +347,11 @@ def vnCDR( Args: circuit (:class:`qibo.models.Circuit`): input circuit decomposed in the primitive gates ``X``, ``CNOT``, ``RX(pi/2)``, ``RZ(theta)``. - observable (numpy.ndarray): observable to be measured. + observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): observable to be measured. noise_levels (numpy.ndarray): sequence of noise levels. noise_model (:class:`qibo.noise.NoiseModel`): noise model used for simulating noisy computation. - nshots (int, optional): number of shots. + nshots (int, optional): number of shots. Defaults to :math:`10000`. model (callable, optional): model used for fitting. This should be a callable function object ``f(x, *params)``, taking as input the predictor variable and the parameters. Default is a simple linear model ``f(x,a,b) := a*x + b``. @@ -362,8 +364,9 @@ def vnCDR( readout (dict, optional): A dictionary that may contain the following keys: - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -457,17 +460,18 @@ def iterative_bayesian_unfolding(probabilities, response_matrix, iterations=10): return unfolded_probabilities -def get_calibration_matrix( +def get_response_matrix( nqubits, qubit_map=None, noise_model=None, nshots: int = 10000, backend=None ): """Computes the calibration matrix for readout mitigation. Args: nqubits (int): Total number of qubits. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. noise_model (:class:`qibo.noise.NoiseModel`, optional): noise model used for simulating noisy computation. This matrix can be used to mitigate the effect of `qibo.noise.ReadoutError`. - nshots (int, optional): number of shots. Defaults to 10000. + nshots (int, optional): number of shots. Defaults to :math:`10000`. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -481,7 +485,7 @@ def get_calibration_matrix( if backend is None: # pragma: no cover backend = GlobalBackend() - calibration_matrix = np.zeros((2**nqubits, 2**nqubits)) + response_matrix = np.zeros((2**nqubits, 2**nqubits)) for i in range(2**nqubits): binary_state = format(i, f"0{nqubits}b") @@ -501,19 +505,19 @@ def get_calibration_matrix( column = np.zeros(2**nqubits) for key, value in frequencies.items(): column[int(key, 2)] = value / nshots - calibration_matrix[:, i] = column + response_matrix[:, i] = column - return calibration_matrix + return response_matrix -def apply_cal_mat_readout_mitigation(state, calibration_matrix, iterations=None): +def apply_cal_mat_readout_mitigation(state, response_matrix, iterations=None): """ Applies readout error mitigation to the given state using the provided calibration matrix. Args: state (:class:`qibo.measurements.CircuitResult`): the input state to be updated. This state should contain the frequencies that need to be mitigated. - calibration_matrix (numpy.ndarray, optional): the calibration matrix for readout mitigation. + response_matrix (numpy.ndarray): the calibration matrix for readout mitigation. iterations (int, optional): the number of iterations to use for the Iterative Bayesian Unfolding method. If ``None`` the 'inverse' method is used. Defaults to ``None``. @@ -527,12 +531,12 @@ def apply_cal_mat_readout_mitigation(state, calibration_matrix, iterations=None) frequencies = frequencies.reshape(-1, 1) if iterations is None: - calibration_matrix = np.linalg.inv(calibration_matrix) + calibration_matrix = np.linalg.inv(response_matrix) for i, value in enumerate(calibration_matrix @ frequencies): state._frequencies[i] = float(value) else: mitigated_probabilities = iterative_bayesian_unfolding( - frequencies / np.sum(frequencies), calibration_matrix, iterations + frequencies / np.sum(frequencies), response_matrix, iterations ) mitigated_frequencies = np.round( mitigated_probabilities * np.sum(frequencies), 0 @@ -550,7 +554,7 @@ def apply_cal_mat_readout_mitigation(state, calibration_matrix, iterations=None) def apply_randomized_readout_mitigation( circuit, noise_model=None, - nshots: int = int(1e3), + nshots: int = 10000, ncircuits: int = 10, qubit_map=None, backend=None, @@ -561,9 +565,10 @@ def apply_randomized_readout_mitigation( circuit (:class:`qibo.models.Circuit`): input circuit. noise_model(:class:`qibo.noise.NoiseModel`, optional): noise model used for simulating noisy computation. Defaults to ``None``. - nshots (int, optional): number of shots. Defaults to 10000. + nshots (int, optional): number of shots. Defaults to :math:`10000`. ncircuits (int, optional): number of randomized circuits. Each of them uses ``int(nshots / ncircuits)`` shots. Defaults to 10. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -633,11 +638,31 @@ def apply_readout_mitigation( circuit, observable, noise_model=None, - nshots: int = int(1e3), + nshots: int = 10000, readout={}, qubit_map=None, backend=None, ): + """ + Applies readout error mitigation to the given circuit and observable. + + Args: + circuit (qibo.models.Circuit): input circuit. + observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): The observable to be measured. + noise_model (qibo.models.noise.Noise, optional): the noise model to be applied. Defaults to ``None``. + nshots (int, optional): the number of shots for the circuit execution. Defaults to :math:`10000`. + readout (dict, optional): a dictionary that may contain the following keys: + - 'ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. + If provided, the corresponding readout error mitigation method is used. Defaults to {}. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. + backend (qibo.backends.abstract.Backend, optional): the backend to be used in the execution. + If None, it uses the global backend. Defaults to ``None``. + + Returns: + float: The mitigated expectation value of the observable. + """ if backend is None: # pragma: no cover backend = GlobalBackend() @@ -672,13 +697,7 @@ def sample_clifford_training_circuit( """Samples a training circuit for CDR by susbtituting some of the non-Clifford gates. Args: - circuit (:class:`qibo.models.Circuit`): circuit to sample from, - decomposed in ``RX(pi/2)``, ``X``, ``CNOT`` and ``RZ`` gates. - replacement_gates (list, optional): candidates for the substitution of the - non-Clifford gates. The ``list`` should be composed by ``tuples`` of the - form (``gates.XYZ``, ``kwargs``). For example, phase gates are used by default: - ``list((RZ, {'theta':0}), (RZ, {'theta':pi/2}), (RZ, {'theta':pi}), (RZ, {'theta':3*pi/2}))``. - sigma (float, optional): standard devation of the Gaussian distribution used for sampling. + circuit (:class:`qibo.models.Circuit`): circuit to sample from. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -732,8 +751,7 @@ def error_sensitive_circuit(circuit, observable, backend=None): Args: circuit (:class:`qibo.models.Circuit`): input circuit. - observable (numpy.ndarray): Pauli observable to be measured. - fuse (bool, optional): if True, fuse the single qubits gates in the circuit. + observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): Pauli observable to be measured. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. if ``None``, it uses :class:`qibo.backends.GlobalBackend`. Defaults to ``None``. @@ -824,16 +842,16 @@ def ICS( Args: circuit (:class:`qibo.models.Circuit`): input circuit. - observable (numpy.ndarray): the observable to be measured. + observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): the observable to be measured. readout (dict, optional): A dictionary that may contain the following keys: - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian update method of readout error mitigation. + - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If ``None`` the 'inverse' method is used. If provided, the corresponding readout error mitigation method is used. - qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. + qubit_map (list, optional): the qubit map. If ``None``, a list of range of circuit's qubits is used. Defaults to ``None``. noise_model (qibo.models.noise.Noise, optional): the noise model to be applied. Defaults to ``None``. - nshots (int, optional): the number of shots for the circuit execution. Defaults to 10000. + nshots (int, optional): the number of shots for the circuit execution. Defaults to :math:`10000`. n_training_samples (int, optional): the number of training samples. Defaults to 10. full_output (bool, optional): if ``True``, this function returns additional information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``. @@ -916,7 +934,21 @@ def ICS( return mitigated_expectation -def _circuit_conf(circuit, qubit_map, noise_model, nshots, backend=None): +def _circuit_conf(circuit, qubit_map, noise_model=None, nshots=10000, backend=None): + """ + Helper function to execute the given circuit with the specified parameters. + + Args: + circuit (qibo.models.Circuit): input circuit. + qubit_map (list): the qubit map. If ``None``, a list of range of circuit's qubits is used. Defaults to ``None``. + noise_model (qibo.models.noise.Noise, optional): The noise model to be applied. Defaults to ``None``. + nshots (int): the number of shots for the circuit execution. Defaults to :math:`10000`.. + backend (qibo.backends.abstract.Backend, optional): the backend to be used in the execution. + If None, it uses the global backend. Defaults to ``None``. + + Returns: + qibo.states.CircuitResult: The result of the circuit execution. + """ from qibo.transpiler.placer import Custom if backend is None: # pragma: no cover diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 6951b4e6e0..d8c0066793 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -9,7 +9,7 @@ ICS, ZNE, apply_readout_mitigation, - get_calibration_matrix, + get_response_matrix, sample_clifford_training_circuit, sample_training_circuit_cdr, vnCDR, @@ -298,7 +298,7 @@ def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): noise = NoiseModel() noise.add(ReadoutError(probabilities=p), gate=gates.M) if method == "cal_matrix": - calibration = get_calibration_matrix( + calibration = get_response_matrix( nmeas, None, noise, nshots=nshots, backend=backend ) readout = {"calibration_matrix": calibration, "ibu_iters": ibu_iters} From d9d0e76cc18d9a77c2f6930d609543c6c2e8f6af Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 20 Dec 2023 09:05:36 +0100 Subject: [PATCH 56/78] updating documentation --- doc/source/api-reference/qibo.rst | 48 +++++++++++++++++++++---- src/qibo/models/error_mitigation.py | 54 ++++++++++++++++------------- 2 files changed, 71 insertions(+), 31 deletions(-) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index 996586c4ab..20cbdfe037 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -279,7 +279,7 @@ and both can be used as standalone functions or in combination with the other general mitigation methods by setting the paramter `readout`. -Calibration Matrix +Response Matrix """""""""""""""""" Given :math:`n` qubits, all the possible :math:`2^n` states are constructed via the application of the corresponding sequence of :math:`X` gates @@ -297,14 +297,22 @@ columns :math:`M=\big(F_0^{noisy},...,F_{n-1}^{noisy}\big)`. We have indeed that and, therefore, the calibration matrix obtained as :math:`M_{\text{cal}}=M^{-1}` can be used to recover the noise-free frequencies. -.. autofunction:: qibo.models.error_mitigation.calibration_matrix +The calibration matrix :math:`M_{\text{cal}}` lacks stochasticity, resulting in a 'negative probability' issue. +The distributions that arise after applying :math:`M_{\text{cal}}` are quasiprobabilities; +the individual elements can be negative surpass 1, provided they sum to 1. +It is posible to use Iterative Bayesian Unfolding (IBU) to preserve non-negativity. +See `Srinivasan et al `_ for more details. + + + +.. autofunction:: qibo.models.error_mitigation.get_response_matrix .. autofunction:: qibo.models.error_mitigation.apply_readout_mitigation -Randomized -"""""""""" +Randomized readout mitigation +"""""""""""""""""""""""""""""" This approach converts the effect of any noise map :math:`A` into a single multiplication factor for each Pauli observable, that is, diagonalizes the measurement channel. The multiplication factor :math:`\lambda` can be directly measured even without @@ -320,7 +328,7 @@ factor results in the mitigated Pauli expectation value :math:`\langle O\rangle_ Zero Noise Extrapolation (ZNE) """""""""""""""""""""""""""""" -Given a noisy circuit :math:`C` and an observable :math:`A`, Zero Noise Extrapolation (ZNE) +Given a noisy circuit :math:`C` and an observable :math:`A`, Zero Noise Extrapolation (ZNE) consists in running :math:`n+1` versions of the circuit with different noise levels :math:`\{c_j\}_{j=0..n}` and, for each of them, measuring the expected value of the observable :math:`E_j=\langle A\rangle_j`. @@ -381,7 +389,7 @@ See `Sopena et al `_ for more details. .. autofunction:: qibo.models.error_mitigation.CDR -.. autofunction:: qibo.models.error_mitigation.sample_training_circuit +.. autofunction:: qibo.models.error_mitigation.sample_training_circuit_cdr Variable Noise CDR (vnCDR) @@ -416,6 +424,34 @@ See `Sopena et al `_ for all the details. .. autofunction:: qibo.models.error_mitigation.vnCDR +Importance Clifford Sampling (ICS) +"""""""""""""""""""""""""""""" + +In the Importance Clifford Sampling (ICS) method, a set of :math:`n` circuits +:math:`S_n=\{C_i\}_{i=1,..,n}` that stabilizes a given Pauli observable is generated starting from the original circuit +:math:`C_0` by replacing all the non-Clifford gates with Clifford ones. +Given an observable :math:`A`, all the circuits of :math:`S_n` are both simulated +to obtain the correspondent expected values of :math:`A` in noise-free condition +:math:`\{a_i^{exact}\}_{i=1,..,n}`, and run in noisy conditions to obtain the noisy +expected values :math:`\{a_i^{noisy}\}_{i=1,..,n}`. + +Finally, a theoretically inspired model :math:`f` is learned using the training data. + +The mitigated expected value of :math:`A` at the end of :math:`C_0` is then +obtained simply with :math:`f(a_0^{noisy})`. + +In this implementation the initial circuit is expected to be decomposed in the three +Clifford gates :math:`RX(\frac{\pi}{2})`, :math:`CNOT`, :math:`X` and in :math:`RZ(\theta)` +(which is Clifford only for :math:`\theta=\frac{n\pi}{2}`). +By default the set of Clifford gates used for substitution is +:math:`\{RZ(0),RZ(\frac{\pi}{2}),RZ(\pi),RZ(\frac{3}{2}\pi)\}`. +See `Sopena et al `_ for more details. + +.. autofunction:: qibo.models.error_mitigation.ICS + + +.. autofunction:: qibo.models.error_mitigation.sample_clifford_training_circuit + _______________________ .. _Gates: diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 93792fac49..6d1bb4cf64 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -130,11 +130,12 @@ def ZNE( insertion_gate (str, optional): gate to be used in the insertion. If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``. Defaults to ``"CNOT"``. - readout (dict, optional): A dictionary that may contain the following keys: - - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. - If provided, the corresponding readout error mitigation method is used. + readout (dict, optional): a dictionary that may contain the following keys: + + * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. @@ -279,11 +280,12 @@ def CDR( n_training_samples (int, optional): number of training circuits to sample. Defaults to 100. full_output (bool, optional): if ``True``, this function returns additional information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``. - readout (dict, optional): A dictionary that may contain the following keys: - - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. - If provided, the corresponding readout error mitigation method is used. + readout (dict, optional): a dictionary that may contain the following keys: + + * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. @@ -361,11 +363,12 @@ def vnCDR( Default is ``"CNOT"``. full_output (bool, optional): if ``True``, this function returns additional information: ``val``, ``optimal_params``, ``train_val``. Defaults to ``False``. - readout (dict, optional): A dictionary that may contain the following keys: - - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. - If provided, the corresponding readout error mitigation method is used. + readout (dict, optional): a dictionary that may contain the following keys: + + * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (:class:`qibo.backends.abstract.Backend`, optional): backend to be used in the execution. If ``None``, it uses :class:`qibo.backends.GlobalBackend`. @@ -652,10 +655,11 @@ def apply_readout_mitigation( noise_model (qibo.models.noise.Noise, optional): the noise model to be applied. Defaults to ``None``. nshots (int, optional): the number of shots for the circuit execution. Defaults to :math:`10000`. readout (dict, optional): a dictionary that may contain the following keys: - - 'ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. - If provided, the corresponding readout error mitigation method is used. Defaults to {}. + + * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. + qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. backend (qibo.backends.abstract.Backend, optional): the backend to be used in the execution. If None, it uses the global backend. Defaults to ``None``. @@ -843,12 +847,12 @@ def ICS( Args: circuit (:class:`qibo.models.Circuit`): input circuit. observable (:class:`qibo.hamiltonians.Hamiltonian/:class:`qibo.hamiltonians.SymbolicHamiltonian`): the observable to be measured. - readout (dict, optional): A dictionary that may contain the following keys: - - 'calibration_matrix': numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. - - 'random_ncircuits': int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - - 'ibu_iters': int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. - If ``None`` the 'inverse' method is used. - If provided, the corresponding readout error mitigation method is used. + readout (dict, optional): a dictionary that may contain the following keys: + + * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. + * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. + qubit_map (list, optional): the qubit map. If ``None``, a list of range of circuit's qubits is used. Defaults to ``None``. noise_model (qibo.models.noise.Noise, optional): the noise model to be applied. Defaults to ``None``. nshots (int, optional): the number of shots for the circuit execution. Defaults to :math:`10000`. From caf47847235772b90ea552077feec620c8489458 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 20 Dec 2023 09:28:23 +0100 Subject: [PATCH 57/78] fix example --- doc/source/code-examples/advancedexamples.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst index 4a3fd766f8..53c166ca07 100644 --- a/doc/source/code-examples/advancedexamples.rst +++ b/doc/source/code-examples/advancedexamples.rst @@ -1293,11 +1293,11 @@ calibration matrix and use it modify the final state after the circuit execution .. testcode:: - from qibo.models.error_mitigation import apply_cal_mat_readout_mitigation, get_calibration_matrix + from qibo.models.error_mitigation import apply_cal_mat_readout_mitigation, get_response_matrix nshots = 10000 # compute the calibration matrix - calibration = get_calibration_matrix( + calibration = get_response_matrix( nqubits, backend=backend, noise_model=noise, nshots=nshots ) # execute the circuit From 4af1eabb1a9c57fe592128b183abde3db1f76a8a Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 20 Dec 2023 09:52:49 +0100 Subject: [PATCH 58/78] add example for ICS method --- doc/source/code-examples/advancedexamples.rst | 103 +++++++++++------- 1 file changed, 65 insertions(+), 38 deletions(-) diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst index 53c166ca07..cbea186c4d 100644 --- a/doc/source/code-examples/advancedexamples.rst +++ b/doc/source/code-examples/advancedexamples.rst @@ -1205,23 +1205,23 @@ Let's see how to use them. For starters, let's define a dummy circuit with some hz = 0.5 hx = 0.5 dt = 0.25 - c = Circuit(nqubits, density_matrix=True) - c.add(gates.RZ(q, theta=-2 * hz * dt - np.pi / 2) for q in range(nqubits)) - c.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits)) - c.add(gates.RZ(q, theta=-2 * hx * dt + np.pi) for q in range(nqubits)) - c.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits)) - c.add(gates.RZ(q, theta=-np.pi / 2) for q in range(nqubits)) - c.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2)) - c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(0, nqubits - 1, 2)) - c.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2)) - c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) - c.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2)) - c.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) + circ = Circuit(nqubits, density_matrix=True) + circ.add(gates.RZ(q, theta=-2 * hz * dt - np.pi / 2) for q in range(nqubits)) + circ.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits)) + circ.add(gates.RZ(q, theta=-2 * hx * dt + np.pi) for q in range(nqubits)) + circ.add(gates.RX(q, theta=np.pi / 2) for q in range(nqubits)) + circ.add(gates.RZ(q, theta=-np.pi / 2) for q in range(nqubits)) + circ.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2)) + circ.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(0, nqubits - 1, 2)) + circ.add(gates.CNOT(q, q + 1) for q in range(0, nqubits - 1, 2)) + circ.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) + circ.add(gates.RZ(q + 1, theta=-2 * dt) for q in range(1, nqubits, 2)) + circ.add(gates.CNOT(q, q + 1) for q in range(1, nqubits, 2)) # Include the measurements - c.add(gates.M(*range(nqubits))) + circ.add(gates.M(*range(nqubits))) # visualize the circuit - print(c.draw()) + print(circ.draw()) # q0: ─RZ─RX─RZ─RX─RZ─o────o────────M─ # q1: ─RZ─RX─RZ─RX─RZ─X─RZ─X─o────o─M─ @@ -1254,7 +1254,7 @@ the real quantum hardware, instead, we can use a noise model: .. testcode:: # Noise-free expected value - exact = obs.expectation(backend.execute_circuit(c).state()) + exact = obs.expectation(backend.execute_circuit(circ).state()) print(exact) # 0.9096065335014379 @@ -1272,7 +1272,7 @@ the real quantum hardware, instead, we can use a noise model: ) noise.add(ReadoutError(probabilities=prob), gate=gates.M) # Noisy expected value without mitigation - noisy = obs.expectation(backend.execute_circuit(noise.apply(c)).state()) + noisy = obs.expectation(backend.execute_circuit(noise.apply(circ)).state()) print(noisy) # 0.5647937721701448 @@ -1293,18 +1293,17 @@ calibration matrix and use it modify the final state after the circuit execution .. testcode:: - from qibo.models.error_mitigation import apply_cal_mat_readout_mitigation, get_response_matrix + from qibo.models.error_mitigation import apply_readout_mitigation, get_response_matrix nshots = 10000 - # compute the calibration matrix - calibration = get_response_matrix( + # compute the response matrix + response_matrix = get_response_matrix( nqubits, backend=backend, noise_model=noise, nshots=nshots ) - # execute the circuit - state = backend.execute_circuit(noise.apply(c), nshots=nshots) + # define mitigation options + readout = {"calibration_matrix": response_matrix} # mitigate the readout errors - mit_state = apply_cal_mat_readout_mitigation(state, calibration) - mit_val = mit_state.expectation_from_samples(obs) + mit_val = apply_readout_mitigation(circ, obs, noise, readout=readout) print(mit_val) # 0.5945794816381054 @@ -1319,14 +1318,10 @@ Or use the randomized readout mitigation: from qibo.models.error_mitigation import apply_randomized_readout_mitigation - ncircuits = 10 - result, result_cal = apply_randomized_readout_mitigation( - c, backend=backend, noise_model=noise, nshots=nshots, ncircuits=ncircuits - ) - mit_val = result.expectation_from_samples( - obs - ) / result_cal.expectation_from_samples(obs) - print(mit_val) + # define mitigation options + readout = {"ncircuits": 10} + # mitigate the readout errors + mit_val = apply_readout_mitigation(circ, obs, noise, readout=readout) # 0.5860884499785314 .. testoutput:: @@ -1365,7 +1360,7 @@ For example if we use the five levels ``[0,1,2,3,4]`` : # Mitigated expected value estimate = ZNE( - circuit=c, + circuit=circ, observable=obs, noise_levels=np.arange(5), noise_model=noise, @@ -1388,13 +1383,13 @@ combined with the readout mitigation: # we can either use # the calibration matrix computed earlier - readout = {'calibration_matrix': calibration} + readout = {'calibration_matrix': response_matrix} # or the randomized readout readout = {'ncircuits': 10} # Mitigated expected value estimate = ZNE( - circuit=c, + circuit=circ, observable=obs, backend=backend, noise_levels=np.arange(5), @@ -1424,15 +1419,16 @@ circuit is expected to be decomposed in the set of primitive gates :math:`RX(\fr # Mitigated expected value estimate = CDR( - circuit=c, + circuit=circ, observable=obs, + n_training_samples=10, backend=backend, noise_model=noise, nshots=10000, readout=readout, ) print(estimate) - # 0.9090604794014961 + # 0.8983676333969615 .. testoutput:: :hide: @@ -1441,6 +1437,7 @@ circuit is expected to be decomposed in the set of primitive gates :math:`RX(\fr Again, the mitigated expected value improves over the noisy one and is also slightly better compared to ZNE. + Variable Noise CDR (vnCDR) ^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -1453,8 +1450,9 @@ caveat about the input circuit for CDR is valid here as well. # Mitigated expected value estimate = vnCDR( - circuit=c, + circuit=circ, observable=obs, + n_training_samples=10, backend=backend, noise_levels=np.arange(3), noise_model=noise, @@ -1463,7 +1461,7 @@ caveat about the input circuit for CDR is valid here as well. readout=readout, ) print(estimate) - # 0.9085991439303123 + # 0.8998376314644383 .. testoutput:: :hide: @@ -1474,6 +1472,35 @@ The result is similar to the one obtained by CDR. Usually, one would expect slig however, this can substantially vary depending on the circuit and the observable considered and, therefore, it is hard to tell a priori. + +Importance Clifford Sampling (ICS) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The use of iCS is straightforward, analogous to CDR and vnCDR. + +.. testcode:: + + from qibo.models.error_mitigation import ICS + + # Mitigated expected value + estimate = ICS( + circuit=circ, + observable=obs, + n_training_samples=10, + backend=backend, + noise_model=noise, + nshots=10000, + readout=readout, + ) + print(estimate) + # 0.9183495097398502 + +.. testoutput:: + :hide: + + ... + +Again, the mitigated expected value improves over the noisy one and is also slightly better compared to ZNE. This was just a basic example usage of the three methods, for all the details about them you should check the API-reference page :ref:`Error Mitigation `. .. _timeevol-example: From 2b39afebb5c8ad1800caa10fb89dfba7a9833744 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 20 Dec 2023 10:51:28 +0100 Subject: [PATCH 59/78] adding print to example --- doc/source/code-examples/advancedexamples.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst index cbea186c4d..87f7397c24 100644 --- a/doc/source/code-examples/advancedexamples.rst +++ b/doc/source/code-examples/advancedexamples.rst @@ -1322,6 +1322,7 @@ Or use the randomized readout mitigation: readout = {"ncircuits": 10} # mitigate the readout errors mit_val = apply_readout_mitigation(circ, obs, noise, readout=readout) + print(mit_val) # 0.5860884499785314 .. testoutput:: From 6fc230db703742a0436e46d5deae4525cb5a6806 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 20 Dec 2023 12:10:52 +0100 Subject: [PATCH 60/78] Add reference --- doc/source/api-reference/qibo.rst | 11 ++++++++++- src/qibo/models/error_mitigation.py | 4 +++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index 20cbdfe037..c472331424 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -301,13 +301,22 @@ The calibration matrix :math:`M_{\text{cal}}` lacks stochasticity, resulting in The distributions that arise after applying :math:`M_{\text{cal}}` are quasiprobabilities; the individual elements can be negative surpass 1, provided they sum to 1. It is posible to use Iterative Bayesian Unfolding (IBU) to preserve non-negativity. -See `Srinivasan et al `_ for more details. +See `Nachman et al `_ for more details. .. autofunction:: qibo.models.error_mitigation.get_response_matrix +.. autofunction:: qibo.models.error_mitigation.iterative_bayesian_unfolding + + +.. autofunction:: qibo.models.error_mitigation.apply_cal_mat_readout_mitigation + + +.. autofunction:: qibo.models.error_mitigation.apply_randomized_readout_mitigation + + .. autofunction:: qibo.models.error_mitigation.apply_readout_mitigation diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 6d1bb4cf64..fdf5ffd5d9 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -449,7 +449,9 @@ def iterative_bayesian_unfolding(probabilities, response_matrix, iterations=10): numpy.ndarray: the unfolded probabilities. Reference: - 1. S. Srinivasan, B. Pokharel et al, *Scalable Measurement Error Mitigation via Iterative Bayesian Unfolding*. + 1. B. Nachman, M. Urbanek et al, *Unfolding Quantum Computer Readout Noise*. + `arXiv:1910.01969 [quant-ph] `_. + 2. S. Srinivasan, B. Pokharel et al, *Scalable Measurement Error Mitigation via Iterative Bayesian Unfolding*. `arXiv:2210.12284 [quant-ph] `_. """ unfolded_probabilities = np.ones((len(probabilities), 1)) / len(probabilities) From a7aa8aa206d5fad7d77723f5bf67180a78b53e2e Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 22 Dec 2023 12:53:32 +0400 Subject: [PATCH 61/78] corrections --- src/qibo/transpiler/pipeline.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/qibo/transpiler/pipeline.py b/src/qibo/transpiler/pipeline.py index 289429e40f..dcebab620c 100644 --- a/src/qibo/transpiler/pipeline.py +++ b/src/qibo/transpiler/pipeline.py @@ -151,10 +151,9 @@ def restrict_connectivity_qubits(connectivity: nx.Graph, qubits: list): ) new_connectivity = nx.Graph() new_connectivity.add_nodes_from(qubits) - new_edges = [] - for edge in connectivity.edges: - if edge[0] in qubits and edge[1] in qubits: - new_edges.append(edge) + new_edges = [ + edge for edge in connectivity.edges if (edge[0] in qubits and edge[1] in qubits) + ] new_connectivity.add_edges_from(new_edges) if not nx.is_connected(new_connectivity): raise_error(ConnectivityError, "The new connectivity graph is not connected.") @@ -165,8 +164,7 @@ class Passes: """Define a transpiler pipeline consisting of smaller transpiler steps that are applied sequentially: Args: - passes (list): list of passes to be applied sequentially, - if None default transpiler will be used. + passes (list): list of passes to be applied sequentially. connectivity (nx.Graph): physical qubits connectivity. native_gates (NativeGates): native gates. on_qubits (list): list of physical qubits to be used. If "None" all qubits are used. @@ -183,10 +181,7 @@ def __init__( connectivity = restrict_connectivity_qubits(connectivity, on_qubits) self.connectivity = connectivity self.native_gates = native_gates - if passes is None: - self.passes = self.default() - else: - self.passes = passes + self.passes = passes def default(self): """Return the default transpiler pipeline for the required hardware connectivity.""" From b564fc4408533253bbce99d3e82a3c7feb4dc4e2 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 22 Dec 2023 13:29:35 +0400 Subject: [PATCH 62/78] corrections --- src/qibo/transpiler/router.py | 2 -- tests/test_transpiler_pipeline.py | 57 ++++--------------------------- tests/test_transpiler_placer.py | 8 ++--- tests/test_transpiler_router.py | 25 ++------------ 4 files changed, 14 insertions(+), 78 deletions(-) diff --git a/src/qibo/transpiler/router.py b/src/qibo/transpiler/router.py index 1d19cdaa7e..00e6a86da5 100644 --- a/src/qibo/transpiler/router.py +++ b/src/qibo/transpiler/router.py @@ -25,8 +25,6 @@ def assert_connectivity(connectivity: nx.Graph, circuit: Circuit): if list(connectivity.nodes) != list(range(connectivity.number_of_nodes())): node_mapping = {node: i for i, node in enumerate(connectivity.nodes)} new_connectivity = nx.Graph() - # for new_name in node_mapping.values(): - # new_connectivity.add_node(new_name) new_connectivity.add_edges_from( [(node_mapping[u], node_mapping[v]) for u, v in connectivity.edges] ) diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index cc01a09b61..65e6cf2534 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -53,13 +53,6 @@ def generate_random_circuit(nqubits, ngates, seed=None): return circuit -def small_circuit(): - circuit = Circuit(2) - circuit.add(gates.H(0)) - circuit.add(gates.CZ(0, 1)) - return circuit - - def star_connectivity(): Q = [i for i in range(5)] chip = nx.Graph() @@ -173,13 +166,11 @@ def test_is_satisfied_false_connectivity(): assert not default_transpiler.is_satisfied(circuit) -@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("gates", [5, 20]) @pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) @pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) -@pytest.mark.parametrize( - "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] -) -def test_custom_passes(circ, placer, routing, reps): +def test_custom_passes(placer, routing, gates): + circ = generate_random_circuit(nqubits=5, ngates=gates) custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) if placer == ReverseTraversal: @@ -191,7 +182,7 @@ def test_custom_passes(circ, placer, routing, reps): ) else: custom_passes.append(placer(connectivity=star_connectivity())) - custom_passes.append(ShortestPaths(connectivity=star_connectivity())) + custom_passes.append(routing(connectivity=star_connectivity())) custom_passes.append(Unroller(native_gates=NativeGates.default())) custom_pipeline = Passes( custom_passes, @@ -210,11 +201,11 @@ def test_custom_passes(circ, placer, routing, reps): ) -@pytest.mark.parametrize("reps", [range(3)]) +@pytest.mark.parametrize("gates", [5, 20]) @pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) @pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) -def test_custom_passes_restict(reps, placer, routing): - circ = generate_random_circuit(nqubits=3, ngates=20) +def test_custom_passes_restict(gates, placer, routing): + circ = generate_random_circuit(nqubits=3, ngates=gates) custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) if placer == ReverseTraversal: @@ -236,8 +227,6 @@ def test_custom_passes_restict(reps, placer, routing): ) transpiled_circ, final_layout = custom_pipeline(circ) initial_layout = custom_pipeline.get_initial_layout() - print(initial_layout) - print(final_layout) assert_transpiling( original_circuit=circ, transpiled_circuit=transpiled_circ, @@ -248,38 +237,6 @@ def test_custom_passes_restict(reps, placer, routing): ) -@pytest.mark.parametrize( - "circ", [generate_random_circuit(nqubits=5, ngates=20), small_circuit()] -) -def test_custom_passes_reverse(circ): - custom_passes = [] - custom_passes.append(Preprocessing(connectivity=star_connectivity())) - custom_passes.append( - ReverseTraversal( - connectivity=star_connectivity(), - routing_algorithm=ShortestPaths(connectivity=star_connectivity()), - depth=20, - ) - ) - custom_passes.append(ShortestPaths(connectivity=star_connectivity())) - custom_passes.append(Unroller(native_gates=NativeGates.default())) - custom_pipeline = Passes( - custom_passes, - connectivity=star_connectivity(), - native_gates=NativeGates.default(), - ) - transpiled_circ, final_layout = custom_pipeline(circ) - initial_layout = custom_pipeline.get_initial_layout() - assert_transpiling( - original_circuit=circ, - transpiled_circuit=transpiled_circ, - connectivity=star_connectivity(), - initial_layout=initial_layout, - final_layout=final_layout, - native_gates=NativeGates.default(), - ) - - def test_custom_passes_multiple_placer(): custom_passes = [] custom_passes.append(Random(connectivity=star_connectivity())) diff --git a/tests/test_transpiler_placer.py b/tests/test_transpiler_placer.py index 09d4a80d2a..434037e7a4 100644 --- a/tests/test_transpiler_placer.py +++ b/tests/test_transpiler_placer.py @@ -75,8 +75,8 @@ def test_mapping_consistency_error(layout): def test_mapping_consistency_restricted(): layout = {"q0": 0, "q2": 1} connectivity = star_connectivity() - restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) - assert_mapping_consistency(layout, restricted_connecyivity) + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) + assert_mapping_consistency(layout, restricted_connectivity) @pytest.mark.parametrize( @@ -88,9 +88,9 @@ def test_mapping_consistency_restricted(): ) def test_mapping_consistency_restricted_error(layout): connectivity = star_connectivity() - restricted_connecyivity = restrict_connectivity_qubits(connectivity, [0, 2]) + restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2]) with pytest.raises(PlacementError): - assert_mapping_consistency(layout, restricted_connecyivity) + assert_mapping_consistency(layout, restricted_connectivity) def test_gates_qubits_pairs(): diff --git a/tests/test_transpiler_router.py b/tests/test_transpiler_router.py index 0a2d8422f3..716f0f02bd 100644 --- a/tests/test_transpiler_router.py +++ b/tests/test_transpiler_router.py @@ -312,7 +312,8 @@ def test_sabre_intermediate_measurements(): assert routed_circ.queue[3].result is measurement.result -def test_sabre_restrict_qubits(): +@pytest.mark.parametrize("router_algorithm", [Sabre, ShortestPaths]) +def test_restrict_qubits(router_algorithm): circ = Circuit(3) circ.add(gates.CZ(0, 1)) circ.add(gates.CZ(0, 2)) @@ -320,27 +321,7 @@ def test_sabre_restrict_qubits(): initial_layout = {"q0": 0, "q2": 2, "q3": 1} connectivity = star_connectivity() restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) - router = Sabre(connectivity=restricted_connectivity) - routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) - assert_circuit_equivalence( - original_circuit=circ, - transpiled_circuit=routed_circ, - final_map=final_layout, - initial_map=initial_layout, - ) - assert_connectivity(restricted_connectivity, routed_circ) - assert_placement(routed_circ, final_layout, connectivity=restricted_connectivity) - - -def test_shortest_paths_restrict_qubits(): - circ = Circuit(3) - circ.add(gates.CZ(0, 1)) - circ.add(gates.CZ(0, 2)) - circ.add(gates.CZ(2, 1)) - initial_layout = {"q0": 0, "q2": 2, "q3": 1} - connectivity = star_connectivity() - restricted_connectivity = restrict_connectivity_qubits(connectivity, [0, 2, 3]) - router = ShortestPaths(connectivity=restricted_connectivity) + router = router_algorithm(connectivity=restricted_connectivity) routed_circ, final_layout = router(circuit=circ, initial_layout=initial_layout) assert_circuit_equivalence( original_circuit=circ, From 015a60c8c82b9fcf77259ceaff356b4e07e383d1 Mon Sep 17 00:00:00 2001 From: simone bordoni Date: Fri, 22 Dec 2023 14:35:19 +0400 Subject: [PATCH 63/78] 100 coverage --- tests/test_transpiler_pipeline.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_transpiler_pipeline.py b/tests/test_transpiler_pipeline.py index 65e6cf2534..ce8afee66d 100644 --- a/tests/test_transpiler_pipeline.py +++ b/tests/test_transpiler_pipeline.py @@ -142,9 +142,10 @@ def test_error_connectivity(): default_transpiler = Passes(passes=None, connectivity=None) -def test_is_satisfied(): +@pytest.mark.parametrize("qubits", [3, 5]) +def test_is_satisfied(qubits): default_transpiler = Passes(passes=None, connectivity=star_connectivity()) - circuit = Circuit(5) + circuit = Circuit(qubits) circuit.add(gates.CZ(0, 2)) circuit.add(gates.Z(0)) assert default_transpiler.is_satisfied(circuit) @@ -166,11 +167,12 @@ def test_is_satisfied_false_connectivity(): assert not default_transpiler.is_satisfied(circuit) +@pytest.mark.parametrize("qubits", [2, 5]) @pytest.mark.parametrize("gates", [5, 20]) @pytest.mark.parametrize("placer", [Random, Trivial, ReverseTraversal]) @pytest.mark.parametrize("routing", [ShortestPaths, Sabre]) -def test_custom_passes(placer, routing, gates): - circ = generate_random_circuit(nqubits=5, ngates=gates) +def test_custom_passes(placer, routing, gates, qubits): + circ = generate_random_circuit(nqubits=qubits, ngates=gates) custom_passes = [] custom_passes.append(Preprocessing(connectivity=star_connectivity())) if placer == ReverseTraversal: From b89e4f5a265ba8d8d9beb49c77200c5144b844fe Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 4 Jan 2024 09:30:24 +0400 Subject: [PATCH 64/78] Andrea's suggestion --- src/qibo/models/encodings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/encodings.py b/src/qibo/models/encodings.py index d87ce0a3db..3bcad17607 100644 --- a/src/qibo/models/encodings.py +++ b/src/qibo/models/encodings.py @@ -203,7 +203,7 @@ def _generate_rbs_pairs(nqubits: int, architecture: str): if architecture == "tree": pairs_rbs = [[(0, int(nqubits / 2))]] - indexes = list(np.array(pairs_rbs).flatten()) + indexes = list(pairs_rbs[0][0]) for depth in range(2, int(math.log2(nqubits)) + 1): pairs_rbs_per_depth = [ [(index, index + int(nqubits / 2**depth)) for index in indexes] From 93024721011020752d34d0e4a8e34ff7e58abe91 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Sun, 7 Jan 2024 03:11:20 +0100 Subject: [PATCH 65/78] matteo's suggestions --- doc/source/api-reference/qibo.rst | 8 +-- doc/source/code-examples/advancedexamples.rst | 14 ++--- src/qibo/models/error_mitigation.py | 42 +++++++------- tests/test_models_error_mitigation.py | 58 ++++++++++--------- 4 files changed, 62 insertions(+), 60 deletions(-) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index dc6122373f..3725e143b9 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -288,7 +288,7 @@ In the presence of readout errors, we will measure for each state :math:`i` some frequencies :math:`F_i^{noisy}` different from the ideal ones :math:`F_i^{ideal}=\delta_{i,j}`. -The effect of the error is modeled by the matrix composed of the noisy frequencies as +The effect of the error is modeled by the response matrix composed of the noisy frequencies as columns :math:`M=\big(F_0^{noisy},...,F_{n-1}^{noisy}\big)`. We have indeed that: .. math:: @@ -311,13 +311,13 @@ See `Nachman et al `_ for more details. .. autofunction:: qibo.models.error_mitigation.iterative_bayesian_unfolding -.. autofunction:: qibo.models.error_mitigation.apply_cal_mat_readout_mitigation +.. autofunction:: qibo.models.error_mitigation.apply_resp_mat_readout_mitigation .. autofunction:: qibo.models.error_mitigation.apply_randomized_readout_mitigation -.. autofunction:: qibo.models.error_mitigation.apply_readout_mitigation +.. autofunction:: qibo.models.error_mitigation.apply_problem_with_readout_conf Randomized readout mitigation @@ -434,7 +434,7 @@ See `Sopena et al `_ for all the details. Importance Clifford Sampling (ICS) -"""""""""""""""""""""""""""""" +"""""""""""""""""""""""""""""""""" In the Importance Clifford Sampling (ICS) method, a set of :math:`n` circuits :math:`S_n=\{C_i\}_{i=1,..,n}` that stabilizes a given Pauli observable is generated starting from the original circuit diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst index 87f7397c24..73176acd65 100644 --- a/doc/source/code-examples/advancedexamples.rst +++ b/doc/source/code-examples/advancedexamples.rst @@ -1289,11 +1289,11 @@ Now let's check that error mitigation produces better estimates of the exact exp Readout Mitigation ^^^^^^^^^^^^^^^^^^ Firstly, let's try to mitigate the readout errors. To do this, we can either compute the -calibration matrix and use it modify the final state after the circuit execution: +response matrix and use it modify the final state after the circuit execution: .. testcode:: - from qibo.models.error_mitigation import apply_readout_mitigation, get_response_matrix + from qibo.models.error_mitigation import apply_problem_with_readout_conf, get_response_matrix nshots = 10000 # compute the response matrix @@ -1301,9 +1301,9 @@ calibration matrix and use it modify the final state after the circuit execution nqubits, backend=backend, noise_model=noise, nshots=nshots ) # define mitigation options - readout = {"calibration_matrix": response_matrix} + readout = {"response_matrix": response_matrix} # mitigate the readout errors - mit_val = apply_readout_mitigation(circ, obs, noise, readout=readout) + mit_val = apply_problem_with_readout_conf(circ, obs, noise, readout=readout) print(mit_val) # 0.5945794816381054 @@ -1321,7 +1321,7 @@ Or use the randomized readout mitigation: # define mitigation options readout = {"ncircuits": 10} # mitigate the readout errors - mit_val = apply_readout_mitigation(circ, obs, noise, readout=readout) + mit_val = apply_problem_with_readout_conf(circ, obs, noise, readout=readout) print(mit_val) # 0.5860884499785314 @@ -1383,8 +1383,8 @@ combined with the readout mitigation: .. testcode:: # we can either use - # the calibration matrix computed earlier - readout = {'calibration_matrix': response_matrix} + # the response matrix computed earlier + readout = {'response_matrix': response_matrix} # or the randomized readout readout = {'ncircuits': 10} diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index fdf5ffd5d9..5f5f1c44ca 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -133,7 +133,7 @@ def ZNE( readout (dict, optional): a dictionary that may contain the following keys: * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation. * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. @@ -156,7 +156,7 @@ def ZNE( noisy_circuit = get_noisy_circuit( circuit, num_insertions, insertion_gate=insertion_gate ) - val = apply_readout_mitigation( + val = apply_problem_with_readout_conf( noisy_circuit, observable, noise_model, @@ -283,7 +283,7 @@ def CDR( readout (dict, optional): a dictionary that may contain the following keys: * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation. * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. @@ -312,14 +312,14 @@ def CDR( for circ in training_circuits: val = circ(nshots=nshots).expectation_from_samples(observable) train_val["noise-free"].append(val) - val = apply_readout_mitigation( + val = apply_problem_with_readout_conf( circ, observable, noise_model, nshots, readout, qubit_map, backend=backend ) train_val["noisy"].append(val) optimal_params = curve_fit(model, train_val["noisy"], train_val["noise-free"])[0] - val = apply_readout_mitigation( + val = apply_problem_with_readout_conf( circuit, observable, noise_model, nshots, readout, qubit_map, backend=backend ) mit_val = model(val, *optimal_params) @@ -366,7 +366,7 @@ def vnCDR( readout (dict, optional): a dictionary that may contain the following keys: * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation. * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. @@ -398,7 +398,7 @@ def vnCDR( train_val["noise-free"].append(val) for level in noise_levels: noisy_c = get_noisy_circuit(circ, level, insertion_gate=insertion_gate) - val = apply_readout_mitigation( + val = apply_problem_with_readout_conf( noisy_c, observable, noise_model, @@ -417,7 +417,7 @@ def vnCDR( val = [] for level in noise_levels: noisy_c = get_noisy_circuit(circuit, level, insertion_gate=insertion_gate) - expval = apply_readout_mitigation( + expval = apply_problem_with_readout_conf( noisy_c, observable, noise_model, @@ -468,7 +468,7 @@ def iterative_bayesian_unfolding(probabilities, response_matrix, iterations=10): def get_response_matrix( nqubits, qubit_map=None, noise_model=None, nshots: int = 10000, backend=None ): - """Computes the calibration matrix for readout mitigation. + """Computes the response matrix for readout mitigation. Args: nqubits (int): Total number of qubits. @@ -482,7 +482,7 @@ def get_response_matrix( Defaults to ``None``. Returns: - numpy.ndarray : The computed (`nqubits`, `nqubits`) calibration matrix for + numpy.ndarray : The computed (`nqubits`, `nqubits`) response matrix for readout mitigation. """ from qibo import Circuit # pylint: disable=import-outside-toplevel @@ -515,14 +515,14 @@ def get_response_matrix( return response_matrix -def apply_cal_mat_readout_mitigation(state, response_matrix, iterations=None): +def apply_resp_mat_readout_mitigation(state, response_matrix, iterations=None): """ - Applies readout error mitigation to the given state using the provided calibration matrix. + Applies readout error mitigation to the given state using the provided response matrix. Args: state (:class:`qibo.measurements.CircuitResult`): the input state to be updated. This state should contain the frequencies that need to be mitigated. - response_matrix (numpy.ndarray): the calibration matrix for readout mitigation. + response_matrix (numpy.ndarray): the response matrix for readout mitigation. iterations (int, optional): the number of iterations to use for the Iterative Bayesian Unfolding method. If ``None`` the 'inverse' method is used. Defaults to ``None``. @@ -639,7 +639,7 @@ def apply_randomized_readout_mitigation( return results -def apply_readout_mitigation( +def apply_problem_with_readout_conf( circuit, observable, noise_model=None, @@ -659,7 +659,7 @@ def apply_readout_mitigation( readout (dict, optional): a dictionary that may contain the following keys: * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation. * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. qubit_map (list, optional): the qubit map. If None, a list of range of circuit's qubits is used. Defaults to ``None``. @@ -681,10 +681,10 @@ def apply_readout_mitigation( circuit, qubit_map, noise_model, nshots, backend=backend ) - if "calibration_matrix" in readout: - circuit_result = apply_cal_mat_readout_mitigation( + if "response_matrix" in readout: + circuit_result = apply_resp_mat_readout_mitigation( circuit_result, - readout["calibration_matrix"], + readout["response_matrix"], readout.get("ibu_iters", None), ) @@ -852,7 +852,7 @@ def ICS( readout (dict, optional): a dictionary that may contain the following keys: * ncircuits: int, specifies the number of random circuits to use for the randomized method of readout error mitigation. - * calibration_matrix: numpy.ndarray, used for applying a pre-computed calibration matrix for readout error mitigation. + * response_matrix: numpy.ndarray, used for applying a pre-computed response matrix for readout error mitigation. * ibu_iters: int, specifies the number of iterations for the iterative Bayesian unfolding method of readout error mitigation. If provided, the corresponding readout error mitigation method is used. Defaults to {}. qubit_map (list, optional): the qubit map. If ``None``, a list of range of circuit's qubits is used. Defaults to ``None``. @@ -894,7 +894,7 @@ def ICS( circuit_result = training_circuit(nshots=nshots) expectation = observable.expectation_from_samples(circuit_result.frequencies()) - noisy_expectation = apply_readout_mitigation( + noisy_expectation = apply_problem_with_readout_conf( training_circuit, observable, noise_model, @@ -911,7 +911,7 @@ def ICS( dep_param = np.mean(lambda_list) dep_param_std = np.std(lambda_list) - noisy_expectation = apply_readout_mitigation( + noisy_expectation = apply_problem_with_readout_conf( circuit, observable, noise_model, nshots, readout, qubit_map, backend=backend ) diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index d8c0066793..22bccd5d48 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -8,7 +8,7 @@ CDR, ICS, ZNE, - apply_readout_mitigation, + apply_problem_with_readout_conf, get_response_matrix, sample_clifford_training_circuit, sample_training_circuit_cdr, @@ -19,11 +19,11 @@ from qibo.symbols import Z -def get_noise_model(error, gate, cal_matrix=[False, None]): +def get_noise_model(error, gate, resp_matrix=[False, None]): noise = NoiseModel() noise.add(error, gate) - if cal_matrix[0]: - noise.add(ReadoutError(probabilities=cal_matrix[1]), gate=gates.M) + if resp_matrix[0]: + noise.add(ReadoutError(probabilities=resp_matrix[1]), gate=gates.M) return noise @@ -53,11 +53,11 @@ def get_circuit(nqubits, nmeas=None): backend = construct_backend("numpy") -# # Generate random calibration matrices -cal_matrix_1q = random_stochastic_matrix( +# # Generate random response matrices +resp_matrix_1q = random_stochastic_matrix( 2, diagonally_dominant=True, seed=2, backend=backend ) -cal_matrix_2q = random_stochastic_matrix( +resp_matrix_2q = random_stochastic_matrix( 4, diagonally_dominant=True, seed=2, backend=backend ) @@ -70,7 +70,7 @@ def get_circuit(nqubits, nmeas=None): 3, get_noise_model(DepolarizingError(0.1), gates.CNOT), "CNOT", - {"calibration_matrix": cal_matrix_2q, "ibu_iters": None}, + {"response_matrix": resp_matrix_2q, "ibu_iters": None}, ), ( 3, @@ -83,13 +83,13 @@ def get_circuit(nqubits, nmeas=None): 1, get_noise_model(DepolarizingError(0.3), gates.RX), "RX", - {"calibration_matrix": cal_matrix_1q, "ibu_iters": None}, + {"response_matrix": resp_matrix_1q, "ibu_iters": None}, ), ( 1, get_noise_model(DepolarizingError(0.3), gates.RX), "RX", - {"calibration_matrix": cal_matrix_1q, "ibu_iters": 10}, + {"response_matrix": resp_matrix_1q, "ibu_iters": 10}, ), (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", {"ncircuits": 2}), ], @@ -143,15 +143,15 @@ def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): [ (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), - {"calibration_matrix": cal_matrix_2q, "ibu_iters": None}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), + {"response_matrix": resp_matrix_2q, "ibu_iters": None}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), - {"calibration_matrix": cal_matrix_2q, "ibu_iters": 10}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), + {"response_matrix": resp_matrix_2q, "ibu_iters": 10}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), {"ncircuits": 2}, ), ], @@ -231,13 +231,13 @@ def test_sample_training_circuit(nqubits): [ ( 1, - get_noise_model(DepolarizingError(0.1), gates.RX, [True, cal_matrix_1q]), + get_noise_model(DepolarizingError(0.1), gates.RX, [True, resp_matrix_1q]), "RX", - {"calibration_matrix": cal_matrix_1q, "ibu_iters": 10}, + {"response_matrix": resp_matrix_1q, "ibu_iters": 10}, ), ( 1, - get_noise_model(DepolarizingError(0.1), gates.RX, [True, cal_matrix_1q]), + get_noise_model(DepolarizingError(0.1), gates.RX, [True, resp_matrix_1q]), "RX", {"ncircuits": 2}, ), @@ -283,7 +283,7 @@ def test_vncdr(backend, nqubits, noise, full_output, insertion_gate, readout): @pytest.mark.parametrize("nqubits,nmeas", [(3, 2)]) -@pytest.mark.parametrize("method", ["cal_matrix", "randomized"]) +@pytest.mark.parametrize("method", ["response_matrix", "randomized"]) @pytest.mark.parametrize("ibu_iters", [None, 10]) def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): if backend.name == "tensorflow": @@ -297,11 +297,11 @@ def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): p = random_stochastic_matrix(2**nmeas, diagonally_dominant=True, seed=5) noise = NoiseModel() noise.add(ReadoutError(probabilities=p), gate=gates.M) - if method == "cal_matrix": - calibration = get_response_matrix( + if method == "response_matrix": + response = get_response_matrix( nmeas, None, noise, nshots=nshots, backend=backend ) - readout = {"calibration_matrix": calibration, "ibu_iters": ibu_iters} + readout = {"response_matrix": response, "ibu_iters": ibu_iters} elif method == "randomized": readout = {"ncircuits": 10} # Define the observable @@ -315,7 +315,9 @@ def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): state = backend.execute_circuit(noise.apply(c), nshots=nshots) noisy_val = state.expectation_from_samples(obs) - mit_val = apply_readout_mitigation(c, obs, noise, nshots, readout, backend=backend) + mit_val = apply_problem_with_readout_conf( + c, obs, noise, nshots, readout, backend=backend + ) assert np.abs(true_val - mit_val) <= np.abs(true_val - noisy_val) @@ -327,15 +329,15 @@ def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): [ (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), - {"calibration_matrix": cal_matrix_2q, "ibu_iters": None}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), + {"response_matrix": resp_matrix_2q, "ibu_iters": None}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), - {"calibration_matrix": cal_matrix_2q, "ibu_iters": 10}, + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), + {"response_matrix": resp_matrix_2q, "ibu_iters": 10}, ), ( - get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, cal_matrix_2q]), + get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), {"ncircuits": 2}, ), ], From 94bbd4f1c35cb3792b9c801419699f5b2d06b613 Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Wed, 10 Jan 2024 11:18:35 +0100 Subject: [PATCH 66/78] code review --- doc/source/api-reference/qibo.rst | 6 +- doc/source/code-examples/advancedexamples.rst | 6 +- src/qibo/gates/special.py | 3 +- src/qibo/models/circuit.py | 2 +- src/qibo/models/error_mitigation.py | 66 +++++++++---------- tests/test_models_error_mitigation.py | 4 +- 6 files changed, 38 insertions(+), 49 deletions(-) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index e63d2ec397..bbeeda603c 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -317,7 +317,7 @@ See `Nachman et al `_ for more details. .. autofunction:: qibo.models.error_mitigation.apply_randomized_readout_mitigation -.. autofunction:: qibo.models.error_mitigation.apply_problem_with_readout_conf +.. autofunction:: qibo.models.error_mitigation.get_expectation_val_with_readout_mitigation Randomized readout mitigation @@ -1465,10 +1465,6 @@ passing a symplectic matrix to the constructor. symplectic_matrix = backend.zero_state(nqubits=3) clifford = Clifford(symplectic_matrix, engine=NumpyBackend()) - # The initialization above is equivalent to the initialization below - circuit = Circuit(nqubits=3) - clifford = Clifford(circuit, engine=NumpyBackend()) - The generators of the stabilizers can be extracted with the :meth:`qibo.quantum_info.clifford.Clifford.generators` method, or the complete set of :math:`d = 2^{n}` stabilizers operators can be extracted through the diff --git a/doc/source/code-examples/advancedexamples.rst b/doc/source/code-examples/advancedexamples.rst index 7325319de8..ca85dd2e25 100644 --- a/doc/source/code-examples/advancedexamples.rst +++ b/doc/source/code-examples/advancedexamples.rst @@ -1291,7 +1291,7 @@ response matrix and use it modify the final state after the circuit execution: .. testcode:: - from qibo.models.error_mitigation import apply_problem_with_readout_conf, get_response_matrix + from qibo.models.error_mitigation import get_expectation_val_with_readout_mitigation, get_response_matrix nshots = 10000 # compute the response matrix @@ -1301,7 +1301,7 @@ response matrix and use it modify the final state after the circuit execution: # define mitigation options readout = {"response_matrix": response_matrix} # mitigate the readout errors - mit_val = apply_problem_with_readout_conf(circ, obs, noise, readout=readout) + mit_val = get_expectation_val_with_readout_mitigation(circ, obs, noise, readout=readout) print(mit_val) # 0.5945794816381054 @@ -1319,7 +1319,7 @@ Or use the randomized readout mitigation: # define mitigation options readout = {"ncircuits": 10} # mitigate the readout errors - mit_val = apply_problem_with_readout_conf(circ, obs, noise, readout=readout) + mit_val = get_expectation_val_with_readout_mitigation(circ, obs, noise, readout=readout) print(mit_val) # 0.5860884499785314 diff --git a/src/qibo/gates/special.py b/src/qibo/gates/special.py index 4f1cb16cd5..d5027bb5ea 100644 --- a/src/qibo/gates/special.py +++ b/src/qibo/gates/special.py @@ -1,6 +1,5 @@ from qibo.backends import GlobalBackend from qibo.gates.abstract import SpecialGate -from qibo.gates.gates import I from qibo.gates.measurements import M @@ -55,7 +54,7 @@ def __init__(self, *q): def from_gate(cls, gate): fgate = cls(*gate.qubits) fgate.append(gate) - if isinstance(gate, (M, SpecialGate, I)): + if isinstance(gate, (M, SpecialGate)): # special gates do not participate in fusion fgate.marked = True return fgate diff --git a/src/qibo/models/circuit.py b/src/qibo/models/circuit.py index e52398af06..419f5635d2 100644 --- a/src/qibo/models/circuit.py +++ b/src/qibo/models/circuit.py @@ -81,7 +81,7 @@ def from_fused(self): queue.append(gate.gates[0]) else: queue.append(gate) - elif isinstance(gate.gates[0], (gates.SpecialGate, gates.M, gates.I)): + elif isinstance(gate.gates[0], (gates.SpecialGate, gates.M)): # special gates are marked by default so we need # to add them manually queue.append(gate.gates[0]) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 5f5f1c44ca..06c799262b 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -156,7 +156,7 @@ def ZNE( noisy_circuit = get_noisy_circuit( circuit, num_insertions, insertion_gate=insertion_gate ) - val = apply_problem_with_readout_conf( + val = get_expectation_val_with_readout_mitigation( noisy_circuit, observable, noise_model, @@ -312,14 +312,14 @@ def CDR( for circ in training_circuits: val = circ(nshots=nshots).expectation_from_samples(observable) train_val["noise-free"].append(val) - val = apply_problem_with_readout_conf( + val = get_expectation_val_with_readout_mitigation( circ, observable, noise_model, nshots, readout, qubit_map, backend=backend ) train_val["noisy"].append(val) optimal_params = curve_fit(model, train_val["noisy"], train_val["noise-free"])[0] - val = apply_problem_with_readout_conf( + val = get_expectation_val_with_readout_mitigation( circuit, observable, noise_model, nshots, readout, qubit_map, backend=backend ) mit_val = model(val, *optimal_params) @@ -398,7 +398,7 @@ def vnCDR( train_val["noise-free"].append(val) for level in noise_levels: noisy_c = get_noisy_circuit(circ, level, insertion_gate=insertion_gate) - val = apply_problem_with_readout_conf( + val = get_expectation_val_with_readout_mitigation( noisy_c, observable, noise_model, @@ -417,7 +417,7 @@ def vnCDR( val = [] for level in noise_levels: noisy_c = get_noisy_circuit(circuit, level, insertion_gate=insertion_gate) - expval = apply_problem_with_readout_conf( + expval = get_expectation_val_with_readout_mitigation( noisy_c, observable, noise_model, @@ -501,7 +501,7 @@ def get_response_matrix( circuit.add(gates.X(qubit)) circuit.add(gates.M(*range(nqubits))) - circuit_result = _circuit_conf( + circuit_result = _execute_circuit( circuit, qubit_map, noise_model, nshots, backend=backend ) @@ -537,8 +537,7 @@ def apply_resp_mat_readout_mitigation(state, response_matrix, iterations=None): if iterations is None: calibration_matrix = np.linalg.inv(response_matrix) - for i, value in enumerate(calibration_matrix @ frequencies): - state._frequencies[i] = float(value) + mitigated_frequencies = calibration_matrix @ frequencies else: mitigated_probabilities = iterative_bayesian_unfolding( frequencies / np.sum(frequencies), response_matrix, iterations @@ -550,8 +549,8 @@ def apply_resp_mat_readout_mitigation(state, response_matrix, iterations=None): mitigated_frequencies / np.sum(mitigated_frequencies) ) * np.sum(frequencies) - for i, value in enumerate(mitigated_frequencies): - state._frequencies[i] = float(value) + for i, value in enumerate(mitigated_frequencies): + state._frequencies[i] = float(value) return state @@ -609,10 +608,6 @@ def apply_randomized_readout_mitigation( for j, gate in enumerate(x_gate): if isinstance(gate, gates.X) and gate.qubits[0] in meas_qubits: error_map[gate.qubits[0]] = 1 - # if gate.qubits[0] in meas_qubits: - # error_map[gate.qubits[0]] = 1 - # else: - # x_gate.queue[j] = gates.I(gate.qubits[0]) circuits = [circuit_c, cal_circuit] results = [] @@ -621,7 +616,7 @@ def apply_randomized_readout_mitigation( circ.add(x_gate) circ.add(gates.M(*meas_qubits)) - result = _circuit_conf( + result = _execute_circuit( circ, qubit_map, noise_model, nshots_r, backend=backend ) result._samples = result.apply_bitflips(error_map) @@ -639,7 +634,7 @@ def apply_randomized_readout_mitigation( return results -def apply_problem_with_readout_conf( +def get_expectation_val_with_readout_mitigation( circuit, observable, noise_model=None, @@ -677,16 +672,15 @@ def apply_problem_with_readout_conf( circuit, noise_model, nshots, readout["ncircuits"], backend ) else: - circuit_result = _circuit_conf( + circuit_result = _execute_circuit( circuit, qubit_map, noise_model, nshots, backend=backend ) - - if "response_matrix" in readout: - circuit_result = apply_resp_mat_readout_mitigation( - circuit_result, - readout["response_matrix"], - readout.get("ibu_iters", None), - ) + if "response_matrix" in readout: + circuit_result = apply_resp_mat_readout_mitigation( + circuit_result, + readout["response_matrix"], + readout.get("ibu_iters", None), + ) exp_val = circuit_result.expectation_from_samples(observable) @@ -718,13 +712,13 @@ def sample_clifford_training_circuit( if backend is None: # pragma: no cover backend = GlobalBackend() - non_clifford_gates = [ - (i, gate) + non_clifford_gates_indices = [ + i for i, gate in enumerate(circuit.queue) if not gate.clifford and not isinstance(gate, gates.M) ] - if not non_clifford_gates: + if not non_clifford_gates_indices: raise_error(ValueError, "No non-Clifford gate found, no circuit sampled.") sampled_circuit = circuit.__class__(**circuit.init_kwargs) @@ -740,7 +734,7 @@ def sample_clifford_training_circuit( sampled_circuit.add(gate_rand) sampled_circuit.add(gate) else: - if i in [index for index, _ in non_clifford_gates]: + if i in non_clifford_gates_indices: gate = gates.Unitary( random_clifford(1, backend=backend, return_circuit=False), gate.qubits[0], @@ -894,7 +888,7 @@ def ICS( circuit_result = training_circuit(nshots=nshots) expectation = observable.expectation_from_samples(circuit_result.frequencies()) - noisy_expectation = apply_problem_with_readout_conf( + noisy_expectation = get_expectation_val_with_readout_mitigation( training_circuit, observable, noise_model, @@ -911,20 +905,20 @@ def ICS( dep_param = np.mean(lambda_list) dep_param_std = np.std(lambda_list) - noisy_expectation = apply_problem_with_readout_conf( + noisy_expectation = get_expectation_val_with_readout_mitigation( circuit, observable, noise_model, nshots, readout, qubit_map, backend=backend ) + one_dep_squared = (1 - dep_param) ** 2 + dep_std_squared = dep_param_std**2 mitigated_expectation = ( - (1 - dep_param) - * noisy_expectation - / ((1 - dep_param) ** 2 + dep_param_std**2) + (1 - dep_param) * noisy_expectation / (one_dep_squared + dep_std_squared) ) mitigated_expectation_std = ( dep_param_std * abs(noisy_expectation) - * abs((1 - dep_param) ** 2 - dep_param_std**2) - / ((1 - dep_param) ** 2 + dep_param_std**2) ** 2 + * abs((1 - dep_param) ** 2 - dep_std_squared) + / (one_dep_squared + dep_std_squared) ** 2 ) if full_output: @@ -940,7 +934,7 @@ def ICS( return mitigated_expectation -def _circuit_conf(circuit, qubit_map, noise_model=None, nshots=10000, backend=None): +def _execute_circuit(circuit, qubit_map, noise_model=None, nshots=10000, backend=None): """ Helper function to execute the given circuit with the specified parameters. diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 22bccd5d48..c1132c5a5a 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -8,7 +8,7 @@ CDR, ICS, ZNE, - apply_problem_with_readout_conf, + get_expectation_val_with_readout_mitigation, get_response_matrix, sample_clifford_training_circuit, sample_training_circuit_cdr, @@ -315,7 +315,7 @@ def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): state = backend.execute_circuit(noise.apply(c), nshots=nshots) noisy_val = state.expectation_from_samples(obs) - mit_val = apply_problem_with_readout_conf( + mit_val = get_expectation_val_with_readout_mitigation( c, obs, noise, nshots, readout, backend=backend ) From 0c3830a89408112ade7c38026b7b51cb87219f77 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jan 2024 20:38:37 +0000 Subject: [PATCH 67/78] Bump jinja2 from 3.1.2 to 3.1.3 Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.2 to 3.1.3. - [Release notes](https://github.com/pallets/jinja/releases) - [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst) - [Commits](https://github.com/pallets/jinja/compare/3.1.2...3.1.3) --- updated-dependencies: - dependency-name: jinja2 dependency-type: indirect ... Signed-off-by: dependabot[bot] --- poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index f65d7e98ea..dff7472a94 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1810,13 +1810,13 @@ testing = ["Django", "attrs", "colorama", "docopt", "pytest (<7.0.0)"] [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.3" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, ] [package.dependencies] From d851a8efcbb6786f5c0410a94d755e857ef6add0 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 11 Jan 2024 23:29:21 +0000 Subject: [PATCH 68/78] Update src/qibo/models/error_mitigation.py Co-authored-by: Matteo Robbiati <62071516+MatteoRobbiati@users.noreply.github.com> --- src/qibo/models/error_mitigation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 06c799262b..7139377432 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -662,7 +662,7 @@ def get_expectation_val_with_readout_mitigation( If None, it uses the global backend. Defaults to ``None``. Returns: - float: The mitigated expectation value of the observable. + float: the mitigated expectation value of the observable. """ if backend is None: # pragma: no cover backend = GlobalBackend() From 7746326e595c61283ed3e3c651648f85dfb1cf7d Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 11 Jan 2024 23:29:33 +0000 Subject: [PATCH 69/78] Update src/qibo/models/error_mitigation.py Co-authored-by: Matteo Robbiati <62071516+MatteoRobbiati@users.noreply.github.com> --- src/qibo/models/error_mitigation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 7139377432..0181a0e7dc 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -703,7 +703,7 @@ def sample_clifford_training_circuit( Defaults to ``None``. Returns: - :class:`qibo.models.Circuit`: The sampled circuit. + :class:`qibo.models.Circuit`: the sampled circuit. """ from qibo.quantum_info import ( # pylint: disable=import-outside-toplevel random_clifford, From 1002a694ea25d2151f2b5988782671c878a936f2 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 11 Jan 2024 23:29:52 +0000 Subject: [PATCH 70/78] Update src/qibo/models/error_mitigation.py Co-authored-by: Matteo Robbiati <62071516+MatteoRobbiati@users.noreply.github.com> --- src/qibo/models/error_mitigation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 0181a0e7dc..8f9eefd0c3 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -527,7 +527,7 @@ def apply_resp_mat_readout_mitigation(state, response_matrix, iterations=None): If ``None`` the 'inverse' method is used. Defaults to ``None``. Returns: - :class:`qibo.measurements.CircuitResult`: The input state with the updated (mitigated) frequencies. + :class:`qibo.measurements.CircuitResult`: the input state with the updated (mitigated) frequencies. """ frequencies = np.zeros(2 ** len(state.measurements[0].qubits)) for key, value in state.frequencies().items(): From f05141e83c07db2424510e4cfb18b980975c3448 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Thu, 11 Jan 2024 23:30:13 +0000 Subject: [PATCH 71/78] Update src/qibo/models/error_mitigation.py Co-authored-by: Matteo Robbiati <62071516+MatteoRobbiati@users.noreply.github.com> --- src/qibo/models/error_mitigation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 8f9eefd0c3..886351e7e1 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -482,7 +482,7 @@ def get_response_matrix( Defaults to ``None``. Returns: - numpy.ndarray : The computed (`nqubits`, `nqubits`) response matrix for + numpy.ndarray : the computed (`nqubits`, `nqubits`) response matrix for readout mitigation. """ from qibo import Circuit # pylint: disable=import-outside-toplevel From 159707bcee4c8e42c9932d015d6531386ab8d68a Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 12 Jan 2024 01:12:35 +0100 Subject: [PATCH 72/78] Matteo's suggestions --- src/qibo/models/error_mitigation.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 886351e7e1..036bb53c1a 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -108,7 +108,7 @@ def ZNE( nshots=10000, solve_for_gammas=False, insertion_gate="CNOT", - readout: dict = {}, + readout=None, qubit_map=None, backend=None, ): @@ -150,6 +150,8 @@ def ZNE( """ if backend is None: # pragma: no cover backend = GlobalBackend() + if readout is None: + readout = {} expected_values = [] for num_insertions in noise_levels: @@ -261,7 +263,7 @@ def CDR( model=lambda x, a, b: a * x + b, n_training_samples: int = 100, full_output: bool = False, - readout: dict = {}, + readout=None, qubit_map=None, backend=None, ): @@ -303,6 +305,8 @@ def CDR( """ if backend is None: # pragma: no cover backend = GlobalBackend() + if readout is None: + readout = {} training_circuits = [ sample_training_circuit_cdr(circuit) for _ in range(n_training_samples) @@ -340,7 +344,7 @@ def vnCDR( n_training_samples: int = 100, insertion_gate: str = "CNOT", full_output: bool = False, - readout: dict = {}, + readout=None, qubit_map=None, backend=None, ): @@ -387,6 +391,8 @@ def vnCDR( """ if backend is None: # pragma: no cover backend = GlobalBackend() + if readout is None: + readout = {} training_circuits = [ sample_training_circuit_cdr(circuit) for _ in range(n_training_samples) @@ -694,7 +700,7 @@ def sample_clifford_training_circuit( circuit, backend=None, ): - """Samples a training circuit for CDR by susbtituting some of the non-Clifford gates. + """Samples a training circuit for CDR by susbtituting all the non-Clifford gates. Args: circuit (:class:`qibo.models.Circuit`): circuit to sample from. @@ -736,8 +742,10 @@ def sample_clifford_training_circuit( else: if i in non_clifford_gates_indices: gate = gates.Unitary( - random_clifford(1, backend=backend, return_circuit=False), - gate.qubits[0], + random_clifford( + len(gate.qubits), backend=backend, return_circuit=False + ), + *gate.qubits, ) gate.clifford = True sampled_circuit.add(gate) From bee350a8053bab97caf24fb1a67be706ffefb37b Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 12 Jan 2024 02:01:58 +0100 Subject: [PATCH 73/78] fix coverage --- src/qibo/models/error_mitigation.py | 8 ++++++-- tests/test_models_error_mitigation.py | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index 036bb53c1a..a1f2c4ec50 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -645,7 +645,7 @@ def get_expectation_val_with_readout_mitigation( observable, noise_model=None, nshots: int = 10000, - readout={}, + readout=None, qubit_map=None, backend=None, ): @@ -672,6 +672,8 @@ def get_expectation_val_with_readout_mitigation( """ if backend is None: # pragma: no cover backend = GlobalBackend() + if readout is None: + readout = {} if "ncircuits" in readout: circuit_result, circuit_result_cal = apply_randomized_readout_mitigation( @@ -837,7 +839,7 @@ def error_sensitive_circuit(circuit, observable, backend=None): def ICS( circuit, observable, - readout={}, + readout=None, qubit_map=None, noise_model=None, nshots=int(1e4), @@ -880,6 +882,8 @@ def ICS( """ if backend is None: # pragma: no cover backend = GlobalBackend() + if readout is None: + readout = {} if qubit_map is None: qubit_map = list(range(circuit.nqubits)) diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index c1132c5a5a..7d64b2481b 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -65,7 +65,7 @@ def get_circuit(nqubits, nmeas=None): @pytest.mark.parametrize( "nqubits,noise,insertion_gate,readout", [ - (3, get_noise_model(DepolarizingError(0.1), gates.CNOT), "CNOT", {}), + (3, get_noise_model(DepolarizingError(0.1), gates.CNOT), "CNOT", None), ( 3, get_noise_model(DepolarizingError(0.1), gates.CNOT), @@ -78,7 +78,7 @@ def get_circuit(nqubits, nmeas=None): "CNOT", {"ncircuits": 2}, ), - (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", {}), + (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", None), ( 1, get_noise_model(DepolarizingError(0.3), gates.RX), @@ -141,7 +141,7 @@ def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): @pytest.mark.parametrize( "noise,readout", [ - (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), + (get_noise_model(DepolarizingError(0.1), gates.CNOT), None), ( get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), {"response_matrix": resp_matrix_2q, "ibu_iters": None}, @@ -327,7 +327,7 @@ def test_readout_mitigation(backend, nqubits, nmeas, method, ibu_iters): @pytest.mark.parametrize( "noise,readout", [ - (get_noise_model(DepolarizingError(0.1), gates.CNOT), {}), + (get_noise_model(DepolarizingError(0.1), gates.CNOT), None), ( get_noise_model(DepolarizingError(0.1), gates.CNOT, [True, resp_matrix_2q]), {"response_matrix": resp_matrix_2q, "ibu_iters": None}, From 9cebb486ee1aec92e1bf432d671d8badd3cb30ea Mon Sep 17 00:00:00 2001 From: Alejandro Sopena Date: Fri, 12 Jan 2024 04:01:03 +0100 Subject: [PATCH 74/78] fix coverage --- src/qibo/models/error_mitigation.py | 2 +- tests/test_models_error_mitigation.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index a1f2c4ec50..2e2d577d2e 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -672,7 +672,7 @@ def get_expectation_val_with_readout_mitigation( """ if backend is None: # pragma: no cover backend = GlobalBackend() - if readout is None: + if readout is None: # pragma: no cover readout = {} if "ncircuits" in readout: diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 7d64b2481b..035daabd1b 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -229,6 +229,7 @@ def test_sample_training_circuit(nqubits): @pytest.mark.parametrize( "nqubits,noise,insertion_gate,readout", [ + (1, get_noise_model(DepolarizingError(0.1), gates.RX), "RX", None), ( 1, get_noise_model(DepolarizingError(0.1), gates.RX, [True, resp_matrix_1q]), From ac4e4769117b2f75b446dc3c70a93766070efb65 Mon Sep 17 00:00:00 2001 From: scarrazza Date: Fri, 12 Jan 2024 11:39:03 +0100 Subject: [PATCH 75/78] increasing version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1209f86ad7..bb2b539695 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "qibo" -version = "0.2.4" +version = "0.2.5" description = "A framework for quantum computing with hardware acceleration." authors = ["The Qibo team"] license = "Apache License 2.0" From 627eab75ed212b85c19374bb80a9fa839398f305 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Sat, 20 Jan 2024 12:37:25 +0400 Subject: [PATCH 76/78] improved docstrings --- src/qibo/noise.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/qibo/noise.py b/src/qibo/noise.py index e7ed76909e..9cbbee25f6 100644 --- a/src/qibo/noise.py +++ b/src/qibo/noise.py @@ -1,6 +1,7 @@ import collections from itertools import combinations from math import log2 +from typing import Optional, Union from qibo import gates from qibo.config import raise_error @@ -211,7 +212,13 @@ def __init__(self): self.errors = collections.defaultdict(list) self.noise_model = {} - def add(self, error, gate=None, qubits=None, condition=None): + def add( + self, + error, + gate: Optional[gates.Gate] = None, + qubits: Optional[Union[int, tuple]] = None, + condition=None, + ): """Add a quantum error for a specific gate and qubit to the noise model. Args: @@ -224,13 +231,14 @@ def add(self, error, gate=None, qubits=None, condition=None): :class:`qibo.noise.ReadoutError`, :class:`qibo.noise.ResetError`, :class:`qibo.noise.UnitaryError`, - :class:`qibo.noise.KrausError` and + :class:`qibo.noise.KrausError`, and :class:`qibo.noise.CustomError`. - gate (:class:`qibo.gates.Gate`): gate after which the noise will be added. + gate (:class:`qibo.gates.Gate`, optional): gate after which the noise will be added. If ``None``, the noise will be added after each gate except :class:`qibo.gates.Channel` and :class:`qibo.gates.M`. - qubits (tuple): qubits where the noise will be applied. If ``None``, + qubits (int or tuple, optional): qubits where the noise will be applied. If ``None``, the noise will be added after every instance of the gate. + Defaults to ``None``. condition (callable, optional): function that takes :class:`qibo.gates.Gate` object as an input and returns ``True`` if noise should be added to it. @@ -306,9 +314,8 @@ def apply(self, circuit): circuit (:class:`qibo.models.circuit.Circuit`): quantum circuit Returns: - A (:class:`qibo.models.circuit.Circuit`) which corresponds - to the initial circuit with noise gates added according - to the noise model. + (:class:`qibo.models.circuit.Circuit`): initial circuit with noise gates + added according to the noise model. """ if isinstance(self.noise_model, CompositeNoiseModel): From 617fc7a6762f2bb98de3efa45d46a67b05bf3185 Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 23 Jan 2024 15:06:06 +0400 Subject: [PATCH 77/78] another fix --- doc/source/api-reference/qibo.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/api-reference/qibo.rst b/doc/source/api-reference/qibo.rst index bbeeda603c..3ee49b39a9 100644 --- a/doc/source/api-reference/qibo.rst +++ b/doc/source/api-reference/qibo.rst @@ -849,7 +849,7 @@ U1q - Pauli-:math:`Z` rotation: :class:`qibo.gates.RZ` - Arbitrary :math:`ZZ` rotation: :class:`qibo.gates.RZZ` - - Fully-entangling :math:`ZZ`-interaction: :math:`R_{ZZ}(\\pi/2)` + - Fully-entangling :math:`ZZ`-interaction: :math:`R_{ZZ}(\pi/2)` _______________________ From d92d4aaf061bd3bd54f35c148028dc5bdb35991f Mon Sep 17 00:00:00 2001 From: Renato Mello Date: Tue, 23 Jan 2024 17:42:56 +0400 Subject: [PATCH 78/78] another minor fix --- src/qibo/quantum_info/metrics.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/qibo/quantum_info/metrics.py b/src/qibo/quantum_info/metrics.py index 4d2632fcaa..eb29f1b486 100644 --- a/src/qibo/quantum_info/metrics.py +++ b/src/qibo/quantum_info/metrics.py @@ -1176,14 +1176,15 @@ def frame_potential( .. math:: \\mathcal{F}_{\\mathcal{U}}^{(t)} = \\int_{U,V \\in \\mathcal{U}} \\, - \\text{d}U \\, \\text{d}V \\, \\text{abs}\\bigl[\\text{tr}(U^{\\dagger} \\, V)\\bigr]^{2t} \\, , + \\text{d}U \\, \\text{d}V \\, \\bigl| \\, \\text{tr}(U^{\\dagger} \\, V) + \\, \\bigr|^{2t} \\, , where :math:`\\mathcal{U}` is the group of unitaries defined by the parametrized circuit. The frame potential is approximated by the average .. math:: \\mathcal{F}_{\\mathcal{U}}^{(t)} \\approx \\frac{1}{N} \\, - \\sum_{k=1}^{N} \\, \\text{abs}\\bigl[ \\text{tr}(U_{k}^{\\dagger} \\, V_{k})\\bigr]^{2t} \\, , + \\sum_{k=1}^{N} \\, \\bigl| \\, \\text{tr}(U_{k}^{\\dagger} \\, V_{k}) \\, \\bigr|^{2t} \\, , where :math:`N` is the number of ``samples``.