Skip to content

Commit

Permalink
Merge pull request #758 from qiboteam/kernel
Browse files Browse the repository at this point in the history
Kernel class
  • Loading branch information
stavros11 authored Jan 26, 2024
2 parents 3063105 + 8f49f89 commit b861009
Show file tree
Hide file tree
Showing 24 changed files with 304 additions and 106 deletions.
48 changes: 37 additions & 11 deletions doc/source/getting-started/experiment.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,27 @@ Define the platform
-------------------

To launch experiments on quantum hardware, users have first to define their platform.
A platform is composed of a python file, with instruments information, and of a runcard file, with calibration parameters.
To define a platform the user needs to provide a folder with the following structure:

.. code-block:: bash
my_platform/
platform.py
parameters.yml
kernels.npz # (optional)
where ``platform.py`` contains instruments information, ``parameters.yml``
includes calibration parameters and ``kernels.npz`` is an optional
file with additional calibration parameters.

More information about defining platforms is provided in :doc:`../tutorials/lab` and several examples can be found at `TII dedicated repository <https://github.com/qiboteam/qibolab_platforms_qrc>`_.

For a first experiment, let's define a single qubit platform at the path previously specified.
For simplicity, the qubit will be controlled by a RFSoC-based system, althought minimal changes are needed to use other devices.

.. testcode:: python

# my_platform.py
# my_platform/platform.py

import pathlib

Expand All @@ -24,14 +36,14 @@ For simplicity, the qubit will be controlled by a RFSoC-based system, althought
from qibolab.serialize import load_qubits, load_runcard, load_settings

NAME = "my_platform" # name of the platform
ADDRESS = "192.168.0.1" # ip adress of the controller
ADDRESS = "192.168.0.1" # ip address of the controller
PORT = 6000 # port of the controller

# path to runcard file with calibration parameter
RUNCARD = pathlib.Path.cwd() / "my_platform.yml"
# folder containing runcard with calibration parameters
FOLDER = pathlib.Path.cwd()


def create(runcard_path=RUNCARD):
def create(folder=FOLDER):
# Instantiate controller instruments
controller = RFSoC(NAME, ADDRESS, PORT)

Expand All @@ -42,7 +54,7 @@ For simplicity, the qubit will be controlled by a RFSoC-based system, althought
channels |= Channel("drive", port=controller[0])
# create qubit objects
runcard = load_runcard(runcard_path)
runcard = load_runcard(folder)
qubits, pairs = load_qubits(runcard)
# assign channels to qubits
qubits[0].readout = channels["L3-22_ro"]
Expand All @@ -53,7 +65,20 @@ For simplicity, the qubit will be controlled by a RFSoC-based system, althought
settings = load_settings(runcard)
return Platform(NAME, qubits, pairs, instruments, settings, resonator_type="3D")

And the we can define the runcard:
.. note::

The ``platform.py`` file must contain a ``create_function`` with the following signature:

.. code-block:: python
import pathlib
from qibolab.platform import Platform
def create(folder: Path) -> Platform:
"""Function that generates Qibolab platform."""
And the we can define the runcard ``my_platform/parameters.yml``:

.. code-block:: yaml
Expand Down Expand Up @@ -106,19 +131,20 @@ And the we can define the runcard:
Setting up the environment
--------------------------

After defining the platform, we must instruct ``qibolab`` of the location of the create file.
After defining the platform, we must instruct ``qibolab`` of the location of the platform(s).
We need to define the path that contains platform folders.
This can be done using an environment variable:
for Unix based systems:

.. code-block:: bash
export QIBOLAB_PLATFORMS=<path-to-create-file>
export QIBOLAB_PLATFORMS=<path-platform-folders>
for Windows:

.. code-block:: bash
$env:QIBOLAB_PLATFORMS="<path-to-create-file>"
$env:QIBOLAB_PLATFORMS="<path-to-platform-folders>"
To avoid having to repeat this export command for every session, this line can be added to the ``.bashrc`` file (or alternatives as ``.zshrc``).

Expand Down
4 changes: 2 additions & 2 deletions doc/source/main-documentation/qibolab.rst
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,14 @@ Following the tutorial in :doc:`/tutorials/lab`, we can continue the initializat
from pathlib import Path
from qibolab.serialize import load_qubits, load_runcard

runcard_path = Path.cwd().parent / "src" / "qibolab" / "dummy.yml"
path = Path.cwd().parent / "src" / "qibolab" / "dummy"

ch_map = ChannelMap()
ch_map |= channel1
ch_map |= channel2
ch_map |= channel3
runcard = load_runcard(runcard_path)
runcard = load_runcard(path)
qubits, couplers, pairs = load_qubits(runcard)

qubits[0].drive = channel1
Expand Down
16 changes: 8 additions & 8 deletions doc/source/tutorials/lab.rst
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ the above runcard:
from qibolab.instruments.dummy import DummyInstrument


def create():
def create(folder: Path):
# Create a controller instrument
instrument = DummyInstrument("my_instrument", "0.0.0.0:0")

Expand All @@ -422,7 +422,7 @@ the above runcard:
channels |= Channel("chf2", port=instrument["o5"])
# create ``Qubit`` and ``QubitPair`` objects by loading the runcard
runcard = load_runcard(Path(__file__).parent / "my_platform.yml")
runcard = load_runcard(folder)
qubits, couplers, pairs = load_qubits(runcard)

# assign channels to the qubit
Expand All @@ -444,7 +444,7 @@ With the following additions for coupler architectures:

.. testcode:: python

def create():
def create(folder):
# Create a controller instrument
instrument = DummyInstrument("my_instrument", "0.0.0.0:0")

Expand All @@ -459,7 +459,7 @@ With the following additions for coupler architectures:
channels |= Channel("chfc0", port=instrument["o6"])
# create ``Qubit`` and ``QubitPair`` objects by loading the runcard
runcard = load_runcard(Path(__file__).parent / "my_platform.yml")
runcard = load_runcard(folder)
qubits, couplers, pairs = load_qubits(runcard)

# assign channels to the qubit
Expand All @@ -486,8 +486,8 @@ With the following additions for coupler architectures:
couplers=couplers,
)

Note that this assumes that the runcard is saved as ``my_platform.yml`` in the
same directory with the Python file that contains ``create()``.
Note that this assumes that the runcard is saved as ``<folder>/parameters.yml`` where ``<folder>``
is the directory containing ``platform.py``.


Instrument settings
Expand Down Expand Up @@ -612,7 +612,7 @@ in this case ``"twpa_pump"``.
from qibolab.instruments.oscillator import LocalOscillator


def create():
def create(folder: Path):
# Create a controller instrument
instrument = DummyInstrument("my_instrument", "0.0.0.0:0")
twpa = LocalOscillator("twpa_pump", "0.0.0.1")
Expand All @@ -625,7 +625,7 @@ in this case ``"twpa_pump"``.
channels |= Channel("ch1in", port=instrument["i1"])
# create ``Qubit`` and ``QubitPair`` objects by loading the runcard
runcard = load_runcard(Path(__file__).parent / "my_platform.yml")
runcard = load_runcard(folder)
qubits, pairs = load_qubits(runcard)

# assign channels to the qubit
Expand Down
12 changes: 7 additions & 5 deletions src/qibolab/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ExecutionParameters,
)
from qibolab.platform import Platform
from qibolab.serialize import PLATFORM

__version__ = im.version(__package__)

Expand All @@ -29,13 +30,14 @@ def get_platforms_path():
return Path(profiles)


def create_platform(name, runcard=None):
def create_platform(name, path: Path = None) -> Platform:
"""A platform for executing quantum algorithms.
It consists of a quantum processor QPU and a set of controlling instruments.
Args:
name (str): name of the platform. Options are 'tiiq', 'qili' and 'icarusq'.
path (pathlib.Path): path with platform serialization
Returns:
The plaform class.
"""
Expand All @@ -44,17 +46,17 @@ def create_platform(name, runcard=None):

return create_dummy(with_couplers=name == "dummy_couplers")

platform = get_platforms_path() / f"{name}.py"
platform = get_platforms_path() / f"{name}"
if not platform.exists():
raise_error(ValueError, f"Platform {name} does not exist.")

spec = importlib.util.spec_from_file_location("platform", platform)
spec = importlib.util.spec_from_file_location("platform", platform / PLATFORM)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)

if runcard is None:
if path is None:
return module.create()
return module.create(runcard)
return module.create(path)


def execute_qasm(circuit: str, platform, runcard=None, initial_state=None, nshots=1000):
Expand Down
1 change: 1 addition & 0 deletions src/qibolab/dummy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .platform import create_dummy
Binary file added src/qibolab/dummy/kernels.npz
Binary file not shown.
File renamed without changes.
14 changes: 7 additions & 7 deletions src/qibolab/dummy.py → src/qibolab/dummy/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

from qibolab.channels import Channel, ChannelMap
from qibolab.instruments.dummy import DummyInstrument, DummyLocalOscillator
from qibolab.kernels import Kernels
from qibolab.platform import Platform
from qibolab.serialize import load_qubits, load_runcard, load_settings

FOLDER = pathlib.Path(__file__).parent


def remove_couplers(runcard):
"""Remove coupler sections from runcard to create a dummy platform without
Expand All @@ -21,11 +24,6 @@ def remove_couplers(runcard):
return runcard


def load_dummy_runcard():
"""Loads the runcard YAML of the dummy platform."""
return load_runcard(pathlib.Path(__file__).parent / "dummy.yml")


def create_dummy(with_couplers: bool = True):
"""Create a dummy platform using the dummy instrument.
Expand All @@ -40,7 +38,9 @@ def create_dummy(with_couplers: bool = True):
twpa_pump.frequency = 1e9
twpa_pump.power = 10

runcard = load_dummy_runcard()
runcard = load_runcard(FOLDER)
kernels = Kernels.load(FOLDER)

if not with_couplers:
runcard = remove_couplers(runcard)

Expand All @@ -64,7 +64,7 @@ def create_dummy(with_couplers: bool = True):
channels["readout"].attenuation = 0
channels["twpa"].local_oscillator = twpa_pump

qubits, couplers, pairs = load_qubits(runcard)
qubits, couplers, pairs = load_qubits(runcard, kernels)
settings = load_settings(runcard)

# map channels to qubits
Expand Down
21 changes: 8 additions & 13 deletions src/qibolab/instruments/zhinst.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import os
from collections import defaultdict
from dataclasses import dataclass, replace
from pathlib import Path
from typing import Dict, List, Tuple, Union

import laboneq._token
Expand Down Expand Up @@ -318,7 +317,6 @@ def __init__(
self.smearing = smearing
self.chip = "iqm5q"
"Parameters read from the runcard not part of ExecutionParameters"
self.kernels = defaultdict(Path)

self.exp = None
self.experiment = None
Expand Down Expand Up @@ -442,17 +440,14 @@ def register_readout_line(self, qubit, intermediate_frequency, options):
f"q{q}"
].logical_signals["acquire_line"]

if qubit.kernel_path:
self.kernels[q] = qubit.kernel_path

oscillator = lo.Oscillator(
frequency=intermediate_frequency,
modulation_type=lo.ModulationType.SOFTWARE,
)
threshold = None

if options.acquisition_type == AcquisitionType.DISCRIMINATION:
if self.kernels[q].is_file():
if qubit.kernel is not None:
# Kernels don't work with the software modulation on the acquire signal
oscillator = None
else:
Expand Down Expand Up @@ -1039,15 +1034,15 @@ def measure_relax(self, exp, qubits, couplers, relaxation_time, acquisition_type
iq_angle_readout_schedule[i].append(iq_angle)

weights = {}
for i, (pulses, qubits, iq_angles) in enumerate(
for i, (pulses, qubits_readout, iq_angles) in enumerate(
zip(
readout_schedule.values(),
qubit_readout_schedule.values(),
iq_angle_readout_schedule.values(),
)
):
qd_finish = self.find_subsequence_finish(i, "drive", qubits)
qf_finish = self.find_subsequence_finish(i, "flux", qubits)
qd_finish = self.find_subsequence_finish(i, "drive", qubits_readout)
qf_finish = self.find_subsequence_finish(i, "flux", qubits_readout)
cf_finish = self.find_subsequence_finish(i, "couplerflux", couplers)
finish_times = np.array(
[
Expand All @@ -1064,7 +1059,7 @@ def measure_relax(self, exp, qubits, couplers, relaxation_time, acquisition_type
play_after = f"sequence_{latest_sequence['line']}_{i}"
# Section on the outside loop allows for multiplex
with exp.section(uid=f"sequence_measure_{i}", play_after=play_after):
for pulse, q, iq_angle in zip(pulses, qubits, iq_angles):
for pulse, q, iq_angle in zip(pulses, qubits_readout, iq_angles):
pulse.zhpulse.uid += str(i)

exp.delay(
Expand All @@ -1073,13 +1068,13 @@ def measure_relax(self, exp, qubits, couplers, relaxation_time, acquisition_type
)

if (
self.kernels[q].is_file()
qubits[q].kernel is not None
and acquisition_type == lo.AcquisitionType.DISCRIMINATION
):
kernels = np.load(self.kernels[q])
kernel = qubits[q].kernel
weight = lo.pulse_library.sampled_pulse_complex(
uid="weight" + str(q),
samples=kernels[str(q)] * np.exp(1j * iq_angle),
samples=kernel * np.exp(1j * iq_angle),
)

else:
Expand Down
39 changes: 39 additions & 0 deletions src/qibolab/kernels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import json
from pathlib import Path

import numpy as np

from qibolab.qubits import QubitId

KERNELS = "kernels.npz"


class Kernels(dict[QubitId, np.ndarray]):
"""A dictionary subclass for handling Qubit Kernels.
This class extends the built-in dict class and maps QubitId to numpy
arrays. It provides methods to load and dump the kernels from and to
a file.
"""

@classmethod
def load(cls, path: Path):
"""Class method to load kernels from a file.
The file should contain a serialized dictionary where keys are
serialized QubitId and values are numpy arrays.
"""
return cls(
{json.loads(key): value for key, value in np.load(path / KERNELS).items()}
)

def dump(self, path: Path):
"""Instance method to dump the kernels to a file.
The keys (QubitId) are serialized to strings and the values
(numpy arrays) are kept as is.
"""
np.savez(
path / KERNELS,
**{json.dumps(qubit_id): value for qubit_id, value in self.items()}
)
Loading

0 comments on commit b861009

Please sign in to comment.