amazon-braket / autoqasm

AutoQASM is an experimental module offering a new quantum-imperative programming experience in Python for developing quantum programs.
Apache License 2.0
13 stars 8 forks source link

Support arithmetic on measurement results #10

Closed rmshaffer closed 2 months ago

rmshaffer commented 3 months ago

Users may commonly want to do arithmetic on measurement results (for example: syndrome calculation in quantum error correction). Today measure() returns a BitVar which cannot be operated on without explicitly casting to IntVar, like:

a1 = aq.IntVar(measure(1))
a0 = aq.IntVar(measure(0))
syndrome = 2*a1 + a0

We should make this easier so that users can simply do something like:

syndrome = 2*measure(1) + measure(0)

which implicitly does the same thing as above.

As a bonus, we should also allow conversion of a BitVar register to an IntVar simply by using a Pythonic conversion to int, such as:

syndrome = int(measure([0,1]))

which is an even simpler way to express the same as above.

abidart commented 2 months ago

Hello @rmshaffer, thank you for adding this issue to UnitaryHack! I am trying to tackle this as part of the challenge.

I have a quick question: if I understand correctly, a call to int() should always return an int, even if I write a custom __int__ method for the class. However, we are using BitVars and IntVars because, at this stage, we do not have access to what the measurements will result in.

Instead of using a call to int(), would it be appropriate to do something like the following, expecting syndrome to be an IntVar?

syndrome = measure([0,1]).to_int()

Thank you for your time!

rmshaffer commented 2 months ago

@abidart Thanks, this is a very good point. So the AutoQASM/AutoGraph model for this would be to: (1) add a new converter which converts int() calls to something like ag__.int_cast(). There's an example here which converts assignment statements to ag__.assign_stmt(tar_name_, val_) calls. (2) implement int_cast() in the operators module - just as assign_stmt is implemented here. This int_cast() operator would return an IntVar in the case where the argument is an aq type (e.g. by calling aq_types.is_qasm_type()), and otherwise it would just fall back to the python int() function. (an example of this type of logic here) (3) add the new converter to the list of converters in the transpiler here

This allows keeping the Pythonic syntax (where the user can just code as if they are dealing with native Python types), but also keeps the implementation code clean and doesn't force us to write a custom __int__ function which returns an IntVar (which, as you say, would be unnatural).

Let me know if I can clarify any of this - although this change might touch several files, there should hopefully be enough existing examples to follow.

abidart commented 2 months ago

Thank you very much! I have a follow-up question. In the current AQ version,

a1 = aq.IntVar(measure(1))
a0 = aq.IntVar(measure(0))
syndrome = 2*a1 + a0

compiles to:

OPENQASM 3.0;
qubit[2] __qubits__;
bit __bit_0__;
__bit_0__ = measure __qubits__[1];
int[32] a1 = __bit_0__;
bit __bit_2__;
__bit_2__ = measure __qubits__[0];
int[32] a0 = __bit_2__;

I am trying to get syndrome = int(measure([0,1])) to compile to the same thing. My initial approach was to add a transformer for typecasts in the transpiler, precisely in line 140. I thought that transforming the typecasting calls might need to come before the call tree gets transformed, otherwise, it gets a lot more difficult to iterate over the "bits" in the BitVar object returned by a call to measure. However, this results in the following instructions which look quite different.

OPENQASM 3.0;
qubit[2] __qubits__;
bit[2] __bit_0__ = "00";
__bit_0__[0] = measure __qubits__[0];
__bit_0__[1] = measure __qubits__[1];

Also, the original three lines still compile to the same instructions and all unit tests pass (tox -e unit-tests), so I do not think the changes I made impacted anything outside calls to int.

Do you think I should move the transformer to (or towards) the end of the transformer calls? Otherwise, given that I followed the steps from your previous message, do the instructions resulting from the int call make you think of something I might be missing/doing wrong? Here are some key snippets from my operator and converter files in case it can be helpful:

operator

    if aq_types.is_qasm_type(args_):
        if args_.size is not None and args_.size > 1 and isinstance(args_, aq_types.BitVar):
            typecasted_var = aq_types.IntVar(args_[0])
            for index in range(1, args_.size):
                typecasted_var += aq_types.IntVar(args_[index]) * 2**index
            return typecasted_var
        else:
            aq_types.IntVar(args_)
    else:
        return type_(*args_)

converter

        if (
            len(node.args) > 1
            and hasattr(node.args[1], "func")
            and hasattr(node.args[1].func, "id")
            and node.args[1].func.id == "int"
        ):
            new_node = templates.replace(
                template,
                type_=node.args[1].func.id,
                args_=node.args[1].args,
                original=node,
            )
            # new_node is a list of gast.Expr with only one element, 
            # whose value attribute is a gast.Call
            new_node = new_node[0].value  

Thanks again for your time and patience 🙏

rmshaffer commented 2 months ago

Hey @abidart, thanks for looking into this! The easiest way for me to help would be if you push your changes to a branch or even open a draft PR here so that I can easily pull your code and run it - would you mind doing that?

abidart commented 2 months ago

I just opened a draft PR!

rmshaffer commented 2 months ago

@abidart Thanks! I've left a few comments on the PR, we can continue the discussion there.