Skip to content
This repository has been archived by the owner on Dec 27, 2022. It is now read-only.

Commit

Permalink
initial API for entropy QPU (#4)
Browse files Browse the repository at this point in the history
* 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
liorella-qm and qguyk authored May 4, 2021
1 parent 9166895 commit 47142af
Show file tree
Hide file tree
Showing 11 changed files with 2,293 additions and 595 deletions.
69 changes: 69 additions & 0 deletions architecture.md
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
18 changes: 18 additions & 0 deletions entropylab_qpudb/__init__.py
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",
]
150 changes: 150 additions & 0 deletions entropylab_qpudb/_entropy_cal.py
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
145 changes: 145 additions & 0 deletions entropylab_qpudb/_gateconcatenator.py
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"
Loading

0 comments on commit 47142af

Please # to comment.