diff --git a/src/qibo/models/error_mitigation.py b/src/qibo/models/error_mitigation.py index dd1f927501..eea1071399 100644 --- a/src/qibo/models/error_mitigation.py +++ b/src/qibo/models/error_mitigation.py @@ -52,51 +52,77 @@ def get_gammas(noise_levels, analytical: bool = True): return zne_coefficients -def get_noisy_circuit(circuit, num_insertions: int, insertion_gate: str = "CNOT"): +def get_noisy_circuit( + circuit, num_insertions: int, global_unitary_folding=True, insertion_gate=None +): """Standalone function to generate the noisy circuit with the inverse gate pairs insertions. Args: circuit (:class:`qibo.models.circuit.Circuit`): circuit to modify. - num_insertions (int): number of insertion gate pairs to add. - insertion_gate (str, optional): gate to be used in the insertion. - If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``. - Default is ``"CNOT"``. + num_insertions (int): number of insertion gate pairs / global unitary folds to add. + global_unitary_folding (bool): If ``True``, noise is increased by global unitary folding. + If ``False``, local unitary folding is used. Defaults to ``True``. + insertion_gate (str, optional): gate to be folded in the local unitary folding. + If ``RX``, the gate used is :math:``RX(\\pi / 2)``. Otherwise, it is the ``CNOT`` gate. Returns: - :class:`qibo.models.Circuit`: circuit with the inserted gate pairs. + :class:`qibo.models.Circuit`: circuit with the inserted gate pairs or with global folding. """ - if insertion_gate not in ("CNOT", "RX"): # pragma: no cover - raise_error( - ValueError, - "Invalid insertion gate specification. Please select between 'CNOT' and 'RX'.", - ) - if insertion_gate == "CNOT" and circuit.nqubits < 2: # pragma: no cover - raise_error( - ValueError, - "Provide a circuit with at least 2 qubits when using the 'CNOT' insertion gate. " - + "Alternatively, try with the 'RX' insertion gate instead.", - ) - i_gate = gates.CNOT if insertion_gate == "CNOT" else gates.RX + from qibo import Circuit # pylint: disable=import-outside-toplevel + + if global_unitary_folding: + + copy_c = Circuit(**circuit.init_kwargs) + for g in circuit.queue: + if not isinstance(g, gates.M): + copy_c.add(g) + + noisy_circuit = copy_c - theta = np.pi / 2 - noisy_circuit = circuit.__class__(**circuit.init_kwargs) + for _ in range(num_insertions): + noisy_circuit += copy_c.invert() + copy_c - for gate in circuit.queue: - noisy_circuit.add(gate) + for m in circuit.measurements: + noisy_circuit.add(m) - if isinstance(gate, i_gate): - if insertion_gate == "CNOT": - control = gate.control_qubits[0] - target = gate.target_qubits[0] - 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 _ in range(num_insertions): - noisy_circuit.add(gates.RX(qubit, theta=theta)) - noisy_circuit.add(gates.RX(qubit, theta=-theta)) + else: + if insertion_gate is None or insertion_gate not in ( + "CNOT", + "RX", + ): # pragma: no cover + raise_error( + ValueError, + "Invalid insertion gate specification. Please select between 'CNOT' and 'RX'.", + ) + if insertion_gate == "CNOT" and circuit.nqubits < 2: # pragma: no cover + raise_error( + ValueError, + "Provide a circuit with at least 2 qubits when using the 'CNOT' insertion gate. " + + "Alternatively, try with the 'RX' insertion gate instead.", + ) + + i_gate = gates.CNOT if insertion_gate == "CNOT" else gates.RX + + theta = np.pi / 2 + noisy_circuit = Circuit(**circuit.init_kwargs) + + 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 _ in range(num_insertions): + noisy_circuit.add(gates.CNOT(control, target)) + noisy_circuit.add(gates.CNOT(control, target)) + elif insertion_gate == "RX": + qubit = gate.qubits[0] + theta = gate.init_kwargs["theta"] + for _ in range(num_insertions): + noisy_circuit.add(gates.RX(qubit, theta=theta)) + noisy_circuit.add(gates.RX(qubit, theta=-theta)) return noisy_circuit @@ -108,6 +134,7 @@ def ZNE( noise_model=None, nshots=10000, solve_for_gammas=False, + global_unitary_folding=True, insertion_gate="CNOT", readout=None, qubit_map=None, @@ -129,9 +156,10 @@ def ZNE( 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. - If ``"RX"``, the gate used is :math:``RX(\\pi / 2)``. - Defaults to ``"CNOT"``. + global_unitary_folding (bool, optional): If ``True``, noise is increased by global unitary folding. + If ``False``, local unitary folding is used. Defaults to ``True``. + insertion_gate (str, optional): gate to be folded in the local unitary folding. + If ``RX``, the gate used is :math:``RX(\\pi / 2)``. Otherwise, it is the ``CNOT`` gate. 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. @@ -162,7 +190,10 @@ def ZNE( expected_values = [] for num_insertions in noise_levels: noisy_circuit = get_noisy_circuit( - circuit, num_insertions, insertion_gate=insertion_gate + circuit, + num_insertions, + global_unitary_folding, + insertion_gate=insertion_gate, ) val = get_expectation_val_with_readout_mitigation( noisy_circuit, diff --git a/tests/test_models_error_mitigation.py b/tests/test_models_error_mitigation.py index 4335f909f7..53d6166a7b 100644 --- a/tests/test_models_error_mitigation.py +++ b/tests/test_models_error_mitigation.py @@ -95,7 +95,8 @@ def get_circuit(nqubits, nmeas=None): ], ) @pytest.mark.parametrize("solve", [False, True]) -def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): +@pytest.mark.parametrize("GUF", [False, True]) +def test_zne(backend, nqubits, noise, solve, GUF, insertion_gate, readout): """Test that ZNE reduces the noise.""" if backend.name == "tensorflow": import tensorflow as tf @@ -128,6 +129,7 @@ def test_zne(backend, nqubits, noise, solve, insertion_gate, readout): noise_model=noise, nshots=10000, solve_for_gammas=solve, + global_unitary_folding=GUF, insertion_gate=insertion_gate, readout=readout, backend=backend,