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

Kernel class #758

Merged
merged 34 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
dabb583
Add support for kernels in dummy platform
Jacfomg Jan 16, 2024
c540535
fix: Update Zurich instrument and qubit objects
Jacfomg Jan 16, 2024
ab5b64c
fix: key error if not all kernels
Jacfomg Jan 16, 2024
92e20eb
fix: qubit kernel assignment and serialization
Jacfomg Jan 16, 2024
8f8f0f6
Update kernels.npz file
Jacfomg Jan 16, 2024
e3e0a8c
fix: Refactor kernel loading and saving
Jacfomg Jan 16, 2024
dac2474
Refactor code to simplify handling of kernel assignment
Jacfomg Jan 16, 2024
cff8946
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 16, 2024
5980fe7
Add extras folder to load_qubits
Jacfomg Jan 16, 2024
2cc1048
Update src/qibolab/serialize.py
Jacfomg Jan 16, 2024
0c5a7ab
Update src/qibolab/serialize.py
Jacfomg Jan 17, 2024
b383fa2
Update src/qibolab/serialize.py
Jacfomg Jan 17, 2024
f7e73e1
Update tests/dummy_qrc/zurich.py
Jacfomg Jan 17, 2024
5899914
Update src/qibolab/instruments/zhinst.py
Jacfomg Jan 17, 2024
f4ec7b4
Update src/qibolab/instruments/zhinst.py
Jacfomg Jan 17, 2024
a32876c
Update src/qibolab/instruments/zhinst.py
Jacfomg Jan 17, 2024
e216176
Update src/qibolab/dummy.py
Jacfomg Jan 17, 2024
0f3ca6d
Update src/qibolab/kernels.py
Jacfomg Jan 17, 2024
7f7bee9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 17, 2024
e12a93e
fix: Refactor kernel saving and loading
Jacfomg Jan 17, 2024
b0b24f9
fix: path with platform name for Kernels
Jacfomg Jan 19, 2024
9f37fd8
refactor: Introduce folders for platform serialization
andrea-pasquale Jan 24, 2024
1e6a7dc
fix: Fix test in doc
andrea-pasquale Jan 24, 2024
3636038
refactor: Standardize file names
andrea-pasquale Jan 24, 2024
4b8a505
fix: Fix doctest
andrea-pasquale Jan 24, 2024
55723ed
fix: Fix tests related to qibosoq 0.1.1
andrea-pasquale Jan 25, 2024
26e30d8
doc: Improve documentation following new folder structure
andrea-pasquale Jan 25, 2024
d33823c
doc: Update QIBOLAB_PLATFORMS meaning
andrea-pasquale Jan 25, 2024
fa13c5d
fix: Usage of runcard folders in docs
alecandido Jan 25, 2024
d869de0
fix: Postpone more json occurrences
alecandido Jan 25, 2024
c13a8a7
Update doc/source/tutorials/lab.rst
alecandido Jan 26, 2024
5e46e6f
fix: Fix dummy interface
andrea-pasquale Jan 26, 2024
a3fd68f
refactor: Dump kernels only if they are not empty
andrea-pasquale Jan 26, 2024
8f49f89
test: Implement additional tests dumping and loading
andrea-pasquale Jan 26, 2024
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
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.
Copy link
Member

Choose a reason for hiding this comment

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

In principle, parameters.yml is also optional, as someone can define the qubit/channel objects hardcoded in the create. This is basically explained in the lab.rst tutorial.

However, dumping the platform after will automatically create parameters, so I am not sure whether we should advocate this kind of usage.


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:
Copy link
Member

Choose a reason for hiding this comment

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

I have the impression that the path argument is no longer required here. Before it was used to link a YAML to the create (assuming you wanted to use different YAMLs for the same create?). But this is no longer valid given that we’re proposing a fixed folder structure.

However, this will become more clear once we try to define some actual platforms (eg in qibolab_platforms_qrc).

"""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
Loading