Skip to content

Commit

Permalink
[QHC-778] Experiment improvements from HW feedback (#819)
Browse files Browse the repository at this point in the history
  • Loading branch information
fedonman authored Oct 14, 2024
1 parent 276df05 commit e3b5e77
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 72 deletions.
21 changes: 10 additions & 11 deletions docs/releases/changelog-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,16 @@
experiment.execute_qprogram(lambda: amplitude=amplitude: get_qprogram(amplitude, 2000))
```

[#814](https://github.com/qilimanjaro-tech/qililab/pull/814)
[#814](https://github.com/qilimanjaro-tech/qililab/pull/814)

- Added offset set and get for quantum machines (both OPX+ and OPX1000). For hardware loops there is `qp.set_offset(bus: str, offset_path0: float, offset_path1: float | None)` where `offset_path0` is a mandatory field (for flux, drive and readout lines) and `offset_path1` is only used when changing the offset of buses that have to IQ lines (drive and readout). For software loops there is `platform.set_parameter(alias=bus_name, parameter=ql.Parameter.OFFSET_PARAMETER, value=offset_value)`. The possible arguments for `ql.Parameter` are: `DC_OFFSET` (flux lines), `OFFSET_I` (I lines for IQ buses), `OFFSET_Q` (Q lines for IQ buses), `OFFSET_OUT1` (output 1 lines for readout lines), `OFFSET_OUT2` (output 2 lines for readout lines).

[#791](https://github.com/qilimanjaro-tech/qililab/pull/791)
[#791](https://github.com/qilimanjaro-tech/qililab/pull/791)

### Improvements

- Legacy linting and formatting tools such as pylint, flake8, isort, bandit, and black have been removed. These have been replaced with Ruff, a more efficient tool that handles both linting and formatting. All configuration settings have been consolidated into the `pyproject.toml` file, simplifying the project's configuration and maintenance. Integration config files like `pre-commit-config.yaml` and `.github/workflows/code_quality.yml` have been updated accordingly. Several rules from Ruff have also been implemented to improve code consistency and quality across the codebase. Additionally, the development dependencies in `dev-requirements.txt` have been updated to their latest versions, ensuring better compatibility and performance.
[#813](https://github.com/qilimanjaro-tech/qililab/pull/813)
- Legacy linting and formatting tools such as pylint, flake8, isort, bandit, and black have been removed. These have been replaced with Ruff, a more efficient tool that handles both linting and formatting. All configuration settings have been consolidated into the `pyproject.toml` file, simplifying the project's configuration and maintenance. Integration config files like `pre-commit-config.yaml` and `.github/workflows/code_quality.yml` have been updated accordingly. Several rules from Ruff have also been implemented to improve code consistency and quality across the codebase. Additionally, the development dependencies in `dev-requirements.txt` have been updated to their latest versions, ensuring better compatibility and performance. [#813](https://github.com/qilimanjaro-tech/qililab/pull/813)

- `platform.execute_experiment()` and the underlying `ExperimentExecutor` can now handle experiments with multiple qprograms and multiple measurements. Parallel loops are also supported in both experiment and qprogram. The structure of the HDF5 results file as well as the functionality of `ExperimentResults` class have been changed accordingly.
[#796](https://github.com/qilimanjaro-tech/qililab/pull/796)
- `platform.execute_experiment()` and the underlying `ExperimentExecutor` can now handle experiments with multiple qprograms and multiple measurements. Parallel loops are also supported in both experiment and qprogram. The structure of the HDF5 results file as well as the functionality of `ExperimentResults` class have been changed accordingly. [#796](https://github.com/qilimanjaro-tech/qililab/pull/796)

- Added pulse distorsions in `execute_qprogram` for QBlox in a similar methodology to the distorsions implemented in pulse circuits. The runcard needs to contain the same structure for distorsions as the runcards for circuits and the code will modify the waveforms after compilation (inside `platform.execute_qprogram`).

Expand Down Expand Up @@ -108,6 +105,10 @@

[#817](https://github.com/qilimanjaro-tech/qililab/pull/817)

- Introduced settable attributes `experiment_results_base_path` and `experiment_results_path_format` in the `Platform` class. These attributes determine the directory and file structure for saving experiment results during execution. By default, results are stored within `experiment_results_base_path` following the format `{date}/{time}/{label}.h5`. [#819](https://github.com/qilimanjaro-tech/qililab/pull/819)

- Added a `save_plot=True` parameter to the `plotS21()` method of `ExperimentResults`. When enabled (default: True), the plot is automatically saved in the same directory as the experiment results. [#819](https://github.com/qilimanjaro-tech/qililab/pull/819)

### Breaking changes

### Deprecations / Removals
Expand All @@ -116,8 +117,6 @@

### Bug fixes

- Fixed typo in ExceptionGroup import statement for python 3.11+
[#808](https://github.com/qilimanjaro-tech/qililab/pull/808)
- Fixed typo in ExceptionGroup import statement for python 3.11+ [#808](https://github.com/qilimanjaro-tech/qililab/pull/808)

- Fixed serialization/deserialization of lambda functions, mainly used in `experiment.execute_qprogram()` method. The fix depends on the `dill` library which is added as requirement.
[#815](https://github.com/qilimanjaro-tech/qililab/pull/815)
- Fixed serialization/deserialization of lambda functions, mainly used in `experiment.execute_qprogram()` method. The fix depends on the `dill` library which is added as requirement. [#815](https://github.com/qilimanjaro-tech/qililab/pull/815)
22 changes: 14 additions & 8 deletions src/qililab/platform/platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import ast
import io
import re
import tempfile
from contextlib import contextmanager
from copy import deepcopy
from dataclasses import asdict
Expand Down Expand Up @@ -326,6 +327,12 @@ def __init__(self, runcard: Runcard):
self._qpy_sequence_cache: dict[str, str] = {}
"""Dictionary for caching qpysequences."""

self.experiment_results_base_path: str = tempfile.gettempdir()
"""Base path for saving experiment results."""

self.experiment_results_path_format: str = "{date}/{time}/{label}.h5"
"""Format of the experiment results path."""

def connect(self):
"""Connects to all the instruments and blocks the connection for other users.
Expand Down Expand Up @@ -723,14 +730,13 @@ def execute_anneal_program(
return self.execute_qprogram(qprogram=qp_annealing, calibration=calibration, debug=debug)
raise ValueError("The calibrated measurement is not present in the calibration file.")

def execute_experiment(self, experiment: Experiment, base_data_path: str) -> str:
def execute_experiment(self, experiment: Experiment) -> str:
"""Executes a quantum experiment on the platform.
This method manages the execution of a given `Experiment` on the platform by utilizing an `ExperimentExecutor`. It orchestrates the entire process, including traversing the experiment's structure, handling loops and operations, and streaming results in real-time to ensure data integrity. The results are saved in a timestamped directory within the specified `base_data_path`.
Args:
experiment (Experiment): The experiment object defining the sequence of operations and loops.
base_data_path (str): The base directory path where the experiment results will be stored.
Returns:
str: The path to the file where the results are stored.
Expand All @@ -741,23 +747,23 @@ def execute_experiment(self, experiment: Experiment, base_data_path: str) -> str
from qililab import Experiment
# Initialize your experiment
experiment = Experiment()
experiment = Experiment(label="my_experiment")
# Add variables, loops, and operations to the experiment
# ...
# Define the base data path for storing results
base_data_path = "/data/experiments"
# Define the base path for storing experiment results
platform.experiment_results_base_path = "/data/experiments"
# Execute the experiment on the platform
results_path = platform.execute_experiment(experiment=experiment, base_data_path=base_data_path)
results_path = platform.execute_experiment(experiment=experiment)
print(f"Results saved to {results_path}")
Note:
- Ensure that the experiment is properly configured before execution.
- The results will be saved in a timestamped directory within the `base_data_path`.
- The results will be saved in a directory within the `experiment_results_base_path` according to the `platform.experiment_results_path_format`. The default format is `{date}/{time}/{label}.h5`.
- This method handles the setup and execution internally, providing a simplified interface for experiment execution.
"""
executor = ExperimentExecutor(platform=self, experiment=experiment, base_data_path=base_data_path)
executor = ExperimentExecutor(platform=self, experiment=experiment)
return executor.execute()

def compile_qprogram(
Expand Down
4 changes: 4 additions & 0 deletions src/qililab/qprogram/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ class Experiment(StructuredProgram):
{Parameter.DRAG_COEFFICIENT: float, Parameter.THRESHOLD: float, Parameter.THRESHOLD_ROTATION: float}
)

def __init__(self, label: str) -> None:
super().__init__()
self.label: str = label

def get_parameter(self, alias: str, parameter: Parameter, channel_id: int | None = None):
"""Set a platform parameter.
Expand Down
109 changes: 75 additions & 34 deletions src/qililab/qprogram/experiment_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
# mypy: disable-error-code="union-attr, arg-type"
import inspect
import os
import threading
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from datetime import datetime
from time import perf_counter
Expand Down Expand Up @@ -83,7 +85,7 @@ class ExperimentExecutor:
platform = build_platform(runcard="path/to/runcard.yml")
# Define your experiment
experiment = Experiment()
experiment = Experiment(label="my_experiment")
# Add blocks, loops, operations to the experiment
# ...
Expand All @@ -102,10 +104,9 @@ class ExperimentExecutor:
- The results will be saved in a timestamped directory within the `base_data_path`.
"""

def __init__(self, platform: "Platform", experiment: Experiment, base_data_path: str):
def __init__(self, platform: "Platform", experiment: Experiment):
self.platform = platform
self.experiment = experiment
self.base_data_path = base_data_path

# Registry of all variables used in the experiment with their labels and values
self._all_variables: dict = defaultdict(lambda: {"label": None, "values": {}})
Expand All @@ -132,12 +133,12 @@ def __init__(self, platform: "Platform", experiment: Experiment, base_data_path:
self._shots = 1

# Metadata dictionary containing information about the experiment structure and variables.
self._metadata: ExperimentMetadata = ExperimentMetadata(qprograms={})
self._metadata: ExperimentMetadata

# ExperimentResultsWriter object responsible for saving experiment results to file in real-time.
self._results_writer: ExperimentResultsWriter

def _prepare_metadata(self):
def _prepare_metadata(self, executed_at: datetime):
"""Prepares the loop values and result shape before execution."""

def traverse_experiment(block: Block):
Expand Down Expand Up @@ -226,6 +227,13 @@ def finalize_measurement_structure():
# Increase index of measurements
self._measurement_index += 1

self._metadata = ExperimentMetadata(
platform=serialize(self.platform.to_dict()),
experiment=serialize(self.experiment),
executed_at=executed_at,
execution_time=0.0,
qprograms={},
)
traverse_experiment(self.experiment.body)
self._all_variables = dict(self._all_variables)

Expand Down Expand Up @@ -491,21 +499,43 @@ def _get_variables_of_loop(self, block: Loop | ForLoop | Parallel) -> list[Varia

return list(variables.values())

def _create_results_path(self, source: str, file: str):
# Get the current date and time
now = datetime.now()
def _create_results_path(self, executed_at: datetime):
# Get base path and path format from platform
base_path = self.platform.experiment_results_base_path
path_format = self.platform.experiment_results_path_format

# Format date and time for directory names
date = now.strftime("%Y%m%d")
timestamp = now.strftime("%H%M%S")
date = executed_at.strftime("%Y%m%d")
timestamp = executed_at.strftime("%H%M%S")
label = self.experiment.label

# Construct the directory path
folder = os.path.join(source, date, timestamp)
# Format the path based on the path's format
path = path_format.format(date=date, time=timestamp, label=label)

# Construct the full path
path = os.path.join(base_path, path)

# Ensure it is an absolute path
path = os.path.abspath(path)

# Create the directories if they don't exist
os.makedirs(folder, exist_ok=True)
os.makedirs(os.path.dirname(path), exist_ok=True)

return path

def _measure_execution_time(self, execution_completed: threading.Event):
"""Measures the execution time while waiting for the experiment to finish."""
# Start measuring execution time
start_time = perf_counter()

# Wait for the experiment to finish
execution_completed.wait()

# Stop measuring execution time
end_time = perf_counter()

return os.path.join(folder, file)
# Return the execution time
return end_time - start_time

def execute(self) -> str:
"""
Expand All @@ -518,31 +548,42 @@ def execute(self) -> str:
Returns:
str: The path to the file where the results are stored.
"""
executed_at = datetime.now()

# Create file path to store results
path = self._create_results_path(self.base_data_path, "data.h5")
results_path = self._create_results_path(executed_at=executed_at)

# Prepare the results metadata
self._prepare_metadata()

# Update metadata
self._metadata["platform"] = serialize(self.platform.to_dict())
self._metadata["experiment"] = serialize(self.experiment)
self._metadata["executed_at"] = datetime.now()
self._prepare_metadata(executed_at=executed_at)

# Create the ExperimentResultsWriter for storing results
self._results_writer = ExperimentResultsWriter(path=path, metadata=self._metadata)
with self._results_writer:
start_time = perf_counter()
self._results_writer = ExperimentResultsWriter(path=results_path, metadata=self._metadata)

with Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(bar_width=None),
"[progress.percentage]{task.percentage:>3.1f}%",
TimeElapsedColumn(),
) as progress:
operations = self._prepare_operations(self.experiment.body, progress)
self._execute_operations(operations, progress)
# Event to signal that the execution has completed
execution_completed = threading.Event()

self._results_writer.execution_time = perf_counter() - start_time
with ThreadPoolExecutor() as executor:
# Start the _measure_execution_time in a separate thread
execution_time_future = executor.submit(self._measure_execution_time, execution_completed)

return path
with self._results_writer:
with Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(bar_width=None),
"[progress.percentage]{task.percentage:>3.1f}%",
TimeElapsedColumn(),
) as progress:
operations = self._prepare_operations(self.experiment.body, progress)
self._execute_operations(operations, progress)

# Signal that the execution has completed
execution_completed.set()

# Retrieve the execution time from the Future
execution_time = execution_time_future.result()

# Now write the execution time to the results writer
with self._results_writer:
self._results_writer.execution_time = execution_time

return results_path
17 changes: 15 additions & 2 deletions src/qililab/result/experiment_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# mypy: disable-error-code="attr-defined"
import os
from dataclasses import dataclass
from datetime import datetime
from typing import Any
Expand Down Expand Up @@ -41,6 +42,8 @@ class ExperimentResults:
EXECUTED_AT_PATH = "executed_at"
EXECUTION_TIME_PATH = "execution_time"

S21_PLOT_NAME = "S21.png"

def __init__(self, path: str):
"""Initializes the ExperimentResults instance.
Expand Down Expand Up @@ -175,7 +178,7 @@ def execution_time(self) -> float:
return float(self._file[ExperimentResults.EXECUTION_TIME_PATH][()].decode("utf-8"))

# pylint: disable=too-many-statements
def plot_S21(self, qprogram: int | str = 0, measurement: int | str = 0):
def plot_S21(self, qprogram: int | str = 0, measurement: int | str = 0, save_plot: bool = True):
"""Plots the S21 parameter from the experiment results.
Args:
Expand All @@ -194,7 +197,7 @@ def plot_1d(s21: np.ndarray, dims: list[DimensionInfo]):
"""Plot 1d"""
x_labels, x_values = dims[0].labels, dims[0].values

_, ax1 = plt.subplots()
fig, ax1 = plt.subplots()
ax1.set_title(self.path)
ax1.set_xlabel(x_labels[0])
ax1.set_ylabel(r"$|S_{21}|$")
Expand All @@ -215,6 +218,11 @@ def plot_1d(s21: np.ndarray, dims: list[DimensionInfo]):
# Force scientific notation
ax2.ticklabel_format(axis="x", style="sci", scilimits=(-3, 3))

if save_plot:
folder = os.path.dirname(self.path)
path = os.path.join(folder, ExperimentResults.S21_PLOT_NAME)
fig.savefig(path)

plt.show()

# pylint: disable=too-many-locals
Expand Down Expand Up @@ -264,6 +272,11 @@ def plot_2d(s21: np.ndarray, dims):
# Force scientific notation
ax3.ticklabel_format(axis="y", style="sci", scilimits=(-3, 3))

if save_plot:
folder = os.path.dirname(self.path)
path = os.path.join(folder, "S21.png")
fig.savefig(path)

plt.tight_layout()
plt.show()

Expand Down
7 changes: 2 additions & 5 deletions tests/platform/test_platform.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,6 @@ def test_execute_experiment(self):
# Create an autospec of the Experiment class
mock_experiment = create_autospec(Experiment)

base_data_path = "mock/results/path/"
expected_results_path = "mock/results/path/data.h5"

# Mock the ExperimentExecutor to ensure it's used correctly
Expand All @@ -608,12 +607,10 @@ def test_execute_experiment(self):
mock_executor_instance.execute.return_value = expected_results_path

# Call the method under test
results_path = platform.execute_experiment(experiment=mock_experiment, base_data_path=base_data_path)
results_path = platform.execute_experiment(experiment=mock_experiment)

# Check that ExperimentExecutor was instantiated with the correct arguments
MockExecutor.assert_called_once_with(
platform=platform, experiment=mock_experiment, base_data_path=base_data_path
)
MockExecutor.assert_called_once_with(platform=platform, experiment=mock_experiment)

# Ensure the execute method was called on the ExperimentExecutor instance
mock_executor_instance.execute.assert_called_once()
Expand Down
Loading

0 comments on commit e3b5e77

Please sign in to comment.