qua-platform / qua-qsim

A quantum simulator for QUA programs
BSD 3-Clause "New" or "Revised" License
9 stars 6 forks source link

Create frontend interface #4

Closed nulinspiratie closed 3 months ago

nulinspiratie commented 4 months ago

Create a web app to interact with simulation results

The current Qiskit backend can compile a QUA program into a Qiskit Pulse Schedule. After the compilation, the Pulse Schedule can be run on a simulated quantum system, providing results in the form of arrays. Encapsulating the compiler and simulator into a web app would allow the outputs (Pulse Schedule + simulated results) to be visualized in a webpage. External programs such as an IDE can submit QUA programs to be simulated on the web app backend, and the results are then viewed on the web frontend, enabling seamless integration into existing workflows.

This feature can serve as a base for issues #5 and #6

Requirements

Dash and FastAPI

It is recommended that both the Pulse Schedule and Simulated results are displayed on a webpage using Plotly Dash. However, suitable alternative solutions can also be proposed.

To combine this into a web app with an API, Dash should be embedded into FastAPI. For details see Embedding Dash dashboards in FastPI framework This allows external programs to submit QUA programs, configurations, and other necessary settings.

FastAPI API calls

Acceptance criteria

  1. Each of the four components in the readme example can be submitted separately through the API
  2. After submitting all four components, a call to api/simulate should start the simulation
  3. Once the simulation is complete, the web frontend should display the simulated results similar to the example
Piwakk commented 4 months ago

Hi! I have started to work on this issue as part of unitaryHACK 2024. However, I've come across a bug in serialization / deserialization of a Program. One can reproduce the issue by inserting a serialization / deserialization step in test_simultaneous_rabi (test_rabi.py):

def test_simultaneous_rabi(transmon_pair_backend, transmon_pair_qua_config, config_to_transmon_pair_backend_map):
    start, stop, step = -2, 2, 0.1
    with program() as prog:
        a = declare(fixed)

        with for_(a, start, a < stop - 0.0001, a + step):
            play("x90"*amp(a), "qubit_1")
            play("x90"*amp(a), "qubit_2")

            align("qubit_1", "qubit_2", "resonator_1", "resonator_2")
            measure("readout", "resonator_1", None)
            measure("readout", "resonator_2", None)

    new_prog = dill.loads(dill.dumps(prog))

    results = simulate_program(
        qua_program=new_prog,
        qua_config=transmon_pair_qua_config,
        qua_config_to_backend_map=config_to_transmon_pair_backend_map,
        backend=transmon_pair_backend,
        num_shots=10_000,
        # schedules_to_plot=[0]
    )
    # plt.show()

The error:

quaqsim/simulate.py:18: in simulate_program
    sim = compiler.compile(qua_program, qua_config_to_backend_map, backend)
quaqsim/program_to_quantum_pulse_sim_compiler/quantum_pulse_sim_compiler.py:21: in compile
    program_tree = ProgramTreeBuilder().build(program)
quaqsim/program_dict_to_program_compiler/program_tree_builder.py:15: in build
    return visitor.visit(program_body)
quaqsim/program_dict_to_program_compiler/visitors/program_visitor.py:10: in visit
    body_dict = program.body._body.to_dict()
../../miniforge3/envs/qua-qsim/lib/python3.11/site-packages/betterproto/__init__.py:1112: in to_dict
    value = [
../../miniforge3/envs/qua-qsim/lib/python3.11/site-packages/betterproto/__init__.py:1113: in <listcomp>
    i.to_dict(casing, include_default_values) for i in value
../../miniforge3/envs/qua-qsim/lib/python3.11/site-packages/betterproto/__init__.py:1127: in to_dict
    output[cased_name] = value.to_dict(casing, include_default_values)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = QuaProgramPlayStatement(qe=QuaProgramQuantumElementReference(name='qubit_1', loc='File "qua-qsim/tes...object at 0x7f8c937b7730>, loc='File "qua-qsim/test/test_rabi.py", line 12: play("x90", "qubit_1") ')
casing = <function camel_case at 0x7f8cfd0958a0>, include_default_values = False

    def to_dict(
        self, casing: Casing = Casing.CAMEL, include_default_values: bool = False
    ) -> Dict[str, Any]:
        """
        Returns a JSON serializable dict representation of this object.

        Parameters
        -----------
        casing: :class:`Casing`
            The casing to use for key values. Default is :attr:`Casing.CAMEL` for
            compatibility purposes.
        include_default_values: :class:`bool`
            If ``True`` will include the default values of fields. Default is ``False``.
            E.g. an ``int32`` field will be included with a value of ``0`` if this is
            set to ``True``, otherwise this would be ignored.

        Returns
        --------
        Dict[:class:`str`, Any]
            The JSON serializable dict representation of this object.
        """
        output: Dict[str, Any] = {}
        field_types = self._type_hints()
        defaults = self._betterproto.default_gen
        for field_name, meta in self._betterproto.meta_by_field_name.items():
            field_is_repeated = defaults[field_name] is list
            value = getattr(self, field_name)
            cased_name = casing(field_name).rstrip("_")  # type: ignore
            if meta.proto_type == TYPE_MESSAGE:
                if isinstance(value, datetime):
                    if (
                        value != DATETIME_ZERO
                        or include_default_values
                        or self._include_default_value_for_oneof(
                            field_name=field_name, meta=meta
                        )
                    ):
                        output[cased_name] = _Timestamp.timestamp_to_json(value)
                elif isinstance(value, timedelta):
                    if (
                        value != timedelta(0)
                        or include_default_values
                        or self._include_default_value_for_oneof(
                            field_name=field_name, meta=meta
                        )
                    ):
                        output[cased_name] = _Duration.delta_to_json(value)
                elif meta.wraps:
                    if value is not None or include_default_values:
                        output[cased_name] = value
                elif field_is_repeated:
                    # Convert each item.
                    cls = self._betterproto.cls_by_field[field_name]
                    if cls == datetime:
                        value = [_Timestamp.timestamp_to_json(i) for i in value]
                    elif cls == timedelta:
                        value = [_Duration.delta_to_json(i) for i in value]
                    else:
                        value = [
                            i.to_dict(casing, include_default_values) for i in value
                        ]
                    if value or include_default_values:
                        output[cased_name] = value
                elif value is None:
                    if include_default_values:
                        output[cased_name] = value
                elif (
>                   value._serialized_on_wire
                    or include_default_values
                    or self._include_default_value_for_oneof(
                        field_name=field_name, meta=meta
                    )
                ):
E               AttributeError: 'object' object has no attribute '_serialized_on_wire'

../../miniforge3/envs/qua-qsim/lib/python3.11/site-packages/betterproto/__init__.py:1121: AttributeError

I've the same error when using pickle instead of dill.

Versions of seemingly relevant libraries:

betterproto           2.0.0b6
dill                  0.3.8
protobuf              4.25.3
qm-octave             2.1.0
qm-qua                1.1.7
qualang-tools         0.17.4

Has someone encountered this error before? @nulinspiratie did you have something else in mind when mentioning “a binary Dill object (the qua.program object)”?

deanpoulos commented 4 months ago

Hi @Piwakk! I see that you're trying to serialize the program itself, and although I can't comment on why it's not serializable, I know it's not the most friendly data structure and not everything that's valid in that object is supported for Qua-Qsim.

I suggest if you want to serialize the program, you should serialize the program AST:

It is the object created on line 21 here in the first pass of the compiler:

program_tree = ProgramTreeBuilder().build(program)

Although this data structure is currently hidden a couple of layers down into the simulate_program function, you can unwrap those functions and deal with the raw objects yourself.

nulinspiratie commented 4 months ago

Hi @Piwakk great to have you on board!

I wasn't aware that serializing a QUA program would cause issues. I agree with @deanpoulos that serializing ProgramTreeBuilder is a good solution here. Let us know if you have any other questions/comments :-)

Piwakk commented 3 months ago

Hi @nulinspiratie, would it be possible to be assigned to this issue? I think UnitaryHACK needs it to know I've solved it. Thanks!

nulinspiratie commented 3 months ago

Also in this one, apologies but it's fixed now