etiket-sync-agent-qcodes 0.3.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- etiket_sync_agent_qcodes/__init__.py +4 -0
- etiket_sync_agent_qcodes/qcodes_config_class.py +93 -0
- etiket_sync_agent_qcodes/qcodes_metadata.py +116 -0
- etiket_sync_agent_qcodes/qcodes_sync_class.py +175 -0
- etiket_sync_agent_qcodes/real_time_sync.py +143 -0
- etiket_sync_agent_qcodes/utility/__init__.py +0 -0
- etiket_sync_agent_qcodes/utility/extract_metadata_from_QCoDeS.py +36 -0
- etiket_sync_agent_qcodes-0.3.0b1.dist-info/METADATA +245 -0
- etiket_sync_agent_qcodes-0.3.0b1.dist-info/RECORD +13 -0
- etiket_sync_agent_qcodes-0.3.0b1.dist-info/WHEEL +5 -0
- etiket_sync_agent_qcodes-0.3.0b1.dist-info/entry_points.txt +2 -0
- etiket_sync_agent_qcodes-0.3.0b1.dist-info/licenses/LICENCE +34 -0
- etiket_sync_agent_qcodes-0.3.0b1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
import dataclasses
|
|
3
|
+
import sqlite3
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import etiket_sync_agent_qcodes
|
|
7
|
+
|
|
8
|
+
from etiket_sync_agent.crud.sync_sources import crud_sync_sources, SyncSources
|
|
9
|
+
from etiket_sync_agent.db import get_db_session_context
|
|
10
|
+
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class QCoDeSConfigData:
|
|
13
|
+
database_path: pathlib.Path
|
|
14
|
+
set_up : str
|
|
15
|
+
static_attributes : Optional[dict] = dataclasses.field(default_factory=dict)
|
|
16
|
+
|
|
17
|
+
def __post_init__(self):
|
|
18
|
+
# ensure the path is of the type pathlib.Path (str is converted to Path) and expand ~
|
|
19
|
+
self.database_path = pathlib.Path(self.database_path).expanduser()
|
|
20
|
+
|
|
21
|
+
async def validate(self, current_sync_source : Optional[SyncSources] = None):
|
|
22
|
+
"""
|
|
23
|
+
Validates the QCoDeS database configuration.
|
|
24
|
+
|
|
25
|
+
Checks:
|
|
26
|
+
1. If the database_path path points to an existing file.
|
|
27
|
+
2. If the file has a .db extension.
|
|
28
|
+
3. If the file can be opened as an SQLite database.
|
|
29
|
+
4. If the database file has already been added as a QCoDeS sync source.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
current_source (Optional[SyncSources]): in case of update, the current sync source is provided.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: If any validation check fails.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
True if all checks pass.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
abs_db_path = pathlib.Path(self.database_path).expanduser().resolve(strict=True)
|
|
42
|
+
except FileNotFoundError:
|
|
43
|
+
raise ValueError(f"The specified database file does not exist: {self.database_path}")
|
|
44
|
+
|
|
45
|
+
if not abs_db_path.is_file():
|
|
46
|
+
raise ValueError(f"The specified path is not a file: {abs_db_path}")
|
|
47
|
+
|
|
48
|
+
if abs_db_path.suffix != ".db":
|
|
49
|
+
raise ValueError(f"A QCoDeS database file must have a .db extension, not '{abs_db_path.suffix}'. Provided path: {abs_db_path}")
|
|
50
|
+
|
|
51
|
+
# test basic sqlite3 connection and verify QCoDeS database structure
|
|
52
|
+
try:
|
|
53
|
+
conn = sqlite3.connect(f'file:{abs_db_path}?mode=ro', uri=True)
|
|
54
|
+
cursor = conn.cursor()
|
|
55
|
+
|
|
56
|
+
# Query sqlite_master to check for QCoDeS-specific tables
|
|
57
|
+
cursor.execute(
|
|
58
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('runs', 'experiments')"
|
|
59
|
+
)
|
|
60
|
+
tables = {row[0] for row in cursor.fetchall()}
|
|
61
|
+
|
|
62
|
+
conn.close()
|
|
63
|
+
|
|
64
|
+
# Verify required QCoDeS tables exist
|
|
65
|
+
required_tables = {'runs', 'experiments'}
|
|
66
|
+
missing_tables = required_tables - tables
|
|
67
|
+
if missing_tables:
|
|
68
|
+
raise ValueError(
|
|
69
|
+
f"The database '{abs_db_path}' does not appear to be a valid QCoDeS database. "
|
|
70
|
+
f"Missing required tables: {', '.join(sorted(missing_tables))}"
|
|
71
|
+
)
|
|
72
|
+
except sqlite3.Error as e:
|
|
73
|
+
raise ValueError(f"Error opening SQLite database file '{abs_db_path}': {e}") from e
|
|
74
|
+
|
|
75
|
+
# check if the database file is already added.
|
|
76
|
+
async with get_db_session_context() as session:
|
|
77
|
+
sync_sources = await crud_sync_sources.list_sync_sources(session)
|
|
78
|
+
for sync_source in sync_sources:
|
|
79
|
+
if sync_source.backend == etiket_sync_agent_qcodes.__name__:
|
|
80
|
+
try:
|
|
81
|
+
existing_path_str = sync_source.config_data['database_path']
|
|
82
|
+
existing_path = pathlib.Path(existing_path_str).resolve()
|
|
83
|
+
except Exception as e:
|
|
84
|
+
# If an existing path can't be resolved, log it but continue validation
|
|
85
|
+
print(f"Warning: Could not resolve existing database path '{existing_path_str}' for sync source '{sync_source.name}' ({sync_source.id}): {e}")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if abs_db_path == existing_path:
|
|
89
|
+
if current_sync_source is not None and current_sync_source.id == sync_source.id:
|
|
90
|
+
continue
|
|
91
|
+
raise ValueError(f"The database file '{abs_db_path}' is already added as sync source '{sync_source.name}'.")
|
|
92
|
+
|
|
93
|
+
return True
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from typing import List, Dict
|
|
2
|
+
|
|
3
|
+
from qcodes.instrument import Instrument
|
|
4
|
+
from qcodes.parameters import Parameter
|
|
5
|
+
|
|
6
|
+
class QHMetaData(Instrument):
|
|
7
|
+
"""
|
|
8
|
+
A QCoDeS Instrument subclass that manages metadata for a dataset within the QHarbor framework.
|
|
9
|
+
|
|
10
|
+
This instrument stores tags and attributes, which the synchronization agent will recognize and add as such to the dataset.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, name : str, static_tags: List[str] = [],
|
|
14
|
+
static_attributes: Dict[str, str] = {}):
|
|
15
|
+
"""
|
|
16
|
+
Initialize a QHMetaData instance.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name (str): The name of the instrument, this should be "qh_meta"
|
|
20
|
+
static_tags (List[str]): A list of static tags that are always included in
|
|
21
|
+
the metadata. These tags remain after the reset method is called.
|
|
22
|
+
static_attributes (Dict[str, str]): A dictionary of static attributes that are
|
|
23
|
+
always included in the metadata. These attributes remain after the reset method is called.
|
|
24
|
+
"""
|
|
25
|
+
super().__init__(name)
|
|
26
|
+
|
|
27
|
+
self._static_tags = static_tags
|
|
28
|
+
self._static_attributes = static_attributes
|
|
29
|
+
|
|
30
|
+
self._dynamic_tags = []
|
|
31
|
+
self._dynamic_attributes = {}
|
|
32
|
+
|
|
33
|
+
self.add_parameter("tags", get_cmd=self.__get_tags)
|
|
34
|
+
self.add_parameter("attributes", get_cmd=self.__get_attributes)
|
|
35
|
+
|
|
36
|
+
def add_tags(self, tags: List[str]) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Add dynamic tags to the metadata. These tags will be cleared upon reset.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
tags (List[str]): A list of tags to add.
|
|
42
|
+
"""
|
|
43
|
+
self._dynamic_tags.extend(tags)
|
|
44
|
+
|
|
45
|
+
def add_attributes(self, attributes: Dict[str, str]) -> None:
|
|
46
|
+
"""
|
|
47
|
+
Add dynamic attributes to the metadata. These attributes will be cleared upon reset.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
attributes (Dict[str, str]): A dictionary of key-value pairs to add as attributes.
|
|
51
|
+
"""
|
|
52
|
+
self._dynamic_attributes.update(attributes)
|
|
53
|
+
|
|
54
|
+
def reset(self) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Clear all dynamic tags and attributes.
|
|
57
|
+
|
|
58
|
+
This does not affect static tags or attributes, nor the list of important parameters.
|
|
59
|
+
"""
|
|
60
|
+
self._dynamic_tags.clear()
|
|
61
|
+
self._dynamic_attributes.clear()
|
|
62
|
+
|
|
63
|
+
def __get_tags(self) -> List[str]:
|
|
64
|
+
"""
|
|
65
|
+
Return a combined list of static and dynamic tags.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List[str]: The current list of tags (static + dynamic).
|
|
69
|
+
"""
|
|
70
|
+
return list(set(self._static_tags + self._dynamic_tags))
|
|
71
|
+
|
|
72
|
+
def __get_attributes(self) -> Dict[str, str]:
|
|
73
|
+
"""
|
|
74
|
+
Return a combined dictionary containing static, dynamic attributes, and parameter values.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dict[str, str]: A dictionary with the static and dynamic attributes.
|
|
78
|
+
"""
|
|
79
|
+
attr = {**self._static_attributes}
|
|
80
|
+
attr.update(self._dynamic_attributes)
|
|
81
|
+
return attr
|
|
82
|
+
|
|
83
|
+
def get_idn(self):
|
|
84
|
+
return {'vendor': 'QHarbor', 'model': 'QHarbor Metadata Manager', 'serial': None, 'firmware': None}
|
|
85
|
+
|
|
86
|
+
def validate_parameter(parameter: Parameter) -> Parameter:
|
|
87
|
+
# for when we add parameters to the metadata.
|
|
88
|
+
"""
|
|
89
|
+
Validate that a parameter is an instance of qcodes.Parameter and its value is int or float.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
parameter (Parameter): The qcodes.Parameter to validate.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If the parameter is not a qcodes.Parameter, or if its current value is not a float or int.
|
|
96
|
+
ValueError: If the parameter value cannot be retrieved (wrapped).
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Parameter: The original parameter if validation succeeds.
|
|
100
|
+
"""
|
|
101
|
+
if not isinstance(parameter, Parameter):
|
|
102
|
+
raise ValueError(f"Invalid parameter type: {type(parameter)}, expected qcodes.Parameter.")
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
value = parameter.get()
|
|
106
|
+
if not isinstance(value, (float, int)):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"Invalid parameter value type: {type(value)}, "
|
|
109
|
+
f"expected float or int (Parameter: {parameter.name})."
|
|
110
|
+
)
|
|
111
|
+
except ValueError as e:
|
|
112
|
+
raise e
|
|
113
|
+
except Exception as e:
|
|
114
|
+
raise ValueError(f"Could not get value from parameter: {parameter.name}") from e
|
|
115
|
+
|
|
116
|
+
return parameter
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
import sqlite3
|
|
3
|
+
import qcodes as qc
|
|
4
|
+
import os
|
|
5
|
+
import logging
|
|
6
|
+
import xarray
|
|
7
|
+
import asyncio
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
from etiket_sync_agent.backends.sync_source_abstract import SyncSourceDatabaseBase, ScopeRequirement
|
|
13
|
+
from etiket_sync_agent.sync.sync_records.manager import SyncRecordManager
|
|
14
|
+
from etiket_sync_agent.sync.sync_utilities import file_info, sync_utilities,\
|
|
15
|
+
dataset_info, FileType
|
|
16
|
+
from etiket_sync_agent.schemas import SyncItemSchema, SyncItemCreate
|
|
17
|
+
|
|
18
|
+
from etiket_sync_agent_qcodes.real_time_sync import QCoDeS_live_sync
|
|
19
|
+
|
|
20
|
+
from etiket_sync_agent_qcodes.qcodes_config_class import QCoDeSConfigData
|
|
21
|
+
from etiket_sync_agent_qcodes.utility.extract_metadata_from_QCoDeS import extract_labels_and_attributes_from_snapshot, MetaDataExtractionError
|
|
22
|
+
|
|
23
|
+
from qcodes.dataset import load_by_guid
|
|
24
|
+
from qcodes.dataset.data_set import DataSet
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
class QCoDeSSync(SyncSourceDatabaseBase):
|
|
29
|
+
sync_agent_name: typing.ClassVar[str] = "QCoDeS"
|
|
30
|
+
config_data_class: typing.ClassVar[typing.Type[QCoDeSConfigData]] = QCoDeSConfigData
|
|
31
|
+
scope_requirement: typing.ClassVar[ScopeRequirement] = ScopeRequirement.REQUIRED
|
|
32
|
+
supports_scope_mapping: typing.ClassVar[bool] = False
|
|
33
|
+
live_sync_implemented: typing.ClassVar[bool] = True
|
|
34
|
+
has_owner: typing.ClassVar[bool] = True
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
async def getNewDatasets(config_data: QCoDeSConfigData, last_sync_item: SyncItemSchema | None) -> typing.List[SyncItemCreate] | None:
|
|
38
|
+
if not os.path.exists(config_data.database_path):
|
|
39
|
+
raise FileNotFoundError(f"Database file not found at {config_data.database_path}")
|
|
40
|
+
|
|
41
|
+
qc.config.core.db_location = str(config_data.database_path)
|
|
42
|
+
|
|
43
|
+
newSyncIdentifiers = []
|
|
44
|
+
|
|
45
|
+
with sqlite3.connect(config_data.database_path) as conn:
|
|
46
|
+
conn.execute("PRAGMA journal_mode=WAL;")
|
|
47
|
+
last_run_id = -1
|
|
48
|
+
if last_sync_item is not None:
|
|
49
|
+
last_run_id = last_sync_item.syncPriority
|
|
50
|
+
|
|
51
|
+
get_newer_guid_query = """SELECT run_id, guid FROM runs WHERE run_id > ? ORDER BY run_id ASC"""
|
|
52
|
+
|
|
53
|
+
cursor = conn.cursor()
|
|
54
|
+
cursor.execute(get_newer_guid_query, (int(last_run_id),))
|
|
55
|
+
rows = cursor.fetchall()
|
|
56
|
+
|
|
57
|
+
newSyncIdentifiers += [SyncItemCreate(dataIdentifier=str(row[1]), syncPriority=row[0]) for row in rows]
|
|
58
|
+
return newSyncIdentifiers
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
async def checkLiveDataset(configData: QCoDeSConfigData, syncIdentifier: SyncItemSchema, maxPriority: bool) -> bool:
|
|
62
|
+
if maxPriority is False:
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
qc.config.core.db_location = str(configData.database_path)
|
|
66
|
+
|
|
67
|
+
ds_qc = load_by_guid(syncIdentifier.dataIdentifier)
|
|
68
|
+
return not ds_qc.completed
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
async def syncDatasetNormal(configData: QCoDeSConfigData, syncIdentifier: SyncItemSchema, sync_record: SyncRecordManager):
|
|
72
|
+
qc.config.core.db_location = str(configData.database_path)
|
|
73
|
+
|
|
74
|
+
with sync_record.task("Creating dataset from QCoDeS dataset (not live)"):
|
|
75
|
+
ds_qc, ds_xr = await create_ds_from_qcodes(configData, syncIdentifier, False, sync_record)
|
|
76
|
+
|
|
77
|
+
with sync_record.task("Getting and uploading the measured data to the server"):
|
|
78
|
+
if ds_qc.run_timestamp_raw is not None:
|
|
79
|
+
created_time = datetime.fromtimestamp(ds_qc.run_timestamp_raw)
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError("Run timestamp is None")
|
|
82
|
+
|
|
83
|
+
f_info = file_info(name = 'measurement', fileName = 'measured_data.hdf5',
|
|
84
|
+
fileType= FileType.HDF5_NETCDF,
|
|
85
|
+
created = created_time, file_generator = "QCoDeS")
|
|
86
|
+
await sync_utilities.upload_xarray(ds_xr, syncIdentifier, f_info, sync_record)
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
async def syncDatasetLive(configData: QCoDeSConfigData, syncIdentifier: SyncItemSchema, sync_record: SyncRecordManager):
|
|
90
|
+
qc.config.core.db_location = str(configData.database_path)
|
|
91
|
+
|
|
92
|
+
with sync_record.task("Creating dataset from QCoDeS dataset (live)"):
|
|
93
|
+
await create_ds_from_qcodes(configData, syncIdentifier, True, sync_record)
|
|
94
|
+
with sync_record.task("Starting live feed to a local hdf5 file"):
|
|
95
|
+
await asyncio.get_event_loop().run_in_executor(
|
|
96
|
+
None, QCoDeS_live_sync,
|
|
97
|
+
syncIdentifier.dataIdentifier, str(configData.database_path), syncIdentifier.datasetUUID
|
|
98
|
+
)
|
|
99
|
+
sync_record.add_log("Live feed completed.")
|
|
100
|
+
|
|
101
|
+
async def create_ds_from_qcodes(configData: QCoDeSConfigData, syncIdentifier: SyncItemSchema, live : bool, sync_record: SyncRecordManager) -> Tuple[DataSet, xarray.Dataset]:
|
|
102
|
+
sync_record.add_log("Loading dataset and extracting metadata from QCoDeS with GUID: " + syncIdentifier.dataIdentifier)
|
|
103
|
+
ds_qc = load_by_guid(syncIdentifier.dataIdentifier)
|
|
104
|
+
sync_record.add_log("Dataset loaded from QCoDeS, will start extracting metadata")
|
|
105
|
+
|
|
106
|
+
if ds_qc.run_timestamp_raw is not None:
|
|
107
|
+
collected_time = datetime.fromtimestamp(ds_qc.run_timestamp_raw)
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError("Run timestamp is None")
|
|
110
|
+
|
|
111
|
+
ranking = 0
|
|
112
|
+
if 'inspectr_tag' in ds_qc.metadata.keys():
|
|
113
|
+
if ds_qc.metadata['inspectr_tag'] == 'star':
|
|
114
|
+
ranking = 1
|
|
115
|
+
if ds_qc.metadata['inspectr_tag'] == 'cross':
|
|
116
|
+
ranking = -1
|
|
117
|
+
|
|
118
|
+
# get variable names in the dataset, this is handy for searching!
|
|
119
|
+
ds_xr = ds_qc.to_xarray_dataset()
|
|
120
|
+
keywords = set()
|
|
121
|
+
try:
|
|
122
|
+
for key in ds_xr.keys():
|
|
123
|
+
if 'long_name' in ds_xr[key].attrs.keys():
|
|
124
|
+
keywords.add(ds_xr[key].attrs['long_name'])
|
|
125
|
+
continue
|
|
126
|
+
if 'name' in ds_xr[key].attrs.keys():
|
|
127
|
+
keywords.add(ds_xr[key].attrs['name'])
|
|
128
|
+
|
|
129
|
+
for key in ds_xr.coords:
|
|
130
|
+
if 'long_name' in ds_xr[key].attrs.keys():
|
|
131
|
+
keywords.add(ds_xr[key].attrs['long_name'])
|
|
132
|
+
continue
|
|
133
|
+
if 'name' in ds_xr[key].attrs.keys():
|
|
134
|
+
keywords.add(ds_xr[key].attrs['name'])
|
|
135
|
+
except Exception:
|
|
136
|
+
pass
|
|
137
|
+
|
|
138
|
+
name_lines = ds_qc.name.splitlines()
|
|
139
|
+
name = name_lines[0] if name_lines else ""
|
|
140
|
+
|
|
141
|
+
additional_description = "\n".join(name_lines[1:]) if len(name_lines) > 1 else ""
|
|
142
|
+
description = f"database : {os.path.basename(configData.database_path)} | run ID : {ds_qc.run_id} | GUID : {ds_qc.guid} | exp name : {ds_qc.exp_name}"
|
|
143
|
+
|
|
144
|
+
if additional_description:
|
|
145
|
+
description += f"\n\n{additional_description}"
|
|
146
|
+
|
|
147
|
+
attributes = {"sample" : ds_qc.sample_name,
|
|
148
|
+
"set-up" : configData.set_up}
|
|
149
|
+
if configData.static_attributes is not None:
|
|
150
|
+
attributes.update(configData.static_attributes)
|
|
151
|
+
|
|
152
|
+
try: # experimental
|
|
153
|
+
if ds_qc.snapshot is not None:
|
|
154
|
+
extra_labels, extra_attributes = extract_labels_and_attributes_from_snapshot(ds_qc.snapshot)
|
|
155
|
+
else:
|
|
156
|
+
extra_labels = []
|
|
157
|
+
extra_attributes = {}
|
|
158
|
+
except MetaDataExtractionError:
|
|
159
|
+
logger.exception("Could not extract labels and attributes from snapshot.")
|
|
160
|
+
extra_labels = []
|
|
161
|
+
extra_attributes = {}
|
|
162
|
+
|
|
163
|
+
keywords.update(extra_labels)
|
|
164
|
+
attributes.update(extra_attributes)
|
|
165
|
+
|
|
166
|
+
sync_record.add_log("Creating dataset info")
|
|
167
|
+
ds_info = dataset_info(name = name, datasetUUID = syncIdentifier.datasetUUID,
|
|
168
|
+
alt_uid = ds_qc.guid, scopeUUID = syncIdentifier.scopeIdentifier,
|
|
169
|
+
created = collected_time, keywords = list(keywords), description = description,
|
|
170
|
+
ranking=ranking,
|
|
171
|
+
# exp_name not added, since some people use it to name their experiments ...
|
|
172
|
+
attributes = attributes)
|
|
173
|
+
|
|
174
|
+
await sync_utilities.create_or_update_dataset(live, syncIdentifier, ds_info, sync_record)
|
|
175
|
+
return ds_qc, ds_xr
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from qcodes import Parameter
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from etiket_client.remote.endpoints.models.types import FileType
|
|
12
|
+
from qdrive.measurement.data_collector import data_collector, from_QCoDeS_parameter
|
|
13
|
+
from qdrive.dataset.dataset import dataset
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
# Timeout for live sync - if no new data is received within this time, sync exits
|
|
18
|
+
LIVE_SYNC_TIMEOUT_SECONDS = 10 * 60 # 10 minutes
|
|
19
|
+
|
|
20
|
+
# Polling interval when waiting for new data
|
|
21
|
+
LIVE_SYNC_POLL_INTERVAL_SECONDS = 0.1
|
|
22
|
+
|
|
23
|
+
def QCoDeS_live_sync(guid : str, database : str, datasetUUID : UUID):
|
|
24
|
+
print("Starting real time sync")
|
|
25
|
+
conn = sqlite3.connect(f'{database}')
|
|
26
|
+
conn.execute('pragma journal_mode=wal')
|
|
27
|
+
|
|
28
|
+
c = conn.cursor()
|
|
29
|
+
|
|
30
|
+
# Get run_id from guid
|
|
31
|
+
c.execute("SELECT run_id FROM runs WHERE guid = ?", (guid,))
|
|
32
|
+
result = c.fetchone()
|
|
33
|
+
if result is None:
|
|
34
|
+
raise ValueError(f"No run found with guid: {guid}")
|
|
35
|
+
run_id = result[0]
|
|
36
|
+
|
|
37
|
+
c.execute("SELECT * FROM layouts WHERE run_id = ?", (run_id,))
|
|
38
|
+
layout = c.fetchall()
|
|
39
|
+
|
|
40
|
+
parameters = {}
|
|
41
|
+
parameters_by_name = {}
|
|
42
|
+
for layout_item in layout:
|
|
43
|
+
parameters[layout_item[0]] = Parameter(layout_item[2], label=layout_item[3], unit=layout_item[4])
|
|
44
|
+
parameters_by_name[layout_item[2]] = parameters[layout_item[0]]
|
|
45
|
+
|
|
46
|
+
dependency_ids= list(parameters.keys())
|
|
47
|
+
c.execute(f"Select * from dependencies where dependent in ({','.join(['?']*len(dependency_ids))})",
|
|
48
|
+
dependency_ids)
|
|
49
|
+
dependencies = c.fetchall()
|
|
50
|
+
|
|
51
|
+
relationships = {}
|
|
52
|
+
for dep in dependencies:
|
|
53
|
+
if dep[0] not in relationships.keys():
|
|
54
|
+
relationships[dep[0]] = []
|
|
55
|
+
relationships[dep[0]].append(dep[1])
|
|
56
|
+
|
|
57
|
+
native_ds = dataset(datasetUUID)
|
|
58
|
+
dc = data_collector(native_ds, FileType.HDF5_CACHE)
|
|
59
|
+
|
|
60
|
+
try :
|
|
61
|
+
for k, v in relationships.items():
|
|
62
|
+
dep = [parameters[i] for i in v]
|
|
63
|
+
dc += from_QCoDeS_parameter(parameters[k], dep, dc)
|
|
64
|
+
|
|
65
|
+
sync_idx = 0
|
|
66
|
+
res = c.execute("Select result_table_name, snapshot from runs where run_id = ?", (run_id,)).fetchone()
|
|
67
|
+
table_name = res[0]
|
|
68
|
+
snapshot_json = res[1]
|
|
69
|
+
|
|
70
|
+
dc.set_attr('snapshot', snapshot_json)
|
|
71
|
+
dc._enable_swmr() # manually set this, sometimes it takes long for qCoDeS to start writing data ... .
|
|
72
|
+
except Exception as e:
|
|
73
|
+
if (hasattr(dc, 'lock_file')):
|
|
74
|
+
if (dc.lock_file is not None):
|
|
75
|
+
if os.path.isfile(dc.lock_file):
|
|
76
|
+
os.remove(dc.lock_file)
|
|
77
|
+
dc.complete()
|
|
78
|
+
raise e
|
|
79
|
+
|
|
80
|
+
t_last_fetch = time.time()
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
while not (check_complete(c, run_id) and sync_idx == getMaxId(c, table_name)):
|
|
84
|
+
c.execute(f"SELECT * FROM '{table_name}' WHERE id > ?", (sync_idx,))
|
|
85
|
+
c_names = [description[0] for description in c.description]
|
|
86
|
+
|
|
87
|
+
data = c.fetchall()
|
|
88
|
+
if len(data) == 0:
|
|
89
|
+
if t_last_fetch + LIVE_SYNC_TIMEOUT_SECONDS < time.time():
|
|
90
|
+
logger.warning(f"No new data found in the last {LIVE_SYNC_TIMEOUT_SECONDS} seconds. Exiting real time sync.")
|
|
91
|
+
raise TimeoutError(f"No new data found in the last {LIVE_SYNC_TIMEOUT_SECONDS} seconds. Exiting real time sync. Expected that the measurement was set to complete.")
|
|
92
|
+
|
|
93
|
+
# check if a new run has already been started:
|
|
94
|
+
max_run_id = c.execute("SELECT MAX(run_id) FROM runs").fetchone()[0]
|
|
95
|
+
if max_run_id != run_id:
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
time.sleep(LIVE_SYNC_POLL_INTERVAL_SECONDS)
|
|
99
|
+
continue
|
|
100
|
+
else:
|
|
101
|
+
t_last_fetch = time.time()
|
|
102
|
+
|
|
103
|
+
for d in data:
|
|
104
|
+
array_size = None
|
|
105
|
+
data_to_write = {}
|
|
106
|
+
for i, name in enumerate(c_names[1:]):
|
|
107
|
+
value = d[i+1]
|
|
108
|
+
if value is not None:
|
|
109
|
+
try:
|
|
110
|
+
value = float(value)
|
|
111
|
+
except Exception as e:
|
|
112
|
+
# array parameters are stored as a blob :/
|
|
113
|
+
value = np.load(io.BytesIO(value))
|
|
114
|
+
if array_size is not None and array_size != value.size:
|
|
115
|
+
raise ValueError("Array size mismatch in the data") from e
|
|
116
|
+
array_size = value.size
|
|
117
|
+
data_to_write[parameters_by_name[name]] = value
|
|
118
|
+
|
|
119
|
+
if array_size is None:
|
|
120
|
+
dc.add_data(data_to_write)
|
|
121
|
+
else:
|
|
122
|
+
for i in range(array_size):
|
|
123
|
+
data_item_to_write = {}
|
|
124
|
+
for k, v in data_to_write.items():
|
|
125
|
+
if isinstance(v, np.ndarray):
|
|
126
|
+
data_item_to_write[k] = v[i]
|
|
127
|
+
else:
|
|
128
|
+
data_item_to_write[k] = v
|
|
129
|
+
dc.add_data(data_item_to_write)
|
|
130
|
+
|
|
131
|
+
sync_idx = data[-1][0]
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.exception("Real time sync failed with error :: %s", e)
|
|
134
|
+
finally:
|
|
135
|
+
dc.complete()
|
|
136
|
+
|
|
137
|
+
def check_complete(cursor : sqlite3.Cursor, run_id : int):
|
|
138
|
+
cursor.execute("Select is_completed from runs where run_id = ?", (run_id,))
|
|
139
|
+
return cursor.fetchone()[0]
|
|
140
|
+
|
|
141
|
+
def getMaxId(cursor : sqlite3.Cursor, table_name : str):
|
|
142
|
+
cursor.execute(f"SELECT MAX(id) FROM '{table_name}'")
|
|
143
|
+
return cursor.fetchone()[0]
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import List, Dict, Union, Tuple
|
|
2
|
+
|
|
3
|
+
class MetaDataExtractionError(Exception):
|
|
4
|
+
pass
|
|
5
|
+
|
|
6
|
+
def extract_labels_and_attributes_from_snapshot(snapshot : dict) -> Tuple[List[str], Dict[str, Union[Dict, str]]]:
|
|
7
|
+
"""
|
|
8
|
+
Extract labels and attributes from a snapshot dictionary from the QH_MetadataManager.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
snapshot (dict): The snapshot dictionary to extract metadata from.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
List[str]: A list of labels extracted from the snapshot.
|
|
15
|
+
Dict[str, Union[float, int, str]]: A dictionary of attributes extracted from the snapshot.
|
|
16
|
+
"""
|
|
17
|
+
try:
|
|
18
|
+
station = snapshot.get("station", {})
|
|
19
|
+
instruments = station.get("instruments", {})
|
|
20
|
+
metadata = instruments.get("qh_meta", {})
|
|
21
|
+
params = metadata.get("parameters", {})
|
|
22
|
+
tags = params.get("tags", {}).get("value", [])
|
|
23
|
+
attributes = params.get("attributes", {}).get("value", {})
|
|
24
|
+
|
|
25
|
+
# fix attr (currently no good support for parameter on the server side)
|
|
26
|
+
attr = {}
|
|
27
|
+
for key, value in attributes.items():
|
|
28
|
+
if isinstance(value, dict):
|
|
29
|
+
if "value" in value:
|
|
30
|
+
attr[key] = str(value.get("value"))
|
|
31
|
+
else:
|
|
32
|
+
attr[key] = value
|
|
33
|
+
|
|
34
|
+
return tags, attr
|
|
35
|
+
except Exception as e:
|
|
36
|
+
raise MetaDataExtractionError("Could not extract labels and attributes from snapshot.") from e
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: etiket_sync_agent_qcodes
|
|
3
|
+
Version: 0.3.0b1
|
|
4
|
+
Summary: QCoDeS backend for eTiKeT sync agent
|
|
5
|
+
Author: QHarbor team
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://qharbor.nl
|
|
8
|
+
Project-URL: Documentation, https://docs.qharbor.nl
|
|
9
|
+
Keywords: etiket,sync,backend,qcodes,quantum
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
15
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENCE
|
|
27
|
+
Requires-Dist: etiket_sync_agent>=0.3.0b1
|
|
28
|
+
Requires-Dist: qcodes
|
|
29
|
+
Requires-Dist: xarray
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# eTiKeT Sync Agent - QCoDeS Backend
|
|
33
|
+
|
|
34
|
+
Backend for synchronizing QCoDeS datasets with the eTiKeT platform. This backend reads measurements from QCoDeS SQLite databases and syncs them to the cloud.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Install the QCoDeS backend using the eTiKeT Sync SDK:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from etiket_sdk.sync import Backends
|
|
42
|
+
|
|
43
|
+
# Install the latest version
|
|
44
|
+
Backends.install_from_pypi("etiket-sync-agent-qcodes")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> ⚠️ **Important:** This backend installs a specific version of QCoDeS. If the installed version is newer than what your main environment uses, it may upgrade your database schema, making it unreadable by older QCoDeS versions. See [Version Pinning](#version-pinning) below.
|
|
48
|
+
|
|
49
|
+
The package is automatically discovered by `etiket_sync_agent` through the entry-point system. Once installed, you can verify with:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
# List installed backends
|
|
53
|
+
print(Backends.list())
|
|
54
|
+
|
|
55
|
+
# Get details for the QCoDeS backend
|
|
56
|
+
backend = Backends.get("etiket_sync_agent_qcodes")
|
|
57
|
+
print(backend)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Version Pinning
|
|
61
|
+
|
|
62
|
+
By default, installing the backend also installs the latest version of `qcodes`. If you need a specific QCoDeS version to match your main working environment, you can install it separately:
|
|
63
|
+
|
|
64
|
+
**Why this matters:** Newer QCoDeS versions may upgrade your SQLite database schema. Once upgraded, older QCoDeS versions may not be able to read the database. This is primarily a concern for very old QCoDeS versions.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# Check your current QCoDeS version in your working environment
|
|
68
|
+
import qcodes
|
|
69
|
+
print(qcodes.__version__)
|
|
70
|
+
|
|
71
|
+
# Then install a specific QCoDeS version in the sync agent
|
|
72
|
+
Backends.install_from_pypi("qcodes", version="x.x.x")
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Updating the Backend
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
# Update to the latest version
|
|
79
|
+
Backends.update_from_pypi("etiket-sync-agent-qcodes")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## What Gets Synchronized
|
|
83
|
+
|
|
84
|
+
When a QCoDeS measurement is synced, the following data is extracted and uploaded:
|
|
85
|
+
|
|
86
|
+
| QCoDeS Data | eTiKeT Field | Description |
|
|
87
|
+
|-------------|--------------|-------------|
|
|
88
|
+
| `guid` | `alt_uid` | Unique identifier for the measurement |
|
|
89
|
+
| Measurement name | `name` | Name of the dataset |
|
|
90
|
+
| `run_timestamp` | `collected` | When the measurement was taken |
|
|
91
|
+
| Parameter labels | `tags` | Extracted from instrument/parameter metadata |
|
|
92
|
+
| Snapshot instruments | `attributes` | Instrument settings stored in the snapshot |
|
|
93
|
+
| xarray dataset | HDF5 file | The actual measurement data |
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
### Metadata Sources
|
|
97
|
+
|
|
98
|
+
The backend extracts metadata from multiple sources:
|
|
99
|
+
|
|
100
|
+
1. **QCoDeS Snapshot**: Instrument labels and parameter values used in the measurement are added as tags.
|
|
101
|
+
2. **QHMetaData Instrument**: Custom tags and attributes (see below)
|
|
102
|
+
3. **`inspectr_tag` metadata**: Ranking (`star` = +1, `cross` = -1)
|
|
103
|
+
4. **Config `static_attributes`**: Static attributes defined in the sync source configuration
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
The QCoDeS backend requires a `QCoDeSConfigData` configuration with the following fields:
|
|
108
|
+
|
|
109
|
+
| Field | Type | Required | Description |
|
|
110
|
+
|-------|------|----------|-------------|
|
|
111
|
+
| `database_path` | `Path` or `str` | Yes | Path to the QCoDeS SQLite database file (`.db`) |
|
|
112
|
+
| `set_up` | `str` | Yes | Name of the experimental setup (added as `set-up` attribute) |
|
|
113
|
+
| `static_attributes` | `dict[str, str]` | No | Optional static key-value pairs added to every dataset |
|
|
114
|
+
|
|
115
|
+
### Example Configuration
|
|
116
|
+
|
|
117
|
+
Example using the `etiket-sdk` package:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from etiket_sdk.sync import SyncSources
|
|
121
|
+
|
|
122
|
+
SyncSources.create(
|
|
123
|
+
name="my_qcodes_source",
|
|
124
|
+
backend_identifier="etiket_sync_agent_qcodes",
|
|
125
|
+
config_data={
|
|
126
|
+
"database_path": "/path/to/experiments.db",
|
|
127
|
+
"set_up": "dilution_fridge_1",
|
|
128
|
+
"static_attributes": { # optional
|
|
129
|
+
"project": "qubit_calibration",
|
|
130
|
+
"lab": "Building A - Room 101"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
default_scope="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Adding QCoDeS Metadata to a Dataset
|
|
140
|
+
|
|
141
|
+
You can attach tags and attributes to a dataset by adding the `QHMetaData` instrument to your QCoDeS Station. Because the instrument is recorded in the QCoDeS snapshot, the sync agent extracts the tags and attributes and adds them to the QHarbor dataset.
|
|
142
|
+
|
|
143
|
+
You can add both **static** and **dynamic** metadata:
|
|
144
|
+
|
|
145
|
+
- **Static metadata**: Stays the same for all measurements (e.g., project, lab PC)
|
|
146
|
+
- **Dynamic metadata**: Changes per run (e.g., calibration type, measurement type)
|
|
147
|
+
|
|
148
|
+
### Initializing the Instrument
|
|
149
|
+
|
|
150
|
+
There are two ways to define the instrument: directly in Python or via a Station YAML file.
|
|
151
|
+
|
|
152
|
+
#### Using Python
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from etiket_sync_agent_qcodes.qcodes_metadata import QHMetaData
|
|
156
|
+
from qcodes import Station
|
|
157
|
+
|
|
158
|
+
station = Station()
|
|
159
|
+
|
|
160
|
+
# Create and register the instrument
|
|
161
|
+
qh_meta = QHMetaData(
|
|
162
|
+
"qh_meta",
|
|
163
|
+
static_tags=["cool_experiment"],
|
|
164
|
+
static_attributes={"project": "resonator project"},
|
|
165
|
+
)
|
|
166
|
+
station.add_component(qh_meta)
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
> **Note**: The instrument name **must** be `qh_meta`. Do not change this.
|
|
170
|
+
|
|
171
|
+
#### Using a YAML Configuration File
|
|
172
|
+
|
|
173
|
+
Add the following to your Station YAML config:
|
|
174
|
+
|
|
175
|
+
```yaml
|
|
176
|
+
instruments:
|
|
177
|
+
qh_meta:
|
|
178
|
+
type: etiket_sync_agent_qcodes.qcodes_metadata.QHMetaData
|
|
179
|
+
init:
|
|
180
|
+
static_tags:
|
|
181
|
+
- "cool_experiment"
|
|
182
|
+
static_attributes:
|
|
183
|
+
project: "resonator project"
|
|
184
|
+
measurement_computer: "LAB-A-PC-5"
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> **Note**: The key **must** be `qh_meta`. Do not change this.
|
|
188
|
+
|
|
189
|
+
Load the instrument from the config:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
from qcodes import Station
|
|
193
|
+
|
|
194
|
+
# Replace with your config file name
|
|
195
|
+
station = Station(config_file='qc_config.yaml')
|
|
196
|
+
station.load_instrument("qh_meta")
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
For more information, see the QCoDeS docs: [Configuring the Station by using a YAML configuration file](https://microsoft.github.io/Qcodes/examples/basic_examples/Station.html#Configuring-the-Station-by-using-a-YAML-configuration-file).
|
|
200
|
+
|
|
201
|
+
### Runtime Usage
|
|
202
|
+
|
|
203
|
+
Access the instrument and set per-run metadata:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# If defined via Station YAML
|
|
207
|
+
qh_meta = station.components["qh_meta"]
|
|
208
|
+
|
|
209
|
+
# Start of run: clear dynamic values, then set new ones
|
|
210
|
+
qh_meta.reset()
|
|
211
|
+
qh_meta.add_tags(["testing", "calibration"])
|
|
212
|
+
qh_meta.add_attributes({
|
|
213
|
+
"calibration": "ALLXY",
|
|
214
|
+
"qubit": "Q1",
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
# ... measurement happens here ...
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Behavior Notes
|
|
221
|
+
|
|
222
|
+
| Aspect | Behavior |
|
|
223
|
+
|--------|----------|
|
|
224
|
+
| **Static vs Dynamic** | Static tags/attributes are set at construction and persist across `reset()`. Dynamic ones are cleared by `reset()`. |
|
|
225
|
+
| **Attribute Overrides** | Dynamic attributes with the same key as a static attribute will override the static value. |
|
|
226
|
+
| **Tag Duplicates** | Tags are deduplicated using a `set()`. |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Features
|
|
231
|
+
|
|
232
|
+
- Direct integration with QCoDeS datasets
|
|
233
|
+
- Automatic metadata extraction from snapshots
|
|
234
|
+
- Live sync support for running measurements
|
|
235
|
+
- xarray dataset conversion
|
|
236
|
+
- Custom metadata via `QHMetaData` instrument
|
|
237
|
+
|
|
238
|
+
## Requirements
|
|
239
|
+
|
|
240
|
+
- Python >= 3.10
|
|
241
|
+
- QCoDeS >= 0.52
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
Copyright © 2025 QHarbor. All Rights Reserved. See [LICENCE](LICENCE) for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
etiket_sync_agent_qcodes/__init__.py,sha256=sGDfPK9czy7e0IrJup6IIW9wAq3byIuJfqolWOSapPE,154
|
|
2
|
+
etiket_sync_agent_qcodes/qcodes_config_class.py,sha256=nNODFGd_qOKMY2ZlKwKuMV8-n0rz-lvQK6yKtgKFiH8,4171
|
|
3
|
+
etiket_sync_agent_qcodes/qcodes_metadata.py,sha256=mLHAUyHlvcDf3hT8wIvJMll7ZH6FLzcvSOj0hFJMQVU,4226
|
|
4
|
+
etiket_sync_agent_qcodes/qcodes_sync_class.py,sha256=wesnxBnLZAF4a0qNW7axNxq4Hp09R3Vwc9nSA6wPtDU,7933
|
|
5
|
+
etiket_sync_agent_qcodes/real_time_sync.py,sha256=OnMnzLfhdabHszeVEaFEeiVLVHu2KtCw2eJwhC0j_ic,5669
|
|
6
|
+
etiket_sync_agent_qcodes/utility/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
etiket_sync_agent_qcodes/utility/extract_metadata_from_QCoDeS.py,sha256=ue8B-Dc_WJUvugYSze4C-sDzmw8lZo_wtQ45RRvl1AU,1409
|
|
8
|
+
etiket_sync_agent_qcodes-0.3.0b1.dist-info/licenses/LICENCE,sha256=tdZwE43Th9efUgN8-4UpMUyh0kYQ4Dk678agSuhpnc0,2357
|
|
9
|
+
etiket_sync_agent_qcodes-0.3.0b1.dist-info/METADATA,sha256=ySUKAIkwj_BzBqlKMCE62wJtAE8tAGIvsqWQSzMiP3c,8114
|
|
10
|
+
etiket_sync_agent_qcodes-0.3.0b1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
etiket_sync_agent_qcodes-0.3.0b1.dist-info/entry_points.txt,sha256=u_Pnr-oNRlteD5NRFmWhv8c-w1gp9shK506jDkK9orQ,85
|
|
12
|
+
etiket_sync_agent_qcodes-0.3.0b1.dist-info/top_level.txt,sha256=6oQt56X8ztFEYdi6-us5iNDTKWpcl2e9uKmDGUzY3pU,25
|
|
13
|
+
etiket_sync_agent_qcodes-0.3.0b1.dist-info/RECORD,,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
All Rights Reserved License
|
|
2
|
+
Copyright ©️ 2024-2026 QHarbor B.V. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
Agreement to Terms
|
|
5
|
+
By accessing, downloading, installing, or viewing this Software (including its source code, binaries, or application files), you acknowledge and agree to the terms outlined below.
|
|
6
|
+
|
|
7
|
+
Terms and Conditions
|
|
8
|
+
This software and its source code (the "Software") are the exclusive property of QHarbor B.V. and are protected by copyright and other intellectual property laws.
|
|
9
|
+
|
|
10
|
+
License and Testing Exemptions
|
|
11
|
+
Commercial License: If you have entered into a separate commercial license agreement with QHarbor B.V., the terms of that agreement shall supersede the restrictions listed below.
|
|
12
|
+
Testing Permission: If you have obtained written permission for testing/evaluation from QHarbor B.V., you are permitted to install and use the Software for evaluation purposes. However, this permission strictly excludes the right to modify, alter, create derivative works, or reverse engineer the Software.
|
|
13
|
+
|
|
14
|
+
Prohibited Actions
|
|
15
|
+
Unless explicitly authorized by the exemptions above, you are NOT permitted to:
|
|
16
|
+
• Copy, reproduce, or duplicate the Software in any form (except as reasonably necessary for viewing or authorized installation)
|
|
17
|
+
• Modify, alter, or create derivative works based on the Software
|
|
18
|
+
• Distribute, publish, or share the Software with others
|
|
19
|
+
• Reverse engineer, decompile, or disassemble the Software
|
|
20
|
+
• Use the Software for any commercial or non-commercial purposes
|
|
21
|
+
• Transfer, sell, lease, or sublicense the Software
|
|
22
|
+
• Remove or alter any copyright notices or proprietary markings
|
|
23
|
+
|
|
24
|
+
Viewing Only
|
|
25
|
+
For those without a commercial license or written testing permission, the Software is made available for viewing and reference purposes only. Any access to view the source code or application does not grant any rights to use, copy, or modify the Software.
|
|
26
|
+
|
|
27
|
+
No Implied Rights
|
|
28
|
+
No rights are granted by implication, estoppel, or otherwise. All rights not expressly granted are reserved by the copyright holder.
|
|
29
|
+
|
|
30
|
+
Governing Law
|
|
31
|
+
This license and any disputes arising from it shall be governed by the laws of the Netherlands.
|
|
32
|
+
|
|
33
|
+
Disclaimer
|
|
34
|
+
This Software is provided "AS IS" without warranty of any kind. The copyright holder disclaims all warranties and shall not be liable for any damages arising from the use or inability to use this Software.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
etiket_sync_agent_qcodes
|