unitaryfund / mitiq

Mitiq is an open source toolkit for implementing error mitigation techniques on most current intermediate-scale quantum computers.
https://mitiq.readthedocs.io
GNU General Public License v3.0
349 stars 145 forks source link

Unitary folding doesn't respect the input circuit's gate set #1041

Closed eth-n closed 2 years ago

eth-n commented 2 years ago

Pre-Report Checklist

Mitiq 0.11.1 Braket SDK 1.11.0

Yes, I looked at the open issues.

Issue Description

Hi! I'm trying to run a six qubit circuit with Mitiq on the Rigetti Aspen-10 machine available through AWS Braket. Following the structure of the example notebook here, https://mitiq.readthedocs.io/en/stable/examples/braket_mirror_circuit.html, I found I had to develop a slightly more complicated workflow. Since we can't just select the best pair of qubits, I decided to offload place and route to the Quil compiler running at the machine by sending a circuit with the minimum number of shots, reading the compiled result from the task result, and parsing that back into a circuit that I can send in a verbatim box (if it already executed on the device, I know the connectivity is going to work out and a Rigetti engineer has already decided it's the best qubit set to use!).

When I do this, some of the gates come back with floating point arguments, and some come back with exact values, eg for a few gates on qubit 21 and then a CZ between 21 and 36 (connected on the chip), it might read,

DECLARE ro BIT[6]
PRAGMA INITIAL_REWIRING "PARTIAL"
RESET
RZ(1.7474272155634216) 21
RX(pi/2) 21
RZ(1.4904665003223143) 21
CZ 21 36
...
measurement/readout

What I've found is that if I put rx(qubit#, np.pi/2) back into my circuit to be executed in a verbatim box, the ZNE factory folds this as a V gate, as it is the sqrt(X). However, the Rigetti machine has a limited gate set; it recognizes only a few gates, I believe it can be summarized to: Rx(±n*pi/2), Rz(theta), CZ, CPHASE(theta), and XY(theta). Those are going to be the native gates allowed in a verbatim box as described here: https://docs.aws.amazon.com/braket/latest/developerguide/braket-constructing-circuit.html, which keeps the compiler from optimizing away the folded gates. Folding the X(±pi/2) gate into a V or Vi gate breaks the verbatim validator, preventing me from running the folded circuits.

How to Reproduce

Code Snippet

# Rigetti Setup
session = boto3.Session(profile_name="REDACTED", region_name="us-west-1")
s3 = session.client("s3")
brkt = session.client("braket")
braket_aws_session = AwsSession(session, brkt)

BUCKET = "amazon-braket-REDACTED"
device = AwsDevice("arn:aws:braket:::device/qpu/rigetti/Aspen-10", aws_session=braket_aws_session)
print("Native gates for this device:", device.properties.paradigm.nativeGateSet)

# SAME ACROSS REGIONS
KEY_PREFIX = "REDACTED"
s3_folder = AwsSession.S3DestinationFolder(BUCKET, KEY_PREFIX)

# minimum number of shots is 10
def rigetti_execute_repro(ckt, shots=10):
    print(f"cost: ${0.3 + shots * 0.00035}")

    # Circuit must be able to run directly on hardware if placed in a verbatim box
    ckt = Circuit().add_verbatim_box(ckt)

    task = device.run(ckt, s3_folder, disable_qubit_rewiring=True, shots=shots)
    res = task.result()
    measurement_probs = res.measurement_probabilities

    # Compute something with the measurement probabilities
    prob_measured_0 = measurement_probs.get("0", 0) / shots

    return prob_measured_0

repro_ckt = Circuit().rx(0, np.pi/2)

assert True
noisy_value = rigetti_execute_repro(repro_ckt)
print("Noisy value:", noisy_value)

zne_value = zne.execute_with_zne(
    repro_ckt, 
    rigetti_execute_repro, 
    scale_noise=zne.scaling.fold_global, 
    factory=zne.inference.PolyFactory(scale_factors=[1, 3, 5], order=2)
)
print("ZNE value:", zne_value)

Error Output

Native gates for this device: ['RX', 'RZ', 'CZ', 'CPHASE', 'XY']
cost: $0.3035
{'0': 0.4, '1': 0.6}
Noisy value: 0.4
cost: $0.3035

---------------------------------------------------------------------------
ValidationException                       Traceback (most recent call last)
<ipython-input-330-7c3fdde4cb47> in <module>
     35 print("Noisy value:", noisy_value)
     36 
---> 37 zne_value = zne.execute_with_zne(
     38     repro_ckt,
     39     rigetti_execute_repro,

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/mitiq/zne/zne.py in execute_with_zne(circuit, executor, observable, factory, scale_noise, num_to_average)
     65         raise ValueError("Argument `num_to_average` must be a positive int.")
     66 
---> 67     return factory.run(
     68         circuit, executor, observable, scale_noise, int(num_to_average)
     69     ).reduce()

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/mitiq/zne/inference.py in run(self, qp, executor, observable, scale_noise, num_to_average)
    576         else:
    577             # Else, run all circuits.
--> 578             res = executor.evaluate(
    579                 to_run, observable, force_run_all=True, **kwargs_list[0]
    580             )

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/mitiq/executor/executor.py in evaluate(self, circuits, observable, force_run_all, **kwargs)
    157 
    158         # Run all required circuits.
--> 159         all_results = self._run(all_circuits, force_run_all, **kwargs)
    160 
    161         # Parse the results.

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/mitiq/executor/executor.py in _run(self, circuits, force_run_all, **kwargs)
    230         if not self.can_batch:
    231             for circuit in to_run:
--> 232                 self._call_executor(circuit, **kwargs)
    233 
    234         else:

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/mitiq/executor/executor.py in _call_executor(self, to_run, **kwargs)
    260             to_run: Circuit(s) to run.
    261         """
--> 262         result = self._executor(to_run, **kwargs)  # type: ignore
    263         self._calls_to_executor += 1
    264 

<ipython-input-330-7c3fdde4cb47> in rigetti_execute_repro(ckt, shots)
     18     ckt = Circuit().add_verbatim_box(ckt)
     19 
---> 20     task = device.run(ckt, s3_folder, disable_qubit_rewiring=True, shots=shots)
     21     res = task.result()
     22     measurement_probs = res.measurement_probabilities

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/braket/aws/aws_device.py in run(self, task_specification, s3_destination_folder, shots, poll_timeout_seconds, poll_interval_seconds, *aws_quantum_task_args, **aws_quantum_task_kwargs)
    141             `braket.aws.aws_quantum_task.AwsQuantumTask.create()`
    142         """
--> 143         return AwsQuantumTask.create(
    144             self._aws_session,
    145             self._arn,

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/braket/aws/aws_quantum_task.py in create(aws_session, device_arn, task_specification, s3_destination_folder, shots, device_parameters, disable_qubit_rewiring, tags, *args, **kwargs)
    134         if tags is not None:
    135             create_task_kwargs.update({"tags": tags})
--> 136         return _create_internal(
    137             task_specification,
    138             aws_session,

/opt/anaconda3/envs/braketenv/lib/python3.8/functools.py in wrapper(*args, **kw)
    873                             '1 positional argument')
    874 
--> 875         return dispatch(args[0].__class__)(*args, **kw)
    876 
    877     funcname = getattr(func, '__name__', 'singledispatch function')

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/braket/aws/aws_quantum_task.py in _(circuit, aws_session, create_task_kwargs, device_arn, device_parameters, disable_qubit_rewiring, *args, **kwargs)
    446         {"action": circuit.to_ir().json(), "deviceParameters": device_parameters.json()}
    447     )
--> 448     task_arn = aws_session.create_quantum_task(**create_task_kwargs)
    449     return AwsQuantumTask(task_arn, aws_session, *args, **kwargs)
    450 

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/braket/aws/aws_session.py in create_quantum_task(self, **boto3_kwargs)
    161         if job_token:
    162             boto3_kwargs.update({"jobToken": job_token})
--> 163         response = self.braket_client.create_quantum_task(**boto3_kwargs)
    164         return response["quantumTaskArn"]
    165 

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/botocore/client.py in _api_call(self, *args, **kwargs)
    389                     "%s() only accepts keyword arguments." % py_operation_name)
    390             # The "self" in this scope is referring to the BaseClient.
--> 391             return self._make_api_call(operation_name, kwargs)
    392 
    393         _api_call.__name__ = str(py_operation_name)

/opt/anaconda3/envs/braketenv/lib/python3.8/site-packages/botocore/client.py in _make_api_call(self, operation_name, api_params)
    717             error_code = parsed_response.get("Error", {}).get("Code")
    718             error_class = self.exceptions.from_code(error_code)
--> 719             raise error_class(parsed_response, operation_name)
    720         else:
    721             return parsed_response

ValidationException: An error occurred (ValidationException) when calling the CreateQuantumTask 
operation: Backend ARN, arn:aws:braket:::device/qpu/rigetti/Aspen-10, does not support the operation v. 
Supported operations for this Backend ARN are ['x', 'cphaseshift01', 's', 'start_verbatim_box', 
'cphaseshift00', 'si', 'h', 'ti', 'y', 'iswap', 'cz', 'cphaseshift10', 'cnot', 'cphaseshift', 'pswap', 
'ccnot', 'rx', 'rz', 't', 'i', 'z', 'end_verbatim_box', 'cswap', 'swap', 'xy', 'phaseshift', 'ry'].

Environment Context

Mitiq: A Python toolkit for implementing error mitigation on quantum computers
==============================================================================
Authored by: Mitiq team, 2020 & later (https://github.com/unitaryfund/mitiq)

Mitiq Version:  0.11.1

Core Dependencies
-----------------
Cirq Version:   0.10.0
NumPy Version:  1.21.4
SciPy Version:  1.7.3

Optional Dependencies
---------------------
PyQuil Version: Not installed
Qiskit Version: Not installed
Braket Version: 1.11.0

Python Version: 3.8.11
Platform Info:  Darwin (x86_64)

Additional Python Environment Details (pip freeze or conda list): (let me know if this is necessary, should be isolated between Mitiq and Braket)

Thank you!

github-actions[bot] commented 2 years ago

Hello @eth-n, thank you for your interest in Mitiq! If this is a bug report, please provide screenshots and/or minimum viable code to reproduce your issue, so we can do our best to help get it fixed. If you have any questions in the meantime, you can also ask us on the Unitary Fund Discord.

rmlarose commented 2 years ago

Thanks @eth-n.

Immediate workaround

You should be able to run by using this quick-and-dirty compiler:

from braket.circuits import Circuit, gates

def compile_to_rigetti_gateset(circuit: Circuit) -> Circuit:
    compiled = Circuit()

    for instr in circuit.instructions:
        if isinstance(instr.operator, gates.Vi):
            compiled.add_instruction(gates.Instruction(gates.Rx(-np.pi / 2), instr.target))
        elif isinstance(instr.operator, gates.V):
            compiled.add_instruction(gates.Instruction(gates.Rx(np.pi / 2), instr.target))
        elif isinstance(instr.operator, gates.Ry):
            compiled.add_instruction(gates.Instruction(gates.Rx(-np.pi / 2), instr.target))
            compiled.add_instruction(gates.Instruction(gates.Rz(instr.operator.angle), instr.target))
            compiled.add_instruction(gates.Instruction(gates.Rx(np.pi / 2), instr.target))
        elif isinstance(instr.operator, gates.Y):
            compiled.add_instruction(gates.Instruction(gates.Rx(-np.pi / 2), instr.target))
            compiled.add_instruction(gates.Instruction(gates.Rz(np.pi), instr.target))
            compiled.add_instruction(gates.Instruction(gates.Rx(np.pi / 2), instr.target))
        elif isinstance(instr.operator, gates.X):
            compiled.add_instruction(gates.Instruction(gates.Rx(np.pi / 2), instr.target))
            compiled.add_instruction(gates.Instruction(gates.Rx(np.pi / 2), instr.target))
        elif isinstance(instr.operator, gates.Z):
            compiled.add_instruction(gates.Instruction(gates.Rz(np.pi), instr.target))
        elif isinstance(instr.operator, gates.S):
            compiled.add_instruction(gates.Instruction(gates.Rz(np.pi / 4), instr.target))
        elif isinstance(instr.operator, gates.Si):
            compiled.add_instruction(gates.Instruction(gates.Rz(-np.pi / 4), instr.target))
        else:
            compiled.add_instruction(instr)

    return compiled

Specifically, use ckt = Circuit().add_verbatim_box(compile_to_rigetti_gateset(ckt)) in your rigetti_execute_repro function. Can you try this and let me know if it works?

(You may need to add some compile instructions to the compile_to_rigetti_gateset function, or can remove ones unnecessary for your example - at your discretion.)

Patch fix

We can probably fiddle with

https://github.com/unitaryfund/mitiq/blob/6b3890e1b3b5736d1a5120b524815d7d7cfac93f/mitiq/interface/mitiq_braket/conversions.py#L316-L324

to return rx instead of v. The only concern is there may be some cases where you actually want it to be v instead of rx.

eth-n commented 2 years ago

Okay, very interesting. I thought I had it covered but I think I see where that step can fit in. Here's more explicitly how I'd set up the pipeline, it might inform an agreed upon design:

  1. Train a varational circuit on {H, X(theta), Z(theta), XY(theta)}. I did this to get as close to the native gates as possible without going to a discrete optimizer for some of the gates.
  2. Run a modified version of the quick-and-dirty compiler that only needs to handle those four gates, to get to a native gate construction.
  3. Submit that circuit to run with the minimum number of shots, to leverage the place and route that should be done "optimally" for the base circuit. The place-and-routed circuit comes back as a long Quil assembly string or whatever term they use. This string includes all the swaps, adds some CPHASE/CZ gates, etc.
  4. Parse the assembly back into a Braket circuit. I confirmed that the unitaries are equivalent up to a global phase for the relaxed or RESET state, |0>. It differs on some other basis states, which is either by design or a parser issue or a Quil issue? This is okay for me because the circuit represents the entire thing I'm going to run anyways, so as long as the action on |0> is the same, I'm okay.
  5. Sub the parsed, routed circuit in a verbatim box. The original works, but the folded don't because I haven't run another 'quick-and-dirty' compiler after the folding but before submission in this step too. This is where I was surprised that the gate was being folded to something else. I could run another recompile step on the parsed circuit, but I would at least prefer that folding leaves the gate the same and just applies -1x to the parameter (or applies the gate again, however else the inverse is constructed per gate).
eth-n commented 2 years ago

So I wrote the second quick-n-dirty compiler for the folded circuit before sending it. A few things come up.

  1. The set of gates I had to recompile from is {X, Z, Ry, S, Si, V, Vi, ISwap, CNot}.
  2. The original circuit still works when passed through this compiler.
  3. The base circuit has gone from depth 22 to depth 370 (not super unexpected given dealing with device connectivity, but may inform the next thing).
  4. The new circuit passes the validator so I'm not getting validation exceptions about the gates in the circuit, but the task simply fails at the device,
    Task is in terminal state FAILED and no result is available.
    Task failure reason is: Failed to compile task; device: Aspen-10.

Edit: Further testing has revealed that the Aspen-10 device is having issues with verbatim_box, the instruction that disables the cloud-based compilation layer for any gates within the box. The Braket team is investigating the problem.

rmlarose commented 2 years ago

Happy to help diagnose if you'd like to post in the Mitiq channel at http://discord.unitary.fund. This seems unrelated to the original issue, though, of unitary folding preventing a circuit which did run from running.

github-actions[bot] commented 2 years ago

This issue had no activity for 2 months, and will be closed in one week unless there is new activity. Cheers!