Source code for qibolab._core.platform.platform
"""A platform for executing quantum algorithms."""
import logging
import signal
from dataclasses import dataclass, field
from pathlib import Path
from typing import Literal, Optional
from ..components import Config
from ..components.channels import Channel
from ..execution_parameters import ExecutionParameters
from ..identifier import ChannelId, QubitId, QubitPairId, Result
from ..instruments.abstract import Controller
from ..parameters import (
InstrumentMap,
NativeGates,
Parameters,
QubitMap,
Settings,
Update,
update_configs,
)
from ..pulses import PulseId
from ..qubits import Qubit
from ..sequence import PulseSequence
from ..sweeper import ParallelSweepers
from ..unrolling import Bounds, batch
__all__ = ["Platform"]
PARAMETERS = "parameters.json"
log = logging.getLogger(__name__)
def _channels_map(elements: QubitMap) -> dict[ChannelId, QubitId]:
"""Map channel names to element (qubit or coupler)."""
return {ch: id for id, el in elements.items() for ch in el.channels}
def _unique_acquisitions(sequences: list[PulseSequence]) -> bool:
"""Check unique acquisition identifiers."""
ids = []
for seq in sequences:
ids += (p.id for _, p in seq.acquisitions)
return len(ids) == len(set(ids))
[docs]
@dataclass
class Platform:
"""Platform for controlling quantum devices."""
name: str
"""Name of the platform."""
parameters: Parameters
"""..."""
instruments: InstrumentMap
"""Mapping instrument names to
:class:`qibolab.instruments.abstract.Instrument` objects."""
qubits: QubitMap
"""Qubit controllers.
The mapped objects hold the :class:`qubit.components.channels.Channel` instances
required to send pulses addressing the desired qubits.
"""
couplers: QubitMap = field(default_factory=dict)
"""Coupler controllers.
Fully analogue to :attr:`qubits`. Only the flux channel is expected to be populated
in the mapped objects.
"""
resonator_type: Literal["2D", "3D"] = "2D"
"""Type of resonator (2D or 3D) in the used QPU."""
is_connected: bool = False
"""Flag for whether we are connected to the physical instruments."""
def __post_init__(self):
log.info("Loading platform %s", self.name)
signal.signal(signal.SIGTERM, self.termination_handler)
signal.signal(signal.SIGINT, self.termination_handler)
if self.resonator_type is None:
self.resonator_type = "3D" if self.nqubits == 1 else "2D"
def __str__(self):
return self.name
@property
def nqubits(self) -> int:
"""Total number of usable qubits in the QPU."""
return len(self.qubits)
@property
def pairs(self) -> list[QubitPairId]:
"""Available pairs in thee platform."""
return list(self.parameters.native_gates.two_qubit)
@property
def ordered_pairs(self):
"""List of qubit pairs that are connected in the QPU."""
return sorted({tuple(sorted(pair)) for pair in self.pairs})
@property
def settings(self) -> Settings:
"""Container with default execution settings."""
return self.parameters.settings
@property
def natives(self) -> NativeGates:
"""Native gates containers."""
return self.parameters.native_gates
@property
def sampling_rate(self):
"""Sampling rate of control electronics in giga samples per second
(GSps)."""
for instrument in self.instruments.values():
if isinstance(instrument, Controller):
return instrument.sampling_rate
@property
def components(self) -> set[str]:
"""Names of all components available in the platform."""
return set(self.parameters.configs.keys())
@property
def channels(self) -> dict[ChannelId, Channel]:
"""Channels in the platform."""
return {
id: ch
for instr in self.instruments.values()
if isinstance(instr, Controller)
for id, ch in instr.channels.items()
}
@property
def qubit_channels(self) -> dict[ChannelId, QubitId]:
"""Channel to qubit map."""
return _channels_map(self.qubits)
@property
def coupler_channels(self):
"""Channel to coupler map."""
return _channels_map(self.couplers)
[docs]
def config(self, name: str) -> Config:
"""Returns configuration of given component."""
# pylint: disable=unsubscriptable-object
return self.parameters.configs[name]
[docs]
def update(self, update: Update):
"""Update platform's parameters."""
self.parameters = self.parameters.replace(update)
[docs]
def connect(self):
"""Connect to all instruments."""
if not self.is_connected:
for name, instrument in self.instruments.items():
try:
instrument.connect()
except Exception as exception:
raise RuntimeError(
f"Cannot establish connection to instrument {name}. Error captured: '{exception}'",
)
self.is_connected = True
[docs]
def disconnect(self):
"""Disconnects from instruments."""
if self.is_connected:
for instrument in self.instruments.values():
instrument.disconnect()
self.is_connected = False
[docs]
def termination_handler(self, signum, frame):
self.disconnect()
raise RuntimeError(
f"Platform {self.name} disconnected because job was cancelled. Signal type: {signum}."
)
@property
def _controller(self):
"""Identify controller instrument.
Used for splitting the unrolled sequences to batches.
This method does not support platforms with more than one
controller instruments.
"""
controllers = [
instr
for instr in self.instruments.values()
if isinstance(instr, Controller)
]
assert len(controllers) == 1
return controllers[0]
def _execute(
self,
sequences: list[PulseSequence],
options: ExecutionParameters,
configs: dict[str, Config],
sweepers: list[ParallelSweepers],
) -> dict[PulseId, Result]:
"""Execute sequences on the controllers."""
result = {}
for instrument in self.instruments.values():
if isinstance(instrument, Controller):
new_result = instrument.play(configs, sequences, options, sweepers)
if isinstance(new_result, dict):
result.update(new_result)
return result
[docs]
def execute(
self,
sequences: list[PulseSequence],
sweepers: Optional[list[ParallelSweepers]] = None,
**options,
) -> dict[PulseId, Result]:
"""Execute pulse sequences.
If any sweeper is passed, the execution is performed for the different values
of sweeped parameters.
Returns readout results acquired by after execution.
Example:
.. testcode::
import numpy as np
from qibolab import Parameter, PulseSequence, Sweeper
from qibolab.instruments.dummy import create_dummy
platform = create_dummy()
qubit = platform.qubits[0]
natives = platform.natives.single_qubit[0]
sequence = natives.MZ.create_sequence()
parameter_range = np.random.randint(10, size=10)
sweeper = [
Sweeper(
parameter=Parameter.frequency,
values=parameter_range,
channels=[qubit.probe],
)
]
platform.execute([sequence], [sweeper])
"""
if sweepers is None:
sweepers = []
if not _unique_acquisitions(sequences):
raise ValueError(
"The acquisitions' identifiers have to be unique across all sequences."
)
options = self.settings.fill(ExecutionParameters(**options))
time = options.estimate_duration(sequences, sweepers)
log.info(f"Minimal execution time: {time:.3f} s")
configs = self.parameters.configs.copy()
update_configs(configs, options.updates)
# for components that represent aux external instruments (e.g. lo) to the main
# control instrument set the config directly
for name, cfg in configs.items():
if name in self.instruments:
self.instruments[name].setup(**cfg.model_dump(exclude={"kind"}))
results = {}
# pylint: disable=unsubscriptable-object
bounds = self.parameters.configs[self._controller.bounds]
assert isinstance(bounds, Bounds)
for b in batch(sequences, bounds):
results |= self._execute(b, options, configs, sweepers)
return results
[docs]
@classmethod
def load(
cls,
path: Path,
instruments: InstrumentMap,
qubits: QubitMap,
couplers: Optional[QubitMap] = None,
name: Optional[str] = None,
) -> "Platform":
"""Dump platform."""
parameters = Parameters.model_validate_json((path / PARAMETERS).read_text())
return cls(
name=name if name is not None else path.name,
parameters=parameters,
instruments=instruments,
qubits=qubits,
couplers=couplers if couplers is not None else {},
)
[docs]
def dump(self, path: Path):
"""Dump platform."""
(path / PARAMETERS).write_text(self.parameters.model_dump_json(indent=4))
def _element(self, qubit: QubitId, coupler=False) -> tuple[QubitId, Qubit]:
elements = self.qubits if not coupler else self.couplers
try:
return qubit, elements[qubit]
except KeyError:
assert isinstance(qubit, int)
return list(self.qubits.items())[qubit]
[docs]
def qubit(self, qubit: QubitId) -> tuple[QubitId, Qubit]:
"""Retrieve physical qubit name and object.
Temporary fix for the compiler to work for platforms where the
qubits are not named as 0, 1, 2, ...
"""
return self._element(qubit)
[docs]
def coupler(self, coupler: QubitId) -> tuple[QubitId, Qubit]:
"""Retrieve physical coupler name and object.
Temporary fix for the compiler to work for platforms where the
couplers are not named as 0, 1, 2, ...
"""
return self._element(coupler, coupler=True)