qc-qiskit 0.5.0
Add a short description here!
License: MIT
Requires Python: >=3.7

=========
qc-qiskit
=========

Client-side IBM Qiskit provider library to access a quantum computers hosted by PGI at Forschungszentrum Jülich [https://www.fz-juelich.de/].

Description
===========

TBD...


Installation
============

Prerequisites 
-------------

The following instruction describe setting up a development environment in a POSIX environment (Linux, MacOSX, Ubuntu-WSL) and primary documents setting up in Ubuntu-20.04 (Focal) on WSL2 (Windows). 
The project is in ``~/project/qc`` folder. 

* Python 3.8.10 
* pip
* virtualenv

.. code-block:: bash

    apt-get install python3.8-venv,python3-pip

Installing 
----------

* create a project folder 

.. code-block:: bash

    cd ~/projects
    mkdir qc
    cd qc

* setup and activate virtual environment 

.. code-block:: bash

    python -m venv .venv
    . .venv/bin/activate

* install qiskit client library 

.. code-block:: bash

    pip install -U qc-qiskit

Access
------

QC is exposed in FZJ intranet, so you need VPN access, to obtain it contact you organization adminstration. 
QC uses FZJ's gitlab service https://iffgit.fz-juelich.de/ for authentication via OpenConnect ID protocol. 
So in case you don't have account on that instance you can bind a your github account to it.
Github https://www.github.org account is free of charge and can be created within a minute. Than you just sign in to https://iffgit.fz-juelich.de/ and choose github for authentication.

In order to activate access to the QC system you have to contact QC operation team and ask to authorize you account to access a QC backend providing primary email you used for you iffgit account (either the .....@fz-juelich.de or the one you used to create your github account).

After you authorized by the QC team you can access dashboard in your profile https://www.qc.optimal-control.net:31603/profile and create a `key_id` and `key_secret` which you will use to access the api.
The secret is displayed only once. Copy it and store in safe place. Please keep your key credentials confidential and delete the keys you don't use anymore.

.. code-block:: python

    from qc_qiskit.provider import QcRemoteProvider, create_endpoint

    QC_HOST = "grpc.qc.optimal-control.net"
    QC_PORT = 31603
    # key ID and secret you created in QC dashboard
    QC_KEY_ID = "ckz....."
    QC_KEY_SECRET = "7uj5px6mYBT......."

    endpoint = create_endpoint(host=QC_HOST, port=QC_PORT, key_id=QC_KEY_ID, key_secret=QC_KEY_SECRET)

    provider = QcRemoteProvider(endpoint)
    backend = provider.get_backend()



Users guide
===========

TBD...

Pulse Gates
-----------

- Pulses will be the **envelope** of the waveform played by the AWG. The supplied pulse shape will be multiplied with a sine with the qubit frequency.
- Pulses should be in the form ``[[I_0, Q_0], ..., [I_N, Q_N]]`` for `N` samples. 
- Samples are in the range ``[-1,1]``, and denote the amplitude of the AWG relative to the maximal amplitude we set the AWG to. Negative samples indicate a pi phase shift. As a reference, you can look up the amplitude of a pi-pulse like this:

.. code-block:: python

    q1 = qiskit_backend.qc_configuration.qpu.qubits[0]
    pi_amp = q1.x180_amplitude

- The sample rate is 2.4 GSamples/s, i.e. ``N=int(TIME_IN_ns * 2.4)``

Here is an example using a pulse gate:

.. code-block:: python

    from qc_qiskit.gates import QcPulseGate
    # Sampling rate is 2.4 GSa/s
    # We play a constant waveform of 20 nanosecond
    q1 = qiskit_backend.qc_configuration.qpu.qubits[0]
    amp = q1.x180_amplitude/2 # Pi pulse is gaussian, we use a const here
    waveform = [[r, 0] for _ in range(int(20 * 2.4))]

    # The QcPulseGate below acts on 1 qubit with the waveform given as params
    # Applying the gate to qubit [0]
    pulse_gate = QcPulseGate(gate_waveform)
    circ = QuantumCircuit(1, 1)
    circ.append(pulse_gate, [0])
    circ.measure_all()

Non-Standard Gates
------------------

OpenQasm 2, and therefore also qiskit, cannot use the full capabilities of our classical control hardware. To enable using these through qiskit anyway, we define special "Gates", that are ignored during the circuit, and give instructions to the backend how to use the control electronics instead.

Attributes
^^^^^^^^^^

These instructions act _before_ execution. The OpenQasm dict looks like this:

.. code-block:: python

    {"name": "attribute_TARGET_ATTRIBUTE", "params": VALUE, "qubits": [int]}


where ``TARGET`` is either "qubit" to change qubit (drive) attributes or 
"readout" to change the readout settings for that qubit. Below is a list of ``ATTRIBUTE``.
Sweepable values are marked.

Qubit Settings
""""""""""""""

 - frequency [Hz], **sweepable** (rotating frame of the drive)
 - x180_amplitude [0-1] (amplitude of a gaussian pi pulse)
 - t1 [s] (Reset is currently done by waiting 5*t1 for thermalization)
 - t2 [s]
 - gate_time (duration of single qubit gates, in [s]

Readout Settings
""""""""""""""""

 - frequency [Hz], **sweepable**
 - duration [s] (duration of readout pulse and integration time)
 - amplitude_dBm [dBm] amplitude of the constant readout pulse in dBm
 - offset [s], **sweepable** (wait time between last command and measurement start)
 - acquisition_offset [s], **sweepable** (wait time between readout pulse start and integration start)
 - relax_time [s] (relaxation time of the resonator. Used if the backend gets the flag `thermalize=False`)
 - spectroscopy [bool] In spectroscopy mode (True), up to 18us readout are possible, but it always uses a square pulse with constant integration weight. acquisition_offset is ignored

These two are not implemented yet, but might be useful:
 - weights [waveform] Integration weights used by the hardware
 - pulse_samples [waveform] Envelope of the measurement pulse

Example
"""""""

As a convenience, we provide `QcSetQubitAttribute`, `QcSetReadoutAttribute`:

.. code-block:: python

    from qc_qiskit.gates import QcSetQubitAttribute, QcSetReadoutAttribute
    # Applying the gate to qubit [0]
    circ = QuantumCircuit(1, 1)
    circ.append(QcSetQubitAttribute("frequency", 100e6), [0])
    circ.append(QcSetReadoutAttribute("amplitude_dBm", -50), [0])

You can also bulk set many qubit and readout attributes, using the `qc_remote.configuration.Qubit/Readout` classes:

.. code-block:: python

    from qc_qiskit.gates import QcSetQubit, QcSetReadout
    from qc_remote.configuration import Qubit, Readout

    readout = Readout(
                        frequency=100e6,
                        duration=1e-6,
                        offset=None,
                        acquisition_offset=None,
                        relax_time=None,
                        spectroscopy=True,
                    )
    qubit = Qubit(frequency=200e6, readout=readout)

    # Applying the gate to qubit [0]
    circ = QuantumCircuit(1, 1)
    circ.append(QcSetReadout(readout), [0]) # This will be overridden in the next line
    circ.append(QcSetQubit(qubit), [0])


Attribute gates are ignored during execution of the circuit. As they act before compilation, only the last gate setting a certain attribute will be taken into account. There are some sanity checks, if you set unreasonable values, the backend will send back an error. You can look up the current settings using

.. code-block:: python

    q1 = qiskit_backend.qc_configuration.qpu.qubits[0]
    q1.x180_amplitude
    q1.readout.frequency

etc.

Loops
-----

Loops are an essential hardware feature missing in qiskit. As both the number and the total length of different gates is limited, loops enable large experiments and reduce the relative overhead. **If Loops are used at all, the AcquireLoop is required.**

Acquisition
^^^^^^^^^^^

This loop implements qiskit's meas_return setting: Repeat the circuit ``int:N`` times, using different averaging modes.

.. code-block:: python

    {"name": "loop_acquire", "params": [N:int, averaging_mode:str]}

allowed values for averaging_mode are "cyclic", "single". The values "sequential", "raw" and "digitized" will be possible, but are not yet implemented in qccs/our backend.

Sweeps
^^^^^^

Sweeps, and in particular frequency sweeps, reduce relative overhead by scheduling a whole batch of similar experiments at once. Sweep a specified variable ``str:name`` over a given numpy array of ``value``.

Specifying a sweep will add the `value` to an internal dictionary, with ``name`` as the key. When any gate (including attribute gates) has `name` as parameter, the `value` will be substituted and swept.

Possible extension: Our backend would allow for sweeping different variables in parallel.

Near Time Sweep
^^^^^^^^^^^^^^^

Any sweep that requires interaction with the controller pc: frequency, mixer calibration, flux offset. About 250ms overhead per point. Near time sweeps must be the outer loops, they cannot be inside real time sweeps or acquisition loops.

.. code-block:: python

    {"name": "loop_sweep_nt", "params": [name:str, np.ndarray]}


Real Time Sweep
^^^^^^^^^^^^^^^

All other sweeps (e.g. amplitudes, delay times, measurement offset). No overhead. For arbitrary arrays, only ~80 points are possible. If the array is a linspace, this restriction does not apply.

.. code-block:: python

    {"name": "loop_sweep_rt", "params": [name:str, np.ndarray]}


Closing loops
^^^^^^^^^^^^^

**All loops must be closed explicitly**

.. code-block:: python

    {"name": "loop_close", "params": []}

Example:
^^^^^^^^

This is a mock for amplitude rabi, with an additional frequency sweep:

.. code-block:: python

    from qc_qiskit import QcSweepRealTime, QcSweepNearTime, QcSetQubitAttribute, QcAcquireLoop, QcCloseLoop

    qc = QuantumCircuit(1)

    qc.append(QcSweepNearTime("FREQ", np.linspace(100e6,110e6,100)), [0])
    qc.append(QcSetQubitAttribute("frequency", "FREQ"), [0])
    qc.append(QcSweepRealTime("AMP", np.linspace(0,1,100)), [0])
    qc.append(QcAcquireLoop(2**8, "single"))
    qc.rx("AMP", 0)
    qc.measure_all()
    qc.append(QcCloseLoop(), [0])
    qc.append(QcCloseLoop(), [0])
    qc.append(QcCloseLoop(), [0])

    # Using Loops will override the shots and meas_return parameters.
    job = backend.run(frequency_sweep, shots=1 << 10, meas_return="single", qubits=[
                    0], dry_run=DRY_RUN, generate_pulse_sheet=True)


Examples
--------

Simple state discrimination experiment example

.. code-block:: python

    # %% [markdown]
    # ## Run an experiment on real hardware
    # %%
    import json
    import os

    import matplotlib.pyplot as plt
    import numpy as np
    from cattr import unstructure
    from qc_qiskit.provider import QcRemoteProvider, create_endpoint
    from qiskit import QuantumCircuit

    DRY_RUN = True  # Change to False to use the hardware
    QC_HOST = "grpc.qc.optimal-control.net"
    QC_PORT = 31603
    # good praxis is not store the key you create on https://www.qc.optimal-control.net:31603/profile 
    # in your source code but rather use files or environment variables. 
    KEY_ID = os.getenv("QC_KEY_ID")
    KEY_SECRET = os.getenv("QC_KEY_SECRET")

    endpoint = create_endpoint(host=QC_HOST, port=QC_PORT, key_id=QC_KEY_ID, key_secret=QC_KEY_SECRET)

    provider = QcRemoteProvider(endpoint)
    backend = provider.get_backend()

    # %%
    print(json.dumps(unstructure(backend.qc_configuration), indent=2))

    # %% [markdown]
    # ## measurements
    # %%
    qc = QuantumCircuit(1, 1)
    qc.measure_all()
    qc.rx(np.pi, 0)
    qc.measure_all()

    job = backend.run(
        qc,
        shots=1 << 10,
        meas_return="single",
        qubits=[0],
        dry_run=DRY_RUN,
        generate_pulse_sheet=True,
    )
    data = job.result().results[0].data.__dict__

    # %% [markdown]
    # ## Plot
    # %%

    # %%
    data_arr = [data["qubit0_readout0"], data["qubit0_readout1"]]

    for i in range(2):
        plt.scatter(data_arr[i].real, data_arr[i].imag)

Hardware limitations
--------------------

The maximal readout time is ``2.3us`` in standard mode, ``18us`` in
spectroscopy mode (``qubit.readout.spectroscopy=True``. Limits: constant
integration kernel, no delay between pulse and integration.). Per
readout line, there is a limit of about 7 different pulse shapes.

There is a hard limit of 8192 instructions for each qubit, and 1024
instructions per readout line. If either limit is exceeded, the compiler
will raise an Exception like:
``Compiler Error: program is too large to fit into memory - has 1153 instructions, maximum is 1024``
If the experiment is written as a loop, this is not a big issue. Rule of
thumb: If the compiler says maximum 1024 is the limit, there are too
many readouts. If it says 8192, there are too many gates. Waiting times
also count as gates in this context.

Visualization and Debugging
---------------------------

To visualize what the instruments are doing, use the `generate_pulse_sheet` flag:

.. code-block:: python
    
    job = backend.run(µ
            circuit,
            shots=1 << 10,
            meas_return="single",
            dry_run=True,
            qubits=[0],
            generate_pulse_sheet=True,
        )
        job_result = job.result()
        result = job_result.results[0]
        assert result.pulse_sheet is not None

The string :code:`result.pulse_sheet` can be saved as an html file
that shows the Sections and Operations defined in the experiment, along
with the timings per channel.