Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Native gates directly defined by the platform #1077

Merged
merged 18 commits into from
Feb 14, 2025
Merged
Changes from 12 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 51 additions & 4 deletions src/qibocal/auto/transpile.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Optional

from qibo import Circuit
from qibo import Circuit, gates
from qibo.backends import Backend
from qibo.transpiler.pipeline import Passes
from qibo.transpiler.unroller import NativeGates, Unroller
from qibolab.pulses import PulseSequence
from qibolab.qubits import QubitId


Expand Down Expand Up @@ -97,19 +98,65 @@ def execute_transpiled_circuit(
backend,
transpiler,
)[0]

return transpiled_circ, backend.execute_circuit(
transpiled_circ, initial_state=initial_state, nshots=nshots
)


def get_natives(platform):
"""
Return the list of native gates defined in the `platform`.
This function assumes the native gates to be the same for each
qubit and pair.
"""
pairs = list(platform.pairs.values())[0]
qubit = list(platform.qubits.values())[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to start from parameters, instead of starting directly from the natives listing exposed by the platform?
https://github.com/qiboteam/qibolab/blob/bfb48cc2d9ec5e563110720889dc74acca6e604c/src/qibolab/backends.py#L64-L79

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure I understood, this function should return the native gates that have a rule in the default compiler, so it will not work if the iSWAP is in the platform (this was my first attempt but as explained before the result was not what I was looking for, I am not sure this is intended).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait a second: this function is used just to feed set_compiler, in which you're doing the following

if gate not in compiler.rules:
rules[gate] = create_rule(native)
else:
rules[gate] = compiler.rules[gate]

To me, that means that you're trying to support rules beyond the default compiler. Including iSWAP, if available.

I'm sure I misunderstood something in your reply...

two_qubit_natives = list(pairs.native_gates.raw)
single_qubit_natives = list(qubit.native_gates.raw)
# Solve Qibo-Qibolab mismatch
single_qubit_natives.append("RZ")
single_qubit_natives.append("Z")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if Z should be somehow directly inferred from RZ by the transpiler itself...

(implying that is not platform's/Qibocal's business)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory yes, I am using Qibolab compiler and NativeGates convention to differentiate between the two.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine, but is there a reason to differentiate?

I.e. with the current compiler, the RZ will always be implemented as virtual phase, and that will be the same for the Z as well...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fine, but is there a reason to differentiate?

No, maybe @andrea-pasquale could have an explanation.

single_qubit_natives.remove("RX12")
replacements = {
"RX": "GPI2",
"MZ": "M",
}
new_single_natives = [replacements.get(i, i) for i in single_qubit_natives]
natives = new_single_natives + two_qubit_natives
return natives


def set_compiler(backend, natives):
"""
Set the compiler to execute the native gates defined by the platform.
"""
compiler = backend.compiler
for native in natives:
gate = getattr(gates, native)
if gate not in compiler.rules:

def rule(qubits_ids, platform, parameters=None):
if len(qubits_ids[1]) == 1:
native_gate = platform.qubits[tuple(qubits_ids[1])].native_gates
else:
native_gate = platform.pairs[tuple(qubits_ids[1])].native_gates
pulses = getattr(native_gate, native).pulses
return PulseSequence(pulses), {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using closures is always delicate, so, I'd suggest moving it in a function on its own, at least will make it clear which are the captured variables

def create_rule(native):
    def rule(qubits_ids, platform, parameters=None):
        if len(qubits_ids[1]) == 1:
            native_gate = platform.qubits[tuple(qubits_ids[1])].native_gates
        else:
            native_gate = platform.pairs[tuple(qubits_ids[1])].native_gates
        pulses = getattr(native_gate, native).pulses
        return PulseSequence(pulses), {}

    return rule

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P.S.: type hints would also help


backend.compiler[gate] = rule
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, usually it is better to avoid mutating objects, especially nested ones.
I could lift the suggestion by one level, and suggest creating the entire backend (here just returning the compiler, instead of modifying the backend). But let's limit to the compiler itself.

You could create an empty dictionary rules, and here do

Suggested change
backend.compiler[gate] = rule
rules[gate] = rule

eventually setting the compiler out of the loops as:

    backend.compiler = Compiler(rules=rules)

(in principle, you can avoid even modifying the dictionary rules, by defining it immediately through comprehension - but I get this is a bit unfamiliar).



def dummy_transpiler(backend: Backend) -> Passes:
"""
If the backend is `qibolab`, a transpiler with just an unroller is returned,
otherwise None.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Properly document that this function is not pure, since it has the side effect of modifying the input backend (in particular, affecting its .compiler attribute).

I know this seems pedantic, but losing track of this changes may have effects later on, when they will become unexpected.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea at the beginning was to make the set_compiler function in each protocol that executes circuits explicit. I decided to put it here to make my life easier but I will open an issue about it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My idea at the beginning was to make the set_compiler function in each protocol that executes circuits explicit.

No need.

I agree with your choice, since there is nothing protocol-specific, so it should not be repeated over and over.

to make my life easier

Everyone's life easier. Good choice :)

I decided to put it here

Even better, you could have put it here

backend = construct_backend(backend="qibolab", platform=platform)
platform = self.platform = backend.platform

if it were the single place where the backend was created.

Since unfortunately it is not, instead of this:

backend = construct_backend("qibolab", platform=platform)
transpiler = dummy_transpiler(backend)

you could actually just make a single function, and invoke as

    backend, transpiler = construct_qibolab_backend(platform)

considering that this double functions invocation is repeat over and over.

https://github.com/search?q=repo%3Aqiboteam%2Fqibocal%20construct_backend&type=code

(in any case, this one could be an issue - but it's especially relevant for 0.2, since we won't maintain 0.1 for long)

"""
unroller = Unroller(NativeGates.default())
return Passes(connectivity=backend.platform.topology, passes=[unroller])
platform = backend.platform
native_gates = get_natives(platform)
set_compiler(backend, native_gates)
native_gates = list(map(lambda x: getattr(gates, x), native_gates))
unroller = Unroller(NativeGates.from_gatelist(native_gates))
return Passes(connectivity=platform.topology, passes=[unroller])


def pad_circuit(nqubits, circuit: Circuit, qubit_map: list[int]) -> Circuit:
Expand Down