Skip to content

Commit

Permalink
Merge branch 'main' into qmlotrig
Browse files Browse the repository at this point in the history
  • Loading branch information
stavros11 committed Mar 29, 2024
2 parents 2914c13 + abe3698 commit 6bd10b3
Show file tree
Hide file tree
Showing 4 changed files with 162 additions and 52 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ repos:
additional_dependencies: [tomli]
args: [--in-place, --config, ./pyproject.toml]
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.1
rev: v3.15.2
hooks:
- id: pyupgrade
- repo: https://github.com/hadialqattan/pycln
Expand Down
68 changes: 68 additions & 0 deletions src/qibolab/instruments/bluefors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import socket

import yaml
from qibo.config import log

from qibolab.instruments.abstract import Instrument


class TemperatureController(Instrument):
"""Bluefors temperature controller.
```
# Example usage
if __name__ == "__main__":
tc = TemperatureController("XLD1000_Temperature_Controller", "192.168.0.114", 8888)
tc.connect()
temperature_values = tc.read_data()
for temperature_value in temperature_values:
print(temperature_value)
```
"""

def __init__(self, name: str, address: str, port: int = 8888):
"""Creation of the controller object.
Args:
name (str): name of the instrument.
address (str): IP address of the board sending cryo temperature data.
port (int): port of the board sending cryo temperature data.
"""
super().__init__(name, address)
self.port = port
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

def connect(self):
"""Connect to the socket."""
if self.is_connected:
return
log.info(f"Bluefors connection. IP: {self.address} Port: {self.port}")
self.client_socket.connect((self.address, self.port))
self.is_connected = True
log.info("Bluefors Temperature Controller Connected")

def disconnect(self):
"""Disconnect from the socket."""
if self.is_connected:
self.client_socket.close()
self.is_connected = False

def setup(self):
"""Required by parent class, but not used here."""

def get_data(self) -> dict[str, dict[str, float]]:
"""Connect to the socket and get temperature data.
The typical message looks like this:
flange_name: {'temperature':12.345678, 'timestamp':1234567890.123456}
`timestamp` can be converted to datetime using `datetime.fromtimestamp`.
Returns:
message (dict[str, dict[str, float]]): socket message in this format:
{"flange_name": {'temperature': <value(float)>, 'timestamp':<value(float)>}}
"""
return yaml.safe_load(self.client_socket.recv(1024).decode())

def read_data(self):
"""Continously read data from the temperature controller."""
while True:
yield self.get_data()
98 changes: 47 additions & 51 deletions src/qibolab/instruments/zhinst/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from qibolab.couplers import Coupler
from qibolab.instruments.abstract import Controller
from qibolab.instruments.port import Port
from qibolab.pulses import PulseSequence, PulseType
from qibolab.pulses import FluxPulse, PulseSequence, PulseType
from qibolab.qubits import Qubit
from qibolab.sweeper import Parameter, Sweeper
from qibolab.unrolling import Bounds
Expand Down Expand Up @@ -114,6 +114,8 @@ def __init__(self, name, device_setup, time_of_flight=0.0, smearing=0.0):
"Zurich pulse sequence"
self.sub_sequences: list[SubSequence] = []
"Sub sequences between each measurement"
self.unsplit_channels: set[str] = set()
"Names of channels that were not split into sub-sequences"

self.processed_sweeps: Optional[ProcessedSweeps] = None
self.nt_sweeps: list[Sweeper] = []
Expand Down Expand Up @@ -307,8 +309,14 @@ def frequency_from_pulses(qubits, sequence):
if pulse.type is PulseType.DRIVE:
qubit.drive_frequency = pulse.frequency

def create_sub_sequences(self, qubits: list[Qubit]) -> list[SubSequence]:
"""Create subsequences based on locations of measurements."""
def create_sub_sequences(
self, qubits: list[Qubit]
) -> tuple[list[SubSequence], set[str]]:
"""Create subsequences based on locations of measurements.
Returns list of subsequences and a set of channel names that
were not split
"""
measure_channels = {measure_channel_name(qb) for qb in qubits}
other_channels = set(self.sequence.keys()) - measure_channels

Expand All @@ -317,17 +325,36 @@ def create_sub_sequences(self, qubits: list[Qubit]) -> list[SubSequence]:
for i, pulse in enumerate(self.sequence[ch]):
measurement_groups[i].append((ch, pulse))

measurement_starts = {}
measurement_start_end = {}
for i, group in measurement_groups.items():
starts = np.array([meas.pulse.start for _, meas in group])
measurement_starts[i] = max(starts)

# split all non-measurement channels according to the locations of the measurements
ends = np.array([meas.pulse.finish for _, meas in group])
measurement_start_end[i] = (
max(starts),
max(ends),
) # max is intended for float arithmetic errors only

# FIXME: this is a hotfix specifically made for any flux experiments in flux pulse mode, where the flux
# pulses extend through the entire duration of the experiment. This should be removed once the sub-sequence
# splitting logic is removed from the driver.
channels_overlapping_measurement = set()
if len(measurement_groups) == 1:
for ch in other_channels:
for pulse in self.sequence[ch]:
if not isinstance(pulse.pulse, FluxPulse):
break
start, end = measurement_start_end[0]
if pulse.pulse.start < end and pulse.pulse.finish > start:
channels_overlapping_measurement.add(ch)
break

# split non-measurement channels according to the locations of the measurements
sub_sequences = defaultdict(lambda: defaultdict(list))
for ch in other_channels:
for ch in other_channels - channels_overlapping_measurement:
measurement_index = 0
for pulse in self.sequence[ch]:
if pulse.pulse.finish > measurement_starts[measurement_index]:
start, _ = measurement_start_end[measurement_index]
if pulse.pulse.finish > start:
measurement_index += 1
sub_sequences[measurement_index][ch].append(pulse)
if len(sub_sequences) > len(measurement_groups):
Expand All @@ -336,7 +363,7 @@ def create_sub_sequences(self, qubits: list[Qubit]) -> list[SubSequence]:
return [
SubSequence(measurement_groups[i], sub_sequences[i])
for i in range(len(measurement_groups))
]
], channels_overlapping_measurement

def experiment_flow(
self,
Expand All @@ -356,7 +383,9 @@ def experiment_flow(
sequence (PulseSequence): sequence of pulses to be played in the experiment.
"""
self.sequence = self.sequence_zh(sequence, qubits)
self.sub_sequences = self.create_sub_sequences(list(qubits.values()))
self.sub_sequences, self.unsplit_channels = self.create_sub_sequences(
list(qubits.values())
)
self.calibration_step(qubits, couplers, options)
self.create_exp(qubits, options)

Expand Down Expand Up @@ -532,6 +561,13 @@ def get_channel_node_path(self, channel_name: str) -> str:

def select_exp(self, exp, qubits, exp_options):
"""Build Zurich Experiment selecting the relevant sections."""
# channels that were not split are just applied in parallel to the rest of the experiment
with exp.section(uid="unsplit_channels"):
for ch in self.unsplit_channels:
for pulse in self.sequence[ch]:
exp.delay(signal=ch, time=pulse.pulse.start)
self.play_sweep(exp, ch, pulse)

weights = {}
previous_section = None
for i, seq in enumerate(self.sub_sequences):
Expand Down Expand Up @@ -662,50 +698,12 @@ def play_sweep(exp, channel_name, pulse):

exp.play(signal=channel_name, pulse=pulse.zhpulse, **play_parameters)

@staticmethod
def rearrange_rt_sweepers(
sweepers: list[Sweeper],
) -> tuple[Optional[tuple[int, int]], list[Sweeper]]:
"""Rearranges list of real-time sweepers based on hardware limitations.
The only known limitation currently is that frequency sweepers must be applied before (on the outer loop) other
(e.g. amplitude) sweepers. Consequently, the only thing done here is to swap the frequency sweeper with the
first sweeper in the list.
Args:
sweepers: Sweepers to rearrange.
Returns:
swapped_axis_pair: tuple containing indices of the two swapped axes, or None if nothing to rearrange.
sweepers: rearranged (or original, if nothing to rearrange) list of sweepers.
"""
freq_sweeper = next(
iter(s for s in sweepers if s.parameter is Parameter.frequency), None
)
if freq_sweeper:
sweepers_copy = sweepers.copy()
freq_sweeper_idx = sweepers_copy.index(freq_sweeper)
sweepers_copy[freq_sweeper_idx] = sweepers_copy[0]
sweepers_copy[0] = freq_sweeper
log.warning("Sweepers were reordered")
return (0, freq_sweeper_idx), sweepers_copy
return None, sweepers

def sweep(self, qubits, couplers, sequence: PulseSequence, options, *sweepers):
"""Play pulse and sweepers sequence."""

self.signal_map = {}
self.processed_sweeps = ProcessedSweeps(sweepers, qubits)
self.nt_sweeps, self.rt_sweeps = classify_sweepers(sweepers)
swapped_axis_pair, self.rt_sweeps = self.rearrange_rt_sweepers(self.rt_sweeps)
if swapped_axis_pair:
# 1. axes corresponding to NT sweeps appear before axes corresponding to RT sweeps
# 2. in singleshot mode, the first axis contains shots, i.e.: (nshots, sweeper_1, sweeper_2)
axis_offset = len(self.nt_sweeps) + int(
options.averaging_mode is AveragingMode.SINGLESHOT
)
swapped_axis_pair = tuple(ax + axis_offset for ax in swapped_axis_pair)

self.frequency_from_pulses(qubits, sequence)

self.acquisition_type = None
Expand All @@ -725,8 +723,6 @@ def sweep(self, qubits, couplers, sequence: PulseSequence, options, *sweepers):
for i, ropulse in enumerate(self.sequence[measure_channel_name(qubit)]):
data = self.results.get_data(f"sequence{q}_{i}")

if swapped_axis_pair:
data = np.moveaxis(data, swapped_axis_pair[0], swapped_axis_pair[1])
if options.acquisition_type is AcquisitionType.DISCRIMINATION:
data = (
np.ones(data.shape) - data.real
Expand Down
46 changes: 46 additions & 0 deletions tests/test_instruments_bluefors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from unittest import mock

import pytest
import yaml

from qibolab.instruments.bluefors import TemperatureController

messages = [
"4K-flange: {'temperature':3.065067, 'timestamp':1710912431.128234}",
"""50K-flange: {'temperature':35.733738, 'timestamp':1710956419.545651}
4K-flange: {'temperature':3.065067, 'timestamp':1710955431.128234}""",
]


def test_connect():
with mock.patch("socket.socket"):
tc = TemperatureController("Test_Temperature_Controller", "")
assert tc.is_connected is False
# if already connected, it should stay connected
for _ in range(2):
tc.connect()
assert tc.is_connected is True


@pytest.mark.parametrize("already_connected", [True, False])
def test_disconnect(already_connected):
with mock.patch("socket.socket"):
tc = TemperatureController("Test_Temperature_Controller", "")
if not already_connected:
tc.connect()
# if already disconnected, it should stay disconnected
for _ in range(2):
tc.disconnect()
assert tc.is_connected is False


def test_continuously_read_data():
with mock.patch(
"qibolab.instruments.bluefors.TemperatureController.get_data",
new=lambda _: yaml.safe_load(messages[0]),
):
tc = TemperatureController("Test_Temperature_Controller", "")
read_temperatures = tc.read_data()
for read_temperature in read_temperatures:
assert read_temperature == yaml.safe_load(messages[0])
break

0 comments on commit 6bd10b3

Please sign in to comment.