This repository has been archived by the owner on Dec 27, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* initial API for entropy QPU architecture document cleanup + rename added failing gateconcatenator test architechture disabling tests the can't pass now converted qpudb to instrument change namespace change namespace to *db fix format formatting and cleaning up dependencies * disable test Co-authored-by: Guy Kerem <guy@quantum-machines.co>
- Loading branch information
1 parent
9166895
commit 47142af
Showing
11 changed files
with
2,293 additions
and
595 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
|
||
Architecture v1 | ||
=============== | ||
|
||
> This is the architecture definition of the first version | ||
The QPU is a specialized interface used to access parameters and calibrations | ||
that are tracked between quantum experiments and enable detection of staleness | ||
|
||
Concept | ||
------- | ||
|
||
The data is stored in a two level hierarchy which can be thought of as a filesystem. | ||
|
||
The first level is `folder` or `directory`, and the second level is `file` | ||
|
||
QPU is an easy interface to save files in directories, while keeping the history of each | ||
file. QPU will also handle the versioning using a git-based single-branched model. | ||
|
||
Create Script | ||
------------- | ||
|
||
Bundled with this package, there should be a script (installed in bin) that will help the | ||
users create new databases. The python API itself focuses on manipulating such database | ||
|
||
```shell | ||
entropy-qpu init | ||
``` | ||
|
||
This creates a clear separation between the initial setup work, and the day-to-day work | ||
|
||
API | ||
--- | ||
|
||
```python | ||
from entropylab_qpudb import QpuDatabase | ||
|
||
# Create the object connecting to the database on the python side | ||
db = QpuDatabase("./db") | ||
|
||
# (Optional) Set the entire content of the database | ||
initial_dictionary = { | ||
"folder1": { | ||
"file1": 6, | ||
"file2": 6 | ||
}, | ||
"folder2": { | ||
"file5": [1, 2, 3], | ||
"file8": np.array([1 + 0.4 * 1j, 2, 3]) | ||
} | ||
} | ||
db.set_all(initial_dictionary) | ||
``` | ||
|
||
Working with individual files and folders can be done using explicit methods | ||
```python | ||
db.get("folder2", "file5").value = 60 | ||
print(db.get("folder2", "file5").value) # prints 60 | ||
``` | ||
|
||
`db.get("folder2", "file5")` returns a wrapping object QpuParameter which | ||
enable access to the content and the metadata (file modification datetime) | ||
|
||
It is possible to access the items using properties | ||
```python | ||
db.folder2.file5.value = 60 | ||
``` | ||
|
||
This should be equivalent |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
from entropylab_qpudb._entropy_cal import QuaCalNode, AncestorRunStrategy | ||
from entropylab_qpudb._qpudatabase import ( | ||
create_new_qpu_database, | ||
QpuDatabaseConnection, | ||
CalState, | ||
) | ||
from entropylab_qpudb._quaconfig import QuaConfig | ||
from entropylab_qpudb._resolver import Resolver | ||
|
||
__all__ = [ | ||
"QuaConfig", | ||
"QuaCalNode", | ||
"AncestorRunStrategy", | ||
"create_new_qpu_database", | ||
"QpuDatabaseConnection", | ||
"CalState", | ||
"Resolver", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,150 @@ | ||
import enum | ||
import inspect | ||
from abc import abstractmethod | ||
from copy import deepcopy | ||
from dataclasses import dataclass | ||
from itertools import count | ||
from typing import Callable, List, Optional, Union, Iterable, Set | ||
|
||
from entropylab_qpudb._quaconfig import QuaConfig | ||
from quaentropy.api.execution import EntropyContext | ||
from quaentropy.api.graph import Node | ||
from quaentropy.graph_experiment import PyNode | ||
|
||
|
||
class AncestorRunStrategy(enum.Enum): | ||
RunAll = 1 | ||
RunOnlyLast = 2 | ||
|
||
|
||
@dataclass | ||
class ConfigPatch: | ||
id: int | ||
depth: int | ||
function: Callable | ||
|
||
def __hash__(self): | ||
return self.id | ||
|
||
|
||
@dataclass | ||
class QuaCalNodeOutput: | ||
base_config: QuaConfig | ||
patches: List[ConfigPatch] | ||
merged_config = None | ||
|
||
def build_config(self, context): | ||
config_copy = deepcopy(self.base_config) | ||
for patch in self.patches: | ||
patch.function(config_copy, context) | ||
self.merged_config = config_copy | ||
return config_copy | ||
|
||
def __repr__(self): | ||
merged = "" | ||
if self.merged_config: | ||
merged = str(self.merged_config) | ||
nl = "\n" | ||
return f""" | ||
Base: | ||
{self.base_config} | ||
Patches: | ||
{nl.join([(f"{patch.id} depth {patch.depth}:{nl}" + inspect.getsource(patch.function)) for patch in self.patches])} | ||
Merged: | ||
{merged} | ||
""" | ||
|
||
|
||
id_iter = count(start=0, step=1) | ||
|
||
|
||
class QuaCalNode(PyNode): | ||
def __init__( | ||
self, | ||
dependency: Optional[Union[Node, Iterable[Node]]] = None, | ||
must_run_after: Set[Node] = None, | ||
name: Optional[str] = None, | ||
): | ||
if dependency: | ||
if isinstance(dependency, Iterable): | ||
input_vars = {} | ||
for node in dependency: | ||
input_vars[f"config_{node.label}_{id(node)}"] = node.outputs[ | ||
"config" | ||
] | ||
else: | ||
input_vars = { | ||
f"config_{dependency.label}": dependency.outputs["config"] | ||
} | ||
else: | ||
input_vars = None | ||
output_vars = {"config"} | ||
|
||
def program( | ||
*configs, | ||
strategy: AncestorRunStrategy = AncestorRunStrategy.RunAll, | ||
is_last: bool, | ||
context: EntropyContext, | ||
): | ||
# sync config | ||
merged_config: QuaCalNodeOutput = self._merge_configs(configs) | ||
config_copy = merged_config.build_config(context) | ||
|
||
# run the actual code | ||
self.prepare_config(config_copy, context) | ||
if strategy == AncestorRunStrategy.RunAll or is_last: | ||
self.run_program(config_copy, context) | ||
|
||
# prepare the output | ||
patch_depth = ( | ||
max([config.depth for config in merged_config.patches] + [0]) + 1 | ||
) | ||
merged_config.patches.append( | ||
ConfigPatch(next(id_iter), patch_depth, self.prepare_config) | ||
) | ||
merged_config.patches.append( | ||
ConfigPatch(next(id_iter), patch_depth, self.update_config) | ||
) | ||
return {"config": merged_config} | ||
|
||
if name is None: | ||
name = self.__class__.__name__ | ||
super().__init__(name, program, input_vars, output_vars, must_run_after) | ||
|
||
def add_config_dependency(self, node): | ||
super().add_input(f"config_{node.label}", node.outputs["config"]) | ||
|
||
def _merge_configs(self, configs: Iterable[QuaCalNodeOutput]) -> QuaCalNodeOutput: | ||
def get_base(config): | ||
if isinstance(config, QuaCalNodeOutput): | ||
return config.base_config | ||
else: | ||
return config | ||
|
||
def get_patches(config): | ||
if isinstance(config, QuaCalNodeOutput): | ||
return config.patches | ||
else: | ||
return [] | ||
|
||
base_configs = [get_base(config) for config in configs] | ||
if len(set([id(config) for config in base_configs])) > 1: | ||
raise RuntimeError("trying to merge different configs") | ||
all_patches = [item for sublist in configs for item in get_patches(sublist)] | ||
unique_patches = list(set(all_patches)) | ||
unique_patches.sort(key=lambda patch: patch.depth) | ||
return QuaCalNodeOutput(base_configs[0], unique_patches) | ||
|
||
@abstractmethod | ||
def prepare_config(self, config: QuaConfig, context: EntropyContext): | ||
pass | ||
|
||
@abstractmethod | ||
def run_program(self, config, context: EntropyContext): | ||
pass | ||
|
||
@abstractmethod | ||
def update_config(self, config: QuaConfig, context: EntropyContext): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
from copy import deepcopy | ||
from dataclasses import dataclass | ||
from typing import Dict, List | ||
|
||
from entropylab_qpudb._quaconfig import QuaConfig | ||
|
||
|
||
@dataclass(frozen=True) | ||
class Moment: | ||
play_statements: Dict[str, str] # quantum element # operation | ||
|
||
|
||
class GateConcatenator: | ||
def __init__(self, moment_sequence: List[Moment], config: QuaConfig, name=None): | ||
# todo: add the ability to add more than one moment sequence | ||
self._moment_sequence = moment_sequence | ||
self._config = deepcopy(config) | ||
# collect all elements | ||
self._elements = set() | ||
for gate in moment_sequence: | ||
self._elements.update(gate.play_statements.keys()) | ||
|
||
self._add_operations_to_config() | ||
self._add_empty_pulses_to_config() | ||
self._add_gates_to_config() | ||
|
||
def _add_operations_to_config(self): | ||
for element in self._elements: | ||
self._config["elements"][element]["operations"][ | ||
"concat_waveform" | ||
] = f"{element}_concat_pulse_in" | ||
|
||
def _add_empty_pulses_to_config(self): | ||
for element in self._elements: | ||
pulse_name = f"{element}_concat_pulse_in" | ||
self._config["pulses"][pulse_name] = { | ||
"length": 0, | ||
"operation": "control", | ||
"waveforms": {}, | ||
} | ||
if "mixInputs" in self._config["elements"][element]: | ||
waveform_i_name = f"{element}_concat_waveform_i" | ||
waveform_q_name = f"{element}_concat_waveform_q" | ||
|
||
self._config["pulses"][pulse_name]["waveforms"] = { | ||
"I": waveform_i_name, | ||
"Q": waveform_q_name, | ||
} | ||
self._config["waveforms"][waveform_i_name] = { | ||
"type": "arbitrary", | ||
"samples": [], | ||
} | ||
self._config["waveforms"][waveform_q_name] = { | ||
"type": "arbitrary", | ||
"samples": [], | ||
} | ||
else: | ||
waveform_name = f"{element}_concat_waveform" | ||
self._config["pulses"][pulse_name]["waveforms"] = { | ||
"single": waveform_name, | ||
} | ||
self._config["waveforms"][waveform_name] = { | ||
"type": "arbitrary", | ||
"samples": [], | ||
} | ||
|
||
def _add_gates_to_config(self): | ||
for moment in self._moment_sequence: | ||
duration = self._get_moment_duration(moment) | ||
for element in self._elements: | ||
concat_pulse = self._config["pulses"][f"{element}_concat_pulse_in"] | ||
concat_pulse["length"] += duration | ||
|
||
# add to concatenated waveforms the waveforms from this operation, add zeros to others | ||
if element in moment.play_statements.keys(): | ||
operation = moment.play_statements[element] | ||
if "mixInputs" in self._config["elements"][element]: | ||
waveform_i, waveform_q = self._config.get_waveforms_from_op( | ||
element, operation | ||
) | ||
self._append_waveform( | ||
f"{element}_concat_waveform_i", waveform_i, duration | ||
) | ||
self._append_waveform( | ||
f"{element}_concat_waveform_q", waveform_q, duration | ||
) | ||
else: | ||
waveform = self._config.get_waveforms_from_op( | ||
element, operation | ||
) | ||
self._append_waveform( | ||
f"{element}_concat_waveform", waveform, duration | ||
) | ||
else: | ||
if "mixInputs" in self._config["elements"][element]: | ||
waveform_i = self._get_empty_waveform(duration) | ||
waveform_q = self._get_empty_waveform(duration) | ||
self._append_waveform( | ||
f"{element}_concat_waveform_i", waveform_i, duration | ||
) | ||
self._append_waveform( | ||
f"{element}_concat_waveform_q", waveform_q, duration | ||
) | ||
else: | ||
waveform = self._get_empty_waveform(duration) | ||
self._append_waveform( | ||
f"{element}_concat_waveform", waveform, duration | ||
) | ||
|
||
def _get_moment_duration(self, moment): | ||
duration = 0 | ||
for element, operation in moment.play_statements.items(): | ||
op_duration = self._config.get_pulse_from_op(element, operation)["length"] | ||
# op_duration = self._config['pulses'][self._config[element]['operations'][operation]]['length'] | ||
duration = max(duration, op_duration) | ||
return duration | ||
|
||
def _append_waveform(self, name, waveform, duration): | ||
waveform_to_append = ( | ||
waveform + [0.0] * (len(waveform) - duration) | ||
if duration > len(waveform) | ||
else waveform | ||
) | ||
self._config["waveforms"][name]["samples"] += waveform_to_append | ||
|
||
def _get_empty_waveform(self, duration): | ||
return [0.0] * duration | ||
|
||
@property | ||
def config(self): | ||
return self._config | ||
|
||
@staticmethod | ||
def concat_op_name(): | ||
return "concat_waveform" | ||
|
||
@staticmethod | ||
def concat_pulse_name(element): | ||
return f"{element}_concat_pulse_in" | ||
|
||
def concat_waveform_name(self, element): | ||
if "mixInputs" in self._config["elements"][element]: | ||
return f"{element}_concat_waveform_i", f"{element}_concat_waveform_q" | ||
else: | ||
return f"{element}_concat_waveform" |
Oops, something went wrong.